forgecad 0.10.4 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/dist/assets/{AdminPage-B3L3W1Uo.js → AdminPage-B1nIvqLS.js} +1 -1
  2. package/dist/assets/{BenchmarkPage-DXKVXMrJ.js → BenchmarkPage-YZJbw5nd.js} +2 -2
  3. package/dist/assets/{BlogPage-B7BWxOCg.js → BlogPage-DIWRApKS.js} +1 -1
  4. package/dist/assets/{DocsPage-BPGGwht1.js → DocsPage-ClL6X1hR.js} +8 -22
  5. package/dist/assets/EditorApp-CYBDvSyT.js +17067 -0
  6. package/dist/assets/{EmbedViewer-DygByZS2.js → EmbedViewer-Dmfu_LIw.js} +2 -2
  7. package/dist/assets/{LandingPageProofDriven-BoVE7JGY.js → LandingPageProofDriven-XYTiYxfM.js} +2 -2
  8. package/dist/assets/{LegalPage-Din8wv8d.js → LegalPage-D5Z3CscF.js} +2 -2
  9. package/dist/assets/{PricingPage-C2PMzmDc.js → PricingPage-BP4lIGio.js} +2 -2
  10. package/dist/assets/{SettingsPage-BlJDCRe8.js → SettingsPage-D3bcPBsC.js} +1 -1
  11. package/dist/assets/{app-BsRYSfxY.js → app-BKjogwIZ.js} +3288 -512
  12. package/dist/assets/{backendInit-6C0DLgH0.js → backendInit-6a9-ilom.js} +80498 -74979
  13. package/dist/assets/cli/{render-XXol_ET7.js → render-CMNudGb0.js} +1264 -113
  14. package/dist/assets/{constructionHistoryWorker-cTHWRJEi.js → constructionHistoryWorker-BuZgc606.js} +8369 -6839
  15. package/dist/assets/{evalWorker-BssDYW9u.js → evalWorker-DQ82ueGu.js} +45438 -39996
  16. package/dist/assets/{forgecad_geometry-CZ_IfuvA.js → forgecad_geometry-D8rWX7nQ.js} +1 -1
  17. package/dist/assets/{forgecad_geometry_bg-C3rQHfwg.wasm → forgecad_geometry_bg-ObqfqjJT.wasm} +0 -0
  18. package/dist/assets/{inspectWorker-ymhBV4Ll.js → inspectWorker-Cuby2qfT.js} +4899 -1303
  19. package/dist/assets/{jointPose-B0blBj9A.js → jointPose-CFql5I-u.js} +1 -1
  20. package/dist/assets/{landing-proof-driven-Cpf-MIbI.css → landing-proof-driven-_u4v_xQb.css} +2 -2
  21. package/dist/assets/{manifold-CYlIm-M6.js → manifold-02pmr7O7.js} +2 -2
  22. package/dist/assets/{manifold-B_7QXpGB.js → manifold-C6KU0oII.js} +1 -1
  23. package/dist/assets/{manifold-CNShmpEJ.js → manifold-P1yF3GKn.js} +1 -1
  24. package/dist/assets/{reportWorker-Cb5eyM7D.js → reportWorker-kg065BVL.js} +76583 -65731
  25. package/dist/cli/render.html +1 -1
  26. package/dist/docs/index.html +2 -2
  27. package/dist/docs-raw/AI/usage.md +6 -8
  28. package/dist/docs-raw/CLI.md +14 -12
  29. package/dist/docs-raw/component-model.md +28 -9
  30. package/dist/docs-raw/generated/assembly.md +76 -3
  31. package/dist/docs-raw/generated/concepts.md +43 -7
  32. package/dist/docs-raw/generated/core.md +399 -73
  33. package/dist/docs-raw/generated/curves.md +357 -6
  34. package/dist/docs-raw/generated/runtime-names.md +12 -12
  35. package/dist/docs-raw/generated/sketch.md +16 -3
  36. package/dist/docs-raw/guides/inspection-bundles.md +5 -3
  37. package/dist/docs-raw/guides/structural-fea.md +235 -0
  38. package/dist/docs-raw/skills/forgecad-build-model.md +70 -147
  39. package/dist/docs-raw/skills/forgecad-image-prompt.md +1 -1
  40. package/dist/docs-raw/skills/forgecad-project-sync.md +3 -3
  41. package/dist/docs-raw/skills/forgecad-reconstruct-cad-file.md +2 -2
  42. package/dist/docs-raw/skills/forgecad-reconstruct-from-images.md +4 -5
  43. package/dist/docs-raw/skills/forgecad.md +4 -1
  44. package/dist/docs-raw/skills/index.md +1 -5
  45. package/dist/docs-raw/welcome.md +3 -4
  46. package/dist/index.html +1 -1
  47. package/dist/llms.txt +1 -2
  48. package/dist/sitemap.xml +15 -15
  49. package/dist-cli/{check-compiler-4RPB6SB5.js → check-compiler-UJWUEIDC.js} +1 -1
  50. package/dist-cli/{check-query-propagation-KN3DFQTX.js → check-query-propagation-O2EPDJSY.js} +1 -1
  51. package/dist-cli/{chunk-UHBRMYA6.js → chunk-MNDROM7T.js} +78926 -73392
  52. package/dist-cli/forgecad.js +6306 -1061
  53. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  54. package/dist-skill/CONTEXT.md +1257 -110
  55. package/dist-skill/SKILL.md +4 -1
  56. package/dist-skill/docs/API/core/concepts.md +31 -4
  57. package/dist-skill/docs/CLI.md +14 -12
  58. package/dist-skill/docs/generated/assembly.md +73 -3
  59. package/dist-skill/docs/generated/core.md +395 -74
  60. package/dist-skill/docs/generated/curves.md +356 -6
  61. package/dist-skill/docs/generated/runtime-names.md +12 -12
  62. package/dist-skill/docs/generated/sketch.md +16 -3
  63. package/dist-skill/docs/guides/inspection-bundles.md +5 -3
  64. package/dist-skill/docs/guides/manual-parameters.md +130 -0
  65. package/dist-skill/docs/guides/structural-fea.md +235 -0
  66. package/dist-skill/library/README.md +0 -4
  67. package/dist-skill/library/forgecad-build-model/SKILL.md +57 -150
  68. package/dist-skill/library/forgecad-build-model/references/inspection-feedback.md +58 -0
  69. package/dist-skill/library/forgecad-build-model/references/module-contracts.md +53 -0
  70. package/dist-skill/library/forgecad-build-model/references/parameter-controls.md +22 -0
  71. package/dist-skill/library/forgecad-build-model/references/readiness-review.md +43 -0
  72. package/dist-skill/library/forgecad-build-model/references/simulation-feedback.md +49 -0
  73. package/dist-skill/library/forgecad-build-model/references/stage-1-design-intent.md +21 -0
  74. package/dist-skill/library/forgecad-build-model/references/stage-2-architecture-plan.md +23 -0
  75. package/dist-skill/library/forgecad-build-model/references/stage-3-build-slices.md +39 -0
  76. package/dist-skill/library/forgecad-build-model/references/stage-4-feedback-iteration.md +24 -0
  77. package/dist-skill/library/forgecad-build-model/references/stage-5-readiness-package.md +34 -0
  78. package/dist-skill/library/forgecad-image-prompt/SKILL.md +1 -1
  79. package/dist-skill/library/forgecad-project-sync/SKILL.md +3 -3
  80. package/dist-skill/library/forgecad-reconstruct-cad-file/SKILL.md +2 -2
  81. package/dist-skill/library/forgecad-reconstruct-from-images/SKILL.md +4 -5
  82. package/dist-skill/website/skills/forgecad-build-model.md +70 -147
  83. package/dist-skill/website/skills/forgecad-image-prompt.md +1 -1
  84. package/dist-skill/website/skills/forgecad-project-sync.md +3 -3
  85. package/dist-skill/website/skills/forgecad-reconstruct-cad-file.md +2 -2
  86. package/dist-skill/website/skills/forgecad-reconstruct-from-images.md +4 -5
  87. package/dist-skill/website/skills/forgecad.md +4 -1
  88. package/dist-skill/website/skills/index.md +1 -5
  89. package/examples/analysis/structural-stress-fea.forge.js +19 -0
  90. package/examples/api/blend-full-round.forge.js +37 -0
  91. package/examples/api/blend-variable-radius.forge.js +51 -0
  92. package/examples/api/curve-project-and-intersect.forge.js +59 -0
  93. package/examples/api/extrude-up-to-face.forge.js +47 -0
  94. package/examples/api/param-path2d.forge.js +65 -0
  95. package/examples/api/param-placement2d.forge.js +80 -0
  96. package/examples/api/param-spline2d-g-continuity.forge.js +57 -0
  97. package/examples/api/spoon-full-tang-handle.forge.js +188 -0
  98. package/examples/api/surface-boundarynet-dished-bowl.forge.js +63 -0
  99. package/examples/api/surface-fill-interior-constraints.forge.js +59 -0
  100. package/examples/api/surface-variable-thickness-panel.forge.js +62 -0
  101. package/examples/mechanical/airplane-propeller.forge.js +81 -28
  102. package/package.json +5 -2
  103. package/dist/assets/EditorApp-BWUGCdD5.js +0 -16610
  104. package/dist/docs-raw/skills/forgecad-design-spec.md +0 -145
  105. package/dist/docs-raw/skills/forgecad-grade-model.md +0 -84
  106. package/dist/docs-raw/skills/forgecad-inspect-model.md +0 -80
  107. package/dist/docs-raw/skills/forgecad-verify-mujoco.md +0 -78
  108. package/dist-skill/library/forgecad-design-spec/SKILL.md +0 -132
  109. package/dist-skill/library/forgecad-design-spec/references/default-profiles.md +0 -99
  110. package/dist-skill/library/forgecad-design-spec/references/master-prompt.md +0 -73
  111. package/dist-skill/library/forgecad-grade-model/SKILL.md +0 -72
  112. package/dist-skill/library/forgecad-grade-model/agents/openai.yaml +0 -4
  113. package/dist-skill/library/forgecad-inspect-model/SKILL.md +0 -68
  114. package/dist-skill/library/forgecad-verify-mujoco/SKILL.md +0 -66
  115. package/dist-skill/website/skills/forgecad-design-spec.md +0 -145
  116. package/dist-skill/website/skills/forgecad-grade-model.md +0 -84
  117. package/dist-skill/website/skills/forgecad-inspect-model.md +0 -80
  118. package/dist-skill/website/skills/forgecad-verify-mujoco.md +0 -78
  119. /package/dist/assets/{landing-proof-driven-BxZZh5r5.js → landing-proof-driven-DNPRKL_p.js} +0 -0
  120. /package/dist-skill/library/{forgecad-verify-mujoco → forgecad-build-model}/scripts/mujoco_verify.py +0 -0
  121. /package/dist-skill/library/{forgecad-inspect-model → forgecad-build-model/scripts}/summarize_manifest.py +0 -0
@@ -0,0 +1,37 @@
1
+ // Full round and face fillet — two face-driven blends. Both require OCCT.
2
+ const length = param('Length', 90);
3
+ const railWidth = param('Rail Width', 14);
4
+ const height = param('Height', 30);
5
+ const gap = length * 0.75;
6
+
7
+ // A bar whose narrow top face ("top") sits between the two long side faces.
8
+ function bar() {
9
+ return box(length, railWidth, height);
10
+ }
11
+
12
+ // Full round: roll a blend over the narrow top face so the two sides meet
13
+ // tangentially. The radius defaults to half the center-face span.
14
+ const rounded = Blend.FullRound({
15
+ shape: bar(),
16
+ centerFace: bar().face('top'),
17
+ })
18
+ .translate(-gap, 0, 0)
19
+ .color('#9ed892');
20
+
21
+ // Face fillet: blend every edge shared by the top face and one side face.
22
+ const seed = bar();
23
+ const faceFilleted = Blend.Face({
24
+ shape: seed,
25
+ faces: [seed.face('top'), seed.face('side-right')],
26
+ radius: 5,
27
+ })
28
+ .translate(gap, 0, 0)
29
+ .color('#e8c06a');
30
+
31
+ verify.noSelfIntersection('Full round is a valid solid', rounded);
32
+ verify.noSelfIntersection('Face fillet is a valid solid', faceFilleted);
33
+
34
+ return [
35
+ { name: 'Full Round (top consumed)', shape: rounded },
36
+ { name: 'Face Fillet (top x side-right)', shape: faceFilleted },
37
+ ];
@@ -0,0 +1,51 @@
1
+ // Variable-radius edge fillet — the blend tapers along the edge instead of
2
+ // holding a single radius. Requires the OCCT backend.
3
+ const width = param('Width', 80);
4
+ const depth = param('Depth', 40);
5
+ const height = param('Height', 24);
6
+ const gap = width * 0.95;
7
+
8
+ function block() {
9
+ return box(width, depth, height);
10
+ }
11
+
12
+ // Constant radius — the reference.
13
+ const constant = Blend.Edge({
14
+ shape: block(),
15
+ edges: [block().edge('top-right')],
16
+ radius: 4,
17
+ })
18
+ .translate(-gap, 0, 0)
19
+ .color('#8aa0c8');
20
+
21
+ // Linear taper from 1mm at u=0 to 8mm at u=1 along the same edge.
22
+ const tapered = Blend.Edge({
23
+ shape: block(),
24
+ edges: [block().edge('top-right')],
25
+ variableRadius: { start: 1, end: 8 },
26
+ })
27
+ .color('#9ed892');
28
+
29
+ // Station law — a bulge in the middle of the edge.
30
+ const bulged = Blend.Edge({
31
+ shape: block(),
32
+ edges: [block().edge('top-right')],
33
+ variableRadius: {
34
+ stations: [
35
+ { at: 0, radius: 1 },
36
+ { at: 0.5, radius: 7 },
37
+ { at: 1, radius: 1 },
38
+ ],
39
+ },
40
+ })
41
+ .translate(gap, 0, 0)
42
+ .color('#e8c06a');
43
+
44
+ verify.noSelfIntersection('Tapered fillet is a valid solid', tapered);
45
+ verify.noSelfIntersection('Bulged fillet is a valid solid', bulged);
46
+
47
+ return [
48
+ { name: 'Constant r=4', shape: constant },
49
+ { name: 'Linear taper 1->8', shape: tapered },
50
+ { name: 'Station bulge 1-7-1', shape: bulged },
51
+ ];
@@ -0,0 +1,59 @@
1
+ // Projected curve & surface-surface intersection curve on Surface.Net sheets.
2
+ //
3
+ // Curve.ProjectOnSurface drapes a planned guide line onto a freeform panel along
4
+ // the surface normal; the foot curve becomes a real on-surface seam bead.
5
+ // Curve.Intersect follows the surface-surface intersection of two sheets and
6
+ // returns the exact NURBS seam where they meet, which we sweep into a weld bead.
7
+
8
+ const span = param('Span', 120);
9
+ const depth = param('Depth', 100);
10
+ const crown = param('Crown', 22);
11
+
12
+ // A gently crowned panel.
13
+ const panelCage = [];
14
+ const M = 6;
15
+ const N = 6;
16
+ for (let i = 0; i <= M; i++) {
17
+ const row = [];
18
+ for (let j = 0; j <= N; j++) {
19
+ const x = (i / M) * span - span / 2;
20
+ const y = (j / N) * depth - depth / 2;
21
+ const z = Math.sin((i / M) * Math.PI) * crown + Math.cos((j / N) * Math.PI) * (crown * 0.4);
22
+ row.push([x, y, z]);
23
+ }
24
+ panelCage.push(row);
25
+ }
26
+ const panel = Surface.Net().cage(panelCage).degree(3, 3);
27
+ const panelSolid = panel.thicken(2).color('#9fd3ff');
28
+
29
+ // 1) Project a straight guide onto the crowned panel → on-surface seam.
30
+ const guide = Curve.Line([-span * 0.4, -depth * 0.2, crown * 2], [span * 0.4, depth * 0.2, crown * 2]);
31
+ const seam = Curve.ProjectOnSurface(guide, panel, { samples: 64, maxGap: crown * 3 });
32
+ const seamBead = sweep(circle2d(0.8), seam).color('#ffd166');
33
+
34
+ // 2) Intersect the panel with a vertical cutting sheet → exact intersection curve.
35
+ const cutCage = [];
36
+ for (let i = 0; i <= 4; i++) {
37
+ const row = [];
38
+ for (let j = 0; j <= 4; j++) {
39
+ const t = i / 4;
40
+ row.push([(t - 0.5) * span * 0.9, (t - 0.5) * depth * 0.4, (j / 4) * crown * 2]);
41
+ }
42
+ cutCage.push(row);
43
+ }
44
+ const cutSheet = Surface.Net().cage(cutCage).degree(3, 1);
45
+
46
+ const branches = Curve.Intersect(panel, cutSheet, { samples: 40 });
47
+ if (branches.length === 0) throw new Error('Expected the panel and cutting sheet to intersect.');
48
+ const weldBeads = branches.map((branch) => sweep(circle2d(0.9), branch).color('#ef476f'));
49
+
50
+ scene({
51
+ background: { top: '#eef3f8', bottom: '#fbfdff' },
52
+ camera: { position: [0, -260, 150], target: [0, 0, 24], fov: 34 },
53
+ });
54
+
55
+ return [
56
+ { name: 'Crowned Panel', shape: panelSolid },
57
+ { name: 'Projected Seam Bead', shape: seamBead },
58
+ ...weldBeads.map((shape, index) => ({ name: `Intersection Weld ${index + 1}`, shape })),
59
+ ];
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Reference-based extrude end conditions.
3
+ *
4
+ * Instead of guessing a numeric height, terminate an extrusion against a real
5
+ * reference: a face on another part, an arbitrary plane, or a vertex. The extent
6
+ * collapses to a numeric distance before the solid is built, so this works on
7
+ * every backend.
8
+ *
9
+ * - `extrude({ upToFace })` — flush with another shape's planar face.
10
+ * - `extrude({ upToPlane, offset })` — to a plane, then a margin past it.
11
+ */
12
+
13
+ scene({
14
+ camera: { position: [120, -140, 90], target: [0, 0, 25], fov: 32 },
15
+ });
16
+
17
+ // A lid sitting above the build plate; its bottom face is the termination target.
18
+ const lid = box(60, 60, 6).translate(0, 0, 40).color('#9aa7b4');
19
+
20
+ // Boss grown from the plate exactly up to the lid's underside (z = 40).
21
+ const boss = circle2d(10).extrude({ upToFace: lid.face('bottom') }).color('#d98c5f');
22
+
23
+ // A post that rises to a plane at z = 30, then 4 mm past it (a press-fit stub).
24
+ const post = rect(10, 10)
25
+ .translate(22, 0)
26
+ .extrude({ upToPlane: { normal: [0, 0, 1], offset: 30 }, offset: 4 })
27
+ .color('#5f9ed9');
28
+
29
+ // A pin that stops at the plane through a vertex.
30
+ const pin = circle2d(4)
31
+ .translate(-22, 0)
32
+ .extrude({ upToVertex: [0, 0, 18] })
33
+ .color('#6fb86f');
34
+
35
+ verify.notEmpty('boss is a solid', boss);
36
+ verify.boundingBoxSize('boss reaches the lid underside (height 40)', boss, [20, 20, 40], 1);
37
+ verify.boundingBoxSize('post stops 4 mm past the z=30 plane', post, [10, 10, 34], 1);
38
+ verify.boundingBoxSize('pin stops at the z=18 vertex plane', pin, [8, 8, 18], 1);
39
+
40
+ return [
41
+ { name: 'extrude up-to references', group: [
42
+ { name: 'Lid', shape: lid },
43
+ { name: 'Boss (upToFace)', shape: boss },
44
+ { name: 'Post (upToPlane + offset)', shape: post },
45
+ { name: 'Pin (upToVertex)', shape: pin },
46
+ ] },
47
+ ];
@@ -0,0 +1,65 @@
1
+ // Editable 2D path parameters: closed outlines and open centerlines.
2
+
3
+ scene({
4
+ camera: { position: [92, -150, 95], target: [4, 2, 6], fov: 35 },
5
+ });
6
+
7
+ const thickness = Param.number('Thickness', 5, { min: 2, max: 12, step: 0.5, unit: 'mm' });
8
+ const cornerRadius = Param.number('Corner Radius', 4, { min: 0, max: 12, step: 0.5, unit: 'mm' });
9
+ const railWidth = Param.number('Rail Width', 4, { min: 1, max: 10, step: 0.5, unit: 'mm' });
10
+
11
+ const outline = Param.path2d(
12
+ 'Bracket Outline',
13
+ [
14
+ [-44, -22],
15
+ [30, -22],
16
+ [44, -6],
17
+ [35, 24],
18
+ [4, 34],
19
+ [-34, 22],
20
+ [-52, 0],
21
+ ],
22
+ {
23
+ closed: true,
24
+ minPoints: 4,
25
+ maxPoints: 12,
26
+ unit: 'mm',
27
+ x: { min: -70, max: 70, step: 0.5 },
28
+ y: { min: -45, max: 45, step: 0.5 },
29
+ anchor: Param.anchor.sheetOnXY([0, 0, 10], { label: 'Bracket outline' }),
30
+ },
31
+ );
32
+
33
+ const rail = Param.path2d(
34
+ 'Cable Rail Centerline',
35
+ [
36
+ [-34, -6],
37
+ [-10, 8],
38
+ [18, 10],
39
+ [34, -2],
40
+ ],
41
+ {
42
+ closed: false,
43
+ minPoints: 2,
44
+ maxPoints: 8,
45
+ unit: 'mm',
46
+ x: { min: -60, max: 60, step: 0.5 },
47
+ y: { min: -35, max: 35, step: 0.5 },
48
+ anchor: Param.anchor.sheetOnXY([0, 0, 14], { label: 'Cable rail centerline', color: '#3c8f83' }),
49
+ },
50
+ );
51
+
52
+ const plateSketch = outline
53
+ .toSketch()
54
+ .filletCorners(cornerRadius)
55
+ .subtract(circle2d(4).translate(-30, 0))
56
+ .subtract(circle2d(4).translate(22, 2));
57
+
58
+ const plate = plateSketch.extrude(thickness).color('#68737c').material({ roughness: 0.4, metalness: 0.2 });
59
+ const cableRail = rail.toStroke(railWidth, 'Round').extrude(1.6).translate(0, 0, thickness + 0.8).color('#3c8f83');
60
+
61
+ verify.notEmpty('path2d outline creates a solid plate', plate);
62
+ verify.notEmpty('path2d centerline creates a raised rail', cableRail);
63
+ verify.boundingBoxSize('path2d bracket stays in expected scale', plate, [96, 56, 5], 18);
64
+
65
+ return [plate, cableRail];
@@ -0,0 +1,80 @@
1
+ // Editable 2D placement sheets: named semantic blocks inside constrained zones.
2
+
3
+ scene({
4
+ camera: { position: [116, -150, 104], target: [0, 0, 9], fov: 35 },
5
+ });
6
+
7
+ const FRAME_W = 126;
8
+ const FRAME_D = 82;
9
+ const FRAME_RADIUS = 7;
10
+ const RIM_W = 5;
11
+ const RIM_H = 6;
12
+ const FLOOR_H = 2;
13
+ const COMPONENT_H = 3;
14
+
15
+ const layout = Param.placement2d('Internal Layout', {
16
+ frame: { size: [FRAME_W, FRAME_D] },
17
+ zones: [
18
+ { id: 'electronics', label: 'Electronics', size: [72, 70], center: [-24, 0] },
19
+ { id: 'service', label: 'Service', size: [42, 70], center: [38, 0] },
20
+ ],
21
+ items: [
22
+ { id: 'pcb', label: 'PCB', footprint: { type: 'rect', size: [48, 30] }, zone: 'electronics', at: [-28, 6] },
23
+ { id: 'battery', label: 'Battery', footprint: { type: 'rect', size: [38, 18] }, zone: 'electronics', at: [-22, -24], angle: 0 },
24
+ { id: 'speaker', label: 'Speaker', footprint: { type: 'circle', radius: 12 }, zone: 'service', at: [39, 18] },
25
+ { id: 'usb', label: 'USB', footprint: { type: 'rect', size: [18, 10] }, zone: 'service', at: [40, -24] },
26
+ ],
27
+ rules: { bounds: 'prevent', collisions: 'warn', snap: 1 },
28
+ unit: 'mm',
29
+ anchor: Param.anchor.sheetOnXY([0, 0, 14], { label: 'Internal layout', color: '#c77dff' }),
30
+ });
31
+
32
+ function placeRectItem(itemId, size, color) {
33
+ const item = layout.item(itemId);
34
+ return box(size[0], size[1], COMPONENT_H)
35
+ .rotateZ(item.angle)
36
+ .translate(item.x, item.y, FLOOR_H)
37
+ .color(color);
38
+ }
39
+
40
+ function placeRoundItem(itemId, radius, color) {
41
+ const item = layout.item(itemId);
42
+ return cylinder(COMPONENT_H, radius)
43
+ .translate(item.x, item.y, FLOOR_H)
44
+ .color(color);
45
+ }
46
+
47
+ const trayRim = roundedRect(FRAME_W + 2 * RIM_W, FRAME_D + 2 * RIM_W, FRAME_RADIUS + RIM_W)
48
+ .subtract(roundedRect(FRAME_W, FRAME_D, FRAME_RADIUS))
49
+ .extrude(RIM_H)
50
+ .color('#38414a');
51
+
52
+ const floor = roundedRect(FRAME_W, FRAME_D, FRAME_RADIUS).extrude(FLOOR_H).color('#232a31');
53
+ const pcb = placeRectItem('pcb', [48, 30], '#3f8f5c');
54
+ const battery = placeRectItem('battery', [38, 18], '#5a6470');
55
+ const usb = placeRectItem('usb', [18, 10], '#d69b45');
56
+ const speaker = placeRoundItem('speaker', 12, '#4d93bf');
57
+ const placedItems = [pcb, battery, usb, speaker];
58
+
59
+ function sitsOnFloor(shape) {
60
+ return Math.abs(shape.boundingBox().min[2] - FLOOR_H) < 0.001;
61
+ }
62
+
63
+ function staysInsideFrame(shape) {
64
+ const bounds = shape.boundingBox();
65
+ return (
66
+ bounds.min[0] >= -FRAME_W / 2 &&
67
+ bounds.max[0] <= FRAME_W / 2 &&
68
+ bounds.min[1] >= -FRAME_D / 2 &&
69
+ bounds.max[1] <= FRAME_D / 2
70
+ );
71
+ }
72
+
73
+ verify.notEmpty('placement2d tray has rim', trayRim);
74
+ verify.that('placement2d returns named placement data', () => layout.item('pcb').zone === 'electronics');
75
+ verify.that('placement2d components share the floor plane', () => placedItems.every(sitsOnFloor));
76
+ verify.that('placement2d components stay below the tray rim', () => placedItems.every((shape) => shape.boundingBox().max[2] <= RIM_H));
77
+ verify.that('placement2d components stay inside the frame', () => placedItems.every(staysInsideFrame));
78
+ verify.boundingBoxSize('placement2d layout stays in tray scale', union(trayRim, floor, pcb, battery, usb, speaker), [136, 92, 6], 0.5);
79
+
80
+ return [trayRim, floor, pcb, battery, usb, speaker];
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Editable 2D spline parameter with per-node G continuity selection.
3
+ *
4
+ * The smooth spine can be emitted as one smooth curve. The groove rail has
5
+ * a G0 node, so it is emitted as a joined sweep path that preserves the hard
6
+ * break while keeping neighboring spans smooth.
7
+ */
8
+
9
+ scene({
10
+ camera: { position: [88, -142, 82], target: [32, 0, 8], fov: 36 },
11
+ });
12
+
13
+ const spine = Param.spline2d(
14
+ 'Handle Spine',
15
+ [
16
+ { x: 0, y: 0, g: 'G2' },
17
+ { x: 24, y: 9, g: 'G2' },
18
+ { x: 58, y: 7, g: 'G2' },
19
+ { x: 92, y: -2, g: 'G1' },
20
+ ],
21
+ {
22
+ unit: 'mm',
23
+ degree: 3,
24
+ x: { min: -10, max: 110, step: 1 },
25
+ y: { min: -18, max: 22, step: 0.5 },
26
+ anchor: Param.anchor.sheetOnXZ([48, -28, 0], { label: 'Handle spine' }),
27
+ },
28
+ );
29
+
30
+ const groove = Param.spline2d(
31
+ 'Index Groove Rail',
32
+ [
33
+ { x: 10, y: -12, g: 'G2' },
34
+ { x: 24, y: -4, g: 'G1' },
35
+ { x: 38, y: -11, g: 'G0' },
36
+ { x: 56, y: -2, g: 'G1' },
37
+ { x: 78, y: -8, g: 'G2' },
38
+ ],
39
+ {
40
+ unit: 'mm',
41
+ degree: 3,
42
+ x: { min: -10, max: 110, step: 1 },
43
+ y: { min: -18, max: 22, step: 0.5 },
44
+ anchor: Param.anchor.sheetOnXY([48, 0, 11], { label: 'Index groove rail', color: '#4d93bf' }),
45
+ },
46
+ );
47
+
48
+ const palmPad = roundedRect(108, 22, 11).extrude(4).translate(50, 0, -3).color('#2b3038');
49
+ const raisedSpine = sweep(circle2d(2.2), spine.toCurveOnXZ(0), { samples: 56, up: [0, 0, 1] }).color('#e0a94b');
50
+ const grooveBead = sweep(circle2d(0.9), groove.toPathOnXY(7, { samples: 18 }), { up: [0, 0, 1] }).color('#4d93bf');
51
+
52
+ verify.notEmpty('spline2d smooth spine creates a raised sweep', raisedSpine);
53
+ verify.notEmpty('spline2d G0 rail creates a groove bead sweep', grooveBead);
54
+ verify.that('spline2d keeps per-point continuity metadata', () => groove.nodes()[2].g === 'G0');
55
+ verify.boundingBoxSize('editable spline handle stays in expected scale', union(palmPad, raisedSpine, grooveBead), [112, 25, 27], 18);
56
+
57
+ return [palmPad, raisedSpine, grooveBead];
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Spoon — v4 smooth-clamp metal bowl + full-tang polymer handle.
3
+ *
4
+ * Built like a full-tang kitchen knife, in two materials:
5
+ * - METAL HEAD: the smooth-clamp dished bowl (see the spoon tutorial / step5_v4),
6
+ * flowing into a flat steel TANG that runs the whole length of the handle.
7
+ * - POLYMER GRIP: a comfortable rounded handle (variableSweep) that is SPLIT by the
8
+ * tang into a top + bottom scale, so the steel tang reads as a liner line around
9
+ * the handle's equator — the classic full-tang look.
10
+ * - Exposed steel BOLSTER where the bowl meets the handle, a steel BUTT at the end,
11
+ * and three steel RIVETS pinning the scales to the tang.
12
+ *
13
+ * Two materials are returned as a colored group (steel + dark polymer).
14
+ *
15
+ * The bowl recipe is the key idea from the build: a smooth-clamped parabola
16
+ * f(s) = s / sqrt(1 + (s/S)^2), s = (y/hw)^2
17
+ * which is a pure (round) parabola in the visible bowl but C-infinity smooth, so the
18
+ * wall has no kink to ring around — the rim stays glassy, not wavy.
19
+ */
20
+
21
+ scene({
22
+ camera: { position: [95, -260, 250], target: [20, 2, -6], fov: 36 },
23
+ });
24
+
25
+ // ───────────────────────── 1. the v4 metal bowl ─────────────────────────
26
+ const H = 5.5; // rim level
27
+ const S = 5.0; // smooth-clamp knee
28
+
29
+ const bowlHalfWidth = Param.spline2d(
30
+ 'Bowl Half Width',
31
+ [
32
+ { x: -92, y: 0, g: 'G2' },
33
+ { x: -90.5, y: 12, g: 'G2' },
34
+ { x: -88, y: 17.5, g: 'G2' },
35
+ { x: -82, y: 22.5, g: 'G2' },
36
+ { x: -67, y: 28, g: 'G2' },
37
+ { x: -48, y: 32, g: 'G2' },
38
+ { x: -28, y: 29, g: 'G2' },
39
+ { x: -6, y: 13, g: 'G2' },
40
+ { x: 12, y: 8.4, g: 'G2' },
41
+ { x: 24, y: 7.1, g: 'G1' },
42
+ ],
43
+ {
44
+ unit: 'mm',
45
+ degree: 3,
46
+ minPoints: 6,
47
+ maxPoints: 16,
48
+ x: { min: -100, max: 30, step: 0.5 },
49
+ y: { min: 0, max: 38, step: 0.5 },
50
+ anchor: Param.anchor.sheetOnXY([-48, 0, H + 16], { label: 'Bowl width curve', color: '#5da9e9' }),
51
+ },
52
+ );
53
+
54
+ const bowlLowZ = Param.spline2d(
55
+ 'Bowl Low Z',
56
+ [
57
+ { x: -92, y: 5.0, g: 'G2' },
58
+ { x: -90.5, y: 2.0, g: 'G2' },
59
+ { x: -88, y: -0.5, g: 'G2' },
60
+ { x: -82, y: -4.0, g: 'G2' },
61
+ { x: -67, y: -8.5, g: 'G2' },
62
+ { x: -48, y: -10.2, g: 'G2' },
63
+ { x: -28, y: -8.4, g: 'G2' },
64
+ { x: -6, y: -2.0, g: 'G2' },
65
+ { x: 12, y: 2.5, g: 'G2' },
66
+ { x: 24, y: 4.0, g: 'G1' },
67
+ ],
68
+ {
69
+ unit: 'mm',
70
+ degree: 3,
71
+ minPoints: 6,
72
+ maxPoints: 16,
73
+ x: { min: -100, max: 30, step: 0.5 },
74
+ y: { min: -13, max: 6, step: 0.25 },
75
+ anchor: Param.anchor.sheetOnXZ([-48, -44, 0], { label: 'Bowl depth curve', color: '#46b6a9' }),
76
+ },
77
+ );
78
+
79
+ function smoothField(curve) {
80
+ const pts = curve.toPathOnXY(0, { samples: 600 }).sort((a, b) => a[0] - b[0]);
81
+ return (x) => {
82
+ if (x <= pts[0][0]) return pts[0][1];
83
+ if (x >= pts[pts.length - 1][0]) return pts[pts.length - 1][1];
84
+ for (let i = 1; i < pts.length; i += 1) {
85
+ if (x <= pts[i][0]) {
86
+ const dx = pts[i][0] - pts[i - 1][0];
87
+ if (Math.abs(dx) < 1e-9) return pts[i][1];
88
+ const t = (x - pts[i - 1][0]) / dx;
89
+ return pts[i - 1][1] + (pts[i][1] - pts[i - 1][1]) * t;
90
+ }
91
+ }
92
+ return pts[pts.length - 1][1];
93
+ };
94
+ }
95
+ const lowAt = smoothField(bowlLowZ);
96
+ const hwAt = smoothField(bowlHalfWidth);
97
+
98
+ function dishZ(x, y) {
99
+ const hw = hwAt(x);
100
+ const lz = lowAt(x);
101
+ if (hw < 1e-6) return lz + (H - lz) * S;
102
+ const s = (y / hw) * (y / hw);
103
+ const f = s / Math.sqrt(1 + (s / S) * (s / S)); // smooth-clamp parabola
104
+ return lz + (H - lz) * f;
105
+ }
106
+
107
+ const X0 = -97, X1 = 28, Y0 = -40, Y1 = 40;
108
+ const NX = 52, NY = 38;
109
+ const grid = [];
110
+ for (let i = 0; i < NX; i += 1) {
111
+ const x = X0 + ((X1 - X0) * i) / (NX - 1);
112
+ const row = [];
113
+ for (let j = 0; j < NY; j += 1) {
114
+ const y = Y0 + ((Y1 - Y0) * j) / (NY - 1);
115
+ row.push([x, y, dishZ(x, y)]);
116
+ }
117
+ grid.push(row);
118
+ }
119
+
120
+ const bowl = Surface.Net()
121
+ .cage(grid)
122
+ .degree(3, 3)
123
+ .toSheet()
124
+ .thicken(1.2, { resolution: 160 })
125
+ .trimByPlane([0, 0, -1], -H); // exact level rim
126
+
127
+ // ───────────────────────── 2. the rounded grip volume ─────────────────────────
128
+ const spine = Curve.Fit(
129
+ [
130
+ [16, 0, 4.0],
131
+ [34, 0, 4.4],
132
+ [74, 0, 4.2],
133
+ [110, 0, 3.5],
134
+ ],
135
+ { tolerance: 0.02 },
136
+ );
137
+
138
+ const gripProfile = (width, thickness) => roundedRect(width, thickness, thickness / 2);
139
+
140
+ const grip = variableSweep(
141
+ spine,
142
+ [
143
+ { t: 0.0, profile: gripProfile(15.0, 7.0) },
144
+ { t: 0.16, profile: gripProfile(16.6, 9.0) },
145
+ { t: 0.62, profile: gripProfile(13.6, 9.8) },
146
+ { t: 1.0, profile: gripProfile(16.2, 9.2) },
147
+ ],
148
+ { edgeLength: 0.65, samples: 90, up: [0, 0, 1] },
149
+ );
150
+
151
+ // ───────────────────────── 3. split into tang + scales ─────────────────────────
152
+ const TANG_Z = 4.0; // grip equator
153
+ const TANG_T = 3.2; // tang thickness
154
+ const tangSlab = box(240, 70, TANG_T).translate(20, 0, TANG_Z); // flat slab at the equator
155
+
156
+ // Tang = grip ∩ slab (flush to the grip outline), fused into the bowl → metal head.
157
+ const tang = grip.intersect(tangSlab);
158
+ const metalHead = union(bowl, tang);
159
+
160
+ // Polymer scales = grip − slab, with the bolster (front) and butt (back) trimmed off
161
+ // so the steel shows there.
162
+ const bolsterCut = box(60, 70, 60).translate(2, 0, TANG_Z); // expose metal for x < 32
163
+ const buttCut = box(30, 70, 60).translate(121, 0, TANG_Z); // expose metal for x > 106
164
+ const scales = grip.subtract(tangSlab).subtract(bolsterCut).subtract(buttCut);
165
+
166
+ // ───────────────────────── 4. rivets ─────────────────────────
167
+ const rivetXs = [46, 72, 96];
168
+ const rivets = rivetXs.map((rx) =>
169
+ cylinder(14, 1.7).translate(rx, 0, TANG_Z - 7), // cylinder(height, radius); base at 0, along Z
170
+ );
171
+
172
+ // ───────────────────────── 5. materials & assembly ─────────────────────────
173
+ const steel = (shape) => shape.color('#c9d0d8').material({ roughness: 0.22, metalness: 0.86 });
174
+ const polymer = (shape) => shape.color('#141619').material({ roughness: 0.55, metalness: 0.0 });
175
+
176
+ const wholeForMeasure = union(metalHead, scales, ...rivets);
177
+
178
+ verify.notEmpty('metal head (bowl + tang) is solid', metalHead);
179
+ verify.notEmpty('polymer scales are solid', scales);
180
+ verify.boundingBoxSize('full-tang spoon keeps its proportions', wholeForMeasure, [203, 69, 23], 16);
181
+ verify.lessThan('mid-bowl is deeper than the neck', dishZ(-48, 0), dishZ(24, 0) - 8);
182
+ verify.greaterThan('bowl has real spoon width', hwAt(-48) * 2, 50);
183
+
184
+ return [
185
+ steel(metalHead),
186
+ polymer(scales),
187
+ ...rivets.map(steel),
188
+ ];
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Smooth closed-rim dished bowl built with ONLY the Boundary Surface primitive.
3
+ *
4
+ * This is the headline case for `Surface.BoundaryNet()`'s closed/periodic form:
5
+ * a dished bowl whose around-rim seam must be tangent-continuous (no G0 kink).
6
+ *
7
+ * Construction (a class-A "boundary surface" cage):
8
+ * - U runs radially, rim -> center (rings of decreasing radius, dishing down).
9
+ * - V runs around the rim; the first and last V columns coincide so the loop
10
+ * closes in position.
11
+ * - `.closedV()` welds those two ends into one smooth seam, so the surface has
12
+ * NO crease where the around-rim parameterization wraps.
13
+ *
14
+ * The innermost ring is a small non-degenerate disk (not a single pole), so the
15
+ * floor rounds over cleanly and `.thicken()` stays watertight.
16
+ */
17
+
18
+ scene({
19
+ camera: { position: [140, -150, 120], target: [0, 0, -14], fov: 32 },
20
+ views: {
21
+ iso: { position: [140, -150, 120], target: [0, 0, -14], fov: 32 },
22
+ top: { position: [0, 0, 230], target: [0, 0, -12], fov: 32 },
23
+ },
24
+ });
25
+
26
+ const RIM_RADIUS = 60;
27
+ const DEPTH = 22; // dish depth at the floor (down -z)
28
+ const AROUND = 48; // around-rim samples (V); seam wraps here
29
+ const RINGS = 10; // radial rings rim -> center (U)
30
+ const CENTER_SCALE = 0.02; // innermost ring radius as a fraction of the rim (near-closed floor, no hard pole)
31
+
32
+ // Radial profile: round dished bottom. t = 0 at rim, 1 at center.
33
+ // radius shrinks to the floor; z dips so the rim tangent rolls in and the floor
34
+ // is flat (cosine dish: flat tangent at BOTH the rim and the floor -> no throat).
35
+ function ringRadius(t) {
36
+ return RIM_RADIUS * (CENTER_SCALE + (1 - CENTER_SCALE) * (1 - t));
37
+ }
38
+ function ringDepth(t) {
39
+ return -DEPTH * 0.5 * (1 - Math.cos(Math.PI * t));
40
+ }
41
+
42
+ // Build the (RINGS x AROUND+1) cage. Each row is a ring; columns go around.
43
+ // Column 0 and column AROUND are the SAME point so the V loop closes.
44
+ const cage = [];
45
+ for (let r = 0; r < RINGS; r++) {
46
+ const t = r / (RINGS - 1);
47
+ const radius = ringRadius(t);
48
+ const z = ringDepth(t);
49
+ const ring = [];
50
+ for (let a = 0; a <= AROUND; a++) {
51
+ const angle = (a / AROUND) * 2 * Math.PI; // a=0 and a=AROUND coincide
52
+ ring.push([radius * Math.cos(angle), radius * Math.sin(angle), z]);
53
+ }
54
+ cage.push(ring);
55
+ }
56
+
57
+ // The Boundary Surface: Gordon fill of the cage, with the around-rim seam welded
58
+ // into a tangent-continuous loop via the closed/periodic form.
59
+ const bowl = Surface.BoundaryNet().cage(cage).degree(3, 3).closedV().thicken(1.2);
60
+
61
+ bowl.color('#b9c4cc');
62
+
63
+ return bowl;