brepjs 18.119.2 → 18.119.3
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/2d/lib/curve2D.d.ts +6 -1
- package/dist/2d/lib/curve2dFns.d.ts +6 -1
- package/dist/2d.cjs +7 -7
- package/dist/2d.js +7 -7
- package/dist/{adjacencyFns-BdM5p58J.js → adjacencyFns-DLityMyV.js} +2 -2
- package/dist/{adjacencyFns-Brrc7jlS.cjs → adjacencyFns-DSxuQYnb.cjs} +2 -2
- package/dist/{blueprint-DTNT4qYJ.cjs → blueprint-BGcBMy3b.cjs} +39 -9
- package/dist/{blueprint-DJJo8WkI.js → blueprint-CAG6jseD.js} +33 -9
- package/dist/{blueprintFns-CcpboZ80.js → blueprintFns-B4C3zH7N.js} +9 -4
- package/dist/{blueprintFns-2XDZfCxX.cjs → blueprintFns-CnoqynzI.cjs} +9 -4
- package/dist/{blueprintSketcher-C0EXFVb2.cjs → blueprintSketcher-BNz6f36D.cjs} +166 -61
- package/dist/{blueprintSketcher-DDPyypmC.js → blueprintSketcher-CKKxvxB-.js} +159 -54
- package/dist/{boolean2D-Ddfo_dd0.cjs → boolean2D-B-0xobBP.cjs} +5 -5
- package/dist/{boolean2D-C_PVz8xT.js → boolean2D-C_1gMcZ6.js} +5 -5
- package/dist/brepjs.cjs +105 -92
- package/dist/brepjs.js +39 -27
- package/dist/{cameraFns-Czr5vABD.cjs → cameraFns-2-haLzLi.cjs} +4 -4
- package/dist/{cameraFns-D9tdBYLM.js → cameraFns-CG_J66Hl.js} +4 -4
- package/dist/core.cjs +12 -11
- package/dist/core.js +4 -3
- package/dist/{cornerFinder-Cqg-Dz74.js → cornerFinder-CPFM7bA0.js} +1 -1
- package/dist/{cornerFinder-DETRDOMz.cjs → cornerFinder-mfsUMi5g.cjs} +1 -1
- package/dist/{curveFns-RENXKp64.js → curveFns-DX13KRh2.js} +2 -2
- package/dist/{curveFns-D7utcbnf.cjs → curveFns-GmFnqNo3.cjs} +2 -2
- package/dist/{drawFns-D9Rf4foW.cjs → drawFns-B14M5M68.cjs} +14 -14
- package/dist/{drawFns-B9HbtAoc.js → drawFns-BShGVVXW.js} +14 -14
- package/dist/{errors-B3CDG537.js → errors-MHLiNE-C.js} +1 -92
- package/dist/{errors-CJU8CHng.cjs → errors-RHkKO5bZ.cjs} +0 -103
- package/dist/{faceFns-CPswi9Cm.cjs → faceFns-BK4r7Dxk.cjs} +3 -3
- package/dist/{faceFns-CB6uzvOg.js → faceFns-DJj9Nb6T.js} +3 -3
- package/dist/{healingFns-BFTYLiAm.js → healingFns-DxcRyVCG.js} +5 -6
- package/dist/{healingFns-bhMxTTqw.cjs → healingFns-Hx4oTlq8.cjs} +27 -28
- package/dist/{helpers-Cu4ag5bh.cjs → helpers-BfkBvokP.cjs} +8 -24
- package/dist/{helpers-BiN4qQZP.js → helpers-Du_OdHuy.js} +9 -25
- package/dist/{importFns-X8vBhZI2.js → importFns-BHs19muu.js} +3 -3
- package/dist/{importFns-DS4J1xLv.cjs → importFns-GySbuvVv.cjs} +3 -3
- package/dist/io.cjs +2 -2
- package/dist/io.js +2 -2
- package/dist/{loftFns-ChHjzUK-.cjs → loftFns-BdpZ8FMN.cjs} +2 -125
- package/dist/{loftFns-BF5X7ltI.js → loftFns-BeGJ6dxx.js} +4 -109
- package/dist/{measureFns-B5Zzg1qM.cjs → measureFns-CJGrmhHT.cjs} +4 -4
- package/dist/{measureFns-CC7HhAH3.js → measureFns-CMnbcUZC.js} +4 -4
- package/dist/measurement.cjs +1 -1
- package/dist/measurement.js +1 -1
- package/dist/{meshFns-D57Fyqyf.js → meshFns-Cs5EBsf7.js} +4 -4
- package/dist/{meshFns-CaXmWmhe.cjs → meshFns-Dqf5QBhQ.cjs} +5 -5
- package/dist/operations.cjs +2 -2
- package/dist/operations.js +2 -2
- package/dist/originTrackingFns-DZmdiQ-O.js +203 -0
- package/dist/originTrackingFns-fpEgTIwV.cjs +226 -0
- package/dist/{planeOps-Cp6GQ23-.js → planeOps-CQahLrCx.js} +1 -1
- package/dist/{planeOps-Cr25KWWA.cjs → planeOps-VlFxtvzY.cjs} +1 -1
- package/dist/primitiveFns-B_rflH83.cjs +773 -0
- package/dist/primitiveFns-Cxo3xDKY.js +558 -0
- package/dist/projection.cjs +1 -1
- package/dist/projection.js +1 -1
- package/dist/query.cjs +2 -2
- package/dist/query.js +2 -2
- package/dist/{refResolveFns-3bw9-U-e.js → refResolveFns-BbRLD8LA.js} +6 -6
- package/dist/{refResolveFns-BxmgIhQM.cjs → refResolveFns-CVDLwh12.cjs} +6 -6
- package/dist/result.cjs +1 -1
- package/dist/result.js +1 -1
- package/dist/rolldown-runtime-DakpK96I.cjs +43 -0
- package/dist/shapeFns-BTFx-os7.js +43 -0
- package/dist/shapeFns-D_5TgiUi.cjs +84 -0
- package/dist/shapeRef.cjs +1 -1
- package/dist/shapeRef.js +1 -1
- package/dist/{shapeTypes-Bpsu5KJO.cjs → shapeTypes-CkcIL7zv.cjs} +1 -222
- package/dist/{shapeTypes-CnKFSfDT.js → shapeTypes-t49f64Dx.js} +2 -139
- package/dist/sketching.cjs +3 -3
- package/dist/sketching.js +3 -3
- package/dist/{solidBuilders-I8_V8qqU.js → solidBuilders-DTgqQC3W.js} +310 -8
- package/dist/{solidBuilders-Bp7lQW62.cjs → solidBuilders-IiIzjKAr.cjs} +401 -15
- package/dist/{surfaceBuilders-D8_08oOR.cjs → surfaceBuilders-Biqx51el.cjs} +6 -5
- package/dist/{surfaceBuilders-BNR7yMQO.js → surfaceBuilders-BryY-i0F.js} +4 -3
- package/dist/text.cjs +2 -2
- package/dist/text.js +2 -2
- package/dist/{textBlueprints-B5zcLGvJ.cjs → textBlueprints-8EcWreab.cjs} +138 -51
- package/dist/{textBlueprints-igbNvKcd.js → textBlueprints-rA1HT6j1.js} +116 -11
- package/dist/{textMetrics-CJvtpbc3.js → textMetrics-Bcbso1Ns.js} +2 -2
- package/dist/{textMetrics-08fF0QNf.cjs → textMetrics-GPBu9JKi.cjs} +2 -2
- package/dist/{threadFns-CqSMNyTZ.js → threadFns-BOtXTHZ9.js} +8 -8
- package/dist/{threadFns-Co6vWEWx.cjs → threadFns-DRrk9Qod.cjs} +8 -8
- package/dist/topology.cjs +9 -9
- package/dist/topology.js +9 -9
- package/dist/{topologyQueryFns-C3fhJndG.cjs → topologyQueryFns-Bia7o8l_.cjs} +97 -6
- package/dist/{topologyQueryFns-DNqT88yx.js → topologyQueryFns-BkQ6Q7gN.js} +93 -2
- package/dist/validityTypes-DiTgeCD9.js +129 -0
- package/dist/validityTypes-ZGMVlv7b.cjs +200 -0
- package/dist/vectors.cjs +1 -1
- package/dist/vectors.js +1 -1
- package/package.json +8 -8
- package/dist/primitiveFns-CRy_CUVg.cjs +0 -492
- package/dist/primitiveFns-DCIH-SBm.js +0 -331
- package/dist/shapeFns-BrHFNLMA.js +0 -771
- package/dist/shapeFns-qm9VRo4i.cjs +0 -962
|
@@ -1,771 +0,0 @@
|
|
|
1
|
-
import { it as getKernel, n as castResultShape } from "./shapeTypes-CnKFSfDT.js";
|
|
2
|
-
import { O as ok, d as validationError, t as BrepErrorCode, v as err } from "./errors-B3CDG537.js";
|
|
3
|
-
import { S as subShapeHashes, T as kernelCallRaw, c as getFaces, l as getOrCreateCache, r as getCacheEntry, w as kernelCall } from "./topologyQueryFns-DNqT88yx.js";
|
|
4
|
-
import { n as HASH_CODE_MAX, t as DEG2RAD } from "./constants-ITRzCnCp.js";
|
|
5
|
-
//#region src/topology/metadata/originTrackingFns.ts
|
|
6
|
-
/**
|
|
7
|
-
* Face origin tracking — tag faces with integer origins and propagate
|
|
8
|
-
* them through kernel operations via ShapeEvolution records.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* Tag all faces of a shape with an opaque integer origin.
|
|
12
|
-
* Consumers assign meaning (e.g., source line number).
|
|
13
|
-
*/
|
|
14
|
-
function setShapeOrigin(shape, origin) {
|
|
15
|
-
const cache = getOrCreateCache(shape);
|
|
16
|
-
const map = /* @__PURE__ */ new Map();
|
|
17
|
-
for (const hash of subShapeHashes(shape, "face")) map.set(hash, origin);
|
|
18
|
-
cache.faceOrigins = map;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Get the face origin map for a shape (faceHash → originTag).
|
|
22
|
-
* Returns undefined if no origins have been set or propagated.
|
|
23
|
-
*/
|
|
24
|
-
function getFaceOrigins(shape) {
|
|
25
|
-
return getCacheEntry(shape)?.faceOrigins;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Propagate face origins using a kernel-provided ShapeEvolution record.
|
|
29
|
-
*/
|
|
30
|
-
function propagateOriginsFromEvolution(evolution, inputs, result) {
|
|
31
|
-
const inputOrigins = /* @__PURE__ */ new Map();
|
|
32
|
-
for (const input of inputs) {
|
|
33
|
-
const origins = getFaceOrigins(input);
|
|
34
|
-
if (!origins) continue;
|
|
35
|
-
for (const [hash, origin] of origins) inputOrigins.set(hash, origin);
|
|
36
|
-
}
|
|
37
|
-
if (inputOrigins.size === 0) return;
|
|
38
|
-
const resultMap = /* @__PURE__ */ new Map();
|
|
39
|
-
for (const [hash, origin] of inputOrigins) {
|
|
40
|
-
if (evolution.deleted.has(hash)) continue;
|
|
41
|
-
const modifiedHashes = evolution.modified.get(hash);
|
|
42
|
-
if (modifiedHashes && modifiedHashes.length > 0) for (const modHash of modifiedHashes) resultMap.set(modHash, origin);
|
|
43
|
-
else resultMap.set(hash, origin);
|
|
44
|
-
const generatedHashes = evolution.generated.get(hash);
|
|
45
|
-
if (generatedHashes) {
|
|
46
|
-
for (const genHash of generatedHashes) if (!resultMap.has(genHash)) resultMap.set(genHash, origin);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
if (resultMap.size > 0) {
|
|
50
|
-
const cache = getOrCreateCache(result);
|
|
51
|
-
cache.faceOrigins = resultMap;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
/** Max 3D centroid separation (mm²) for a "near" match — the general case. */
|
|
55
|
-
var MAX_MATCH_DIST_SQ = 100;
|
|
56
|
-
/** Min |outNormal·inNormal| for two faces to count as sharing a plane's orientation. */
|
|
57
|
-
var COPLANAR_NORMAL_MIN = .985;
|
|
58
|
-
/**
|
|
59
|
-
* Max squared perpendicular offset (mm²) from an input face's plane for the
|
|
60
|
-
* output face to count as *coplanar* with it — i.e. a piece the boolean sliced
|
|
61
|
-
* off that same planar surface. 0.1mm² tolerates kernel/mesh float noise while
|
|
62
|
-
* still separating floors that sit even a fraction of a mm apart in depth.
|
|
63
|
-
*/
|
|
64
|
-
var COPLANAR_OFFSET_SQ = .1;
|
|
65
|
-
/**
|
|
66
|
-
* Find the best origin match for a result face by comparing normals and centroids
|
|
67
|
-
* against input face signatures. Returns `undefined` if no good match exists.
|
|
68
|
-
*
|
|
69
|
-
* Two passes, near strictly preferred:
|
|
70
|
-
* 1. Near match — same-facing normal (signed dot ≥ 0.707) and centroids within
|
|
71
|
-
* {@link MAX_MATCH_DIST_SQ}. The general case for regenerated seam faces
|
|
72
|
-
* (feature tops, flush walls). Unchanged legacy behavior. If any near match
|
|
73
|
-
* exists it wins outright — the coplanar pass never runs — so a distant
|
|
74
|
-
* coplanar candidate can't outscore a valid near one.
|
|
75
|
-
* 2. Coplanar match (only when no near match, and only between PLANAR faces) —
|
|
76
|
-
* the output face lies on an input face's plane (parallel *or* antiparallel
|
|
77
|
-
* normal, tiny perpendicular offset) regardless of how far its centroid
|
|
78
|
-
* drifted in-plane. Two cases pass 1 misses:
|
|
79
|
-
* • A boolean that splits a large planar face — a wide cutout floor sliced
|
|
80
|
-
* by an overlapping deeper cutout — leaves pieces >10mm from the parent
|
|
81
|
-
* centroid (past pass 1's cutoff).
|
|
82
|
-
* • A cut flips the cavity face's normal versus the tool face it came from,
|
|
83
|
-
* so the floor is antiparallel to its origin's input face.
|
|
84
|
-
* Both left the surface tagless → body color in multi-color consumers
|
|
85
|
-
* (gridfinity GH #2443). The planar gate keeps a curved output face from
|
|
86
|
-
* inheriting an origin off an unrelated face that merely shares a sampled
|
|
87
|
-
* tangent plane; among coplanar candidates the nearest in-plane wins.
|
|
88
|
-
*/
|
|
89
|
-
function findBestOriginMatch(outNormal, outCentroid, outPlanar, inputSigs) {
|
|
90
|
-
let bestNearScore = -Infinity;
|
|
91
|
-
let bestNear;
|
|
92
|
-
for (const inp of inputSigs) {
|
|
93
|
-
const dot = outNormal[0] * inp.normal[0] + outNormal[1] * inp.normal[1] + outNormal[2] * inp.normal[2];
|
|
94
|
-
if (dot < .707) continue;
|
|
95
|
-
const dx = outCentroid[0] - inp.centroid[0];
|
|
96
|
-
const dy = outCentroid[1] - inp.centroid[1];
|
|
97
|
-
const dz = outCentroid[2] - inp.centroid[2];
|
|
98
|
-
const distSq = dx * dx + dy * dy + dz * dz;
|
|
99
|
-
if (distSq > MAX_MATCH_DIST_SQ) continue;
|
|
100
|
-
const score = dot - distSq / MAX_MATCH_DIST_SQ;
|
|
101
|
-
if (score > bestNearScore) {
|
|
102
|
-
bestNearScore = score;
|
|
103
|
-
bestNear = inp.origin;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (bestNear !== void 0) return bestNear;
|
|
107
|
-
if (!outPlanar) return void 0;
|
|
108
|
-
let bestInPlaneSq = Infinity;
|
|
109
|
-
let bestCoplanar;
|
|
110
|
-
for (const inp of inputSigs) {
|
|
111
|
-
if (!inp.planar) continue;
|
|
112
|
-
const dot = outNormal[0] * inp.normal[0] + outNormal[1] * inp.normal[1] + outNormal[2] * inp.normal[2];
|
|
113
|
-
if (Math.abs(dot) < COPLANAR_NORMAL_MIN) continue;
|
|
114
|
-
const dx = outCentroid[0] - inp.centroid[0];
|
|
115
|
-
const dy = outCentroid[1] - inp.centroid[1];
|
|
116
|
-
const dz = outCentroid[2] - inp.centroid[2];
|
|
117
|
-
const perp = dx * inp.normal[0] + dy * inp.normal[1] + dz * inp.normal[2];
|
|
118
|
-
if (perp * perp > COPLANAR_OFFSET_SQ) continue;
|
|
119
|
-
const inPlaneSq = Math.max(0, dx * dx + dy * dy + dz * dz - perp * perp);
|
|
120
|
-
if (inPlaneSq < bestInPlaneSq) {
|
|
121
|
-
bestInPlaneSq = inPlaneSq;
|
|
122
|
-
bestCoplanar = inp.origin;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return bestCoplanar;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Fallback origin propagation when no kernel op object is available
|
|
129
|
-
* (native `fuseAll` / `cutAll`, which expose no ShapeEvolution record).
|
|
130
|
-
*
|
|
131
|
-
* Matches result faces to input faces by hash code first. Faces that pass
|
|
132
|
-
* through the boolean unchanged keep their hash and recover their origin
|
|
133
|
-
* directly. Faces the boolean *regenerates* (split, merged, or re-created at
|
|
134
|
-
* the intersection) get a fresh hash that matches no input — these fall back
|
|
135
|
-
* to geometric matching (normal + centroid) against the input face
|
|
136
|
-
* signatures.
|
|
137
|
-
*
|
|
138
|
-
* The geometric pass runs for every unmatched face, not only when the hash
|
|
139
|
-
* pass found nothing. A partial hash match is the common case for additive
|
|
140
|
-
* features (pass-through side walls match by hash; the surfaces where the
|
|
141
|
-
* tool meets the body — feature tops, flush walls — are regenerated and need
|
|
142
|
-
* the geometric pass). Gating the fallback on "zero hash matches" left those
|
|
143
|
-
* regenerated faces with no origin, so they fell back to 0 (body) at mesh
|
|
144
|
-
* time — the multi-color export bug where a scoop/label/lip top printed in
|
|
145
|
-
* the body color. Matching against the full signature set (body origin 0
|
|
146
|
-
* included) keeps body faces body and feature faces feature.
|
|
147
|
-
*/
|
|
148
|
-
function propagateOriginsByHash(inputs, result) {
|
|
149
|
-
const lookup = /* @__PURE__ */ new Map();
|
|
150
|
-
for (const input of inputs) {
|
|
151
|
-
const origins = getFaceOrigins(input);
|
|
152
|
-
if (!origins) continue;
|
|
153
|
-
for (const [hash, origin] of origins) lookup.set(hash, origin);
|
|
154
|
-
}
|
|
155
|
-
if (lookup.size === 0) return;
|
|
156
|
-
const kernel = getKernel();
|
|
157
|
-
const resultMap = /* @__PURE__ */ new Map();
|
|
158
|
-
const resultFaces = getFaces(result);
|
|
159
|
-
const unmatched = [];
|
|
160
|
-
for (const f of resultFaces) {
|
|
161
|
-
const hash = kernel.hashCode(f.wrapped, HASH_CODE_MAX);
|
|
162
|
-
const origin = lookup.get(hash);
|
|
163
|
-
if (origin !== void 0) resultMap.set(hash, origin);
|
|
164
|
-
else unmatched.push(f);
|
|
165
|
-
}
|
|
166
|
-
if (unmatched.length > 0) {
|
|
167
|
-
const inputSigs = [];
|
|
168
|
-
for (const input of inputs) {
|
|
169
|
-
const origins = getFaceOrigins(input);
|
|
170
|
-
if (!origins) continue;
|
|
171
|
-
for (const f of getFaces(input)) {
|
|
172
|
-
const hash = kernel.hashCode(f.wrapped, HASH_CODE_MAX);
|
|
173
|
-
const origin = origins.get(hash);
|
|
174
|
-
if (origin === void 0) continue;
|
|
175
|
-
try {
|
|
176
|
-
const bounds = kernel.uvBounds(f.wrapped);
|
|
177
|
-
const normal = kernel.surfaceNormal(f.wrapped, .5 * (bounds.uMin + bounds.uMax), .5 * (bounds.vMin + bounds.vMax));
|
|
178
|
-
const centroid = kernel.surfaceCenterOfMass(f.wrapped);
|
|
179
|
-
const planar = kernel.surfaceType(f.wrapped) === "plane";
|
|
180
|
-
inputSigs.push({
|
|
181
|
-
origin,
|
|
182
|
-
normal,
|
|
183
|
-
centroid,
|
|
184
|
-
planar
|
|
185
|
-
});
|
|
186
|
-
} catch {}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
if (inputSigs.length > 0) for (const f of unmatched) {
|
|
190
|
-
const hash = kernel.hashCode(f.wrapped, HASH_CODE_MAX);
|
|
191
|
-
try {
|
|
192
|
-
const outBounds = kernel.uvBounds(f.wrapped);
|
|
193
|
-
const bestOrigin = findBestOriginMatch(kernel.surfaceNormal(f.wrapped, .5 * (outBounds.uMin + outBounds.uMax), .5 * (outBounds.vMin + outBounds.vMax)), kernel.surfaceCenterOfMass(f.wrapped), kernel.surfaceType(f.wrapped) === "plane", inputSigs);
|
|
194
|
-
if (bestOrigin !== void 0) resultMap.set(hash, bestOrigin);
|
|
195
|
-
} catch {}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
if (resultMap.size > 0) {
|
|
199
|
-
const cache = getOrCreateCache(result);
|
|
200
|
-
cache.faceOrigins = resultMap;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
//#endregion
|
|
204
|
-
//#region src/topology/metadata/faceTagFns.ts
|
|
205
|
-
var shapeTagStore = /* @__PURE__ */ new WeakMap();
|
|
206
|
-
var tagMetadataStore = /* @__PURE__ */ new WeakMap();
|
|
207
|
-
/** O(1) check whether a shape has any face tags attached. */
|
|
208
|
-
function hasFaceTags(shape) {
|
|
209
|
-
return shapeTagStore.has(shape.wrapped);
|
|
210
|
-
}
|
|
211
|
-
function getTagMap(shape) {
|
|
212
|
-
let map = shapeTagStore.get(shape.wrapped);
|
|
213
|
-
if (!map) {
|
|
214
|
-
map = /* @__PURE__ */ new Map();
|
|
215
|
-
shapeTagStore.set(shape.wrapped, map);
|
|
216
|
-
}
|
|
217
|
-
return map;
|
|
218
|
-
}
|
|
219
|
-
function getMetaMap(shape) {
|
|
220
|
-
let map = tagMetadataStore.get(shape.wrapped);
|
|
221
|
-
if (!map) {
|
|
222
|
-
map = /* @__PURE__ */ new Map();
|
|
223
|
-
tagMetadataStore.set(shape.wrapped, map);
|
|
224
|
-
}
|
|
225
|
-
return map;
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Tag selected faces with a string name.
|
|
229
|
-
*
|
|
230
|
-
* @param shape - The shape containing the faces.
|
|
231
|
-
* @param selector - Array of faces, or a predicate function.
|
|
232
|
-
* @param tag - The tag name to assign.
|
|
233
|
-
* @returns The same shape (tags are stored externally).
|
|
234
|
-
*/
|
|
235
|
-
function tagFaces(shape, selector, tag) {
|
|
236
|
-
const faces = Array.isArray(selector) ? selector : getFaces(shape).filter(selector);
|
|
237
|
-
const tagMap = getTagMap(shape);
|
|
238
|
-
const existing = tagMap.get(tag) ?? /* @__PURE__ */ new Set();
|
|
239
|
-
for (const face of faces) existing.add(getKernel().hashCode(face.wrapped, HASH_CODE_MAX));
|
|
240
|
-
tagMap.set(tag, existing);
|
|
241
|
-
return shape;
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Find all faces on a shape that have the given tag.
|
|
245
|
-
*
|
|
246
|
-
* Checks both direct tags and propagated origins (for faces that
|
|
247
|
-
* survived boolean/modifier operations).
|
|
248
|
-
*/
|
|
249
|
-
function findFacesByTag(shape, tag) {
|
|
250
|
-
const tagMap = shapeTagStore.get(shape.wrapped);
|
|
251
|
-
if (!tagMap) return [];
|
|
252
|
-
const hashes = tagMap.get(tag);
|
|
253
|
-
if (!hashes || hashes.size === 0) return [];
|
|
254
|
-
const result = [];
|
|
255
|
-
for (const face of getFaces(shape)) {
|
|
256
|
-
const hash = getKernel().hashCode(face.wrapped, HASH_CODE_MAX);
|
|
257
|
-
if (hashes.has(hash)) result.push(face);
|
|
258
|
-
}
|
|
259
|
-
return result;
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Get all tags and their associated faces on a shape.
|
|
263
|
-
*/
|
|
264
|
-
function getFaceTags(shape) {
|
|
265
|
-
const result = /* @__PURE__ */ new Map();
|
|
266
|
-
const tagMap = shapeTagStore.get(shape.wrapped);
|
|
267
|
-
if (!tagMap) return result;
|
|
268
|
-
const faces = getFaces(shape);
|
|
269
|
-
const faceByHash = /* @__PURE__ */ new Map();
|
|
270
|
-
for (const face of faces) faceByHash.set(getKernel().hashCode(face.wrapped, HASH_CODE_MAX), face);
|
|
271
|
-
for (const [tag, hashes] of tagMap) {
|
|
272
|
-
const taggedFaces = [];
|
|
273
|
-
for (const hash of hashes) {
|
|
274
|
-
const face = faceByHash.get(hash);
|
|
275
|
-
if (face) taggedFaces.push(face);
|
|
276
|
-
}
|
|
277
|
-
if (taggedFaces.length > 0) result.set(tag, taggedFaces);
|
|
278
|
-
}
|
|
279
|
-
return result;
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Store arbitrary metadata for a tag on a shape.
|
|
283
|
-
*/
|
|
284
|
-
function setTagMetadata(shape, tag, metadata) {
|
|
285
|
-
getMetaMap(shape).set(tag, metadata);
|
|
286
|
-
return shape;
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* Retrieve metadata for a tag on a shape.
|
|
290
|
-
*/
|
|
291
|
-
function getTagMetadata(shape, tag) {
|
|
292
|
-
return tagMetadataStore.get(shape.wrapped)?.get(tag);
|
|
293
|
-
}
|
|
294
|
-
function addTagToResult(resultTagMap, tag, hash) {
|
|
295
|
-
const set = resultTagMap.get(tag) ?? /* @__PURE__ */ new Set();
|
|
296
|
-
set.add(hash);
|
|
297
|
-
resultTagMap.set(tag, set);
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Propagate face tags from input shapes to a result shape using a
|
|
301
|
-
* kernel-provided ShapeEvolution record (no direct kernel op access needed).
|
|
302
|
-
*/
|
|
303
|
-
function propagateFaceTagsFromEvolution(evolution, inputs, result) {
|
|
304
|
-
const resultTagMap = getTagMap(result);
|
|
305
|
-
for (const input of inputs) {
|
|
306
|
-
const inputTagMap = shapeTagStore.get(input.wrapped);
|
|
307
|
-
if (!inputTagMap) continue;
|
|
308
|
-
const hashToTags = /* @__PURE__ */ new Map();
|
|
309
|
-
for (const [tag, hashes] of inputTagMap) for (const hash of hashes) {
|
|
310
|
-
const tags = hashToTags.get(hash) ?? [];
|
|
311
|
-
tags.push(tag);
|
|
312
|
-
hashToTags.set(hash, tags);
|
|
313
|
-
}
|
|
314
|
-
for (const [hash, tags] of hashToTags) {
|
|
315
|
-
if (evolution.deleted.has(hash)) continue;
|
|
316
|
-
const modifiedHashes = evolution.modified.get(hash);
|
|
317
|
-
const targetHashes = modifiedHashes && modifiedHashes.length > 0 ? modifiedHashes : [hash];
|
|
318
|
-
for (const targetHash of targetHashes) for (const tag of tags) addTagToResult(resultTagMap, tag, targetHash);
|
|
319
|
-
}
|
|
320
|
-
const inputMetaMap = tagMetadataStore.get(input.wrapped);
|
|
321
|
-
if (inputMetaMap) {
|
|
322
|
-
const resultMetaMap = getMetaMap(result);
|
|
323
|
-
for (const [tag, meta] of inputMetaMap) if (!resultMetaMap.has(tag)) resultMetaMap.set(tag, meta);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
//#endregion
|
|
328
|
-
//#region src/topology/metadata/colorFns.ts
|
|
329
|
-
var shapeColorStore = /* @__PURE__ */ new WeakMap();
|
|
330
|
-
var faceColorStore = /* @__PURE__ */ new WeakMap();
|
|
331
|
-
/** O(1) check whether a shape has any color metadata (shape or face level). */
|
|
332
|
-
function hasColorMetadata(shape) {
|
|
333
|
-
return shapeColorStore.has(shape.wrapped) || faceColorStore.has(shape.wrapped);
|
|
334
|
-
}
|
|
335
|
-
function getFaceColorMap(shape) {
|
|
336
|
-
let map = faceColorStore.get(shape.wrapped);
|
|
337
|
-
if (!map) {
|
|
338
|
-
map = /* @__PURE__ */ new Map();
|
|
339
|
-
faceColorStore.set(shape.wrapped, map);
|
|
340
|
-
}
|
|
341
|
-
return map;
|
|
342
|
-
}
|
|
343
|
-
function parseColor(input) {
|
|
344
|
-
if (typeof input === "string") {
|
|
345
|
-
let hex = input.startsWith("#") ? input.slice(1) : input;
|
|
346
|
-
if (hex.length === 3) hex = hex.charAt(0) + hex.charAt(0) + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2);
|
|
347
|
-
return [
|
|
348
|
-
parseInt(hex.slice(0, 2), 16) / 255,
|
|
349
|
-
parseInt(hex.slice(2, 4), 16) / 255,
|
|
350
|
-
parseInt(hex.slice(4, 6), 16) / 255,
|
|
351
|
-
1
|
|
352
|
-
];
|
|
353
|
-
}
|
|
354
|
-
if (input.length === 3) return [
|
|
355
|
-
input[0],
|
|
356
|
-
input[1],
|
|
357
|
-
input[2],
|
|
358
|
-
1
|
|
359
|
-
];
|
|
360
|
-
return [
|
|
361
|
-
input[0],
|
|
362
|
-
input[1],
|
|
363
|
-
input[2],
|
|
364
|
-
input[3]
|
|
365
|
-
];
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
|
-
* Set a whole-shape color (stored externally via WeakMap).
|
|
369
|
-
* Returns the same shape reference.
|
|
370
|
-
*/
|
|
371
|
-
function colorShape(shape, color) {
|
|
372
|
-
shapeColorStore.set(shape.wrapped, parseColor(color));
|
|
373
|
-
return shape;
|
|
374
|
-
}
|
|
375
|
-
/**
|
|
376
|
-
* Set per-face colors on a shape.
|
|
377
|
-
* Returns the same shape reference.
|
|
378
|
-
*/
|
|
379
|
-
function colorFaces(shape, faces, color) {
|
|
380
|
-
const parsed = parseColor(color);
|
|
381
|
-
const map = getFaceColorMap(shape);
|
|
382
|
-
for (const face of faces) map.set(getKernel().hashCode(face.wrapped, HASH_CODE_MAX), parsed);
|
|
383
|
-
return shape;
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* Get the whole-shape color, or undefined if none set.
|
|
387
|
-
*/
|
|
388
|
-
function getShapeColor(shape) {
|
|
389
|
-
return shapeColorStore.get(shape.wrapped);
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Get the color of a specific face, or undefined if none set.
|
|
393
|
-
*/
|
|
394
|
-
function getFaceColor(shape, face) {
|
|
395
|
-
const map = faceColorStore.get(shape.wrapped);
|
|
396
|
-
if (!map) return void 0;
|
|
397
|
-
return map.get(getKernel().hashCode(face.wrapped, HASH_CODE_MAX));
|
|
398
|
-
}
|
|
399
|
-
/**
|
|
400
|
-
* Propagate colors from input shapes to a result shape using a
|
|
401
|
-
* kernel-provided ShapeEvolution record (no direct kernel op access needed).
|
|
402
|
-
*/
|
|
403
|
-
function propagateColorsFromEvolution(evolution, inputs, result) {
|
|
404
|
-
for (const input of inputs) {
|
|
405
|
-
const c = shapeColorStore.get(input.wrapped);
|
|
406
|
-
if (c) {
|
|
407
|
-
shapeColorStore.set(result.wrapped, c);
|
|
408
|
-
break;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
const resultFaceMap = getFaceColorMap(result);
|
|
412
|
-
for (const input of inputs) {
|
|
413
|
-
const inputFaceMap = faceColorStore.get(input.wrapped);
|
|
414
|
-
if (!inputFaceMap || inputFaceMap.size === 0) continue;
|
|
415
|
-
for (const [hash, color] of inputFaceMap) {
|
|
416
|
-
if (evolution.deleted.has(hash)) continue;
|
|
417
|
-
const modifiedHashes = evolution.modified.get(hash);
|
|
418
|
-
if (modifiedHashes && modifiedHashes.length > 0) for (const modHash of modifiedHashes) resultFaceMap.set(modHash, color);
|
|
419
|
-
else resultFaceMap.set(hash, color);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
//#endregion
|
|
424
|
-
//#region src/topology/metadata/metadataPropagation.ts
|
|
425
|
-
/**
|
|
426
|
-
* Collect ALL face hashes from input shapes for WithHistory kernel methods.
|
|
427
|
-
*
|
|
428
|
-
* Fast-path: returns empty array when no inputs have any metadata (origins,
|
|
429
|
-
* tags, or colors), avoiding expensive WASM topology exploration.
|
|
430
|
-
*/
|
|
431
|
-
/**
|
|
432
|
-
* O(1) check: does a shape carry any propagatable metadata (face origins,
|
|
433
|
-
* tags, or colors)? Lets callers skip both expensive face iteration and
|
|
434
|
-
* metadata-preserving slow paths when there's nothing to preserve.
|
|
435
|
-
*/
|
|
436
|
-
function hasAnyMetadata(shape) {
|
|
437
|
-
return getFaceOrigins(shape) !== void 0 || hasFaceTags(shape) || hasColorMetadata(shape);
|
|
438
|
-
}
|
|
439
|
-
function collectInputFaceHashes(inputs) {
|
|
440
|
-
if (!inputs.some(hasAnyMetadata)) return [];
|
|
441
|
-
const hashes = [];
|
|
442
|
-
for (const input of inputs) hashes.push(...subShapeHashes(input, "face"));
|
|
443
|
-
return hashes;
|
|
444
|
-
}
|
|
445
|
-
/**
|
|
446
|
-
* Propagate all metadata (origins, tags, colors) from inputs to result
|
|
447
|
-
* using a kernel-provided ShapeEvolution record.
|
|
448
|
-
*
|
|
449
|
-
* This is the standard pipeline for any operation that returns
|
|
450
|
-
* `{ shape, evolution }` from a WithHistory kernel method.
|
|
451
|
-
*/
|
|
452
|
-
function propagateAllMetadata(evolution, inputs, result) {
|
|
453
|
-
propagateOriginsFromEvolution(evolution, inputs, result);
|
|
454
|
-
propagateFaceTagsFromEvolution(evolution, inputs, result);
|
|
455
|
-
propagateColorsFromEvolution(evolution, inputs, result);
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* Fallback metadata propagation when no ShapeEvolution is available.
|
|
459
|
-
*
|
|
460
|
-
* Matches result faces to input faces by hash code (and geometric fallback).
|
|
461
|
-
* Used by operations that don't support WithHistory (e.g., native fuseAll).
|
|
462
|
-
*
|
|
463
|
-
* **Limitation:** Only propagates face origins. Face tags and face colors
|
|
464
|
-
* require a ShapeEvolution record to map input→output face hashes, so they
|
|
465
|
-
* are lost through this path. Batch booleans (`fuseAll`, `cutAll`) therefore
|
|
466
|
-
* have weaker metadata guarantees than pairwise booleans (`fuse`, `cut`).
|
|
467
|
-
*/
|
|
468
|
-
function propagateMetadataByHash(inputs, result) {
|
|
469
|
-
propagateOriginsByHash(inputs, result);
|
|
470
|
-
}
|
|
471
|
-
/**
|
|
472
|
-
* Propagate all metadata through a *rigid relocation* (a `locate` re-tag).
|
|
473
|
-
*
|
|
474
|
-
* A relocated shape shares the source's TShape, so its faces correspond 1:1 to
|
|
475
|
-
* the source faces in iteration order — but carry new, location-dependent
|
|
476
|
-
* hashes, so the hash-keyed metadata must be re-keyed rather than left as-is.
|
|
477
|
-
* This synthesizes a 1:1 `modified` evolution (source face hash → moved face
|
|
478
|
-
* hash) and runs the standard propagation pipeline, so origins, tags, and
|
|
479
|
-
* colors all survive a move at `locate` cost (O(faces)), not a full copy.
|
|
480
|
-
*
|
|
481
|
-
* Callers should gate on {@link hasAnyMetadata} first: with no metadata this
|
|
482
|
-
* does pointless face iteration. The face counts always match for a rigid
|
|
483
|
-
* relocation; the length guard is purely defensive.
|
|
484
|
-
*/
|
|
485
|
-
function propagateMetadataThroughRelocation(source, moved) {
|
|
486
|
-
const kernel = getKernel();
|
|
487
|
-
const srcFaces = [...kernel.iterShapes(source.wrapped, "face")];
|
|
488
|
-
const movedFaces = [...kernel.iterShapes(moved.wrapped, "face")];
|
|
489
|
-
if (srcFaces.length !== movedFaces.length) return;
|
|
490
|
-
const modified = /* @__PURE__ */ new Map();
|
|
491
|
-
for (let i = 0; i < srcFaces.length; i++) {
|
|
492
|
-
const sf = srcFaces[i];
|
|
493
|
-
const mf = movedFaces[i];
|
|
494
|
-
if (sf === void 0 || mf === void 0) continue;
|
|
495
|
-
modified.set(kernel.hashCode(sf, HASH_CODE_MAX), [kernel.hashCode(mf, HASH_CODE_MAX)]);
|
|
496
|
-
}
|
|
497
|
-
for (const f of srcFaces) kernel.dispose(f);
|
|
498
|
-
for (const f of movedFaces) kernel.dispose(f);
|
|
499
|
-
propagateAllMetadata({
|
|
500
|
-
modified,
|
|
501
|
-
generated: /* @__PURE__ */ new Map(),
|
|
502
|
-
deleted: /* @__PURE__ */ new Set()
|
|
503
|
-
}, [source], moved);
|
|
504
|
-
}
|
|
505
|
-
//#endregion
|
|
506
|
-
//#region src/topology/transformFns.ts
|
|
507
|
-
/**
|
|
508
|
-
* Immutable shape transforms — translate, rotate, scale, mirror, applyMatrix.
|
|
509
|
-
* All functions return new shapes without disposing inputs.
|
|
510
|
-
*/
|
|
511
|
-
/** Translate a shape by a vector. Returns a new shape. */
|
|
512
|
-
function translate(shape, v) {
|
|
513
|
-
const inputFaceHashes = collectInputFaceHashes([shape]);
|
|
514
|
-
const { shape: resultShape, evolution } = getKernel().translateWithHistory(shape.wrapped, v[0], v[1], v[2], inputFaceHashes, HASH_CODE_MAX);
|
|
515
|
-
const result = castResultShape(resultShape);
|
|
516
|
-
propagateAllMetadata(evolution, [shape], result);
|
|
517
|
-
return result;
|
|
518
|
-
}
|
|
519
|
-
/** Rotate a shape around an axis. Angle is in degrees. Returns a new shape. */
|
|
520
|
-
function rotate(shape, angle, position = [
|
|
521
|
-
0,
|
|
522
|
-
0,
|
|
523
|
-
0
|
|
524
|
-
], direction = [
|
|
525
|
-
0,
|
|
526
|
-
0,
|
|
527
|
-
1
|
|
528
|
-
]) {
|
|
529
|
-
const inputFaceHashes = collectInputFaceHashes([shape]);
|
|
530
|
-
const { shape: resultShape, evolution } = getKernel().rotateWithHistory(shape.wrapped, angle * DEG2RAD, inputFaceHashes, HASH_CODE_MAX, direction, position);
|
|
531
|
-
const result = castResultShape(resultShape);
|
|
532
|
-
propagateAllMetadata(evolution, [shape], result);
|
|
533
|
-
return result;
|
|
534
|
-
}
|
|
535
|
-
/** Mirror a shape through a plane defined by origin and normal. Returns a new shape. */
|
|
536
|
-
function mirror(shape, planeNormal = [
|
|
537
|
-
0,
|
|
538
|
-
1,
|
|
539
|
-
0
|
|
540
|
-
], planeOrigin = [
|
|
541
|
-
0,
|
|
542
|
-
0,
|
|
543
|
-
0
|
|
544
|
-
]) {
|
|
545
|
-
const inputFaceHashes = collectInputFaceHashes([shape]);
|
|
546
|
-
const { shape: resultShape, evolution } = getKernel().mirrorWithHistory(shape.wrapped, planeOrigin, planeNormal, inputFaceHashes, HASH_CODE_MAX);
|
|
547
|
-
const result = castResultShape(resultShape);
|
|
548
|
-
propagateAllMetadata(evolution, [shape], result);
|
|
549
|
-
return result;
|
|
550
|
-
}
|
|
551
|
-
/** Scale a shape uniformly. Returns a new shape. */
|
|
552
|
-
function scale(shape, factor, center = [
|
|
553
|
-
0,
|
|
554
|
-
0,
|
|
555
|
-
0
|
|
556
|
-
]) {
|
|
557
|
-
const inputFaceHashes = collectInputFaceHashes([shape]);
|
|
558
|
-
const { shape: resultShape, evolution } = getKernel().scaleWithHistory(shape.wrapped, center, factor, inputFaceHashes, HASH_CODE_MAX);
|
|
559
|
-
const result = castResultShape(resultShape);
|
|
560
|
-
propagateAllMetadata(evolution, [shape], result);
|
|
561
|
-
return result;
|
|
562
|
-
}
|
|
563
|
-
/** Resize a shape to exact target dimensions with optional auto-proportional scaling. */
|
|
564
|
-
function resize(shape, dimensions, options) {
|
|
565
|
-
const bbox = getKernel().boundingBox(shape.wrapped);
|
|
566
|
-
const size = [
|
|
567
|
-
bbox.max[0] - bbox.min[0],
|
|
568
|
-
bbox.max[1] - bbox.min[1],
|
|
569
|
-
bbox.max[2] - bbox.min[2]
|
|
570
|
-
];
|
|
571
|
-
const auto = options?.auto === true;
|
|
572
|
-
function factor(dim, sz, baseFactor) {
|
|
573
|
-
if (dim !== void 0 && sz > 1e-12) return dim / sz;
|
|
574
|
-
if (dim === void 0 && auto) return baseFactor;
|
|
575
|
-
return 1;
|
|
576
|
-
}
|
|
577
|
-
let autoFactor = 1;
|
|
578
|
-
if (auto) {
|
|
579
|
-
if (dimensions[0] !== void 0 && size[0] > 1e-12) autoFactor = dimensions[0] / size[0];
|
|
580
|
-
else if (dimensions[1] !== void 0 && size[1] > 1e-12) autoFactor = dimensions[1] / size[1];
|
|
581
|
-
else if (dimensions[2] !== void 0 && size[2] > 1e-12) autoFactor = dimensions[2] / size[2];
|
|
582
|
-
}
|
|
583
|
-
const factors = [
|
|
584
|
-
factor(dimensions[0], size[0], autoFactor),
|
|
585
|
-
factor(dimensions[1], size[1], autoFactor),
|
|
586
|
-
factor(dimensions[2], size[2], autoFactor)
|
|
587
|
-
];
|
|
588
|
-
if (!(Math.abs(factors[0] - factors[1]) < 1e-6 && Math.abs(factors[1] - factors[2]) < 1e-6)) return err(validationError(BrepErrorCode.VALIDATION_FAILED, "resize: non-uniform scaling is not supported (WASM build lacks BRepBuilderAPI_GTransform).", void 0, void 0, "Use auto: true to scale proportionally, or set all three dimensions to achieve uniform scaling."));
|
|
589
|
-
return ok(scale(shape, factors[0]));
|
|
590
|
-
}
|
|
591
|
-
/**
|
|
592
|
-
* Parse a MatrixInput into a 3x3 linear part and translation vector.
|
|
593
|
-
* Validates the bottom row of a Matrix4x4.
|
|
594
|
-
*/
|
|
595
|
-
function parseMatrixInput(input) {
|
|
596
|
-
if ("linear" in input) return ok({
|
|
597
|
-
linear: input.linear,
|
|
598
|
-
translation: input.translation
|
|
599
|
-
});
|
|
600
|
-
const [r0, r1, r2, r3] = input;
|
|
601
|
-
const TOL = 1e-10;
|
|
602
|
-
if (Math.abs(r3[0]) > TOL || Math.abs(r3[1]) > TOL || Math.abs(r3[2]) > TOL || Math.abs(r3[3] - 1) > TOL) return err(validationError(BrepErrorCode.VALIDATION_FAILED, `applyMatrix: invalid bottom row [${String(r3[0])}, ${String(r3[1])}, ${String(r3[2])}, ${String(r3[3])}]. Must be [0, 0, 0, 1] for an affine transform.`));
|
|
603
|
-
return ok({
|
|
604
|
-
linear: [
|
|
605
|
-
r0[0],
|
|
606
|
-
r0[1],
|
|
607
|
-
r0[2],
|
|
608
|
-
r1[0],
|
|
609
|
-
r1[1],
|
|
610
|
-
r1[2],
|
|
611
|
-
r2[0],
|
|
612
|
-
r2[1],
|
|
613
|
-
r2[2]
|
|
614
|
-
],
|
|
615
|
-
translation: [
|
|
616
|
-
r0[3],
|
|
617
|
-
r1[3],
|
|
618
|
-
r2[3]
|
|
619
|
-
]
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
/** Determinant of a 3x3 matrix given as 9 row-major values. */
|
|
623
|
-
function det3x3(m) {
|
|
624
|
-
return m[0] * (m[4] * m[8] - m[5] * m[7]) - m[1] * (m[3] * m[8] - m[5] * m[6]) + m[2] * (m[3] * m[7] - m[4] * m[6]);
|
|
625
|
-
}
|
|
626
|
-
/**
|
|
627
|
-
* Check if a 3x3 matrix is orthogonal (possibly with uniform scale).
|
|
628
|
-
* M is orthogonal-with-scale if M^T * M = s^2 * I for some scalar s.
|
|
629
|
-
*/
|
|
630
|
-
function isOrthogonalMatrix(m) {
|
|
631
|
-
const TOL = 1e-8;
|
|
632
|
-
const d00 = m[0] * m[0] + m[3] * m[3] + m[6] * m[6];
|
|
633
|
-
const d11 = m[1] * m[1] + m[4] * m[4] + m[7] * m[7];
|
|
634
|
-
const d22 = m[2] * m[2] + m[5] * m[5] + m[8] * m[8];
|
|
635
|
-
const d01 = m[0] * m[1] + m[3] * m[4] + m[6] * m[7];
|
|
636
|
-
const d02 = m[0] * m[2] + m[3] * m[5] + m[6] * m[8];
|
|
637
|
-
const d12 = m[1] * m[2] + m[4] * m[5] + m[7] * m[8];
|
|
638
|
-
if (Math.abs(d01) > TOL) return false;
|
|
639
|
-
if (Math.abs(d02) > TOL) return false;
|
|
640
|
-
if (Math.abs(d12) > TOL) return false;
|
|
641
|
-
if (Math.abs(d00 - d11) > TOL) return false;
|
|
642
|
-
if (Math.abs(d00 - d22) > TOL) return false;
|
|
643
|
-
return true;
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* Apply a 4x4 affine transformation matrix to a shape.
|
|
647
|
-
* Equivalent to OpenSCAD's `multmatrix`.
|
|
648
|
-
*
|
|
649
|
-
* Uses the fast `kernel transform` path for orthogonal matrices (rotation, uniform scale, mirror)
|
|
650
|
-
* and the general `gp_GTrsf` path for non-orthogonal transforms (shear, non-uniform scale).
|
|
651
|
-
*/
|
|
652
|
-
function applyMatrix(shape, matrix) {
|
|
653
|
-
const parsed = parseMatrixInput(matrix);
|
|
654
|
-
if (!parsed.ok) return parsed;
|
|
655
|
-
const { linear, translation } = parsed.value;
|
|
656
|
-
const d = det3x3(linear);
|
|
657
|
-
if (Math.abs(d) < 1e-12) return err(validationError(BrepErrorCode.VALIDATION_FAILED, "applyMatrix: singular matrix (determinant ≈ 0). Cannot apply a non-invertible transform."));
|
|
658
|
-
if (isOrthogonalMatrix(linear)) {
|
|
659
|
-
const inputFaceHashes = collectInputFaceHashes([shape]);
|
|
660
|
-
const { shape: resultShape, evolution } = getKernel().generalTransformWithHistory(shape.wrapped, linear, translation, true, inputFaceHashes, HASH_CODE_MAX);
|
|
661
|
-
const result = castResultShape(resultShape);
|
|
662
|
-
propagateAllMetadata(evolution, [shape], result);
|
|
663
|
-
return ok(result);
|
|
664
|
-
}
|
|
665
|
-
const result = castResultShape(getKernel().generalTransformNonOrthogonal(shape.wrapped, linear, translation));
|
|
666
|
-
propagateMetadataByHash([shape], result);
|
|
667
|
-
return ok(result);
|
|
668
|
-
/* v8 ignore stop */
|
|
669
|
-
}
|
|
670
|
-
/**
|
|
671
|
-
* Compose multiple translate/rotate operations into a single kernel transform.
|
|
672
|
-
* Operations are applied in order (first element applied first).
|
|
673
|
-
* Call `.cleanup()` on the result when done to free the kernel object.
|
|
674
|
-
*/
|
|
675
|
-
function composeTransforms(ops) {
|
|
676
|
-
const kernelOps = ops.map((op) => {
|
|
677
|
-
if (op.type === "translate") return {
|
|
678
|
-
type: "translate",
|
|
679
|
-
x: op.v[0],
|
|
680
|
-
y: op.v[1],
|
|
681
|
-
z: op.v[2]
|
|
682
|
-
};
|
|
683
|
-
return {
|
|
684
|
-
type: "rotate",
|
|
685
|
-
angle: op.angle,
|
|
686
|
-
axis: op.axis,
|
|
687
|
-
center: op.center
|
|
688
|
-
};
|
|
689
|
-
});
|
|
690
|
-
const { handle, dispose } = getKernel().composeTransform(kernelOps);
|
|
691
|
-
return {
|
|
692
|
-
trsf: handle,
|
|
693
|
-
cleanup: dispose
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
/**
|
|
697
|
-
* Placement-only rigid move (translate / rotate) via the kernel's cheap location
|
|
698
|
-
* re-tag — an O(1) `TopLoc_Location` swap on occt-wasm ≥3.6.0, falling back to a
|
|
699
|
-
* copying transform on kernels without it. Unlike `translate` / `rotate` (which
|
|
700
|
-
* deep-copy through the history-tracking path), `locate` shares the source
|
|
701
|
-
* geometry and only re-tags its placement, then re-keys face metadata
|
|
702
|
-
* (tags/colors) onto the moved faces so it survives — the same metadata
|
|
703
|
-
* guarantee `translate` gives, at location-swap cost. For per-position placement
|
|
704
|
-
* of one cached cell (template instancing), this is the move without the copy.
|
|
705
|
-
*
|
|
706
|
-
* Accepts a single op or an ordered list (applied first-to-last, e.g. rotate
|
|
707
|
-
* then translate). Non-rigid ops (`scale`) aren't a placement and don't belong
|
|
708
|
-
* here — use `applyMatrix` for those.
|
|
709
|
-
*/
|
|
710
|
-
function locate(shape, placement) {
|
|
711
|
-
const { trsf, cleanup } = composeTransforms("type" in placement ? [placement] : placement);
|
|
712
|
-
let moved;
|
|
713
|
-
try {
|
|
714
|
-
moved = castResultShape(getKernel().locate(shape.wrapped, trsf));
|
|
715
|
-
} finally {
|
|
716
|
-
cleanup();
|
|
717
|
-
}
|
|
718
|
-
if (hasAnyMetadata(shape)) propagateMetadataThroughRelocation(shape, moved);
|
|
719
|
-
return moved;
|
|
720
|
-
}
|
|
721
|
-
/**
|
|
722
|
-
* Clone a shape and apply a pre-composed transform in a single kernel operation.
|
|
723
|
-
* Much faster than separate clone() + translate() + rotate() calls.
|
|
724
|
-
*/
|
|
725
|
-
function transformCopy(shape, composed) {
|
|
726
|
-
const inputFaceHashes = collectInputFaceHashes([shape]);
|
|
727
|
-
const { shape: resultShape, evolution } = getKernel().applyComposedTransformWithHistory(shape.wrapped, composed.trsf, inputFaceHashes, HASH_CODE_MAX);
|
|
728
|
-
const result = castResultShape(resultShape);
|
|
729
|
-
propagateAllMetadata(evolution, [shape], result);
|
|
730
|
-
return result;
|
|
731
|
-
}
|
|
732
|
-
//#endregion
|
|
733
|
-
//#region src/topology/shapeFns.ts
|
|
734
|
-
/**
|
|
735
|
-
* Shape identity, introspection, and serialization functions.
|
|
736
|
-
*
|
|
737
|
-
* Re-exports from focused modules for backward compatibility:
|
|
738
|
-
* - Transform functions from `./transformFns.js`
|
|
739
|
-
* - Topology query functions from `./topologyQueryFns.js`
|
|
740
|
-
* - Origin tracking functions from `./metadata/originTrackingFns.js`
|
|
741
|
-
*/
|
|
742
|
-
/** Clone a shape (an independently-disposable deep copy). */
|
|
743
|
-
function clone(shape) {
|
|
744
|
-
return kernelCall(() => getKernel().copyShape(shape.wrapped), BrepErrorCode.CLONE_FAILED, "Failed to clone shape");
|
|
745
|
-
}
|
|
746
|
-
/** Serialize a shape to BREP string format. */
|
|
747
|
-
function toBREP(shape) {
|
|
748
|
-
return kernelCallRaw(() => getKernel().toBREP(shape.wrapped), BrepErrorCode.TO_BREP_FAILED, "Failed to serialize shape to BREP");
|
|
749
|
-
}
|
|
750
|
-
/** Get the topology hash code of a shape. */
|
|
751
|
-
function getHashCode(shape) {
|
|
752
|
-
return getKernel().hashCode(shape.wrapped, HASH_CODE_MAX);
|
|
753
|
-
}
|
|
754
|
-
/** Check if a shape is null. */
|
|
755
|
-
function isEmpty(shape) {
|
|
756
|
-
return getKernel().isNull(shape.wrapped);
|
|
757
|
-
}
|
|
758
|
-
/** Check if two shapes are the same topological entity. */
|
|
759
|
-
function isSameShape(a, b) {
|
|
760
|
-
return getKernel().isSame(a.wrapped, b.wrapped);
|
|
761
|
-
}
|
|
762
|
-
/** Check if two shapes are geometrically equal. */
|
|
763
|
-
function isEqualShape(a, b) {
|
|
764
|
-
return getKernel().isEqual(a.wrapped, b.wrapped);
|
|
765
|
-
}
|
|
766
|
-
/** Simplify a shape by merging same-domain faces/edges. Returns a new shape. */
|
|
767
|
-
function simplify(shape) {
|
|
768
|
-
return kernelCall(() => getKernel().simplify(shape.wrapped), BrepErrorCode.SIMPLIFY_FAILED, "Failed to simplify shape");
|
|
769
|
-
}
|
|
770
|
-
//#endregion
|
|
771
|
-
export { tagFaces as A, colorShape as C, getFaceTags as D, findFacesByTag as E, setShapeOrigin as M, getTagMetadata as O, colorFaces as S, getShapeColor as T, collectInputFaceHashes as _, isSameShape as a, propagateMetadataByHash as b, applyMatrix as c, mirror as d, resize as f, translate as g, transformCopy as h, isEqualShape as i, getFaceOrigins as j, setTagMetadata as k, composeTransforms as l, scale as m, getHashCode as n, simplify as o, rotate as p, isEmpty as r, toBREP as s, clone as t, locate as u, hasAnyMetadata as v, getFaceColor as w, propagateMetadataThroughRelocation as x, propagateAllMetadata as y };
|