//
// Lumber Stack Generator (OpenSCAD) — with hand-stacked random variation
//

//////////////////////////////
// User Parameters
//////////////////////////////
/* [BASIC PARAMETERS] */
// -   Scale - HO(1:87), O(1:48), N(1:160)
model_scale = "HO"; // [HO,O,N]

//---BOARD DIMENSIONS
_t1 = true;  // just for title comment organization
//-    thickness of each board (in real inches)
board_thickness_in = 2;
//-    width of each board (in real inches)
board_width_in     = 6;
//-    Length of each board (in real feet)
board_length_ft    = 12;

// ---SIZE OF STACK
_t2 = true;  // just for title comment organization
boards_per_layer = 8;
board_layers     = 10;

// ---STICKER SIZE
_t3 = true;  // just for title comment organization
//-    height of each sticker (in real inches)
sticker_height_in = 1;
//-    width of each sticker (in real inches)
sticker_width_in  = 2;
//-    how far each sticker overhands the stack (in real inches)
sticker_overhang_in = 2;

//-     spacing between stickers (in real inches)
sticker_spacing_in    = 16;
//-     how far from end of stack (in real inches)
sticker_end_margin_in = 6;

// ---HOLLOWING
_t4 = true;  // just for title comment organization
// -    Use this mode for large stacks to reduce print time and material usage
Hollow = false;                 // enable hollow mode


/* [HAND STACKING PARAMETERS] */
// -    Change Seed to get different stack layouts
random_seed = 12345;   // change this to get a different (but stable) layout

// -    How much a board is allowed to overhang past the ends (real inches)
board_end_overhang_in = 2.0;   // try 1–3 inches
// ---- BOARD "HAND STACKED" VARIATION(model mm)
_t5 = true;  // just for title comment organization
board_jitter_x_mm = 0.20;  // sideways wiggle (keep < ~gap+radius for best look)
board_jitter_y_mm = 1.50;  // lengthwise wiggle (small)
board_jitter_z_mm = 0.06;  // tiny height variation
board_rot_deg     = 0.2;   // tiny rotation about Z (degrees)

// ---- STICKER VARIATION (model mm)
_t6 = true;  // just for title comment organization
sticker_jitter_x_mm = 0.25; // across width (X)
sticker_jitter_y_mm = 0.60; // along length (Y)

// -    Sticker spacing variation (real inches, 1:1)
sticker_spacing_jitter_in = 2; // +/- inches applied per sticker position per layer

/* [WOOD GRAIN PARAMETERS] */
// --- Simulated wood grain (printed texture) ---
enable_grain = true;
//-    0.03–0.10 typical for resin; 0.08–0.15 for FDM with fine nozzle
grain_depth_mm = 0.06;     
//-    groove width (use small)
grain_line_width_mm = 0.18;
//-    4–10 looks good
grain_lines_per_board = 6;  
//-    sideways wander of each grain line
grain_wiggle_mm = 0.25;    
//-    keep lines away from edges
grain_margin_mm = 0.30;    

/* [HOLLOWING PARAMETERS] */
//-   stacks less than these will not be hollow
_t7 = true; // just for comment organization
hollow_min_width_boards = 5;   // only hollow if >4 boards wide
hollow_min_layers = 7;         // only hollow if >6 layers tall
hollow_wall_mm = 1.5;          // wall thickness of internal open-top box

//-    How many full layers at bottom/top
_t8 = true; // just for comment organization
hollow_full_bottom_layers = 3;
hollow_full_top_layers = 3;    // printed as a separate "cap"

//-    how far sticker ends overlap into the support box wall (0.2–0.6 typical)
sticker_box_overlap_mm = 0.25;  

hollow_active =
    Hollow &&
    (boards_per_layer >= hollow_min_width_boards) &&
    (board_layers >= hollow_min_layers);


/* [ADVANCED PARAMETERS] */    
fuse_eps_mm = 0.04;  // tiny overlap to fuse touching solids (0.02–0.06 works well)
add_fusion_slab = true;
fusion_slab_thickness_mm = 0.12; // 0.08–0.20 works; keep it under one typical layer if you want it subtle
fusion_slab_inset_mm = 0.30;     // pulls it in so it won’t peek out past edges due to jitter
board_gap_mm   = 0.12;
edge_radius_mm = 0.15;
//-     smoothness of curves
$fn = 18;

include_top_stickers = false;
//-    CAREFUL - changing this to false will make render times WAY longer
stickers_are_cubes = true;  //cubed stickers are much faster to render


//////////////////////////////
// Helpers
//////////////////////////////

function denom_for_scale(s) =
    (s == "HO") ? 87 :
    (s == "O")  ? 48 :
    (s == "N")  ? 160 :
    87;

/* [Hidden] */
inch_to_mm = 25.4;

function in_to_model_mm(x_in) = (x_in / denom_for_scale(model_scale)) * inch_to_mm;
function ft_to_model_mm(x_ft) = in_to_model_mm(x_ft * 12);

// Deterministic random in [min,max] using rands() and a seed
function rand_range(minv, maxv, seedv) = rands(minv, maxv, 1, seedv)[0];

function sticker_spacing_mm() = in_to_model_mm(sticker_spacing_in);
function sticker_margin_mm()  = in_to_model_mm(sticker_end_margin_in);

// Number of stickers for a given board length (same logic as your sticker_layer)
function sticker_count(L) =
    let(usable = L - 2*sticker_margin_mm(),
        s = sticker_spacing_mm())
    (usable > s) ? (floor(usable/s) + 1) : 1;

// First sticker Y (front edge of sticker board)
function first_sticker_y(L) =
    let(n = sticker_count(L))
    (n == 1) ? (L/2 - stkr_w/2) : (sticker_margin_mm() - stkr_w/2);

// Last sticker Y (front edge)
function last_sticker_y(L) =
    let(n = sticker_count(L),
        s = sticker_spacing_mm())
    (n == 1) ? (L/2 - stkr_w/2) : (sticker_margin_mm() + (n-1)*s - stkr_w/2);


module rounded_box(size_xyz, r, center=false) {
    sx = size_xyz[0]; sy = size_xyz[1]; sz = size_xyz[2];
    rr = min(r, sx/2 - 0.001, sy/2 - 0.001, sz/2 - 0.001);

    if (rr <= 0) {
        cube(size_xyz, center=center);
    } else {
        minkowski() {
            cube([sx - 2*rr, sy - 2*rr, sz - 2*rr], center=center);
            sphere(r=rr);
        }
    }
}


//////////////////////////////
// Derived dimensions (scaled)
//////////////////////////////

board_t = in_to_model_mm(board_thickness_in);
board_w = in_to_model_mm(board_width_in);
board_L = ft_to_model_mm(board_length_ft);

stkr_h = in_to_model_mm(sticker_height_in);
stkr_w = in_to_model_mm(sticker_width_in);

stack_width = boards_per_layer * board_w + (boards_per_layer - 1) * board_gap_mm;
stkr_L = stack_width + 2 * in_to_model_mm(sticker_overhang_in);

layer_pitch = board_t + stkr_h;

cap_clearance_mm = 5;
cap_place_mode = "right";
cap_vertical_offset_mm = 0.01;

cap_dx = (cap_place_mode == "right") ? (stack_width + cap_clearance_mm) : 0;
cap_dy = (cap_place_mode == "front") ? (board_L    + cap_clearance_mm) : 0;

// Inner edges of the "full" outer-board shell (after board #2 and before board #(n-1))
x_void_start = 2*board_w + 1*board_gap_mm; // right edge of board 2
x_void_end   = (boards_per_layer - 2) * (board_w + board_gap_mm); // left edge of board (n-1)


//////////////////////////////
// Lumber Stack
//////////////////////////////
module rounded_box_fast(size_xyz, r) {
    sx=size_xyz[0]; sy=size_xyz[1]; sz=size_xyz[2];
    rr = min(r, sx/2-0.001, sy/2-0.001, sz/2-0.001);

    if (rr <= 0) {
        cube(size_xyz, center=false);
    } else {
        hull() {
            for (x=[rr, sx-rr])
            for (y=[rr, sy-rr])
            for (z=[rr, sz-rr])
                translate([x,y,z]) sphere(r=rr);
        }
    }
}

module rounded_prism_xy(size_xyz, r) {
    sx = size_xyz[0]; sy = size_xyz[1]; sz = size_xyz[2];
    rr = min(r, sx/2 - 0.001, sy/2 - 0.001);

    if (rr <= 0) {
        cube([sx, sy, sz], center=false);
    } else {
        // Rounded rectangle in XY, extruded in Z
        linear_extrude(height=sz)
            offset(r=rr)
                square([sx - 2*rr, sy - 2*rr], center=false);
    }
}

module fusion_slab(width_mm, length_mm) {
    // Slight inset so jittered boards don't cause the slab to protrude
    translate([fusion_slab_inset_mm, fusion_slab_inset_mm, 0])
        cube([max(0, width_mm  - 2*fusion_slab_inset_mm),
              max(0, length_mm - 2*fusion_slab_inset_mm),
              fusion_slab_thickness_mm], center=false);
}

module grain_grooves_on_top(w, L, t, layer_idx, board_idx) {
    // Engrave shallow, lengthwise grooves into the top surface (z = t)
    // Grooves are long cylinders subtracted from a thin "top skin" region.
    // We cut slightly below the top face by grain_depth_mm.

    // Safety clamps
    gd = min(grain_depth_mm, t*0.45);
    gw = max(0.05, grain_line_width_mm);

    for (g = [0 : grain_lines_per_board - 1]) {
        // Seed per groove
        s0 = random_seed + 500000 + layer_idx*10000 + board_idx*100 + g*10 + 1;
        s1 = random_seed + 500000 + layer_idx*10000 + board_idx*100 + g*10 + 2;
        s2 = random_seed + 500000 + layer_idx*10000 + board_idx*100 + g*10 + 3;

        // Pick a base X position across the board width
        x_base = rand_range(grain_margin_mm, w - grain_margin_mm, s0);

        // Add a slow “wander” by tilting slightly and offsetting
        tilt = rand_range(-1.2, 1.2, s1); // degrees (small)
        x_wig = rand_range(-grain_wiggle_mm, grain_wiggle_mm, s2);

        // Place a long cylinder just under the top face, aligned with length (Y)
        // Cylinder axis along Y: rotate 90° about X
        translate([x_base + x_wig, L/2, t - gd/2])
            rotate([90, 0, tilt])
                cylinder(h=L + 2, r=gw/2, center=true);
    }
}

module board_with_optional_grain(w, L, t, layer_idx, board_idx) {
    // Only add grain on the very top visible board layer
    do_grain = enable_grain && (layer_idx == board_layers - 1);

    if (!do_grain) {
//      rounded_box_fast([w, L, t], r=edge_radius_mm);
        rounded_prism_xy([w, L, t], r=edge_radius_mm);
    } else {
        difference() {
//          rounded_box_fast([w, L, t], r=edge_radius_mm);
            rounded_prism_xy([w, L, t], r=edge_radius_mm);
            grain_grooves_on_top(w, L, t, layer_idx, board_idx);
        }
    }
}

module board_ends_only(w, L, t, y_keep_front_end, y_keep_back_start, layer_idx, board_idx) {
    // two “end blocks”
    union() {
        // front end
        if (y_keep_front_end > 0.01)
            translate([0, 0, 0])
                board_with_optional_grain(w, y_keep_front_end, t, layer_idx, board_idx);

        // back end
        back_len = L - y_keep_back_start;
        if (back_len > 0.01)
            translate([0, y_keep_back_start, 0])
                board_with_optional_grain(w, back_len, t, layer_idx, board_idx + 10000);
    }
}

module hollow_support_box(z0, z1) {

    eps = 0.20;  // overshoot amount (mm). 0.1–0.3 is typical.

    x0 = x_void_start - 1.0;
    x1 = x_void_end + 1.0; 

    y0 = first_sticker_y(board_L) + stkr_w - 0.5;
    y1 = last_sticker_y(board_L) + 0.5;

    height = z1 - z0;

    if ((x1 - x0) > 2*hollow_wall_mm &&
        (y1 - y0) > 2*hollow_wall_mm &&
        height > hollow_wall_mm) {

        translate([x0, y0, z0])
            difference() {

                // Outer shell
                cube([x1-x0, y1-y0, height], center=false);

                // Inner cavity: starts ABOVE the bottom to preserve the bottom wall,
                // but extends ABOVE the top to guarantee a truly open top.
                translate([hollow_wall_mm,
                           hollow_wall_mm,
                           hollow_wall_mm])
                    cube([x1-x0 - 2*hollow_wall_mm,
                          y1-y0 - 2*hollow_wall_mm,
                          (height - hollow_wall_mm) + eps],
                         center=false);
            }
    }
}


module board_layer(layer_idx, z0) {
    // Boards laid across X, length along Y
    // Add slight random offsets/rotation per board per layer
    for (i = [0 : boards_per_layer - 1]) {
        base_x = i * (board_w + board_gap_mm);

        // Unique-ish seeds for each board
        sx = random_seed + layer_idx*1000 + i*10 + 1;
        sy = random_seed + layer_idx*1000 + i*10 + 2;
        sz = random_seed + layer_idx*1000 + i*10 + 3;
        sr = random_seed + layer_idx*1000 + i*10 + 4;

        dx = rand_range(-board_jitter_x_mm, board_jitter_x_mm, sx);
        dy = rand_range(-board_jitter_y_mm, board_jitter_y_mm, sy);

        // Allow controlled overhang at board ends
        end_limit = in_to_model_mm(board_end_overhang_in);
        dy_clamped = min(max(dy, -end_limit), end_limit);

        dz = rand_range(0, board_jitter_z_mm, sz);
        rz = rand_range(-board_rot_deg, board_rot_deg, sr);

        translate([base_x + dx, 0 + dy_clamped, z0 + dz])
            rotate([0, 0, rz])
                board_with_optional_grain(board_w, board_L, board_t, layer_idx, i);
    }
}

module sticker_seg(size_xyz) {
    if (stickers_are_cubes) {
        cube(size_xyz, center=false);                 // FAST for interactive preview
    } else {
        rounded_box_fast(size_xyz, r=edge_radius_mm); // FINAL detail for STL
    }
}

module sticker_full(x0, y0, z0, dx_jit=0) {
    translate([x0 + dx_jit, y0, z0 - fuse_eps_mm/2])
        sticker_seg([stkr_L, stkr_w, stkr_h + fuse_eps_mm]);
}

module sticker_ends_only(x0, y0, z0, dx_jit=0) {
    // Sticker local X extents (including overhang)
    x_left  = x0 + dx_jit;
    x_right = x_left + stkr_L;

    // If the void doesn't exist (or would invert), just draw full
    if (x_void_end <= x_void_start + 0.01) {
        sticker_full(x0, y0, z0, dx_jit);
    } else {
        // Move cut points inward so sticker ends overlap the support box walls
        cut_L = x_void_start + sticker_box_overlap_mm;
        cut_R = x_void_end   - sticker_box_overlap_mm;

        // Safety: don't let overlap invert the void
        if (cut_R <= cut_L + 0.2) {
            sticker_full(x0, y0, z0, dx_jit);
        } else {
            // Left segment: from sticker left edge up to cut_L
            left_len = cut_L - x_left;
            if (left_len > 0.01) {
                translate([x_left, y0, z0 - fuse_eps_mm/2])
                    sticker_seg([left_len, stkr_w, stkr_h + fuse_eps_mm]);
            }

            // Right segment: from cut_R to sticker right edge
            right_len = x_right - cut_R;
            if (right_len > 0.01) {
                translate([cut_R, y0, z0 - fuse_eps_mm/2])
                    sticker_seg([right_len, stkr_w, stkr_h + fuse_eps_mm]);
            }
        }
    }
}

module sticker_layer(layer_idx, z0) {
    spacing = in_to_model_mm(sticker_spacing_in);
    margin  = in_to_model_mm(sticker_end_margin_in);

    usable = board_L - 2*margin;
    n = (usable > spacing) ? (floor(usable/spacing) + 1) : 1;

    // Sticker base X (centered across the stack width)
    x0 = (stack_width - stkr_L)/2;

    for (k = [0 : n - 1]) {
        // Base Y position (evenly spaced)
        y_base = (n == 1) ? (board_L/2 - stkr_w/2)
                          : (margin + k*spacing - stkr_w/2);

        // Randomness per sticker per layer
        sx = random_seed + 200000 + layer_idx*1000 + k*10 + 1;
        sy = random_seed + 200000 + layer_idx*1000 + k*10 + 2;
        sj = random_seed + 200000 + layer_idx*1000 + k*10 + 3;

        dx = rand_range(-sticker_jitter_x_mm, sticker_jitter_x_mm, sx);
        dy = rand_range(-sticker_jitter_y_mm, sticker_jitter_y_mm, sy);

        jitter_in = rand_range(-sticker_spacing_jitter_in, sticker_spacing_jitter_in, sj);
        dy2 = in_to_model_mm(jitter_in) * 0.15;

        // Clamp so stickers don't drift off the ends
        y = min(max(y_base + dy + dy2, 0), board_L - stkr_w);

        // Determine whether this sticker should be full or ends-only
        is_end_sticker = (k == 0) || (k == n - 1) || (n <= 2);

        if (!hollow_active || is_end_sticker) {
            sticker_full(x0, y, z0, dx);
        } else {
            sticker_ends_only(x0, y, z0, dx);
        }
    }
}

module stack_part_base() {
    // Bottom full layers + hollow middle layers
    bottomN = hollow_full_bottom_layers;
    topN = hollow_full_top_layers;

    middle_layers = board_layers - bottomN - topN;

    // Render bottom full layers
    for (layer = [0 : bottomN - 1]) {
        z_boards = layer * layer_pitch;
        board_layer(layer, z_boards);

        if (layer < board_layers - 1) {
            z_stickers = z_boards + board_t;
            sticker_layer(layer, z_stickers);
        }
    }

    // Render hollow middle layers (if active and there are middle layers)
    if (hollow_active && middle_layers > 0) {
        y_front_end  = first_sticker_y(board_L) + stkr_w;
        y_back_start = last_sticker_y(board_L);

        for (m = [0 : middle_layers - 1]) {
            layer = bottomN + m;
            z_boards = layer * layer_pitch;

            // boards across X
            for (i = [0 : boards_per_layer - 1]) {
                base_x = i * (board_w + board_gap_mm);

                // reuse your jitter seeds (same as before)
                sx = random_seed + layer*1000 + i*10 + 1;
                sy = random_seed + layer*1000 + i*10 + 2;
                sz = random_seed + layer*1000 + i*10 + 3;
                sr = random_seed + layer*1000 + i*10 + 4;

                dx = rand_range(-board_jitter_x_mm, board_jitter_x_mm, sx);
                dy = rand_range(-board_jitter_y_mm, board_jitter_y_mm, sy);
                dz = rand_range(0, board_jitter_z_mm, sz);
                rz = rand_range(-board_rot_deg, board_rot_deg, sr);

                // Outer two boards per side are full; inner boards are end-stubs
                is_full = (i < 2) || (i >= boards_per_layer - 2);

                translate([base_x + dx, dy, z_boards + dz])
                    rotate([0, 0, rz])
                        if (is_full)
                            board_with_optional_grain(board_w, board_L, board_t, layer, i);
                        else
                            board_ends_only(board_w, board_L, board_t, y_front_end, y_back_start, layer, i);
            }

            // stickers still exist in the hollowed region
            z_stickers = z_boards + board_t;
            sticker_layer(layer, z_stickers);
        }

        // Add internal support box spanning the hollow region in Z
        z0 = bottomN * layer_pitch;                 // start of hollow region
        z1 = (bottomN + middle_layers) * layer_pitch; // end of hollow region
        hollow_support_box(z0, z1);
    }
}

module stack_part_cap() {
    // Top cap: last N layers printed separately on the bed (z starts at 0)
    topN = hollow_full_top_layers;
    bottomN = hollow_full_bottom_layers;

    for (k = [0 : topN - 1]) {
        layer = board_layers - topN + k;

        // cap's z is relative (printed on bed)
        z_boards = k * layer_pitch;

        board_layer(layer, z_boards);

        // stickers between cap layers (except above last cap layer unless include_top_stickers)
        if (k < topN - 1 || include_top_stickers) {
            z_stickers = z_boards + board_t;
            sticker_layer(layer, z_stickers);
        }
    }
}

module lumber_stack() {

    // Print cap aside on the same plate when hollow_active
    if (hollow_active) {
        union() {
            if (add_fusion_slab) fusion_slab(stack_width, board_L);
            stack_part_base();
        }
        translate([cap_dx, cap_dy, cap_vertical_offset_mm])
            union() {
                if (add_fusion_slab) fusion_slab(stack_width, board_L);
                stack_part_cap();
            }
    } else {
        union() {
            for (layer = [0 : board_layers - 1]) {
                z_boards = layer * layer_pitch;
                board_layer(layer, z_boards);

                if (layer < board_layers - 1 || include_top_stickers) {
                    z_stickers = z_boards + board_t;
                    sticker_layer(layer, z_stickers);
                }
            }
        }
    }
}

// Build
lumber_stack();

