Feature/square hex and oval cutouts#9
Open
layersafe wants to merge 14 commits into
Open
Conversation
For some sizes (e.g. 24.7mm with --edge-offsets 0.1), the edge_offset self-intersection in generate_cutout returns a dimension-less Compound. Shape.__add__ compares class-level _dim (None for plain Compound), so the later lip adjustor union crashed with "ValueError: Only shapes with the same dimension can be added". The old inline workaround only unwrapped ShapeList results and only on the lip adjustor side. Normalize every boolean result in generate_cutout through _unwrap_boolean_result, and skip the lip adjustor union when its intersection is empty instead of crashing. The helper now extracts solids directly (rewrapping several as a Part) rather than re-unioning children, and handles nested dimension-less results. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I unleashed my free Fable 5 credits onto the codbase and it threw up quite a few suggestions.
Ive since spent the rest of the day going through and getting it to implement the suggestions.
I started with establishing tests for current code generation to ensure there is no loss in the STLS as it rewrote a large portion of the codebase.
I appreciate that this is your codebase and this is a large sweeping changes so feel free to ignore and or reject the changes, but ive sp far test printed some hex trays and it works very well.
I have Oval Tray lined up next.
Tray Generator — Architecture & Code Review
A. Architecture changes needed before adding Hex/Oval (the main ask)
A1. Introduce a
CutoutShapeabstraction (strategy + registry)Today shape support is a string scattered across three files: the CLI
choices=["circle", "square"](tray_generator.py:146), the dispatchcutout_fn = generate_square_cutout if cutout_shape == 'square' else generate_cutout(full_tray_generator.py:169), and the layout overridecutout_shape == 'square'(full_tray_generator.py:161). Adding hex/oval means touching all three plus each layout file.Instead define one class/protocol per shape:
The CLI derives choices from
SHAPES.keys(), the orchestrator callsshape.build(...), and layout code consumesfootprint()— nobody string-compares shape names anymore.A2. Stop calling everything
diameterLayout position dicts carry
'diameter'(calculate_linear_cutout_positions.py:132, alternating too), and the CLI positional arg isdiameters. For a square it's a side; for a hex it's ambiguous (across-flats vs across-corners — these differ by ~15%, which matters at 0.1mm fit tolerance); an oval needs two numbers.Rename to
size/sizesinternally, and make layout consumefootprint_x/footprint_yper item rather than a single scalar. This is the single most invasive rename, so do it before adding shapes, not after.A3. Decide the CLI input format for two-dimensional shapes (oval) and hex orientation
The current
nargs="+", type=floatpositional list can't express an oval.Recommendation:
WxDtokens (e.g.40x60) parsed by a customtype=function--hex-orientation {flat,point}or fold orientation into the shape name (hex-flat)circle:40 square:25) — the registry design in A1 supports either, but the CLI grammar should be chosen onceA4. Make layout shape-aware instead of hard-excluding shapes
The alternating layout's nesting math is pure circle tangency (
_side_from_hyp,radius_i + radius_nextinTrays/functions/calculate_cutout_positions/calculate_alternating_cutout_positions.py:129). Squares are currently forced to linear (full_tray_generator.py:158-161).With A1's
min_center_distance(other, gap), alternating layout works for any shape that implements it (circle: sum of radii; square/hex: conservative circumscribed-circle radius, or exact support-function math later). Minimum viable: keep the linear-only restriction but drive it fromshape.supports_alternatinginstead of a string check.A5. Unify the two cutout generators' shared structure
generate_cutoutandgenerate_square_cutoutduplicate the pattern "tapered extrusion + slide-path extrusion toward the flap + flattener intersection", but with different magic numbers: circle usesextrude(amount=6, taper=12.5)(cutout_generator.py:25), square usesamount=5, taper=5(cutout_generator.py:134).If the taper difference isn't a deliberate design decision, extract shared constants; either way, factor the common "profile sketch → taper → slide path → flatten" pipeline so a new shape only supplies the 2D profile plus its lip/edge treatment.
Note also
generate_square_cutoutacceptslip_offsetandfloor_thicknessbut ignores both silently — with A1, shapes that don't support a feature should say so (warning or error), because a user passing--edge-offsetswith squares currently gets no fit change and no explanation.A6. Replace the 24-positional-argument call chain with a config object
tray_generator.py:211-240passes ~24 positional args togenerate_full_tray, which forwards ~19 positionally togenerate_base_tray.This has already produced a latent unit bug: the value flows from
hinge_pin_radius(tray_generator) into a parameter namedhinge_pin_diameter(base_tray_generator.py:17), which is then used as a radius inCylinder(hinge_pin_diameter + ...).One
@dataclass TrayConfig(with the defaults living in exactly one place) eliminates the whole class of error and makes adding shape params cheap.Related: defaults currently diverge between layers —
hinge_lock_radiusis 3.5 intray_generatorbut 2 in the two function signatures; square tolerance default 0.6 vs circle 0.55;floor_thicknessREADME says 0.4, code says 0.8.A7. Fix parameter repurposing in the cutout call
full_tray_generator.py:171-182passesfloor_thickness=edge_adjusts[i]andcutout_edge_spacing=safety_margin[1]— the names on the receiving side don't mean what they say, and the actualfloor_thicknessis used two lines later for the Z translate.Also
generate_full_tray's owncutout_edge_spacing=.4parameter is dead (always overridden). Rename the cutout parameters to what they really are (edge_adjust,edge_margin_y, …) as part of A1'sbuild()signature.A8. Fix or remove the base-tray cache
base_tray_storage(full_tray_generator.py:18) is keyed only on((width, depth), is_double_tray)— any other geometry change (rail height, flap depth, hinge params) silently returns a stale tray if the module is reused in-process (batch generation, notebook, future GUI).Either key on all inputs (easy once A6's config is a frozen dataclass) or drop the cache; a CLI run builds the base tray once anyway.
B. Correctness bugs (independent of the refactor)
B1. Alternating validator's boundary check is dead code. In
calculate_alternating_cutout_positions.py:69-80, an out-of-bounds position just breaks the loop without settinghas_error— boundary violations are silently accepted, and cutouts can be placed intersecting the rails/flap edges.B2. Overlap check only tests consecutive pairs (lines 84-94). In alternating layouts, positions
iandi+2sit on the same side and can overlap withouti/i+1doing so. Check same-side neighbors too (or all pairs; n is tiny).B3. Edge-offset sign inconsistency in alternating layout. First position:
y = min + d/2 - edge_offsets[0](line 42); subsequent non-flipped positions:y = min + d/2 + edge_offsets[i](line 54). Same side, opposite sign — one is wrong. (Linear layout uses+on the near line,-on the far line, i.e. "inward"; match that convention.)B4. Single-diameter alternating path works by accident and its demo crashes. Line 19 uses loop leftover
diameter(happens to equaldiameters[0]) andedge_offsets[0](IndexErrors if unpadded). The file's own__main__block calls the function with two args missing →TypeError.B5.
{RESET}printed literally in error messages. Intray_generator.py:271-293, the multi-line implicit string concatenations only havefon the first fragment, so{RESET}is emitted as literal text and the terminal stays red. Also: the handler print-then-raises, soexit(1)is unreachable for those branches (user gets a duplicate traceback), and matching exceptions by substring ("math domain error","keyerror: 'flipped'") is brittle — raise typed exceptions (LayoutError, etc.) at the source instead. Note_side_from_hypalready raises a friendlyValueError, so the "math domain error" branch may be mostly stale.B6. Output directory depends on CWD.
tray_generator.py:250doesos.makedirs("output")— running from the repo root writes to./output(that stray directory already exists in the repo), contradicting the README'sTrays/output/. Anchor to the script:Path(__file__).parent / "output".B7. Bare
except:aroundshow()(tray_generator.py:244-247) swallowsKeyboardInterrupt/SystemExit. Useexcept Exception:— or better, only callshow()when a viewer is actually requested (see C2).B8. Linear-layout edge-offset guard checks the wrong length.
calculate_linear_cutout_positions.py:85-86guardsi < len(edge_offsets)but indexesedge_offsets[line_one_indices[i]]— safe only because the caller pre-pads; a direct call with a short listIndexErrors. Guard online_one_indices[i] < len(edge_offsets)or make padding the callee's job.B9. Tolerance is inconsistently applied between the two layouts. Linear adds tolerance between items; alternating computes
full_diametersand then never uses it — nesting math runs on raw diameters, so alternating trays get ~0.55mm less clearance than linear ones. Decide once (probably: layout always works on toleranced footprints) and delete the deadfull_diametersloops in both files.C. Code quality / hygiene
C1. Dead code and unused imports.
math,copyunused intray_generator.py:4-5;mathunused in linear positions; unusede =andnormal = copy.deepcopy(...)incutout_generator.py:30-33; big commented-outshow(...)blocks; leftover debug printsprint("usable_area", ...)infull_tray_generator.py:153-157(route through logging or a--verboseflag).C2.
from ocp_vscode import *at module import time everywhere. Every module (including pure-math ones' siblings) hard-requires a VS Code viewer package just to run the CLI. Import it lazily inside the__main__/preview paths only. Same forfrom build123d import *star-imports — switch to explicit imports (Circle,BuildPart, …); star-imports from two libraries into the same namespace is how shadowing bugs happen.C3. Package structure / dual-import hack. The
if __name__ == "__main__":absolute-vs-relative import blocks (full_tray_generator.py:7-16) should go away: add apyproject.toml, makeTrays(or a renamedlayersafe) a proper package, run modules viapython -m. Also addrequirements.txt/dependencies (build123d,ocp-vscode) — currently only the README mentions them.C4. Typos and naming.
base_heigth→base_height(everywhere);hinge_pin_radius/hinge_pin_diametermismatch (see A6);calculate_line_positions's results are calledx_positions/y_positionsin the caller but both are rows (front/back), not axes — renamefront_row/back_row.C5. Mutable default arguments.
diameters=[],edge_offsets=[],edge_adjusts=[]ingenerate_full_tray(full_tray_generator.py:75-95). UseNone+ normalize. Currently benign, classic footgun.C6. Magic numbers in geometry.
extrude(amount=6, taper=12.5),Cylinder(base_diameter, 6),2 - floor_thickness, boxes of height 5/8,0.4 - epsilonchamfers,hinge_top_offset = floor_thickness - 0.4. Name them as module constants or config fields with a comment on what each controls physically — essential once four shapes must produce matching wall/lip behavior.C7. No type hints, no docstrings on the core API. The layout functions and
generate_full_traytake/return dicts with implicit keys ('x','y','diameter','flipped'). At minimum add aPositiondataclass/TypedDict— this pairs naturally with A2.C8. The two chamfer
try/excepts inbase_tray_generatorsilently skip on failure (base_tray_generator.py:116-148). Fine as a policy, but log a warning so a user knows their print lost a functional chamfer (the hinge-rotation one is arguably not cosmetic — the comment says it "allows hinge to rotate back").D. Testing, docs, repo
D1. No tests at all. The layout modules are pure math with zero CAD dependency — perfect unit-test targets, and they're exactly what you'll be changing for new shapes. Before the refactor, pin current behavior with tests for: linear fit/overflow, alternating nesting + the B1–B3 bugs, edge-offset padding, tolerance handling, usable-area math. Add one slow "smoke" test that builds a small tray end-to-end and asserts the export succeeds and cutout count matches. This is the safety net that makes everything in section A cheap.
D2. README drift. Options table is missing
--cutout-shapeand--min-cutout-spacing; defaults differ from code (floor_thickness0.4 vs 0.8); it referencesTrays/tray_generator.ipynbwhich doesn't exist; auto-filenames don't encode shape (a 40mm square tray exports astray_1x40.0mm— indistinguishable from a circle tray; include the shape in the name).D3. Repo hygiene.
venv/,path/to/venv/(an accidental literal-path venv — delete it),__pycache__/,.DS_Store, and generatedTrays/output/*.stl|*.stepare all committed. Add/extend.gitignoreandgit rm -r --cachedthem. Also the stray root-leveloutput/dir (symptom of B6).