@faicad/cq-compat-compare 0.32.3 → 0.33.2
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/dist/step-compare.d.ts +36 -0
- package/dist/step-compare.d.ts.map +1 -1
- package/dist/step-compare.js +118 -1
- package/dist/step-compare.js.map +1 -1
- package/package.json +2 -2
package/dist/step-compare.d.ts
CHANGED
|
@@ -23,6 +23,32 @@ export interface CompareOptions {
|
|
|
23
23
|
booleanVolumeTolerance?: number;
|
|
24
24
|
/** Whether to require topology counts to match exactly. Default true. */
|
|
25
25
|
strictTopology?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* P7/D3: pose comparison mode. Default 'bitwise' (exact placement match).
|
|
28
|
+
* 'canonical-pose' = the two files are two LEGAL solutions of an
|
|
29
|
+
* underdetermined constraint set (solver DOF diagnostic said dof > 0): the
|
|
30
|
+
* placement difference along the free twist is not a defect. B is pre-aligned
|
|
31
|
+
* to A by scanning the twist about `canonicalFreeAxis` (through B's COM) to
|
|
32
|
+
* the minimal-difference angle before the rotation-sensitive metrics (bbox,
|
|
33
|
+
* boolean diff); invariants (topology / volume / COM) stay bitwise — they are
|
|
34
|
+
* unchanged by that rotation anyway.
|
|
35
|
+
*/
|
|
36
|
+
poseMode?: 'bitwise' | 'canonical-pose';
|
|
37
|
+
/**
|
|
38
|
+
* Free twist axis (world direction, any nonzero length — normalized
|
|
39
|
+
* internally). Required when poseMode is 'canonical-pose'; ignored otherwise.
|
|
40
|
+
* Feed the solver's underdetermined free direction here.
|
|
41
|
+
*/
|
|
42
|
+
canonicalFreeAxis?: BrepVec3;
|
|
43
|
+
/**
|
|
44
|
+
* P7/D4: a point ON the free twist axis (world coords). The canonical scan
|
|
45
|
+
* rotates B about the axis through this point. Default: B's center of mass.
|
|
46
|
+
* For assemblies the free twist acts per-member around its own constraint
|
|
47
|
+
* anchor — a compound-COM pivot moves the members when it is not on the
|
|
48
|
+
* twist axis and the scan cannot converge (residual = the twist offset left
|
|
49
|
+
* behind). Pass the anchor point of the constrained member here.
|
|
50
|
+
*/
|
|
51
|
+
canonicalPivot?: BrepVec3;
|
|
26
52
|
}
|
|
27
53
|
/** Per-metric comparison result. */
|
|
28
54
|
export interface MetricResult<T> {
|
|
@@ -65,6 +91,16 @@ export interface StepCompareResult {
|
|
|
65
91
|
match: boolean;
|
|
66
92
|
};
|
|
67
93
|
details: string[];
|
|
94
|
+
/**
|
|
95
|
+
* P7/D3 audit fields (present only when poseMode is 'canonical-pose'):
|
|
96
|
+
* the aligned twist angle (deg) applied to B and the resulting minimal
|
|
97
|
+
* bbox diff. Makes every canonical-channel relaxation visible in reports —
|
|
98
|
+
* no silent loosening.
|
|
99
|
+
*/
|
|
100
|
+
canonicalPose?: {
|
|
101
|
+
twistAngleDeg: number;
|
|
102
|
+
alignedBboxDiff: number;
|
|
103
|
+
};
|
|
68
104
|
}
|
|
69
105
|
/**
|
|
70
106
|
* Compare two STEP files for geometric equivalence.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"step-compare.d.ts","sourceRoot":"","sources":["../src/step-compare.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAA2B,eAAe,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAEvF,wCAAwC;AACxC,MAAM,WAAW,cAAc;IAC7B,mFAAmF;IACnF,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,6EAA6E;IAC7E,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,4EAA4E;IAC5E,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,yEAAyE;IACzE,cAAc,CAAC,EAAE,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"step-compare.d.ts","sourceRoot":"","sources":["../src/step-compare.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAA2B,eAAe,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAEvF,wCAAwC;AACxC,MAAM,WAAW,cAAc;IAC7B,mFAAmF;IACnF,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,6EAA6E;IAC7E,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,4EAA4E;IAC5E,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,yEAAyE;IACzE,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,SAAS,GAAG,gBAAgB,CAAA;IACvC;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,QAAQ,CAAA;IAC5B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,QAAQ,CAAA;CAC1B;AAED,oCAAoC;AACpC,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,CAAC,EAAE,CAAC,CAAA;IACJ,CAAC,EAAE,CAAC,CAAA;IACJ,KAAK,EAAE,OAAO,CAAA;CACf;AAED,uCAAuC;AACvC,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;CACf;AAED,8BAA8B;AAC9B,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,OAAO,CAAA;IACnB,IAAI,EAAE,YAAY,CAAC,eAAe,CAAC,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;IACzD,MAAM,EAAE,YAAY,CAAC,MAAM,CAAC,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;IAChE,YAAY,EAAE,YAAY,CAAC,QAAQ,CAAC,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1D,QAAQ,EAAE,YAAY,CAAC,aAAa,CAAC,CAAA;IACrC,WAAW,EAAE;QACX,OAAO,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,OAAO,CAAA;SAAE,CAAA;QAC9C,OAAO,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,OAAO,CAAA;SAAE,CAAA;QAC9C,KAAK,EAAE,OAAO,CAAA;KACf,CAAA;IACD,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB;;;;;OAKG;IACH,aAAa,CAAC,EAAE;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAA;CACnE;AAqCD;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,iBAAiB,CAAC,CA8M5B;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAclE"}
|
package/dist/step-compare.js
CHANGED
|
@@ -59,7 +59,121 @@ export async function compareStepFiles(fileA, fileB, options = {}) {
|
|
|
59
59
|
// fail on every STEP < ~4KB (e.g. single-sphere files). Pass an exact copy.
|
|
60
60
|
const exact = (buf) => buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
61
61
|
const shapeA = kernel.importStep(exact(bufA));
|
|
62
|
-
|
|
62
|
+
let shapeB = kernel.importStep(exact(bufB));
|
|
63
|
+
// P7/D3: canonical-pose pre-alignment. Two legal solutions of an
|
|
64
|
+
// underdetermined constraint set differ only by the twist about the free
|
|
65
|
+
// axis; scan that twist to the minimal-difference angle before the
|
|
66
|
+
// rotation-sensitive metrics. Invariants (topology/volume/COM) are unchanged
|
|
67
|
+
// by a rotation about the axis through B's COM, so computing them before or
|
|
68
|
+
// after this alignment is equivalent — do it before, once.
|
|
69
|
+
let canonicalTwistAngle;
|
|
70
|
+
if (opts.poseMode === 'canonical-pose') {
|
|
71
|
+
if (!opts.canonicalFreeAxis) {
|
|
72
|
+
throw new Error("poseMode 'canonical-pose' requires canonicalFreeAxis (the solver's underdetermined free direction)");
|
|
73
|
+
}
|
|
74
|
+
const ux = opts.canonicalFreeAxis.x;
|
|
75
|
+
const uy = opts.canonicalFreeAxis.y;
|
|
76
|
+
const uz = opts.canonicalFreeAxis.z;
|
|
77
|
+
const len = Math.hypot(ux, uy, uz);
|
|
78
|
+
if (!(len > 0))
|
|
79
|
+
throw new Error('canonicalFreeAxis must be a nonzero direction');
|
|
80
|
+
const axis = { x: ux / len, y: uy / len, z: uz / len };
|
|
81
|
+
const bboxA0 = kernel.getBoundingBox(shapeA);
|
|
82
|
+
// The free twist acts PER-MEMBER around the member's own constraint anchor
|
|
83
|
+
// — a single rigid twist of the whole compound cannot absorb it (a compound
|
|
84
|
+
// COM or anchor pivot that is off a member's twist axis translates that
|
|
85
|
+
// member while rotating). So split both compounds into solids and align
|
|
86
|
+
// each B solid independently: rotate it about its own COM (twist axis
|
|
87
|
+
// through a member's FixedPoint anchor passes through the member's COM for
|
|
88
|
+
// unary constraints) to the minimal-bbox-diff angle vs the whole A.
|
|
89
|
+
const solidsA = kernel.getSubShapes(shapeA, 'solid');
|
|
90
|
+
const solidsB = kernel.getSubShapes(shapeB, 'solid');
|
|
91
|
+
if (solidsA.length !== solidsB.length || solidsA.length === 0) {
|
|
92
|
+
throw new Error(`canonical-pose: solid count mismatch (A=${solidsA.length}, B=${solidsB.length}) — per-member alignment needs 1:1 solids`);
|
|
93
|
+
}
|
|
94
|
+
const rotAbout = (shape, deg, pivot) => {
|
|
95
|
+
const a = (deg * Math.PI) / 180;
|
|
96
|
+
const c = Math.cos(a);
|
|
97
|
+
const s = Math.sin(a);
|
|
98
|
+
const t = 1 - c;
|
|
99
|
+
const r00 = c + axis.x * axis.x * t;
|
|
100
|
+
const r01 = axis.x * axis.y * t - axis.z * s;
|
|
101
|
+
const r02 = axis.x * axis.z * t + axis.y * s;
|
|
102
|
+
const r10 = axis.y * axis.x * t + axis.z * s;
|
|
103
|
+
const r11 = c + axis.y * axis.y * t;
|
|
104
|
+
const r12 = axis.y * axis.z * t - axis.x * s;
|
|
105
|
+
const r20 = axis.z * axis.x * t - axis.y * s;
|
|
106
|
+
const r21 = axis.z * axis.y * t + axis.x * s;
|
|
107
|
+
const r22 = c + axis.z * axis.z * t;
|
|
108
|
+
const tx = pivot.x - (r00 * pivot.x + r01 * pivot.y + r02 * pivot.z);
|
|
109
|
+
const ty = pivot.y - (r10 * pivot.x + r11 * pivot.y + r12 * pivot.z);
|
|
110
|
+
const tz = pivot.z - (r20 * pivot.x + r21 * pivot.y + r22 * pivot.z);
|
|
111
|
+
return kernel.transform(shape, [r00, r01, r02, tx, r10, r11, r12, ty, r20, r21, r22, tz]);
|
|
112
|
+
};
|
|
113
|
+
// Pair each B solid with its nearest A solid (COM distance) — ordering in
|
|
114
|
+
// a STEP compound is not guaranteed to match the solver's member order.
|
|
115
|
+
const comA = solidsA.map((s) => kernel.getCenterOfMass(s));
|
|
116
|
+
const aligned = new Array(solidsB.length);
|
|
117
|
+
const usedA = new Set();
|
|
118
|
+
let maxAbsTwist = 0;
|
|
119
|
+
for (const sb of solidsB) {
|
|
120
|
+
const com = kernel.getCenterOfMass(sb);
|
|
121
|
+
let ai = -1;
|
|
122
|
+
let best = Infinity;
|
|
123
|
+
for (let i = 0; i < comA.length; i++) {
|
|
124
|
+
if (usedA.has(i))
|
|
125
|
+
continue;
|
|
126
|
+
const d = Math.hypot(comA[i].x - com.x, comA[i].y - com.y, comA[i].z - com.z);
|
|
127
|
+
if (d < best) {
|
|
128
|
+
best = d;
|
|
129
|
+
ai = i;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
usedA.add(ai);
|
|
133
|
+
// Scan the twist about the member's own COM (pivot override applies when
|
|
134
|
+
// the anchor is off-COM); MEMBER-vs-MEMBER bbox — aligning one member
|
|
135
|
+
// against the whole-assembly bbox would chase the wrong optimum.
|
|
136
|
+
// 2° grid + golden-section refine.
|
|
137
|
+
const pv = opts.canonicalPivot ?? com;
|
|
138
|
+
const bboxAi = kernel.getBoundingBox(solidsA[ai]);
|
|
139
|
+
const diffAtDeg = (deg) => {
|
|
140
|
+
const rot = rotAbout(sb, deg, pv);
|
|
141
|
+
const b = kernel.getBoundingBox(rot);
|
|
142
|
+
kernel.release(rot);
|
|
143
|
+
return Math.max(Math.abs(bboxAi.xmin - b.xmin), Math.abs(bboxAi.xmax - b.xmax), Math.abs(bboxAi.ymin - b.ymin), Math.abs(bboxAi.ymax - b.ymax), Math.abs(bboxAi.zmin - b.zmin), Math.abs(bboxAi.zmax - b.zmax));
|
|
144
|
+
};
|
|
145
|
+
let bestDeg = 0;
|
|
146
|
+
let bestDiff = diffAtDeg(0);
|
|
147
|
+
for (let deg = 2; deg < 360; deg += 2) {
|
|
148
|
+
const d = diffAtDeg(deg);
|
|
149
|
+
if (d < bestDiff) {
|
|
150
|
+
bestDiff = d;
|
|
151
|
+
bestDeg = deg;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
let lo = bestDeg - 2;
|
|
155
|
+
let hi = bestDeg + 2;
|
|
156
|
+
for (let i = 0; i < 40; i++) {
|
|
157
|
+
const m1 = lo + (hi - lo) * 0.382;
|
|
158
|
+
const m2 = lo + (hi - lo) * 0.618;
|
|
159
|
+
if (diffAtDeg(m1) < diffAtDeg(m2))
|
|
160
|
+
hi = m2;
|
|
161
|
+
else
|
|
162
|
+
lo = m1;
|
|
163
|
+
}
|
|
164
|
+
const angle = (lo + hi) / 2;
|
|
165
|
+
if (Math.abs(angle) > Math.abs(maxAbsTwist))
|
|
166
|
+
maxAbsTwist = angle;
|
|
167
|
+
aligned[ai] = rotAbout(sb, angle, pv);
|
|
168
|
+
kernel.release(sb);
|
|
169
|
+
}
|
|
170
|
+
for (const s of solidsA)
|
|
171
|
+
kernel.release(s);
|
|
172
|
+
kernel.release(shapeB);
|
|
173
|
+
shapeB = solidsB.length === 1 ? aligned[0] : kernel.makeCompound(aligned);
|
|
174
|
+
canonicalTwistAngle = maxAbsTwist;
|
|
175
|
+
details.push(`canonical-pose: per-member twist about (${axis.x.toFixed(4)},${axis.y.toFixed(4)},${axis.z.toFixed(4)}), max |twist| ${maxAbsTwist.toFixed(3)}°`);
|
|
176
|
+
}
|
|
63
177
|
// 1. Bounding box
|
|
64
178
|
const bboxA = kernel.getBoundingBox(shapeA);
|
|
65
179
|
const bboxB = kernel.getBoundingBox(shapeB);
|
|
@@ -129,6 +243,9 @@ export async function compareStepFiles(fileA, fileB, options = {}) {
|
|
|
129
243
|
match: boolMatch,
|
|
130
244
|
},
|
|
131
245
|
details,
|
|
246
|
+
...(canonicalTwistAngle !== undefined
|
|
247
|
+
? { canonicalPose: { twistAngleDeg: canonicalTwistAngle, alignedBboxDiff: bboxDiff } }
|
|
248
|
+
: {}),
|
|
132
249
|
};
|
|
133
250
|
}
|
|
134
251
|
/**
|
package/dist/step-compare.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"step-compare.js","sourceRoot":"","sources":["../src/step-compare.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * STEP file equivalence comparison using BREP geometry.\n *\n * Compares two STEP files across five dimensions:\n * 1. Bounding box\n * 2. Volume\n * 3. Center of mass\n * 4. Topology statistics (face/edge/vertex/solid counts)\n * 5. Boolean difference (A-B and B-A volumes)\n *\n * Usage:\n * const result = await compareStepFiles('a.step', 'b.step')\n * console.log(result.equivalent)\n */\n\nimport { readFileSync } from 'node:fs'\nimport { initOcctWasm } from '@faicad/faijs'\nimport type { OcctKernel, ShapeHandle, BrepBoundingBox, BrepVec3 } from '@faicad/faijs'\n\n/** Tolerance options for comparison. */\nexport interface CompareOptions {\n /** Absolute tolerance for bbox / center-of-mass coordinates (mm). Default 1e-4. */\n linearTolerance?: number\n /** Relative tolerance for volume comparison (0.001 = 0.1%). Default 1e-4. */\n volumeRelativeTolerance?: number\n /** Absolute tolerance for boolean difference volume (mm³). Default 1e-3. */\n booleanVolumeTolerance?: number\n /** Whether to require topology counts to match exactly. Default true. */\n strictTopology?: boolean\n}\n\n/** Per-metric comparison result. */\nexport interface MetricResult<T> {\n a: T\n b: T\n match: boolean\n}\n\n/** Topology statistics for a shape. */\nexport interface TopologyStats {\n faces: number\n edges: number\n vertices: number\n solids: number\n}\n\n/** Full comparison result. */\nexport interface StepCompareResult {\n fileA: string\n fileB: string\n equivalent: boolean\n bbox: MetricResult<BrepBoundingBox> & { maxDiff: number }\n volume: MetricResult<number> & { diff: number; diffPct: number }\n centerOfMass: MetricResult<BrepVec3> & { maxDiff: number }\n topology: MetricResult<TopologyStats>\n booleanDiff: {\n aMinusB: { volume: number; nonEmpty: boolean }\n bMinusA: { volume: number; nonEmpty: boolean }\n match: boolean\n }\n details: string[]\n}\n\nconst DEFAULT_OPTIONS: Required<CompareOptions> = {\n linearTolerance: 1e-4,\n volumeRelativeTolerance: 1e-4,\n booleanVolumeTolerance: 1e-3,\n strictTopology: true,\n}\n\nfunction bboxMaxDiff(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 comMaxDiff(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 getTopologyStats(kernel: OcctKernel, shape: ShapeHandle): TopologyStats {\n return {\n faces: kernel.getSubShapes(shape, 'face').length,\n edges: kernel.getSubShapes(shape, 'edge').length,\n vertices: kernel.getSubShapes(shape, 'vertex').length,\n solids: kernel.getSubShapes(shape, 'solid').length,\n }\n}\n\nfunction topologyMatch(a: TopologyStats, b: TopologyStats, strict: boolean): boolean {\n if (strict) {\n return a.faces === b.faces && a.edges === b.edges && a.vertices === b.vertices && a.solids === b.solids\n }\n return a.solids === b.solids\n}\n\n/**\n * Compare two STEP files for geometric equivalence.\n *\n * @param fileA - Path to the first STEP file.\n * @param fileB - Path to the second STEP file.\n * @param options - Comparison tolerances.\n * @returns Detailed comparison result with per-metric match status.\n */\nexport async function compareStepFiles(\n fileA: string,\n fileB: string,\n options: CompareOptions = {},\n): Promise<StepCompareResult> {\n const opts = { ...DEFAULT_OPTIONS, ...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 8KB pool (with garbage beyond byteLength), which makes importStep\n // fail on every STEP < ~4KB (e.g. single-sphere files). Pass an exact copy.\n const exact = (buf: Buffer): ArrayBuffer =>\n buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer\n const shapeA = kernel.importStep(exact(bufA))\n const shapeB = kernel.importStep(exact(bufB))\n\n // 1. Bounding box\n const bboxA = kernel.getBoundingBox(shapeA)\n const bboxB = kernel.getBoundingBox(shapeB)\n const bboxDiff = bboxMaxDiff(bboxA, bboxB)\n const bboxMatch = bboxDiff <= opts.linearTolerance\n details.push(`bbox diff: ${bboxDiff.toExponential(3)} mm (tol ${opts.linearTolerance})`)\n\n // 2. Topology (needed to decide whether volume/COM/boolean are meaningful)\n const topoA = getTopologyStats(kernel, shapeA)\n const topoB = getTopologyStats(kernel, shapeB)\n const topoMatch = topologyMatch(topoA, topoB, opts.strictTopology)\n details.push(`topology: A(f=${topoA.faces},e=${topoA.edges},v=${topoA.vertices},s=${topoA.solids}) B(f=${topoB.faces},e=${topoB.edges},v=${topoB.vertices},s=${topoB.solids})`)\n\n // 3. Volume / COM / boolean difference: only meaningful when BOTH shapes\n // contain solids or faces (e.g. `siblings(..., \"Vertex\", ...)` yields a\n // pure edges compound; BRepGProp volume / BRepAlgoAPI_Cut throw on those).\n const hasMass =\n (topoA.solids > 0 || topoA.faces > 0) && (topoB.solids > 0 || topoB.faces > 0)\n let volA = 0\n let volB = 0\n let volDiff = 0\n let volPct = 0\n let volMatch = true\n let comA: BrepVec3 = { x: 0, y: 0, z: 0 }\n let comB: BrepVec3 = { x: 0, y: 0, z: 0 }\n let comDiff = 0\n let comMatch = true\n let volAB = 0\n let volBA = 0\n let boolMatch = true\n if (hasMass) {\n volA = kernel.getVolume(shapeA)\n volB = kernel.getVolume(shapeB)\n volDiff = Math.abs(volA - volB)\n volPct = volA > 0 ? (volDiff / volA) * 100 : 0\n volMatch = volPct <= opts.volumeRelativeTolerance * 100\n details.push(`volume diff: ${volDiff.toExponential(3)} mm³ (${volPct.toFixed(4)}%, tol ${opts.volumeRelativeTolerance * 100}%)`)\n\n comA = kernel.getCenterOfMass(shapeA)\n comB = kernel.getCenterOfMass(shapeB)\n comDiff = comMaxDiff(comA, comB)\n comMatch = comDiff <= opts.linearTolerance\n details.push(`center-of-mass diff: ${comDiff.toExponential(3)} mm (tol ${opts.linearTolerance})`)\n\n const aMinusB = kernel.cut(shapeA, shapeB)\n const bMinusA = kernel.cut(shapeB, shapeA)\n volAB = kernel.getVolume(aMinusB)\n volBA = kernel.getVolume(bMinusA)\n boolMatch = volAB <= opts.booleanVolumeTolerance && volBA <= opts.booleanVolumeTolerance\n details.push(`boolean diff: A-B=${volAB.toExponential(3)} mm³, B-A=${volBA.toExponential(3)} mm³ (tol ${opts.booleanVolumeTolerance})`)\n kernel.release(aMinusB)\n kernel.release(bMinusA)\n } else {\n details.push('volume/COM/boolean: skipped (no solid or face topology in either shape)')\n }\n\n // Cleanup\n kernel.release(shapeA)\n kernel.release(shapeB)\n\n const equivalent = bboxMatch && volMatch && comMatch && topoMatch && boolMatch\n\n return {\n fileA,\n fileB,\n equivalent,\n bbox: { a: bboxA, b: bboxB, match: bboxMatch, maxDiff: bboxDiff },\n volume: { a: volA, b: volB, match: volMatch, diff: volDiff, diffPct: volPct },\n centerOfMass: { a: comA, b: comB, match: comMatch, maxDiff: comDiff },\n topology: { a: topoA, b: topoB, match: topoMatch },\n booleanDiff: {\n aMinusB: { volume: volAB, nonEmpty: volAB > opts.booleanVolumeTolerance },\n bMinusA: { volume: volBA, nonEmpty: volBA > opts.booleanVolumeTolerance },\n match: boolMatch,\n },\n details,\n }\n}\n\n/**\n * Print a human-readable comparison report to stdout.\n * @param result - The comparison result from compareStepFiles.\n */\nexport function printCompareReport(result: StepCompareResult): void {\n console.log(`\\n=== STEP Comparison: ${result.fileA} vs ${result.fileB} ===`)\n console.log(`Overall: ${result.equivalent ? '✓ EQUIVALENT' : '✗ DIFFERENT'}\\n`)\n for (const d of result.details) {\n console.log(` ${d}`)\n }\n console.log('')\n if (!result.equivalent) {\n if (!result.bbox.match) console.log(' ✗ Bounding box mismatch')\n if (!result.volume.match) console.log(' ✗ Volume mismatch')\n if (!result.centerOfMass.match) console.log(' ✗ Center of mass mismatch')\n if (!result.topology.match) console.log(' ✗ Topology mismatch')\n if (!result.booleanDiff.match) console.log(' ✗ Boolean difference non-empty')\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"step-compare.js","sourceRoot":"","sources":["../src/step-compare.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * STEP file equivalence comparison using BREP geometry.\n *\n * Compares two STEP files across five dimensions:\n * 1. Bounding box\n * 2. Volume\n * 3. Center of mass\n * 4. Topology statistics (face/edge/vertex/solid counts)\n * 5. Boolean difference (A-B and B-A volumes)\n *\n * Usage:\n * const result = await compareStepFiles('a.step', 'b.step')\n * console.log(result.equivalent)\n */\n\nimport { readFileSync } from 'node:fs'\nimport { initOcctWasm } from '@faicad/faijs'\nimport type { OcctKernel, ShapeHandle, BrepBoundingBox, BrepVec3 } from '@faicad/faijs'\n\n/** Tolerance options for comparison. */\nexport interface CompareOptions {\n /** Absolute tolerance for bbox / center-of-mass coordinates (mm). Default 1e-4. */\n linearTolerance?: number\n /** Relative tolerance for volume comparison (0.001 = 0.1%). Default 1e-4. */\n volumeRelativeTolerance?: number\n /** Absolute tolerance for boolean difference volume (mm³). Default 1e-3. */\n booleanVolumeTolerance?: number\n /** Whether to require topology counts to match exactly. Default true. */\n strictTopology?: boolean\n /**\n * P7/D3: pose comparison mode. Default 'bitwise' (exact placement match).\n * 'canonical-pose' = the two files are two LEGAL solutions of an\n * underdetermined constraint set (solver DOF diagnostic said dof > 0): the\n * placement difference along the free twist is not a defect. B is pre-aligned\n * to A by scanning the twist about `canonicalFreeAxis` (through B's COM) to\n * the minimal-difference angle before the rotation-sensitive metrics (bbox,\n * boolean diff); invariants (topology / volume / COM) stay bitwise — they are\n * unchanged by that rotation anyway.\n */\n poseMode?: 'bitwise' | 'canonical-pose'\n /**\n * Free twist axis (world direction, any nonzero length — normalized\n * internally). Required when poseMode is 'canonical-pose'; ignored otherwise.\n * Feed the solver's underdetermined free direction here.\n */\n canonicalFreeAxis?: BrepVec3\n /**\n * P7/D4: a point ON the free twist axis (world coords). The canonical scan\n * rotates B about the axis through this point. Default: B's center of mass.\n * For assemblies the free twist acts per-member around its own constraint\n * anchor — a compound-COM pivot moves the members when it is not on the\n * twist axis and the scan cannot converge (residual = the twist offset left\n * behind). Pass the anchor point of the constrained member here.\n */\n canonicalPivot?: BrepVec3\n}\n\n/** Per-metric comparison result. */\nexport interface MetricResult<T> {\n a: T\n b: T\n match: boolean\n}\n\n/** Topology statistics for a shape. */\nexport interface TopologyStats {\n faces: number\n edges: number\n vertices: number\n solids: number\n}\n\n/** Full comparison result. */\nexport interface StepCompareResult {\n fileA: string\n fileB: string\n equivalent: boolean\n bbox: MetricResult<BrepBoundingBox> & { maxDiff: number }\n volume: MetricResult<number> & { diff: number; diffPct: number }\n centerOfMass: MetricResult<BrepVec3> & { maxDiff: number }\n topology: MetricResult<TopologyStats>\n booleanDiff: {\n aMinusB: { volume: number; nonEmpty: boolean }\n bMinusA: { volume: number; nonEmpty: boolean }\n match: boolean\n }\n details: string[]\n /**\n * P7/D3 audit fields (present only when poseMode is 'canonical-pose'):\n * the aligned twist angle (deg) applied to B and the resulting minimal\n * bbox diff. Makes every canonical-channel relaxation visible in reports —\n * no silent loosening.\n */\n canonicalPose?: { twistAngleDeg: number; alignedBboxDiff: number }\n}\n\nconst DEFAULT_OPTIONS: Required<Pick<CompareOptions, 'linearTolerance' | 'volumeRelativeTolerance' | 'booleanVolumeTolerance' | 'strictTopology'>> = {\n linearTolerance: 1e-4,\n volumeRelativeTolerance: 1e-4,\n booleanVolumeTolerance: 1e-3,\n strictTopology: true,\n}\n\nfunction bboxMaxDiff(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 comMaxDiff(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 getTopologyStats(kernel: OcctKernel, shape: ShapeHandle): TopologyStats {\n return {\n faces: kernel.getSubShapes(shape, 'face').length,\n edges: kernel.getSubShapes(shape, 'edge').length,\n vertices: kernel.getSubShapes(shape, 'vertex').length,\n solids: kernel.getSubShapes(shape, 'solid').length,\n }\n}\n\nfunction topologyMatch(a: TopologyStats, b: TopologyStats, strict: boolean): boolean {\n if (strict) {\n return a.faces === b.faces && a.edges === b.edges && a.vertices === b.vertices && a.solids === b.solids\n }\n return a.solids === b.solids\n}\n\n/**\n * Compare two STEP files for geometric equivalence.\n *\n * @param fileA - Path to the first STEP file.\n * @param fileB - Path to the second STEP file.\n * @param options - Comparison tolerances.\n * @returns Detailed comparison result with per-metric match status.\n */\nexport async function compareStepFiles(\n fileA: string,\n fileB: string,\n options: CompareOptions = {},\n): Promise<StepCompareResult> {\n const opts = { ...DEFAULT_OPTIONS, ...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 8KB pool (with garbage beyond byteLength), which makes importStep\n // fail on every STEP < ~4KB (e.g. single-sphere files). Pass an exact copy.\n const exact = (buf: Buffer): ArrayBuffer =>\n buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer\n const shapeA = kernel.importStep(exact(bufA))\n let shapeB = kernel.importStep(exact(bufB))\n\n // P7/D3: canonical-pose pre-alignment. Two legal solutions of an\n // underdetermined constraint set differ only by the twist about the free\n // axis; scan that twist to the minimal-difference angle before the\n // rotation-sensitive metrics. Invariants (topology/volume/COM) are unchanged\n // by a rotation about the axis through B's COM, so computing them before or\n // after this alignment is equivalent — do it before, once.\n let canonicalTwistAngle: number | undefined\n if (opts.poseMode === 'canonical-pose') {\n if (!opts.canonicalFreeAxis) {\n throw new Error(\"poseMode 'canonical-pose' requires canonicalFreeAxis (the solver's underdetermined free direction)\")\n }\n const ux = opts.canonicalFreeAxis.x\n const uy = opts.canonicalFreeAxis.y\n const uz = opts.canonicalFreeAxis.z\n const len = Math.hypot(ux, uy, uz)\n if (!(len > 0)) throw new Error('canonicalFreeAxis must be a nonzero direction')\n const axis: BrepVec3 = { x: ux / len, y: uy / len, z: uz / len }\n const bboxA0 = kernel.getBoundingBox(shapeA)\n\n // The free twist acts PER-MEMBER around the member's own constraint anchor\n // — a single rigid twist of the whole compound cannot absorb it (a compound\n // COM or anchor pivot that is off a member's twist axis translates that\n // member while rotating). So split both compounds into solids and align\n // each B solid independently: rotate it about its own COM (twist axis\n // through a member's FixedPoint anchor passes through the member's COM for\n // unary constraints) to the minimal-bbox-diff angle vs the whole A.\n const solidsA = kernel.getSubShapes(shapeA, 'solid')\n const solidsB = kernel.getSubShapes(shapeB, 'solid')\n if (solidsA.length !== solidsB.length || solidsA.length === 0) {\n throw new Error(`canonical-pose: solid count mismatch (A=${solidsA.length}, B=${solidsB.length}) — per-member alignment needs 1:1 solids`)\n }\n\n const rotAbout = (shape: ShapeHandle, deg: number, pivot: BrepVec3): ShapeHandle => {\n const a = (deg * Math.PI) / 180\n const c = Math.cos(a)\n const s = Math.sin(a)\n const t = 1 - c\n const r00 = c + axis.x * axis.x * t\n const r01 = axis.x * axis.y * t - axis.z * s\n const r02 = axis.x * axis.z * t + axis.y * s\n const r10 = axis.y * axis.x * t + axis.z * s\n const r11 = c + axis.y * axis.y * t\n const r12 = axis.y * axis.z * t - axis.x * s\n const r20 = axis.z * axis.x * t - axis.y * s\n const r21 = axis.z * axis.y * t + axis.x * s\n const r22 = c + axis.z * axis.z * t\n const tx = pivot.x - (r00 * pivot.x + r01 * pivot.y + r02 * pivot.z)\n const ty = pivot.y - (r10 * pivot.x + r11 * pivot.y + r12 * pivot.z)\n const tz = pivot.z - (r20 * pivot.x + r21 * pivot.y + r22 * pivot.z)\n return kernel.transform(shape, [r00, r01, r02, tx, r10, r11, r12, ty, r20, r21, r22, tz])\n }\n\n // Pair each B solid with its nearest A solid (COM distance) — ordering in\n // a STEP compound is not guaranteed to match the solver's member order.\n const comA = solidsA.map((s) => kernel.getCenterOfMass(s))\n const aligned: ShapeHandle[] = new Array(solidsB.length)\n const usedA = new Set<number>()\n let maxAbsTwist = 0\n for (const sb of solidsB) {\n const com = kernel.getCenterOfMass(sb)\n let ai = -1\n let best = Infinity\n for (let i = 0; i < comA.length; i++) {\n if (usedA.has(i)) continue\n const d = Math.hypot(comA[i].x - com.x, comA[i].y - com.y, comA[i].z - com.z)\n if (d < best) { best = d; ai = i }\n }\n usedA.add(ai)\n // Scan the twist about the member's own COM (pivot override applies when\n // the anchor is off-COM); MEMBER-vs-MEMBER bbox — aligning one member\n // against the whole-assembly bbox would chase the wrong optimum.\n // 2° grid + golden-section refine.\n const pv = opts.canonicalPivot ?? com\n const bboxAi = kernel.getBoundingBox(solidsA[ai])\n const diffAtDeg = (deg: number): number => {\n const rot = rotAbout(sb, deg, pv)\n const b = kernel.getBoundingBox(rot)\n kernel.release(rot)\n return Math.max(\n Math.abs(bboxAi.xmin - b.xmin), Math.abs(bboxAi.xmax - b.xmax),\n Math.abs(bboxAi.ymin - b.ymin), Math.abs(bboxAi.ymax - b.ymax),\n Math.abs(bboxAi.zmin - b.zmin), Math.abs(bboxAi.zmax - b.zmax),\n )\n }\n let bestDeg = 0\n let bestDiff = diffAtDeg(0)\n for (let deg = 2; deg < 360; deg += 2) {\n const d = diffAtDeg(deg)\n if (d < bestDiff) { bestDiff = d; bestDeg = deg }\n }\n let lo = bestDeg - 2\n let hi = bestDeg + 2\n for (let i = 0; i < 40; i++) {\n const m1 = lo + (hi - lo) * 0.382\n const m2 = lo + (hi - lo) * 0.618\n if (diffAtDeg(m1) < diffAtDeg(m2)) hi = m2\n else lo = m1\n }\n const angle = (lo + hi) / 2\n if (Math.abs(angle) > Math.abs(maxAbsTwist)) maxAbsTwist = angle\n aligned[ai] = rotAbout(sb, angle, pv)\n kernel.release(sb)\n }\n for (const s of solidsA) kernel.release(s)\n kernel.release(shapeB)\n shapeB = solidsB.length === 1 ? aligned[0] : kernel.makeCompound(aligned)\n canonicalTwistAngle = maxAbsTwist\n details.push(`canonical-pose: per-member twist about (${axis.x.toFixed(4)},${axis.y.toFixed(4)},${axis.z.toFixed(4)}), max |twist| ${maxAbsTwist.toFixed(3)}°`)\n }\n\n // 1. Bounding box\n const bboxA = kernel.getBoundingBox(shapeA)\n const bboxB = kernel.getBoundingBox(shapeB)\n const bboxDiff = bboxMaxDiff(bboxA, bboxB)\n const bboxMatch = bboxDiff <= opts.linearTolerance\n details.push(`bbox diff: ${bboxDiff.toExponential(3)} mm (tol ${opts.linearTolerance})`)\n\n // 2. Topology (needed to decide whether volume/COM/boolean are meaningful)\n const topoA = getTopologyStats(kernel, shapeA)\n const topoB = getTopologyStats(kernel, shapeB)\n const topoMatch = topologyMatch(topoA, topoB, opts.strictTopology)\n details.push(`topology: A(f=${topoA.faces},e=${topoA.edges},v=${topoA.vertices},s=${topoA.solids}) B(f=${topoB.faces},e=${topoB.edges},v=${topoB.vertices},s=${topoB.solids})`)\n\n // 3. Volume / COM / boolean difference: only meaningful when BOTH shapes\n // contain solids or faces (e.g. `siblings(..., \"Vertex\", ...)` yields a\n // pure edges compound; BRepGProp volume / BRepAlgoAPI_Cut throw on those).\n const hasMass =\n (topoA.solids > 0 || topoA.faces > 0) && (topoB.solids > 0 || topoB.faces > 0)\n let volA = 0\n let volB = 0\n let volDiff = 0\n let volPct = 0\n let volMatch = true\n let comA: BrepVec3 = { x: 0, y: 0, z: 0 }\n let comB: BrepVec3 = { x: 0, y: 0, z: 0 }\n let comDiff = 0\n let comMatch = true\n let volAB = 0\n let volBA = 0\n let boolMatch = true\n if (hasMass) {\n volA = kernel.getVolume(shapeA)\n volB = kernel.getVolume(shapeB)\n volDiff = Math.abs(volA - volB)\n volPct = volA > 0 ? (volDiff / volA) * 100 : 0\n volMatch = volPct <= opts.volumeRelativeTolerance * 100\n details.push(`volume diff: ${volDiff.toExponential(3)} mm³ (${volPct.toFixed(4)}%, tol ${opts.volumeRelativeTolerance * 100}%)`)\n\n comA = kernel.getCenterOfMass(shapeA)\n comB = kernel.getCenterOfMass(shapeB)\n comDiff = comMaxDiff(comA, comB)\n comMatch = comDiff <= opts.linearTolerance\n details.push(`center-of-mass diff: ${comDiff.toExponential(3)} mm (tol ${opts.linearTolerance})`)\n\n const aMinusB = kernel.cut(shapeA, shapeB)\n const bMinusA = kernel.cut(shapeB, shapeA)\n volAB = kernel.getVolume(aMinusB)\n volBA = kernel.getVolume(bMinusA)\n boolMatch = volAB <= opts.booleanVolumeTolerance && volBA <= opts.booleanVolumeTolerance\n details.push(`boolean diff: A-B=${volAB.toExponential(3)} mm³, B-A=${volBA.toExponential(3)} mm³ (tol ${opts.booleanVolumeTolerance})`)\n kernel.release(aMinusB)\n kernel.release(bMinusA)\n } else {\n details.push('volume/COM/boolean: skipped (no solid or face topology in either shape)')\n }\n\n // Cleanup\n kernel.release(shapeA)\n kernel.release(shapeB)\n\n const equivalent = bboxMatch && volMatch && comMatch && topoMatch && boolMatch\n\n return {\n fileA,\n fileB,\n equivalent,\n bbox: { a: bboxA, b: bboxB, match: bboxMatch, maxDiff: bboxDiff },\n volume: { a: volA, b: volB, match: volMatch, diff: volDiff, diffPct: volPct },\n centerOfMass: { a: comA, b: comB, match: comMatch, maxDiff: comDiff },\n topology: { a: topoA, b: topoB, match: topoMatch },\n booleanDiff: {\n aMinusB: { volume: volAB, nonEmpty: volAB > opts.booleanVolumeTolerance },\n bMinusA: { volume: volBA, nonEmpty: volBA > opts.booleanVolumeTolerance },\n match: boolMatch,\n },\n details,\n ...(canonicalTwistAngle !== undefined\n ? { canonicalPose: { twistAngleDeg: canonicalTwistAngle, alignedBboxDiff: bboxDiff } }\n : {}),\n }\n}\n\n/**\n * Print a human-readable comparison report to stdout.\n * @param result - The comparison result from compareStepFiles.\n */\nexport function printCompareReport(result: StepCompareResult): void {\n console.log(`\\n=== STEP Comparison: ${result.fileA} vs ${result.fileB} ===`)\n console.log(`Overall: ${result.equivalent ? '✓ EQUIVALENT' : '✗ DIFFERENT'}\\n`)\n for (const d of result.details) {\n console.log(` ${d}`)\n }\n console.log('')\n if (!result.equivalent) {\n if (!result.bbox.match) console.log(' ✗ Bounding box mismatch')\n if (!result.volume.match) console.log(' ✗ Volume mismatch')\n if (!result.centerOfMass.match) console.log(' ✗ Center of mass mismatch')\n if (!result.topology.match) console.log(' ✗ Topology mismatch')\n if (!result.booleanDiff.match) console.log(' ✗ Boolean difference non-empty')\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faicad/cq-compat-compare",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@faicad/faijs": "^0.
|
|
35
|
+
"@faicad/faijs": "^0.33.0",
|
|
36
36
|
"occt-wasm": "^5.6.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|