forgecad 0.10.5 → 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 (104) hide show
  1. package/dist/assets/{AdminPage-raksfnNA.js → AdminPage-B1nIvqLS.js} +1 -1
  2. package/dist/assets/{BenchmarkPage-DP3RxhPs.js → BenchmarkPage-YZJbw5nd.js} +1 -1
  3. package/dist/assets/{BlogPage-D7Dos-vl.js → BlogPage-DIWRApKS.js} +1 -1
  4. package/dist/assets/{DocsPage-DO1kvBns.js → DocsPage-ClL6X1hR.js} +2 -22
  5. package/dist/assets/{EditorApp-DQJmcmRT.js → EditorApp-CYBDvSyT.js} +575 -119
  6. package/dist/assets/{EmbedViewer-DFDUhOma.js → EmbedViewer-Dmfu_LIw.js} +2 -2
  7. package/dist/assets/{LandingPageProofDriven-DbE_tp8-.js → LandingPageProofDriven-XYTiYxfM.js} +1 -1
  8. package/dist/assets/{LegalPage-CominSso.js → LegalPage-D5Z3CscF.js} +1 -1
  9. package/dist/assets/{PricingPage-CcVIN9yj.js → PricingPage-BP4lIGio.js} +1 -1
  10. package/dist/assets/{SettingsPage-DLWcP289.js → SettingsPage-D3bcPBsC.js} +1 -1
  11. package/dist/assets/{app-xW3hOdq9.js → app-BKjogwIZ.js} +2192 -231
  12. package/dist/assets/{backendInit-mDHk97u7.js → backendInit-6a9-ilom.js} +76448 -75066
  13. package/dist/assets/cli/{render--SIU27W_.js → render-CMNudGb0.js} +3 -3
  14. package/dist/assets/{constructionHistoryWorker-uEe_Q7Kg.js → constructionHistoryWorker-BuZgc606.js} +6985 -6706
  15. package/dist/assets/{evalWorker-BqyDHDcI.js → evalWorker-DQ82ueGu.js} +40862 -39497
  16. package/dist/assets/{inspectWorker-UXMxlcR8.js → inspectWorker-Cuby2qfT.js} +2078 -478
  17. package/dist/assets/{jointPose-bYMlwU3v.js → jointPose-CFql5I-u.js} +1 -1
  18. package/dist/assets/{manifold-CyOV5B9S.js → manifold-02pmr7O7.js} +2 -2
  19. package/dist/assets/{manifold-BR7UYI4P.js → manifold-C6KU0oII.js} +1 -1
  20. package/dist/assets/{manifold-D4d5NQst.js → manifold-P1yF3GKn.js} +1 -1
  21. package/dist/assets/{reportWorker-DsaICZsn.js → reportWorker-kg065BVL.js} +85183 -78309
  22. package/dist/cli/render.html +1 -1
  23. package/dist/docs/index.html +2 -2
  24. package/dist/docs-raw/AI/usage.md +6 -8
  25. package/dist/docs-raw/CLI.md +10 -10
  26. package/dist/docs-raw/component-model.md +28 -9
  27. package/dist/docs-raw/generated/concepts.md +13 -4
  28. package/dist/docs-raw/generated/core.md +244 -56
  29. package/dist/docs-raw/generated/curves.md +13 -0
  30. package/dist/docs-raw/generated/runtime-names.md +2 -2
  31. package/dist/docs-raw/guides/inspection-bundles.md +1 -1
  32. package/dist/docs-raw/guides/structural-fea.md +11 -0
  33. package/dist/docs-raw/skills/forgecad-build-model.md +70 -147
  34. package/dist/docs-raw/skills/forgecad-image-prompt.md +1 -1
  35. package/dist/docs-raw/skills/forgecad-project-sync.md +3 -3
  36. package/dist/docs-raw/skills/forgecad-reconstruct-cad-file.md +2 -2
  37. package/dist/docs-raw/skills/forgecad-reconstruct-from-images.md +4 -5
  38. package/dist/docs-raw/skills/forgecad.md +3 -1
  39. package/dist/docs-raw/skills/index.md +1 -5
  40. package/dist/docs-raw/welcome.md +3 -4
  41. package/dist/index.html +1 -1
  42. package/dist/llms.txt +1 -2
  43. package/dist/sitemap.xml +15 -15
  44. package/dist-cli/{check-compiler-7YAHVXYM.js → check-compiler-UJWUEIDC.js} +1 -1
  45. package/dist-cli/{check-query-propagation-ZRR6IOJW.js → check-query-propagation-O2EPDJSY.js} +1 -1
  46. package/dist-cli/{chunk-VNM67DIV.js → chunk-MNDROM7T.js} +77145 -75767
  47. package/dist-cli/forgecad.js +1145 -441
  48. package/dist-skill/CONTEXT.md +429 -64
  49. package/dist-skill/SKILL.md +3 -1
  50. package/dist-skill/docs/API/core/concepts.md +31 -4
  51. package/dist-skill/docs/CLI.md +10 -10
  52. package/dist-skill/docs/generated/core.md +240 -57
  53. package/dist-skill/docs/generated/curves.md +13 -0
  54. package/dist-skill/docs/generated/runtime-names.md +2 -2
  55. package/dist-skill/docs/guides/inspection-bundles.md +1 -1
  56. package/dist-skill/docs/guides/manual-parameters.md +130 -0
  57. package/dist-skill/docs/guides/structural-fea.md +11 -0
  58. package/dist-skill/library/README.md +0 -4
  59. package/dist-skill/library/forgecad-build-model/SKILL.md +57 -150
  60. package/dist-skill/library/forgecad-build-model/references/inspection-feedback.md +58 -0
  61. package/dist-skill/library/forgecad-build-model/references/module-contracts.md +53 -0
  62. package/dist-skill/library/forgecad-build-model/references/parameter-controls.md +22 -0
  63. package/dist-skill/library/forgecad-build-model/references/readiness-review.md +43 -0
  64. package/dist-skill/library/forgecad-build-model/references/simulation-feedback.md +49 -0
  65. package/dist-skill/library/forgecad-build-model/references/stage-1-design-intent.md +21 -0
  66. package/dist-skill/library/forgecad-build-model/references/stage-2-architecture-plan.md +23 -0
  67. package/dist-skill/library/forgecad-build-model/references/stage-3-build-slices.md +39 -0
  68. package/dist-skill/library/forgecad-build-model/references/stage-4-feedback-iteration.md +24 -0
  69. package/dist-skill/library/forgecad-build-model/references/stage-5-readiness-package.md +34 -0
  70. package/dist-skill/library/forgecad-image-prompt/SKILL.md +1 -1
  71. package/dist-skill/library/forgecad-project-sync/SKILL.md +3 -3
  72. package/dist-skill/library/forgecad-reconstruct-cad-file/SKILL.md +2 -2
  73. package/dist-skill/library/forgecad-reconstruct-from-images/SKILL.md +4 -5
  74. package/dist-skill/website/skills/forgecad-build-model.md +70 -147
  75. package/dist-skill/website/skills/forgecad-image-prompt.md +1 -1
  76. package/dist-skill/website/skills/forgecad-project-sync.md +3 -3
  77. package/dist-skill/website/skills/forgecad-reconstruct-cad-file.md +2 -2
  78. package/dist-skill/website/skills/forgecad-reconstruct-from-images.md +4 -5
  79. package/dist-skill/website/skills/forgecad.md +3 -1
  80. package/dist-skill/website/skills/index.md +1 -5
  81. package/examples/api/param-path2d.forge.js +65 -0
  82. package/examples/api/param-placement2d.forge.js +80 -0
  83. package/examples/api/param-spline2d-g-continuity.forge.js +57 -0
  84. package/examples/api/spoon-full-tang-handle.forge.js +57 -17
  85. package/examples/api/surface-variable-thickness-panel.forge.js +62 -0
  86. package/examples/mechanical/airplane-propeller.forge.js +81 -28
  87. package/package.json +2 -2
  88. package/dist/docs-raw/skills/forgecad-design-spec.md +0 -145
  89. package/dist/docs-raw/skills/forgecad-grade-model.md +0 -84
  90. package/dist/docs-raw/skills/forgecad-inspect-model.md +0 -80
  91. package/dist/docs-raw/skills/forgecad-verify-mujoco.md +0 -78
  92. package/dist-skill/library/forgecad-design-spec/SKILL.md +0 -132
  93. package/dist-skill/library/forgecad-design-spec/references/default-profiles.md +0 -99
  94. package/dist-skill/library/forgecad-design-spec/references/master-prompt.md +0 -73
  95. package/dist-skill/library/forgecad-grade-model/SKILL.md +0 -72
  96. package/dist-skill/library/forgecad-grade-model/agents/openai.yaml +0 -4
  97. package/dist-skill/library/forgecad-inspect-model/SKILL.md +0 -68
  98. package/dist-skill/library/forgecad-verify-mujoco/SKILL.md +0 -66
  99. package/dist-skill/website/skills/forgecad-design-spec.md +0 -145
  100. package/dist-skill/website/skills/forgecad-grade-model.md +0 -84
  101. package/dist-skill/website/skills/forgecad-inspect-model.md +0 -80
  102. package/dist-skill/website/skills/forgecad-verify-mujoco.md +0 -78
  103. /package/dist-skill/library/{forgecad-verify-mujoco → forgecad-build-model}/scripts/mujoco_verify.py +0 -0
  104. /package/dist-skill/library/{forgecad-inspect-model → forgecad-build-model/scripts}/summarize_manifest.py +0 -0
@@ -13,12 +13,12 @@ Manage hosted ForgeCAD project sync from the CLI: init, clone, pull, push, file
13
13
 
14
14
  ## Project Sync
15
15
 
16
- forgecad.io is the hosted ForgeCAD platform; a project is a local folder linked to the server by `forgecad.json`. The full command inventory (project, file, member, share, token commands and flags) lives in `forgecad project --help` and the forgecad skill's `docs/CLI.md` — do not relearn it here.
16
+ forgecad.io is the hosted ForgeCAD platform; a project is a local folder marked by `forgecad.json`, and linked to the server after its first push. The full command inventory (project, file, member, share, token commands and flags) lives in `forgecad project --help` and the forgecad skill's `docs/CLI.md` — do not relearn it here.
17
17
 
18
18
  - **One studio process.** Run a single long-running `forgecad studio <folder> [<folder> ...]` naming every active project folder. The user opens the one printed localhost port once; create and edit files only under those folders so the browser live-updates. Never spawn extra servers per project.
19
19
  - **studio vs dev.** `forgecad studio` is for users and agents; `forgecad dev` is only for developing ForgeCAD itself.
20
- - **Login only for hosted commands.** `forgecad login` before any `project`/`publish` command; local work (run, render, studio) needs no auth.
21
- - **init creates, push syncs.** `project init "Name"` creates the remote project, writes `forgecad.json`, and pushes existing local files. `push` only syncs an already-initialized project. `clone <slug>` is the inverse: remote → new local folder.
20
+ - **Login only for hosted commands.** `project init` plus local work (run, render, studio) need no auth. Run `forgecad login` before hosted sync/admin commands such as `project push`, `project pull`, `project file *`, members, shares, and `publish`.
21
+ - **init is local, push creates remote.** `project init "Name"` writes local project metadata to `forgecad.json` without contacting the server. The first `project push` creates the hosted project if `forgecad.json` has no project ID yet, uploads existing local files, and records server file IDs. Later pushes sync the already-linked project. `clone <slug>` is the inverse: remote → new local folder.
22
22
  - **Sync is content-hash based.** `status`/`push`/`pull` compare file content hashes — no timestamps, no git; a file is "modified" purely by content difference. Loop: edit → `project status` → `project push`.
23
23
  - **Sync vs single-file ops.** Use `status`/`pull`/`push` for normal sync; use `project file <read|save|delete|...>` only for one hosted-file operation without a full push/pull cycle.
24
24
  - **Project context required.** All `project file *` and `publish` commands must run inside a folder containing `forgecad.json`.
@@ -15,7 +15,7 @@ Reconstruct a readable parametric ForgeCAD model from an existing CAD or mesh fi
15
15
 
16
16
  The reference asset is evidence, not the deliverable. The deliverable is a readable, parametric `.forge.js` model that runs, renders, and scores well against the source. Never return `Import.mesh()`/`Import.step()` of the source as the final model unless the user explicitly asks for an import wrapper — imports are for measurement, rendering, and scoring only.
17
17
 
18
- Routing: user wants to KEEP the file as a live component and design around it (bracket, enclosure, mating assembly) → `forgecad-build-model` (Imported Parts section), not this skill; prepared benchmark/RL episodes → use the task-local benchmark instructions, not the public skill library; inspection-bundle interpretation → `forgecad-inspect-model`; independent grading after reconstruction → `forgecad-grade-model`; API and command reference → `forgecad` skill + CLI.md.
18
+ Routing: user wants to KEEP the file as a live component and design around it (bracket, enclosure, mating assembly) → `forgecad-build-model`, not this skill; prepared benchmark/RL episodes → use the task-local benchmark instructions, not the public skill library; inspection-bundle interpretation → `forgecad-build-model/references/inspection-feedback.md`; readiness review → `forgecad-build-model/references/readiness-review.md`; API and command reference → `forgecad` skill + CLI.md.
19
19
 
20
20
  ### Workflow
21
21
 
@@ -50,7 +50,7 @@ Faceted sources: decide whether tessellation itself is evidence. Matching low-po
50
50
 
51
51
  ### Done Criteria
52
52
 
53
- The final model must run, render, re-compare at `--samples 5000`, and pass an `inspect compare overlay`. Add targeted inspects (`forgecad-inspect-model`) when the object is multi-part, hollow, thin-walled, or surface-sensitive. Report: source and candidate paths, score JSON path, final metrics, and every known mismatch classified as intentional simplification or remaining work.
53
+ The final model must run, render, re-compare at `--samples 5000`, and pass an `inspect compare overlay`. Add targeted inspects using `forgecad-build-model`'s inspection feedback reference when the object is multi-part, hollow, thin-walled, or surface-sensitive. Report: source and candidate paths, score JSON path, final metrics, and every known mismatch classified as intentional simplification or remaining work.
54
54
 
55
55
 
56
56
  ## Bundled Files
@@ -18,9 +18,8 @@ The reference image is evidence, not the deliverable. The deliverable is a real
18
18
  ### Companion Skills
19
19
 
20
20
  - `forgecad` — API docs, model authoring, renderer behavior.
21
- - `forgecad-design-spec` — when the images underdetermine artifact family, process posture, scale, operating story, or validation boundary.
22
- - `forgecad-build-model` — file placement, project structure, decomposition, definition of done.
23
- - `forgecad-inspect-model` — pre-delivery inspection for multi-part, internal, mechanical, thin-wall, or fit-sensitive objects.
21
+ - `forgecad-build-model` — design stages, file placement, project structure, decomposition, definition of done.
22
+ - `forgecad-build-model/references/inspection-feedback.md` — pre-delivery inspection for multi-part, internal, mechanical, thin-wall, or fit-sensitive objects.
24
23
 
25
24
  ### Core Rule
26
25
 
@@ -30,13 +29,13 @@ Infer the real object before matching any camera — identity, manufacture, scal
30
29
 
31
30
  1. Stage references in `/tmp/<slug>-replicate/refs`, keeping originals and adding view names where possible (`front`, `side`, `rear-iso`, `top`, `detail`).
32
31
  2. Read each image as evidence, recording: visible facts; scale cues; camera cues; unknowns (hidden/occluded geometry); conflicts across images or stylization.
33
- 3. Write a Real Object Brief — a hard gate before modeling: (a) artifact identity + operating story; (b) assumed scale and units; (c) process posture + part/BOM boundary (real geometry vs purchased vs ghost vs omitted); (d) inferred hidden-side geometry + expected canonical front/back/left/right/top/bottom forms; (e) validation views and inspection evidence. Use `forgecad-design-spec` when these are underdetermined.
32
+ 3. Write a Real Object Brief — a hard gate before modeling: (a) artifact identity + operating story; (b) assumed scale and units; (c) process posture + part/BOM boundary (real geometry vs purchased vs ghost vs omitted); (d) inferred hidden-side geometry + expected canonical front/back/left/right/top/bottom forms; (e) validation views and inspection evidence. Follow `forgecad-build-model` design stages when these are underdetermined.
34
33
  4. Build a coarse 3D blockout — model the object, not the image: large volumes, axes, symmetry, side depth, rear form, underside, hidden continuations. Render canonical views before any reference-camera comparison. Follow `forgecad-build-model` for project structure.
35
34
  5. Calibrate one camera per usable reference, only after the blockout makes sense from canonical views. Use the object center as `target`; estimate azimuth/elevation/distance/FOV from visible faces and perspective cues; use orthographic when parallel edges stay parallel with no perspective convergence.
36
35
  6. Render comparison boards: render the model from each calibrated reference camera and place it next to the original. Never compare from memory.
37
36
  7. Iterate one class of change at a time, in order: object hypothesis → major proportions → canonical geometry → camera → details → presentation. If improving one reference view makes another view or a canonical render worse, the object hypothesis is wrong — fix the model, not the camera illusion.
38
37
  8. Use every image as a constraint. Never pick one target image and ignore the rest: assign each image a camera, evidence list, and confidence; optimize one shared geometry against the whole set; state how distorted or decorative images were weighted.
39
- 9. Validate the final object: `forgecad run`, reference comparison boards, canonical renders, and targeted inspections via `forgecad-inspect-model`.
38
+ 9. Validate the final object: `forgecad run`, reference comparison boards, canonical renders, and targeted inspections via `forgecad-build-model`'s inspection feedback reference.
40
39
 
41
40
  ### Comparison Boards
42
41
 
@@ -30,7 +30,8 @@ Author or modify ForgeCAD models, sketches, assemblies, and CLI workflows. Prefe
30
30
 
31
31
  #### Import and Composition
32
32
 
33
- - Always include the extension in relative imports: `require("./file.forge.js", { Param: value })` for model files, `require("./helpers.js")` for plain helper modules. Extensionless imports such as `require("./file")` do not resolve; ForgeCAD resolves project imports by exact path.
33
+ - Always include the extension in relative imports: `require("./file.forge.js")` for model files, `require("./helpers.js")` for plain helper modules. Extensionless imports such as `require("./file")` do not resolve; ForgeCAD resolves project imports by exact path.
34
+ - Reusable `.forge.js` part files should return builder functions such as `return { buildPart }`; direct-run preview params belong inside `if (require.main === module)`.
34
35
  - ForgeCAD APIs are injected globals in `.forge.js` files. Use `bom()`, `box()`, `scene()`, `Shape`, etc. directly; never destructure those names from helpers (`const { bom } = require("./bom.js")`). Import helper files under a project-specific name such as `const bomHelpers = require("./bom.js")`.
35
36
  - For static multi-part models, connectors + `matchTo()` are the default way to assemble touching parts.
36
37
  - Top-level scripts can return `Assembly` or `SolvedAssembly` directly. Do not call `.toGroup()` just to render an assembly; use it only when you need `ShapeGroup` composition, transforms, or named-child lookup.
@@ -48,6 +49,7 @@ Execution model, colors, coordinate system, primitives, booleans, patterns, impo
48
49
 
49
50
  - `docs/skill/API/core/concepts.md`
50
51
  - `docs/skill/generated/runtime-names.md`
52
+ - `docs/skill/guides/manual-parameters.md`
51
53
  - `docs/skill/generated/core.md`
52
54
 
53
55
  #### 2. Static Assembly and Positioning (for any multi-part model)
@@ -11,12 +11,8 @@ forgecad skill install
11
11
  | Skill | Installed by | Purpose |
12
12
  | --- | --- | --- |
13
13
  | [forgecad](/docs/skills/forgecad) | `forgecad skill install` | ForgeCAD model authoring, editing, debugging, and execution guidance for .forge.js, SVG-import, assembly, and CLI workflows. Use when building or modifying ForgeCAD geometry, structuring multi-file projects, validating scripts, or using ForgeCAD export/render tooling. |
14
- | [forgecad-build-model](/docs/skills/forgecad-build-model) | `forgecad skill install` | Build or edit a manufacture-realistic `.forge.js` model in a project, then validate it with run, render, inspect, and export evidence. |
15
- | [forgecad-design-spec](/docs/skills/forgecad-design-spec) | `forgecad skill install` | Create a ForgeCAD design brief, HLD, or LLD before coding by walking through use, assembly, interfaces, decisions, and verification. |
16
- | [forgecad-grade-model](/docs/skills/forgecad-grade-model) | `forgecad skill install` | Grade a ForgeCAD or CAD-as-code model against a requirement, brief, prompt, reference, or acceptance criteria with evidence and a 0-10 score. |
14
+ | [forgecad-build-model](/docs/skills/forgecad-build-model) | `forgecad skill install` | Build or edit `.forge.js` real product models through design, automatic/manual feedback gathering, inspection/simulation/FEA evidence, and iteration until ready. |
17
15
  | [forgecad-image-prompt](/docs/skills/forgecad-image-prompt) | `forgecad skill install` | Write builder-honest AI image prompts from a concrete ForgeCAD model, build brief, HLD, or LLD without hiding how the artifact is built. |
18
- | [forgecad-inspect-model](/docs/skills/forgecad-inspect-model) | `forgecad skill install` | Select, run, and interpret ForgeCAD inspection evidence for collisions, sections, wall thickness, components, masks, depth, normals, surface continuity, and fit. |
19
16
  | [forgecad-project-sync](/docs/skills/forgecad-project-sync) | `forgecad skill install` | Manage hosted ForgeCAD project sync from the CLI: init, clone, pull, push, file operations, members, publishing, and shares. |
20
17
  | [forgecad-reconstruct-cad-file](/docs/skills/forgecad-reconstruct-cad-file) | `forgecad skill install` | Reconstruct a readable parametric ForgeCAD model from an existing CAD or mesh file such as STL, OBJ, 3MF, STEP, or STP. |
21
18
  | [forgecad-reconstruct-from-images](/docs/skills/forgecad-reconstruct-from-images) | `forgecad skill install` | Reconstruct a real parametric ForgeCAD object from reference images by using images as evidence, not as a one-view facade. |
22
- | [forgecad-verify-mujoco](/docs/skills/forgecad-verify-mujoco) | `forgecad skill install` | Verify a ForgeCAD MJCF export in MuJoCo with dynamics, contacts, controls, joint travel, and rendered evidence before calling it simulation-ready. |
@@ -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];
@@ -25,35 +25,75 @@ scene({
25
25
  // ───────────────────────── 1. the v4 metal bowl ─────────────────────────
26
26
  const H = 5.5; // rim level
27
27
  const S = 5.0; // smooth-clamp knee
28
- const stations = [
29
- { x: -92, hw: 0, lowZ: 5.0 },
30
- { x: -90.5, hw: 12, lowZ: 2.0 },
31
- { x: -88, hw: 17.5, lowZ: -0.5 },
32
- { x: -82, hw: 22.5, lowZ: -4.0 },
33
- { x: -67, hw: 28, lowZ: -8.5 },
34
- { x: -48, hw: 32, lowZ: -10.2 }, // widest & deepest
35
- { x: -28, hw: 29, lowZ: -8.4 },
36
- { x: -6, hw: 13, lowZ: -2.0 },
37
- { x: 12, hw: 8.4, lowZ: 2.5 },
38
- { x: 24, hw: 7.1, lowZ: 4.0 }, // neck
39
- ];
40
28
 
41
- function smoothField(key) {
42
- const pts = Curve.Fit(stations.map((s) => [s.x, s[key], 0]), { tolerance: 0.0005 }).sample(600);
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]);
43
81
  return (x) => {
44
82
  if (x <= pts[0][0]) return pts[0][1];
45
83
  if (x >= pts[pts.length - 1][0]) return pts[pts.length - 1][1];
46
84
  for (let i = 1; i < pts.length; i += 1) {
47
85
  if (x <= pts[i][0]) {
48
- const t = (x - pts[i - 1][0]) / (pts[i][0] - pts[i - 1][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;
49
89
  return pts[i - 1][1] + (pts[i][1] - pts[i - 1][1]) * t;
50
90
  }
51
91
  }
52
92
  return pts[pts.length - 1][1];
53
93
  };
54
94
  }
55
- const lowAt = smoothField('lowZ');
56
- const hwAt = smoothField('hw');
95
+ const lowAt = smoothField(bowlLowZ);
96
+ const hwAt = smoothField(bowlHalfWidth);
57
97
 
58
98
  function dishZ(x, y) {
59
99
  const hw = hwAt(x);
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Variable-thickness surface panel.
3
+ *
4
+ * `Surface.Net()` builds the carrier sheet. `Thickness.grid()` defines a scalar
5
+ * UV wall field, so the panel can be thin at one edge, thicker through the load
6
+ * path, and locally reinforced near one corner without hand-offset geometry.
7
+ */
8
+
9
+ scene({
10
+ background: { top: '#eef4f7', bottom: '#f9fbfd' },
11
+ camera: { position: [150, -190, 92], target: [0, 0, 8], fov: 34 },
12
+ environment: { preset: 'studio', intensity: 0.25, background: false },
13
+ ground: { visible: true, color: '#d6dde3', height: -5, receiveShadow: true },
14
+ });
15
+
16
+ const panelWidth = param('Panel Width', 128);
17
+ const panelDepth = param('Panel Depth', 82);
18
+ const crownHeight = param('Crown Height', 15);
19
+
20
+ const uCount = 6;
21
+ const vCount = 5;
22
+ const panelCage = [];
23
+
24
+ for (let i = 0; i <= uCount; i++) {
25
+ const u = i / uCount;
26
+ const x = (u - 0.5) * panelWidth;
27
+ const row = [];
28
+
29
+ for (let j = 0; j <= vCount; j++) {
30
+ const v = j / vCount;
31
+ const y = (v - 0.5) * panelDepth;
32
+ const xProfile = 1 - (2 * u - 1) ** 2;
33
+ const yProfile = 1 - 0.42 * (2 * v - 1) ** 2;
34
+ const diagonalBias = 2.8 * u * (1 - v);
35
+ row.push([x, y, crownHeight * xProfile * yProfile + diagonalBias]);
36
+ }
37
+
38
+ panelCage.push(row);
39
+ }
40
+
41
+ const panel = Surface.Net()
42
+ .cage(panelCage)
43
+ .degree(3, 3)
44
+ .thickenInsideBy(
45
+ Thickness.grid(
46
+ [
47
+ [1.1, 1.3, 1.5],
48
+ [1.8, 2.4, 2.9],
49
+ [2.8, 3.5, 4.2],
50
+ ],
51
+ { easing: 'easeInOut' },
52
+ ),
53
+ { resolution: 56 },
54
+ )
55
+ .color('#5c8fb5')
56
+ .material({ roughness: 0.62, metalness: 0.04, clearcoat: 0.12 });
57
+
58
+ verify.notEmpty('Variable-thickness panel produces solid geometry', panel);
59
+ verify.volumeApprox('Variable thickness stays in the expected volume range', panel, panelWidth * panelDepth * 2.55, 6000);
60
+ verify.physicalComponentCount('Variable-thickness panel is one connected component', 1);
61
+
62
+ return [{ name: 'Variable Thickness Panel', shape: panel }];
@@ -16,6 +16,13 @@
16
16
  //
17
17
  // Thickness: 24% at root (structural) → 6% at tip (aerodynamic)
18
18
  // Camber: 4% at root → 1.5% at tip (Cl margin at low-speed root)
19
+ //
20
+ // Skin: The blade is lofted through the closed airfoil sections with
21
+ // Shape.fromSlices — an ordered slice loft that keeps a smooth
22
+ // section-to-section correspondence, so the blade reads class-A
23
+ // (no spanwise faceting) even on the default Manifold backend.
24
+ // Run with `--backend truck` (or occt) for an exact kernel-native
25
+ // NURBS solid — preferred for STEP export.
19
26
 
20
27
  // ─── Design Parameters ──────────────────────────────────────────
21
28
  const diameter = Param.number("Diameter", 1900, { min: 1200, max: 2600, unit: "mm" });
@@ -171,49 +178,98 @@ function camberAt(rNorm) {
171
178
  // - Zero pitch → chord along tangential (Y in world)
172
179
  // - Positive β → leading edge tilts toward thrust axis (+Z)
173
180
 
174
- const NUM_STATIONS = 24;
175
- const NACA_PTS = 40; // points per airfoil surface (80 total outline)
181
+ const NUM_STATIONS = 28; // a few more stations than the old loft for a smoother span
182
+ const NACA_PTS = 48; // points per airfoil surface (96 total outline)
176
183
  const CAMBER_POS = 0.4; // max camber at 40% chord (standard for props)
177
184
  const rRoot = rHub * 0.55; // blade shank starts inside the spinner
178
185
 
179
- // Airfoil section at a given normalized span position (clamped to the
180
- // aerodynamic range the shank inside the spinner reuses the hub section).
181
- function bladeSection(rNorm, r) {
186
+ // Airfoil section as a 2D point list (before it becomes a Sketch), already
187
+ // rotated for twist. The buried shank can blend toward a fuller, rounder cuff:
188
+ // rNorm normalized span for the aerodynamic distributions
189
+ // r — true radius (drives the BEMT twist)
190
+ // fatten — root-cuff blend 0..1: 0 = pure airfoil, 1 = fully rounded fat
191
+ // shank. Pulls each outline point toward an enclosing ellipse so the
192
+ // blade meets the spinner with a beefy, non-spindly junction instead
193
+ // of necking down to a thin stalk (the old root-quality defect).
194
+ function bladeSectionPoints(rNorm, r, fatten) {
182
195
  const chord = chordAt(rNorm);
183
196
  const thick = thicknessRatioAt(rNorm);
184
197
  const camber = camberAt(rNorm);
185
198
  const twist = twistAt(Math.max(r, rHub));
186
199
 
187
200
  let pts = nacaPoints(camber, CAMBER_POS, thick, chord, NACA_PTS);
188
- pts = rotate2D(pts, 90 + twist); // 90° aligns chord with tangential, +twist adds pitch
189
- return polygon(pts);
201
+
202
+ if (fatten > 0) {
203
+ // Blend toward an enclosing ellipse centered at the quarter-chord origin —
204
+ // fills the thin airfoil into a fuller shank without changing the point
205
+ // count or ordering, so the loft keeps a clean ordered correspondence.
206
+ const exA = chord * 0.34; // half-length along chord
207
+ const exB = Math.max(chord * thick * 0.62, chord * 0.16); // half-thickness, floored fat
208
+ pts = pts.map(([x, y]) => {
209
+ const ang = Math.atan2(y, x);
210
+ const ex = exA * Math.cos(ang);
211
+ const ey = exB * Math.sin(ang);
212
+ return [x + (ex - x) * fatten, y + (ey - y) * fatten];
213
+ });
214
+ }
215
+
216
+ // 90° aligns chord with the tangential direction; +twist adds pitch.
217
+ return rotate2D(pts, 90 + twist);
190
218
  }
191
219
 
192
- const profiles = [];
193
- const heights = [];
220
+ function bladeSection(rNorm, r, fatten) {
221
+ return polygon(bladeSectionPoints(rNorm, r, fatten || 0));
222
+ }
194
223
 
195
- // Shank stations buried inside the spinner a constant hub section carries
196
- // the blade to a structural attachment instead of floating at the hub surface.
197
- for (const r of [rRoot, rHub * 0.92]) {
198
- profiles.push(bladeSection(0, rHub));
199
- heights.push(r);
224
+ // Ordered station list: span coordinate (world X, radial in the disk) + the
225
+ // closed airfoil section's points. Sections sit perpendicular to X, so the
226
+ // blade extends radially with no post-loft rotation.
227
+ const stationX = [];
228
+ const stationPoints = [];
229
+
230
+ // Two buried shank stations inside the spinner: the deepest is a fully round
231
+ // cuff, the next blends back toward the airfoil so the aero blade grows
232
+ // smoothly out of the shank instead of pinching at the hub.
233
+ const shankStations = [
234
+ { x: rRoot, fatten: 1.0 },
235
+ { x: rHub * 0.86, fatten: 0.55 },
236
+ ];
237
+ for (const { x, fatten } of shankStations) {
238
+ stationX.push(x);
239
+ stationPoints.push(bladeSectionPoints(0, rHub, fatten));
200
240
  }
201
241
 
202
242
  // Aerodynamic stations from the hub surface to the tip. Cosine spacing
203
243
  // clusters stations at the root (where the BEMT twist gradient is steepest)
204
- // and at the tip (where the planform rounds off).
244
+ // and at the tip (where the planform rounds off). A light residual cuff on the
245
+ // first aero stations avoids a sudden chord/thickness step at the hub line.
205
246
  for (let i = 0; i <= NUM_STATIONS; i++) {
206
- const rNorm = 0.5 - 0.5 * Math.cos(Math.PI * i / NUM_STATIONS);
207
- const r = rHub + rNorm * (R - rHub);
208
- profiles.push(bladeSection(rNorm, r));
209
- heights.push(r);
247
+ const rNorm = 0.5 - 0.5 * Math.cos(Math.PI * i / NUM_STATIONS);
248
+ const r = rHub + rNorm * (R - rHub);
249
+ const fatten = rNorm < 0.06 ? 0.25 * (1 - rNorm / 0.06) : 0;
250
+ stationX.push(r);
251
+ stationPoints.push(bladeSectionPoints(rNorm, r, fatten));
210
252
  }
211
253
 
212
- // Loft all profiles into a smooth blade solid
213
- const bladeRaw = loft(profiles, heights, { edgeLength: meshRes });
214
-
215
- // Rotate blade from Z-axis (span) to X-axis (radial in propeller disk)
216
- const blade = bladeRaw.rotateY(90);
254
+ // Loft the closed sections into a smooth blade solid. Shape.fromSlices keeps an
255
+ // ordered, smooth section-to-section correspondence (no SDF faceting). On the
256
+ // default Manifold backend edgeLength tunes the mesh; the truck/occt kernel
257
+ // ignores it and builds an exact NURBS solid.
258
+ const blade = Shape.fromSlices(
259
+ stationX.map((x, i) => Shape.slicePerpendicularToX(x, polygon(stationPoints[i]))),
260
+ { edgeLength: meshRes },
261
+ );
262
+
263
+ // Blade-shaped cutouts through the spinner: the same ordered sections grown
264
+ // ~5% for running clearance, lofted the same exact way so the cut tracks the
265
+ // blade skin (keeping the span direction exact).
266
+ const CLEARANCE = 1.05;
267
+ const bladeCutout = Shape.fromSlices(
268
+ stationX.map((x, i) => Shape.slicePerpendicularToX(
269
+ x, polygon(stationPoints[i].map(([px, py]) => [px * CLEARANCE, py * CLEARANCE])),
270
+ )),
271
+ { edgeLength: meshRes },
272
+ );
217
273
 
218
274
  // ─── Spinner / Hub ──────────────────────────────────────────────
219
275
  //
@@ -269,10 +325,7 @@ const hubBarrel = cylinder(spinnerLen * 0.5, rHub * 0.62)
269
325
 
270
326
  const bladeAssembly = union(allBlades, hubBarrel);
271
327
 
272
- // Blade-shaped cutouts through the spinner with a small running
273
- // clearance: the blade cross-section is grown in chord and thickness
274
- // (x/y before the radial rotation), keeping the span direction exact.
275
- const bladeCutout = bladeRaw.scale([1.04, 1.04, 1]).rotateY(90);
328
+ // Pattern the blade-shaped cutouts (defined with the blade above) around the disk.
276
329
  const spinnerCutouts = circularPattern(bladeCutout, numBlades);
277
330
 
278
331
  // ─── Optional: Airfoil Section Visualization ────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgecad",
3
- "version": "0.10.5",
3
+ "version": "0.11.0",
4
4
  "description": "Code-first parametric CAD for JavaScript/TypeScript, in the browser and CLI.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://forgecad.io",
@@ -115,7 +115,7 @@
115
115
  "dev:cli": "npm run build:solver:if-missing && npm run build:geometry:if-missing && tsup cli/forgecad.ts --format esm --platform node --target node20 --out-dir dist-cli --sourcemap --external typescript && node scripts/copy-cli-assets.mjs dist-cli",
116
116
  "build:cli": "npm run build:solver:if-missing && npm run build:geometry:if-missing && tsup cli/forgecad.ts --format esm --platform node --target node20 --out-dir dist-cli --clean --external typescript && node scripts/copy-cli-assets.mjs dist-cli",
117
117
  "build:native-occt": "node scripts/build-native-occt.mjs",
118
- "build:backend": "npm run build:native-occt && tsup apps/backend/src/server.ts apps/backend/src/nativeOcctWorker.ts --format esm --platform node --target node20 --out-dir dist-backend --clean --sourcemap",
118
+ "build:backend": "npm run build:geometry:if-missing && npm run build:native-occt && tsup apps/backend/src/server.ts apps/backend/src/nativeOcctWorker.ts --format esm --platform node --target node20 --out-dir dist-backend --clean --sourcemap && node scripts/copy-wasm-assets.mjs dist-backend",
119
119
  "build:binary": "node scripts/build-binary.mjs",
120
120
  "build:binary:all": "node scripts/build-binary.mjs --all",
121
121
  "bundle:offline": "node scripts/build-install-bundle.mjs",