Skip to content

Sketch-anchoring refactor: make validate_design deps PASS for the Gochnour sideboard #59

Description

@YLZha

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:

  1. 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/...).
  2. 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:

  1. 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.
  2. 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).
  3. 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).

  1. 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.)

  2. 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).

  3. 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.

  4. 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.

  5. 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.

  6. 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).

  7. 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_designdeps.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/.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions