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.
Files changed (216) hide show
  1. package/LICENSE +6 -4
  2. package/README.md +8 -4
  3. package/dist/assets/{AdminPage-DramHHDf.js → AdminPage-CDyGUinA.js} +2 -2
  4. package/dist/assets/{BenchmarkPage-Bjgkh5m9.js → BenchmarkPage-DfPMY_-d.js} +4 -15
  5. package/dist/assets/{BlogPage-n_HGP3Qm.js → BlogPage-kF0fkdJT.js} +2 -2
  6. package/dist/assets/{DocsPage-WCIkPmzC.js → DocsPage-B954L3YN.js} +9 -3
  7. package/dist/assets/EditorApp-Beb-IZ0y.js +14014 -0
  8. package/dist/assets/{EditorApp-BAnckbsk.css → EditorApp-CuDLxKqL.css} +698 -0
  9. package/dist/assets/{EmbedViewer-DEZKqdfW.js → EmbedViewer-C77B-TrF.js} +3 -3
  10. package/dist/assets/{LandingPageProofDriven-CeRIctuj.js → LandingPageProofDriven-Cr6fXMDj.js} +35 -37
  11. package/dist/assets/LegalPage-BRlScr9A.css +91 -0
  12. package/dist/assets/LegalPage-Dzklqmmg.js +39 -0
  13. package/dist/assets/{PricingPage-BMedqFef.css → PricingPage-BPF6HKyO.css} +25 -0
  14. package/dist/assets/{PricingPage-rIRa8p4Y.js → PricingPage-zWXkvlwl.js} +19 -19
  15. package/dist/assets/{SettingsPage-BqCUvEXM.js → SettingsPage-Bz0of4KQ.js} +2 -2
  16. package/dist/assets/app-CE3sYcV7.css +3890 -0
  17. package/dist/assets/{app-BUZqJvSO.js → app-D3kDkggg.js} +2305 -960
  18. package/dist/assets/cli/{render-lhGxj50Y.js → render-DSY3mMQa.js} +423 -30
  19. package/dist/assets/{constructionHistoryWorker-ipD1jcIv.js → constructionHistoryWorker-gpDo-uH2.js} +927 -243
  20. package/dist/assets/{evalWorker-CHXSe_-u.js → evalWorker-CU0Ke6DP.js} +7799 -4163
  21. package/dist/assets/{forgecad_geometry-BVnIeXMG.js → forgecad_geometry-Dgceylq9.js} +43 -1
  22. package/dist/assets/{forgecad_geometry_bg-DufhhCBV.wasm → forgecad_geometry_bg-dD4RNQF1.wasm} +0 -0
  23. package/dist/assets/{inspectWorker-DeRnMVv1.js → inspectWorker-COyp8XXA.js} +927 -243
  24. package/dist/assets/{javascript-70-4uGcz.js → javascript-1kQXfVaz.js} +1 -1
  25. package/dist/assets/landing-proof-driven-DiGqdtWa.js +18 -0
  26. package/dist/assets/{landing-proof-driven-oFYW6mjz.css → landing-proof-driven-ORyigZ6p.css} +13 -7
  27. package/dist/assets/legalContent-ZfFGMmi4.js +251 -0
  28. package/dist/assets/{manifold-D1LZIHqn.js → manifold-BRI5prcH.js} +1 -1
  29. package/dist/assets/{manifold-C2fwoTgd.js → manifold-C-3h2M7p.js} +2 -2
  30. package/dist/assets/{manifold-BTkzxi9V.js → manifold-DNkrUWpA.js} +1 -1
  31. package/dist/assets/{reportWorker-Cq1qGmg0.js → reportWorker-CdBz5bNg.js} +7537 -10856
  32. package/dist/assets/{scalar-sampling-budget-D9Qv_UlJ.js → scalar-sampling-budget-wJF98aY9.js} +6943 -4345
  33. package/dist/assets/{scanProxyWorker-Bs2TDgLw.js → scanProxyWorker-B-9VbLIs.js} +32 -1
  34. package/dist/assets/{renderSceneState-Dr0xPq1A.js → targets-B9sGB5nB.js} +27 -1
  35. package/dist/assets/{vendor-react-Da3A2QmU.js → vendor-react-6j1Kke-Y.js} +6 -5
  36. package/dist/cli/render.html +1 -1
  37. package/dist/docs/index.html +2 -2
  38. package/dist/docs-raw/AI/ai-native-cad.md +50 -0
  39. package/dist/docs-raw/AI/usage.md +9 -17
  40. package/dist/docs-raw/CLI.md +71 -21
  41. package/dist/docs-raw/component-model.md +27 -11
  42. package/dist/docs-raw/generated/assembly.md +301 -212
  43. package/dist/docs-raw/generated/concepts.md +238 -240
  44. package/dist/docs-raw/generated/core.md +283 -6
  45. package/dist/docs-raw/generated/curves.md +274 -361
  46. package/dist/docs-raw/generated/lib.md +7 -1
  47. package/dist/docs-raw/generated/output.md +19 -4
  48. package/dist/docs-raw/generated/runtime-names.md +41 -0
  49. package/dist/docs-raw/generated/sdf.md +31 -0
  50. package/dist/docs-raw/generated/sheet-metal.md +9 -0
  51. package/dist/docs-raw/generated/sketch.md +44 -1
  52. package/dist/docs-raw/generated/viewport.md +14 -6
  53. package/dist/docs-raw/guides/coordinate-system.md +20 -16
  54. package/dist/docs-raw/guides/geometry-conventions.md +2 -2
  55. package/dist/docs-raw/guides/inspection-bundles.md +2 -1
  56. package/dist/docs-raw/guides/joint-design.md +24 -0
  57. package/dist/docs-raw/guides/positioning.md +13 -3
  58. package/dist/docs-raw/legal/privacy.md +63 -0
  59. package/dist/docs-raw/legal/software-license.md +55 -0
  60. package/dist/docs-raw/legal/terms.md +87 -0
  61. package/dist/docs-raw/skills/forgecad-3d-reconstruction.md +3 -3
  62. package/dist/docs-raw/skills/forgecad-blockout-model.md +1 -1
  63. package/dist/docs-raw/skills/forgecad-component-model.md +11 -2
  64. package/dist/docs-raw/skills/forgecad-high-level-spec.md +1 -1
  65. package/dist/docs-raw/skills/forgecad-image-replicator.md +8 -8
  66. package/dist/docs-raw/skills/forgecad-lld.md +1 -1
  67. package/dist/docs-raw/skills/forgecad-make-a-model.md +4 -4
  68. package/dist/docs-raw/skills/forgecad-model-grader.md +2 -2
  69. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +2 -2
  70. package/dist/docs-raw/skills/forgecad-project.md +1 -1
  71. package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +4 -4
  72. package/dist/docs-raw/skills/forgecad-render-inspect.md +4 -2
  73. package/dist/docs-raw/skills/forgecad-visual-spec.md +1 -1
  74. package/dist/docs-raw/skills/forgecad.md +4 -3
  75. package/dist/index.html +40 -12
  76. package/dist/llms.txt +8 -0
  77. package/dist/site.webmanifest +1 -1
  78. package/dist/sitemap.xml +49 -13
  79. package/dist-cli/{check-compiler-LOXCPEOI.js → check-compiler-SDX5QIXI.js} +1 -2
  80. package/dist-cli/{check-query-propagation-BAKNVWXR.js → check-query-propagation-EAYEFT77.js} +1 -2
  81. package/dist-cli/{chunk-RY43WF46.js → chunk-N4O47JLF.js} +13772 -9938
  82. package/dist-cli/forgecad.js +2387 -899
  83. package/dist-cli/{forgecad_geometry-GYVNKPIE.js → forgecad_geometry-QOQIIP53.js} +42 -1
  84. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  85. package/dist-cli/{solver-46FFSK2U.js → solver-OK4HECRH.js} +0 -1
  86. package/dist-skill/CONTEXT.md +1120 -724
  87. package/dist-skill/SKILL.md +3 -2
  88. package/dist-skill/docs/API/core/concepts.md +64 -1
  89. package/dist-skill/docs/CLI.md +71 -21
  90. package/dist-skill/docs/generated/assembly.md +277 -229
  91. package/dist-skill/docs/generated/core.md +283 -6
  92. package/dist-skill/docs/generated/curves.md +272 -362
  93. package/dist-skill/docs/generated/lib.md +7 -1
  94. package/dist-skill/docs/generated/output.md +19 -4
  95. package/dist-skill/docs/generated/runtime-names.md +41 -0
  96. package/dist-skill/docs/generated/sdf.md +31 -0
  97. package/dist-skill/docs/generated/sheet-metal.md +9 -0
  98. package/dist-skill/docs/generated/sketch.md +44 -2
  99. package/dist-skill/docs/generated/viewport.md +5 -90
  100. package/dist-skill/docs/guides/coordinate-system.md +20 -16
  101. package/dist-skill/docs/guides/geometry-conventions.md +2 -2
  102. package/dist-skill/docs/guides/inspection-bundles.md +2 -1
  103. package/dist-skill/docs/guides/joint-design.md +24 -0
  104. package/dist-skill/docs/guides/positioning.md +13 -3
  105. package/dist-skill/library/forgecad-3d-reconstruction/SKILL.md +2 -2
  106. package/dist-skill/library/forgecad-component-model/SKILL.md +10 -1
  107. package/dist-skill/library/forgecad-image-replicator/SKILL.md +6 -6
  108. package/dist-skill/library/forgecad-image-replicator/scripts/compare_images.py +166 -0
  109. package/dist-skill/library/forgecad-make-a-model/SKILL.md +3 -3
  110. package/dist-skill/library/forgecad-model-grader/SKILL.md +1 -1
  111. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +1 -1
  112. package/dist-skill/library/forgecad-reconstruction-benchmark/SKILL.md +3 -3
  113. package/dist-skill/library/forgecad-render-inspect/SKILL.md +3 -1
  114. package/examples/api/assembly-kinematics-foundation.forge.js +65 -0
  115. package/examples/api/assembly-kinematics-four-bar.forge.js +115 -0
  116. package/examples/api/assembly-kinematics-limb.forge.js +116 -0
  117. package/examples/api/connector-frame-rig-chain.forge.js +102 -0
  118. package/examples/api/exact-sheet-shell-assembly.forge.js +0 -2
  119. package/examples/api/exact-surface-studio.forge.js +6 -8
  120. package/examples/api/helix-basics.forge.js +6 -6
  121. package/examples/api/lean-foundations/README.md +12 -0
  122. package/examples/api/lean-foundations/curve-blend-exact.forge.js +22 -0
  123. package/examples/api/lean-foundations/curve-fit-interpolation.forge.js +18 -0
  124. package/examples/api/lean-foundations/curve-helix-canonicalization.forge.js +27 -0
  125. package/examples/api/lean-foundations/curve-route-canonicalization.forge.js +16 -0
  126. package/examples/api/lean-foundations/curve-trim-reverse.forge.js +24 -0
  127. package/examples/api/lean-foundations/exact-curve-arc.forge.js +36 -0
  128. package/examples/api/mixed-edge-finishes-proof.forge.js +8 -11
  129. package/examples/api/route3d-elbow.forge.js +68 -0
  130. package/examples/api/transition-curves.forge.js +44 -15
  131. package/examples/api/y-blend-corner-showcase.forge.js +0 -2
  132. package/examples/generative/coral-vase.forge.js +1 -1
  133. package/examples/nurbs-tube.forge.js +1 -1
  134. package/package.json +14 -18
  135. package/dist/assets/EditorApp-CP9Za6tm.js +0 -13630
  136. package/dist/assets/app-CsHnaBWt.css +0 -1789
  137. package/dist/docs-raw/API/README.md +0 -16
  138. package/dist/docs-raw/API/core/concepts.md +0 -118
  139. package/dist/docs-raw/INDEX.md +0 -138
  140. package/dist/docs-raw/RELEASING.md +0 -87
  141. package/dist/docs-raw/agent-native-api.md +0 -27
  142. package/dist/docs-raw/beta-deployment.md +0 -304
  143. package/dist/docs-raw/beta-operations.md +0 -325
  144. package/dist/docs-raw/blueprint-first.md +0 -145
  145. package/dist/docs-raw/cli-monetization.md +0 -112
  146. package/dist/docs-raw/coding-best-practices.md +0 -120
  147. package/dist/docs-raw/coding.md +0 -340
  148. package/dist/docs-raw/deployment.md +0 -374
  149. package/dist/docs-raw/guides/skill-maintenance.md +0 -161
  150. package/dist/docs-raw/guides/surface-members.md +0 -82
  151. package/dist/docs-raw/internals/backend-vocabulary.md +0 -35
  152. package/dist/docs-raw/internals/compiler.md +0 -307
  153. package/dist/docs-raw/internals/constraint-solver-quality.md +0 -161
  154. package/dist/docs-raw/internals/constraint-solver.md +0 -176
  155. package/dist/docs-raw/internals/shape-from-slices.md +0 -152
  156. package/dist/docs-raw/internals/sketch-2d-pipeline.md +0 -108
  157. package/dist/docs-raw/platform/admin.md +0 -45
  158. package/dist/docs-raw/platform/architecture.md +0 -82
  159. package/dist/docs-raw/platform/auth.md +0 -139
  160. package/dist/docs-raw/platform/email.md +0 -67
  161. package/dist/docs-raw/platform/google-oauth-setup.md +0 -88
  162. package/dist/docs-raw/platform/observability.md +0 -197
  163. package/dist/docs-raw/platform/projects.md +0 -111
  164. package/dist/docs-raw/platform/sharing.md +0 -90
  165. package/dist/docs-raw/product/README.md +0 -39
  166. package/dist/docs-raw/product/api-as-product-language.md +0 -13
  167. package/dist/docs-raw/product/business-model.md +0 -15
  168. package/dist/docs-raw/product/competitive-positioning.md +0 -17
  169. package/dist/docs-raw/product/creative-manufacturing.md +0 -15
  170. package/dist/docs-raw/product/founder-story.md +0 -11
  171. package/dist/docs-raw/product/manufacturing-workflows.md +0 -15
  172. package/dist/docs-raw/product/onboarding-first-experience.md +0 -256
  173. package/dist/docs-raw/product/product-loop.md +0 -17
  174. package/dist/docs-raw/product/strategic-decisions.md +0 -22
  175. package/dist/docs-raw/product/user-outreach-email-templates.md +0 -161
  176. package/dist/docs-raw/product/user-segments.md +0 -15
  177. package/dist/docs-raw/product/vision.md +0 -26
  178. package/dist/docs-raw/rl-environments.md +0 -508
  179. package/dist/docs-raw/runbook.md +0 -611
  180. package/dist-cli/check-compiler-LOXCPEOI.js.map +0 -1
  181. package/dist-cli/check-query-propagation-BAKNVWXR.js.map +0 -1
  182. package/dist-cli/chunk-RY43WF46.js.map +0 -1
  183. package/dist-cli/forgecad.js.map +0 -1
  184. package/dist-cli/forgecad_geometry-GYVNKPIE.js.map +0 -1
  185. package/dist-cli/solver-46FFSK2U.js.map +0 -1
  186. package/dist-skill/SKILL-dev.md +0 -145
  187. package/dist-skill/docs-dev/API/core/concepts.md +0 -118
  188. package/dist-skill/docs-dev/CLI.md +0 -647
  189. package/dist-skill/docs-dev/agent-native-api.md +0 -27
  190. package/dist-skill/docs-dev/blueprint-first.md +0 -145
  191. package/dist-skill/docs-dev/coding-best-practices.md +0 -120
  192. package/dist-skill/docs-dev/coding.md +0 -340
  193. package/dist-skill/docs-dev/component-model.md +0 -164
  194. package/dist-skill/docs-dev/generated/assembly.md +0 -794
  195. package/dist-skill/docs-dev/generated/core.md +0 -2117
  196. package/dist-skill/docs-dev/generated/curves.md +0 -2583
  197. package/dist-skill/docs-dev/generated/lib.md +0 -169
  198. package/dist-skill/docs-dev/generated/output.md +0 -247
  199. package/dist-skill/docs-dev/generated/sdf.md +0 -446
  200. package/dist-skill/docs-dev/generated/sheet-metal.md +0 -504
  201. package/dist-skill/docs-dev/generated/sketch.md +0 -1811
  202. package/dist-skill/docs-dev/generated/viewport.md +0 -585
  203. package/dist-skill/docs-dev/generated/wood.md +0 -108
  204. package/dist-skill/docs-dev/guides/coordinate-system.md +0 -46
  205. package/dist-skill/docs-dev/guides/geometry-conventions.md +0 -52
  206. package/dist-skill/docs-dev/guides/inspection-bundles.md +0 -485
  207. package/dist-skill/docs-dev/guides/joint-design.md +0 -78
  208. package/dist-skill/docs-dev/guides/modeling-recipes.md +0 -78
  209. package/dist-skill/docs-dev/guides/positioning.md +0 -161
  210. package/dist-skill/docs-dev/guides/skill-maintenance.md +0 -161
  211. package/dist-skill/docs-dev/internals/backend-vocabulary.md +0 -35
  212. package/dist-skill/docs-dev/internals/compiler.md +0 -307
  213. package/dist-skill/docs-dev/internals/constraint-solver-quality.md +0 -161
  214. package/dist-skill/docs-dev/internals/constraint-solver.md +0 -176
  215. package/dist-skill/docs-dev/internals/sketch-2d-pipeline.md +0 -108
  216. 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
- });