@faicad/cq-compat 0.13.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 (41) hide show
  1. package/dist/assembly-compare.d.ts +135 -0
  2. package/dist/assembly-compare.d.ts.map +1 -0
  3. package/dist/assembly-compare.js +266 -0
  4. package/dist/assembly-compare.js.map +1 -0
  5. package/dist/assembly.d.ts +102 -0
  6. package/dist/assembly.d.ts.map +1 -0
  7. package/dist/assembly.js +403 -0
  8. package/dist/assembly.js.map +1 -0
  9. package/dist/browser.d.ts +13 -0
  10. package/dist/browser.d.ts.map +1 -0
  11. package/dist/browser.js +13 -0
  12. package/dist/browser.js.map +1 -0
  13. package/dist/gear-test-harness.d.ts +79 -0
  14. package/dist/gear-test-harness.d.ts.map +1 -0
  15. package/dist/gear-test-harness.js +112 -0
  16. package/dist/gear-test-harness.js.map +1 -0
  17. package/dist/gears.d.ts +226 -0
  18. package/dist/gears.d.ts.map +1 -0
  19. package/dist/gears.js +274 -0
  20. package/dist/gears.js.map +1 -0
  21. package/dist/geom-types.d.ts +7 -0
  22. package/dist/geom-types.d.ts.map +1 -0
  23. package/dist/geom-types.js +2 -0
  24. package/dist/geom-types.js.map +1 -0
  25. package/dist/index.d.ts +26 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +23 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/step-compare.d.ts +83 -0
  30. package/dist/step-compare.d.ts.map +1 -0
  31. package/dist/step-compare.js +140 -0
  32. package/dist/step-compare.js.map +1 -0
  33. package/dist/transpile.d.ts +42 -0
  34. package/dist/transpile.d.ts.map +1 -0
  35. package/dist/transpile.js +557 -0
  36. package/dist/transpile.js.map +1 -0
  37. package/dist/workplane.d.ts +1216 -0
  38. package/dist/workplane.d.ts.map +1 -0
  39. package/dist/workplane.js +4026 -0
  40. package/dist/workplane.js.map +1 -0
  41. package/package.json +48 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Assembly STEP equivalence comparison.
3
+ *
4
+ * Four-level comparison:
5
+ * 1. Assembly structure (leaf count, part names, hierarchy)
6
+ * 2. Part pose (center of mass / bbox center in assembly coords)
7
+ * 3. Part geometry (volume, topology — per-part, name-matched)
8
+ * 4. Overall geometry (fuse all parts → boolean difference)
9
+ *
10
+ * Part names must match exactly (port verification scenario).
11
+ */
12
+ import type { BrepBoundingBox, BrepVec3 } from '@faicad/faijs';
13
+ /** Tolerance options. */
14
+ export interface AssemblyCompareOptions {
15
+ linearTolerance?: number;
16
+ volumeRelativeTolerance?: number;
17
+ booleanVolumeTolerance?: number;
18
+ strictTopology?: boolean;
19
+ /**
20
+ * Require part names to match between the two files (default true).
21
+ * Set to false for files whose PRODUCT names are not under our control
22
+ * (e.g. CadQuery/OCC references named "SOLID" vs our "shape_x") — parts are
23
+ * then paired by index in sorted order. Structure still requires the same
24
+ * leaf count, so the compound-vs-parts check remains intact.
25
+ */
26
+ matchNames?: boolean;
27
+ /**
28
+ * Part-pairing strategy for the per-part comparison (P0b):
29
+ * - `'names'` (default): exact PRODUCT-name match — faijs-vs-faijs.
30
+ * - `'order-index'`: pair leaves by index after sorting both lists by name
31
+ * (legacy fallback for foreign PRODUCT names; safe only when leaf count
32
+ * matches and parts line up in the same sorted order).
33
+ * - `'order-centroid'`: pair each A-leaf to the nearest B-leaf by assembly
34
+ * centroid (greedy nearest-first); robust to arbitrary naming/ordering,
35
+ * used for CadQuery-reference vs faijs-candidate cross-naming compare.
36
+ * When `pairing` is omitted it is derived from `matchNames`
37
+ * (`true` → `'names'`, `false` → `'order-index'`).
38
+ */
39
+ pairing?: 'names' | 'order-index' | 'order-centroid';
40
+ /**
41
+ * Skip the fused (A∪B → cut) boolean-difference computation entirely
42
+ * (default false). The fused cut is expensive on near-coincident B-spline
43
+ * faces and the occt-wasm kernel can return inverted/garbage solids for it
44
+ * (documented in fai_cq_gears analysis docs); per-part volume/CoM/bbox
45
+ * checks remain the verdict. When true, `booleanDiff` is reported as
46
+ * {aMinusB: NaN, bMinusA: NaN, match: true}.
47
+ */
48
+ skipFusedBoolean?: boolean;
49
+ }
50
+ /** Per-part comparison result. */
51
+ export interface PartCompareResult {
52
+ name: string;
53
+ found: boolean;
54
+ volume?: {
55
+ a: number;
56
+ b: number;
57
+ match: boolean;
58
+ diffPct: number;
59
+ };
60
+ centerOfMass?: {
61
+ a: BrepVec3;
62
+ b: BrepVec3;
63
+ match: boolean;
64
+ maxDiff: number;
65
+ };
66
+ bbox?: {
67
+ a: BrepBoundingBox;
68
+ b: BrepBoundingBox;
69
+ match: boolean;
70
+ maxDiff: number;
71
+ };
72
+ topology?: {
73
+ a: {
74
+ faces: number;
75
+ edges: number;
76
+ vertices: number;
77
+ };
78
+ b: {
79
+ faces: number;
80
+ edges: number;
81
+ vertices: number;
82
+ };
83
+ match: boolean;
84
+ };
85
+ color?: {
86
+ a: [number, number, number] | null;
87
+ b: [number, number, number] | null;
88
+ match: boolean;
89
+ };
90
+ }
91
+ /** Full assembly comparison result. */
92
+ export interface AssemblyCompareResult {
93
+ fileA: string;
94
+ fileB: string;
95
+ equivalent: boolean;
96
+ structure: {
97
+ leafCountA: number;
98
+ leafCountB: number;
99
+ namesA: string[];
100
+ namesB: string[];
101
+ match: boolean;
102
+ missingInB: string[];
103
+ missingInA: string[];
104
+ };
105
+ parts: PartCompareResult[];
106
+ overall: {
107
+ volumeA: number;
108
+ volumeB: number;
109
+ volumeMatch: boolean;
110
+ volumeDiffPct: number;
111
+ bboxMatch: boolean;
112
+ bboxMaxDiff: number;
113
+ booleanDiff: {
114
+ aMinusB: number;
115
+ bMinusA: number;
116
+ match: boolean;
117
+ };
118
+ };
119
+ details: string[];
120
+ }
121
+ /**
122
+ * Compare two assembly STEP files for equivalence.
123
+ *
124
+ * @param fileA - Path to reference STEP.
125
+ * @param fileB - Path to candidate STEP.
126
+ * @param options - Comparison tolerances.
127
+ * @returns Detailed per-level comparison result.
128
+ */
129
+ export declare function compareAssemblyFiles(fileA: string, fileB: string, options?: AssemblyCompareOptions): Promise<AssemblyCompareResult>;
130
+ /**
131
+ * Print a human-readable assembly comparison report.
132
+ * @param result - Comparison result from compareAssemblyFiles.
133
+ */
134
+ export declare function printAssemblyReport(result: AssemblyCompareResult): void;
135
+ //# sourceMappingURL=assembly-compare.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assembly-compare.d.ts","sourceRoot":"","sources":["../src/assembly-compare.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Assembly STEP equivalence comparison.
3
+ *
4
+ * Four-level comparison:
5
+ * 1. Assembly structure (leaf count, part names, hierarchy)
6
+ * 2. Part pose (center of mass / bbox center in assembly coords)
7
+ * 3. Part geometry (volume, topology — per-part, name-matched)
8
+ * 4. Overall geometry (fuse all parts → boolean difference)
9
+ *
10
+ * Part names must match exactly (port verification scenario).
11
+ */
12
+ import { readFileSync } from 'node:fs';
13
+ import { initOcctWasm, importAssemblyFromStep, collectLeafParts, } from '@faicad/faijs';
14
+ // pairing 由 matchNames 推导(见 compareAssemblyFiles 内部),不进 DEFAULT_OPTS,
15
+ // 否则会覆盖「matchNames:false 但缺 pairing」的推定语义。
16
+ const DEFAULT_OPTS = {
17
+ linearTolerance: 1e-3,
18
+ volumeRelativeTolerance: 1e-3,
19
+ booleanVolumeTolerance: 1e-1,
20
+ strictTopology: false,
21
+ matchNames: true,
22
+ skipFusedBoolean: false,
23
+ };
24
+ function vmax(a, b) {
25
+ return Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y), Math.abs(a.z - b.z));
26
+ }
27
+ function bbmax(a, b) {
28
+ return Math.max(Math.abs(a.xmin - b.xmin), Math.abs(a.xmax - b.xmax), Math.abs(a.ymin - b.ymin), Math.abs(a.ymax - b.ymax), Math.abs(a.zmin - b.zmin), Math.abs(a.zmax - b.zmax));
29
+ }
30
+ function topoStats(kernel, shape) {
31
+ return {
32
+ faces: kernel.getSubShapes(shape, 'face').length,
33
+ edges: kernel.getSubShapes(shape, 'edge').length,
34
+ vertices: kernel.getSubShapes(shape, 'vertex').length,
35
+ };
36
+ }
37
+ function fuseAll(kernel, shapes) {
38
+ if (shapes.length === 0)
39
+ return null;
40
+ let acc = shapes[0];
41
+ for (let i = 1; i < shapes.length; i++) {
42
+ const fused = kernel.fuse(acc, shapes[i]);
43
+ if (i > 1)
44
+ kernel.release(acc);
45
+ acc = fused;
46
+ }
47
+ return acc;
48
+ }
49
+ /**
50
+ * Compare two assembly STEP files for equivalence.
51
+ *
52
+ * @param fileA - Path to reference STEP.
53
+ * @param fileB - Path to candidate STEP.
54
+ * @param options - Comparison tolerances.
55
+ * @returns Detailed per-level comparison result.
56
+ */
57
+ export async function compareAssemblyFiles(fileA, fileB, options = {}) {
58
+ const opts = { ...DEFAULT_OPTS, ...options };
59
+ const details = [];
60
+ const kernel = await initOcctWasm();
61
+ const bufA = readFileSync(fileA);
62
+ const bufB = readFileSync(fileB);
63
+ // readFileSync returns pooled Buffers for small files: buf.buffer is the
64
+ // whole pool with garbage beyond byteLength, which corrupts STEP imports
65
+ // of files < ~4KB. Pass exact copies.
66
+ const exact = (buf) => buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
67
+ const nodesA = await importAssemblyFromStep(exact(bufA));
68
+ const nodesB = await importAssemblyFromStep(exact(bufB));
69
+ const leavesA = collectLeafParts(nodesA).filter(n => n.shapeHandle !== null);
70
+ const leavesB = collectLeafParts(nodesB).filter(n => n.shapeHandle !== null);
71
+ // ── Level 1: Structure ──
72
+ const namesA = leavesA.map(n => n.name).sort();
73
+ const namesB = leavesB.map(n => n.name).sort();
74
+ const missingInB = namesA.filter(n => !namesB.includes(n));
75
+ const missingInA = namesB.filter(n => !namesA.includes(n));
76
+ // P0b:pairing 模式(缺省由 matchNames 推导)
77
+ const pairing = opts.pairing ?? (opts.matchNames ? 'names' : 'order-index');
78
+ const namesMatch = pairing === 'names' && missingInB.length === 0 && missingInA.length === 0;
79
+ const structureMatch = leavesA.length === leavesB.length && (pairing !== 'names' || namesMatch);
80
+ details.push(`structure: ${leavesA.length} vs ${leavesB.length} leaves, names match=${structureMatch} (pairing=${pairing})`);
81
+ if (missingInB.length)
82
+ details.push(` missing in B: ${missingInB.join(', ')}`);
83
+ if (missingInA.length)
84
+ details.push(` missing in A: ${missingInA.join(', ')}`);
85
+ // ── Level 2 & 3: Per-part pose + geometry ──
86
+ const partResults = [];
87
+ // 配对:按 pairing 模式生成 (A-leaf, B-leaf) 对。
88
+ const byName = (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
89
+ const centroidOf = (leaf) => kernel.getCenterOfMass(leaf.shapeHandle);
90
+ const pairs = [];
91
+ if (pairing === 'names') {
92
+ const mapB = new Map(leavesB.map(n => [n.name, n]));
93
+ for (const a of leavesA)
94
+ pairs.push([a, mapB.get(a.name) ?? null]);
95
+ }
96
+ else if (pairing === 'order-index') {
97
+ const sortedA = [...leavesA].sort(byName);
98
+ const sortedB = [...leavesB].sort(byName);
99
+ for (let i = 0; i < sortedA.length; i++)
100
+ pairs.push([sortedA[i], sortedB[i] ?? null]);
101
+ }
102
+ else {
103
+ // order-centroid:贪心最近质心配对(leaf 数相等由 structureMatch 保证)
104
+ const centB = leavesB.map(centroidOf);
105
+ const usedB = new Set();
106
+ for (const a of leavesA) {
107
+ const ca = centroidOf(a);
108
+ let best = -1;
109
+ let bestD = Infinity;
110
+ for (let j = 0; j < leavesB.length; j++) {
111
+ if (usedB.has(j))
112
+ continue;
113
+ const d = vmax(ca, centB[j]);
114
+ if (d < bestD) {
115
+ bestD = d;
116
+ best = j;
117
+ }
118
+ }
119
+ if (best >= 0) {
120
+ usedB.add(best);
121
+ pairs.push([a, leavesB[best]]);
122
+ }
123
+ else
124
+ pairs.push([a, null]);
125
+ }
126
+ }
127
+ for (const [leafA, leafB] of pairs) {
128
+ if (!leafB) {
129
+ partResults.push({ name: leafA.name, found: false });
130
+ continue;
131
+ }
132
+ const shapeA = leafA.shapeHandle;
133
+ const shapeB = leafB.shapeHandle;
134
+ const volA = kernel.getVolume(shapeA);
135
+ const volB = kernel.getVolume(shapeB);
136
+ const volDiffPct = volA > 0 ? (Math.abs(volA - volB) / volA) * 100 : 0;
137
+ const volMatch = volDiffPct <= opts.volumeRelativeTolerance * 100;
138
+ const comA = kernel.getCenterOfMass(shapeA);
139
+ const comB = kernel.getCenterOfMass(shapeB);
140
+ const comDiff = vmax(comA, comB);
141
+ const comMatch = comDiff <= opts.linearTolerance;
142
+ const bbA = kernel.getBoundingBox(shapeA);
143
+ const bbB = kernel.getBoundingBox(shapeB);
144
+ const bbDiff = bbmax(bbA, bbB);
145
+ const bbMatch = bbDiff <= opts.linearTolerance;
146
+ const topA = topoStats(kernel, shapeA);
147
+ const topB = topoStats(kernel, shapeB);
148
+ const topoMatch = opts.strictTopology
149
+ ? topA.faces === topB.faces && topA.edges === topB.edges && topA.vertices === topB.vertices
150
+ : true;
151
+ const colA = leafA.color;
152
+ const colB = leafB.color;
153
+ const colMatch = Boolean((!colA && !colB) || (colA && colB &&
154
+ Math.abs(colA[0] - colB[0]) < 0.01 &&
155
+ Math.abs(colA[1] - colB[1]) < 0.01 &&
156
+ Math.abs(colA[2] - colB[2]) < 0.01));
157
+ partResults.push({
158
+ name: leafA.name,
159
+ found: true,
160
+ volume: { a: volA, b: volB, match: volMatch, diffPct: volDiffPct },
161
+ centerOfMass: { a: comA, b: comB, match: comMatch, maxDiff: comDiff },
162
+ bbox: { a: bbA, b: bbB, match: bbMatch, maxDiff: bbDiff },
163
+ topology: { a: topA, b: topB, match: topoMatch },
164
+ color: { a: colA, b: colB, match: colMatch },
165
+ });
166
+ details.push(`part "${leafA.name}": vol ${volA.toFixed(1)} vs ${volB.toFixed(1)} (${volDiffPct.toFixed(3)}%), com diff=${comDiff.toExponential(2)}, bbox diff=${bbDiff.toExponential(2)}, color=${colMatch}`);
167
+ }
168
+ // ── Level 4: Overall fused geometry ──
169
+ const shapesA = leavesA.map(n => n.shapeHandle);
170
+ const shapesB = leavesB.map(n => n.shapeHandle);
171
+ const fusedA = fuseAll(kernel, shapesA);
172
+ const fusedB = fuseAll(kernel, shapesB);
173
+ let overallVolumeA = 0, overallVolumeB = 0, overallVolMatch = false, overallVolDiffPct = 0;
174
+ let overallBboxMatch = false, overallBboxDiff = 0;
175
+ let boolAB = 0, boolBA = 0, boolMatch = false;
176
+ if (fusedA && fusedB) {
177
+ overallVolumeA = kernel.getVolume(fusedA);
178
+ overallVolumeB = kernel.getVolume(fusedB);
179
+ overallVolDiffPct = overallVolumeA > 0 ? (Math.abs(overallVolumeA - overallVolumeB) / overallVolumeA) * 100 : 0;
180
+ overallVolMatch = overallVolDiffPct <= opts.volumeRelativeTolerance * 100;
181
+ const fbbA = kernel.getBoundingBox(fusedA);
182
+ const fbbB = kernel.getBoundingBox(fusedB);
183
+ overallBboxDiff = bbmax(fbbA, fbbB);
184
+ overallBboxMatch = overallBboxDiff <= opts.linearTolerance;
185
+ if (opts.skipFusedBoolean) {
186
+ boolAB = NaN;
187
+ boolBA = NaN;
188
+ boolMatch = true;
189
+ }
190
+ else {
191
+ const cutAB = kernel.cut(fusedA, fusedB);
192
+ const cutBA = kernel.cut(fusedB, fusedA);
193
+ boolAB = kernel.getVolume(cutAB);
194
+ boolBA = kernel.getVolume(cutBA);
195
+ boolMatch = boolAB <= opts.booleanVolumeTolerance && boolBA <= opts.booleanVolumeTolerance;
196
+ kernel.release(cutAB);
197
+ kernel.release(cutBA);
198
+ }
199
+ }
200
+ details.push(`overall: vol ${overallVolumeA.toFixed(1)} vs ${overallVolumeB.toFixed(1)} (${overallVolDiffPct.toFixed(3)}%), bbox diff=${overallBboxDiff.toExponential(2)}, bool A-B=${boolAB.toExponential(2)}, B-A=${boolBA.toExponential(2)}`);
201
+ // Cleanup
202
+ if (fusedA)
203
+ kernel.release(fusedA);
204
+ if (fusedB)
205
+ kernel.release(fusedB);
206
+ // Note: leaf shapeHandles are owned by the assembly tree, not released here
207
+ const partsAllMatch = partResults.every(p => p.found && p.volume?.match && p.centerOfMass?.match && p.bbox?.match && p.topology?.match && p.color?.match);
208
+ const equivalent = structureMatch && partsAllMatch && overallVolMatch && overallBboxMatch && boolMatch;
209
+ return {
210
+ fileA,
211
+ fileB,
212
+ equivalent,
213
+ structure: {
214
+ leafCountA: leavesA.length,
215
+ leafCountB: leavesB.length,
216
+ namesA,
217
+ namesB,
218
+ match: structureMatch,
219
+ missingInB,
220
+ missingInA,
221
+ },
222
+ parts: partResults,
223
+ overall: {
224
+ volumeA: overallVolumeA,
225
+ volumeB: overallVolumeB,
226
+ volumeMatch: overallVolMatch,
227
+ volumeDiffPct: overallVolDiffPct,
228
+ bboxMatch: overallBboxMatch,
229
+ bboxMaxDiff: overallBboxDiff,
230
+ booleanDiff: { aMinusB: boolAB, bMinusA: boolBA, match: boolMatch },
231
+ },
232
+ details,
233
+ };
234
+ }
235
+ /**
236
+ * Print a human-readable assembly comparison report.
237
+ * @param result - Comparison result from compareAssemblyFiles.
238
+ */
239
+ export function printAssemblyReport(result) {
240
+ console.log(`\n=== Assembly Comparison: ${result.fileA} vs ${result.fileB} ===`);
241
+ console.log(`Overall: ${result.equivalent ? '✓ EQUIVALENT' : '✗ DIFFERENT'}\n`);
242
+ console.log('── Level 1: Structure ──');
243
+ console.log(` Leaves: ${result.structure.leafCountA} vs ${result.structure.leafCountB} — ${result.structure.match ? '✓' : '✗'}`);
244
+ console.log(` Names A: [${result.structure.namesA.join(', ')}]`);
245
+ console.log(` Names B: [${result.structure.namesB.join(', ')}]`);
246
+ if (result.structure.missingInB.length)
247
+ console.log(` ✗ Missing in B: ${result.structure.missingInB.join(', ')}`);
248
+ if (result.structure.missingInA.length)
249
+ console.log(` ✗ Missing in A: ${result.structure.missingInA.join(', ')}`);
250
+ console.log('\n── Level 2 & 3: Per-part pose + geometry ──');
251
+ for (const p of result.parts) {
252
+ if (!p.found) {
253
+ console.log(` ✗ "${p.name}": not found in B`);
254
+ continue;
255
+ }
256
+ const ok = p.volume?.match && p.centerOfMass?.match && p.bbox?.match && p.color?.match;
257
+ console.log(` ${ok ? '✓' : '✗'} "${p.name}": vol ${p.volume?.a.toFixed(1)} vs ${p.volume?.b.toFixed(1)} (${p.volume?.diffPct.toFixed(3)}%), com diff=${p.centerOfMass?.maxDiff.toExponential(2)}, bbox diff=${p.bbox?.maxDiff.toExponential(2)}, color=${p.color?.match ? '✓' : '✗'}`);
258
+ }
259
+ console.log('\n── Level 4: Overall fused geometry ──');
260
+ console.log(` Volume: ${result.overall.volumeA.toFixed(1)} vs ${result.overall.volumeB.toFixed(1)} (${result.overall.volumeDiffPct.toFixed(3)}%) — ${result.overall.volumeMatch ? '✓' : '✗'}`);
261
+ console.log(` BBox diff: ${result.overall.bboxMaxDiff.toExponential(2)} — ${result.overall.bboxMatch ? '✓' : '✗'}`);
262
+ console.log(` Boolean A-B: ${result.overall.booleanDiff.aMinusB.toExponential(2)} mm³`);
263
+ console.log(` Boolean B-A: ${result.overall.booleanDiff.bMinusA.toExponential(2)} mm³ — ${result.overall.booleanDiff.match ? '✓' : '✗'}`);
264
+ console.log('');
265
+ }
266
+ //# sourceMappingURL=assembly-compare.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assembly-compare.js","sourceRoot":"","sources":["../src/assembly-compare.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Assembly STEP equivalence comparison.\n *\n * Four-level comparison:\n * 1. Assembly structure (leaf count, part names, hierarchy)\n * 2. Part pose (center of mass / bbox center in assembly coords)\n * 3. Part geometry (volume, topology — per-part, name-matched)\n * 4. Overall geometry (fuse all parts → boolean difference)\n *\n * Part names must match exactly (port verification scenario).\n */\n\nimport { readFileSync } from 'node:fs'\nimport {\n initOcctWasm,\n importAssemblyFromStep,\n collectLeafParts,\n} from '@faicad/faijs'\nimport type { BrepEngineApi, BrepHandle, BrepBoundingBox, BrepVec3 } from '@faicad/faijs'\nimport type { AssemblyPartNode } from '@faicad/faijs'\n\n/** Tolerance options. */\nexport interface AssemblyCompareOptions {\n linearTolerance?: number\n volumeRelativeTolerance?: number\n booleanVolumeTolerance?: number\n strictTopology?: boolean\n /**\n * Require part names to match between the two files (default true).\n * Set to false for files whose PRODUCT names are not under our control\n * (e.g. CadQuery/OCC references named \"SOLID\" vs our \"shape_x\") — parts are\n * then paired by index in sorted order. Structure still requires the same\n * leaf count, so the compound-vs-parts check remains intact.\n */\n matchNames?: boolean\n /**\n * Part-pairing strategy for the per-part comparison (P0b):\n * - `'names'` (default): exact PRODUCT-name match — faijs-vs-faijs.\n * - `'order-index'`: pair leaves by index after sorting both lists by name\n * (legacy fallback for foreign PRODUCT names; safe only when leaf count\n * matches and parts line up in the same sorted order).\n * - `'order-centroid'`: pair each A-leaf to the nearest B-leaf by assembly\n * centroid (greedy nearest-first); robust to arbitrary naming/ordering,\n * used for CadQuery-reference vs faijs-candidate cross-naming compare.\n * When `pairing` is omitted it is derived from `matchNames`\n * (`true` → `'names'`, `false` → `'order-index'`).\n */\n pairing?: 'names' | 'order-index' | 'order-centroid'\n /**\n * Skip the fused (A∪B → cut) boolean-difference computation entirely\n * (default false). The fused cut is expensive on near-coincident B-spline\n * faces and the occt-wasm kernel can return inverted/garbage solids for it\n * (documented in fai_cq_gears analysis docs); per-part volume/CoM/bbox\n * checks remain the verdict. When true, `booleanDiff` is reported as\n * {aMinusB: NaN, bMinusA: NaN, match: true}.\n */\n skipFusedBoolean?: boolean\n}\n\n/** Per-part comparison result. */\nexport interface PartCompareResult {\n name: string\n found: boolean\n volume?: { a: number; b: number; match: boolean; diffPct: number }\n centerOfMass?: { a: BrepVec3; b: BrepVec3; match: boolean; maxDiff: number }\n bbox?: { a: BrepBoundingBox; b: BrepBoundingBox; match: boolean; maxDiff: number }\n topology?: {\n a: { faces: number; edges: number; vertices: number }\n b: { faces: number; edges: number; vertices: number }\n match: boolean\n }\n color?: { a: [number, number, number] | null; b: [number, number, number] | null; match: boolean }\n}\n\n/** Full assembly comparison result. */\nexport interface AssemblyCompareResult {\n fileA: string\n fileB: string\n equivalent: boolean\n structure: {\n leafCountA: number\n leafCountB: number\n namesA: string[]\n namesB: string[]\n match: boolean\n missingInB: string[]\n missingInA: string[]\n }\n parts: PartCompareResult[]\n overall: {\n volumeA: number\n volumeB: number\n volumeMatch: boolean\n volumeDiffPct: number\n bboxMatch: boolean\n bboxMaxDiff: number\n booleanDiff: { aMinusB: number; bMinusA: number; match: boolean }\n }\n details: string[]\n}\n\n// pairing 由 matchNames 推导(见 compareAssemblyFiles 内部),不进 DEFAULT_OPTS,\n// 否则会覆盖「matchNames:false 但缺 pairing」的推定语义。\nconst DEFAULT_OPTS = {\n linearTolerance: 1e-3,\n volumeRelativeTolerance: 1e-3,\n booleanVolumeTolerance: 1e-1,\n strictTopology: false,\n matchNames: true,\n skipFusedBoolean: false,\n}\n\nfunction vmax(a: BrepVec3, b: BrepVec3): number {\n return Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y), Math.abs(a.z - b.z))\n}\n\nfunction bbmax(a: BrepBoundingBox, b: BrepBoundingBox): number {\n return Math.max(\n Math.abs(a.xmin - b.xmin), Math.abs(a.xmax - b.xmax),\n Math.abs(a.ymin - b.ymin), Math.abs(a.ymax - b.ymax),\n Math.abs(a.zmin - b.zmin), Math.abs(a.zmax - b.zmax),\n )\n}\n\nfunction topoStats(kernel: BrepEngineApi, shape: BrepHandle) {\n return {\n faces: kernel.getSubShapes(shape, 'face').length,\n edges: kernel.getSubShapes(shape, 'edge').length,\n vertices: kernel.getSubShapes(shape, 'vertex').length,\n }\n}\n\nfunction fuseAll(kernel: BrepEngineApi, shapes: BrepHandle[]): BrepHandle | null {\n if (shapes.length === 0) return null\n let acc = shapes[0]\n for (let i = 1; i < shapes.length; i++) {\n const fused = kernel.fuse(acc, shapes[i])\n if (i > 1) kernel.release(acc)\n acc = fused\n }\n return acc\n}\n\n/**\n * Compare two assembly STEP files for equivalence.\n *\n * @param fileA - Path to reference STEP.\n * @param fileB - Path to candidate STEP.\n * @param options - Comparison tolerances.\n * @returns Detailed per-level comparison result.\n */\nexport async function compareAssemblyFiles(\n fileA: string,\n fileB: string,\n options: AssemblyCompareOptions = {},\n): Promise<AssemblyCompareResult> {\n const opts = { ...DEFAULT_OPTS, ...options }\n const details: string[] = []\n\n const kernel = await initOcctWasm()\n\n const bufA = readFileSync(fileA)\n const bufB = readFileSync(fileB)\n // readFileSync returns pooled Buffers for small files: buf.buffer is the\n // whole pool with garbage beyond byteLength, which corrupts STEP imports\n // of files < ~4KB. Pass exact copies.\n const exact = (buf: Buffer): ArrayBuffer =>\n buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer\n const nodesA = await importAssemblyFromStep(exact(bufA))\n const nodesB = await importAssemblyFromStep(exact(bufB))\n const leavesA = collectLeafParts(nodesA).filter(n => n.shapeHandle !== null)\n const leavesB = collectLeafParts(nodesB).filter(n => n.shapeHandle !== null)\n\n // ── Level 1: Structure ──\n const namesA = leavesA.map(n => n.name).sort()\n const namesB = leavesB.map(n => n.name).sort()\n const missingInB = namesA.filter(n => !namesB.includes(n))\n const missingInA = namesB.filter(n => !namesA.includes(n))\n // P0b:pairing 模式(缺省由 matchNames 推导)\n const pairing: 'names' | 'order-index' | 'order-centroid' =\n opts.pairing ?? (opts.matchNames ? 'names' : 'order-index')\n const namesMatch = pairing === 'names' && missingInB.length === 0 && missingInA.length === 0\n const structureMatch = leavesA.length === leavesB.length && (pairing !== 'names' || namesMatch)\n details.push(`structure: ${leavesA.length} vs ${leavesB.length} leaves, names match=${structureMatch} (pairing=${pairing})`)\n if (missingInB.length) details.push(` missing in B: ${missingInB.join(', ')}`)\n if (missingInA.length) details.push(` missing in A: ${missingInA.join(', ')}`)\n\n // ── Level 2 & 3: Per-part pose + geometry ──\n const partResults: PartCompareResult[] = []\n\n // 配对:按 pairing 模式生成 (A-leaf, B-leaf) 对。\n const byName = (a: AssemblyPartNode, b: AssemblyPartNode): number =>\n a.name < b.name ? -1 : a.name > b.name ? 1 : 0\n const centroidOf = (leaf: AssemblyPartNode): BrepVec3 =>\n kernel.getCenterOfMass(leaf.shapeHandle as unknown as BrepHandle)\n\n const pairs: Array<[AssemblyPartNode, AssemblyPartNode | null]> = []\n if (pairing === 'names') {\n const mapB = new Map(leavesB.map(n => [n.name, n]))\n for (const a of leavesA) pairs.push([a, mapB.get(a.name) ?? null])\n } else if (pairing === 'order-index') {\n const sortedA = [...leavesA].sort(byName)\n const sortedB = [...leavesB].sort(byName)\n for (let i = 0; i < sortedA.length; i++) pairs.push([sortedA[i], sortedB[i] ?? null])\n } else {\n // order-centroid:贪心最近质心配对(leaf 数相等由 structureMatch 保证)\n const centB = leavesB.map(centroidOf)\n const usedB = new Set<number>()\n for (const a of leavesA) {\n const ca = centroidOf(a)\n let best = -1\n let bestD = Infinity\n for (let j = 0; j < leavesB.length; j++) {\n if (usedB.has(j)) continue\n const d = vmax(ca, centB[j])\n if (d < bestD) { bestD = d; best = j }\n }\n if (best >= 0) { usedB.add(best); pairs.push([a, leavesB[best]]) }\n else pairs.push([a, null])\n }\n }\n\n for (const [leafA, leafB] of pairs) {\n if (!leafB) {\n partResults.push({ name: leafA.name, found: false })\n continue\n }\n const shapeA = leafA.shapeHandle! as unknown as BrepHandle\n const shapeB = leafB.shapeHandle! as unknown as BrepHandle\n\n const volA = kernel.getVolume(shapeA)\n const volB = kernel.getVolume(shapeB)\n const volDiffPct = volA > 0 ? (Math.abs(volA - volB) / volA) * 100 : 0\n const volMatch = volDiffPct <= opts.volumeRelativeTolerance * 100\n\n const comA = kernel.getCenterOfMass(shapeA)\n const comB = kernel.getCenterOfMass(shapeB)\n const comDiff = vmax(comA, comB)\n const comMatch = comDiff <= opts.linearTolerance\n\n const bbA = kernel.getBoundingBox(shapeA)\n const bbB = kernel.getBoundingBox(shapeB)\n const bbDiff = bbmax(bbA, bbB)\n const bbMatch = bbDiff <= opts.linearTolerance\n\n const topA = topoStats(kernel, shapeA)\n const topB = topoStats(kernel, shapeB)\n const topoMatch = opts.strictTopology\n ? topA.faces === topB.faces && topA.edges === topB.edges && topA.vertices === topB.vertices\n : true\n\n const colA = leafA.color\n const colB = leafB.color\n const colMatch = Boolean((!colA && !colB) || (colA && colB &&\n Math.abs(colA[0] - colB[0]) < 0.01 &&\n Math.abs(colA[1] - colB[1]) < 0.01 &&\n Math.abs(colA[2] - colB[2]) < 0.01))\n\n partResults.push({\n name: leafA.name,\n found: true,\n volume: { a: volA, b: volB, match: volMatch, diffPct: volDiffPct },\n centerOfMass: { a: comA, b: comB, match: comMatch, maxDiff: comDiff },\n bbox: { a: bbA, b: bbB, match: bbMatch, maxDiff: bbDiff },\n topology: { a: topA, b: topB, match: topoMatch },\n color: { a: colA, b: colB, match: colMatch },\n })\n\n details.push(`part \"${leafA.name}\": vol ${volA.toFixed(1)} vs ${volB.toFixed(1)} (${volDiffPct.toFixed(3)}%), com diff=${comDiff.toExponential(2)}, bbox diff=${bbDiff.toExponential(2)}, color=${colMatch}`)\n }\n\n // ── Level 4: Overall fused geometry ──\n const shapesA = leavesA.map(n => n.shapeHandle! as unknown as BrepHandle)\n const shapesB = leavesB.map(n => n.shapeHandle! as unknown as BrepHandle)\n const fusedA = fuseAll(kernel, shapesA)\n const fusedB = fuseAll(kernel, shapesB)\n\n let overallVolumeA = 0, overallVolumeB = 0, overallVolMatch = false, overallVolDiffPct = 0\n let overallBboxMatch = false, overallBboxDiff = 0\n let boolAB = 0, boolBA = 0, boolMatch = false\n\n if (fusedA && fusedB) {\n overallVolumeA = kernel.getVolume(fusedA)\n overallVolumeB = kernel.getVolume(fusedB)\n overallVolDiffPct = overallVolumeA > 0 ? (Math.abs(overallVolumeA - overallVolumeB) / overallVolumeA) * 100 : 0\n overallVolMatch = overallVolDiffPct <= opts.volumeRelativeTolerance * 100\n\n const fbbA = kernel.getBoundingBox(fusedA)\n const fbbB = kernel.getBoundingBox(fusedB)\n overallBboxDiff = bbmax(fbbA, fbbB)\n overallBboxMatch = overallBboxDiff <= opts.linearTolerance\n\n if (opts.skipFusedBoolean) {\n boolAB = NaN\n boolBA = NaN\n boolMatch = true\n } else {\n const cutAB = kernel.cut(fusedA, fusedB)\n const cutBA = kernel.cut(fusedB, fusedA)\n boolAB = kernel.getVolume(cutAB)\n boolBA = kernel.getVolume(cutBA)\n boolMatch = boolAB <= opts.booleanVolumeTolerance && boolBA <= opts.booleanVolumeTolerance\n kernel.release(cutAB)\n kernel.release(cutBA)\n }\n }\n\n details.push(`overall: vol ${overallVolumeA.toFixed(1)} vs ${overallVolumeB.toFixed(1)} (${overallVolDiffPct.toFixed(3)}%), bbox diff=${overallBboxDiff.toExponential(2)}, bool A-B=${boolAB.toExponential(2)}, B-A=${boolBA.toExponential(2)}`)\n\n // Cleanup\n if (fusedA) kernel.release(fusedA)\n if (fusedB) kernel.release(fusedB)\n // Note: leaf shapeHandles are owned by the assembly tree, not released here\n\n const partsAllMatch = partResults.every(p =>\n p.found && p.volume?.match && p.centerOfMass?.match && p.bbox?.match && p.topology?.match && p.color?.match\n )\n const equivalent = structureMatch && partsAllMatch && overallVolMatch && overallBboxMatch && boolMatch\n\n return {\n fileA,\n fileB,\n equivalent,\n structure: {\n leafCountA: leavesA.length,\n leafCountB: leavesB.length,\n namesA,\n namesB,\n match: structureMatch,\n missingInB,\n missingInA,\n },\n parts: partResults,\n overall: {\n volumeA: overallVolumeA,\n volumeB: overallVolumeB,\n volumeMatch: overallVolMatch,\n volumeDiffPct: overallVolDiffPct,\n bboxMatch: overallBboxMatch,\n bboxMaxDiff: overallBboxDiff,\n booleanDiff: { aMinusB: boolAB, bMinusA: boolBA, match: boolMatch },\n },\n details,\n }\n}\n\n/**\n * Print a human-readable assembly comparison report.\n * @param result - Comparison result from compareAssemblyFiles.\n */\nexport function printAssemblyReport(result: AssemblyCompareResult): void {\n console.log(`\\n=== Assembly Comparison: ${result.fileA} vs ${result.fileB} ===`)\n console.log(`Overall: ${result.equivalent ? '✓ EQUIVALENT' : '✗ DIFFERENT'}\\n`)\n\n console.log('── Level 1: Structure ──')\n console.log(` Leaves: ${result.structure.leafCountA} vs ${result.structure.leafCountB} — ${result.structure.match ? '✓' : '✗'}`)\n console.log(` Names A: [${result.structure.namesA.join(', ')}]`)\n console.log(` Names B: [${result.structure.namesB.join(', ')}]`)\n if (result.structure.missingInB.length) console.log(` ✗ Missing in B: ${result.structure.missingInB.join(', ')}`)\n if (result.structure.missingInA.length) console.log(` ✗ Missing in A: ${result.structure.missingInA.join(', ')}`)\n\n console.log('\\n── Level 2 & 3: Per-part pose + geometry ──')\n for (const p of result.parts) {\n if (!p.found) {\n console.log(` ✗ \"${p.name}\": not found in B`)\n continue\n }\n const ok = p.volume?.match && p.centerOfMass?.match && p.bbox?.match && p.color?.match\n console.log(` ${ok ? '✓' : '✗'} \"${p.name}\": vol ${p.volume?.a.toFixed(1)} vs ${p.volume?.b.toFixed(1)} (${p.volume?.diffPct.toFixed(3)}%), com diff=${p.centerOfMass?.maxDiff.toExponential(2)}, bbox diff=${p.bbox?.maxDiff.toExponential(2)}, color=${p.color?.match ? '✓' : '✗'}`)\n }\n\n console.log('\\n── Level 4: Overall fused geometry ──')\n console.log(` Volume: ${result.overall.volumeA.toFixed(1)} vs ${result.overall.volumeB.toFixed(1)} (${result.overall.volumeDiffPct.toFixed(3)}%) — ${result.overall.volumeMatch ? '✓' : '✗'}`)\n console.log(` BBox diff: ${result.overall.bboxMaxDiff.toExponential(2)} — ${result.overall.bboxMatch ? '✓' : '✗'}`)\n console.log(` Boolean A-B: ${result.overall.booleanDiff.aMinusB.toExponential(2)} mm³`)\n console.log(` Boolean B-A: ${result.overall.booleanDiff.bMinusA.toExponential(2)} mm³ — ${result.overall.booleanDiff.match ? '✓' : '✗'}`)\n console.log('')\n}\n"]}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * cq-compat assembly helpers — CadQuery Assembly.constrain → faijs cad.assembly.
3
+ *
4
+ * Maps CadQuery constraint DSL ("part@faces@>Z[-2]", "Plane"/"Axis") to faijs
5
+ * AssemblyConstraint objects with EntityRef geometry snapshots.
6
+ *
7
+ * Constraint mapping (verified against faijs api/assembly/lower.ts):
8
+ * - "Plane" → mate (face-to-face: normal reversed + center coincident)
9
+ * - "Axis" → align (normal same direction + center coincident; plane face refs
10
+ * are encoded as axis via axisFromFace — concentric would reject
11
+ * plane faces because faceGeometryToSolverEntity maps plane→plane
12
+ * entity, not axis)
13
+ */
14
+ import type { Shape } from '@faicad/faijs/mesh/types';
15
+ import type { AssemblyConstraint, EntityRef } from '@faicad/faijs/api/assembly/types';
16
+ import type { CompoundShape } from '@faicad/faijs/shape';
17
+ import type { RGB } from './workplane.js';
18
+ /**
19
+ * faceRef
20
+ * @param partName - string
21
+ * @param selector - string
22
+ * @param shape - Shape
23
+ * @returns Promise<EntityRef>
24
+ */
25
+ export declare function faceRef(partName: string, selector: string, shape: Shape): Promise<EntityRef>;
26
+ /**
27
+ * constraint
28
+ * @param aPart - string
29
+ * @param aSelector - string
30
+ * @param aShape - Shape
31
+ * @param bPart - string
32
+ * @param bSelector - string
33
+ * @param bShape - Shape
34
+ * @param type - 'Plane' | 'Axis'
35
+ * @returns Promise<AssemblyConstraint>
36
+ */
37
+ export declare function constraint(aPart: string, aSelector: string, aShape: Shape, bPart: string, bSelector: string, bShape: Shape, type: 'Plane' | 'Axis'): Promise<AssemblyConstraint>;
38
+ /**
39
+ * pointRef — 字面坐标点引用(无需几何解析)。
40
+ * @param part - 部件名
41
+ * @param coords - [x,y,z](本地系 mm)
42
+ * @returns EntityRef(point)
43
+ */
44
+ export declare function pointRef(part: string, coords: [number, number, number]): EntityRef;
45
+ /**
46
+ * axisRef — 字面轴引用(无需几何解析)。
47
+ * @param part - 部件名
48
+ * @param origin - 轴上一点 [x,y,z](本地系 mm)
49
+ * @param direction - 单位方向 [x,y,z]
50
+ * @returns EntityRef(edge.axis)
51
+ */
52
+ export declare function axisRef(part: string, origin: [number, number, number], direction: [number, number, number]): EntityRef;
53
+ type AssemblyKind = 'Plane' | 'Axis' | 'Point' | 'Cylinder' | 'Distance' | 'Fixed' | 'Revolute';
54
+ /**
55
+ * constraintEx — 统一约束构造,覆盖 CQ 公共种类并映射到 faijs 类型面。
56
+ *
57
+ * 映射(对齐 global 求解器 consuming 的 AssemblyConstraint):
58
+ * - 'Plane' → [mate](faceRef→plane)
59
+ * - 'Axis' → [angle:180](纯方向反平行、无点项——CQ 2.8.0 `axis_cost` 缺省语义)
60
+ * - 'Point' → [coincident](selector 为字面坐标 "x,y,z")
61
+ * - 'Cylinder' → [concentric, coincident(point_on_line)](圆边解析轴)
62
+ * - 'Distance' → [distance(value)](point-point 或 plane-plane)
63
+ * - 'Fixed' → [fixed](aPart 锁定)
64
+ * - 'Revolute' → [fixed](暂降级为锚定占位;旋转 DOF 后续走 joints 机制)
65
+ *
66
+ * @param aPart A 侧成员名。
67
+ * @param aSelector A 侧面选择器字符串(如 ">Z[-2]")。
68
+ * @param aShape A 侧成员几何(真实 Shape 或借用视图)。
69
+ * @param bPart B 侧成员名。
70
+ * @param bSelector B 侧面选择器字符串。
71
+ * @param bShape B 侧成员几何(真实 Shape 或借用视图)。
72
+ * @param kind 约束种类(Plane/Axis/Point/Cylinder/Distance/Fixed/Revolute)。
73
+ * @param param 可选参数(Distance 的距离值等)。
74
+ * @returns AssemblyConstraint[](Cylinder 拆两条,其余单条)
75
+ */
76
+ export declare function constraintEx(aPart: string, aSelector: string, aShape: Shape | null, bPart: string, bSelector: string, bShape: Shape | null, kind: AssemblyKind, param?: number): Promise<AssemblyConstraint[]>;
77
+ /**
78
+ * buildAssembly
79
+ * @param name - string
80
+ * @param members - Array<{ name: string; shape: Shape; color?: RGB }>
81
+ * @param constraints - AssemblyConstraint[]
82
+ * @param opts - optional: { solver?: 'chain' | 'global' };cq-compat = CadQuery 兼容,默认 'global'
83
+ * @returns CompoundShape
84
+ */
85
+ export declare function buildAssembly(name: string, members: Array<{
86
+ name: string;
87
+ shape: Shape;
88
+ color?: RGB;
89
+ }>, constraints: AssemblyConstraint[], opts?: {
90
+ solver?: 'chain' | 'global';
91
+ }): CompoundShape;
92
+ /**
93
+ * Color
94
+ * @param r - number
95
+ * @param g - number
96
+ * @param b - number
97
+ * @param _a - number
98
+ * @returns RGB
99
+ */
100
+ export declare function Color(r: number, g: number, b: number, _a?: number): RGB;
101
+ export {};
102
+ //# sourceMappingURL=assembly.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assembly.d.ts","sourceRoot":"","sources":["../src/assembly.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,KAAK,EACV,kBAAkB,EAClB,SAAS,EACV,MAAM,kCAAkC,CAAA;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAOxD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AAKtC;;;;;;GAMG;AACH,wBAAsB,OAAO,CAC3B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,KAAK,GACX,OAAO,CAAC,SAAS,CAAC,CAepB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,UAAU,CAC9B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,KAAK,EACb,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,KAAK,EACb,IAAI,EAAE,OAAO,GAAG,MAAM,GACrB,OAAO,CAAC,kBAAkB,CAAC,CAG7B;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAElF;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CACrB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAChC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAClC,SAAS,CAEX;AAED,KAAK,YAAY,GACb,OAAO,GACP,MAAM,GACN,OAAO,GACP,UAAU,GACV,UAAU,GACV,OAAO,GACP,UAAU,CAAA;AAoId;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,KAAK,GAAG,IAAI,EACpB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,KAAK,GAAG,IAAI,EACpB,IAAI,EAAE,YAAY,EAClB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAuD/B;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC;IAAC,KAAK,CAAC,EAAE,GAAG,CAAA;CAAE,CAAC,EAC3D,WAAW,EAAE,kBAAkB,EAAE,EACjC,IAAI,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAA;CAAE,GACrC,aAAa,CA0Ef;AAED;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,GAAG,CAEvE"}