Gochnour Sideboard — Sketch-Anchoring Refactor Brief
For a fresh agent session. This is self-contained; you do not need the
prior conversation. Goal: make validate_design's deps check PASS by
anchoring every non-root sketch to projected parent geometry, fully
constraining the taper sketches, and declaring a single origin root.
The model is geometrically complete and correct (47 bodies). This refactor is
purely about sketch quality / parametric traceability — do not change the
resulting geometry (positions, sizes, joinery) — only how each sketch is
anchored and dimensioned.
0. Orientation
- Script (source of truth):
~/shopprentice-projects/gochnour-sideboard/gochnour_sideboard.py
- README: same folder — describes the built model.
- What's built: white-oak sideboard. Legs(4, tapered) + Rails(4) + Case(9:
bottom/top/2 sides/2 partitions/3 back panels, mitered-dovetail corners) +
Doors(12: 2 frame-and-panel leaves + 2 tenoned knobs) + Drawers(18: 3
dovetailed boxes, each with a spherical cove + tenoned knob). 47 bodies.
- Coordinates: X = width (case_w=48"), Y = depth (case_d=15.75"), Z = up.
Legs/rails were built at frame depth then Move'd -reveal so the frame stands
proud of the case. Case at y=[0, case_d]; legs front face at y=-reveal.
How to run a build (IMPORTANT — environment quirks)
The Fusion add-in imports woodworking/helpers/addin from a staging dir
/tmp/sp-work (re-staged from /var/tmp/shopprentice on each
execute_script), NOT from the conductor workspace. Two consequences:
- Template edits must be copied to the staging source or they won't load.
After editing any template (woodworking/templates/*.py) or helper, copy it
to BOTH /var/tmp/shopprentice/... and /tmp/sp-work/... (and keep the
canonical copy in ~/conductor/workspaces/shopprentice/miami/...).
- Validate after execution via the MCP
validate_design tool, not from
inside the script (in-script reads are stale). Tool-handler edits
(addin/tools/*.py) need an add-in reload to take effect.
Build with a wrapper that reloads modules + execs the file on a freshly
created document (a churned doc causes mirror getObjectPath errors and
leftover-occurrence/param collisions):
# execute_script(script=<this>, script_path=<the .py>) -- on a NEW document
import sys, adsk.core, adsk.fusion
def run(context):
app = adsk.core.Application.get()
design = adsk.fusion.Design.cast(app.activeProduct)
root = design.rootComponent
# clear any leftover occurrences / params from a recycled "new" doc
for i in range(root.occurrences.count-1,-1,-1):
try: root.occurrences.item(i).deleteMe()
except: pass
params = design.userParameters
for i in range(params.count):
try:
u=params.item(i).unit
params.item(i).expression = "1 deg" if u=="deg" else ("1" if u=="" else "1 cm")
except: pass
for _ in range(20):
if params.count==0: break
for i in range(params.count-1,-1,-1):
try: params.item(i).deleteMe()
except: pass
for m in list(sys.modules):
if m.startswith("woodworking") or m.startswith("helpers"):
del sys.modules[m]
path = "/Users/frankzha/shopprentice-projects/gochnour-sideboard/gochnour_sideboard.py"
with open(path) as f: src=f.read()
ns={"__name__":"gochnour_build"}; exec(compile(src,path,"exec"),ns); ns["run"](context)
Use manage_documents(action="new") first; if it returns a doc with leftover
occurrences, the wrapper's cleanup handles it. Avoid force_clean on a churned
doc (it triggers the mirror getObjectPath failure).
1. What the deps check wants
From validate_design deps output, three failing checks:
- Single origin root. "2 bodies reference origin (only 1 allowed):
['Leg_FL','Leg_FR']." → provide model.json declaring ONE root (Leg_FL→
origin) and every other body chained to a parent.
- Sketch origin check. "Non-root sketches must dimension from projected
reference geometry, not sketch origin." Every sketch_rect_model(...) call
currently uses ORIGIN mode (dims from sk.originPoint).
- Sketch traceability. Each non-root sketch must (a) project real parent
geometry, (b) use no Fix/Ground, (c) be fully constrained relative to that
reference. Only fitted-spline INTERIOR points may stay free (their
start/end may not). The taper sketches are flagged UNDER-CONSTRAINED.
2. The mechanism (verified this session)
Rect sketches — sp.sketch_rect_model(..., anchor=dict(...))
ANCHORED mode builds the rect from model corners, H/V-constrains all 4 edges,
projects the parent face (demoted to construction) and dimensions one
NON-origin corner from the projected parent corner with two positive offsets.
Keys:
anchor = dict(
parent_body = <BRepBody>, # the body this sketch references
parent_occ = None, # occurrence for a cross-component proxy; None if same comp
face_axis = 'z', # 'x'/'y'/'z': normal of the reference face
face_dir = +1, # +1 = max face along axis, -1 = min
anchor_xyz = (x_expr,y_expr,z_expr), # a model point ON that face (its projected
# corner); must NOT be the projection of the world origin
off1 = ('x', "<expr>"), # offset (positive) from the projected corner to the
off2 = ('y', "<expr>"), # anchored rect corner, along two model axes
which = 0, # which rect corner to anchor (0..3); use 1/2/3 if corner 0
# sits on the sketch origin
size_far = False, # size the FAR edges if a near edge touches an origin vertex
)
sk, prof = sp.sketch_rect_model(comp, plane, origin, size, name, ev=ev, anchor=anchor)
Cross-component parent (e.g. a rail sketch referencing a leg in another
component): pass parent_occ=<leg occurrence> so the proxy resolves to BRep.
Dovetail/miter sketches — trapezoid_sketch(..., anchor=dict(...))
woodworking/templates/_dovetail_common.trapezoid_sketch already supports an
anchor dict (parent_body, parent_occ, face_axis, face_dir, anchor_xyz,
off1, off2). The case dovetail pins call it with anchor=None today — pass an
anchor tying each pin trapezoid to the pin-board face it sits on. The 4 miter
triangles are hand-built sketches in miter_cut(); rewrite them to project the
board face and dimension from it (or accept they're cosmetic and exclude — but
prefer anchoring).
Memory / pitfalls (hard-won)
- Prefer
sketch_rect_model(anchor=) over sp.reanchor — reanchor's axis
mis-map can shift Z.
- Pass the EXACT parent corner in
anchor_xyz; offsets are POSITIVE magnitudes.
- Mirror symmetric parts AFTER anchoring the template part (the mirror inherits
a valid anchored sketch). Capture mirror-result bodies directly
(sp.mirror_body(...).bodies.item(0)) — do NOT re-find by name (a recycled
doc may hold stale duplicates → getObjectPath mirror failure).
- Run MCP
validate_design after each component; iterate until that
component's sketches drop off the deps FAIL list before moving on.
3. Component-by-component plan (rebuild + validate after each)
Work in dependency order; after each step, rebuild on a fresh doc and confirm
the just-touched sketches no longer appear under the deps "Sketch origin" /
"traceability" FAIL lists, and that body count stays 47 with geometry
unchanged (spot-check a couple of bounding boxes).
-
model.json (no geometry change). Create next to the script. One origin
root; chain the rest. Schema (per sp.validate_deps):
{"name":"Gochnour Sideboard","deps":[
{"body":"Leg_FL","ref":"origin","side":"outside","direction":[0,0,1],"contact":true,"note":"on floor"},
{"body":"Leg_FR","ref":"Leg_FL","side":"outside","direction":[1,0,0],"contact":false},
{"body":"Leg_BL","ref":"Leg_FL","side":"outside","direction":[0,1,0],"contact":false},
{"body":"Leg_BR","ref":"Leg_FL","side":"outside","direction":[1,1,0],"contact":false},
{"body":"Rail_F","ref":"Leg_FL","side":"outside","direction":[0,0,1],"contact":true},
... rails→legs, Case_Bot→Rail_F, Case_Top/Sides/Partitions/BackPanels→Case_Bot,
Doors→Case_SideL/R, Drawers→Case_Bot ...
]}
Re-run; the "2 bodies reference origin" FAIL should clear and each dep line
read OK. (Body-side/contact use centre-of-mass + bbox overlap — set
direction to the side that body sits on relative to its ref.)
-
Rails (parent = legs). Each rail spans between two legs at the leg tops.
Anchor FrontRail_Sk to Leg_FL's top/inner face (face_axis 'z' +1 or 'y');
anchor_xyz = the leg corner the rail starts from; off1/off2 the gap to
the rail corner. Build front+side once, mirror to back/right (mirrors
inherit). Note rails are Move'd -reveal AFTER building — anchoring is on the
pre-move sketch; that's fine (the Move is a separate feature).
-
Case bottom (parent = rails/leg tops, face_axis 'z'). Then top, sides,
partitions, back panels anchor to Case_Bot's projected faces (top→its
top face via the case_h offset; sides→its end faces; partitions→its top;
back panels→its back face). Mirror L/R partners.
-
Miters + dovetail pins. Pass anchor= to trapezoid_sketch for the
pin trapezoids (parent = the pin board, face_axis 'x' at the side's inner
face). Re-anchor the 4 miter_cut triangles to the board face, or fold them
into anchored sketches.
-
Taper sketches (TaperX_Sk, TaperY_Sk). Currently under-constrained
(a free DOF). Project the leg's faces, dimension the 3 taper points from the
projected leg corner, and add the missing constraint (the third point's
position) so Fusion reports fully constrained. Keep leg_foot/taper_start_z
as the driving dims.
-
Doors. Anchor the left leaf's stiles to the case opening (Case_SideL /
partition faces); anchor rails + panel to the stiles; mirror the leaf. Knob
sketches are revolves with fitted splines — their start/end + axis are
already dimensioned (knob_proj/knob_drw_proj, knob_stem); confirm they
pass (interior fit points may stay free).
-
Drawer template (woodworking/templates/dovetailed_drawer.py). Its
front/back/left/right/bottom rect sketches are ORIGIN-mode because the
anchor= support was REMOVED from build() on this branch. Restore it:
the original build(comp, prefix, ev, anchor=None) anchored the FRONT board
to an external parent and the back/left/bottom to the FRONT board's parallel
faces (see git history of that file on main). Re-add that logic (or
re-anchor the 5 boards in-place after build) so each traces to the front
board. Then in gochnour_sideboard.py pass the appropriate anchor (front
board → the drawer opening / lower stretcher). Copy the edited template to
/var/tmp/shopprentice + /tmp/sp-work before rebuilding.
4. Acceptance criteria
- MCP
validate_design → deps.passed = true (single root; no "Sketch origin"
FAILs; no "traceability" FAILs; taper sketches fully constrained).
- Connectivity unchanged: main structure 1 cluster; doors/drawers separate
(removable) — that's expected, not a failure.
- Interference: only the ~6 × 0.005 cm³ drawer bottom-groove specks (inherent
to the drawer template) — no new overlaps.
- Body count 47; geometry unchanged (legs proud of case by
reveal; drawer
fronts flush with door inner face; knobs seated/tenoned).
5. Related improvements already committed (this session)
- Weak-connection metric is now ratio-based (contact ≥ 8% of the smaller
part's volume^(2/3), OR ≥ 1.0 cm² absolute) in
addin/tools/validate_design.py + check_connectivity.py (canonical copy in
~/conductor/workspaces/shopprentice/miami/addin/tools/, plus staging). This
stops small parts (knobs) being false-flagged. Needs an add-in reload to go
live (the running handler is cached). Verify after reload that the knob
connections (~0.52 cm²) are no longer listed under weakConnections.
- Template fixes that must stay in the staging dirs (
/var/tmp/shopprentice
/tmp/sp-work): _dovetail_common.py (trapezoid angle-bisector fix that
un-leans the dovetails) and dovetailed_drawer.py (y_offset support). If a
rebuild shows leaning dovetails or a y_offset TypeError, the staging copy
was reverted — re-copy from miami/woodworking/templates/.
Gochnour Sideboard — Sketch-Anchoring Refactor Brief
For a fresh agent session. This is self-contained; you do not need the
prior conversation. Goal: make
validate_design's deps check PASS byanchoring every non-root sketch to projected parent geometry, fully
constraining the taper sketches, and declaring a single origin root.
The model is geometrically complete and correct (47 bodies). This refactor is
purely about sketch quality / parametric traceability — do not change the
resulting geometry (positions, sizes, joinery) — only how each sketch is
anchored and dimensioned.
0. Orientation
~/shopprentice-projects/gochnour-sideboard/gochnour_sideboard.pybottom/top/2 sides/2 partitions/3 back panels, mitered-dovetail corners) +
Doors(12: 2 frame-and-panel leaves + 2 tenoned knobs) + Drawers(18: 3
dovetailed boxes, each with a spherical cove + tenoned knob). 47 bodies.
Legs/rails were built at frame depth then Move'd -reveal so the frame stands
proud of the case. Case at y=[0, case_d]; legs front face at y=-reveal.
How to run a build (IMPORTANT — environment quirks)
The Fusion add-in imports
woodworking/helpers/addinfrom a staging dir/tmp/sp-work(re-staged from/var/tmp/shopprenticeon eachexecute_script), NOT from the conductor workspace. Two consequences:After editing any template (
woodworking/templates/*.py) or helper, copy itto BOTH
/var/tmp/shopprentice/...and/tmp/sp-work/...(and keep thecanonical copy in
~/conductor/workspaces/shopprentice/miami/...).validate_designtool, not frominside the script (in-script reads are stale). Tool-handler edits
(addin/tools/*.py) need an add-in reload to take effect.
Build with a wrapper that reloads modules + execs the file on a freshly
created document (a churned doc causes
mirrorgetObjectPatherrors andleftover-occurrence/param collisions):
Use
manage_documents(action="new")first; if it returns a doc with leftoveroccurrences, the wrapper's cleanup handles it. Avoid
force_cleanon a churneddoc (it triggers the mirror
getObjectPathfailure).1. What the deps check wants
From
validate_designdeps output, three failing checks:['Leg_FL','Leg_FR']." → provide
model.jsondeclaring ONE root (Leg_FL→origin) and every other body chained to a parent.
reference geometry, not sketch origin." Every
sketch_rect_model(...)callcurrently uses ORIGIN mode (dims from
sk.originPoint).geometry, (b) use no Fix/Ground, (c) be fully constrained relative to that
reference. Only fitted-spline INTERIOR points may stay free (their
start/end may not). The taper sketches are flagged UNDER-CONSTRAINED.
2. The mechanism (verified this session)
Rect sketches —
sp.sketch_rect_model(..., anchor=dict(...))ANCHORED mode builds the rect from model corners, H/V-constrains all 4 edges,
projects the parent face (demoted to construction) and dimensions one
NON-origin corner from the projected parent corner with two positive offsets.
Keys:
Cross-component parent (e.g. a rail sketch referencing a leg in another
component): pass
parent_occ=<leg occurrence>so the proxy resolves to BRep.Dovetail/miter sketches —
trapezoid_sketch(..., anchor=dict(...))woodworking/templates/_dovetail_common.trapezoid_sketchalready supports ananchordict (parent_body, parent_occ, face_axis, face_dir, anchor_xyz,off1, off2). The case dovetail pins call it with
anchor=Nonetoday — pass ananchor tying each pin trapezoid to the pin-board face it sits on. The 4 miter
triangles are hand-built sketches in
miter_cut(); rewrite them to project theboard face and dimension from it (or accept they're cosmetic and exclude — but
prefer anchoring).
Memory / pitfalls (hard-won)
sketch_rect_model(anchor=)oversp.reanchor— reanchor's axismis-map can shift Z.
anchor_xyz; offsets are POSITIVE magnitudes.a valid anchored sketch). Capture mirror-result bodies directly
(
sp.mirror_body(...).bodies.item(0)) — do NOT re-find by name (a recycleddoc may hold stale duplicates →
getObjectPathmirror failure).validate_designafter each component; iterate until thatcomponent's sketches drop off the deps FAIL list before moving on.
3. Component-by-component plan (rebuild + validate after each)
Work in dependency order; after each step, rebuild on a fresh doc and confirm
the just-touched sketches no longer appear under the deps "Sketch origin" /
"traceability" FAIL lists, and that body count stays 47 with geometry
unchanged (spot-check a couple of bounding boxes).
model.json (no geometry change). Create next to the script. One origin
root; chain the rest. Schema (per
sp.validate_deps):{"name":"Gochnour Sideboard","deps":[ {"body":"Leg_FL","ref":"origin","side":"outside","direction":[0,0,1],"contact":true,"note":"on floor"}, {"body":"Leg_FR","ref":"Leg_FL","side":"outside","direction":[1,0,0],"contact":false}, {"body":"Leg_BL","ref":"Leg_FL","side":"outside","direction":[0,1,0],"contact":false}, {"body":"Leg_BR","ref":"Leg_FL","side":"outside","direction":[1,1,0],"contact":false}, {"body":"Rail_F","ref":"Leg_FL","side":"outside","direction":[0,0,1],"contact":true}, ... rails→legs, Case_Bot→Rail_F, Case_Top/Sides/Partitions/BackPanels→Case_Bot, Doors→Case_SideL/R, Drawers→Case_Bot ... ]}Re-run; the "2 bodies reference origin" FAIL should clear and each dep line
read OK. (Body-side/contact use centre-of-mass + bbox overlap — set
directionto the side that body sits on relative to its ref.)Rails (parent = legs). Each rail spans between two legs at the leg tops.
Anchor
FrontRail_SktoLeg_FL's top/inner face (face_axis 'z' +1 or 'y');anchor_xyz= the leg corner the rail starts from;off1/off2the gap tothe rail corner. Build front+side once, mirror to back/right (mirrors
inherit). Note rails are Move'd -reveal AFTER building — anchoring is on the
pre-move sketch; that's fine (the Move is a separate feature).
Case bottom (parent = rails/leg tops, face_axis 'z'). Then top, sides,
partitions, back panels anchor to
Case_Bot's projected faces (top→itstop face via the case_h offset; sides→its end faces; partitions→its top;
back panels→its back face). Mirror L/R partners.
Miters + dovetail pins. Pass
anchor=totrapezoid_sketchfor thepin trapezoids (parent = the pin board, face_axis 'x' at the side's inner
face). Re-anchor the 4
miter_cuttriangles to the board face, or fold theminto anchored sketches.
Taper sketches (
TaperX_Sk,TaperY_Sk). Currently under-constrained(a free DOF). Project the leg's faces, dimension the 3 taper points from the
projected leg corner, and add the missing constraint (the third point's
position) so Fusion reports fully constrained. Keep
leg_foot/taper_start_zas the driving dims.
Doors. Anchor the left leaf's stiles to the case opening (Case_SideL /
partition faces); anchor rails + panel to the stiles; mirror the leaf. Knob
sketches are revolves with fitted splines — their start/end + axis are
already dimensioned (
knob_proj/knob_drw_proj,knob_stem); confirm theypass (interior fit points may stay free).
Drawer template (
woodworking/templates/dovetailed_drawer.py). Itsfront/back/left/right/bottom rect sketches are ORIGIN-mode because the
anchor=support was REMOVED frombuild()on this branch. Restore it:the original
build(comp, prefix, ev, anchor=None)anchored the FRONT boardto an external parent and the back/left/bottom to the FRONT board's parallel
faces (see git history of that file on
main). Re-add that logic (orre-anchor the 5 boards in-place after build) so each traces to the front
board. Then in
gochnour_sideboard.pypass the appropriateanchor(frontboard → the drawer opening / lower stretcher). Copy the edited template to
/var/tmp/shopprentice+/tmp/sp-workbefore rebuilding.4. Acceptance criteria
validate_design→deps.passed = true(single root; no "Sketch origin"FAILs; no "traceability" FAILs; taper sketches fully constrained).
(removable) — that's expected, not a failure.
to the drawer template) — no new overlaps.
reveal; drawerfronts flush with door inner face; knobs seated/tenoned).
5. Related improvements already committed (this session)
part's
volume^(2/3), OR ≥ 1.0 cm² absolute) inaddin/tools/validate_design.py+check_connectivity.py(canonical copy in~/conductor/workspaces/shopprentice/miami/addin/tools/, plus staging). Thisstops small parts (knobs) being false-flagged. Needs an add-in reload to go
live (the running handler is cached). Verify after reload that the knob
connections (~0.52 cm²) are no longer listed under
weakConnections./var/tmp/shopprentice/tmp/sp-work):_dovetail_common.py(trapezoid angle-bisector fix thatun-leans the dovetails) and
dovetailed_drawer.py(y_offsetsupport). If arebuild shows leaning dovetails or a
y_offsetTypeError, the staging copywas reverted — re-copy from
miami/woodworking/templates/.