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.
- package/dist/assets/{AdminPage-raksfnNA.js → AdminPage-B1nIvqLS.js} +1 -1
- package/dist/assets/{BenchmarkPage-DP3RxhPs.js → BenchmarkPage-YZJbw5nd.js} +1 -1
- package/dist/assets/{BlogPage-D7Dos-vl.js → BlogPage-DIWRApKS.js} +1 -1
- package/dist/assets/{DocsPage-DO1kvBns.js → DocsPage-ClL6X1hR.js} +2 -22
- package/dist/assets/{EditorApp-DQJmcmRT.js → EditorApp-CYBDvSyT.js} +575 -119
- package/dist/assets/{EmbedViewer-DFDUhOma.js → EmbedViewer-Dmfu_LIw.js} +2 -2
- package/dist/assets/{LandingPageProofDriven-DbE_tp8-.js → LandingPageProofDriven-XYTiYxfM.js} +1 -1
- package/dist/assets/{LegalPage-CominSso.js → LegalPage-D5Z3CscF.js} +1 -1
- package/dist/assets/{PricingPage-CcVIN9yj.js → PricingPage-BP4lIGio.js} +1 -1
- package/dist/assets/{SettingsPage-DLWcP289.js → SettingsPage-D3bcPBsC.js} +1 -1
- package/dist/assets/{app-xW3hOdq9.js → app-BKjogwIZ.js} +2192 -231
- package/dist/assets/{backendInit-mDHk97u7.js → backendInit-6a9-ilom.js} +76448 -75066
- package/dist/assets/cli/{render--SIU27W_.js → render-CMNudGb0.js} +3 -3
- package/dist/assets/{constructionHistoryWorker-uEe_Q7Kg.js → constructionHistoryWorker-BuZgc606.js} +6985 -6706
- package/dist/assets/{evalWorker-BqyDHDcI.js → evalWorker-DQ82ueGu.js} +40862 -39497
- package/dist/assets/{inspectWorker-UXMxlcR8.js → inspectWorker-Cuby2qfT.js} +2078 -478
- package/dist/assets/{jointPose-bYMlwU3v.js → jointPose-CFql5I-u.js} +1 -1
- package/dist/assets/{manifold-CyOV5B9S.js → manifold-02pmr7O7.js} +2 -2
- package/dist/assets/{manifold-BR7UYI4P.js → manifold-C6KU0oII.js} +1 -1
- package/dist/assets/{manifold-D4d5NQst.js → manifold-P1yF3GKn.js} +1 -1
- package/dist/assets/{reportWorker-DsaICZsn.js → reportWorker-kg065BVL.js} +85183 -78309
- package/dist/cli/render.html +1 -1
- package/dist/docs/index.html +2 -2
- package/dist/docs-raw/AI/usage.md +6 -8
- package/dist/docs-raw/CLI.md +10 -10
- package/dist/docs-raw/component-model.md +28 -9
- package/dist/docs-raw/generated/concepts.md +13 -4
- package/dist/docs-raw/generated/core.md +244 -56
- package/dist/docs-raw/generated/curves.md +13 -0
- package/dist/docs-raw/generated/runtime-names.md +2 -2
- package/dist/docs-raw/guides/inspection-bundles.md +1 -1
- package/dist/docs-raw/guides/structural-fea.md +11 -0
- package/dist/docs-raw/skills/forgecad-build-model.md +70 -147
- package/dist/docs-raw/skills/forgecad-image-prompt.md +1 -1
- package/dist/docs-raw/skills/forgecad-project-sync.md +3 -3
- package/dist/docs-raw/skills/forgecad-reconstruct-cad-file.md +2 -2
- package/dist/docs-raw/skills/forgecad-reconstruct-from-images.md +4 -5
- package/dist/docs-raw/skills/forgecad.md +3 -1
- package/dist/docs-raw/skills/index.md +1 -5
- package/dist/docs-raw/welcome.md +3 -4
- package/dist/index.html +1 -1
- package/dist/llms.txt +1 -2
- package/dist/sitemap.xml +15 -15
- package/dist-cli/{check-compiler-7YAHVXYM.js → check-compiler-UJWUEIDC.js} +1 -1
- package/dist-cli/{check-query-propagation-ZRR6IOJW.js → check-query-propagation-O2EPDJSY.js} +1 -1
- package/dist-cli/{chunk-VNM67DIV.js → chunk-MNDROM7T.js} +77145 -75767
- package/dist-cli/forgecad.js +1145 -441
- package/dist-skill/CONTEXT.md +429 -64
- package/dist-skill/SKILL.md +3 -1
- package/dist-skill/docs/API/core/concepts.md +31 -4
- package/dist-skill/docs/CLI.md +10 -10
- package/dist-skill/docs/generated/core.md +240 -57
- package/dist-skill/docs/generated/curves.md +13 -0
- package/dist-skill/docs/generated/runtime-names.md +2 -2
- package/dist-skill/docs/guides/inspection-bundles.md +1 -1
- package/dist-skill/docs/guides/manual-parameters.md +130 -0
- package/dist-skill/docs/guides/structural-fea.md +11 -0
- package/dist-skill/library/README.md +0 -4
- package/dist-skill/library/forgecad-build-model/SKILL.md +57 -150
- package/dist-skill/library/forgecad-build-model/references/inspection-feedback.md +58 -0
- package/dist-skill/library/forgecad-build-model/references/module-contracts.md +53 -0
- package/dist-skill/library/forgecad-build-model/references/parameter-controls.md +22 -0
- package/dist-skill/library/forgecad-build-model/references/readiness-review.md +43 -0
- package/dist-skill/library/forgecad-build-model/references/simulation-feedback.md +49 -0
- package/dist-skill/library/forgecad-build-model/references/stage-1-design-intent.md +21 -0
- package/dist-skill/library/forgecad-build-model/references/stage-2-architecture-plan.md +23 -0
- package/dist-skill/library/forgecad-build-model/references/stage-3-build-slices.md +39 -0
- package/dist-skill/library/forgecad-build-model/references/stage-4-feedback-iteration.md +24 -0
- package/dist-skill/library/forgecad-build-model/references/stage-5-readiness-package.md +34 -0
- package/dist-skill/library/forgecad-image-prompt/SKILL.md +1 -1
- package/dist-skill/library/forgecad-project-sync/SKILL.md +3 -3
- package/dist-skill/library/forgecad-reconstruct-cad-file/SKILL.md +2 -2
- package/dist-skill/library/forgecad-reconstruct-from-images/SKILL.md +4 -5
- package/dist-skill/website/skills/forgecad-build-model.md +70 -147
- package/dist-skill/website/skills/forgecad-image-prompt.md +1 -1
- package/dist-skill/website/skills/forgecad-project-sync.md +3 -3
- package/dist-skill/website/skills/forgecad-reconstruct-cad-file.md +2 -2
- package/dist-skill/website/skills/forgecad-reconstruct-from-images.md +4 -5
- package/dist-skill/website/skills/forgecad.md +3 -1
- package/dist-skill/website/skills/index.md +1 -5
- package/examples/api/param-path2d.forge.js +65 -0
- package/examples/api/param-placement2d.forge.js +80 -0
- package/examples/api/param-spline2d-g-continuity.forge.js +57 -0
- package/examples/api/spoon-full-tang-handle.forge.js +57 -17
- package/examples/api/surface-variable-thickness-panel.forge.js +62 -0
- package/examples/mechanical/airplane-propeller.forge.js +81 -28
- package/package.json +2 -2
- package/dist/docs-raw/skills/forgecad-design-spec.md +0 -145
- package/dist/docs-raw/skills/forgecad-grade-model.md +0 -84
- package/dist/docs-raw/skills/forgecad-inspect-model.md +0 -80
- package/dist/docs-raw/skills/forgecad-verify-mujoco.md +0 -78
- package/dist-skill/library/forgecad-design-spec/SKILL.md +0 -132
- package/dist-skill/library/forgecad-design-spec/references/default-profiles.md +0 -99
- package/dist-skill/library/forgecad-design-spec/references/master-prompt.md +0 -73
- package/dist-skill/library/forgecad-grade-model/SKILL.md +0 -72
- package/dist-skill/library/forgecad-grade-model/agents/openai.yaml +0 -4
- package/dist-skill/library/forgecad-inspect-model/SKILL.md +0 -68
- package/dist-skill/library/forgecad-verify-mujoco/SKILL.md +0 -66
- package/dist-skill/website/skills/forgecad-design-spec.md +0 -145
- package/dist-skill/website/skills/forgecad-grade-model.md +0 -84
- package/dist-skill/website/skills/forgecad-inspect-model.md +0 -80
- package/dist-skill/website/skills/forgecad-verify-mujoco.md +0 -78
- /package/dist-skill/library/{forgecad-verify-mujoco → forgecad-build-model}/scripts/mujoco_verify.py +0 -0
- /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
|
|
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.** `
|
|
21
|
-
- **init
|
|
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
|
|
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
|
|
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-
|
|
22
|
-
- `forgecad-build-model` —
|
|
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.
|
|
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-
|
|
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"
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
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
|
|
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(
|
|
56
|
-
const hwAt = smoothField(
|
|
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 =
|
|
175
|
-
const NACA_PTS =
|
|
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
|
|
180
|
-
//
|
|
181
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
193
|
-
|
|
220
|
+
function bladeSection(rNorm, r, fatten) {
|
|
221
|
+
return polygon(bladeSectionPoints(rNorm, r, fatten || 0));
|
|
222
|
+
}
|
|
194
223
|
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
207
|
-
const r
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
//
|
|
216
|
-
const blade =
|
|
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
|
-
//
|
|
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.
|
|
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",
|