@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.
- package/LICENSE +29 -0
- package/NOTICE +20 -0
- package/dist/attachment.d.ts +72 -0
- package/dist/attachment.d.ts.map +1 -0
- package/dist/attachment.js +93 -0
- package/dist/attachment.js.map +1 -0
- package/dist/bspline.d.ts +58 -0
- package/dist/bspline.d.ts.map +1 -0
- package/dist/bspline.js +93 -0
- package/dist/bspline.js.map +1 -0
- package/dist/build-fai-zip.d.ts +40 -0
- package/dist/build-fai-zip.d.ts.map +1 -0
- package/dist/build-fai-zip.js +111 -0
- package/dist/build-fai-zip.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +37 -0
- package/dist/cli.js.map +1 -0
- package/dist/codegen.d.ts +83 -0
- package/dist/codegen.d.ts.map +1 -0
- package/dist/codegen.js +618 -0
- package/dist/codegen.js.map +1 -0
- package/dist/container.d.ts +75 -0
- package/dist/container.d.ts.map +1 -0
- package/dist/container.js +36 -0
- package/dist/container.js.map +1 -0
- package/dist/contour.d.ts +43 -0
- package/dist/contour.d.ts.map +1 -0
- package/dist/contour.js +132 -0
- package/dist/contour.js.map +1 -0
- package/dist/convert.d.ts +69 -0
- package/dist/convert.d.ts.map +1 -0
- package/dist/convert.js +303 -0
- package/dist/convert.js.map +1 -0
- package/dist/document.d.ts +48 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +140 -0
- package/dist/document.js.map +1 -0
- package/dist/expressions.d.ts +79 -0
- package/dist/expressions.d.ts.map +1 -0
- package/dist/expressions.js +162 -0
- package/dist/expressions.js.map +1 -0
- package/dist/external-geo.d.ts +49 -0
- package/dist/external-geo.d.ts.map +1 -0
- package/dist/external-geo.js +140 -0
- package/dist/external-geo.js.map +1 -0
- package/dist/feature-translate.d.ts +193 -0
- package/dist/feature-translate.d.ts.map +1 -0
- package/dist/feature-translate.js +1441 -0
- package/dist/feature-translate.js.map +1 -0
- package/dist/fillet-edges.d.ts +26 -0
- package/dist/fillet-edges.d.ts.map +1 -0
- package/dist/fillet-edges.js +36 -0
- package/dist/fillet-edges.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/placement.d.ts +93 -0
- package/dist/placement.d.ts.map +1 -0
- package/dist/placement.js +117 -0
- package/dist/placement.js.map +1 -0
- package/dist/planegcs-backend.d.ts +34 -0
- package/dist/planegcs-backend.d.ts.map +1 -0
- package/dist/planegcs-backend.js +665 -0
- package/dist/planegcs-backend.js.map +1 -0
- package/dist/sketch-parse.d.ts +199 -0
- package/dist/sketch-parse.d.ts.map +1 -0
- package/dist/sketch-parse.js +263 -0
- package/dist/sketch-parse.js.map +1 -0
- package/dist/sketch-solver.d.ts +63 -0
- package/dist/sketch-solver.d.ts.map +1 -0
- package/dist/sketch-solver.js +28 -0
- package/dist/sketch-solver.js.map +1 -0
- package/dist/sketch-verify.d.ts +53 -0
- package/dist/sketch-verify.d.ts.map +1 -0
- package/dist/sketch-verify.js +71 -0
- package/dist/sketch-verify.js.map +1 -0
- package/dist/structural-types.d.ts +96 -0
- package/dist/structural-types.d.ts.map +1 -0
- package/dist/structural-types.js +155 -0
- package/dist/structural-types.js.map +1 -0
- package/dist/unpack.d.ts +44 -0
- package/dist/unpack.d.ts.map +1 -0
- package/dist/unpack.js +64 -0
- package/dist/unpack.js.map +1 -0
- package/package.json +65 -0
package/dist/convert.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B0 — THE FCStd → .fai.zip conversion pipeline (batch CLI and tests share it;
|
|
3
|
+
* `src/fcstd/cli.ts` is its only CLI). Returns a structured summary; the caller
|
|
4
|
+
* decides exit codes and output formatting (C4: batch driver must never absorb
|
|
5
|
+
* failures).
|
|
6
|
+
*
|
|
7
|
+
* 2026-09-20: the M5.4-era dev script `scripts/fcstd-to-fai-zip.ts` was a second
|
|
8
|
+
* implementation of this same pipeline (no C4 audit, no exit-code contract) and
|
|
9
|
+
* has been deleted. Whether the conversion entry is published or not, this file
|
|
10
|
+
* is the single implementation.
|
|
11
|
+
*
|
|
12
|
+
* Dispositions (plan §2, C4): translated | python-baked | preserved-only.
|
|
13
|
+
* Any other baked reason = translation gap → result.ok = false, no zip
|
|
14
|
+
* written (final check `auditMapping`).
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { unpackFcstd, memberText } from './unpack.js';
|
|
18
|
+
import { parseFilletEdges } from './fillet-edges.js';
|
|
19
|
+
import { parseDocumentXml } from './document.js';
|
|
20
|
+
import { parseSketchObject } from './sketch-parse.js';
|
|
21
|
+
import { createPlanegcsSolver } from './planegcs-backend.js';
|
|
22
|
+
import { classifySketch } from './sketch-verify.js';
|
|
23
|
+
import { resolveExternalGeometry } from './external-geo.js';
|
|
24
|
+
import { extractContours } from './contour.js';
|
|
25
|
+
import { generateModel } from './codegen.js';
|
|
26
|
+
import { effectivePlacement } from './attachment.js';
|
|
27
|
+
import { buildFaiZip } from './build-fai-zip.js';
|
|
28
|
+
import { isOk } from '@faicad/faijs/api/result';
|
|
29
|
+
import { zipSync, unzipSync, strToU8 } from 'fflate';
|
|
30
|
+
import { STRUCTURAL_TYPES, STRUCTURAL_TYPES_EXTENDED, isFemStructural, isNonModelingType, } from './structural-types.js';
|
|
31
|
+
// Kept on this module's public surface: these sets are part of the
|
|
32
|
+
// `./fcstd-convert` contract that the batch project and its tests consume. The
|
|
33
|
+
// definitions live in `structural-types.ts` so codegen can share them without
|
|
34
|
+
// a circular import.
|
|
35
|
+
export { STRUCTURAL_TYPES, STRUCTURAL_TYPES_EXTENDED, isFemStructural };
|
|
36
|
+
/** V2 tolerance: solver must reproduce stored geometry (single source). */
|
|
37
|
+
export const SKETCH_T1 = 1e-6;
|
|
38
|
+
/** Dispositions allowed in a conforming container (C4). */
|
|
39
|
+
export const ALLOWED_DISPOSITIONS = new Set(['translated', 'python-baked', 'preserved-only']);
|
|
40
|
+
/**
|
|
41
|
+
* C4 final check: reclassify `baked` entries. A baked disposition is only
|
|
42
|
+
* legitimate for Python-opaque objects (python-baked), structural/datum
|
|
43
|
+
* containers (preserved-only), or preserved display members. Everything
|
|
44
|
+
* else is a translation gap.
|
|
45
|
+
*/
|
|
46
|
+
function auditMapping(mapping) {
|
|
47
|
+
const gaps = [];
|
|
48
|
+
for (const o of mapping.objects) {
|
|
49
|
+
if (o.disposition === 'baked') {
|
|
50
|
+
// python-opaque stays legitimate but is renamed for the ledger
|
|
51
|
+
if (o.reason === 'python-opaque') {
|
|
52
|
+
o.disposition = 'python-baked';
|
|
53
|
+
}
|
|
54
|
+
else if (STRUCTURAL_TYPES.has(o.type) || isFemStructural(o.type) || STRUCTURAL_TYPES_EXTENDED.has(o.type)) {
|
|
55
|
+
o.disposition = 'preserved-only';
|
|
56
|
+
o.reason = o.reason
|
|
57
|
+
?? (isFemStructural(o.type) ? 'fem-simulation'
|
|
58
|
+
: STRUCTURAL_TYPES_EXTENDED.has(o.type) ? 'container-link' : 'structural');
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
gaps.push({ name: o.name, type: o.type, reason: o.reason ?? 'unspecified' });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return gaps;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Convert one FCStd file through the full pipeline (unpack → parse → sketch
|
|
69
|
+
* solving → codegen → container build) and return a structured summary.
|
|
70
|
+
* @param input path to the .FCStd file to convert
|
|
71
|
+
* @param opts optional knobs (see {@link ConvertOptions}); defaults keep the C4 contract
|
|
72
|
+
* @returns a summary with ok=false (and no zip, unless `keepGappedContainer`) on any pipeline failure or translation gap
|
|
73
|
+
*/
|
|
74
|
+
export async function convertFcstdFile(input, opts) {
|
|
75
|
+
const t0 = Date.now();
|
|
76
|
+
const baseName = input.replace(/^.*[/\\]/, '').replace(/\.fcstd$/i, '');
|
|
77
|
+
const fail = (error) => ({
|
|
78
|
+
file: input, ok: false, gaps: [],
|
|
79
|
+
counts: { translated: 0, pythonBaked: 0, preservedOnly: 0, baked: 0 },
|
|
80
|
+
sketches: { total: 0, l0: 0, l1: 0, l2: 0 }, error, elapsedMs: Date.now() - t0,
|
|
81
|
+
});
|
|
82
|
+
let raw;
|
|
83
|
+
try {
|
|
84
|
+
raw = new Uint8Array(readFileSync(input));
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
return fail(`read failed: ${e.message}`);
|
|
88
|
+
}
|
|
89
|
+
const unpacked = unpackFcstd(raw);
|
|
90
|
+
if (!isOk(unpacked))
|
|
91
|
+
return fail(`unpack failed: ${JSON.stringify(unpacked.error)}`);
|
|
92
|
+
const xml = memberText(unpacked.value, 'Document.xml');
|
|
93
|
+
if (xml === undefined)
|
|
94
|
+
return fail('Document.xml missing');
|
|
95
|
+
const doc = parseDocumentXml(xml);
|
|
96
|
+
if (!isOk(doc))
|
|
97
|
+
return fail(`parse failed: ${doc.error.message}`);
|
|
98
|
+
// M3: solve every sketch (same pipeline as the dev script)
|
|
99
|
+
const solver = await createPlanegcsSolver();
|
|
100
|
+
const sketchVerdict = new Map();
|
|
101
|
+
const sketchContours = new Map();
|
|
102
|
+
for (const obj of doc.value.objects) {
|
|
103
|
+
if (obj.type !== 'Sketcher::SketchObject')
|
|
104
|
+
continue;
|
|
105
|
+
const sk = parseSketchObject(obj.properties.get('Geometry'), obj.properties.get('Constraints'), false);
|
|
106
|
+
const offPlane = sk.geoms.some((g) => {
|
|
107
|
+
const zs = g.kind === 'point' ? [g.z]
|
|
108
|
+
: g.kind === 'line' ? [g.z1, g.z2]
|
|
109
|
+
: g.kind === 'bspline' ? [g.z1, g.z2] // P4: spline endpoints carry z
|
|
110
|
+
: [g.cz];
|
|
111
|
+
return zs.some((z) => Math.abs(z) > 1e-9);
|
|
112
|
+
});
|
|
113
|
+
const preBlocked = offPlane ? 'sketch-geometry-off-plane'
|
|
114
|
+
: sk.geoms.some((g) => !Number.isFinite(g.x ?? 0)) ? 'unsupported-geometry'
|
|
115
|
+
: undefined;
|
|
116
|
+
if (preBlocked) {
|
|
117
|
+
sketchVerdict.set(obj.name, { level: 'L2', reason: preBlocked });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
let external;
|
|
121
|
+
if (sk.externalGeoIds.length > 0) {
|
|
122
|
+
const ext = await resolveExternalGeometry(obj.properties.get('ExternalGeometry'), doc.value, unpacked.value, obj.properties.get('Placement'));
|
|
123
|
+
// P3-3 (2026-09-24): external EDGES are discretized to polylines — a
|
|
124
|
+
// straight edge yields 2 points, an ARC yields many. The old
|
|
125
|
+
// `length === 2` filter silently dropped every arc link with no
|
|
126
|
+
// failure record (sketch baked `external-geometry-unresolved: no
|
|
127
|
+
// links` while resolveExternalGeometry had actually succeeded). The
|
|
128
|
+
// solver pins multi-point polylines as fixed sampled targets
|
|
129
|
+
// (planegcs-backend M6.3), so any deduped polyline >= 2 is usable.
|
|
130
|
+
const usable = ext.links.filter((l) => l.polyline.length >= 2);
|
|
131
|
+
if (usable.length === 0) {
|
|
132
|
+
sketchVerdict.set(obj.name, {
|
|
133
|
+
level: 'L2',
|
|
134
|
+
reason: `external-geometry-unresolved: ${ext.failures[0]?.reason ?? 'no links'}`,
|
|
135
|
+
});
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
external = usable.map((l, i) => ({ geoId: -3 - i, polyline: l.polyline }));
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const r = await solver.solve(sk.geoms, sk.constraints, external);
|
|
142
|
+
if (!isOk(r)) {
|
|
143
|
+
sketchVerdict.set(obj.name, { level: 'L2', reason: 'solver-error' });
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const verdict = classifySketch(r.value, sk.geoms, SKETCH_T1);
|
|
147
|
+
const contours = verdict.level === 'L0' ? extractContours(r.value.geoms) : [];
|
|
148
|
+
sketchVerdict.set(obj.name, { ...verdict, loopCount: contours.length });
|
|
149
|
+
if (verdict.level === 'L0')
|
|
150
|
+
sketchContours.set(obj.name, contours);
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
sketchVerdict.set(obj.name, { level: 'L2', reason: `solver-throw: ${e.message.slice(0, 60)}` });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// M4/M5: translate + codegen
|
|
157
|
+
// H3: attachment-resolved placements — an attached sketch's stored Placement
|
|
158
|
+
// is recomputed by FreeCAD from Support ∘ AttachmentOffset; resolve the chain
|
|
159
|
+
// so non-XY-plane sketches land on their support frame (fall back to the
|
|
160
|
+
// stored value when the attachment is deactivated or unresolvable).
|
|
161
|
+
const placements = new Map();
|
|
162
|
+
for (const obj of doc.value.objects) {
|
|
163
|
+
placements.set(obj.name, effectivePlacement(obj, placements));
|
|
164
|
+
}
|
|
165
|
+
// H7: objects whose Shape is stored as a ZIP .brp member (pure-Shape
|
|
166
|
+
// carriers, e.g. Part::Feature) — collected from the archive members so the
|
|
167
|
+
// translator can record `shape-asset` instead of a translation gap.
|
|
168
|
+
// H7 follow-up: SubShape carriers (feature result caches in Body-less
|
|
169
|
+
// PartDesign files) join the same set — the translator only honors SubShape
|
|
170
|
+
// evidence for features whose SubShape .brp member exists.
|
|
171
|
+
// GOTCHA (PadTest, 2026-09-21): non-modeling types are EXCLUDED. A
|
|
172
|
+
// `PartDesign::Plane` also has a Shape .brp — the plane face — so the
|
|
173
|
+
// generic rule imported it as a solid and the Body chain unioned the datum
|
|
174
|
+
// plane into the part (a bare face cannot take part in the boolean). A
|
|
175
|
+
// datum's shape is support geometry for attachment/up-to resolution.
|
|
176
|
+
const shapeCarriers = new Set();
|
|
177
|
+
// E4: objects whose Shape/SubShape `file` attribute points at a missing or
|
|
178
|
+
// ZERO-BYTE member (FC_site_simple-102: `Site.Shape.brp` exists but is 0
|
|
179
|
+
// bytes). They must surface as an explicit convert-time gap — without this
|
|
180
|
+
// the object falls through to python-opaque and the broken-asset fact is
|
|
181
|
+
// silently swallowed into `python-baked` (the product then reports no
|
|
182
|
+
// geometry instead of naming the defect).
|
|
183
|
+
const brokenShapeAssets = new Set();
|
|
184
|
+
for (const obj of doc.value.objects) {
|
|
185
|
+
if (isNonModelingType(obj.type))
|
|
186
|
+
continue;
|
|
187
|
+
const shapeFile = obj.properties.get('Shape')?.children[0]?.attributes['file'];
|
|
188
|
+
const subShapeFile = obj.properties.get('SubShape')?.children[0]?.attributes['file'];
|
|
189
|
+
const shapeOk = shapeFile !== undefined && !!memberText(unpacked.value, shapeFile);
|
|
190
|
+
const subShapeOk = subShapeFile !== undefined && !!memberText(unpacked.value, subShapeFile);
|
|
191
|
+
if (shapeOk || subShapeOk)
|
|
192
|
+
shapeCarriers.add(obj.name);
|
|
193
|
+
else if (shapeFile !== undefined || subShapeFile !== undefined)
|
|
194
|
+
brokenShapeAssets.add(obj.name);
|
|
195
|
+
}
|
|
196
|
+
// P8: parse the binary PropertyFilletEdges members for Part::Chamfer /
|
|
197
|
+
// Part::Fillet objects (edge selection + sizes do NOT live in Document.xml).
|
|
198
|
+
const filletEdgesData = new Map();
|
|
199
|
+
for (const obj of doc.value.objects) {
|
|
200
|
+
if (obj.type !== 'Part::Chamfer' && obj.type !== 'Part::Fillet')
|
|
201
|
+
continue;
|
|
202
|
+
const file = obj.properties.get('Edges')?.children[0]?.children[0]?.attributes['file']
|
|
203
|
+
?? obj.properties.get('Edges')?.children[0]?.attributes['file'];
|
|
204
|
+
if (!file)
|
|
205
|
+
continue;
|
|
206
|
+
const parsed = parseFilletEdges(unpacked.value.members.get(file));
|
|
207
|
+
if (parsed)
|
|
208
|
+
filletEdgesData.set(obj.name, parsed);
|
|
209
|
+
}
|
|
210
|
+
let gen;
|
|
211
|
+
try {
|
|
212
|
+
gen = generateModel(doc.value, sketchVerdict, sketchContours, baseName, placements, shapeCarriers, brokenShapeAssets, filletEdgesData);
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
return fail(`codegen failed: ${e.message}`);
|
|
216
|
+
}
|
|
217
|
+
// M2: container with shadow, then inject model/ + updated mapping
|
|
218
|
+
const built = buildFaiZip(unpacked.value, baseName + '.FCStd');
|
|
219
|
+
if (built.error || !built.result)
|
|
220
|
+
return fail(`container build failed: ${built.error ?? 'unknown'}`);
|
|
221
|
+
const members = {};
|
|
222
|
+
for (const [k, v] of Object.entries(unzipSync(built.result.zip)))
|
|
223
|
+
members[k] = v;
|
|
224
|
+
members['model/main.fai.js'] = strToU8(gen.code);
|
|
225
|
+
for (const f of gen.files)
|
|
226
|
+
members[f.path] = strToU8(f.code);
|
|
227
|
+
const mapping = built.result.mapping;
|
|
228
|
+
for (const o of gen.objects) {
|
|
229
|
+
const entry = mapping.objects.find((e) => e.name === o.name);
|
|
230
|
+
if (!entry)
|
|
231
|
+
continue;
|
|
232
|
+
entry.disposition = o.disposition;
|
|
233
|
+
if (o.reason)
|
|
234
|
+
entry.reason = o.reason;
|
|
235
|
+
if (o.sketch) {
|
|
236
|
+
entry.sketch = {
|
|
237
|
+
level: o.sketch.level === 'L0' ? 'solved' : o.sketch.level === 'L1' ? 'initial-value' : 'baked',
|
|
238
|
+
reason: o.sketch.reason,
|
|
239
|
+
gcs: o.sketch,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (o.disposition === 'translated' && !entry.artifacts.includes('model/main.fai.js')) {
|
|
243
|
+
entry.artifacts.push('model/main.fai.js');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// M11.4 (G9): L1/L2 sketches persist raw geometry as a contour asset
|
|
247
|
+
for (const obj of doc.value.objects) {
|
|
248
|
+
if (obj.type !== 'Sketcher::SketchObject')
|
|
249
|
+
continue;
|
|
250
|
+
const verdict = sketchVerdict.get(obj.name);
|
|
251
|
+
if (!verdict || verdict.level === 'L0')
|
|
252
|
+
continue;
|
|
253
|
+
const sk = parseSketchObject(obj.properties.get('Geometry'), obj.properties.get('Constraints'), false);
|
|
254
|
+
const asset = {
|
|
255
|
+
sketch: obj.name,
|
|
256
|
+
level: verdict.level,
|
|
257
|
+
reason: verdict.reason,
|
|
258
|
+
geoms: sk.geoms,
|
|
259
|
+
constraints: sk.constraints.map((c) => ({ index: c.index, type: c.type, refs: c.refs, value: c.value, isDriving: c.isDriving })),
|
|
260
|
+
};
|
|
261
|
+
const path = `assets/${obj.name}.contour.json`;
|
|
262
|
+
members[path] = strToU8(JSON.stringify(asset, null, 2));
|
|
263
|
+
const entry = mapping.objects.find((e) => e.name === obj.name);
|
|
264
|
+
if (entry && !entry.artifacts.includes(path))
|
|
265
|
+
entry.artifacts.push(path);
|
|
266
|
+
}
|
|
267
|
+
// C4 final check: reclassify python-opaque; everything else baked = gap
|
|
268
|
+
const gaps = auditMapping(mapping);
|
|
269
|
+
members['mapping.json'] = strToU8(JSON.stringify(mapping, null, 2));
|
|
270
|
+
const counts = { translated: 0, pythonBaked: 0, preservedOnly: 0, baked: 0 };
|
|
271
|
+
for (const o of mapping.objects) {
|
|
272
|
+
const d = o.disposition; // auditMapping may rename baked -> python-baked
|
|
273
|
+
if (d === 'translated')
|
|
274
|
+
counts.translated++;
|
|
275
|
+
else if (d === 'python-baked')
|
|
276
|
+
counts.pythonBaked++;
|
|
277
|
+
else if (d === 'preserved-only')
|
|
278
|
+
counts.preservedOnly++;
|
|
279
|
+
else
|
|
280
|
+
counts.baked++;
|
|
281
|
+
}
|
|
282
|
+
const sketches = { total: sketchVerdict.size, l0: 0, l1: 0, l2: 0 };
|
|
283
|
+
for (const v of sketchVerdict.values())
|
|
284
|
+
sketches[v.level.toLowerCase()]++;
|
|
285
|
+
if (gaps.length > 0) {
|
|
286
|
+
// C4: translation gaps → no container produced (exit contract: 2).
|
|
287
|
+
// `keepGappedContainer` is a diagnostics-only escape hatch (see ConvertOptions).
|
|
288
|
+
if (!opts?.keepGappedContainer) {
|
|
289
|
+
return { file: input, ok: false, gaps, counts, sketches, elapsedMs: Date.now() - t0 };
|
|
290
|
+
}
|
|
291
|
+
const gappedZip = zipSync(members, { level: 6 });
|
|
292
|
+
return { file: input, ok: false, gaps, counts, sketches, zip: gappedZip, elapsedMs: Date.now() - t0 };
|
|
293
|
+
}
|
|
294
|
+
const zip = zipSync(members, { level: 6 });
|
|
295
|
+
return { file: input, ok: true, gaps, counts, sketches, zip, elapsedMs: Date.now() - t0 };
|
|
296
|
+
}
|
|
297
|
+
// Public conversion surface re-exports (former top-level `fcstd-convert` barrel).
|
|
298
|
+
// The batch project's tooling consumes these without reaching into `src/`.
|
|
299
|
+
export { createPlanegcsSolver, planegcsWasmPath } from './planegcs-backend.js';
|
|
300
|
+
export { classifySketch, maxPointDistance } from './sketch-verify.js';
|
|
301
|
+
export { resolveExternalGeometry } from './external-geo.js';
|
|
302
|
+
export { isWhitelisted } from './feature-translate.js';
|
|
303
|
+
//# sourceMappingURL=convert.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"convert.js","sourceRoot":"","sources":["../src/convert.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * B0 — THE FCStd → .fai.zip conversion pipeline (batch CLI and tests share it;\n * `src/fcstd/cli.ts` is its only CLI). Returns a structured summary; the caller\n * decides exit codes and output formatting (C4: batch driver must never absorb\n * failures).\n *\n * 2026-09-20: the M5.4-era dev script `scripts/fcstd-to-fai-zip.ts` was a second\n * implementation of this same pipeline (no C4 audit, no exit-code contract) and\n * has been deleted. Whether the conversion entry is published or not, this file\n * is the single implementation.\n *\n * Dispositions (plan §2, C4): translated | python-baked | preserved-only.\n * Any other baked reason = translation gap → result.ok = false, no zip\n * written (final check `auditMapping`).\n */\nimport { readFileSync } from 'node:fs';\nimport { unpackFcstd, memberText } from './unpack.js';\nimport { parseFilletEdges, type FilletEdgeEntry } from './fillet-edges.js';\nimport { parseDocumentXml } from './document.js';\nimport { parseSketchObject } from './sketch-parse.js';\nimport { createPlanegcsSolver } from './planegcs-backend.js';\nimport { classifySketch } from './sketch-verify.js';\nimport { resolveExternalGeometry } from './external-geo.js';\nimport { extractContours } from './contour.js';\nimport type { Contour } from './contour.js';\nimport { generateModel } from './codegen.js';\nimport type { Placement } from './placement.js';\nimport { effectivePlacement } from './attachment.js';\nimport { buildFaiZip } from './build-fai-zip.js';\nimport { isOk } from '@faicad/faijs/api/result';\nimport { zipSync, unzipSync, strToU8 } from 'fflate';\nimport {\n STRUCTURAL_TYPES,\n STRUCTURAL_TYPES_EXTENDED,\n isFemStructural,\n isNonModelingType,\n} from './structural-types.js';\n\n// Kept on this module's public surface: these sets are part of the\n// `./fcstd-convert` contract that the batch project and its tests consume. The\n// definitions live in `structural-types.ts` so codegen can share them without\n// a circular import.\nexport { STRUCTURAL_TYPES, STRUCTURAL_TYPES_EXTENDED, isFemStructural };\n\n/** V2 tolerance: solver must reproduce stored geometry (single source). */\nexport const SKETCH_T1 = 1e-6;\n\n/** Dispositions allowed in a conforming container (C4). */\nexport const ALLOWED_DISPOSITIONS = new Set(['translated', 'python-baked', 'preserved-only']);\n\n/**\n * Optional knobs for `convertFcstdFile`.\n */\nexport interface ConvertOptions {\n /**\n * Return the container bytes even when the mapping audit reports gaps.\n *\n * Diagnostics only. C4 requires that a gapped document produces **no\n * product**, and the CLI never sets this — `ok` stays false and the exit\n * code stays 2 regardless. It exists because everything the pipeline built\n * before the audit is otherwise unobservable for exactly the documents that\n * need triage: gaps and non-L0 sketches usually co-occur (a sketch the\n * solver cannot handle usually starves the features built on it), so\n * \"which contour assets / mapping entries would this document have had\"\n * cannot be answered from `gaps[]` alone.\n */\n keepGappedContainer?: boolean;\n}\n\n/** Structured result of one FCStd → .fai.zip conversion; the caller decides exit codes and formatting. */\nexport interface ConvertSummary {\n /** input file path */\n file: string;\n /** conversion succeeded AND mapping final check passed */\n ok: boolean;\n /** translation gaps (non-Python baked) — empty when ok */\n gaps: { name: string; type: string; reason: string }[];\n counts: { translated: number; pythonBaked: number; preservedOnly: number; baked: number };\n sketches: { total: number; l0: number; l1: number; l2: number };\n /** container bytes (undefined when ok=false, unless `keepGappedContainer`) */\n zip?: Uint8Array;\n /** human-readable failure when the pipeline itself failed */\n error?: string;\n elapsedMs: number;\n}\n\n/**\n * C4 final check: reclassify `baked` entries. A baked disposition is only\n * legitimate for Python-opaque objects (python-baked), structural/datum\n * containers (preserved-only), or preserved display members. Everything\n * else is a translation gap.\n */\nfunction auditMapping(\n mapping: { objects: { name: string; type: string; disposition: string; reason?: string }[] },\n): ConvertSummary['gaps'] {\n const gaps: ConvertSummary['gaps'] = [];\n for (const o of mapping.objects) {\n if (o.disposition === 'baked') {\n // python-opaque stays legitimate but is renamed for the ledger\n if (o.reason === 'python-opaque') {\n o.disposition = 'python-baked';\n } else if (STRUCTURAL_TYPES.has(o.type) || isFemStructural(o.type) || STRUCTURAL_TYPES_EXTENDED.has(o.type)) {\n o.disposition = 'preserved-only';\n o.reason = o.reason\n ?? (isFemStructural(o.type) ? 'fem-simulation'\n : STRUCTURAL_TYPES_EXTENDED.has(o.type) ? 'container-link' : 'structural');\n } else {\n gaps.push({ name: o.name, type: o.type, reason: o.reason ?? 'unspecified' });\n }\n }\n }\n return gaps;\n}\n\n/**\n * Convert one FCStd file through the full pipeline (unpack → parse → sketch\n * solving → codegen → container build) and return a structured summary.\n * @param input path to the .FCStd file to convert\n * @param opts optional knobs (see {@link ConvertOptions}); defaults keep the C4 contract\n * @returns a summary with ok=false (and no zip, unless `keepGappedContainer`) on any pipeline failure or translation gap\n */\nexport async function convertFcstdFile(input: string, opts?: ConvertOptions): Promise<ConvertSummary> {\n const t0 = Date.now();\n const baseName = input.replace(/^.*[/\\\\]/, '').replace(/\\.fcstd$/i, '');\n const fail = (error: string): ConvertSummary => ({\n file: input, ok: false, gaps: [],\n counts: { translated: 0, pythonBaked: 0, preservedOnly: 0, baked: 0 },\n sketches: { total: 0, l0: 0, l1: 0, l2: 0 }, error, elapsedMs: Date.now() - t0,\n });\n\n let raw: Uint8Array;\n try {\n raw = new Uint8Array(readFileSync(input));\n } catch (e) {\n return fail(`read failed: ${(e as Error).message}`);\n }\n const unpacked = unpackFcstd(raw);\n if (!isOk(unpacked)) return fail(`unpack failed: ${JSON.stringify(unpacked.error)}`);\n const xml = memberText(unpacked.value, 'Document.xml');\n if (xml === undefined) return fail('Document.xml missing');\n const doc = parseDocumentXml(xml);\n if (!isOk(doc)) return fail(`parse failed: ${doc.error.message}`);\n\n // M3: solve every sketch (same pipeline as the dev script)\n const solver = await createPlanegcsSolver();\n const sketchVerdict = new Map<string, { level: 'L0' | 'L1' | 'L2'; reason?: string; loopCount?: number }>();\n const sketchContours = new Map<string, Contour[]>();\n for (const obj of doc.value.objects) {\n if (obj.type !== 'Sketcher::SketchObject') continue;\n const sk = parseSketchObject(obj.properties.get('Geometry'), obj.properties.get('Constraints'), false);\n const offPlane = sk.geoms.some((g) => {\n const zs = g.kind === 'point' ? [g.z]\n : g.kind === 'line' ? [g.z1, g.z2]\n : g.kind === 'bspline' ? [g.z1, g.z2] // P4: spline endpoints carry z\n : [g.cz];\n return zs.some((z) => Math.abs(z) > 1e-9);\n });\n const preBlocked =\n offPlane ? 'sketch-geometry-off-plane'\n : sk.geoms.some((g) => !Number.isFinite((g as { x?: number }).x ?? 0)) ? 'unsupported-geometry'\n : undefined;\n if (preBlocked) {\n sketchVerdict.set(obj.name, { level: 'L2', reason: preBlocked });\n continue;\n }\n let external: { geoId: number; polyline: [number, number][] }[] | undefined;\n if (sk.externalGeoIds.length > 0) {\n const ext = await resolveExternalGeometry(\n obj.properties.get('ExternalGeometry'), doc.value, unpacked.value, obj.properties.get('Placement'),\n );\n // P3-3 (2026-09-24): external EDGES are discretized to polylines — a\n // straight edge yields 2 points, an ARC yields many. The old\n // `length === 2` filter silently dropped every arc link with no\n // failure record (sketch baked `external-geometry-unresolved: no\n // links` while resolveExternalGeometry had actually succeeded). The\n // solver pins multi-point polylines as fixed sampled targets\n // (planegcs-backend M6.3), so any deduped polyline >= 2 is usable.\n const usable = ext.links.filter((l) => l.polyline.length >= 2);\n if (usable.length === 0) {\n sketchVerdict.set(obj.name, {\n level: 'L2',\n reason: `external-geometry-unresolved: ${ext.failures[0]?.reason ?? 'no links'}`,\n });\n continue;\n }\n external = usable.map((l, i) => ({ geoId: -3 - i, polyline: l.polyline }));\n }\n try {\n const r = await solver.solve(sk.geoms, sk.constraints, external);\n if (!isOk(r)) {\n sketchVerdict.set(obj.name, { level: 'L2', reason: 'solver-error' });\n continue;\n }\n const verdict = classifySketch(r.value, sk.geoms, SKETCH_T1);\n const contours = verdict.level === 'L0' ? extractContours(r.value.geoms) : [];\n sketchVerdict.set(obj.name, { ...verdict, loopCount: contours.length });\n if (verdict.level === 'L0') sketchContours.set(obj.name, contours);\n } catch (e) {\n sketchVerdict.set(obj.name, { level: 'L2', reason: `solver-throw: ${(e as Error).message.slice(0, 60)}` });\n }\n }\n\n // M4/M5: translate + codegen\n // H3: attachment-resolved placements — an attached sketch's stored Placement\n // is recomputed by FreeCAD from Support ∘ AttachmentOffset; resolve the chain\n // so non-XY-plane sketches land on their support frame (fall back to the\n // stored value when the attachment is deactivated or unresolvable).\n const placements = new Map<string, Placement>();\n for (const obj of doc.value.objects) {\n placements.set(obj.name, effectivePlacement(obj, placements));\n }\n // H7: objects whose Shape is stored as a ZIP .brp member (pure-Shape\n // carriers, e.g. Part::Feature) — collected from the archive members so the\n // translator can record `shape-asset` instead of a translation gap.\n // H7 follow-up: SubShape carriers (feature result caches in Body-less\n // PartDesign files) join the same set — the translator only honors SubShape\n // evidence for features whose SubShape .brp member exists.\n // GOTCHA (PadTest, 2026-09-21): non-modeling types are EXCLUDED. A\n // `PartDesign::Plane` also has a Shape .brp — the plane face — so the\n // generic rule imported it as a solid and the Body chain unioned the datum\n // plane into the part (a bare face cannot take part in the boolean). A\n // datum's shape is support geometry for attachment/up-to resolution.\n const shapeCarriers = new Set<string>();\n // E4: objects whose Shape/SubShape `file` attribute points at a missing or\n // ZERO-BYTE member (FC_site_simple-102: `Site.Shape.brp` exists but is 0\n // bytes). They must surface as an explicit convert-time gap — without this\n // the object falls through to python-opaque and the broken-asset fact is\n // silently swallowed into `python-baked` (the product then reports no\n // geometry instead of naming the defect).\n const brokenShapeAssets = new Set<string>();\n for (const obj of doc.value.objects) {\n if (isNonModelingType(obj.type)) continue;\n const shapeFile = obj.properties.get('Shape')?.children[0]?.attributes['file'];\n const subShapeFile = obj.properties.get('SubShape')?.children[0]?.attributes['file'];\n const shapeOk = shapeFile !== undefined && !!memberText(unpacked.value, shapeFile);\n const subShapeOk = subShapeFile !== undefined && !!memberText(unpacked.value, subShapeFile);\n if (shapeOk || subShapeOk) shapeCarriers.add(obj.name);\n else if (shapeFile !== undefined || subShapeFile !== undefined) brokenShapeAssets.add(obj.name);\n }\n\n // P8: parse the binary PropertyFilletEdges members for Part::Chamfer /\n // Part::Fillet objects (edge selection + sizes do NOT live in Document.xml).\n const filletEdgesData = new Map<string, FilletEdgeEntry[]>();\n for (const obj of doc.value.objects) {\n if (obj.type !== 'Part::Chamfer' && obj.type !== 'Part::Fillet') continue;\n const file = obj.properties.get('Edges')?.children[0]?.children[0]?.attributes['file']\n ?? obj.properties.get('Edges')?.children[0]?.attributes['file'];\n if (!file) continue;\n const parsed = parseFilletEdges(unpacked.value.members.get(file));\n if (parsed) filletEdgesData.set(obj.name, parsed);\n }\n\n let gen;\n try {\n gen = generateModel(\n doc.value, sketchVerdict, sketchContours, baseName, placements, shapeCarriers,\n brokenShapeAssets, filletEdgesData,\n );\n } catch (e) {\n return fail(`codegen failed: ${(e as Error).message}`);\n }\n\n // M2: container with shadow, then inject model/ + updated mapping\n const built = buildFaiZip(unpacked.value, baseName + '.FCStd');\n if (built.error || !built.result) return fail(`container build failed: ${built.error ?? 'unknown'}`);\n const members: Record<string, Uint8Array> = {};\n for (const [k, v] of Object.entries(unzipSync(built.result.zip))) members[k] = v;\n members['model/main.fai.js'] = strToU8(gen.code);\n for (const f of gen.files) members[f.path] = strToU8(f.code);\n\n const mapping = built.result.mapping;\n for (const o of gen.objects) {\n const entry = mapping.objects.find((e) => e.name === o.name);\n if (!entry) continue;\n entry.disposition = o.disposition;\n if (o.reason) entry.reason = o.reason;\n if (o.sketch) {\n entry.sketch = {\n level: o.sketch.level === 'L0' ? 'solved' : o.sketch.level === 'L1' ? 'initial-value' : 'baked',\n reason: o.sketch.reason,\n gcs: o.sketch,\n };\n }\n if (o.disposition === 'translated' && !entry.artifacts.includes('model/main.fai.js')) {\n entry.artifacts.push('model/main.fai.js');\n }\n }\n\n // M11.4 (G9): L1/L2 sketches persist raw geometry as a contour asset\n for (const obj of doc.value.objects) {\n if (obj.type !== 'Sketcher::SketchObject') continue;\n const verdict = sketchVerdict.get(obj.name);\n if (!verdict || verdict.level === 'L0') continue;\n const sk = parseSketchObject(obj.properties.get('Geometry'), obj.properties.get('Constraints'), false);\n const asset = {\n sketch: obj.name,\n level: verdict.level,\n reason: verdict.reason,\n geoms: sk.geoms,\n constraints: sk.constraints.map((c) => ({ index: c.index, type: c.type, refs: c.refs, value: c.value, isDriving: c.isDriving })),\n };\n const path = `assets/${obj.name}.contour.json`;\n members[path] = strToU8(JSON.stringify(asset, null, 2));\n const entry = mapping.objects.find((e) => e.name === obj.name);\n if (entry && !entry.artifacts.includes(path)) entry.artifacts.push(path);\n }\n\n // C4 final check: reclassify python-opaque; everything else baked = gap\n const gaps = auditMapping(mapping);\n members['mapping.json'] = strToU8(JSON.stringify(mapping, null, 2));\n\n const counts = { translated: 0, pythonBaked: 0, preservedOnly: 0, baked: 0 };\n for (const o of mapping.objects) {\n const d = o.disposition as string; // auditMapping may rename baked -> python-baked\n if (d === 'translated') counts.translated++;\n else if (d === 'python-baked') counts.pythonBaked++;\n else if (d === 'preserved-only') counts.preservedOnly++;\n else counts.baked++;\n }\n const sketches = { total: sketchVerdict.size, l0: 0, l1: 0, l2: 0 };\n for (const v of sketchVerdict.values()) sketches[v.level.toLowerCase() as 'l0' | 'l1' | 'l2']++;\n\n if (gaps.length > 0) {\n // C4: translation gaps → no container produced (exit contract: 2).\n // `keepGappedContainer` is a diagnostics-only escape hatch (see ConvertOptions).\n if (!opts?.keepGappedContainer) {\n return { file: input, ok: false, gaps, counts, sketches, elapsedMs: Date.now() - t0 };\n }\n const gappedZip = zipSync(members, { level: 6 });\n return { file: input, ok: false, gaps, counts, sketches, zip: gappedZip, elapsedMs: Date.now() - t0 };\n }\n\n const zip = zipSync(members, { level: 6 });\n return { file: input, ok: true, gaps, counts, sketches, zip, elapsedMs: Date.now() - t0 };\n}\n\n// Public conversion surface re-exports (former top-level `fcstd-convert` barrel).\n// The batch project's tooling consumes these without reaching into `src/`.\nexport { createPlanegcsSolver, planegcsWasmPath } from './planegcs-backend.js';\nexport { classifySketch, maxPointDistance } from './sketch-verify.js';\nexport type { SketchVerdict } from './sketch-verify.js';\nexport { resolveExternalGeometry } from './external-geo.js';\nexport type { ExternalGeoResult, ExternalLink } from './external-geo.js';\nexport { isWhitelisted } from './feature-translate.js';\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type Result } from '@faicad/faijs/api/result';
|
|
2
|
+
/** One parsed XML node from Document.xml: a property element (or nested value element) with its children. */
|
|
3
|
+
export interface FcstdProperty {
|
|
4
|
+
/** property name attribute ('' for non-property elements like <Geometry>) */
|
|
5
|
+
name: string;
|
|
6
|
+
/** e.g. "App::PropertyString", "Part::PropertyPartShape" */
|
|
7
|
+
type: string;
|
|
8
|
+
/** serialized element tag, e.g. "Geometry", "Constrain", "LineSegment" */
|
|
9
|
+
tagName: string;
|
|
10
|
+
/** raw nested XML content (first element child), serialized */
|
|
11
|
+
valueXml?: string;
|
|
12
|
+
/** for simple value properties, the text content */
|
|
13
|
+
valueText?: string;
|
|
14
|
+
/** named sub-elements (e.g. <Python .../>, <ExpressionEngine> children) */
|
|
15
|
+
children: FcstdProperty[];
|
|
16
|
+
attributes: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
/** One object from <ObjectData>: name, resolved type and its property map. */
|
|
19
|
+
export interface FcstdObject {
|
|
20
|
+
/** object name, e.g. "Sketch" or "Pad" */
|
|
21
|
+
name: string;
|
|
22
|
+
/** e.g. "Sketcher::SketchObject", "PartDesign::Pad" */
|
|
23
|
+
type: string;
|
|
24
|
+
properties: Map<string, FcstdProperty>;
|
|
25
|
+
}
|
|
26
|
+
/** Parsed Document.xml: objects, the <Objects> type index and document-level meta. */
|
|
27
|
+
export interface FcstdDocument {
|
|
28
|
+
objects: FcstdObject[];
|
|
29
|
+
/** from <Objects>: name → type (index only, not authoritative) */
|
|
30
|
+
typeIndex: Map<string, string>;
|
|
31
|
+
/** document-level properties (Creator, LastModifiedDate, ...) */
|
|
32
|
+
meta: Map<string, FcstdProperty>;
|
|
33
|
+
}
|
|
34
|
+
/** Parse failure detail; currently only XML-level errors (`kind: 'xml'`). */
|
|
35
|
+
export type ParseError = {
|
|
36
|
+
kind: 'xml';
|
|
37
|
+
message: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Parse Document.xml into the object graph. Only `<ObjectData>` entries are
|
|
41
|
+
* returned as objects; `<Objects>` feeds `typeIndex`; document meta from
|
|
42
|
+
* `<Document ...>` attributes' sibling properties.
|
|
43
|
+
*
|
|
44
|
+
* @param xml raw Document.xml content
|
|
45
|
+
* @returns the parsed object graph, or an xml ParseError
|
|
46
|
+
*/
|
|
47
|
+
export declare function parseDocumentXml(xml: string): Result<FcstdDocument, ParseError>;
|
|
48
|
+
//# sourceMappingURL=document.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AAQA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAEhE,6GAA6G;AAC7G,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IAC1B,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACxC;AAED,sFAAsF;AACtF,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,kEAAkE;IAClE,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,iEAAiE;IACjE,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CAClC;AAED,6EAA6E;AAC7E,MAAM,MAAM,UAAU,GAAG;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AA6C1D;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,aAAa,EAAE,UAAU,CAAC,CAuE/E"}
|
package/dist/document.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M1.2 — Document.xml → object graph.
|
|
3
|
+
*
|
|
4
|
+
* Contract per plan §5.5 / 前文 A §3.3: `<ObjectData>` is authoritative
|
|
5
|
+
* (objects + properties); `<Objects>` is only a type index. Transient
|
|
6
|
+
* `_Property` elements are skipped.
|
|
7
|
+
*/
|
|
8
|
+
import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
|
|
9
|
+
import { err, ok } from '@faicad/faijs/api/result';
|
|
10
|
+
function attrs(el) {
|
|
11
|
+
const out = {};
|
|
12
|
+
for (let i = 0; i < el.attributes.length; i++) {
|
|
13
|
+
const a = el.attributes.item(i);
|
|
14
|
+
if (a)
|
|
15
|
+
out[a.name] = a.value;
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
function firstElementChild(el) {
|
|
20
|
+
for (let c = el.firstChild; c; c = c.nextSibling) {
|
|
21
|
+
if (c.nodeType === 1)
|
|
22
|
+
return c;
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
function serialize(el) {
|
|
27
|
+
return new XMLSerializer().serializeToString(el);
|
|
28
|
+
}
|
|
29
|
+
function parseProperty(propEl) {
|
|
30
|
+
const children = [];
|
|
31
|
+
for (let c = propEl.firstChild; c; c = c.nextSibling) {
|
|
32
|
+
if (c.nodeType === 1) {
|
|
33
|
+
const child = c;
|
|
34
|
+
children.push(parseProperty(child));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const valueEl = firstElementChild(propEl);
|
|
38
|
+
return {
|
|
39
|
+
name: propEl.getAttribute('name') ?? '',
|
|
40
|
+
type: propEl.getAttribute('type') ?? '',
|
|
41
|
+
tagName: propEl.tagName,
|
|
42
|
+
attributes: attrs(propEl),
|
|
43
|
+
valueXml: valueEl ? serialize(valueEl) : undefined,
|
|
44
|
+
valueText: valueEl?.textContent ?? propEl.textContent ?? '',
|
|
45
|
+
children,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parse Document.xml into the object graph. Only `<ObjectData>` entries are
|
|
50
|
+
* returned as objects; `<Objects>` feeds `typeIndex`; document meta from
|
|
51
|
+
* `<Document ...>` attributes' sibling properties.
|
|
52
|
+
*
|
|
53
|
+
* @param xml raw Document.xml content
|
|
54
|
+
* @returns the parsed object graph, or an xml ParseError
|
|
55
|
+
*/
|
|
56
|
+
export function parseDocumentXml(xml) {
|
|
57
|
+
let doc;
|
|
58
|
+
try {
|
|
59
|
+
// xmldom's Document type conflicts with lib: DOM; cast across the boundary.
|
|
60
|
+
doc = new DOMParser().parseFromString(xml, 'application/xml');
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
return err({ kind: 'xml', message: `Document.xml parse failed: ${e instanceof Error ? e.message : String(e)}` });
|
|
64
|
+
}
|
|
65
|
+
const root = doc.documentElement;
|
|
66
|
+
if (!root || root.tagName !== 'Document') {
|
|
67
|
+
return err({ kind: 'xml', message: `root element is <${root?.tagName ?? 'none'}>, expected <Document>` });
|
|
68
|
+
}
|
|
69
|
+
const objects = [];
|
|
70
|
+
const typeIndex = new Map();
|
|
71
|
+
const meta = new Map();
|
|
72
|
+
for (let n = root.firstChild; n; n = n.nextSibling) {
|
|
73
|
+
if (n.nodeType !== 1)
|
|
74
|
+
continue;
|
|
75
|
+
const el = n;
|
|
76
|
+
switch (el.tagName) {
|
|
77
|
+
case 'Objects': {
|
|
78
|
+
for (let c = el.firstChild; c; c = c.nextSibling) {
|
|
79
|
+
if (c.nodeType !== 1)
|
|
80
|
+
continue;
|
|
81
|
+
const oe = c;
|
|
82
|
+
if (oe.tagName !== 'Object')
|
|
83
|
+
continue;
|
|
84
|
+
const name = oe.getAttribute('name');
|
|
85
|
+
const type = oe.getAttribute('type');
|
|
86
|
+
if (name && type)
|
|
87
|
+
typeIndex.set(name, type);
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case 'ObjectData': {
|
|
92
|
+
for (let c = el.firstChild; c; c = c.nextSibling) {
|
|
93
|
+
if (c.nodeType !== 1)
|
|
94
|
+
continue;
|
|
95
|
+
const oe = c;
|
|
96
|
+
if (oe.tagName !== 'Object')
|
|
97
|
+
continue;
|
|
98
|
+
const name = oe.getAttribute('name') ?? '';
|
|
99
|
+
// ObjectData <Object> carries only `name`; the type lives in the
|
|
100
|
+
// <Objects> index (plan §3.3: Objects is the type index).
|
|
101
|
+
const type = oe.getAttribute('type') ?? typeIndex.get(name) ?? '';
|
|
102
|
+
const properties = new Map();
|
|
103
|
+
// properties live under a <Properties Count="N"> wrapper element
|
|
104
|
+
for (let p = oe.firstChild; p; p = p.nextSibling) {
|
|
105
|
+
if (p.nodeType !== 1)
|
|
106
|
+
continue;
|
|
107
|
+
const pe = p;
|
|
108
|
+
if (pe.tagName === 'Properties') {
|
|
109
|
+
for (let q = pe.firstChild; q; q = q.nextSibling) {
|
|
110
|
+
if (q.nodeType !== 1)
|
|
111
|
+
continue;
|
|
112
|
+
const qe = q;
|
|
113
|
+
if (qe.tagName === '_Property')
|
|
114
|
+
continue; // transient
|
|
115
|
+
const prop = parseProperty(qe);
|
|
116
|
+
if (prop.name)
|
|
117
|
+
properties.set(prop.name, prop);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (pe.tagName !== '_Property') {
|
|
121
|
+
const prop = parseProperty(pe);
|
|
122
|
+
if (prop.name)
|
|
123
|
+
properties.set(prop.name, prop);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
objects.push({ name, type, properties });
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
default: {
|
|
131
|
+
// document meta properties (Creator, Uuid, LastModifiedDate, ...)
|
|
132
|
+
const prop = parseProperty(el);
|
|
133
|
+
if (prop.name)
|
|
134
|
+
meta.set(prop.name, prop);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return ok({ objects, typeIndex, meta });
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=document.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document.js","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * M1.2 — Document.xml → object graph.\n *\n * Contract per plan §5.5 / 前文 A §3.3: `<ObjectData>` is authoritative\n * (objects + properties); `<Objects>` is only a type index. Transient\n * `_Property` elements are skipped.\n */\nimport { DOMParser, XMLSerializer } from '@xmldom/xmldom';\nimport { err, ok, type Result } from '@faicad/faijs/api/result';\n\n/** One parsed XML node from Document.xml: a property element (or nested value element) with its children. */\nexport interface FcstdProperty {\n /** property name attribute ('' for non-property elements like <Geometry>) */\n name: string;\n /** e.g. \"App::PropertyString\", \"Part::PropertyPartShape\" */\n type: string;\n /** serialized element tag, e.g. \"Geometry\", \"Constrain\", \"LineSegment\" */\n tagName: string;\n /** raw nested XML content (first element child), serialized */\n valueXml?: string;\n /** for simple value properties, the text content */\n valueText?: string;\n /** named sub-elements (e.g. <Python .../>, <ExpressionEngine> children) */\n children: FcstdProperty[];\n attributes: Record<string, string>;\n}\n\n/** One object from <ObjectData>: name, resolved type and its property map. */\nexport interface FcstdObject {\n /** object name, e.g. \"Sketch\" or \"Pad\" */\n name: string;\n /** e.g. \"Sketcher::SketchObject\", \"PartDesign::Pad\" */\n type: string;\n properties: Map<string, FcstdProperty>;\n}\n\n/** Parsed Document.xml: objects, the <Objects> type index and document-level meta. */\nexport interface FcstdDocument {\n objects: FcstdObject[];\n /** from <Objects>: name → type (index only, not authoritative) */\n typeIndex: Map<string, string>;\n /** document-level properties (Creator, LastModifiedDate, ...) */\n meta: Map<string, FcstdProperty>;\n}\n\n/** Parse failure detail; currently only XML-level errors (`kind: 'xml'`). */\nexport type ParseError = { kind: 'xml'; message: string };\n\nfunction attrs(el: Element): Record<string, string> {\n const out: Record<string, string> = {};\n for (let i = 0; i < el.attributes.length; i++) {\n const a = el.attributes.item(i);\n if (a) out[a.name] = a.value;\n }\n return out;\n}\n\nfunction firstElementChild(el: Element): Element | undefined {\n for (let c = el.firstChild; c; c = c.nextSibling) {\n if (c.nodeType === 1) return c as Element;\n }\n return undefined;\n}\n\nfunction serialize(el: Element): string {\n // @xmldom/xmldom ships its own Node/Element types that conflict with lib: DOM;\n // the parser/serializer boundary is the only place the two universes meet.\n type SerNode = Parameters<XMLSerializer['serializeToString']>[0];\n return new XMLSerializer().serializeToString(el as unknown as SerNode);\n}\n\nfunction parseProperty(propEl: Element): FcstdProperty {\n const children: FcstdProperty[] = [];\n for (let c = propEl.firstChild; c; c = c.nextSibling) {\n if (c.nodeType === 1) {\n const child = c as Element;\n children.push(parseProperty(child));\n }\n }\n const valueEl = firstElementChild(propEl);\n return {\n name: propEl.getAttribute('name') ?? '',\n type: propEl.getAttribute('type') ?? '',\n tagName: propEl.tagName,\n attributes: attrs(propEl),\n valueXml: valueEl ? serialize(valueEl) : undefined,\n valueText: valueEl?.textContent ?? propEl.textContent ?? '',\n children,\n };\n}\n\n/**\n * Parse Document.xml into the object graph. Only `<ObjectData>` entries are\n * returned as objects; `<Objects>` feeds `typeIndex`; document meta from\n * `<Document ...>` attributes' sibling properties.\n *\n * @param xml raw Document.xml content\n * @returns the parsed object graph, or an xml ParseError\n */\nexport function parseDocumentXml(xml: string): Result<FcstdDocument, ParseError> {\n let doc: Document;\n try {\n // xmldom's Document type conflicts with lib: DOM; cast across the boundary.\n doc = new DOMParser().parseFromString(xml, 'application/xml') as unknown as Document;\n } catch (e) {\n return err({ kind: 'xml', message: `Document.xml parse failed: ${e instanceof Error ? e.message : String(e)}` });\n }\n const root = doc.documentElement;\n if (!root || root.tagName !== 'Document') {\n return err({ kind: 'xml', message: `root element is <${root?.tagName ?? 'none'}>, expected <Document>` });\n }\n\n const objects: FcstdObject[] = [];\n const typeIndex = new Map<string, string>();\n const meta = new Map<string, FcstdProperty>();\n\n for (let n = root.firstChild; n; n = n.nextSibling) {\n if (n.nodeType !== 1) continue;\n const el = n as Element;\n switch (el.tagName) {\n case 'Objects': {\n for (let c = el.firstChild; c; c = c.nextSibling) {\n if (c.nodeType !== 1) continue;\n const oe = c as Element;\n if (oe.tagName !== 'Object') continue;\n const name = oe.getAttribute('name');\n const type = oe.getAttribute('type');\n if (name && type) typeIndex.set(name, type);\n }\n break;\n }\n case 'ObjectData': {\n for (let c = el.firstChild; c; c = c.nextSibling) {\n if (c.nodeType !== 1) continue;\n const oe = c as Element;\n if (oe.tagName !== 'Object') continue;\n const name = oe.getAttribute('name') ?? '';\n // ObjectData <Object> carries only `name`; the type lives in the\n // <Objects> index (plan §3.3: Objects is the type index).\n const type = oe.getAttribute('type') ?? typeIndex.get(name) ?? '';\n const properties = new Map<string, FcstdProperty>();\n // properties live under a <Properties Count=\"N\"> wrapper element\n for (let p = oe.firstChild; p; p = p.nextSibling) {\n if (p.nodeType !== 1) continue;\n const pe = p as Element;\n if (pe.tagName === 'Properties') {\n for (let q = pe.firstChild; q; q = q.nextSibling) {\n if (q.nodeType !== 1) continue;\n const qe = q as Element;\n if (qe.tagName === '_Property') continue; // transient\n const prop = parseProperty(qe);\n if (prop.name) properties.set(prop.name, prop);\n }\n } else if (pe.tagName !== '_Property') {\n const prop = parseProperty(pe);\n if (prop.name) properties.set(prop.name, prop);\n }\n }\n objects.push({ name, type, properties });\n }\n break;\n }\n default: {\n // document meta properties (Creator, Uuid, LastModifiedDate, ...)\n const prop = parseProperty(el);\n if (prop.name) meta.set(prop.name, prop);\n }\n }\n }\n return ok({ objects, typeIndex, meta });\n}\n"]}
|