forgecad 0.9.13 → 0.9.15
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/LICENSE +6 -4
- package/README.md +8 -4
- package/dist/assets/{AdminPage-DramHHDf.js → AdminPage-CDyGUinA.js} +2 -2
- package/dist/assets/{BenchmarkPage-Bjgkh5m9.js → BenchmarkPage-DfPMY_-d.js} +4 -15
- package/dist/assets/{BlogPage-n_HGP3Qm.js → BlogPage-kF0fkdJT.js} +2 -2
- package/dist/assets/{DocsPage-WCIkPmzC.js → DocsPage-B954L3YN.js} +9 -3
- package/dist/assets/EditorApp-Beb-IZ0y.js +14014 -0
- package/dist/assets/{EditorApp-BAnckbsk.css → EditorApp-CuDLxKqL.css} +698 -0
- package/dist/assets/{EmbedViewer-DEZKqdfW.js → EmbedViewer-C77B-TrF.js} +3 -3
- package/dist/assets/{LandingPageProofDriven-CeRIctuj.js → LandingPageProofDriven-Cr6fXMDj.js} +35 -37
- package/dist/assets/LegalPage-BRlScr9A.css +91 -0
- package/dist/assets/LegalPage-Dzklqmmg.js +39 -0
- package/dist/assets/{PricingPage-BMedqFef.css → PricingPage-BPF6HKyO.css} +25 -0
- package/dist/assets/{PricingPage-rIRa8p4Y.js → PricingPage-zWXkvlwl.js} +19 -19
- package/dist/assets/{SettingsPage-BqCUvEXM.js → SettingsPage-Bz0of4KQ.js} +2 -2
- package/dist/assets/app-CE3sYcV7.css +3890 -0
- package/dist/assets/{app-BUZqJvSO.js → app-D3kDkggg.js} +2305 -960
- package/dist/assets/cli/{render-lhGxj50Y.js → render-DSY3mMQa.js} +423 -30
- package/dist/assets/{constructionHistoryWorker-ipD1jcIv.js → constructionHistoryWorker-gpDo-uH2.js} +927 -243
- package/dist/assets/{evalWorker-CHXSe_-u.js → evalWorker-CU0Ke6DP.js} +7799 -4163
- package/dist/assets/{forgecad_geometry-BVnIeXMG.js → forgecad_geometry-Dgceylq9.js} +43 -1
- package/dist/assets/{forgecad_geometry_bg-DufhhCBV.wasm → forgecad_geometry_bg-dD4RNQF1.wasm} +0 -0
- package/dist/assets/{inspectWorker-DeRnMVv1.js → inspectWorker-COyp8XXA.js} +927 -243
- package/dist/assets/{javascript-70-4uGcz.js → javascript-1kQXfVaz.js} +1 -1
- package/dist/assets/landing-proof-driven-DiGqdtWa.js +18 -0
- package/dist/assets/{landing-proof-driven-oFYW6mjz.css → landing-proof-driven-ORyigZ6p.css} +13 -7
- package/dist/assets/legalContent-ZfFGMmi4.js +251 -0
- package/dist/assets/{manifold-D1LZIHqn.js → manifold-BRI5prcH.js} +1 -1
- package/dist/assets/{manifold-C2fwoTgd.js → manifold-C-3h2M7p.js} +2 -2
- package/dist/assets/{manifold-BTkzxi9V.js → manifold-DNkrUWpA.js} +1 -1
- package/dist/assets/{reportWorker-Cq1qGmg0.js → reportWorker-CdBz5bNg.js} +7537 -10856
- package/dist/assets/{scalar-sampling-budget-D9Qv_UlJ.js → scalar-sampling-budget-wJF98aY9.js} +6943 -4345
- package/dist/assets/{scanProxyWorker-Bs2TDgLw.js → scanProxyWorker-B-9VbLIs.js} +32 -1
- package/dist/assets/{renderSceneState-Dr0xPq1A.js → targets-B9sGB5nB.js} +27 -1
- package/dist/assets/{vendor-react-Da3A2QmU.js → vendor-react-6j1Kke-Y.js} +6 -5
- package/dist/cli/render.html +1 -1
- package/dist/docs/index.html +2 -2
- package/dist/docs-raw/AI/ai-native-cad.md +50 -0
- package/dist/docs-raw/AI/usage.md +9 -17
- package/dist/docs-raw/CLI.md +71 -21
- package/dist/docs-raw/component-model.md +27 -11
- package/dist/docs-raw/generated/assembly.md +301 -212
- package/dist/docs-raw/generated/concepts.md +238 -240
- package/dist/docs-raw/generated/core.md +283 -6
- package/dist/docs-raw/generated/curves.md +274 -361
- package/dist/docs-raw/generated/lib.md +7 -1
- package/dist/docs-raw/generated/output.md +19 -4
- package/dist/docs-raw/generated/runtime-names.md +41 -0
- package/dist/docs-raw/generated/sdf.md +31 -0
- package/dist/docs-raw/generated/sheet-metal.md +9 -0
- package/dist/docs-raw/generated/sketch.md +44 -1
- package/dist/docs-raw/generated/viewport.md +14 -6
- package/dist/docs-raw/guides/coordinate-system.md +20 -16
- package/dist/docs-raw/guides/geometry-conventions.md +2 -2
- package/dist/docs-raw/guides/inspection-bundles.md +2 -1
- package/dist/docs-raw/guides/joint-design.md +24 -0
- package/dist/docs-raw/guides/positioning.md +13 -3
- package/dist/docs-raw/legal/privacy.md +63 -0
- package/dist/docs-raw/legal/software-license.md +55 -0
- package/dist/docs-raw/legal/terms.md +87 -0
- package/dist/docs-raw/skills/forgecad-3d-reconstruction.md +3 -3
- package/dist/docs-raw/skills/forgecad-blockout-model.md +1 -1
- package/dist/docs-raw/skills/forgecad-component-model.md +11 -2
- package/dist/docs-raw/skills/forgecad-high-level-spec.md +1 -1
- package/dist/docs-raw/skills/forgecad-image-replicator.md +8 -8
- package/dist/docs-raw/skills/forgecad-lld.md +1 -1
- package/dist/docs-raw/skills/forgecad-make-a-model.md +4 -4
- package/dist/docs-raw/skills/forgecad-model-grader.md +2 -2
- package/dist/docs-raw/skills/forgecad-prepare-prompt.md +2 -2
- package/dist/docs-raw/skills/forgecad-project.md +1 -1
- package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +4 -4
- package/dist/docs-raw/skills/forgecad-render-inspect.md +4 -2
- package/dist/docs-raw/skills/forgecad-visual-spec.md +1 -1
- package/dist/docs-raw/skills/forgecad.md +4 -3
- package/dist/index.html +40 -12
- package/dist/llms.txt +8 -0
- package/dist/site.webmanifest +1 -1
- package/dist/sitemap.xml +49 -13
- package/dist-cli/{check-compiler-LOXCPEOI.js → check-compiler-SDX5QIXI.js} +1 -2
- package/dist-cli/{check-query-propagation-BAKNVWXR.js → check-query-propagation-EAYEFT77.js} +1 -2
- package/dist-cli/{chunk-RY43WF46.js → chunk-N4O47JLF.js} +13772 -9938
- package/dist-cli/forgecad.js +2387 -899
- package/dist-cli/{forgecad_geometry-GYVNKPIE.js → forgecad_geometry-QOQIIP53.js} +42 -1
- package/dist-cli/forgecad_geometry_bg.wasm +0 -0
- package/dist-cli/{solver-46FFSK2U.js → solver-OK4HECRH.js} +0 -1
- package/dist-skill/CONTEXT.md +1120 -724
- package/dist-skill/SKILL.md +3 -2
- package/dist-skill/docs/API/core/concepts.md +64 -1
- package/dist-skill/docs/CLI.md +71 -21
- package/dist-skill/docs/generated/assembly.md +277 -229
- package/dist-skill/docs/generated/core.md +283 -6
- package/dist-skill/docs/generated/curves.md +272 -362
- package/dist-skill/docs/generated/lib.md +7 -1
- package/dist-skill/docs/generated/output.md +19 -4
- package/dist-skill/docs/generated/runtime-names.md +41 -0
- package/dist-skill/docs/generated/sdf.md +31 -0
- package/dist-skill/docs/generated/sheet-metal.md +9 -0
- package/dist-skill/docs/generated/sketch.md +44 -2
- package/dist-skill/docs/generated/viewport.md +5 -90
- package/dist-skill/docs/guides/coordinate-system.md +20 -16
- package/dist-skill/docs/guides/geometry-conventions.md +2 -2
- package/dist-skill/docs/guides/inspection-bundles.md +2 -1
- package/dist-skill/docs/guides/joint-design.md +24 -0
- package/dist-skill/docs/guides/positioning.md +13 -3
- package/dist-skill/library/forgecad-3d-reconstruction/SKILL.md +2 -2
- package/dist-skill/library/forgecad-component-model/SKILL.md +10 -1
- package/dist-skill/library/forgecad-image-replicator/SKILL.md +6 -6
- package/dist-skill/library/forgecad-image-replicator/scripts/compare_images.py +166 -0
- package/dist-skill/library/forgecad-make-a-model/SKILL.md +3 -3
- package/dist-skill/library/forgecad-model-grader/SKILL.md +1 -1
- package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +1 -1
- package/dist-skill/library/forgecad-reconstruction-benchmark/SKILL.md +3 -3
- package/dist-skill/library/forgecad-render-inspect/SKILL.md +3 -1
- package/examples/api/assembly-kinematics-foundation.forge.js +65 -0
- package/examples/api/assembly-kinematics-four-bar.forge.js +115 -0
- package/examples/api/assembly-kinematics-limb.forge.js +116 -0
- package/examples/api/connector-frame-rig-chain.forge.js +102 -0
- package/examples/api/exact-sheet-shell-assembly.forge.js +0 -2
- package/examples/api/exact-surface-studio.forge.js +6 -8
- package/examples/api/helix-basics.forge.js +6 -6
- package/examples/api/lean-foundations/README.md +12 -0
- package/examples/api/lean-foundations/curve-blend-exact.forge.js +22 -0
- package/examples/api/lean-foundations/curve-fit-interpolation.forge.js +18 -0
- package/examples/api/lean-foundations/curve-helix-canonicalization.forge.js +27 -0
- package/examples/api/lean-foundations/curve-route-canonicalization.forge.js +16 -0
- package/examples/api/lean-foundations/curve-trim-reverse.forge.js +24 -0
- package/examples/api/lean-foundations/exact-curve-arc.forge.js +36 -0
- package/examples/api/mixed-edge-finishes-proof.forge.js +8 -11
- package/examples/api/route3d-elbow.forge.js +68 -0
- package/examples/api/transition-curves.forge.js +44 -15
- package/examples/api/y-blend-corner-showcase.forge.js +0 -2
- package/examples/generative/coral-vase.forge.js +1 -1
- package/examples/nurbs-tube.forge.js +1 -1
- package/package.json +14 -18
- package/dist/assets/EditorApp-CP9Za6tm.js +0 -13630
- package/dist/assets/app-CsHnaBWt.css +0 -1789
- package/dist/docs-raw/API/README.md +0 -16
- package/dist/docs-raw/API/core/concepts.md +0 -118
- package/dist/docs-raw/INDEX.md +0 -138
- package/dist/docs-raw/RELEASING.md +0 -87
- package/dist/docs-raw/agent-native-api.md +0 -27
- package/dist/docs-raw/beta-deployment.md +0 -304
- package/dist/docs-raw/beta-operations.md +0 -325
- package/dist/docs-raw/blueprint-first.md +0 -145
- package/dist/docs-raw/cli-monetization.md +0 -112
- package/dist/docs-raw/coding-best-practices.md +0 -120
- package/dist/docs-raw/coding.md +0 -340
- package/dist/docs-raw/deployment.md +0 -374
- package/dist/docs-raw/guides/skill-maintenance.md +0 -161
- package/dist/docs-raw/guides/surface-members.md +0 -82
- package/dist/docs-raw/internals/backend-vocabulary.md +0 -35
- package/dist/docs-raw/internals/compiler.md +0 -307
- package/dist/docs-raw/internals/constraint-solver-quality.md +0 -161
- package/dist/docs-raw/internals/constraint-solver.md +0 -176
- package/dist/docs-raw/internals/shape-from-slices.md +0 -152
- package/dist/docs-raw/internals/sketch-2d-pipeline.md +0 -108
- package/dist/docs-raw/platform/admin.md +0 -45
- package/dist/docs-raw/platform/architecture.md +0 -82
- package/dist/docs-raw/platform/auth.md +0 -139
- package/dist/docs-raw/platform/email.md +0 -67
- package/dist/docs-raw/platform/google-oauth-setup.md +0 -88
- package/dist/docs-raw/platform/observability.md +0 -197
- package/dist/docs-raw/platform/projects.md +0 -111
- package/dist/docs-raw/platform/sharing.md +0 -90
- package/dist/docs-raw/product/README.md +0 -39
- package/dist/docs-raw/product/api-as-product-language.md +0 -13
- package/dist/docs-raw/product/business-model.md +0 -15
- package/dist/docs-raw/product/competitive-positioning.md +0 -17
- package/dist/docs-raw/product/creative-manufacturing.md +0 -15
- package/dist/docs-raw/product/founder-story.md +0 -11
- package/dist/docs-raw/product/manufacturing-workflows.md +0 -15
- package/dist/docs-raw/product/onboarding-first-experience.md +0 -256
- package/dist/docs-raw/product/product-loop.md +0 -17
- package/dist/docs-raw/product/strategic-decisions.md +0 -22
- package/dist/docs-raw/product/user-outreach-email-templates.md +0 -161
- package/dist/docs-raw/product/user-segments.md +0 -15
- package/dist/docs-raw/product/vision.md +0 -26
- package/dist/docs-raw/rl-environments.md +0 -508
- package/dist/docs-raw/runbook.md +0 -611
- package/dist-cli/check-compiler-LOXCPEOI.js.map +0 -1
- package/dist-cli/check-query-propagation-BAKNVWXR.js.map +0 -1
- package/dist-cli/chunk-RY43WF46.js.map +0 -1
- package/dist-cli/forgecad.js.map +0 -1
- package/dist-cli/forgecad_geometry-GYVNKPIE.js.map +0 -1
- package/dist-cli/solver-46FFSK2U.js.map +0 -1
- package/dist-skill/SKILL-dev.md +0 -145
- package/dist-skill/docs-dev/API/core/concepts.md +0 -118
- package/dist-skill/docs-dev/CLI.md +0 -647
- package/dist-skill/docs-dev/agent-native-api.md +0 -27
- package/dist-skill/docs-dev/blueprint-first.md +0 -145
- package/dist-skill/docs-dev/coding-best-practices.md +0 -120
- package/dist-skill/docs-dev/coding.md +0 -340
- package/dist-skill/docs-dev/component-model.md +0 -164
- package/dist-skill/docs-dev/generated/assembly.md +0 -794
- package/dist-skill/docs-dev/generated/core.md +0 -2117
- package/dist-skill/docs-dev/generated/curves.md +0 -2583
- package/dist-skill/docs-dev/generated/lib.md +0 -169
- package/dist-skill/docs-dev/generated/output.md +0 -247
- package/dist-skill/docs-dev/generated/sdf.md +0 -446
- package/dist-skill/docs-dev/generated/sheet-metal.md +0 -504
- package/dist-skill/docs-dev/generated/sketch.md +0 -1811
- package/dist-skill/docs-dev/generated/viewport.md +0 -585
- package/dist-skill/docs-dev/generated/wood.md +0 -108
- package/dist-skill/docs-dev/guides/coordinate-system.md +0 -46
- package/dist-skill/docs-dev/guides/geometry-conventions.md +0 -52
- package/dist-skill/docs-dev/guides/inspection-bundles.md +0 -485
- package/dist-skill/docs-dev/guides/joint-design.md +0 -78
- package/dist-skill/docs-dev/guides/modeling-recipes.md +0 -78
- package/dist-skill/docs-dev/guides/positioning.md +0 -161
- package/dist-skill/docs-dev/guides/skill-maintenance.md +0 -161
- package/dist-skill/docs-dev/internals/backend-vocabulary.md +0 -35
- package/dist-skill/docs-dev/internals/compiler.md +0 -307
- package/dist-skill/docs-dev/internals/constraint-solver-quality.md +0 -161
- package/dist-skill/docs-dev/internals/constraint-solver.md +0 -176
- package/dist-skill/docs-dev/internals/sketch-2d-pipeline.md +0 -108
- package/dist-skill/library/forgecad-image-replicator/scripts/compare_images.mjs +0 -289
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Connector-frame rig chain: serial articulated parts need full connector frames.
|
|
2
|
+
//
|
|
3
|
+
// Use link() graphs for solved point skeletons and closed loops. Use connect()
|
|
4
|
+
// when real geometry must inherit orientation from a joint frame. Here the upper
|
|
5
|
+
// leg's local +X axis is locked by connector up vectors, so the part points from
|
|
6
|
+
// the hip pivot toward the knee pivot at every solved pose. Return the Assembly
|
|
7
|
+
// itself so editor controls call solve(state) instead of stacking viewport FK.
|
|
8
|
+
|
|
9
|
+
const DEFAULT_POSE = { hip: 22, knee: -42, wheel: 0 };
|
|
10
|
+
|
|
11
|
+
const upperLength = 68;
|
|
12
|
+
const lowerLength = 58;
|
|
13
|
+
const legWidth = 10;
|
|
14
|
+
const legThickness = 8;
|
|
15
|
+
const hubRadius = 9;
|
|
16
|
+
const wheelRadius = 16;
|
|
17
|
+
const wheelThickness = 8;
|
|
18
|
+
|
|
19
|
+
function hingeConnector(origin, axis, up, type = "planar-hinge") {
|
|
20
|
+
return connector(type, { origin, axis, up, kind: "revolute" });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function legSegment(length, colorHex) {
|
|
24
|
+
const body = box(length, legWidth, legThickness).translate(length / 2, 0, -legThickness / 2);
|
|
25
|
+
const nearHub = cylinder(legThickness, hubRadius).translate(0, 0, -legThickness / 2);
|
|
26
|
+
const farHub = cylinder(legThickness, hubRadius).translate(length, 0, -legThickness / 2);
|
|
27
|
+
|
|
28
|
+
return group(
|
|
29
|
+
{ name: "Body", shape: body },
|
|
30
|
+
{ name: "Near hub", shape: nearHub },
|
|
31
|
+
{ name: "Far hub", shape: farHub },
|
|
32
|
+
).color(colorHex);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const hipDrum = group({
|
|
36
|
+
name: "Drum",
|
|
37
|
+
shape: cylinder(18, 14).translate(0, 0, -9).color("#495057"),
|
|
38
|
+
}).withConnectors({
|
|
39
|
+
hip: hingeConnector([0, 0, 0], [0, 0, 1], [1, 0, 0]),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const upperLeg = legSegment(upperLength, "#d55e5e").withConnectors({
|
|
43
|
+
hip: hingeConnector([0, 0, 0], [0, 0, -1], [1, 0, 0]),
|
|
44
|
+
knee: hingeConnector([upperLength, 0, 0], [0, 0, 1], [1, 0, 0]),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const lowerLeg = legSegment(lowerLength, "#3d87c7").withConnectors({
|
|
48
|
+
knee: hingeConnector([0, 0, 0], [0, 0, -1], [1, 0, 0]),
|
|
49
|
+
axle: hingeConnector([lowerLength, 0, 0], [0, 1, 0], [1, 0, 0], "wheel-axle"),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const wheel = group({
|
|
53
|
+
name: "Wheel",
|
|
54
|
+
shape: cylinder(wheelThickness, wheelRadius)
|
|
55
|
+
.pointAlong([0, 1, 0])
|
|
56
|
+
.translate(0, -wheelThickness / 2, 0)
|
|
57
|
+
.color("#212529"),
|
|
58
|
+
}).withConnectors({
|
|
59
|
+
axle: hingeConnector([0, 0, 0], [0, -1, 0], [1, 0, 0], "wheel-axle"),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const rig = assembly("Connector Frame Rig Chain")
|
|
63
|
+
.addPart("Hip Drum", hipDrum)
|
|
64
|
+
.addPart("Upper Leg", upperLeg)
|
|
65
|
+
.addPart("Lower Leg", lowerLeg)
|
|
66
|
+
.addPart("Wheel", wheel)
|
|
67
|
+
.connect("Hip Drum.hip", "Upper Leg.hip", { as: "hip", min: -35, max: 55, default: DEFAULT_POSE.hip })
|
|
68
|
+
.connect("Upper Leg.knee", "Lower Leg.knee", { as: "knee", min: -90, max: 20, default: DEFAULT_POSE.knee })
|
|
69
|
+
.connect("Lower Leg.axle", "Wheel.axle", { as: "wheel", min: -180, max: 180, default: DEFAULT_POSE.wheel });
|
|
70
|
+
|
|
71
|
+
const solved = rig.solve(DEFAULT_POSE);
|
|
72
|
+
|
|
73
|
+
const upper = solved.getTransform("Upper Leg");
|
|
74
|
+
const lower = solved.getTransform("Lower Leg");
|
|
75
|
+
const wheelTx = solved.getTransform("Wheel");
|
|
76
|
+
|
|
77
|
+
const hip = upper.point([0, 0, 0]);
|
|
78
|
+
const kneeFromUpper = upper.point([upperLength, 0, 0]);
|
|
79
|
+
const kneeFromLower = lower.point([0, 0, 0]);
|
|
80
|
+
const axleFromLower = lower.point([lowerLength, 0, 0]);
|
|
81
|
+
const axleFromWheel = wheelTx.point([0, 0, 0]);
|
|
82
|
+
|
|
83
|
+
const upperLocalX = upper.vector([1, 0, 0]);
|
|
84
|
+
const hipToKnee = Points.direction(hip, kneeFromUpper);
|
|
85
|
+
|
|
86
|
+
verify.equal("knee connector origins coincide", Points.distance(kneeFromUpper, kneeFromLower), 0, 0.001);
|
|
87
|
+
verify.equal("wheel axle connector origins coincide", Points.distance(axleFromLower, axleFromWheel), 0, 0.001);
|
|
88
|
+
verify.equal("upper leg local +X points hip to knee", Points.distance(upperLocalX, hipToKnee), 0, 0.001);
|
|
89
|
+
|
|
90
|
+
scene({
|
|
91
|
+
background: { top: "#edf2f7", bottom: "#ffffff" },
|
|
92
|
+
camera: { position: [115, -190, 120], target: [45, 12, 0], fov: 34 },
|
|
93
|
+
environment: { preset: "studio", intensity: 0.45 },
|
|
94
|
+
lights: [
|
|
95
|
+
{ type: "ambient", color: "#e8edf5", intensity: 0.25 },
|
|
96
|
+
{ type: "directional", position: [120, -140, 180], target: [35, 5, 0], color: "#fff5e6", intensity: 1.6, castShadow: true },
|
|
97
|
+
{ type: "directional", position: [-80, 100, 120], target: [40, 15, 0], color: "#c9ddff", intensity: 0.7 },
|
|
98
|
+
],
|
|
99
|
+
ground: { visible: true, color: "#f6f8fa", height: -wheelRadius, receiveShadow: true },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return rig;
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
if (getActiveBackend() === 'manifold') setActiveBackend('truck');
|
|
2
|
-
|
|
3
1
|
const span = param('Span', 128);
|
|
4
2
|
const depth = param('Depth', 88);
|
|
5
3
|
const crown = param('Crown', 26);
|
|
@@ -41,7 +39,7 @@ const canopySheet = Surface.Nurbs(
|
|
|
41
39
|
const canopySolid = canopySheet.thicken(wall).translate(-span * 0.82, 0, 0).color('#9fd3ff');
|
|
42
40
|
const canopyGhost = canopySheet.translate(-span * 0.82, 0, wall * 2.4).color('#d9f0ff');
|
|
43
41
|
|
|
44
|
-
const archFront =
|
|
42
|
+
const archFront = Curve.Nurbs(
|
|
45
43
|
[
|
|
46
44
|
[-halfSpan * 0.55, -halfDepth * 0.58, 2],
|
|
47
45
|
[-halfSpan * 0.18, -halfDepth * 0.58, crown * 0.95],
|
|
@@ -50,7 +48,7 @@ const archFront = nurbs3d(
|
|
|
50
48
|
],
|
|
51
49
|
{ degree: 3 },
|
|
52
50
|
);
|
|
53
|
-
const archBack =
|
|
51
|
+
const archBack = Curve.Nurbs(
|
|
54
52
|
[
|
|
55
53
|
[-halfSpan * 0.48, halfDepth * 0.52, 0],
|
|
56
54
|
[-halfSpan * 0.14, halfDepth * 0.52, crown * 0.68],
|
|
@@ -65,7 +63,7 @@ const ruledRibbon = Surface.Ruled(archFront, archBack)
|
|
|
65
63
|
|
|
66
64
|
const boundaryPatch = Surface.Patch(
|
|
67
65
|
{
|
|
68
|
-
bottom:
|
|
66
|
+
bottom: Curve.Nurbs(
|
|
69
67
|
[
|
|
70
68
|
[-halfSpan * 0.48, -halfDepth * 0.42, -2],
|
|
71
69
|
[-halfSpan * 0.16, -halfDepth * 0.46, crown * 0.35],
|
|
@@ -74,7 +72,7 @@ const boundaryPatch = Surface.Patch(
|
|
|
74
72
|
],
|
|
75
73
|
{ degree: 3 },
|
|
76
74
|
),
|
|
77
|
-
top:
|
|
75
|
+
top: Curve.Nurbs(
|
|
78
76
|
[
|
|
79
77
|
[-halfSpan * 0.48, halfDepth * 0.4, 1],
|
|
80
78
|
[-halfSpan * 0.16, halfDepth * 0.43, crown * 0.62],
|
|
@@ -83,7 +81,7 @@ const boundaryPatch = Surface.Patch(
|
|
|
83
81
|
],
|
|
84
82
|
{ degree: 3 },
|
|
85
83
|
),
|
|
86
|
-
left:
|
|
84
|
+
left: Curve.Nurbs(
|
|
87
85
|
[
|
|
88
86
|
[-halfSpan * 0.48, -halfDepth * 0.42, -2],
|
|
89
87
|
[-halfSpan * 0.58, -halfDepth * 0.12, crown * 0.22],
|
|
@@ -92,7 +90,7 @@ const boundaryPatch = Surface.Patch(
|
|
|
92
90
|
],
|
|
93
91
|
{ degree: 3 },
|
|
94
92
|
),
|
|
95
|
-
right:
|
|
93
|
+
right: Curve.Nurbs(
|
|
96
94
|
[
|
|
97
95
|
[halfSpan * 0.48, -halfDepth * 0.42, -2],
|
|
98
96
|
[halfSpan * 0.58, -halfDepth * 0.12, crown * 0.22],
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
// Helix API basics.
|
|
1
|
+
// Curve.Helix API basics.
|
|
2
2
|
//
|
|
3
|
-
// Helix.path() is the reusable centerline primitive.
|
|
4
|
-
// Helix.coil() turns the same helix definition into a solid helix-oriented coil.
|
|
3
|
+
// Curve.Helix.path() is the reusable centerline primitive.
|
|
4
|
+
// Curve.Helix.coil() turns the same helix definition into a solid helix-oriented coil.
|
|
5
5
|
|
|
6
6
|
const radius = Param.number("Radius", 18, { min: 8, max: 32, step: 1, unit: "mm" });
|
|
7
7
|
const pitch = Param.number("Pitch", 6, { min: 2, max: 12, step: 0.5, unit: "mm" });
|
|
8
8
|
const turns = Param.number("Turns", 4, { min: 1, max: 7, step: 0.25 });
|
|
9
9
|
const wireRadius = Param.number("Wire Radius", 1, { min: 0.25, max: 2.5, step: 0.25, unit: "mm" });
|
|
10
10
|
|
|
11
|
-
const guide = Helix.path({
|
|
11
|
+
const guide = Curve.Helix.path({
|
|
12
12
|
radius,
|
|
13
13
|
pitch,
|
|
14
14
|
turns,
|
|
@@ -16,7 +16,7 @@ const guide = Helix.path({
|
|
|
16
16
|
samplesPerTurn: 48,
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
const circularWireCoil = Helix.coil({
|
|
19
|
+
const circularWireCoil = Curve.Helix.coil({
|
|
20
20
|
radius,
|
|
21
21
|
pitch,
|
|
22
22
|
turns,
|
|
@@ -26,7 +26,7 @@ const circularWireCoil = Helix.coil({
|
|
|
26
26
|
}).translate(-70, 0, 0);
|
|
27
27
|
|
|
28
28
|
const rectangularProfile = rect(wireRadius * 2.4, wireRadius * 1.1);
|
|
29
|
-
const rectangularWireCoil = Helix.coil(rectangularProfile, {
|
|
29
|
+
const rectangularWireCoil = Curve.Helix.coil(rectangularProfile, {
|
|
30
30
|
radius,
|
|
31
31
|
height: guide.height,
|
|
32
32
|
turns,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Selected Lean Foundations Examples
|
|
2
|
+
|
|
3
|
+
This pack contains only the currently selected lean API buckets: Curve / Route and route endpoint canonicalization. Surface, standalone Frame, FaceRef, and topology-selector expansion examples were intentionally left out of this split.
|
|
4
|
+
|
|
5
|
+
Run these with `node dist-cli/forgecad.js run <file>`:
|
|
6
|
+
|
|
7
|
+
- `curve-blend-exact.forge.js` - `Curve.Blend` and `Curve.BlendG2` as exact reference rails that also feed `sweep`.
|
|
8
|
+
- `curve-fit-interpolation.forge.js` - `Curve.Fit` interpolation with residual validation.
|
|
9
|
+
- `curve-helix-canonicalization.forge.js` - `Curve.Helix` path and coil canonicalization.
|
|
10
|
+
- `curve-route-canonicalization.forge.js` - `Curve.Route.fromPolyline()` segment and port preservation.
|
|
11
|
+
- `curve-trim-reverse.forge.js` - exact trim and reverse operations.
|
|
12
|
+
- `exact-curve-arc.forge.js` - exact `Curve.Arc` construction and sweep use.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const g1Rail = Curve.Blend(
|
|
2
|
+
{ point: [0, 0, 0], tangent: [1, 0, 0], weight: 0.7 },
|
|
3
|
+
{ point: [48, 22, 10], tangent: [0, 1, 0], weight: 0.65 },
|
|
4
|
+
);
|
|
5
|
+
|
|
6
|
+
const g2Rail = Curve.BlendG2(
|
|
7
|
+
{ point: [0, 18, 0], tangent: [1, 0, 0], curvature: [0, 0.01, 0], weight: 0.55 },
|
|
8
|
+
{ point: [48, 42, 10], tangent: [0, 1, 0], curvature: [-0.01, 0, 0], weight: 0.55 },
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const g1Tube = sweep(circle2d(1.6), g1Rail);
|
|
12
|
+
const g2Tube = sweep(circle2d(1.2), g2Rail).color("#5a8f7b");
|
|
13
|
+
|
|
14
|
+
const close3d = (a, b, tolerance = 1e-6) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]) <= tolerance;
|
|
15
|
+
|
|
16
|
+
verify.equal("Curve.Blend returns a cubic NURBS", g1Rail.degree, 3);
|
|
17
|
+
verify.equal("Curve.BlendG2 returns a quintic NURBS", g2Rail.degree, 5);
|
|
18
|
+
verify.that("Curve.Blend hits directed endpoints", () => close3d(g1Rail.pointAt(0), [0, 0, 0]) && close3d(g1Rail.pointAt(1), [48, 22, 10]));
|
|
19
|
+
verify.that("Curve.BlendG2 hits directed endpoints", () => close3d(g2Rail.pointAt(0), [0, 18, 0]) && close3d(g2Rail.pointAt(1), [48, 42, 10]));
|
|
20
|
+
verify.greaterThan("Blend rails are usable as sweep paths", g1Tube.boundingBox().max[0], 45);
|
|
21
|
+
|
|
22
|
+
return group(g1Tube, g2Tube);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const fittedRail = Curve.Fit(
|
|
2
|
+
[
|
|
3
|
+
[0, 0, 0],
|
|
4
|
+
[16, 8, 10],
|
|
5
|
+
[36, -6, 18],
|
|
6
|
+
[58, 10, 12],
|
|
7
|
+
[78, 0, 0],
|
|
8
|
+
],
|
|
9
|
+
{ degree: 3, tolerance: 0.001 },
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const tube = sweep(circle2d(2), fittedRail);
|
|
13
|
+
|
|
14
|
+
verify.equal('Curve.Fit returns a cubic NURBS for this rail', fittedRail.degree, 3);
|
|
15
|
+
verify.equal('Curve.Fit keeps one control point per interpolated point', fittedRail.controlPoints.length, 5);
|
|
16
|
+
verify.greaterThan('Curve.Fit rail has positive length', fittedRail.length(), 0);
|
|
17
|
+
|
|
18
|
+
return tube;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const guide = Curve.Helix.path({
|
|
2
|
+
radius: 14,
|
|
3
|
+
pitch: 5,
|
|
4
|
+
turns: 3,
|
|
5
|
+
samplesPerTurn: 30,
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const roundCoil = Curve.Helix.coil({
|
|
9
|
+
radius: 14,
|
|
10
|
+
pitch: 5,
|
|
11
|
+
turns: 3,
|
|
12
|
+
wireRadius: 0.8,
|
|
13
|
+
profileSegments: 16,
|
|
14
|
+
divisionsPerTurn: 100,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const rectangularCoil = Curve.Helix.coil(rect(1.2, 0.8), {
|
|
18
|
+
radius: 20,
|
|
19
|
+
height: 15,
|
|
20
|
+
turns: 3,
|
|
21
|
+
divisionsPerTurn: 18,
|
|
22
|
+
}).translate(50, 0, 0);
|
|
23
|
+
|
|
24
|
+
verify.equal('Curve.Helix guide derives height from pitch and turns', guide.height, 15);
|
|
25
|
+
verify.greaterThan('Curve.Helix guide has positive centerline length', guide.length(), 0);
|
|
26
|
+
|
|
27
|
+
return union(roundCoil, rectangularCoil);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const route = Curve.Route.fromPolyline(
|
|
2
|
+
[
|
|
3
|
+
[0, 0, 0],
|
|
4
|
+
[0, 0, 48],
|
|
5
|
+
[36, 0, 48],
|
|
6
|
+
[36, 24, 72],
|
|
7
|
+
],
|
|
8
|
+
{ cornerRadius: 10, startPort: 'inlet', endPort: 'outlet' },
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const tube = sweep(difference2d(circle2d(5), circle2d(3.5)), route);
|
|
12
|
+
verify.equal('Curve.Route keeps line-arc-line-arc-line route intent', route.segments.length, 5);
|
|
13
|
+
verify.equal('Curve.Route exposes named ports', Object.keys(route.ports).length, 2);
|
|
14
|
+
verify.greaterThan('Curve.Route remains usable as a sweep path', tube.boundingBox().max[2], 70);
|
|
15
|
+
|
|
16
|
+
return route;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const rail = Curve.Fit(
|
|
2
|
+
[
|
|
3
|
+
[0, 0, 0],
|
|
4
|
+
[24, 10, 8],
|
|
5
|
+
[54, -8, 12],
|
|
6
|
+
[84, 0, 0],
|
|
7
|
+
],
|
|
8
|
+
{ degree: 3 },
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const middle = Curve.Trim(rail, 0.2, 0.8);
|
|
12
|
+
const reversed = Curve.Reverse(middle);
|
|
13
|
+
|
|
14
|
+
const middleTube = sweep(circle2d(1.2), middle).color("#5f8cc0");
|
|
15
|
+
const reversedTube = sweep(circle2d(0.8), reversed).translate(0, 18, 0).color("#c06f5f");
|
|
16
|
+
|
|
17
|
+
const close3d = (a, b, tolerance = 1e-6) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]) <= tolerance;
|
|
18
|
+
|
|
19
|
+
verify.that("Curve.Trim starts at the source subdomain", () => close3d(middle.pointAt(0), rail.pointAt(0.2)));
|
|
20
|
+
verify.that("Curve.Trim ends at the source subdomain", () => close3d(middle.pointAt(1), rail.pointAt(0.8)));
|
|
21
|
+
verify.that("Curve.Reverse preserves geometry with opposite parameter direction", () => close3d(reversed.pointAt(0), middle.pointAt(1)));
|
|
22
|
+
verify.greaterThan("Trimmed curves feed exact sweep rails", middleTube.boundingBox().max[0], 60);
|
|
23
|
+
|
|
24
|
+
return group(middleTube, reversedTube);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const lower = Curve.Arc({
|
|
2
|
+
start: [36, 0, 0],
|
|
3
|
+
end: [0, 36, 0],
|
|
4
|
+
tangent: [0, 1, 0],
|
|
5
|
+
});
|
|
6
|
+
const upper = Curve.Arc({
|
|
7
|
+
start: [36, 0, 10],
|
|
8
|
+
end: [0, 36, 10],
|
|
9
|
+
tangent: [0, 1, 0],
|
|
10
|
+
});
|
|
11
|
+
const half = Curve.Arc({
|
|
12
|
+
start: [12, 0, 18],
|
|
13
|
+
end: [-12, 0, 18],
|
|
14
|
+
tangent: [0, 1, 0],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const tube = sweep(circle2d(0.8), lower);
|
|
18
|
+
const upperTube = sweep(circle2d(0.5), upper).color("#5a8f7b");
|
|
19
|
+
|
|
20
|
+
const close3d = (a, b, tolerance = 1e-6) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]) <= tolerance;
|
|
21
|
+
|
|
22
|
+
verify.that("Curve.Arc hits start, midpoint, and end exactly", () =>
|
|
23
|
+
close3d(lower.pointAt(0), [36, 0, 0]) &&
|
|
24
|
+
close3d(lower.pointAt(0.5), [36 * Math.SQRT1_2, 36 * Math.SQRT1_2, 0]) &&
|
|
25
|
+
close3d(lower.pointAt(1), [0, 36, 0]));
|
|
26
|
+
|
|
27
|
+
verify.that("Curve.Arc emits stable rational spans for half circles", () =>
|
|
28
|
+
half.controlPoints.length === 5 && close3d(half.pointAt(0.5), [0, 12, 18]));
|
|
29
|
+
|
|
30
|
+
verify.that("Curve.Arc feeds sweep as an exact NURBS rail", () =>
|
|
31
|
+
tube.geometryInfo().backend !== "unknown" && tube.boundingBox().max[0] > 30 && tube.boundingBox().max[1] > 30);
|
|
32
|
+
|
|
33
|
+
verify.that("Multiple Curve.Arc rails remain reusable sweep inputs", () =>
|
|
34
|
+
upperTube.boundingBox().max[2] > 10 && upperTube.boundingBox().max[0] > 30);
|
|
35
|
+
|
|
36
|
+
return group(tube, upperTube);
|
|
@@ -27,17 +27,14 @@ const body = plate.subtract(cutout);
|
|
|
27
27
|
const bodyEdges = selectEdges(body);
|
|
28
28
|
const raw = body.color('#7b8490');
|
|
29
29
|
const chamfered = chamfer(body, 0.75, bodyEdges).color('#d3a33a');
|
|
30
|
-
|
|
30
|
+
// Native Truck fillet now produces analytic rolling-ball geometry (cylinders,
|
|
31
|
+
// spheres, and toroidal corner blends) that exports to STEP canonically
|
|
32
|
+
// identical to OpenCascade. See:
|
|
33
|
+
// forgecad export step examples/api/mixed-edge-finishes-proof.forge.js --backend truck
|
|
34
|
+
const filleted = fillet(body, 0.75, bodyEdges).color('#4e8fd6');
|
|
35
|
+
|
|
36
|
+
return [
|
|
31
37
|
{ name: 'Raw', shape: raw.translate(-80, 0, 0) },
|
|
32
38
|
{ name: 'Chamfered', shape: chamfered.translate(30, 0, 0) },
|
|
39
|
+
{ name: 'Filleted', shape: filleted.translate(140, 0, 0) },
|
|
33
40
|
];
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const filleted = fillet(body, 0.75, bodyEdges).color('#4e8fd6');
|
|
37
|
-
filleted.volume();
|
|
38
|
-
shapes.push({ name: 'Filleted', shape: filleted.translate(140, 0, 0) });
|
|
39
|
-
} catch (error) {
|
|
40
|
-
console.log(`Filleted: unsupported in native Truck (${error instanceof Error ? error.message : String(error)})`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return shapes;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Curve.Route elbow - demonstrates virtual 3D route corners, named route ports,
|
|
2
|
+
// and sweeping an annular profile without hand-computing tangent points.
|
|
3
|
+
|
|
4
|
+
const route = Curve.Route.fromPolyline(
|
|
5
|
+
[
|
|
6
|
+
[0, 0, 0],
|
|
7
|
+
[0, 0, 203.2],
|
|
8
|
+
[203.2, 0, 203.2],
|
|
9
|
+
],
|
|
10
|
+
{ cornerRadius: 114.3, startPort: 'inlet', endPort: 'outlet' },
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const outerRadius = 44.45;
|
|
14
|
+
const innerRadius = 38.96;
|
|
15
|
+
const flangeRadius = 89.0;
|
|
16
|
+
const boltCircleRadius = 72.3;
|
|
17
|
+
const boltRadius = 5.3;
|
|
18
|
+
const flangeThickness = 17.8;
|
|
19
|
+
const neckLength = 26.645;
|
|
20
|
+
const neckRadius = 55.6;
|
|
21
|
+
|
|
22
|
+
const tubeProfile = difference2d(circle2d(outerRadius, 80), circle2d(innerRadius, 80));
|
|
23
|
+
const pipe = sweep(tubeProfile, route, { samples: 72 });
|
|
24
|
+
|
|
25
|
+
function placeAtPort(shape, port, offset) {
|
|
26
|
+
return shape.pointAlong(port.axis).translate(
|
|
27
|
+
port.origin[0] + port.axis[0] * offset,
|
|
28
|
+
port.origin[1] + port.axis[1] * offset,
|
|
29
|
+
port.origin[2] + port.axis[2] * offset,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function flangePlate() {
|
|
34
|
+
const bore = cylinder(flangeThickness + 2, innerRadius, undefined, 80).translate(0, 0, -1);
|
|
35
|
+
const firstBolt = cylinder(flangeThickness + 2, boltRadius, undefined, 24).translate(boltCircleRadius, 0, -1);
|
|
36
|
+
const bolts = circularPattern(firstBolt, 5);
|
|
37
|
+
return cylinder(flangeThickness, flangeRadius, undefined, 96).subtract(bore, bolts);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function neck() {
|
|
41
|
+
const bore = cylinder(neckLength + 2, innerRadius, undefined, 80).translate(0, 0, -1);
|
|
42
|
+
return cylinder(neckLength, neckRadius, undefined, 80).subtract(bore);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const inlet = route.port('inlet');
|
|
46
|
+
const outlet = route.port('outlet');
|
|
47
|
+
const body = union(
|
|
48
|
+
pipe,
|
|
49
|
+
placeAtPort(neck(), inlet, -4.445),
|
|
50
|
+
placeAtPort(flangePlate(), inlet, 20.0),
|
|
51
|
+
placeAtPort(neck(), outlet, -4.445),
|
|
52
|
+
placeAtPort(flangePlate(), outlet, 20.0),
|
|
53
|
+
).color('#b9c0c8');
|
|
54
|
+
|
|
55
|
+
const bbox = body.boundingBox();
|
|
56
|
+
const bboxSize = [bbox.max[0] - bbox.min[0], bbox.max[1] - bbox.min[1], bbox.max[2] - bbox.min[2]];
|
|
57
|
+
|
|
58
|
+
verify.equal('Curve.Route elbow keeps line-arc-line route intent', route.segments.length, 3);
|
|
59
|
+
verify.equal('Curve.Route elbow exposes inlet and outlet ports', Object.keys(route.ports).length, 2);
|
|
60
|
+
verify.boundingBoxSize('Curve.Route elbow preserves BenchCAD-scale envelope', body, [337, 178, 330], 18);
|
|
61
|
+
verify.that(
|
|
62
|
+
'Curve.Route elbow bbox stays in expected quadrants',
|
|
63
|
+
() => bbox.min[0] < -40 && bbox.max[0] > 235 && bbox.min[2] < -35 && bbox.max[2] > 245 && bboxSize[1] > 175,
|
|
64
|
+
`bbox was ${JSON.stringify(bbox)}`,
|
|
65
|
+
);
|
|
66
|
+
verify.physicalComponentCount('Curve.Route elbow is one connected component', 1);
|
|
67
|
+
|
|
68
|
+
return body;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Demonstrates
|
|
3
|
-
// and weighted blending (G1 + G2 continuity).
|
|
1
|
+
// Curve blends — smooth connections between edges
|
|
2
|
+
// Demonstrates Curve.Blend(), sweep(), local edge recipes, and weighted blending.
|
|
4
3
|
|
|
5
4
|
const weight = Param.number("Weight A", 1.0, { min: 0.1, max: 5.0, step: 0.1 });
|
|
6
5
|
const weightB = Param.number("Weight B", 1.0, { min: 0.1, max: 5.0, step: 0.1 });
|
|
@@ -12,9 +11,43 @@ function anchor(pos) {
|
|
|
12
11
|
return box(s, s, s).translate(pos[0], pos[1], pos[2]);
|
|
13
12
|
}
|
|
14
13
|
|
|
14
|
+
function blendTube(edgeA, edgeB, options = {}) {
|
|
15
|
+
const curve = Curve.Blend(
|
|
16
|
+
{ point: edgeA.point, tangent: edgeA.tangent, weight: options.weightA },
|
|
17
|
+
{ point: edgeB.point, tangent: edgeB.tangent, weight: options.weightB },
|
|
18
|
+
);
|
|
19
|
+
const profile = options.profile ?? circle2d(options.radius ?? radius);
|
|
20
|
+
return sweep(profile, curve, { samples: options.samples, up: options.up });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalized(vec, fallback = [1, 0, 0]) {
|
|
24
|
+
const length = Math.hypot(vec[0], vec[1], vec[2]);
|
|
25
|
+
return length > 1e-9 ? [vec[0] / length, vec[1] / length, vec[2] / length] : fallback;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function edgePoint(edge, which = 'mid') {
|
|
29
|
+
if (which === 'start') return edge.start;
|
|
30
|
+
if (which === 'end') return edge.end;
|
|
31
|
+
return edge.midpoint;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function edgeOutward(edge) {
|
|
35
|
+
const a = edge.normalA ?? [0, 0, 0];
|
|
36
|
+
const b = edge.normalB ?? [0, 0, 0];
|
|
37
|
+
return normalized([a[0] + b[0], a[1] + b[1], a[2] + b[2]], edge.direction ?? [1, 0, 0]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function edgeBlendTube(edgeA, edgeB, options = {}) {
|
|
41
|
+
return blendTube(
|
|
42
|
+
{ point: edgePoint(edgeA, options.endA), tangent: edgeOutward(edgeA) },
|
|
43
|
+
{ point: edgePoint(edgeB, options.endB), tangent: edgeOutward(edgeB) },
|
|
44
|
+
options,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
15
48
|
// ── 1. L-bend: 90-degree pipe turn ──────────────────────────────
|
|
16
49
|
const pA1 = [0, 0, 0], pB1 = [30, 20, 0];
|
|
17
|
-
const lBend =
|
|
50
|
+
const lBend = blendTube(
|
|
18
51
|
{ point: pA1, tangent: [1, 0, 0] },
|
|
19
52
|
{ point: pB1, tangent: [0, 1, 0] },
|
|
20
53
|
{ radius, weightA: weight, weightB: weightB },
|
|
@@ -22,7 +55,7 @@ const lBend = transitionSurface(
|
|
|
22
55
|
|
|
23
56
|
// ── 2. S-curve: opposing tangents ────────────────────────────────
|
|
24
57
|
const pA2 = [0, 40, 0], pB2 = [30, 60, 0];
|
|
25
|
-
const sCurve =
|
|
58
|
+
const sCurve = blendTube(
|
|
26
59
|
{ point: pA2, tangent: [1, 0, 0] },
|
|
27
60
|
{ point: pB2, tangent: [-1, 0, 0] },
|
|
28
61
|
{ radius, weightA: weight, weightB: weightB },
|
|
@@ -30,7 +63,7 @@ const sCurve = transitionSurface(
|
|
|
30
63
|
|
|
31
64
|
// ── 3. 3D transition: going up and around ────────────────────────
|
|
32
65
|
const pA3 = [0, 0, 30], pB3 = [25, 15, 45];
|
|
33
|
-
const transition3D =
|
|
66
|
+
const transition3D = blendTube(
|
|
34
67
|
{ point: pA3, tangent: [1, 0, 0] },
|
|
35
68
|
{ point: pB3, tangent: [0, 0, 1] },
|
|
36
69
|
{ radius, weightA: weight, weightB: weightB, up: [0, 1, 0] },
|
|
@@ -38,7 +71,7 @@ const transition3D = transitionSurface(
|
|
|
38
71
|
|
|
39
72
|
// ── 4. Asymmetric weight (3:0.5) ────────────────────────────────
|
|
40
73
|
const pA4 = [55, 0, 0], pB4 = [85, 20, 0];
|
|
41
|
-
const weighted =
|
|
74
|
+
const weighted = blendTube(
|
|
42
75
|
{ point: pA4, tangent: [1, 0, 0] },
|
|
43
76
|
{ point: pB4, tangent: [0, 1, 0] },
|
|
44
77
|
{ radius, weightA: 3.0, weightB: 0.5 },
|
|
@@ -51,25 +84,21 @@ const boxB = box(10, 10, 10).translate(35, 15, -20);
|
|
|
51
84
|
const topEdgeA = selectEdge(boxA, { atZ: -25, parallel: [1, 0, 0] });
|
|
52
85
|
const bottomEdgeB = selectEdge(boxB, { atZ: -20, parallel: [1, 0, 0] });
|
|
53
86
|
|
|
54
|
-
const edgeConnector =
|
|
87
|
+
const edgeConnector = edgeBlendTube(topEdgeA, bottomEdgeB, {
|
|
55
88
|
endA: 'mid',
|
|
56
89
|
endB: 'mid',
|
|
57
|
-
tangentModeA: 'outward',
|
|
58
|
-
tangentModeB: 'outward',
|
|
59
90
|
radius: 2,
|
|
60
91
|
weightA: weight,
|
|
61
92
|
weightB: weightB,
|
|
62
93
|
});
|
|
63
94
|
|
|
64
|
-
// ── 6.
|
|
95
|
+
// ── 6. Same local edge recipe on vertical side edges ─────────────
|
|
65
96
|
const sideEdgeA = selectEdge(boxA, { near: [5, 0, -30], parallel: [0, 0, 1] });
|
|
66
97
|
const sideEdgeB = selectEdge(boxB, { near: [30, 15, -15], parallel: [0, 0, 1] });
|
|
67
98
|
|
|
68
|
-
const shortcutConnector =
|
|
99
|
+
const shortcutConnector = edgeBlendTube(sideEdgeA, sideEdgeB, {
|
|
69
100
|
endA: 'mid',
|
|
70
101
|
endB: 'mid',
|
|
71
|
-
tangentModeA: 'outward',
|
|
72
|
-
tangentModeB: 'outward',
|
|
73
102
|
radius: 1.5,
|
|
74
103
|
weightA: weight,
|
|
75
104
|
weightB: weightB,
|
|
@@ -85,7 +114,7 @@ const box2 = box(12, 12, 12).translate(25, -55, 25);
|
|
|
85
114
|
// Cylinder rim at Y=-55: full radius available, so X=8 is on the rim.
|
|
86
115
|
// Box left face at X=25, Y=-55 is on the face edge, Z=31 is mid-height.
|
|
87
116
|
// End tangent [1,0,0] means the curve arrives going +X (into the face).
|
|
88
|
-
const cylToBox =
|
|
117
|
+
const cylToBox = blendTube(
|
|
89
118
|
{ point: [8, -55, 20], tangent: [1, 0, 0.65] },
|
|
90
119
|
{ point: [25, -55, 31], tangent: [1, 0, 0] },
|
|
91
120
|
{ radius: 1.5, weightA: weight, weightB: weightB },
|