forgecad 0.10.4 → 0.10.5
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-B3L3W1Uo.js → AdminPage-raksfnNA.js} +1 -1
- package/dist/assets/{BenchmarkPage-DXKVXMrJ.js → BenchmarkPage-DP3RxhPs.js} +2 -2
- package/dist/assets/{BlogPage-B7BWxOCg.js → BlogPage-D7Dos-vl.js} +1 -1
- package/dist/assets/{DocsPage-BPGGwht1.js → DocsPage-DO1kvBns.js} +7 -1
- package/dist/assets/{EditorApp-BWUGCdD5.js → EditorApp-DQJmcmRT.js} +9 -8
- package/dist/assets/{EmbedViewer-DygByZS2.js → EmbedViewer-DFDUhOma.js} +2 -2
- package/dist/assets/{LandingPageProofDriven-BoVE7JGY.js → LandingPageProofDriven-DbE_tp8-.js} +2 -2
- package/dist/assets/{LegalPage-Din8wv8d.js → LegalPage-CominSso.js} +2 -2
- package/dist/assets/{PricingPage-C2PMzmDc.js → PricingPage-CcVIN9yj.js} +2 -2
- package/dist/assets/{SettingsPage-BlJDCRe8.js → SettingsPage-DLWcP289.js} +1 -1
- package/dist/assets/{app-BsRYSfxY.js → app-xW3hOdq9.js} +1135 -320
- package/dist/assets/{backendInit-6C0DLgH0.js → backendInit-mDHk97u7.js} +6630 -2493
- package/dist/assets/cli/{render-XXol_ET7.js → render--SIU27W_.js} +1263 -112
- package/dist/assets/{constructionHistoryWorker-cTHWRJEi.js → constructionHistoryWorker-uEe_Q7Kg.js} +1861 -610
- package/dist/assets/{evalWorker-BssDYW9u.js → evalWorker-BqyDHDcI.js} +6254 -2177
- package/dist/assets/{forgecad_geometry-CZ_IfuvA.js → forgecad_geometry-D8rWX7nQ.js} +1 -1
- package/dist/assets/{forgecad_geometry_bg-C3rQHfwg.wasm → forgecad_geometry_bg-ObqfqjJT.wasm} +0 -0
- package/dist/assets/{inspectWorker-ymhBV4Ll.js → inspectWorker-UXMxlcR8.js} +2738 -742
- package/dist/assets/{jointPose-B0blBj9A.js → jointPose-bYMlwU3v.js} +1 -1
- package/dist/assets/{landing-proof-driven-Cpf-MIbI.css → landing-proof-driven-_u4v_xQb.css} +2 -2
- package/dist/assets/{manifold-B_7QXpGB.js → manifold-BR7UYI4P.js} +1 -1
- package/dist/assets/{manifold-CYlIm-M6.js → manifold-CyOV5B9S.js} +2 -2
- package/dist/assets/{manifold-CNShmpEJ.js → manifold-D4d5NQst.js} +1 -1
- package/dist/assets/{reportWorker-Cb5eyM7D.js → reportWorker-DsaICZsn.js} +6010 -2032
- package/dist/cli/render.html +1 -1
- package/dist/docs/index.html +2 -2
- package/dist/docs-raw/CLI.md +4 -2
- package/dist/docs-raw/generated/assembly.md +76 -3
- package/dist/docs-raw/generated/concepts.md +31 -4
- package/dist/docs-raw/generated/core.md +159 -21
- package/dist/docs-raw/generated/curves.md +344 -6
- package/dist/docs-raw/generated/runtime-names.md +12 -12
- package/dist/docs-raw/generated/sketch.md +16 -3
- package/dist/docs-raw/guides/inspection-bundles.md +4 -2
- package/dist/docs-raw/guides/structural-fea.md +224 -0
- package/dist/docs-raw/skills/forgecad.md +1 -0
- package/dist/index.html +1 -1
- package/dist/sitemap.xml +15 -15
- package/dist-cli/{check-compiler-4RPB6SB5.js → check-compiler-7YAHVXYM.js} +1 -1
- package/dist-cli/{check-query-propagation-KN3DFQTX.js → check-query-propagation-ZRR6IOJW.js} +1 -1
- package/dist-cli/{chunk-UHBRMYA6.js → chunk-VNM67DIV.js} +6489 -2333
- package/dist-cli/forgecad.js +5258 -717
- package/dist-cli/forgecad_geometry_bg.wasm +0 -0
- package/dist-skill/CONTEXT.md +827 -45
- package/dist-skill/SKILL.md +1 -0
- package/dist-skill/docs/CLI.md +4 -2
- package/dist-skill/docs/generated/assembly.md +73 -3
- package/dist-skill/docs/generated/core.md +159 -21
- package/dist-skill/docs/generated/curves.md +343 -6
- package/dist-skill/docs/generated/runtime-names.md +12 -12
- package/dist-skill/docs/generated/sketch.md +16 -3
- package/dist-skill/docs/guides/inspection-bundles.md +4 -2
- package/dist-skill/docs/guides/structural-fea.md +224 -0
- package/dist-skill/website/skills/forgecad.md +1 -0
- package/examples/analysis/structural-stress-fea.forge.js +19 -0
- package/examples/api/blend-full-round.forge.js +37 -0
- package/examples/api/blend-variable-radius.forge.js +51 -0
- package/examples/api/curve-project-and-intersect.forge.js +59 -0
- package/examples/api/extrude-up-to-face.forge.js +47 -0
- package/examples/api/spoon-full-tang-handle.forge.js +148 -0
- package/examples/api/surface-boundarynet-dished-bowl.forge.js +63 -0
- package/examples/api/surface-fill-interior-constraints.forge.js +59 -0
- package/package.json +4 -1
- /package/dist/assets/{landing-proof-driven-BxZZh5r5.js → landing-proof-driven-DNPRKL_p.js} +0 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
---
|
|
2
|
+
skill-group: cli
|
|
3
|
+
skill-order: 3
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Structural FEA Stress Inspection
|
|
7
|
+
|
|
8
|
+
Use structural FEA when you want a ForgeCAD model to answer a load-case question:
|
|
9
|
+
|
|
10
|
+
- Where does this part see the highest stress?
|
|
11
|
+
- How far does it deflect?
|
|
12
|
+
- What is the minimum safety factor against the material yield strength?
|
|
13
|
+
- Did the mesh and solver produce evidence that is good enough to inspect?
|
|
14
|
+
|
|
15
|
+
ForgeCAD owns the authoring contract, solver orchestration, result feedback, and inspection report. The numerical solve is done out of process with Gmsh and CalculiX. Users author a study in the model, run `forgecad fea run`, and inspect a result bundle.
|
|
16
|
+
|
|
17
|
+
## What You Get
|
|
18
|
+
|
|
19
|
+
A solved FEA result bundle can produce:
|
|
20
|
+
|
|
21
|
+
- max von Mises stress
|
|
22
|
+
- max displacement
|
|
23
|
+
- minimum safety factor
|
|
24
|
+
- mesh quality and solver trust flags
|
|
25
|
+
- region-level hot spots
|
|
26
|
+
- `report.html`
|
|
27
|
+
- `summary.json`
|
|
28
|
+
- a safety-factor heatmap PNG
|
|
29
|
+
- a solver stress heatmap PNG
|
|
30
|
+
- a displacement magnitude heatmap PNG
|
|
31
|
+
|
|
32
|
+
The deformed render is display-only. It helps explain the displacement shape; it does not change the stress, displacement, or safety-factor numbers reported by the solver.
|
|
33
|
+
|
|
34
|
+
## What You Need Installed
|
|
35
|
+
|
|
36
|
+
The ForgeCAD CLI creates the package and renders the heatmap. The package runner uses self-contained `uv` Python scripts for Gmsh so every package resolves the same Python dependency set by default.
|
|
37
|
+
|
|
38
|
+
Run `forgecad doctor` to check these optional FEA tools in a separate section. Missing FEA tools do not block core ForgeCAD modeling, export, or render commands.
|
|
39
|
+
|
|
40
|
+
| Tool | Used For | Quick Check |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| `uv` | Runs the packaged Python scripts with pinned dependencies | `uv --version` |
|
|
43
|
+
| CalculiX `ccx` | Solves the static stress deck | `ccx -v` |
|
|
44
|
+
| Bash | Runs the package script | `bash --version` |
|
|
45
|
+
| Chrome or Chromium | Renders PNG heatmaps from solved evidence | Chrome installed in a standard location, `CHROME_PATH=/path/to/chrome`, or `--chrome-path /path/to/chrome` |
|
|
46
|
+
|
|
47
|
+
If `uv` is not on `PATH`, set `UV=/path/to/uv` when running `forgecad fea run` or `forgecad fea check`.
|
|
48
|
+
|
|
49
|
+
If `ccx` is not on `PATH`, set `CCX=/path/to/ccx` when running `forgecad fea run` or `forgecad fea check`.
|
|
50
|
+
|
|
51
|
+
If you need an offline or pre-provisioned Python environment, set `PYTHON=/path/to/python`. That opt-out Python must be able to `import gmsh`; use `GMSH_PYTHONPATH` / `GMSH_PYTHON_PATH` only for that override path.
|
|
52
|
+
|
|
53
|
+
ForgeCAD does not bundle CalculiX. The generated `uv` scripts pin the Gmsh Python package, and `uv` downloads/caches it from the configured Python package index. If you redistribute solver binaries or Python wheels to customers, handle their licenses as part of your distribution.
|
|
54
|
+
|
|
55
|
+
## Author The Study
|
|
56
|
+
|
|
57
|
+
Structural FEA starts in the `.forge.js` file. The script should return an authored `assembly(...)` with:
|
|
58
|
+
|
|
59
|
+
1. a structural part marked with `Fea.body(...)`
|
|
60
|
+
2. one or more static stress studies from `Fea.study.staticStress(...)`
|
|
61
|
+
3. explicit fixtures and loads
|
|
62
|
+
4. a second-order tetrahedral mesh intent
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
const aluminum = Fea.material("6061-T6", {
|
|
66
|
+
densityKgM3: 2700,
|
|
67
|
+
youngsModulusMPa: 68900,
|
|
68
|
+
poissonRatio: 0.33,
|
|
69
|
+
yieldStrengthMPa: 276,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const beam = box(120, 12, 12);
|
|
73
|
+
|
|
74
|
+
return assembly("Cantilever Stress Study")
|
|
75
|
+
.addPart("Beam", beam, {
|
|
76
|
+
fea: Fea.body({ material: aluminum }),
|
|
77
|
+
})
|
|
78
|
+
.withFeaStudy(
|
|
79
|
+
Fea.study.staticStress("end-load", {
|
|
80
|
+
fixtures: [
|
|
81
|
+
Fea.fix.fixed(Fea.region.face("fixed-end", beam.face("left"))),
|
|
82
|
+
],
|
|
83
|
+
loads: [
|
|
84
|
+
Fea.load.force(Fea.region.face("load-end", beam.face("right")), {
|
|
85
|
+
newtons: 80,
|
|
86
|
+
direction: [0, 0, -1],
|
|
87
|
+
}),
|
|
88
|
+
],
|
|
89
|
+
target: Fea.target.minSafetyFactor(2),
|
|
90
|
+
mesh: Fea.mesh.quadraticTets({ maxSizeMm: 4 }),
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The complete API reference is generated from source in [Assembly](../generated/assembly.md). Keep reusable examples in `.forge.js` files; do not duplicate every API signature in handwritten docs.
|
|
96
|
+
|
|
97
|
+
## Choose Stable Regions
|
|
98
|
+
|
|
99
|
+
Fixtures and loads must name real geometric regions. ForgeCAD will not guess them later.
|
|
100
|
+
|
|
101
|
+
Use `Fea.region.face(...)` when you can refer to a compiler-owned exact face, such as a simple box face or a named face from the model API.
|
|
102
|
+
|
|
103
|
+
Use `Fea.region.plane(...)` when the target is a planar face created by profiles, booleans, or imported geometry and the face name is not stable enough. Make the plane specific enough that it matches exactly one STEP/Gmsh surface.
|
|
104
|
+
|
|
105
|
+
During export, ForgeCAD writes a region map and a STEP tag plan. During the package run, the Gmsh preflight matches every authored fixture/load region against the STEP surfaces. Missing or ambiguous matches fail hard. That is intentional: a silent substitute face would make the stress result untrustworthy.
|
|
106
|
+
|
|
107
|
+
## Run The Flow
|
|
108
|
+
|
|
109
|
+
Installed users run the CLI as `forgecad`. Developers running inside this repository can replace `forgecad` with `node dist-cli/forgecad.js`.
|
|
110
|
+
|
|
111
|
+
Run every authored FEA study and save an inspection result bundle:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
forgecad fea run examples/analysis/structural-stress-fea.forge.js
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Run one named study:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
forgecad fea run bracket.forge.js --study side-load
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Open the report:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
forgecad fea open out/bracket-fea
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Render a customer-facing safety view:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
forgecad fea render out/bracket-fea/side-load --field safety
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Render the engineering stress heatmap:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
forgecad fea render out/bracket-fea/side-load --field stress
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Render the displacement magnitude heatmap:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
forgecad fea render out/bracket-fea/side-load --field displacement
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Render a deformed stress view only when the displacement shape is useful to inspect:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
forgecad fea render out/bracket-fea/side-load \
|
|
151
|
+
--field stress \
|
|
152
|
+
--shape deformed \
|
|
153
|
+
--exaggerate 10
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The deformation scale only affects the render. It does not change the reported stress, displacement, or safety factor.
|
|
157
|
+
|
|
158
|
+
Each solved study result directory includes:
|
|
159
|
+
|
|
160
|
+
- `report.html` for the human inspection report
|
|
161
|
+
- `summary.json` for automation
|
|
162
|
+
- `renders/safety-factor.png` for the customer-facing safety heatmap
|
|
163
|
+
- `renders/stress.png` for the engineering von Mises stress heatmap
|
|
164
|
+
|
|
165
|
+
Displacement and deformed-shape PNGs are explicit render outputs from `forgecad fea render --field displacement` or `--shape deformed`.
|
|
166
|
+
|
|
167
|
+
Compare two solved result bundles:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
forgecad fea compare out/baseline-fea/side-load out/four-x-fea/side-load
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Comparison renders use one shared camera, image size, and safety-factor legend.
|
|
174
|
+
|
|
175
|
+
Run in CI and fail the process when authored targets fail:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
forgecad fea check bracket.forge.js --json
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Read The Results
|
|
182
|
+
|
|
183
|
+
Start with `report.html` or `summary.json` in the result directory. The important fields are the maximum stress, maximum displacement, minimum safety factor, hot spots, and any mesh or solver trust findings.
|
|
184
|
+
|
|
185
|
+
The default user-facing result is safety factor because it answers "is this part okay?" Use stress when you need the raw engineering von Mises field.
|
|
186
|
+
|
|
187
|
+
Advanced users can still run the lower-level package flow:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
forgecad export fea model.forge.js --output out/beam.feapkg
|
|
191
|
+
forgecad sim fea out/beam.feapkg --json
|
|
192
|
+
forgecad inspect structural stress out/beam.feapkg --camera iso --output out/stress.png
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Those commands are useful for debugging package evidence. Customer docs should prefer `forgecad fea ...`.
|
|
196
|
+
|
|
197
|
+
## Current Scope
|
|
198
|
+
|
|
199
|
+
Structural FEA V1 is intentionally narrow:
|
|
200
|
+
|
|
201
|
+
- linear static stress only
|
|
202
|
+
- one structural body per package
|
|
203
|
+
- exact OCCT STEP export only
|
|
204
|
+
- second-order tetrahedral elements only
|
|
205
|
+
- fixed fixtures and force loads only
|
|
206
|
+
- no contacts, bonded assemblies, thermal loads, buckling, fatigue, plasticity, or certification workflow
|
|
207
|
+
|
|
208
|
+
ForgeCAD refuses mesh or faceted fallback for FEA export. If exact geometry export, region mapping, mesh quality, solver convergence, result parsing, or evidence trust fails, the command should fail with an actionable error instead of inventing a weaker path.
|
|
209
|
+
|
|
210
|
+
## Troubleshooting
|
|
211
|
+
|
|
212
|
+
| Symptom | What It Means | What To Do |
|
|
213
|
+
| --- | --- | --- |
|
|
214
|
+
| `FEA.TOOLCHAIN_UV_MISSING` | The package runner cannot find `uv`. | Install `uv` or run with `UV=/path/to/uv`. |
|
|
215
|
+
| `FEA.TOOLCHAIN_PYTHON_MISSING` | A `PYTHON=...` override points to a missing Python executable. | Install Python 3 or fix the `PYTHON` path. |
|
|
216
|
+
| `FEA.TOOLCHAIN_GMSH_MISSING` | The selected Python process cannot import Gmsh. | Prefer the default `uv` path, or install the Gmsh Python module for the `PYTHON=...` override. |
|
|
217
|
+
| `FEA.TOOLCHAIN_CCX_MISSING` | CalculiX is not available as `ccx`. | Install CalculiX or run with `CCX=/path/to/ccx`. |
|
|
218
|
+
| `FEA.GMSH_FACE_MATCH_NONE` | An authored fixture/load region did not match a STEP surface. | Use a more stable face reference or a more precise planar region. |
|
|
219
|
+
| `FEA.GMSH_FACE_MATCH_AMBIGUOUS` | A region matched more than one STEP surface. | Make the target region more specific or change the model so the load/fixture face is unique. |
|
|
220
|
+
| `FEA.MESH_QUALITY_BELOW_TARGET` | The mesh exists but did not meet the package quality target. | Reduce mesh size, simplify tiny features, or improve the geometry around the hot area. |
|
|
221
|
+
| `FEA.SOLVER_FAILED` | CalculiX did not complete the solve. | Inspect `solver/static_stress.log`, then check fixtures, loads, material values, and over-constraint. |
|
|
222
|
+
| `FEA.FIELD_UNTRUSTED` | The heatmap input is not trusted package evidence. | Run inspection on the `.feapkg` directory after `forgecad sim fea`, not a copied JSON file. |
|
|
223
|
+
|
|
224
|
+
For command flags, use the [CLI reference](../CLI.md). For the public API, use the generated [Assembly reference](../generated/assembly.md).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const aluminum = Fea.material('6061-T6', {
|
|
2
|
+
densityKgM3: 2700,
|
|
3
|
+
youngsModulusMPa: 68900,
|
|
4
|
+
poissonRatio: 0.33,
|
|
5
|
+
yieldStrengthMPa: 276,
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const beam = box(200, 20, 10);
|
|
9
|
+
|
|
10
|
+
return assembly('Cantilever FEA')
|
|
11
|
+
.addPart('Beam', beam, { fea: Fea.body({ material: aluminum }) })
|
|
12
|
+
.withFeaStudy(
|
|
13
|
+
Fea.study.staticStress('tip-load', {
|
|
14
|
+
fixtures: [Fea.fix.fixed(Fea.region.face('Beam', 'left'))],
|
|
15
|
+
loads: [Fea.load.force(Fea.region.face('Beam', 'right'), { newtons: 100, direction: [0, 0, -1] })],
|
|
16
|
+
target: Fea.target.minSafetyFactor(2),
|
|
17
|
+
mesh: Fea.mesh.quadraticTets({ maxSizeMm: 3 }),
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
@@ -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,148 @@
|
|
|
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
|
+
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
|
+
|
|
41
|
+
function smoothField(key) {
|
|
42
|
+
const pts = Curve.Fit(stations.map((s) => [s.x, s[key], 0]), { tolerance: 0.0005 }).sample(600);
|
|
43
|
+
return (x) => {
|
|
44
|
+
if (x <= pts[0][0]) return pts[0][1];
|
|
45
|
+
if (x >= pts[pts.length - 1][0]) return pts[pts.length - 1][1];
|
|
46
|
+
for (let i = 1; i < pts.length; i += 1) {
|
|
47
|
+
if (x <= pts[i][0]) {
|
|
48
|
+
const t = (x - pts[i - 1][0]) / (pts[i][0] - pts[i - 1][0]);
|
|
49
|
+
return pts[i - 1][1] + (pts[i][1] - pts[i - 1][1]) * t;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return pts[pts.length - 1][1];
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const lowAt = smoothField('lowZ');
|
|
56
|
+
const hwAt = smoothField('hw');
|
|
57
|
+
|
|
58
|
+
function dishZ(x, y) {
|
|
59
|
+
const hw = hwAt(x);
|
|
60
|
+
const lz = lowAt(x);
|
|
61
|
+
if (hw < 1e-6) return lz + (H - lz) * S;
|
|
62
|
+
const s = (y / hw) * (y / hw);
|
|
63
|
+
const f = s / Math.sqrt(1 + (s / S) * (s / S)); // smooth-clamp parabola
|
|
64
|
+
return lz + (H - lz) * f;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const X0 = -97, X1 = 28, Y0 = -40, Y1 = 40;
|
|
68
|
+
const NX = 52, NY = 38;
|
|
69
|
+
const grid = [];
|
|
70
|
+
for (let i = 0; i < NX; i += 1) {
|
|
71
|
+
const x = X0 + ((X1 - X0) * i) / (NX - 1);
|
|
72
|
+
const row = [];
|
|
73
|
+
for (let j = 0; j < NY; j += 1) {
|
|
74
|
+
const y = Y0 + ((Y1 - Y0) * j) / (NY - 1);
|
|
75
|
+
row.push([x, y, dishZ(x, y)]);
|
|
76
|
+
}
|
|
77
|
+
grid.push(row);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const bowl = Surface.Net()
|
|
81
|
+
.cage(grid)
|
|
82
|
+
.degree(3, 3)
|
|
83
|
+
.toSheet()
|
|
84
|
+
.thicken(1.2, { resolution: 160 })
|
|
85
|
+
.trimByPlane([0, 0, -1], -H); // exact level rim
|
|
86
|
+
|
|
87
|
+
// ───────────────────────── 2. the rounded grip volume ─────────────────────────
|
|
88
|
+
const spine = Curve.Fit(
|
|
89
|
+
[
|
|
90
|
+
[16, 0, 4.0],
|
|
91
|
+
[34, 0, 4.4],
|
|
92
|
+
[74, 0, 4.2],
|
|
93
|
+
[110, 0, 3.5],
|
|
94
|
+
],
|
|
95
|
+
{ tolerance: 0.02 },
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const gripProfile = (width, thickness) => roundedRect(width, thickness, thickness / 2);
|
|
99
|
+
|
|
100
|
+
const grip = variableSweep(
|
|
101
|
+
spine,
|
|
102
|
+
[
|
|
103
|
+
{ t: 0.0, profile: gripProfile(15.0, 7.0) },
|
|
104
|
+
{ t: 0.16, profile: gripProfile(16.6, 9.0) },
|
|
105
|
+
{ t: 0.62, profile: gripProfile(13.6, 9.8) },
|
|
106
|
+
{ t: 1.0, profile: gripProfile(16.2, 9.2) },
|
|
107
|
+
],
|
|
108
|
+
{ edgeLength: 0.65, samples: 90, up: [0, 0, 1] },
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// ───────────────────────── 3. split into tang + scales ─────────────────────────
|
|
112
|
+
const TANG_Z = 4.0; // grip equator
|
|
113
|
+
const TANG_T = 3.2; // tang thickness
|
|
114
|
+
const tangSlab = box(240, 70, TANG_T).translate(20, 0, TANG_Z); // flat slab at the equator
|
|
115
|
+
|
|
116
|
+
// Tang = grip ∩ slab (flush to the grip outline), fused into the bowl → metal head.
|
|
117
|
+
const tang = grip.intersect(tangSlab);
|
|
118
|
+
const metalHead = union(bowl, tang);
|
|
119
|
+
|
|
120
|
+
// Polymer scales = grip − slab, with the bolster (front) and butt (back) trimmed off
|
|
121
|
+
// so the steel shows there.
|
|
122
|
+
const bolsterCut = box(60, 70, 60).translate(2, 0, TANG_Z); // expose metal for x < 32
|
|
123
|
+
const buttCut = box(30, 70, 60).translate(121, 0, TANG_Z); // expose metal for x > 106
|
|
124
|
+
const scales = grip.subtract(tangSlab).subtract(bolsterCut).subtract(buttCut);
|
|
125
|
+
|
|
126
|
+
// ───────────────────────── 4. rivets ─────────────────────────
|
|
127
|
+
const rivetXs = [46, 72, 96];
|
|
128
|
+
const rivets = rivetXs.map((rx) =>
|
|
129
|
+
cylinder(14, 1.7).translate(rx, 0, TANG_Z - 7), // cylinder(height, radius); base at 0, along Z
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// ───────────────────────── 5. materials & assembly ─────────────────────────
|
|
133
|
+
const steel = (shape) => shape.color('#c9d0d8').material({ roughness: 0.22, metalness: 0.86 });
|
|
134
|
+
const polymer = (shape) => shape.color('#141619').material({ roughness: 0.55, metalness: 0.0 });
|
|
135
|
+
|
|
136
|
+
const wholeForMeasure = union(metalHead, scales, ...rivets);
|
|
137
|
+
|
|
138
|
+
verify.notEmpty('metal head (bowl + tang) is solid', metalHead);
|
|
139
|
+
verify.notEmpty('polymer scales are solid', scales);
|
|
140
|
+
verify.boundingBoxSize('full-tang spoon keeps its proportions', wholeForMeasure, [203, 69, 23], 16);
|
|
141
|
+
verify.lessThan('mid-bowl is deeper than the neck', dishZ(-48, 0), dishZ(24, 0) - 8);
|
|
142
|
+
verify.greaterThan('bowl has real spoon width', hwAt(-48) * 2, 50);
|
|
143
|
+
|
|
144
|
+
return [
|
|
145
|
+
steel(metalHead),
|
|
146
|
+
polymer(scales),
|
|
147
|
+
...rivets.map(steel),
|
|
148
|
+
];
|
|
@@ -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;
|