Skip to content

Commit be2d01f

Browse files
aldoruizlunaclaude
andauthored
feat(corner-idler-bracket): graph twin of the script (graph format 1.1) (#169)
corner-idler.graph.json is a golden twin of main.py, declared as graph_file on the corner_idler mode beside the script (owner decision D5: graph twins coexist with scripts, parity-checked; main.py stays the rendered source and the oracle). Digital Twins phase 8b, lane P8-TWINS-B. Same parameters (declared in `parameters`, `mirrored` included), same derivations (the nine clamps as ternary pairs in `derived`, `length`'s max as a ternary, the constants under main.py's names and values) and the same operations in the same order. `mirrored` selects the pure YZ `reflect` of the body, as body.mirror("YZ") does. Each _cyl_y is an XZ profile_circle extruded along +Y, turned -90 degrees about its own axis, then translated into place. The turn puts the cylinder's seam at +Z, where makeCylinder (gp_Ax2 with an axis along Y) puts it; the seam decides where the tessellator puts vertices, and without it the two meshes differ by 0.001635 mm at the counterbore floors. y4d-spec check --render --require-openscad --parity ./corner-idler-bracket: 10 renders, parity 5/5 at 0.000000 mm (defaults + gates_9mm_right, gates_9mm_left, belt_6mm_right, long_drop_wide_pitch; the two mirrored presets included), frames 35/35 ok, failures 0. Frames and presets unchanged. Assembly A validates (235/235 placed, 292/292 mates, poses 23/23); its digest moves cb80b679... -> 0839e06c... because the cartridge gains a file. Signed-off-by: Aldo Ruiz Luna <aldo.ruiz.luna@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 04950d6 commit be2d01f

3 files changed

Lines changed: 129 additions & 0 deletions

File tree

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
{
2+
"version": "1.1.0",
3+
"units": "mm",
4+
"meta": {
5+
"title": "Corner Idler Bracket (graph twin)",
6+
"notes": "Golden twin of main.py (graph format 1.1): the same parameters, the same derivations and the same operations in the same order. main.py stays the oracle; y4d-spec --parity compares the two at the defaults and at every preset. Constants keep main.py's names and values, and their sources are main.py's: ISO 7380-1 M5x30 (Keller & Kalmbach 157380530: SCREW_D 5, SCREW_L 30, HEAD_DK 9.5, HEAD_K 2.75), MISUMI HFS5 T-slot (SLOT_LIP_T 2, PILOT_D 4.2, PROFILE 20), MISUMI HNTAP5-5 (TNUT_L 15); HOLE_CLEAR, HEAD_CLEAR, TAB_W, POCKET_RADIAL, AXLE_WALL, HEEL_GAP, MOUNT_X1 and EDGE are main.py's labelled conventions. Clamps max(lo, min(x, hi)) and length's max are ternaries, because the expression dialect has no function calls. _cyl_y (makeCylinder along +Y) is an XZ profile_circle at (x, z) extruded by -(y1 - y0), since the XZ normal is -Y, then translated to y0. `mirrored` selects the pure YZ reflection of the body, as body.mirror(\"YZ\") does."
7+
},
8+
"parameters": {
9+
"idler_width": {"default": 14.0},
10+
"idler_od": {"default": 18.0},
11+
"idler_clear": {"default": 0.5},
12+
"idler_offset": {"default": 13.0},
13+
"idler_drop": {"default": 24.0},
14+
"front_arm": {"default": 5.0},
15+
"mount_grip": {"default": 24.5},
16+
"mount_pitch": {"default": 20.0},
17+
"heel_reach": {"default": 18.0},
18+
"mirrored": {"default": false}
19+
},
20+
"derived": [
21+
{"id": "SCREW_D", "expr": "5.0"},
22+
{"id": "SCREW_L", "expr": "30.0"},
23+
{"id": "HEAD_DK", "expr": "9.5"},
24+
{"id": "HEAD_K", "expr": "2.75"},
25+
{"id": "SLOT_LIP_T", "expr": "2.0"},
26+
{"id": "PILOT_D", "expr": "4.2"},
27+
{"id": "PROFILE", "expr": "20.0"},
28+
{"id": "TNUT_L", "expr": "15.0"},
29+
{"id": "HOLE_CLEAR", "expr": "0.25"},
30+
{"id": "HEAD_CLEAR", "expr": "0.5"},
31+
{"id": "TAB_W", "expr": "5.8"},
32+
{"id": "POCKET_RADIAL", "expr": "1.0"},
33+
{"id": "AXLE_WALL", "expr": "7.0"},
34+
{"id": "HEEL_GAP", "expr": "0.5"},
35+
{"id": "MOUNT_X1", "expr": "10.0"},
36+
{"id": "EDGE", "expr": "4.75"},
37+
{"id": "iw_cap", "expr": "idler_width < 20 ? idler_width : 20"},
38+
{"id": "iw", "expr": "9 > iw_cap ? 9 : iw_cap"},
39+
{"id": "od_cap", "expr": "idler_od < 24 ? idler_od : 24"},
40+
{"id": "od", "expr": "12 > od_cap ? 12 : od_cap"},
41+
{"id": "ic_cap", "expr": "idler_clear < 2 ? idler_clear : 2"},
42+
{"id": "ic", "expr": "0.2 > ic_cap ? 0.2 : ic_cap"},
43+
{"id": "fa_cap", "expr": "front_arm < 8 ? front_arm : 8"},
44+
{"id": "fa", "expr": "3 > fa_cap ? 3 : fa_cap"},
45+
{"id": "gap", "expr": "iw + 2 * ic"},
46+
{"id": "rear_arm", "expr": "SCREW_L - fa - gap"},
47+
{"id": "pocket_r", "expr": "od / 2 + POCKET_RADIAL"},
48+
{"id": "io_cap", "expr": "idler_offset < 40 ? idler_offset : 40"},
49+
{"id": "io", "expr": "(pocket_r + 1) > io_cap ? (pocket_r + 1) : io_cap"},
50+
{"id": "idrop_cap", "expr": "idler_drop < 60 ? idler_drop : 60"},
51+
{"id": "idrop", "expr": "(pocket_r + 8) > idrop_cap ? (pocket_r + 8) : idrop_cap"},
52+
{"id": "mg_cap", "expr": "mount_grip < (SCREW_L - HEAD_K - 0.5) ? mount_grip : (SCREW_L - HEAD_K - 0.5)"},
53+
{"id": "mg", "expr": "(SCREW_L - 8) > mg_cap ? (SCREW_L - 8) : mg_cap"},
54+
{"id": "mp_cap", "expr": "mount_pitch < 60 ? mount_pitch : 60"},
55+
{"id": "mp", "expr": "(TNUT_L + 1) > mp_cap ? (TNUT_L + 1) : mp_cap"},
56+
{"id": "hr_cap", "expr": "heel_reach < PROFILE ? heel_reach : PROFILE"},
57+
{"id": "hr", "expr": "(PROFILE / 2 + TAB_W / 2 + 2) > hr_cap ? (PROFILE / 2 + TAB_W / 2 + 2) : hr_cap"},
58+
{"id": "DEPTH", "expr": "SCREW_L"},
59+
{"id": "TOP", "expr": "PROFILE / 2"},
60+
{"id": "z_axle", "expr": "-idrop"},
61+
{"id": "z_bottom", "expr": "z_axle - AXLE_WALL"},
62+
{"id": "x2", "expr": "MOUNT_X1 + mp"},
63+
{"id": "length_a", "expr": "x2 + (HEAD_DK / 2 + HEAD_CLEAR) + EDGE"},
64+
{"id": "length_b", "expr": "io + pocket_r + 4"},
65+
{"id": "length", "expr": "length_a > length_b ? length_a : length_b"},
66+
{"id": "heel_top", "expr": "-PROFILE / 2 - HEEL_GAP"},
67+
{"id": "hole_d", "expr": "SCREW_D + 2 * HOLE_CLEAR"},
68+
{"id": "cbore_d", "expr": "HEAD_DK + 2 * HEAD_CLEAR"}
69+
],
70+
"nodes": [
71+
{"id": "block_blank", "type": "box", "params": {"w": {"expr": "length - 0"}, "d": {"expr": "DEPTH - 0"}, "h": {"expr": "TOP - z_bottom"}}},
72+
{"id": "block", "type": "translate", "inputs": {"shape": "block_blank"}, "params": {"x": {"expr": "(0 + length) / 2"}, "y": {"expr": "(0 + DEPTH) / 2"}, "z": {"expr": "(z_bottom + TOP) / 2"}}},
73+
{"id": "heel_blank", "type": "box", "params": {"w": {"expr": "0.5 - -hr"}, "d": {"expr": "DEPTH - 0"}, "h": {"expr": "heel_top - z_bottom"}}},
74+
{"id": "heel", "type": "translate", "inputs": {"shape": "heel_blank"}, "params": {"x": {"expr": "(-hr + 0.5) / 2"}, "y": {"expr": "(0 + DEPTH) / 2"}, "z": {"expr": "(z_bottom + heel_top) / 2"}}},
75+
{"id": "body_1", "type": "union", "inputs": {"a": "block", "b": "heel"}},
76+
{"id": "corner_tongue_blank", "type": "box", "params": {"w": {"expr": "0.5 - -SLOT_LIP_T"}, "d": {"expr": "DEPTH - 2 - 2"}, "h": {"expr": "TAB_W / 2 - (-TAB_W / 2)"}}},
77+
{"id": "corner_tongue", "type": "translate", "inputs": {"shape": "corner_tongue_blank"}, "params": {"x": {"expr": "(-SLOT_LIP_T + 0.5) / 2"}, "y": {"expr": "(2 + (DEPTH - 2)) / 2"}, "z": {"expr": "(-TAB_W / 2 + (TAB_W / 2)) / 2"}}},
78+
{"id": "body_2", "type": "union", "inputs": {"a": "body_1", "b": "corner_tongue"}},
79+
{"id": "heel_tongue_blank", "type": "box", "params": {"w": {"expr": "-PROFILE / 2 + TAB_W / 2 - (-PROFILE / 2 - TAB_W / 2)"}, "d": {"expr": "0.5 - -SLOT_LIP_T"}, "h": {"expr": "heel_top - 2 - (z_bottom + 2)"}}},
80+
{"id": "heel_tongue", "type": "translate", "inputs": {"shape": "heel_tongue_blank"}, "params": {"x": {"expr": "(-PROFILE / 2 - TAB_W / 2 + (-PROFILE / 2 + TAB_W / 2)) / 2"}, "y": {"expr": "(-SLOT_LIP_T + 0.5) / 2"}, "z": {"expr": "(z_bottom + 2 + (heel_top - 2)) / 2"}}},
81+
{"id": "body_3", "type": "union", "inputs": {"a": "body_2", "b": "heel_tongue"}},
82+
{"id": "mount_hole_1_outline", "type": "profile_circle", "params": {"plane": "XZ", "r": {"expr": "hole_d / 2"}}},
83+
{"id": "mount_hole_1_blank", "type": "extrude", "inputs": {"profile": "mount_hole_1_outline"}, "params": {"height": {"expr": "-(DEPTH + 1 - -1)"}}},
84+
{"id": "mount_hole_1_seam", "type": "rotate", "inputs": {"shape": "mount_hole_1_blank"}, "params": {"axis": "y", "angle": -90.0}},
85+
{"id": "mount_hole_1", "type": "translate", "inputs": {"shape": "mount_hole_1_seam"}, "params": {"x": {"expr": "MOUNT_X1"}, "y": -1.0}},
86+
{"id": "body_4", "type": "cut", "inputs": {"a": "body_3", "b": "mount_hole_1"}},
87+
{"id": "mount_cbore_1_outline", "type": "profile_circle", "params": {"plane": "XZ", "r": {"expr": "cbore_d / 2"}}},
88+
{"id": "mount_cbore_1_blank", "type": "extrude", "inputs": {"profile": "mount_cbore_1_outline"}, "params": {"height": {"expr": "-(DEPTH + 1 - mg)"}}},
89+
{"id": "mount_cbore_1_seam", "type": "rotate", "inputs": {"shape": "mount_cbore_1_blank"}, "params": {"axis": "y", "angle": -90.0}},
90+
{"id": "mount_cbore_1", "type": "translate", "inputs": {"shape": "mount_cbore_1_seam"}, "params": {"x": {"expr": "MOUNT_X1"}, "y": {"expr": "mg"}}},
91+
{"id": "body_5", "type": "cut", "inputs": {"a": "body_4", "b": "mount_cbore_1"}},
92+
{"id": "mount_hole_2_outline", "type": "profile_circle", "params": {"plane": "XZ", "r": {"expr": "hole_d / 2"}}},
93+
{"id": "mount_hole_2_blank", "type": "extrude", "inputs": {"profile": "mount_hole_2_outline"}, "params": {"height": {"expr": "-(DEPTH + 1 - -1)"}}},
94+
{"id": "mount_hole_2_seam", "type": "rotate", "inputs": {"shape": "mount_hole_2_blank"}, "params": {"axis": "y", "angle": -90.0}},
95+
{"id": "mount_hole_2", "type": "translate", "inputs": {"shape": "mount_hole_2_seam"}, "params": {"x": {"expr": "x2"}, "y": -1.0}},
96+
{"id": "body_6", "type": "cut", "inputs": {"a": "body_5", "b": "mount_hole_2"}},
97+
{"id": "mount_cbore_2_outline", "type": "profile_circle", "params": {"plane": "XZ", "r": {"expr": "cbore_d / 2"}}},
98+
{"id": "mount_cbore_2_blank", "type": "extrude", "inputs": {"profile": "mount_cbore_2_outline"}, "params": {"height": {"expr": "-(DEPTH + 1 - mg)"}}},
99+
{"id": "mount_cbore_2_seam", "type": "rotate", "inputs": {"shape": "mount_cbore_2_blank"}, "params": {"axis": "y", "angle": -90.0}},
100+
{"id": "mount_cbore_2", "type": "translate", "inputs": {"shape": "mount_cbore_2_seam"}, "params": {"x": {"expr": "x2"}, "y": {"expr": "mg"}}},
101+
{"id": "body_7", "type": "cut", "inputs": {"a": "body_6", "b": "mount_cbore_2"}},
102+
{"id": "idler_pocket_blank", "type": "box", "params": {"w": {"expr": "io + pocket_r - (io - pocket_r)"}, "d": {"expr": "rear_arm + gap - rear_arm"}, "h": {"expr": "z_axle + pocket_r - (z_bottom - 1)"}}},
103+
{"id": "idler_pocket", "type": "translate", "inputs": {"shape": "idler_pocket_blank"}, "params": {"x": {"expr": "(io - pocket_r + (io + pocket_r)) / 2"}, "y": {"expr": "(rear_arm + (rear_arm + gap)) / 2"}, "z": {"expr": "(z_bottom - 1 + (z_axle + pocket_r)) / 2"}}},
104+
{"id": "body_8", "type": "cut", "inputs": {"a": "body_7", "b": "idler_pocket"}},
105+
{"id": "axle_clearance_outline", "type": "profile_circle", "params": {"plane": "XZ", "r": {"expr": "hole_d / 2"}}},
106+
{"id": "axle_clearance_blank", "type": "extrude", "inputs": {"profile": "axle_clearance_outline"}, "params": {"height": {"expr": "-(DEPTH + 1 - (rear_arm + gap - 0.5))"}}},
107+
{"id": "axle_clearance_seam", "type": "rotate", "inputs": {"shape": "axle_clearance_blank"}, "params": {"axis": "y", "angle": -90.0}},
108+
{"id": "axle_clearance", "type": "translate", "inputs": {"shape": "axle_clearance_seam"}, "params": {"x": {"expr": "io"}, "y": {"expr": "rear_arm + gap - 0.5"}, "z": {"expr": "z_axle"}}},
109+
{"id": "body_9", "type": "cut", "inputs": {"a": "body_8", "b": "axle_clearance"}},
110+
{"id": "axle_pilot_outline", "type": "profile_circle", "params": {"plane": "XZ", "r": {"expr": "PILOT_D / 2"}}},
111+
{"id": "axle_pilot_blank", "type": "extrude", "inputs": {"profile": "axle_pilot_outline"}, "params": {"height": {"expr": "-(rear_arm + 0.5 - -1)"}}},
112+
{"id": "axle_pilot_seam", "type": "rotate", "inputs": {"shape": "axle_pilot_blank"}, "params": {"axis": "y", "angle": -90.0}},
113+
{"id": "axle_pilot", "type": "translate", "inputs": {"shape": "axle_pilot_seam"}, "params": {"x": {"expr": "io"}, "y": -1.0, "z": {"expr": "z_axle"}}},
114+
{"id": "body_10", "type": "cut", "inputs": {"a": "body_9", "b": "axle_pilot"}},
115+
{"id": "body_mirrored", "type": "reflect", "inputs": {"shape": "body_10"}, "params": {"plane": "YZ"}},
116+
{"id": "corner_idler", "type": "select", "inputs": {"if_true": "body_mirrored", "if_false": "body_10"}, "params": {"when": {"expr": "mirrored"}}}
117+
],
118+
"outputs": {
119+
"corner_idler": "corner_idler"
120+
}
121+
}

‎corner-idler-bracket/docs/README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ That closure is the assembly A's top corner should use.
100100
- **Long Drop, Wide T-Nut Spacing.** Left-hand: the axle 36 mm down and 20 mm from the corner,
101101
with the T-nuts 30 mm apart.
102102

103+
## Graph twin
104+
105+
`corner-idler.graph.json` is a node-graph twin of `main.py` (graph format 1.1). It has the same parameters, the same derivations and the same operations in the same order. Each `_box` is a centred `box` translated to its centre, and each clamp a min/max ternary pair in `derived`. Each `_cyl_y` is an XZ `profile_circle` extruded along +Y (the XZ normal is −Y, so the height is negative), turned −90° about its own axis so the cylinder's seam sits at +Z where `makeCylinder` puts it, then translated into place; the seam decides where the tessellator puts vertices, so without the turn the two meshes differ by 0.0016 mm. `mirrored` is a `select` between the body and its pure YZ `reflect`, as `body.mirror("YZ")`. The constants keep `main.py`'s names, values and sources.
106+
- **Declaration.** The mode declares it as `graph_file` next to the script.
107+
- **Verification.** `y4d-spec check --render --parity` compares the two at the defaults and at every preset (`gates_9mm_right`, `gates_9mm_left`, `belt_6mm_right`, `long_drop_wide_pitch`; the two mirrored presets included); they agree to 0.000000 mm.
108+
- **Which one renders.** `main.py` stays the source the platform renders and the oracle the graph is checked against. A script retires only after parity holds across the nightly sweep (owner decision D5, 2026-10-04).
109+
103110
## Hyperobject Profile
104111

105112
- **Domain:** industrial.

‎corner-idler-bracket/project.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
},
4343
"scad_file": "main.py",
4444
"cq_file": "main.py",
45+
"graph_file": "corner-idler.graph.json",
4546
"parts": [
4647
"corner_idler"
4748
],

0 commit comments

Comments
 (0)