forgecad 0.9.14 → 0.9.16
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-eWGs2K6H.js → AdminPage-CXvls4-J.js} +2 -2
- package/dist/assets/{BenchmarkPage-CTrLKfpo.js → BenchmarkPage-B27zk8xL.js} +4 -15
- package/dist/assets/{BlogPage-5nPesyds.js → BlogPage-CMAVvgQL.js} +2 -2
- package/dist/assets/{DocsPage-C4Y3nbYc.js → DocsPage-knf4I4h7.js} +9 -3
- package/dist/assets/EditorApp-BHMQlJ-D.js +14686 -0
- package/dist/assets/{EditorApp-BAnckbsk.css → EditorApp-BpjZgzk0.css} +846 -0
- package/dist/assets/{EmbedViewer-C8fB4n5U.js → EmbedViewer-D7ZGlFjx.js} +3 -3
- package/dist/assets/{LandingPageProofDriven-jSz0LaMM.js → LandingPageProofDriven-CnevhTE8.js} +36 -38
- package/dist/assets/LegalPage-BPTUmqeg.js +39 -0
- package/dist/assets/LegalPage-BRlScr9A.css +91 -0
- package/dist/assets/{PricingPage-B83B90zh.js → PricingPage-B0D4goG_.js} +19 -19
- package/dist/assets/{PricingPage-BMedqFef.css → PricingPage-BPF6HKyO.css} +25 -0
- package/dist/assets/{SettingsPage-DY889pcu.js → SettingsPage-CFF-UgjI.js} +2 -2
- package/dist/assets/app-CE3sYcV7.css +3890 -0
- package/dist/assets/{app-bEww1ic4.js → app-T0pDcSX4.js} +3382 -1069
- package/dist/assets/cli/{render-Cho2uKG_.js → render-C5pcIISc.js} +477 -29
- package/dist/assets/{constructionHistoryWorker-HYwzJY4m.js → constructionHistoryWorker-Ba2Hm58b.js} +928 -243
- package/dist/assets/{evalWorker-CjQwJSE-.js → evalWorker-vkx310U2.js} +8883 -6040
- package/dist/assets/{forgecad_geometry-CH2nvuLA.js → forgecad_geometry-Dgceylq9.js} +43 -1
- package/dist/assets/forgecad_geometry_bg-dD4RNQF1.wasm +0 -0
- package/dist/assets/{inspectWorker-DeRnMVv1.js → inspectWorker-BuTJDVX6.js} +1179 -273
- package/dist/assets/{javascript-70-4uGcz.js → javascript-1kQXfVaz.js} +1 -1
- package/dist/assets/{targets-D6PWsv6X.js → jointPose-B_Cgedn9.js} +71 -3
- 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-rmfAcdwF.js → manifold-BWgsjmAM.js} +1 -1
- package/dist/assets/{manifold-uRzgk5O8.js → manifold-D6IFSkhH.js} +2 -2
- package/dist/assets/{manifold-CG9Fokx-.js → manifold-rZexZI0G.js} +1 -1
- package/dist/assets/{reportWorker-4cW_ZpoS.js → reportWorker-0AGij1Ru.js} +8659 -12771
- package/dist/assets/{scalar-sampling-budget-CfDiFvh7.js → scalar-sampling-budget-J5cuzxT1.js} +8050 -6203
- package/dist/assets/{scanProxyWorker-Bs2TDgLw.js → scanProxyWorker-Vl4Wxa1y.js} +50 -6
- package/dist/assets/{solver-DuJAO8S6.js → solver-BZ9LPTHs.js} +1 -1
- package/dist/assets/solver_bg-DAHZJ_rw.wasm +0 -0
- 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 +5 -12
- package/dist/docs-raw/CLI.md +34 -10
- package/dist/docs-raw/component-model.md +27 -11
- package/dist/docs-raw/generated/assembly.md +374 -187
- package/dist/docs-raw/generated/concepts.md +245 -237
- 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 +9 -19
- package/dist/docs-raw/generated/output.md +29 -4
- package/dist/docs-raw/generated/runtime-names.md +49 -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 +11 -3
- 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 +1 -1
- 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 +40 -39
- 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 +3 -1
- package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +1 -1
- 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/docs-raw/welcome.md +2 -0
- 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-U5SOPN7X.js → check-compiler-SYQ2PWOB.js} +1 -2
- package/dist-cli/{check-query-propagation-XOKNSSYU.js → check-query-propagation-HIAGV62W.js} +1 -2
- package/dist-cli/{chunk-EXWGNL6K.js → chunk-SPZE3DUY.js} +20659 -17930
- package/dist-cli/forgecad.js +3568 -1250
- 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-cli/solver_bg.wasm +0 -0
- package/dist-skill/CONTEXT.md +1192 -725
- package/dist-skill/SKILL.md +3 -2
- package/dist-skill/docs/API/core/concepts.md +64 -1
- package/dist-skill/docs/CLI.md +34 -10
- package/dist-skill/docs/generated/assembly.md +339 -213
- 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 +9 -19
- package/dist-skill/docs/generated/output.md +29 -4
- package/dist-skill/docs/generated/runtime-names.md +40 -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 +2 -87
- 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-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 +39 -38
- 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-project/SKILL.md +2 -0
- 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 +8 -8
- 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 +71 -0
- package/examples/api/transition-curves.forge.js +44 -15
- package/examples/api/variable-sweep-test.forge.js +3 -1
- 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 +17 -13
- package/dist/assets/EditorApp-lXv53A1m.js +0 -13610
- package/dist/assets/app-CsHnaBWt.css +0 -1789
- package/dist/assets/forgecad_geometry_bg-C5_E9Oa9.wasm +0 -0
- package/dist/assets/solver_bg-CWvv4lnN.wasm +0 -0
- 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/harbor-cli.md +0 -854
- 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 -350
- package/dist/docs-raw/runbook.md +0 -611
- package/dist-cli/check-compiler-U5SOPN7X.js.map +0 -1
- package/dist-cli/check-query-propagation-XOKNSSYU.js.map +0 -1
- package/dist-cli/chunk-EXWGNL6K.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 -677
- 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
- package/examples/api/bolted-service-cover.forge.js +0 -17
- package/examples/api/cable-gland-anchor.forge.js +0 -14
- package/examples/api/captured-cartridge-guide.forge.js +0 -14
- package/examples/api/captured-linear-slide.forge.js +0 -13
- package/examples/api/clevis-pin-joint.forge.js +0 -13
- package/examples/api/datum-enclosure.forge.js +0 -16
- package/examples/api/hose-barb-port.forge.js +0 -14
- package/examples/api/knuckled-hinge-assembly.forge.js +0 -15
- package/examples/api/living-hinge-cover.forge.js +0 -14
- package/examples/api/pcb-terminal-block.forge.js +0 -22
- package/examples/api/pinned-lever-pivot-stack.forge.js +0 -14
- package/examples/api/retained-shaft-knob-stack.forge.js +0 -15
- package/examples/api/routed-tube-clip.forge.js +0 -15
- package/examples/api/seated-bearing-stack.forge.js +0 -30
- package/examples/api/snap-latch-cover.forge.js +0 -14
- package/examples/api/thumb-screw-clamp.forge.js +0 -15
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
skill-group: dev-compiler
|
|
3
|
-
skill-order: 2
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# 2D Sketch Pipeline: Runtime Mesh ↔ ProfileCompilePlan ↔ Export Backend
|
|
7
|
-
|
|
8
|
-
Captured from implementing sketch region selection, planar arrangement detection, and cross-sketch reference geometry (2026-03).
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## The Two-Track Export Model
|
|
13
|
-
|
|
14
|
-
Every 3D shape in ForgeCAD has two representations:
|
|
15
|
-
|
|
16
|
-
1. **Runtime mesh** — a watertight triangulated solid used for preview rendering and mesh-domain operations.
|
|
17
|
-
2. **`ProfileCompilePlan`** — a serializable Forge intent description (discriminated union with `kind: 'rect' | 'circle' | 'polygon' | 'boolean' | ...`) that the current export backend reads to produce STEP geometry.
|
|
18
|
-
|
|
19
|
-
When exporting STEP, the system walks the `ProfileCompilePlan` tree. If a node has no plan (plan is `null`), it falls back to a faceted mesh-to-STEP conversion — useful for interchange, but it loses parametric intent.
|
|
20
|
-
|
|
21
|
-
**Key invariant**: any sketch API that only calls `polygon()` and boolean ops (`add`/`subtract`) automatically stays on the maintained STEP path. No special backend-specific code is needed.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## What `Sketch` Is
|
|
26
|
-
|
|
27
|
-
`Sketch` wraps a Manifold `CrossSection` (2D polygon set). It also carries:
|
|
28
|
-
- An optional `ProfileCompilePlan` for the STEP path.
|
|
29
|
-
- A `Placement3D` for face-mounted sketches (`.onFace()`).
|
|
30
|
-
|
|
31
|
-
`ConstrainedSketchBuilder` → `.solve()` → `ConstraintSketch extends Sketch` holds the constraint definition alongside the solved `CrossSection`. The solved `CrossSection` is built from explicitly declared loops (`addLoop`); geometry outside loops is not included in the area — only in the constraint definition for arrangement detection.
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
## New 2D Sketch Surface-Selection APIs (2026-03)
|
|
36
|
-
|
|
37
|
-
### 1. `sketch.regions()` / `sketch.region(seed)`
|
|
38
|
-
|
|
39
|
-
**File**: `src/forge/sketch/regions.ts`
|
|
40
|
-
|
|
41
|
-
Decomposes a Manifold `CrossSection` into its distinct filled areas.
|
|
42
|
-
|
|
43
|
-
- `CrossSection.toPolygons()` returns a flat list of contours. Positive signed area → outer boundary; negative → hole.
|
|
44
|
-
- Holes are nested into their smallest containing outer boundary via `pointInPolygon`.
|
|
45
|
-
- Each outer boundary + its holes is reassembled as a `polygon(outerPts, holePts)` sketch.
|
|
46
|
-
- The `region(seed)` variant picks the one face whose outer boundary contains the seed and no hole does.
|
|
47
|
-
|
|
48
|
-
**STEP export**: uses `polygon()` and potentially `boolean(difference)` for rings — both are handled by the current export backend without extra sketch-specific code.
|
|
49
|
-
|
|
50
|
-
### 2. `constraintSketch.detectArrangement()` / `detectArrangementRegion(seed)`
|
|
51
|
-
|
|
52
|
-
**File**: `src/forge/sketch/arrangement.ts`
|
|
53
|
-
|
|
54
|
-
DCEL-based planar arrangement detection from the raw line segments in a `ConstraintDefinition`.
|
|
55
|
-
|
|
56
|
-
Algorithm:
|
|
57
|
-
1. Extract non-construction line segments from `def.lines`.
|
|
58
|
-
2. Split segments at all pairwise intersections — both X-crossings (`segSegT`) and **T-junctions** (`pointOnSegT`). T-junction support is critical: when a divider endpoint touches a boundary edge interior, only `pointOnSegT` detects it.
|
|
59
|
-
3. Snap nearby nodes and build a clean planar graph.
|
|
60
|
-
4. Build DCEL half-edges. At each node sort outgoing half-edges by polar angle.
|
|
61
|
-
5. `next(u→v)` = outgoing from v immediately preceding `twin(u→v)` in CCW order at v — i.e., `out[(pos−1+n) % n]`.
|
|
62
|
-
6. Traverse all face cycles; keep CCW faces (positive signed area). CW = unbounded outer face, excluded.
|
|
63
|
-
7. Return each face as a `polygon(pts)` sketch.
|
|
64
|
-
|
|
65
|
-
No explicit loops needed from the caller. Works on any set of line constraints.
|
|
66
|
-
|
|
67
|
-
**STEP export**: each face is a `polygon()` — handled by the maintained export path.
|
|
68
|
-
|
|
69
|
-
### 3. `builderB.referenceFrom(sketchA, entityId)` / `referenceAllFrom(sketchA)`
|
|
70
|
-
|
|
71
|
-
**File**: `src/forge/sketch/constraints.ts`
|
|
72
|
-
|
|
73
|
-
Import solved geometry from another `ConstraintSketch` as fixed construction references.
|
|
74
|
-
|
|
75
|
-
- Fixed points (`fixed: true`) and construction lines (`construction: true`) participate in constraint solving but contribute zero area to the resulting CrossSection profile.
|
|
76
|
-
- `referenceFrom(source, id)` looks up the entity by id in the source's `ConstraintDefinition` and creates a fixed copy in the current builder.
|
|
77
|
-
- Enables constraints like `parallel(bBot, refBase)` to lock relationships between separate sketches.
|
|
78
|
-
|
|
79
|
-
**STEP export**: construction elements are ignored by `buildSketchFromDefinition` when assembling loops → no impact on export path.
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## Backend Agnosticism
|
|
84
|
-
|
|
85
|
-
These APIs are fully backend-agnostic because they operate on:
|
|
86
|
-
- **2D coordinates only** — no 3D kernel calls during region/arrangement detection.
|
|
87
|
-
- **`polygon()`** — the lowest-level sketch primitive, available on every backend.
|
|
88
|
-
- **Boolean ops** — available on every backend (Manifold, OCCT, …).
|
|
89
|
-
|
|
90
|
-
No backend-specific code was added. Adding a new backend (e.g., CGAL, OpenCASCADE directly) automatically inherits all three APIs as long as it handles `polygon` and `boolean` plan kinds.
|
|
91
|
-
|
|
92
|
-
The only Manifold-specific call is `CrossSection.toPolygons()` in `regions.ts`. If a future backend doesn't use `CrossSection`, `sketchRegions` would need an adapter — but the algorithm and public API shape would be unchanged.
|
|
93
|
-
|
|
94
|
-
---
|
|
95
|
-
|
|
96
|
-
## Gotchas
|
|
97
|
-
|
|
98
|
-
### Empty CrossSection for loop-less sketches
|
|
99
|
-
|
|
100
|
-
`constrainedSketch().solve()` used to throw "at least one closed loop" when called without `addLoop()`. Users calling `.detectArrangement()` never add loops. Fix: return an empty `CrossSection` via `CrossSection.difference([unit, unit])` (not `new CrossSection([])` — Manifold's `polygons2vec` crashes on empty array).
|
|
101
|
-
|
|
102
|
-
### T-junctions
|
|
103
|
-
|
|
104
|
-
Interior-only intersection detection (`segSegT`) misses T-junctions entirely. A 3×2 grid yields only 4 cells instead of 6 if T-junctions aren't split. Always run `pointOnSegT` for every endpoint of every other segment against every segment.
|
|
105
|
-
|
|
106
|
-
### DCEL `next` pointer direction
|
|
107
|
-
|
|
108
|
-
The formula traces faces to the **LEFT** of each directed half-edge. CW winding = unbounded outer face. Keep only faces with positive signed area (CCW winding = bounded interior face).
|
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
4
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
-
import { dirname, extname, resolve } from 'node:path';
|
|
6
|
-
import { execFileSync } from 'node:child_process';
|
|
7
|
-
import puppeteer from 'puppeteer-core';
|
|
8
|
-
|
|
9
|
-
const CHROME_PATHS = [
|
|
10
|
-
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
11
|
-
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
12
|
-
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
13
|
-
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
14
|
-
'/usr/bin/google-chrome',
|
|
15
|
-
'/usr/bin/google-chrome-stable',
|
|
16
|
-
'/usr/bin/chromium',
|
|
17
|
-
'/usr/bin/chromium-browser',
|
|
18
|
-
'/snap/bin/chromium',
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
const MIME_BY_EXT = new Map([
|
|
22
|
-
['.png', 'image/png'],
|
|
23
|
-
['.jpg', 'image/jpeg'],
|
|
24
|
-
['.jpeg', 'image/jpeg'],
|
|
25
|
-
['.webp', 'image/webp'],
|
|
26
|
-
['.gif', 'image/gif'],
|
|
27
|
-
['.bmp', 'image/bmp'],
|
|
28
|
-
['.svg', 'image/svg+xml'],
|
|
29
|
-
]);
|
|
30
|
-
|
|
31
|
-
function usage() {
|
|
32
|
-
return `Usage:
|
|
33
|
-
compare_images.mjs <reference-image> <forgecad-render> <output.png> [options]
|
|
34
|
-
|
|
35
|
-
Options:
|
|
36
|
-
--height <px> Panel height in pixels (default: 900)
|
|
37
|
-
--panel-width <px> Panel width in pixels (default: max input aspect at --height)
|
|
38
|
-
--gap <px> Gap between panels (default: 16)
|
|
39
|
-
--padding <px> Outer padding (default: 16)
|
|
40
|
-
--background <color> Canvas background (default: #111111)
|
|
41
|
-
--fit <contain|cover> Fit mode inside equal panels (default: contain)
|
|
42
|
-
--labels <left,right> Labels (default: Reference,ForgeCAD)
|
|
43
|
-
--no-labels Disable label band
|
|
44
|
-
--chrome-path <path> Chrome or Chromium executable
|
|
45
|
-
-h, --help Show help`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function readValue(argv, index, flag) {
|
|
49
|
-
const value = argv[index + 1];
|
|
50
|
-
if (!value || value.startsWith('--')) {
|
|
51
|
-
throw new Error(`Missing value for ${flag}`);
|
|
52
|
-
}
|
|
53
|
-
return value;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function parsePositiveInt(raw, label) {
|
|
57
|
-
const value = Number.parseInt(raw, 10);
|
|
58
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
59
|
-
throw new Error(`${label} must be a positive integer.`);
|
|
60
|
-
}
|
|
61
|
-
return value;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function parseArgs(argv) {
|
|
65
|
-
if (argv.includes('-h') || argv.includes('--help')) {
|
|
66
|
-
console.log(usage());
|
|
67
|
-
process.exit(0);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const positionals = [];
|
|
71
|
-
const options = {
|
|
72
|
-
height: 900,
|
|
73
|
-
panelWidth: null,
|
|
74
|
-
gap: 16,
|
|
75
|
-
padding: 16,
|
|
76
|
-
background: '#111111',
|
|
77
|
-
fit: 'contain',
|
|
78
|
-
labels: ['Reference', 'ForgeCAD'],
|
|
79
|
-
chromePath: process.env.CHROME_PATH || null,
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
83
|
-
const arg = argv[i];
|
|
84
|
-
if (arg === '--height') {
|
|
85
|
-
options.height = parsePositiveInt(readValue(argv, i, arg), '--height');
|
|
86
|
-
i += 1;
|
|
87
|
-
} else if (arg === '--panel-width') {
|
|
88
|
-
options.panelWidth = parsePositiveInt(readValue(argv, i, arg), '--panel-width');
|
|
89
|
-
i += 1;
|
|
90
|
-
} else if (arg === '--gap') {
|
|
91
|
-
options.gap = parsePositiveInt(readValue(argv, i, arg), '--gap');
|
|
92
|
-
i += 1;
|
|
93
|
-
} else if (arg === '--padding') {
|
|
94
|
-
options.padding = parsePositiveInt(readValue(argv, i, arg), '--padding');
|
|
95
|
-
i += 1;
|
|
96
|
-
} else if (arg === '--background') {
|
|
97
|
-
options.background = readValue(argv, i, arg);
|
|
98
|
-
i += 1;
|
|
99
|
-
} else if (arg === '--fit') {
|
|
100
|
-
const fit = readValue(argv, i, arg);
|
|
101
|
-
if (fit !== 'contain' && fit !== 'cover') {
|
|
102
|
-
throw new Error('--fit must be contain or cover.');
|
|
103
|
-
}
|
|
104
|
-
options.fit = fit;
|
|
105
|
-
i += 1;
|
|
106
|
-
} else if (arg === '--labels') {
|
|
107
|
-
const labels = readValue(argv, i, arg)
|
|
108
|
-
.split(',')
|
|
109
|
-
.map((entry) => entry.trim())
|
|
110
|
-
.filter(Boolean);
|
|
111
|
-
if (labels.length !== 2) {
|
|
112
|
-
throw new Error('--labels must contain two comma-separated labels.');
|
|
113
|
-
}
|
|
114
|
-
options.labels = labels;
|
|
115
|
-
i += 1;
|
|
116
|
-
} else if (arg === '--no-labels') {
|
|
117
|
-
options.labels = null;
|
|
118
|
-
} else if (arg === '--chrome-path') {
|
|
119
|
-
options.chromePath = readValue(argv, i, arg);
|
|
120
|
-
i += 1;
|
|
121
|
-
} else if (arg.startsWith('--')) {
|
|
122
|
-
throw new Error(`Unknown option: ${arg}`);
|
|
123
|
-
} else {
|
|
124
|
-
positionals.push(arg);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (positionals.length !== 3) {
|
|
129
|
-
throw new Error(`Expected reference, render, and output paths.\n\n${usage()}`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
referencePath: resolve(positionals[0]),
|
|
134
|
-
renderPath: resolve(positionals[1]),
|
|
135
|
-
outputPath: resolve(positionals[2]),
|
|
136
|
-
...options,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function commandPath(name) {
|
|
141
|
-
try {
|
|
142
|
-
const found = execFileSync(process.platform === 'win32' ? 'where' : 'which', [name], {
|
|
143
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
144
|
-
})
|
|
145
|
-
.toString()
|
|
146
|
-
.trim()
|
|
147
|
-
.split(/\r?\n/)[0];
|
|
148
|
-
return found || null;
|
|
149
|
-
} catch {
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function resolveChromePath(explicitPath) {
|
|
155
|
-
if (explicitPath && existsSync(explicitPath)) return explicitPath;
|
|
156
|
-
for (const candidate of CHROME_PATHS) {
|
|
157
|
-
if (existsSync(candidate)) return candidate;
|
|
158
|
-
}
|
|
159
|
-
for (const candidate of ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'brave-browser', 'microsoft-edge', 'chrome']) {
|
|
160
|
-
const found = commandPath(candidate);
|
|
161
|
-
if (found && existsSync(found)) return found;
|
|
162
|
-
}
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function imageDataUrl(path) {
|
|
167
|
-
if (!existsSync(path)) {
|
|
168
|
-
throw new Error(`Image not found: ${path}`);
|
|
169
|
-
}
|
|
170
|
-
const ext = extname(path).toLowerCase();
|
|
171
|
-
const mime = MIME_BY_EXT.get(ext);
|
|
172
|
-
if (!mime) {
|
|
173
|
-
throw new Error(`Unsupported image extension "${ext}" for ${path}`);
|
|
174
|
-
}
|
|
175
|
-
const bytes = await readFile(path);
|
|
176
|
-
return `data:${mime};base64,${bytes.toString('base64')}`;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async function main() {
|
|
180
|
-
const options = parseArgs(process.argv.slice(2));
|
|
181
|
-
const chromePath = resolveChromePath(options.chromePath);
|
|
182
|
-
if (!chromePath) {
|
|
183
|
-
throw new Error('Chrome or Chromium was not found. Pass --chrome-path or set CHROME_PATH.');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const [referenceUrl, renderUrl] = await Promise.all([imageDataUrl(options.referencePath), imageDataUrl(options.renderPath)]);
|
|
187
|
-
const browser = await puppeteer.launch({
|
|
188
|
-
executablePath: chromePath,
|
|
189
|
-
headless: true,
|
|
190
|
-
args: ['--no-sandbox', '--disable-gpu-sandbox'],
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
const page = await browser.newPage();
|
|
195
|
-
const result = await page.evaluate(
|
|
196
|
-
async (payload) => {
|
|
197
|
-
const loadImage = (src) =>
|
|
198
|
-
new Promise((resolveImage, rejectImage) => {
|
|
199
|
-
const img = new Image();
|
|
200
|
-
img.onload = () => resolveImage(img);
|
|
201
|
-
img.onerror = () => rejectImage(new Error('Failed to decode image'));
|
|
202
|
-
img.src = src;
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
const [reference, render] = await Promise.all([loadImage(payload.referenceUrl), loadImage(payload.renderUrl)]);
|
|
206
|
-
const panelHeight = payload.height;
|
|
207
|
-
const maxAspect = Math.max(reference.naturalWidth / reference.naturalHeight, render.naturalWidth / render.naturalHeight);
|
|
208
|
-
const panelWidth = payload.panelWidth ?? Math.ceil(panelHeight * maxAspect);
|
|
209
|
-
const labelHeight = payload.labels ? 34 : 0;
|
|
210
|
-
const canvasWidth = payload.padding * 2 + panelWidth * 2 + payload.gap;
|
|
211
|
-
const canvasHeight = payload.padding * 2 + labelHeight + panelHeight;
|
|
212
|
-
const canvas = document.createElement('canvas');
|
|
213
|
-
canvas.width = canvasWidth;
|
|
214
|
-
canvas.height = canvasHeight;
|
|
215
|
-
const ctx = canvas.getContext('2d');
|
|
216
|
-
ctx.fillStyle = payload.background;
|
|
217
|
-
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
218
|
-
|
|
219
|
-
const drawLabel = (text, x) => {
|
|
220
|
-
ctx.fillStyle = 'rgba(255,255,255,0.9)';
|
|
221
|
-
ctx.font = '600 18px system-ui, -apple-system, BlinkMacSystemFont, sans-serif';
|
|
222
|
-
ctx.textBaseline = 'top';
|
|
223
|
-
ctx.fillText(text, x, payload.padding + 4);
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const drawPanel = (img, x, y) => {
|
|
227
|
-
const scale =
|
|
228
|
-
payload.fit === 'cover'
|
|
229
|
-
? Math.max(panelWidth / img.naturalWidth, panelHeight / img.naturalHeight)
|
|
230
|
-
: Math.min(panelWidth / img.naturalWidth, panelHeight / img.naturalHeight);
|
|
231
|
-
const width = img.naturalWidth * scale;
|
|
232
|
-
const height = img.naturalHeight * scale;
|
|
233
|
-
const dx = x + (panelWidth - width) * 0.5;
|
|
234
|
-
const dy = y + (panelHeight - height) * 0.5;
|
|
235
|
-
|
|
236
|
-
ctx.save();
|
|
237
|
-
ctx.beginPath();
|
|
238
|
-
ctx.rect(x, y, panelWidth, panelHeight);
|
|
239
|
-
ctx.clip();
|
|
240
|
-
ctx.drawImage(img, dx, dy, width, height);
|
|
241
|
-
ctx.restore();
|
|
242
|
-
|
|
243
|
-
ctx.strokeStyle = 'rgba(255,255,255,0.25)';
|
|
244
|
-
ctx.lineWidth = 1;
|
|
245
|
-
ctx.strokeRect(x + 0.5, y + 0.5, panelWidth - 1, panelHeight - 1);
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
const leftX = payload.padding;
|
|
249
|
-
const rightX = payload.padding + panelWidth + payload.gap;
|
|
250
|
-
const panelY = payload.padding + labelHeight;
|
|
251
|
-
if (payload.labels) {
|
|
252
|
-
drawLabel(payload.labels[0], leftX);
|
|
253
|
-
drawLabel(payload.labels[1], rightX);
|
|
254
|
-
}
|
|
255
|
-
drawPanel(reference, leftX, panelY);
|
|
256
|
-
drawPanel(render, rightX, panelY);
|
|
257
|
-
|
|
258
|
-
return {
|
|
259
|
-
png: canvas.toDataURL('image/png'),
|
|
260
|
-
width: canvasWidth,
|
|
261
|
-
height: canvasHeight,
|
|
262
|
-
};
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
referenceUrl,
|
|
266
|
-
renderUrl,
|
|
267
|
-
height: options.height,
|
|
268
|
-
panelWidth: options.panelWidth,
|
|
269
|
-
gap: options.gap,
|
|
270
|
-
padding: options.padding,
|
|
271
|
-
background: options.background,
|
|
272
|
-
fit: options.fit,
|
|
273
|
-
labels: options.labels,
|
|
274
|
-
},
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
const png = Buffer.from(result.png.replace(/^data:image\/png;base64,/, ''), 'base64');
|
|
278
|
-
await mkdir(dirname(options.outputPath), { recursive: true });
|
|
279
|
-
await writeFile(options.outputPath, png);
|
|
280
|
-
console.log(`Wrote ${options.outputPath} (${result.width}x${result.height})`);
|
|
281
|
-
} finally {
|
|
282
|
-
await browser.close();
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
main().catch((error) => {
|
|
287
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
288
|
-
process.exit(1);
|
|
289
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// Bolted service cover: parent ledge, gasket, fused pull tabs, aligned holes, and installed screws.
|
|
2
|
-
|
|
3
|
-
const cover = lib.boltedServiceCover({
|
|
4
|
-
width: 90,
|
|
5
|
-
depth: 56,
|
|
6
|
-
coverThickness: 3,
|
|
7
|
-
parentThickness: 8,
|
|
8
|
-
ledgeWidth: 10,
|
|
9
|
-
screwSize: 'M4',
|
|
10
|
-
boltInset: [6, 6],
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
verify.equal('four retained cover screws', cover.screws.length, 4);
|
|
14
|
-
verify.greaterThan('cover has edge margin around service opening', cover.dims.ledgeWidth, 6);
|
|
15
|
-
verify.notColliding('cover does not collide with gasket', cover.cover, cover.gasket);
|
|
16
|
-
|
|
17
|
-
return cover.parts;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
const anchor = lib.cableGlandAnchorAssembly({
|
|
2
|
-
cableDiameter: 6,
|
|
3
|
-
panelThickness: 3,
|
|
4
|
-
panelWidth: 62,
|
|
5
|
-
panelHeight: 44,
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
verify.equal('cable gland anchor has four retained parts', anchor.parts.length, 4);
|
|
9
|
-
verify.greaterThan('panel leaves material around gland flange', anchor.dims.panelHeight - anchor.dims.flangeDiameter, 8);
|
|
10
|
-
verify.notColliding('cable clears gland bore', anchor.cable, anchor.gland);
|
|
11
|
-
verify.notColliding('gland clears panel hole', anchor.gland, anchor.panel);
|
|
12
|
-
verify.clearanceBetween('gland flange is seated at panel pocket', anchor.gland, anchor.panel, 0.01, 0.2);
|
|
13
|
-
|
|
14
|
-
return anchor.parts;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
const cassette = lib.capturedCartridgeGuideAssembly({
|
|
2
|
-
length: 150,
|
|
3
|
-
cartridgeLength: 72,
|
|
4
|
-
insertion: 28,
|
|
5
|
-
});
|
|
6
|
-
|
|
7
|
-
verify.equal('cartridge guide has guide and removable cassette', cassette.parts.length, 2);
|
|
8
|
-
verify.notColliding('cartridge clears guide rails and rear stop', cassette.cartridge, cassette.guide);
|
|
9
|
-
verify.greaterThan('cartridge flange is captured by guide lips', cassette.dims.cartridgeWidth, cassette.dims.throatWidth);
|
|
10
|
-
verify.lessThan('cartridge still clears inner guide width', cassette.dims.cartridgeWidth, cassette.dims.innerWidth);
|
|
11
|
-
verify.greaterThan('cartridge body passes through the guide throat', cassette.dims.throatWidth, cassette.dims.cartridgeBodyWidth);
|
|
12
|
-
verify.inRange('cartridge insertion stays inside travel', cassette.dims.insertion, 0, cassette.dims.maxInsertion);
|
|
13
|
-
|
|
14
|
-
return cassette.parts;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
const slide = lib.capturedLinearSlide({
|
|
2
|
-
length: 160,
|
|
3
|
-
railWidth: 38,
|
|
4
|
-
carriageLength: 52,
|
|
5
|
-
travel: 42,
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
verify.equal('captured slide has rail and carriage parts', slide.parts.length, 2);
|
|
9
|
-
verify.greaterThan('carriage is wider than the throat opening', slide.dims.carriageWidth, slide.dims.throatWidth);
|
|
10
|
-
verify.greaterThan('slide retains usable travel', slide.dims.maxTravel, 80);
|
|
11
|
-
verify.minClearance('carriage clears captured rail', slide.carriage, slide.rail, 0.05);
|
|
12
|
-
|
|
13
|
-
return slide.parts;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
const clevis = lib.clevisPinJointAssembly({
|
|
2
|
-
pinDiameter: 4,
|
|
3
|
-
linkThickness: 6,
|
|
4
|
-
linkArmLength: 38,
|
|
5
|
-
earThickness: 4,
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
verify.equal('clevis joint has three retained parts', clevis.parts.length, 3);
|
|
9
|
-
verify.greaterThan('link eye has material around pin bore', clevis.dims.eyeOuterRadius - clevis.dims.boreDiameter / 2, 4);
|
|
10
|
-
verify.greaterThan('clevis gap clears the link eye', clevis.dims.clevisGap, clevis.dims.linkThickness);
|
|
11
|
-
verify.minClearance('link eye clears clevis yoke', clevis.link, clevis.clevis, 0.05);
|
|
12
|
-
|
|
13
|
-
return clevis.parts;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
const enclosure = lib.datumEnclosureAssembly({
|
|
2
|
-
width: 96,
|
|
3
|
-
depth: 64,
|
|
4
|
-
height: 18,
|
|
5
|
-
screwSize: 'M3',
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
verify.equal('datum enclosure has base gasket cover and four screws', enclosure.parts.length, 7);
|
|
9
|
-
verify.greaterThan('internal cavity keeps usable width', enclosure.dims.innerWidth, 80);
|
|
10
|
-
verify.greaterThan('standoff wall remains around screw envelope', enclosure.dims.standoffDiameter - enclosure.dims.threadEnvelopeDia, 4);
|
|
11
|
-
verify.greaterThan('service port leaves top wall material', enclosure.dims.height - enclosure.dims.baseThickness - enclosure.dims.portHeight, 6);
|
|
12
|
-
verify.notColliding('cover clears seated gasket', enclosure.cover, enclosure.gasket);
|
|
13
|
-
verify.notColliding('first screw clears standoff thread envelope', enclosure.screws[0], enclosure.base);
|
|
14
|
-
verify.inRange('cover stack has small seating clearance', enclosure.dims.faceClearance, 0.01, 0.08);
|
|
15
|
-
|
|
16
|
-
return enclosure.parts;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
const hosePort = lib.hoseBarbPortAssembly({
|
|
2
|
-
hoseInnerDiameter: 6,
|
|
3
|
-
hoseOuterDiameter: 10,
|
|
4
|
-
barbCount: 3,
|
|
5
|
-
});
|
|
6
|
-
|
|
7
|
-
verify.equal('hose barb port has receiver fitting hose and clamp', hosePort.parts.length, 4);
|
|
8
|
-
verify.greaterThan('barb peaks retain nominal hose ID', hosePort.dims.barbPeakDiameter, hosePort.dims.hoseInnerDiameter);
|
|
9
|
-
verify.greaterThan('hose has wall around installed barb envelope', hosePort.dims.hoseOuterDiameter - hosePort.dims.installedHoseBoreDiameter, 1.2);
|
|
10
|
-
verify.notColliding('hose clears barb peaks', hosePort.hose, hosePort.fitting);
|
|
11
|
-
verify.notColliding('clamp clears hose outside', hosePort.clamp, hosePort.hose);
|
|
12
|
-
verify.inRange('fitting shoulder seats near boss face', hosePort.dims.faceClearance, 0.01, 0.08);
|
|
13
|
-
|
|
14
|
-
return hosePort.parts;
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
const hinge = lib.knuckledHingeAssembly({
|
|
2
|
-
length: 70,
|
|
3
|
-
leafLength: 28,
|
|
4
|
-
leafThickness: 1.8,
|
|
5
|
-
barrelOuterRadius: 3.2,
|
|
6
|
-
pinDiameter: 2,
|
|
7
|
-
openAngleDeg: 42,
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
verify.equal('hinge has two leaves and one retained pin', hinge.parts.length, 3);
|
|
11
|
-
verify.greaterThan('barrel wall remains around pin bore', hinge.dims.barrelOuterRadius - hinge.dims.boreDiameter / 2, 1.5);
|
|
12
|
-
verify.greaterThan('hinge has meaningful knuckle length', hinge.dims.knuckleLength, hinge.dims.pinDiameter * 2);
|
|
13
|
-
verify.minClearance('moving leaf clears fixed leaf', hinge.movingLeaf, hinge.fixedLeaf, 0.05);
|
|
14
|
-
|
|
15
|
-
return hinge.parts;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
const livingCover = lib.livingHingeCoverAssembly({
|
|
2
|
-
width: 64,
|
|
3
|
-
coverDepth: 42,
|
|
4
|
-
fixedLeafDepth: 18,
|
|
5
|
-
});
|
|
6
|
-
|
|
7
|
-
verify.equal('living hinge cover is one molded part', livingCover.parts.length, 1);
|
|
8
|
-
verify.equal('one-piece living hinge cover has one body', livingCover.cover.numBodies(), 1);
|
|
9
|
-
verify.greaterThan('living hinge web is much thinner than rigid leaves', livingCover.dims.flexRatio, 3.5);
|
|
10
|
-
verify.greaterThan('hinge web spans the full cover width', livingCover.dims.width, 60);
|
|
11
|
-
verify.greaterThan('snap barb has positive retention height', livingCover.dims.snapBarbHeight, 1);
|
|
12
|
-
verify.greaterThan('catch land is present on fixed leaf', livingCover.dims.catchLandDepth, 2);
|
|
13
|
-
|
|
14
|
-
return livingCover.parts;
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
const terminalStack = lib.pcbTerminalBlockAssembly({
|
|
2
|
-
terminalCount: 5,
|
|
3
|
-
terminalPitch: 5.08,
|
|
4
|
-
screwSize: 'M3',
|
|
5
|
-
});
|
|
6
|
-
|
|
7
|
-
verify.equal('terminal block exposes one pin per terminal', terminalStack.pinPositions.length, terminalStack.dims.terminalCount);
|
|
8
|
-
verify.greaterThan(
|
|
9
|
-
'PCB leaves mounting margin around terminal block',
|
|
10
|
-
terminalStack.dims.boardWidth - terminalStack.dims.terminalBlockWidth,
|
|
11
|
-
20,
|
|
12
|
-
);
|
|
13
|
-
verify.notColliding('terminal pins clear PCB holes', terminalStack.terminalBlock, terminalStack.pcb);
|
|
14
|
-
verify.notColliding('mounting screws clear PCB and standoff holes', union(...terminalStack.screws), union(terminalStack.pcb, terminalStack.backplate));
|
|
15
|
-
verify.greaterThan(
|
|
16
|
-
'standoffs retain wall around screw envelopes',
|
|
17
|
-
terminalStack.dims.standoffDiameter - terminalStack.dims.standoffThreadEnvelopeDiameter,
|
|
18
|
-
3,
|
|
19
|
-
);
|
|
20
|
-
verify.greaterThan('pin holes leave PCB web between terminals', terminalStack.dims.terminalPitch - terminalStack.dims.pinHoleDiameter, 3);
|
|
21
|
-
|
|
22
|
-
return terminalStack.parts;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
const lever = lib.pinnedLeverAssembly({
|
|
2
|
-
armLength: 56,
|
|
3
|
-
armWidth: 10,
|
|
4
|
-
leverThickness: 5,
|
|
5
|
-
pinDiameter: 5,
|
|
6
|
-
supportThickness: 8,
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
verify.equal('pinned lever stack has five retained parts', lever.parts.length, 5);
|
|
10
|
-
verify.greaterThan('thrust washers have real thickness', lever.dims.washerThickness, 0);
|
|
11
|
-
verify.greaterThan('pivot hub has material around the bore', lever.dims.hubRadius - lever.dims.boreDiameter / 2, 3);
|
|
12
|
-
verify.notColliding('lower washer does not collide with support', lever.washers.lower, lever.support);
|
|
13
|
-
|
|
14
|
-
return lever.parts;
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
const trunnion = lib.retainedShaftAssembly({
|
|
2
|
-
supportSpacing: 96,
|
|
3
|
-
shaftDiameter: 8,
|
|
4
|
-
supportThickness: 6,
|
|
5
|
-
supportHeight: 42,
|
|
6
|
-
knobDiameter: 24,
|
|
7
|
-
knobThickness: 8,
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
verify.equal('retained shaft stack has seven parts', trunnion.parts.length, 7);
|
|
11
|
-
verify.greaterThan('support cheeks leave material around bore', trunnion.dims.supportHeight - trunnion.dims.boreDiameter, 20);
|
|
12
|
-
verify.greaterThan('shaft is long enough to retain knobs', trunnion.dims.shaftLength, trunnion.dims.supportSpacing);
|
|
13
|
-
verify.notColliding('left washer does not collide with support cheek', trunnion.washers.left, trunnion.supports.left);
|
|
14
|
-
|
|
15
|
-
return trunnion.parts;
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
const route = lib.routedTubeClipAssembly({
|
|
2
|
-
tubeDiameter: 6,
|
|
3
|
-
tubeLength: 130,
|
|
4
|
-
clipCount: 3,
|
|
5
|
-
screwSize: 'M3',
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
verify.equal('tube route has three retaining clips', route.clips.length, 3);
|
|
9
|
-
verify.equal('each retaining clip has two installed screws', route.screws.length, route.clips.length * 2);
|
|
10
|
-
verify.notColliding('tube clears saddle clip bores', route.tube, union(...route.clips));
|
|
11
|
-
verify.notColliding('clip screws clear panel and clip holes', union(...route.screws), union(route.panel, ...route.clips));
|
|
12
|
-
verify.greaterThan('clip wall leaves material around tube bore', route.dims.clipWallThickness, route.dims.screwHeadDiameter);
|
|
13
|
-
verify.greaterThan('tube bore clears tube outside diameter', route.dims.tubeBoreDiameter - route.dims.tubeDiameter, 0.3);
|
|
14
|
-
|
|
15
|
-
return route.parts;
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
const bearingStack = lib.seatedBearingAssembly({
|
|
2
|
-
bearingOuterDiameter: 22,
|
|
3
|
-
bearingInnerDiameter: 8,
|
|
4
|
-
bearingWidth: 7,
|
|
5
|
-
shaftDiameter: 7.4,
|
|
6
|
-
housingWidth: 54,
|
|
7
|
-
housingDepth: 42,
|
|
8
|
-
housingThickness: 10,
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
verify.equal('seated bearing stack has three retained parts', bearingStack.parts.length, 3);
|
|
12
|
-
verify.greaterThan(
|
|
13
|
-
'housing boss leaves material around bearing pocket',
|
|
14
|
-
bearingStack.dims.bossOuterDiameter - bearingStack.dims.pocketDiameter,
|
|
15
|
-
5,
|
|
16
|
-
);
|
|
17
|
-
verify.greaterThan(
|
|
18
|
-
'bearing bore clears the shaft',
|
|
19
|
-
bearingStack.dims.bearingInnerDiameter,
|
|
20
|
-
bearingStack.dims.shaftDiameter + bearingStack.dims.runningClearance,
|
|
21
|
-
);
|
|
22
|
-
verify.clearanceBetween(
|
|
23
|
-
'bearing is seated in counterbore',
|
|
24
|
-
bearingStack.bearing,
|
|
25
|
-
bearingStack.housing,
|
|
26
|
-
-0.01,
|
|
27
|
-
bearingStack.dims.runningClearance + 0.1,
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
return bearingStack.parts;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
const snapCover = lib.snapLatchCoverAssembly({
|
|
2
|
-
width: 72,
|
|
3
|
-
depth: 44,
|
|
4
|
-
ledgeWidth: 8,
|
|
5
|
-
parentThickness: 6,
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
verify.equal('snap latch cover has receiver and cover parts', snapCover.parts.length, 2);
|
|
9
|
-
verify.greaterThan('snap hooks tuck under catch land', snapCover.dims.hookThrow, snapCover.dims.latchThickness);
|
|
10
|
-
verify.greaterThan('receiver leaves a usable service opening', snapCover.dims.openingWidth, 48);
|
|
11
|
-
verify.notColliding('snap hooks clear receiver windows', snapCover.cover, snapCover.parent);
|
|
12
|
-
verify.inRange('snap cover has small seating clearance', snapCover.dims.faceClearance, 0.01, 0.08);
|
|
13
|
-
|
|
14
|
-
return snapCover.parts;
|