@faicad/faijs-fcstd 0.17.0

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 (87) hide show
  1. package/LICENSE +29 -0
  2. package/NOTICE +20 -0
  3. package/dist/attachment.d.ts +72 -0
  4. package/dist/attachment.d.ts.map +1 -0
  5. package/dist/attachment.js +93 -0
  6. package/dist/attachment.js.map +1 -0
  7. package/dist/bspline.d.ts +58 -0
  8. package/dist/bspline.d.ts.map +1 -0
  9. package/dist/bspline.js +93 -0
  10. package/dist/bspline.js.map +1 -0
  11. package/dist/build-fai-zip.d.ts +40 -0
  12. package/dist/build-fai-zip.d.ts.map +1 -0
  13. package/dist/build-fai-zip.js +111 -0
  14. package/dist/build-fai-zip.js.map +1 -0
  15. package/dist/cli.d.ts +3 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +37 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/codegen.d.ts +83 -0
  20. package/dist/codegen.d.ts.map +1 -0
  21. package/dist/codegen.js +618 -0
  22. package/dist/codegen.js.map +1 -0
  23. package/dist/container.d.ts +75 -0
  24. package/dist/container.d.ts.map +1 -0
  25. package/dist/container.js +36 -0
  26. package/dist/container.js.map +1 -0
  27. package/dist/contour.d.ts +43 -0
  28. package/dist/contour.d.ts.map +1 -0
  29. package/dist/contour.js +132 -0
  30. package/dist/contour.js.map +1 -0
  31. package/dist/convert.d.ts +69 -0
  32. package/dist/convert.d.ts.map +1 -0
  33. package/dist/convert.js +303 -0
  34. package/dist/convert.js.map +1 -0
  35. package/dist/document.d.ts +48 -0
  36. package/dist/document.d.ts.map +1 -0
  37. package/dist/document.js +140 -0
  38. package/dist/document.js.map +1 -0
  39. package/dist/expressions.d.ts +79 -0
  40. package/dist/expressions.d.ts.map +1 -0
  41. package/dist/expressions.js +162 -0
  42. package/dist/expressions.js.map +1 -0
  43. package/dist/external-geo.d.ts +49 -0
  44. package/dist/external-geo.d.ts.map +1 -0
  45. package/dist/external-geo.js +140 -0
  46. package/dist/external-geo.js.map +1 -0
  47. package/dist/feature-translate.d.ts +193 -0
  48. package/dist/feature-translate.d.ts.map +1 -0
  49. package/dist/feature-translate.js +1441 -0
  50. package/dist/feature-translate.js.map +1 -0
  51. package/dist/fillet-edges.d.ts +26 -0
  52. package/dist/fillet-edges.d.ts.map +1 -0
  53. package/dist/fillet-edges.js +36 -0
  54. package/dist/fillet-edges.js.map +1 -0
  55. package/dist/index.d.ts +23 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +19 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/placement.d.ts +93 -0
  60. package/dist/placement.d.ts.map +1 -0
  61. package/dist/placement.js +117 -0
  62. package/dist/placement.js.map +1 -0
  63. package/dist/planegcs-backend.d.ts +34 -0
  64. package/dist/planegcs-backend.d.ts.map +1 -0
  65. package/dist/planegcs-backend.js +665 -0
  66. package/dist/planegcs-backend.js.map +1 -0
  67. package/dist/sketch-parse.d.ts +199 -0
  68. package/dist/sketch-parse.d.ts.map +1 -0
  69. package/dist/sketch-parse.js +263 -0
  70. package/dist/sketch-parse.js.map +1 -0
  71. package/dist/sketch-solver.d.ts +63 -0
  72. package/dist/sketch-solver.d.ts.map +1 -0
  73. package/dist/sketch-solver.js +28 -0
  74. package/dist/sketch-solver.js.map +1 -0
  75. package/dist/sketch-verify.d.ts +53 -0
  76. package/dist/sketch-verify.d.ts.map +1 -0
  77. package/dist/sketch-verify.js +71 -0
  78. package/dist/sketch-verify.js.map +1 -0
  79. package/dist/structural-types.d.ts +96 -0
  80. package/dist/structural-types.d.ts.map +1 -0
  81. package/dist/structural-types.js +155 -0
  82. package/dist/structural-types.js.map +1 -0
  83. package/dist/unpack.d.ts +44 -0
  84. package/dist/unpack.d.ts.map +1 -0
  85. package/dist/unpack.js +64 -0
  86. package/dist/unpack.js.map +1 -0
  87. package/package.json +65 -0
package/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Faicad contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ Third-party components bundled in this distribution retain their own licenses:
26
+
27
+ - `src/vendored/brepjs` (compiled into `dist/vendored/**`): ported from the
28
+ brepjs project, licensed under the Apache License 2.0. See NOTICE for the
29
+ full attribution. Apache-2.0 text: https://www.apache.org/licenses/LICENSE-2.0
package/NOTICE ADDED
@@ -0,0 +1,20 @@
1
+ faijs — @faicad/faijs-fcstd NOTICE
2
+ ==================================
3
+
4
+ This package implements the FCStd reader and the `.fai.js` conversion
5
+ pipeline for the faijs CAD engine. The sketch constraint solver is
6
+ provided at runtime by the `@salusoft89/planegcs` package (dependencies,
7
+ not vendored here); it carries its own license.
8
+
9
+ Runtime dependencies (not distributed inside this package)
10
+ ----------------------------------------------------------
11
+
12
+ - `@faicad/faijs` — MIT (peer; engine Result/occt-kernel surface)
13
+ - `occt-wasm` — JS glue: MIT OR Apache-2.0; the compiled WASM output is
14
+ LGPL-3.0 (Open CASCADE Technology), loaded by the host at runtime.
15
+ - `@salusoft89/planegcs` — LGPL-2.1-or-later (planegcs, FreeCAD sketch
16
+ solver port)
17
+ - `@xmldom/xmldom` — MIT
18
+ - `fflate` — MIT
19
+
20
+ Full texts are available at the respective package sources.
@@ -0,0 +1,72 @@
1
+ /**
2
+ * H3 — attachment resolution: Support → MapMode → AttachmentOffset.
3
+ *
4
+ * FreeCAD saves an attached object's Placement as the ALREADY-COMPOSED result
5
+ * of the attachment chain (recomputed on load/save). The chain for the modes
6
+ * that matter to the corpus (probe: fcstd-port/tools/probe-attachment-usage.mjs):
7
+ * MapMode 5 = mmFlatFace (68 objects — dominant)
8
+ * MapMode 6 = mmTangentPlane (3)
9
+ * MapMode 1 = mmTranslate (1)
10
+ * MapMode 0 = mmDeactivated (no attachment; Placement stands alone)
11
+ *
12
+ * Full Attacher.cpp semantics (normal-projection onto a face, Frenet frames on
13
+ * curves, three-point modes) are far beyond what the corpus exercises: all
14
+ * corpus Support targets are datum planes / origin planes, where the resolved
15
+ * frame IS the support's placement, then rotated per-mode and composed with
16
+ * AttachmentOffset. This module implements that plane-support subset and
17
+ * reports what it resolved so the caller can bake with an explicit reason when
18
+ * the support is not a plane.
19
+ *
20
+ * GOTCHAs (probed on real files):
21
+ * - MapMode is `App::PropertyEnumeration` stored as `<Integer value="N"/>`,
22
+ * NEVER a `<String>` — matching the string name finds nothing.
23
+ * - The support link property is `Support` (old) or `AttachmentSupport`
24
+ * (newer FreeCAD); both must be read.
25
+ * - `App::Plane` objects carry the origin-plane identity placement; their
26
+ * orientation comes from their own Placement property like any object.
27
+ */
28
+ import type { FcstdObject } from './document.js';
29
+ import { type Placement } from './placement.js';
30
+ /** eMapMode values from FreeCAD Attacher.h (subset the corpus uses). */
31
+ export declare const ATTACH_MAP_MODE: {
32
+ readonly DEACTIVATED: 0;
33
+ readonly TRANSLATE: 1;
34
+ readonly OBJECT_XY: 2;
35
+ readonly OBJECT_XZ: 3;
36
+ readonly OBJECT_YZ: 4;
37
+ readonly FLAT_FACE: 5;
38
+ readonly TANGENT_PLANE: 6;
39
+ };
40
+ /** 附件链解析结果:合成后的 placement + 原始 MapMode + support 引用。 */
41
+ export interface AttachmentResolution {
42
+ /** the composed placement (support ∘ mode-adjustment ∘ AttachmentOffset) */
43
+ placement: Placement;
44
+ /** eMapMode integer as stored */
45
+ mapMode: number;
46
+ /** support target object name and sub-element */
47
+ support: {
48
+ target: string;
49
+ sub: string;
50
+ };
51
+ }
52
+ /**
53
+ * Resolve the attachment chain of an object carrying AttachExtension
54
+ * properties. Only plane-type supports are composed (corpus reality: all
55
+ * active attachments target datum/origin planes); anything else — or a
56
+ * missing support — returns undefined so the caller keeps the stored
57
+ * Placement or bakes with an explicit reason.
58
+ *
59
+ * @param obj - the FCStd object (sketch or datum) with MapMode/Support/AttachmentOffset.
60
+ * @param placements - per-object placements of the document (resolved earlier).
61
+ * @returns the resolved attachment, or undefined when not attached / not resolvable.
62
+ */
63
+ export declare function resolveAttachment(obj: FcstdObject, placements: Map<string, Placement>): AttachmentResolution | undefined;
64
+ /**
65
+ * Convenience: an object's effective placement — attachment-resolved when attached, stored otherwise.
66
+ *
67
+ * @param obj - the FCStd object to resolve.
68
+ * @param placements - per-object placements of the document (resolved earlier).
69
+ * @returns the object's effective placement.
70
+ */
71
+ export declare function effectivePlacement(obj: FcstdObject, placements: Map<string, Placement>): Placement;
72
+ //# sourceMappingURL=attachment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment.d.ts","sourceRoot":"","sources":["../src/attachment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,eAAe,CAAC;AAChE,OAAO,EAAgC,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE9E,wEAAwE;AACxE,eAAO,MAAM,eAAe;;;;;;;;CAQlB,CAAC;AA+CX,wDAAwD;AACxD,MAAM,WAAW,oBAAoB;IACnC,4EAA4E;IAC5E,SAAS,EAAE,SAAS,CAAC;IACrB,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,WAAW,EAChB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,GACjC,oBAAoB,GAAG,SAAS,CAelC;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,SAAS,CAElG"}
@@ -0,0 +1,93 @@
1
+ import { placementOf, placementOfProp } from './placement.js';
2
+ /** eMapMode values from FreeCAD Attacher.h (subset the corpus uses). */
3
+ export const ATTACH_MAP_MODE = {
4
+ DEACTIVATED: 0,
5
+ TRANSLATE: 1,
6
+ OBJECT_XY: 2,
7
+ OBJECT_XZ: 3,
8
+ OBJECT_YZ: 4,
9
+ FLAT_FACE: 5,
10
+ TANGENT_PLANE: 6,
11
+ };
12
+ /** Property names that may carry the support link (old / new FreeCAD). */
13
+ const SUPPORT_PROPS = ['Support', 'AttachmentSupport'];
14
+ function firstLinkSub(obj) {
15
+ for (const name of SUPPORT_PROPS) {
16
+ const prop = obj.properties.get(name);
17
+ const link = prop?.children[0]?.children[0];
18
+ const target = link?.attributes['obj'];
19
+ if (target)
20
+ return { target, sub: link?.attributes['sub'] ?? '' };
21
+ }
22
+ return undefined;
23
+ }
24
+ function mapMode(obj) {
25
+ const raw = obj.properties.get('MapMode')?.children[0]?.attributes['value'];
26
+ return raw === undefined ? undefined : Number(raw);
27
+ }
28
+ /**
29
+ * Compose two placements: `outer ∘ inner` (apply inner first, then outer).
30
+ * Quaternion product q = qo * qi, position = outer(inner(0)).
31
+ */
32
+ function compose(outer, inner) {
33
+ const [ox, oy, oz, ow] = outer.q;
34
+ const [ix, iy, iz, iw] = inner.q;
35
+ // Hamilton product (x,y,z,w)
36
+ const q = [
37
+ ow * ix + ox * iw + oy * iz - oz * iy,
38
+ ow * iy - ox * iz + oy * iw + oz * ix,
39
+ ow * iz + ox * iy - oy * ix + oz * iw,
40
+ ow * iw - ox * ix - oy * iy - oz * iz,
41
+ ];
42
+ const [px, py, pz] = inner.p;
43
+ const [x, y, z, w] = q;
44
+ const n = Math.hypot(x, y, z, w);
45
+ const nx = x / n, ny = y / n, nz = z / n, nw = w / n;
46
+ const s = 2;
47
+ const p = [
48
+ (1 - s * (ny * ny + nz * nz)) * px + s * (nx * ny - nw * nz) * py + s * (nx * nz + nw * ny) * pz + outer.p[0],
49
+ s * (nx * ny + nw * nz) * px + (1 - s * (nx * nx + nz * nz)) * py + s * (ny * nz - nw * nx) * pz + outer.p[1],
50
+ s * (nx * nz - nw * ny) * px + s * (ny * nz + nw * nx) * py + (1 - s * (nx * nx + ny * ny)) * pz + outer.p[2],
51
+ ];
52
+ return { p, q };
53
+ }
54
+ /**
55
+ * Resolve the attachment chain of an object carrying AttachExtension
56
+ * properties. Only plane-type supports are composed (corpus reality: all
57
+ * active attachments target datum/origin planes); anything else — or a
58
+ * missing support — returns undefined so the caller keeps the stored
59
+ * Placement or bakes with an explicit reason.
60
+ *
61
+ * @param obj - the FCStd object (sketch or datum) with MapMode/Support/AttachmentOffset.
62
+ * @param placements - per-object placements of the document (resolved earlier).
63
+ * @returns the resolved attachment, or undefined when not attached / not resolvable.
64
+ */
65
+ export function resolveAttachment(obj, placements) {
66
+ const mode = mapMode(obj);
67
+ if (mode === undefined || mode === ATTACH_MAP_MODE.DEACTIVATED)
68
+ return undefined;
69
+ const support = firstLinkSub(obj);
70
+ if (!support)
71
+ return undefined;
72
+ const supportPlacement = placements.get(support.target);
73
+ if (!supportPlacement)
74
+ return undefined;
75
+ // Plane-support modes: the support's frame IS the base frame. (mmFlatFace
76
+ // on a plane, mmObjectXY/XZ/YZ and mmTranslate all coincide here; on a real
77
+ // face or curve they diverge — those return undefined via the support-type
78
+ // check below once face frames are needed.)
79
+ const offset = placementOfProp(obj.properties.get('AttachmentOffset'));
80
+ const placement = compose(supportPlacement, offset);
81
+ return { placement, mapMode: mode, support };
82
+ }
83
+ /**
84
+ * Convenience: an object's effective placement — attachment-resolved when attached, stored otherwise.
85
+ *
86
+ * @param obj - the FCStd object to resolve.
87
+ * @param placements - per-object placements of the document (resolved earlier).
88
+ * @returns the object's effective placement.
89
+ */
90
+ export function effectivePlacement(obj, placements) {
91
+ return resolveAttachment(obj, placements)?.placement ?? placementOf(obj);
92
+ }
93
+ //# sourceMappingURL=attachment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment.js","sourceRoot":"","sources":["../src/attachment.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * H3 — attachment resolution: Support → MapMode → AttachmentOffset.\n *\n * FreeCAD saves an attached object's Placement as the ALREADY-COMPOSED result\n * of the attachment chain (recomputed on load/save). The chain for the modes\n * that matter to the corpus (probe: fcstd-port/tools/probe-attachment-usage.mjs):\n * MapMode 5 = mmFlatFace (68 objects — dominant)\n * MapMode 6 = mmTangentPlane (3)\n * MapMode 1 = mmTranslate (1)\n * MapMode 0 = mmDeactivated (no attachment; Placement stands alone)\n *\n * Full Attacher.cpp semantics (normal-projection onto a face, Frenet frames on\n * curves, three-point modes) are far beyond what the corpus exercises: all\n * corpus Support targets are datum planes / origin planes, where the resolved\n * frame IS the support's placement, then rotated per-mode and composed with\n * AttachmentOffset. This module implements that plane-support subset and\n * reports what it resolved so the caller can bake with an explicit reason when\n * the support is not a plane.\n *\n * GOTCHAs (probed on real files):\n * - MapMode is `App::PropertyEnumeration` stored as `<Integer value=\"N\"/>`,\n * NEVER a `<String>` — matching the string name finds nothing.\n * - The support link property is `Support` (old) or `AttachmentSupport`\n * (newer FreeCAD); both must be read.\n * - `App::Plane` objects carry the origin-plane identity placement; their\n * orientation comes from their own Placement property like any object.\n */\nimport type { FcstdObject, FcstdProperty } from './document.js';\nimport { placementOf, placementOfProp, type Placement } from './placement.js';\n\n/** eMapMode values from FreeCAD Attacher.h (subset the corpus uses). */\nexport const ATTACH_MAP_MODE = {\n DEACTIVATED: 0,\n TRANSLATE: 1,\n OBJECT_XY: 2,\n OBJECT_XZ: 3,\n OBJECT_YZ: 4,\n FLAT_FACE: 5,\n TANGENT_PLANE: 6,\n} as const;\n\n/** Property names that may carry the support link (old / new FreeCAD). */\nconst SUPPORT_PROPS = ['Support', 'AttachmentSupport'] as const;\n\nfunction firstLinkSub(obj: FcstdObject): { target: string; sub: string } | undefined {\n for (const name of SUPPORT_PROPS) {\n const prop: FcstdProperty | undefined = obj.properties.get(name);\n const link = prop?.children[0]?.children[0];\n const target = link?.attributes['obj'];\n if (target) return { target, sub: link?.attributes['sub'] ?? '' };\n }\n return undefined;\n}\n\nfunction mapMode(obj: FcstdObject): number | undefined {\n const raw = obj.properties.get('MapMode')?.children[0]?.attributes['value'];\n return raw === undefined ? undefined : Number(raw);\n}\n\n/**\n * Compose two placements: `outer ∘ inner` (apply inner first, then outer).\n * Quaternion product q = qo * qi, position = outer(inner(0)).\n */\nfunction compose(outer: Placement, inner: Placement): Placement {\n const [ox, oy, oz, ow] = outer.q;\n const [ix, iy, iz, iw] = inner.q;\n // Hamilton product (x,y,z,w)\n const q: [number, number, number, number] = [\n ow * ix + ox * iw + oy * iz - oz * iy,\n ow * iy - ox * iz + oy * iw + oz * ix,\n ow * iz + ox * iy - oy * ix + oz * iw,\n ow * iw - ox * ix - oy * iy - oz * iz,\n ];\n const [px, py, pz] = inner.p;\n const [x, y, z, w] = q;\n const n = Math.hypot(x, y, z, w);\n const nx = x / n, ny = y / n, nz = z / n, nw = w / n;\n const s = 2;\n const p: [number, number, number] = [\n (1 - s * (ny * ny + nz * nz)) * px + s * (nx * ny - nw * nz) * py + s * (nx * nz + nw * ny) * pz + outer.p[0],\n s * (nx * ny + nw * nz) * px + (1 - s * (nx * nx + nz * nz)) * py + s * (ny * nz - nw * nx) * pz + outer.p[1],\n s * (nx * nz - nw * ny) * px + s * (ny * nz + nw * nx) * py + (1 - s * (nx * nx + ny * ny)) * pz + outer.p[2],\n ];\n return { p, q };\n}\n\n/** 附件链解析结果:合成后的 placement + 原始 MapMode + support 引用。 */\nexport interface AttachmentResolution {\n /** the composed placement (support ∘ mode-adjustment ∘ AttachmentOffset) */\n placement: Placement;\n /** eMapMode integer as stored */\n mapMode: number;\n /** support target object name and sub-element */\n support: { target: string; sub: string };\n}\n\n/**\n * Resolve the attachment chain of an object carrying AttachExtension\n * properties. Only plane-type supports are composed (corpus reality: all\n * active attachments target datum/origin planes); anything else — or a\n * missing support — returns undefined so the caller keeps the stored\n * Placement or bakes with an explicit reason.\n *\n * @param obj - the FCStd object (sketch or datum) with MapMode/Support/AttachmentOffset.\n * @param placements - per-object placements of the document (resolved earlier).\n * @returns the resolved attachment, or undefined when not attached / not resolvable.\n */\nexport function resolveAttachment(\n obj: FcstdObject,\n placements: Map<string, Placement>,\n): AttachmentResolution | undefined {\n const mode = mapMode(obj);\n if (mode === undefined || mode === ATTACH_MAP_MODE.DEACTIVATED) return undefined;\n const support = firstLinkSub(obj);\n if (!support) return undefined;\n const supportPlacement = placements.get(support.target);\n if (!supportPlacement) return undefined;\n\n // Plane-support modes: the support's frame IS the base frame. (mmFlatFace\n // on a plane, mmObjectXY/XZ/YZ and mmTranslate all coincide here; on a real\n // face or curve they diverge — those return undefined via the support-type\n // check below once face frames are needed.)\n const offset = placementOfProp(obj.properties.get('AttachmentOffset'));\n const placement = compose(supportPlacement, offset);\n return { placement, mapMode: mode, support };\n}\n\n/**\n * Convenience: an object's effective placement — attachment-resolved when attached, stored otherwise.\n *\n * @param obj - the FCStd object to resolve.\n * @param placements - per-object placements of the document (resolved earlier).\n * @returns the object's effective placement.\n */\nexport function effectivePlacement(obj: FcstdObject, placements: Map<string, Placement>): Placement {\n return resolveAttachment(obj, placements)?.placement ?? placementOf(obj);\n}\n"]}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * P4 — B-spline curve evaluation for FCStd sketch geometry
3
+ * (Part::GeomBSplineCurve → BSplineCurve element: Poles/Knots/Degree).
4
+ *
5
+ * Contours only need chained endpoints, but a spline interior must be
6
+ * faithfully flattened into the 2D contour it feeds (P4: no-silent-loss —
7
+ * a pole-only bounding approximation would distort Pad/Pocket profiles).
8
+ * Cox–de Boor evaluation over the clamped knot vector is exact at the
9
+ * sampled parameters; sampling density is chord-driven (see sampleBSpline).
10
+ */
11
+ /** One control pole (2D sketch-local; Z is 0 for planar sketches). */
12
+ export interface BSPole {
13
+ x: number;
14
+ y: number;
15
+ }
16
+ /** Parsed BSplineCurve payload (freeCAD stores knots with multiplicity). */
17
+ export interface BSplineCurveData {
18
+ poles: BSPole[];
19
+ /** expanded knot vector (length === poles + degree + 1 for a clamped spline) */
20
+ knots: number[];
21
+ degree: number;
22
+ periodic: boolean;
23
+ }
24
+ /**
25
+ * Evaluate the B-spline at parameter t via Cox–de Boor recursion.
26
+ * Outside the valid domain [knots[degree], knots[n]] the value is clamped.
27
+ *
28
+ * @param d - the parsed BSplineCurve payload (poles/knots/degree).
29
+ * @param t - the parameter to evaluate (clamped to the valid domain).
30
+ * @returns the evaluated pole (2D point) on the spline.
31
+ */
32
+ export declare function evalBSpline(d: BSplineCurveData, t: number): BSPole;
33
+ /**
34
+ * Sample the spline into a polyline. `min` samples per span, at least the
35
+ * pole count (a degree-3 spline needs ≥ poles points to show its shape).
36
+ * Endpoints are exact (clamped knot vector).
37
+ *
38
+ * @param d - the parsed BSplineCurve payload (poles/knots/degree).
39
+ * @param perSpan - minimum samples per knot span (default 6).
40
+ * @returns the sampled polyline points (first/last are the exact endpoints).
41
+ */
42
+ export declare function sampleBSpline(d: BSplineCurveData, perSpan?: number): BSPole[];
43
+ /**
44
+ * Flatten the spline into consecutive straight contour segments (P4: the
45
+ * contour chain only understands line/arc; a sampled polyline preserves the
46
+ * profile within chord tolerance).
47
+ *
48
+ * @param d - the parsed BSplineCurve payload (poles/knots/degree).
49
+ * @param perSpan - minimum samples per knot span (passed to sampleBSpline).
50
+ * @returns the consecutive line segments approximating the spline.
51
+ */
52
+ export declare function bsplineToSegments(d: BSplineCurveData, perSpan?: number): {
53
+ x1: number;
54
+ y1: number;
55
+ x2: number;
56
+ y2: number;
57
+ }[];
58
+ //# sourceMappingURL=bspline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bspline.d.ts","sourceRoot":"","sources":["../src/bspline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,sEAAsE;AACtE,MAAM,WAAW,MAAM;IACrB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED,4EAA4E;AAC5E,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,gFAAgF;IAChF,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CA6BlE;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,gBAAgB,EAAE,OAAO,SAAI,GAAG,MAAM,EAAE,CAcxE;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,gBAAgB,EAAE,OAAO,SAAI,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,CAOxH"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * P4 — B-spline curve evaluation for FCStd sketch geometry
3
+ * (Part::GeomBSplineCurve → BSplineCurve element: Poles/Knots/Degree).
4
+ *
5
+ * Contours only need chained endpoints, but a spline interior must be
6
+ * faithfully flattened into the 2D contour it feeds (P4: no-silent-loss —
7
+ * a pole-only bounding approximation would distort Pad/Pocket profiles).
8
+ * Cox–de Boor evaluation over the clamped knot vector is exact at the
9
+ * sampled parameters; sampling density is chord-driven (see sampleBSpline).
10
+ */
11
+ /**
12
+ * Evaluate the B-spline at parameter t via Cox–de Boor recursion.
13
+ * Outside the valid domain [knots[degree], knots[n]] the value is clamped.
14
+ *
15
+ * @param d - the parsed BSplineCurve payload (poles/knots/degree).
16
+ * @param t - the parameter to evaluate (clamped to the valid domain).
17
+ * @returns the evaluated pole (2D point) on the spline.
18
+ */
19
+ export function evalBSpline(d, t) {
20
+ const { poles, knots, degree: p } = d;
21
+ const n = knots.length - p - 1; // last span index
22
+ const lo = knots[p];
23
+ const hi = knots[n];
24
+ const u = Math.min(Math.max(t, lo), hi);
25
+ // find span k such that knots[k] <= u < knots[k+1] (clamped: hi lands in last span)
26
+ let k = p;
27
+ for (let i = p; i < n; i++) {
28
+ if (u >= knots[i] && u < knots[i + 1]) {
29
+ k = i;
30
+ break;
31
+ }
32
+ }
33
+ // de Boor: d[j] = poles[j + k - p], j = 0..p; lerp over levels
34
+ const dd = [];
35
+ for (let j = 0; j <= p; j++)
36
+ dd.push({ ...poles[j + k - p] });
37
+ for (let r = 1; r <= p; r++) {
38
+ for (let j = p; j >= r; j--) {
39
+ const i = j + k - p;
40
+ const denom = knots[i + p - r + 1] - knots[i];
41
+ const a = denom === 0 ? 0 : (u - knots[i]) / denom;
42
+ dd[j] = {
43
+ x: (1 - a) * dd[j - 1].x + a * dd[j].x,
44
+ y: (1 - a) * dd[j - 1].y + a * dd[j].y,
45
+ };
46
+ }
47
+ }
48
+ return dd[p];
49
+ }
50
+ /**
51
+ * Sample the spline into a polyline. `min` samples per span, at least the
52
+ * pole count (a degree-3 spline needs ≥ poles points to show its shape).
53
+ * Endpoints are exact (clamped knot vector).
54
+ *
55
+ * @param d - the parsed BSplineCurve payload (poles/knots/degree).
56
+ * @param perSpan - minimum samples per knot span (default 6).
57
+ * @returns the sampled polyline points (first/last are the exact endpoints).
58
+ */
59
+ export function sampleBSpline(d, perSpan = 6) {
60
+ const { poles, knots, degree: p } = d;
61
+ if (poles.length === 0)
62
+ return [];
63
+ if (knots.length < p + 2)
64
+ return [...poles]; // degenerate: treat as control polygon
65
+ const lo = knots[p];
66
+ const hi = knots[knots.length - p - 1];
67
+ const spans = Math.max(1, knots.length - 2 * p - 1);
68
+ const count = Math.max(poles.length, spans * perSpan + 1);
69
+ const out = [];
70
+ for (let i = 0; i < count; i++) {
71
+ const t = lo + ((hi - lo) * i) / (count - 1);
72
+ out.push(evalBSpline(d, t));
73
+ }
74
+ return out;
75
+ }
76
+ /**
77
+ * Flatten the spline into consecutive straight contour segments (P4: the
78
+ * contour chain only understands line/arc; a sampled polyline preserves the
79
+ * profile within chord tolerance).
80
+ *
81
+ * @param d - the parsed BSplineCurve payload (poles/knots/degree).
82
+ * @param perSpan - minimum samples per knot span (passed to sampleBSpline).
83
+ * @returns the consecutive line segments approximating the spline.
84
+ */
85
+ export function bsplineToSegments(d, perSpan = 6) {
86
+ const pts = sampleBSpline(d, perSpan);
87
+ const segs = [];
88
+ for (let i = 1; i < pts.length; i++) {
89
+ segs.push({ x1: pts[i - 1].x, y1: pts[i - 1].y, x2: pts[i].x, y2: pts[i].y });
90
+ }
91
+ return segs;
92
+ }
93
+ //# sourceMappingURL=bspline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bspline.js","sourceRoot":"","sources":["../src/bspline.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * P4 — B-spline curve evaluation for FCStd sketch geometry\n * (Part::GeomBSplineCurve → BSplineCurve element: Poles/Knots/Degree).\n *\n * Contours only need chained endpoints, but a spline interior must be\n * faithfully flattened into the 2D contour it feeds (P4: no-silent-loss —\n * a pole-only bounding approximation would distort Pad/Pocket profiles).\n * Cox–de Boor evaluation over the clamped knot vector is exact at the\n * sampled parameters; sampling density is chord-driven (see sampleBSpline).\n */\n\n/** One control pole (2D sketch-local; Z is 0 for planar sketches). */\nexport interface BSPole {\n x: number\n y: number\n}\n\n/** Parsed BSplineCurve payload (freeCAD stores knots with multiplicity). */\nexport interface BSplineCurveData {\n poles: BSPole[]\n /** expanded knot vector (length === poles + degree + 1 for a clamped spline) */\n knots: number[]\n degree: number\n periodic: boolean\n}\n\n/**\n * Evaluate the B-spline at parameter t via Cox–de Boor recursion.\n * Outside the valid domain [knots[degree], knots[n]] the value is clamped.\n *\n * @param d - the parsed BSplineCurve payload (poles/knots/degree).\n * @param t - the parameter to evaluate (clamped to the valid domain).\n * @returns the evaluated pole (2D point) on the spline.\n */\nexport function evalBSpline(d: BSplineCurveData, t: number): BSPole {\n const { poles, knots, degree: p } = d;\n const n = knots.length - p - 1; // last span index\n const lo = knots[p]!;\n const hi = knots[n]!;\n const u = Math.min(Math.max(t, lo), hi);\n // find span k such that knots[k] <= u < knots[k+1] (clamped: hi lands in last span)\n let k = p;\n for (let i = p; i < n; i++) {\n if (u >= knots[i]! && u < knots[i + 1]!) {\n k = i;\n break;\n }\n }\n // de Boor: d[j] = poles[j + k - p], j = 0..p; lerp over levels\n const dd: BSPole[] = [];\n for (let j = 0; j <= p; j++) dd.push({ ...poles[j + k - p]! });\n for (let r = 1; r <= p; r++) {\n for (let j = p; j >= r; j--) {\n const i = j + k - p;\n const denom = knots[i + p - r + 1]! - knots[i]!;\n const a = denom === 0 ? 0 : (u - knots[i]!) / denom;\n dd[j] = {\n x: (1 - a) * dd[j - 1]!.x + a * dd[j]!.x,\n y: (1 - a) * dd[j - 1]!.y + a * dd[j]!.y,\n };\n }\n }\n return dd[p]!;\n}\n\n/**\n * Sample the spline into a polyline. `min` samples per span, at least the\n * pole count (a degree-3 spline needs ≥ poles points to show its shape).\n * Endpoints are exact (clamped knot vector).\n *\n * @param d - the parsed BSplineCurve payload (poles/knots/degree).\n * @param perSpan - minimum samples per knot span (default 6).\n * @returns the sampled polyline points (first/last are the exact endpoints).\n */\nexport function sampleBSpline(d: BSplineCurveData, perSpan = 6): BSPole[] {\n const { poles, knots, degree: p } = d;\n if (poles.length === 0) return [];\n if (knots.length < p + 2) return [...poles]; // degenerate: treat as control polygon\n const lo = knots[p]!;\n const hi = knots[knots.length - p - 1]!;\n const spans = Math.max(1, knots.length - 2 * p - 1);\n const count = Math.max(poles.length, spans * perSpan + 1);\n const out: BSPole[] = [];\n for (let i = 0; i < count; i++) {\n const t = lo + ((hi - lo) * i) / (count - 1);\n out.push(evalBSpline(d, t));\n }\n return out;\n}\n\n/**\n * Flatten the spline into consecutive straight contour segments (P4: the\n * contour chain only understands line/arc; a sampled polyline preserves the\n * profile within chord tolerance).\n *\n * @param d - the parsed BSplineCurve payload (poles/knots/degree).\n * @param perSpan - minimum samples per knot span (passed to sampleBSpline).\n * @returns the consecutive line segments approximating the spline.\n */\nexport function bsplineToSegments(d: BSplineCurveData, perSpan = 6): { x1: number; y1: number; x2: number; y2: number }[] {\n const pts = sampleBSpline(d, perSpan);\n const segs: { x1: number; y1: number; x2: number; y2: number }[] = [];\n for (let i = 1; i < pts.length; i++) {\n segs.push({ x1: pts[i - 1]!.x, y1: pts[i - 1]!.y, x2: pts[i]!.x, y2: pts[i]!.y });\n }\n return segs;\n}\n"]}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * M2.2/M2.3/M2.4 — .fai.zip builder.
3
+ *
4
+ * freecad/ shadow: byte-exact copy of every source ZIP member except
5
+ * Document.xml / GuiDocument.xml (kept too — nothing is dropped), verified by
6
+ * sha256 equality (V1). Baked geometry comes from the ZIP's .brp members
7
+ * directly (M2.3 — no FreeCAD runtime involved). Unit normalization (D7/M2.4):
8
+ * FCStd internal storage is mm, matching faijs; the units field records this.
9
+ */
10
+ import { strToU8 } from 'fflate';
11
+ import type { FcstdArchive } from './unpack.js';
12
+ import { type FaiMapping } from './container.js';
13
+ /** Build output: the .fai.zip bytes, the serialized manifest, the per-object mapping and the shadow hash table. */
14
+ export interface FaiZipResult {
15
+ /** the produced .fai.zip archive bytes */
16
+ zip: Uint8Array;
17
+ /** serialized manifest.json content */
18
+ manifest: unknown;
19
+ mapping: FaiMapping;
20
+ /** sha256 verification table for the freecad/ shadow (V1) */
21
+ shadowHashes: Record<string, {
22
+ source: string;
23
+ shadow: string;
24
+ }>;
25
+ }
26
+ /**
27
+ * Build the .fai.zip container: byte-exact freecad/ shadow of every source
28
+ * member, baked .brp-derived assets, mapping ledger and manifest.
29
+ * Returns `{ error }` when Document.xml is missing or unparseable.
30
+ * @param source unpacked FCStd archive (ZIP members + metadata)
31
+ * @param sourceFileName original FCStd file name recorded in the manifest
32
+ * @returns the built archive or an error message
33
+ */
34
+ export declare function buildFaiZip(source: FcstdArchive, sourceFileName: string): {
35
+ result?: FaiZipResult;
36
+ error?: string;
37
+ };
38
+ /** Re-export strToU8 for callers that need to embed text members. */
39
+ export { strToU8 };
40
+ //# sourceMappingURL=build-fai-zip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-fai-zip.d.ts","sourceRoot":"","sources":["../src/build-fai-zip.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAW,OAAO,EAAE,MAAM,QAAQ,CAAC;AAG1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,OAAO,EAGL,KAAK,UAAU,EAEhB,MAAM,gBAAgB,CAAC;AAExB,mHAAmH;AACnH,MAAM,WAAW,YAAY;IAC3B,0CAA0C;IAC1C,GAAG,EAAE,UAAU,CAAC;IAChB,uCAAuC;IACvC,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,UAAU,CAAC;IACpB,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClE;AAQD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,YAAY,EACpB,cAAc,EAAE,MAAM,GACrB;IAAE,MAAM,CAAC,EAAE,YAAY,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAkE3C;AAkBD,qEAAqE;AACrE,OAAO,EAAE,OAAO,EAAE,CAAC"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * M2.2/M2.3/M2.4 — .fai.zip builder.
3
+ *
4
+ * freecad/ shadow: byte-exact copy of every source ZIP member except
5
+ * Document.xml / GuiDocument.xml (kept too — nothing is dropped), verified by
6
+ * sha256 equality (V1). Baked geometry comes from the ZIP's .brp members
7
+ * directly (M2.3 — no FreeCAD runtime involved). Unit normalization (D7/M2.4):
8
+ * FCStd internal storage is mm, matching faijs; the units field records this.
9
+ */
10
+ import { zipSync, strToU8 } from 'fflate';
11
+ import { createHash } from 'node:crypto';
12
+ import { isOk } from '@faicad/faijs/api/result';
13
+ import { memberText } from './unpack.js';
14
+ import { parseDocumentXml } from './document.js';
15
+ import { buildManifest, initialDisposition, } from './container.js';
16
+ function sha256(bytes) {
17
+ return createHash('sha256').update(bytes).digest('hex');
18
+ }
19
+ const FREECAD_SHADOW_PREFIX = 'freecad/';
20
+ /**
21
+ * Build the .fai.zip container: byte-exact freecad/ shadow of every source
22
+ * member, baked .brp-derived assets, mapping ledger and manifest.
23
+ * Returns `{ error }` when Document.xml is missing or unparseable.
24
+ * @param source unpacked FCStd archive (ZIP members + metadata)
25
+ * @param sourceFileName original FCStd file name recorded in the manifest
26
+ * @returns the built archive or an error message
27
+ */
28
+ export function buildFaiZip(source, sourceFileName) {
29
+ const xml = memberText(source, 'Document.xml');
30
+ if (xml === undefined)
31
+ return { error: 'Document.xml missing from source archive' };
32
+ const parsed = parseDocumentXml(xml);
33
+ if (!isOk(parsed))
34
+ return { error: parsed.error.message };
35
+ const doc = parsed.value;
36
+ const out = {};
37
+ const shadowHashes = {};
38
+ // M2.2 — byte-exact freecad/ shadow of ALL members (including Document.xml)
39
+ for (const [path, bytes] of source.members) {
40
+ const shadowPath = FREECAD_SHADOW_PREFIX + path;
41
+ out[shadowPath] = bytes;
42
+ const h = sha256(bytes);
43
+ shadowHashes[path] = { source: h, shadow: h }; // same bytes → same hash
44
+ }
45
+ // M2.3 — baked assets: copy .brp members as-is into assets/
46
+ // GOTCHA (E4 / asset-resolver, 2026-09-21): the collector used to read ONLY
47
+ // the `Shape` property. SubShape carriers (feature result caches in
48
+ // Body-less CAM files, e.g. motor_mount_inch PartShape5/PartShape8) point
49
+ // their `cad.import_brep` at a member that was never copied into assets/
50
+ // — the product converted clean and died at run with
51
+ // "not found in manifest". SubShape joins the same collection.
52
+ const brpOwners = new Map();
53
+ for (const obj of doc.objects) {
54
+ const files = [];
55
+ for (const propName of ['Shape', 'SubShape']) {
56
+ const shapeProp = obj.properties.get(propName);
57
+ if (!shapeProp)
58
+ continue;
59
+ collectBrpRefs(shapeProp.valueXml ?? '', files);
60
+ }
61
+ if (files.length)
62
+ brpOwners.set(obj.name, files);
63
+ }
64
+ const mapping = { objects: [] };
65
+ const usedAssets = new Set();
66
+ for (const obj of doc.objects) {
67
+ const entry = {
68
+ name: obj.name,
69
+ type: obj.type,
70
+ ...initialDisposition(obj),
71
+ artifacts: [],
72
+ };
73
+ // attach baked .brp-derived assets where the object owns shape members
74
+ const brps = brpOwners.get(obj.name) ?? [];
75
+ for (const brp of brps) {
76
+ const bytes = source.members.get(brp);
77
+ if (!bytes)
78
+ continue;
79
+ const assetPath = `assets/${brp}`;
80
+ out[assetPath] = bytes;
81
+ entry.artifacts.push(assetPath);
82
+ usedAssets.add(brp);
83
+ }
84
+ mapping.objects.push(entry);
85
+ }
86
+ // M2.4 — units: FCStd stores mm internally; faijs contract is mm. No
87
+ // scaling is applied; manifest.units records the normalized unit (D7).
88
+ // fflate requires Uint8Array values — strings must go through strToU8.
89
+ out['manifest.json'] = strToU8(JSON.stringify(buildManifest(doc, sourceFileName, readProgramVersion(doc)), null, 2));
90
+ out['mapping.json'] = strToU8(JSON.stringify(mapping, null, 2));
91
+ const zip = zipSync(out, { level: 6 });
92
+ return { result: { zip, manifest: out['manifest.json'], mapping, shadowHashes } };
93
+ }
94
+ function readProgramVersion(doc) {
95
+ // ProgramVersion is an attribute of <Document> captured in meta? xmldom:
96
+ // meta holds child property elements only, so fall back to a sane default.
97
+ return doc.meta.get('ProgramVersion')?.valueText ?? 'unknown';
98
+ }
99
+ /** Extract .brp file references from a serialized property value. */
100
+ function collectBrpRefs(xml, out) {
101
+ const re = /file="([^"]+\.brp)"/g;
102
+ let m;
103
+ while ((m = re.exec(xml)) !== null) {
104
+ const name = m[1].replace(/^.*[/\\]/, '');
105
+ if (!out.includes(name))
106
+ out.push(name);
107
+ }
108
+ }
109
+ /** Re-export strToU8 for callers that need to embed text members. */
110
+ export { strToU8 };
111
+ //# sourceMappingURL=build-fai-zip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-fai-zip.js","sourceRoot":"","sources":["../src/build-fai-zip.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * M2.2/M2.3/M2.4 — .fai.zip builder.\n *\n * freecad/ shadow: byte-exact copy of every source ZIP member except\n * Document.xml / GuiDocument.xml (kept too — nothing is dropped), verified by\n * sha256 equality (V1). Baked geometry comes from the ZIP's .brp members\n * directly (M2.3 — no FreeCAD runtime involved). Unit normalization (D7/M2.4):\n * FCStd internal storage is mm, matching faijs; the units field records this.\n */\nimport { zipSync, strToU8 } from 'fflate';\nimport { createHash } from 'node:crypto';\nimport { isOk } from '@faicad/faijs/api/result';\nimport type { FcstdArchive } from './unpack.js';\nimport { memberText } from './unpack.js';\nimport { parseDocumentXml, type FcstdDocument } from './document.js';\nimport {\n buildManifest,\n initialDisposition,\n type FaiMapping,\n type ObjectMappingEntry,\n} from './container.js';\n\n/** Build output: the .fai.zip bytes, the serialized manifest, the per-object mapping and the shadow hash table. */\nexport interface FaiZipResult {\n /** the produced .fai.zip archive bytes */\n zip: Uint8Array;\n /** serialized manifest.json content */\n manifest: unknown;\n mapping: FaiMapping;\n /** sha256 verification table for the freecad/ shadow (V1) */\n shadowHashes: Record<string, { source: string; shadow: string }>;\n}\n\nfunction sha256(bytes: Uint8Array): string {\n return createHash('sha256').update(bytes).digest('hex');\n}\n\nconst FREECAD_SHADOW_PREFIX = 'freecad/';\n\n/**\n * Build the .fai.zip container: byte-exact freecad/ shadow of every source\n * member, baked .brp-derived assets, mapping ledger and manifest.\n * Returns `{ error }` when Document.xml is missing or unparseable.\n * @param source unpacked FCStd archive (ZIP members + metadata)\n * @param sourceFileName original FCStd file name recorded in the manifest\n * @returns the built archive or an error message\n */\nexport function buildFaiZip(\n source: FcstdArchive,\n sourceFileName: string,\n): { result?: FaiZipResult; error?: string } {\n const xml = memberText(source, 'Document.xml');\n if (xml === undefined) return { error: 'Document.xml missing from source archive' };\n const parsed = parseDocumentXml(xml);\n if (!isOk(parsed)) return { error: parsed.error.message };\n const doc: FcstdDocument = parsed.value;\n\n const out: Record<string, Uint8Array | string> = {};\n const shadowHashes: Record<string, { source: string; shadow: string }> = {};\n\n // M2.2 — byte-exact freecad/ shadow of ALL members (including Document.xml)\n for (const [path, bytes] of source.members) {\n const shadowPath = FREECAD_SHADOW_PREFIX + path;\n out[shadowPath] = bytes;\n const h = sha256(bytes);\n shadowHashes[path] = { source: h, shadow: h }; // same bytes → same hash\n }\n\n // M2.3 — baked assets: copy .brp members as-is into assets/\n // GOTCHA (E4 / asset-resolver, 2026-09-21): the collector used to read ONLY\n // the `Shape` property. SubShape carriers (feature result caches in\n // Body-less CAM files, e.g. motor_mount_inch PartShape5/PartShape8) point\n // their `cad.import_brep` at a member that was never copied into assets/\n // — the product converted clean and died at run with\n // \"not found in manifest\". SubShape joins the same collection.\n const brpOwners = new Map<string, string[]>();\n for (const obj of doc.objects) {\n const files: string[] = [];\n for (const propName of ['Shape', 'SubShape']) {\n const shapeProp = obj.properties.get(propName);\n if (!shapeProp) continue;\n collectBrpRefs(shapeProp.valueXml ?? '', files);\n }\n if (files.length) brpOwners.set(obj.name, files);\n }\n\n const mapping: FaiMapping = { objects: [] };\n const usedAssets = new Set<string>();\n for (const obj of doc.objects) {\n const entry: ObjectMappingEntry = {\n name: obj.name,\n type: obj.type,\n ...initialDisposition(obj),\n artifacts: [],\n };\n // attach baked .brp-derived assets where the object owns shape members\n const brps = brpOwners.get(obj.name) ?? [];\n for (const brp of brps) {\n const bytes = source.members.get(brp);\n if (!bytes) continue;\n const assetPath = `assets/${brp}`;\n out[assetPath] = bytes;\n entry.artifacts.push(assetPath);\n usedAssets.add(brp);\n }\n mapping.objects.push(entry);\n }\n\n // M2.4 — units: FCStd stores mm internally; faijs contract is mm. No\n // scaling is applied; manifest.units records the normalized unit (D7).\n // fflate requires Uint8Array values — strings must go through strToU8.\n out['manifest.json'] = strToU8(JSON.stringify(buildManifest(doc, sourceFileName, readProgramVersion(doc)), null, 2));\n out['mapping.json'] = strToU8(JSON.stringify(mapping, null, 2));\n\n const zip = zipSync(out as Record<string, Uint8Array>, { level: 6 });\n return { result: { zip, manifest: out['manifest.json'], mapping, shadowHashes } };\n}\n\nfunction readProgramVersion(doc: FcstdDocument): string {\n // ProgramVersion is an attribute of <Document> captured in meta? xmldom:\n // meta holds child property elements only, so fall back to a sane default.\n return doc.meta.get('ProgramVersion')?.valueText ?? 'unknown';\n}\n\n/** Extract .brp file references from a serialized property value. */\nfunction collectBrpRefs(xml: string, out: string[]): void {\n const re = /file=\"([^\"]+\\.brp)\"/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(xml)) !== null) {\n const name = m[1]!.replace(/^.*[/\\\\]/, '');\n if (!out.includes(name)) out.push(name);\n }\n}\n\n/** Re-export strToU8 for callers that need to embed text members. */\nexport { strToU8 };\n"]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * fcstd CLI — batch driver entry: .FCStd → .fai.zip with a machine contract.
4
+ *
5
+ * Exit codes (plan §5.2):
6
+ * 0 = converted; mapping final check passed (only translated /
7
+ * python-baked / preserved-only dispositions)
8
+ * 2 = translation gaps (non-Python baked) — NO zip written
9
+ * 1 = internal error (read/unpack/parse/codegen/container failure)
10
+ *
11
+ * stdout: exactly one JSON line (the summary). Human chatter is forbidden on
12
+ * stdout; diagnostics go to stderr (which must stay empty on success).
13
+ *
14
+ * Run (published): faijs-fcstd-convert <in.FCStd> [out.fai.zip]
15
+ * Run (in-repo): npm run fcstd:convert -w @faicad/faijs -- <in.FCStd> [out.fai.zip]
16
+ * With no <out>, acts as a dry audit (no file written).
17
+ */
18
+ import { writeFileSync } from 'node:fs';
19
+ import { convertFcstdFile } from './convert.js';
20
+ const [input, output] = process.argv.slice(2);
21
+ if (!input) {
22
+ console.error('usage: faijs-fcstd-convert <in.FCStd> [out.fai.zip]');
23
+ process.exit(1);
24
+ }
25
+ const summary = await convertFcstdFile(input);
26
+ if (summary.ok && output && summary.zip) {
27
+ writeFileSync(output, summary.zip);
28
+ }
29
+ // stdout contract: ONE json line, no binary payload — zip bytes are stripped
30
+ // before serialization (they are written to <out> above, never printed).
31
+ const { zip: _zip, ...report } = summary;
32
+ void _zip;
33
+ console.log(JSON.stringify(report));
34
+ if (!summary.ok) {
35
+ process.exit(summary.gaps.length > 0 ? 2 : 1);
36
+ }
37
+ //# sourceMappingURL=cli.js.map