diff --git a/cad/drawers/tools/__pycache__/freecad_box_generator.cpython-311.pyc b/cad/drawers/tools/__pycache__/freecad_box_generator.cpython-311.pyc new file mode 100644 index 0000000..87a2650 Binary files /dev/null and b/cad/drawers/tools/__pycache__/freecad_box_generator.cpython-311.pyc differ diff --git a/cad/drawers/tools/box_folding_tool.FCStd b/cad/drawers/tools/box_folding_tool.FCStd deleted file mode 100644 index 58fce83..0000000 Binary files a/cad/drawers/tools/box_folding_tool.FCStd and /dev/null differ diff --git a/cad/drawers/tools/cl.md b/cad/drawers/tools/cl.md deleted file mode 100644 index bb7f7e8..0000000 --- a/cad/drawers/tools/cl.md +++ /dev/null @@ -1,25 +0,0 @@ -# Saw Operator Cut Plan -## Board Dimensions: `500 x 500 mm` - -## Specifications -* **V-Groove Spec:** Depth `5 mm` -* **Slot Spec:** Width `8 mm`, Depth `5 mm` - -## Blade Setup -* **For V-Grooves:** Set saw blade angle to **45 degrees**. -* **For Slots:** Set saw blade angle to **0 degrees** (straight up). - -## SETUP 1: VERTICAL CUTS -### 1. Reference from LEFT edge: -* Set fence to: `80 mm` -- for **V-GROOVE** -### 2. Reference from RIGHT edge (FLIP board 180 deg): -* Set fence to: `80 mm` -- for **V-GROOVE** - -## SETUP 2: HORIZONTAL CUTS -### 1. Rotate board 90 degrees CCW, reference from NEW LEFT edge (original BOTTOM edge): -* Set fence to: `80 mm` -- for **V-GROOVE** -### 2. Reference from NEW RIGHT edge (original TOP edge, FLIP board 180 deg): -* Set fence to: `80 mm` -- for **V-GROOVE** - ---- -*End of Plan* diff --git a/cad/drawers/tools/cp.md b/cad/drawers/tools/cp.md deleted file mode 100644 index 2db285e..0000000 --- a/cad/drawers/tools/cp.md +++ /dev/null @@ -1,27 +0,0 @@ -# Saw Operator Cut Plan -## Board Dimensions: `500 x 500 mm` - -## Specifications -* **V-Groove Spec:** Depth `5 mm` -* **Slot Spec:** Width `8 mm`, Depth `5 mm` - -## Blade Setup -* **For V-Grooves:** Set saw blade angle to **45 degrees**. -* **For Slots:** Set saw blade angle to **0 degrees** (straight up). - -## SETUP 1: VERTICAL CUTS -All distances are from the LEFT edge. Flip board for cuts > 50%. - -* `80 mm` -- **V-GROOVE** -* `254 mm` -- **SLOT** -* `420 mm` -- **V-GROOVE** - -## SETUP 2: HORIZONTAL CUTS -Rotate board 90 deg. All distances from the NEW LEFT edge. - -* `80 mm` -- **V-GROOVE** -* `254 mm` -- **SLOT** -* `420 mm` -- **V-GROOVE** - ---- -*End of Plan* diff --git a/cad/drawers/tools/cutlist.md b/cad/drawers/tools/cutlist.md deleted file mode 100644 index 7875a37..0000000 --- a/cad/drawers/tools/cutlist.md +++ /dev/null @@ -1,21 +0,0 @@ -# Saw Operator Cut Plan -## Board Dimensions: `500 x 500 mm` - -## Specifications -* **V-Groove Spec:** Depth `5 mm` -* **Slot Spec:** Width `8 mm`, Depth `5 mm` - -## Blade Setup -* **For V-Grooves:** Set saw blade angle to **45 degrees**. -* **For Slots:** Set saw blade angle to **0 degrees** (straight up). - -## SETUP 1: VERTICAL CUTS -### 1. Reference from LEFT edge: -### 2. Reference from RIGHT edge (FLIP board 180 deg): - -## SETUP 2: HORIZONTAL CUTS -### 1. Rotate board 90 degrees CCW, reference from NEW LEFT edge (original BOTTOM edge): -### 2. Reference from NEW RIGHT edge (original TOP edge, FLIP board 180 deg): - ---- -*End of Plan* diff --git a/cad/drawers/tools/freecad_box_generator.py b/cad/drawers/tools/freecad_box_generator.py new file mode 100644 index 0000000..b764a9c --- /dev/null +++ b/cad/drawers/tools/freecad_box_generator.py @@ -0,0 +1,187 @@ +# freecad_box_generator.py +# +# A Python script to generate a parametric box natively in FreeCAD. +# +# --- How to use (GUI) --- +# 1. Open FreeCAD and ensure you have a new, empty document open. +# 2. Paste this script into the Python console and press Enter. +# +# --- How to use (Command Line) --- +# FreeCADCmd.exe freecad_box_generator.py [output_file.FCStd] + +import FreeCAD +import Part +import sys + +# --- Customizable Parameters --- +# (Mirrors the parameters from the OpenSCAD file) + +# Overall dimensions +TOTAL_WIDTH = 500 +TOTAL_LENGTH = 500 +BOX_HEIGHT = 80 + +# Material and slot properties +WALL_THICKNESS = 8 + +# Internal grid configuration +NUM_BOXES_U = 2 # Along X-axis (width) +NUM_BOXES_V = 2 # Along Y-axis (length) + + +def create_box_assembly(doc): + """ + Generates the box assembly as native FreeCAD objects in the specified document. + This function does not save the file. + """ + + # --- Create a Group for organization --- + box_group = doc.addObject("App::DocumentObjectGroup", "BoxAssembly") + + # --- Create the Base Plate --- + base = Part.makeBox(TOTAL_WIDTH, TOTAL_LENGTH, WALL_THICKNESS) + base_obj = doc.addObject("Part::Feature", "Base") + base_obj.Shape = base + box_group.addObject(base_obj) + + # --- Calculate Inner Dimensions --- + inner_width = TOTAL_WIDTH - (2 * WALL_THICKNESS) + inner_length = TOTAL_LENGTH - (2 * WALL_THICKNESS) + + # --- Create Outer Walls --- + # South Wall (along the front X-axis) + wall_south = Part.makeBox(TOTAL_WIDTH, WALL_THICKNESS, BOX_HEIGHT) + wall_south.translate(FreeCAD.Vector(0, 0, WALL_THICKNESS)) # Position it on top of the base + wall_south_obj = doc.addObject("Part::Feature", "Wall_South") + wall_south_obj.Shape = wall_south + box_group.addObject(wall_south_obj) + + # North Wall (along the back X-axis) + wall_north = Part.makeBox(TOTAL_WIDTH, WALL_THICKNESS, BOX_HEIGHT) + wall_north.translate(FreeCAD.Vector(0, TOTAL_LENGTH - WALL_THICKNESS, WALL_THICKNESS)) + wall_north_obj = doc.addObject("Part::Feature", "Wall_North") + wall_north_obj.Shape = wall_north + box_group.addObject(wall_north_obj) + + # West Wall (along the left Y-axis, fits between N/S walls) + wall_west = Part.makeBox(WALL_THICKNESS, inner_length, BOX_HEIGHT) + wall_west.translate(FreeCAD.Vector(0, WALL_THICKNESS, WALL_THICKNESS)) + wall_west_obj = doc.addObject("Part::Feature", "Wall_West") + wall_west_obj.Shape = wall_west + box_group.addObject(wall_west_obj) + + # East Wall (along the right Y-axis, fits between N/S walls) + wall_east = Part.makeBox(WALL_THICKNESS, inner_length, BOX_HEIGHT) + wall_east.translate(FreeCAD.Vector(TOTAL_WIDTH - WALL_THICKNESS, WALL_THICKNESS, WALL_THICKNESS)) + wall_east_obj = doc.addObject("Part::Feature", "Wall_East") + wall_east_obj.Shape = wall_east + box_group.addObject(wall_east_obj) + + # --- Create Internal Dividers --- + if NUM_BOXES_U > 1: + # --- Vertical Dividers (along Y-axis) --- + compartment_width = inner_width / NUM_BOXES_U + for i in range(1, NUM_BOXES_U): + # Create the main body of the divider + x_pos = (i * compartment_width) + divider_v_body = Part.makeBox(WALL_THICKNESS, inner_length, BOX_HEIGHT) + + # --- Create Notches for Horizontal Dividers (top-down) --- + if NUM_BOXES_V > 1: + # Create all notch cutting tools first + notch_tools = [] + notch_cutout = Part.makeBox(WALL_THICKNESS, WALL_THICKNESS, BOX_HEIGHT / 2) + for j in range(1, NUM_BOXES_V): + compartment_length = inner_length / NUM_BOXES_V + y_pos = (j * compartment_length) - (WALL_THICKNESS / 2) + notch_tools.append(notch_cutout.translated(FreeCAD.Vector(0, y_pos, BOX_HEIGHT / 2))) + # Fuse them into a single cutting tool + cutting_compound = Part.makeCompound(notch_tools) + # Perform a single, efficient cut + divider_v_body = divider_v_body.cut(cutting_compound) + + # Position the final divider and add to document + divider_v_body.translate(FreeCAD.Vector(x_pos, WALL_THICKNESS, WALL_THICKNESS)) + divider_v_obj = doc.addObject("Part::Feature", f"Divider_V_{i}") + divider_v_obj.Shape = divider_v_body + box_group.addObject(divider_v_obj) + + if NUM_BOXES_V > 1: + # --- Horizontal Dividers (along X-axis) --- + compartment_length = inner_length / NUM_BOXES_V + for i in range(1, NUM_BOXES_V): + # Create the main body of the divider + y_pos = (i * compartment_length) + divider_h_body = Part.makeBox(inner_width, WALL_THICKNESS, BOX_HEIGHT) + + # --- Create Notches for Vertical Dividers (bottom-up) --- + if NUM_BOXES_U > 1: + # Create all notch cutting tools first + notch_tools = [] + notch_cutout = Part.makeBox(WALL_THICKNESS, WALL_THICKNESS, BOX_HEIGHT / 2) + for j in range(1, NUM_BOXES_U): + compartment_width = inner_width / NUM_BOXES_U + x_pos = (j * compartment_width) - (WALL_THICKNESS / 2) + notch_tools.append(notch_cutout.translated(FreeCAD.Vector(x_pos, 0, 0))) + # Fuse them into a single cutting tool + cutting_compound = Part.makeCompound(notch_tools) + # Perform a single, efficient cut + divider_h_body = divider_h_body.cut(cutting_compound) + + # Position the final divider and add to document + divider_h_body.translate(FreeCAD.Vector(WALL_THICKNESS, y_pos, WALL_THICKNESS)) + divider_h_obj = doc.addObject("Part::Feature", f"Divider_H_{i}") + divider_h_obj.Shape = divider_h_body + box_group.addObject(divider_h_obj) + + # --- Finalize --- + doc.recompute() + print("Box assembly generated.") + + +def main_cli(): + """Function to run when script is executed from the command line.""" + output_file = "box.FCStd" # Default filename + + # Look for the output file as a positional argument after the script name + # This is a robust way to handle arguments when FreeCAD's parser is unpredictable. + try: + script_index = [i for i, arg in enumerate(sys.argv) if 'freecad_box_generator.py' in arg][0] + if script_index + 1 < len(sys.argv): + output_file = sys.argv[script_index + 1] + except IndexError: + # This case handles running the script directly without FreeCAD, for debugging. + pass + + doc = FreeCAD.newDocument("Box") + create_box_assembly(doc) + + try: + doc.saveAs(output_file) + print(f"Successfully saved box assembly to: {output_file}") + except Exception as e: + print(f"Error saving file: {e}") + + FreeCAD.closeDocument(doc.Name) + # Ensure the command-line application exits cleanly + sys.exit() + +def main_gui(): + """Function to run when script is executed from the FreeCAD GUI.""" + doc = FreeCAD.activeDocument() + if not doc: + doc = FreeCAD.newDocument("Box") + + create_box_assembly(doc) + + if FreeCAD.GuiUp: + FreeCAD.Gui.activeDocument().activeView().viewAxonometric() + FreeCAD.Gui.SendMsgToActiveView("ViewFit") + +# --- Main Execution Block --- +# This determines if the script is running in the GUI or from the command line +# and calls the appropriate main function. +if FreeCAD.GuiUp: + main_gui() +else: + main_cli() \ No newline at end of file diff --git a/cad/drawers/tools/my_final_box.FCStd b/cad/drawers/tools/my_final_box.FCStd new file mode 100644 index 0000000..46042fa Binary files /dev/null and b/cad/drawers/tools/my_final_box.FCStd differ diff --git a/cad/drawers/tools/run_freecad_generator.sh b/cad/drawers/tools/run_freecad_generator.sh new file mode 100644 index 0000000..33c40ed --- /dev/null +++ b/cad/drawers/tools/run_freecad_generator.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# --- run_freecad_generator.sh --- +# +# A wrapper script to correctly call the FreeCAD box generator from the command line. +# +# Usage: ./run_freecad_generator.sh + +# --- Argument Validation --- +if [ -z "$1" ]; then + echo "Usage: $0 " + echo "Error: Output file not specified." + exit 1 +fi + +OUTPUT_FILE="$1" + +# --- Execute FreeCAD --- +# We pass the python script to the FreeCAD executable, followed by the +# output filename as a simple positional argument. + +echo "Executing FreeCAD generator script..." +FreeCADCmd.exe -c freecad_box_generator.py "$OUTPUT_FILE" + +if [ $? -ne 0 ]; then + echo "" + echo "Error: FreeCAD script execution failed." + exit 1 +fi + +echo "Script executed successfully." \ No newline at end of file diff --git a/cad/drawers/tools/test/box_folding_tool.scad b/cad/drawers/tools/test/box_folding_tool.scad new file mode 100644 index 0000000..ac1f28a --- /dev/null +++ b/cad/drawers/tools/test/box_folding_tool.scad @@ -0,0 +1,348 @@ +// Customizable parameters for the box folding tool +// These values are based on the provided screenshot. + +// Overall dimensions +TotalWidth = 500; // [100:1000] +TotalLength = 500; // [100:1000] +Height = 80; // [20:200] + +// Material and slot properties +// Updated to reflect the V-groove cutting example +TotalThickness = 8; // [1:20] +BaseThickness = 3; // [0.5:19] +Slot_Width_Walls = 8; // [1:20] for internal dividers + +// Internal grid configuration +Nb_Boxes_U = 2; // [1:10] +Nb_Boxes_V = 2; // [1:10] + +// View control +Folded_View = false; // [true, false] +// Use "export" to render all parts separately for STEP conversion +view_mode = "preview"; // ["preview", "export", "dxf", "cutplan"] + +// Note: InnerBox_Width from the screenshot (100) is not used directly. +// Instead, the compartment widths are calculated based on TotalWidth, Height, and Nb_Boxes_U/V. +// This ensures the design remains consistent with the overall dimensions. + +// --------------------------------------------------------------------- +// Main module to generate the box folding pattern +// --------------------------------------------------------------------- +module unfolded_pattern() { + GrooveDepth = TotalThickness - BaseThickness; + + // To prevent export errors, we perform the cuts sequentially. + // This is more stable than a single, complex difference(). + difference() { + // Start with the result of the first cut... + difference() { + // 1. Create the base plate + cube([TotalWidth, TotalLength, TotalThickness], center = true); + + // 2. Cut the V-Grooves for folding the main box walls + translate([0, 0, TotalThickness/2]) { + wall_grooves(); + } + } + + // 3. ...and then cut the internal rectangular slots from that result. + if (Nb_Boxes_U > 1 || Nb_Boxes_V > 1) { + translate([0,0,TotalThickness/2 - GrooveDepth/2]) + internal_grooves(); + } + } +} + +// --------------------------------------------------------------------- +// Helper modules +// --------------------------------------------------------------------- + +// Module for creating the grooves for the outer walls +// Reverted to simple rectangular slots as a final workaround to bypass +// a persistent, unfixable geometry kernel bug with V-grooves. +module wall_grooves() { + InnerWidth = TotalWidth - 2 * Height; + InnerLength = TotalLength - 2 * Height; + GrooveDepth = TotalThickness - BaseThickness; + + // For a folding groove, the width should be slightly less than the material thickness + // to leave some material at the corners. We'll use the depth as a proxy. + FoldingSlotWidth = GrooveDepth; + + // Grooves parallel to Y-axis (vertical) + translate([-InnerWidth/2, 0, 0]) + cube([FoldingSlotWidth, TotalLength, GrooveDepth], center=true); + translate([InnerWidth/2, 0, 0]) + cube([FoldingSlotWidth, TotalLength, GrooveDepth], center=true); + + // Grooves parallel to X-axis (horizontal) + translate([0, -InnerLength/2, 0]) + cube([TotalWidth, FoldingSlotWidth, GrooveDepth], center=true); + translate([0, InnerLength/2, 0]) + cube([TotalWidth, FoldingSlotWidth, GrooveDepth], center=true); +} + +// Module for creating a V-shaped groove for folding (45-degree cuts) +module v_groove(length) { + // This module is no longer used but is kept for historical reference. +} + +// Module for creating the grooves for the internal compartments +module internal_grooves() { + InnerWidth = TotalWidth - 2 * Height; + InnerLength = TotalLength - 2 * Height; + GrooveDepth = TotalThickness - BaseThickness; + + CompartmentWidth = (InnerWidth - (Nb_Boxes_U - 1) * Slot_Width_Walls) / Nb_Boxes_U; + CompartmentLength = (InnerLength - (Nb_Boxes_V - 1) * Slot_Width_Walls) / Nb_Boxes_V; + + // Internal vertical grooves + if (Nb_Boxes_U > 1) { + for (i = [1 : Nb_Boxes_U - 1]) { + x_pos = -InnerWidth/2 + i * CompartmentWidth + (i - 1/2) * Slot_Width_Walls; + translate([x_pos, 0, 0]) + cube([Slot_Width_Walls, InnerLength, GrooveDepth], center = true); + } + } + + // Internal horizontal grooves + if (Nb_Boxes_V > 1) { + for (i = [1 : Nb_Boxes_V - 1]) { + y_pos = -InnerLength/2 + i * CompartmentLength + (i - 1/2) * Slot_Width_Walls; + translate([0, y_pos, 0]) + cube([InnerWidth, Slot_Width_Walls, GrooveDepth], center = true); + } + } +} + +// --------------------------------------------------------------------- +// Modules for Folded View +// --------------------------------------------------------------------- + +// Generates the fully assembled 3D box +module folded_box() { + // Inner dimensions of the box + InnerWidth = TotalWidth - 2 * Height; + InnerLength = TotalLength - 2 * Height; + + // Material thickness for all parts + wall_thickness = TotalThickness; + + // 1. Base Plate + translate([0, 0, wall_thickness / 2]) + cube([InnerWidth, InnerLength, wall_thickness], center = true); + + // 2. Outer Walls + // South Wall (bottom) - Full Width + translate([0, -InnerLength/2, Height/2 + wall_thickness]) + rotate([90, 0, 0]) + cube([InnerWidth, Height, wall_thickness], center=true); + + // North Wall (top) - Full Width + translate([0, InnerLength/2, Height/2 + wall_thickness]) + rotate([-90, 0, 0]) + cube([InnerWidth, Height, wall_thickness], center=true); + + // West Wall (left) - Shortened to fit between North/South walls + translate([-InnerWidth/2, 0, Height/2 + wall_thickness]) + rotate([0, 90, 0]) + cube([Height, InnerLength - 2 * wall_thickness, wall_thickness], center=true); + + // East Wall (right) - Shortened to fit between North/South walls + translate([InnerWidth/2, 0, Height/2 + wall_thickness]) + rotate([0, -90, 0]) + cube([Height, InnerLength - 2 * wall_thickness, wall_thickness], center=true); + + // 3. Internal Dividers + internal_dividers_folded(); +} + +// Generates the interlocking internal dividers for the folded view +module internal_dividers_folded() { + InnerWidth = TotalWidth - 2 * Height; + InnerLength = TotalLength - 2 * Height; + divider_thickness = Slot_Width_Walls; // Use slot width as the divider material thickness + + // Calculate compartment sizes + Compartment_U = (InnerWidth + divider_thickness) / Nb_Boxes_U; + Compartment_V = (InnerLength + divider_thickness) / Nb_Boxes_V; + + // Vertical dividers (U-direction) + for (i = [1 : Nb_Boxes_U - 1]) { + x_pos = -InnerWidth/2 + i * Compartment_U - divider_thickness/2; + difference() { + // Main divider piece + translate([x_pos, 0, Height/2 + TotalThickness]) + cube([divider_thickness, InnerLength, Height], center=true); + // Slots for horizontal dividers (top-down) + for (j = [1 : Nb_Boxes_V - 1]) { + y_pos = -InnerLength/2 + j * Compartment_V - divider_thickness/2; + translate([x_pos, y_pos, Height * 0.75 + TotalThickness]) + cube([divider_thickness + 0.1, divider_thickness, Height/2], center=true); + } + } + } + + // Horizontal dividers (V-direction) + for (j = [1 : Nb_Boxes_V - 1]) { + y_pos = -InnerLength/2 + j * Compartment_V - divider_thickness/2; + difference() { + // Main divider piece + translate([0, y_pos, Height/2 + TotalThickness]) + cube([InnerWidth, divider_thickness, Height], center=true); + // Slots for vertical dividers (bottom-up) + for (i = [1 : Nb_Boxes_U - 1]) { + x_pos = -InnerWidth/2 + i * Compartment_U - divider_thickness/2; + translate([x_pos, y_pos, Height * 0.25 + TotalThickness]) + cube([divider_thickness, divider_thickness + 0.1, Height/2], center=true); + } + } + } +} + +// --------------------------------------------------------------------- +// Module for exporting all parts separately +// --------------------------------------------------------------------- +module export_layout() { + wall_thickness = TotalThickness; + InnerWidth = TotalWidth - 2 * Height; + InnerLength = TotalLength - 2 * Height; + + // 1. Base Plate + translate([0, 0, -TotalThickness/2]) + cube([InnerWidth, InnerLength, wall_thickness], center = true); + + // Spacing for laying out parts + spacing = TotalWidth; + + // 2. Outer Walls + translate([spacing, 0, 0]) + cube([InnerWidth, Height, wall_thickness], center=true); // South + translate([spacing, Height + 10, 0]) + cube([InnerWidth, Height, wall_thickness], center=true); // North + + translate([spacing + InnerWidth + 10, 0, 0]) + cube([InnerLength - 2 * wall_thickness, Height, wall_thickness], center=true); // West + translate([spacing + InnerWidth + 10, Height + 10, 0]) + cube([InnerLength - 2 * wall_thickness, Height, wall_thickness], center=true); // East + + // 3. Internal Dividers + divider_thickness = Slot_Width_Walls; + Compartment_U = (InnerWidth + divider_thickness) / Nb_Boxes_U; + Compartment_V = (InnerLength + divider_thickness) / Nb_Boxes_V; + + // Place all vertical dividers in a row + for (i = [1 : Nb_Boxes_U - 1]) { + translate([2 * spacing + (i-1)*(divider_thickness+10), 0, 0]) + internal_divider_vertical_export(InnerLength, Compartment_V); + } + + // Place all horizontal dividers in a row + for (j = [1 : Nb_Boxes_V - 1]) { + translate([2 * spacing, (j-1)*(Height+10) + Height + 10, 0]) + internal_divider_horizontal_export(InnerWidth, Compartment_U); + } +} + +// Helper modules for export layout (without difference operations) +module internal_divider_vertical_export(length, compartment_v_size) { + divider_thickness = Slot_Width_Walls; + difference() { + cube([divider_thickness, length, Height], center=true); + // Notches for horizontal dividers (bottom-up) + for (i = [1 : Nb_Boxes_V - 1]) { + y_pos = -length/2 + i * compartment_v_size - divider_thickness/2; + translate([0, y_pos, -Height/4]) + cube([divider_thickness + 0.1, divider_thickness, Height/2], center=true); + } + } +} + +module internal_divider_horizontal_export(length, compartment_u_size) { + divider_thickness = Slot_Width_Walls; + difference() { + cube([length, divider_thickness, Height], center=true); + // Notches for vertical dividers (top-down) + for (j = [1 : Nb_Boxes_U - 1]) { + x_pos = -length/2 + j * compartment_u_size - divider_thickness/2; + translate([x_pos, 0, Height/4]) + cube([divider_thickness, divider_thickness + 0.1, Height/2], center=true); + } + } +} + +// --------------------------------------------------------------------- +// Module to generate a markdown cut plan for the operator. +// This version avoids all list manipulation for maximum compatibility. +// --------------------------------------------------------------------- +module generate_cut_plan_text() { + // --- Calculations --- + InnerWidth = TotalWidth - 2 * Height; + InnerLength = TotalLength - 2 * Height; + GrooveDepth = TotalThickness - BaseThickness; + + // --- Markdown Output --- + echo("# Saw Operator Cut Plan"); + echo(str("## Board Dimensions: `", TotalWidth, " x ", TotalLength, " mm`")); + echo(""); + echo("## Specifications"); + echo(str("* **V-Groove Spec:** Depth `", GrooveDepth, " mm`")); + echo(str("* **Slot Spec:** Width `", Slot_Width_Walls, " mm`, Depth `", GrooveDepth, " mm`")); + echo(""); + echo("## Blade Setup"); + echo("* **For V-Grooves:** Set saw blade angle to **45 degrees**."); + echo("* **For Slots:** Set saw blade angle to **0 degrees** (straight up)."); + echo(""); + + // --- Setup 1: Vertical Cuts --- + echo("## SETUP 1: VERTICAL CUTS"); + echo("All distances are from the LEFT edge. Flip board for cuts > 50%."); + echo(""); + echo(str("* `", Height, " mm` -- **V-GROOVE**")); + if (Nb_Boxes_U > 1) { + for (i = [1 : Nb_Boxes_U - 1]) { + pos = Height + i * ((InnerWidth - (Nb_Boxes_U - 1) * Slot_Width_Walls) / Nb_Boxes_U) + i * Slot_Width_Walls; + echo(str("* `", pos, " mm` -- **SLOT**")); + } + } + echo(str("* `", TotalWidth - Height, " mm` -- **V-GROOVE**")); + echo(""); + + // --- Setup 2: Horizontal Cuts --- + echo("## SETUP 2: HORIZONTAL CUTS"); + echo("Rotate board 90 deg. All distances from the NEW LEFT edge."); + echo(""); + echo(str("* `", Height, " mm` -- **V-GROOVE**")); + if (Nb_Boxes_V > 1) { + for (i = [1 : Nb_Boxes_V - 1]) { + pos = Height + i * ((InnerLength - (Nb_Boxes_V - 1) * Slot_Width_Walls) / Nb_Boxes_V) + i * Slot_Width_Walls; + echo(str("* `", pos, " mm` -- **SLOT**")); + } + } + echo(str("* `", TotalLength - Height, " mm` -- **V-GROOVE**")); + + echo(""); + echo("---"); + echo("*End of Plan*"); + + // Generate a tiny invisible cube because OpenSCAD needs to produce some geometry. + cube(0.01); +} + +// --------------------------------------------------------------------- +// Render the final object +// --------------------------------------------------------------------- +if (view_mode == "export") { + export_layout(); +} else if (view_mode == "dxf") { + // Project the 2D unfolded pattern for DXF export + projection(cut = true) unfolded_pattern(); +} else if (view_mode == "cutplan") { + generate_cut_plan_text(); +} else { + if (Folded_View) { + folded_box(); + } else { + unfolded_pattern(); + } +} \ No newline at end of file