forgecad 0.9.14 → 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-eWGs2K6H.js → AdminPage-CDyGUinA.js} +2 -2
- package/dist/assets/{BenchmarkPage-CTrLKfpo.js → BenchmarkPage-DfPMY_-d.js} +4 -15
- package/dist/assets/{BlogPage-5nPesyds.js → BlogPage-kF0fkdJT.js} +2 -2
- package/dist/assets/{DocsPage-C4Y3nbYc.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-C8fB4n5U.js → EmbedViewer-C77B-TrF.js} +3 -3
- package/dist/assets/{LandingPageProofDriven-jSz0LaMM.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-B83B90zh.js → PricingPage-zWXkvlwl.js} +19 -19
- package/dist/assets/{SettingsPage-DY889pcu.js → SettingsPage-Bz0of4KQ.js} +2 -2
- package/dist/assets/app-CE3sYcV7.css +3890 -0
- package/dist/assets/{app-bEww1ic4.js → app-D3kDkggg.js} +2293 -946
- package/dist/assets/cli/{render-Cho2uKG_.js → render-DSY3mMQa.js} +337 -7
- package/dist/assets/{constructionHistoryWorker-HYwzJY4m.js → constructionHistoryWorker-gpDo-uH2.js} +927 -243
- package/dist/assets/{evalWorker-CjQwJSE-.js → evalWorker-CU0Ke6DP.js} +7800 -4164
- 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-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-CG9Fokx-.js → manifold-BRI5prcH.js} +1 -1
- package/dist/assets/{manifold-uRzgk5O8.js → manifold-C-3h2M7p.js} +2 -2
- package/dist/assets/{manifold-rmfAcdwF.js → manifold-DNkrUWpA.js} +1 -1
- package/dist/assets/{reportWorker-4cW_ZpoS.js → reportWorker-CdBz5bNg.js} +7538 -10857
- package/dist/assets/{scalar-sampling-budget-CfDiFvh7.js → scalar-sampling-budget-wJF98aY9.js} +6935 -4331
- package/dist/assets/{scanProxyWorker-Bs2TDgLw.js → scanProxyWorker-B-9VbLIs.js} +32 -1
- package/dist/assets/{solver-DuJAO8S6.js → solver-BZ9LPTHs.js} +1 -1
- package/dist/assets/solver_bg-DAHZJ_rw.wasm +0 -0
- package/dist/assets/{targets-D6PWsv6X.js → targets-B9sGB5nB.js} +1 -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 +3 -12
- package/dist/docs-raw/CLI.md +30 -10
- 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 +235 -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 +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 +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 +1 -1
- 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 +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/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-SDX5QIXI.js} +1 -2
- package/dist-cli/{check-query-propagation-XOKNSSYU.js → check-query-propagation-EAYEFT77.js} +1 -2
- package/dist-cli/{chunk-EXWGNL6K.js → chunk-N4O47JLF.js} +12540 -9046
- package/dist-cli/forgecad.js +1786 -679
- 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 +1117 -721
- package/dist-skill/SKILL.md +3 -2
- package/dist-skill/docs/API/core/concepts.md +64 -1
- package/dist-skill/docs/CLI.md +30 -10
- 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 +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-model-grader/SKILL.md +1 -1
- package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +1 -1
- 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 -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
|
@@ -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
|
-
});
|