fluidcad 0.0.32 → 0.0.33
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/lib/dist/common/scene-object.d.ts +2 -0
- package/lib/dist/common/scene-object.js +6 -0
- package/lib/dist/common/solid.d.ts +4 -1
- package/lib/dist/common/solid.js +13 -0
- package/lib/dist/core/index.d.ts +2 -1
- package/lib/dist/core/index.js +1 -0
- package/lib/dist/core/interfaces.d.ts +76 -0
- package/lib/dist/core/load.d.ts +2 -2
- package/lib/dist/core/rib.d.ts +18 -0
- package/lib/dist/core/rib.js +37 -0
- package/lib/dist/features/load.d.ts +6 -0
- package/lib/dist/features/load.js +53 -1
- package/lib/dist/features/rib.d.ts +31 -0
- package/lib/dist/features/rib.js +321 -0
- package/lib/dist/features/select.d.ts +1 -0
- package/lib/dist/features/select.js +81 -10
- package/lib/dist/filters/edge/belongs-to-face.d.ts +12 -9
- package/lib/dist/filters/edge/belongs-to-face.js +64 -15
- package/lib/dist/filters/filter-builder-base.d.ts +25 -0
- package/lib/dist/filters/filter-builder-base.js +47 -0
- package/lib/dist/filters/filter.js +39 -14
- package/lib/dist/filters/from-object.d.ts +4 -0
- package/lib/dist/filters/from-object.js +10 -0
- package/lib/dist/helpers/scene-helpers.d.ts +1 -1
- package/lib/dist/helpers/scene-helpers.js +146 -12
- package/lib/dist/io/file-import.d.ts +5 -1
- package/lib/dist/io/file-import.js +29 -18
- package/lib/dist/oc/color-transfer.d.ts +19 -8
- package/lib/dist/oc/color-transfer.js +70 -12
- package/lib/dist/oc/extrude-ops.d.ts +2 -1
- package/lib/dist/oc/extrude-ops.js +51 -2
- package/lib/dist/oc/rib-ops.d.ts +35 -0
- package/lib/dist/oc/rib-ops.js +619 -0
- package/lib/dist/oc/topology-index.d.ts +6 -0
- package/lib/dist/oc/topology-index.js +36 -0
- package/lib/dist/rendering/render-solid.js +1 -3
- package/lib/dist/rendering/render.d.ts +1 -0
- package/lib/dist/rendering/render.js +44 -1
- package/lib/dist/rendering/scene-compare.js +3 -0
- package/lib/dist/rendering/scene.d.ts +1 -0
- package/lib/dist/rendering/scene.js +4 -0
- package/lib/dist/tests/features/color-lineage.test.js +18 -0
- package/lib/dist/tests/features/filter-positional.test.d.ts +1 -0
- package/lib/dist/tests/features/filter-positional.test.js +129 -0
- package/lib/dist/tests/features/rib.test.d.ts +1 -0
- package/lib/dist/tests/features/rib.test.js +598 -0
- package/lib/dist/tests/scene-compare.test.d.ts +1 -0
- package/lib/dist/tests/scene-compare.test.js +77 -0
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/ui/dist/assets/{index-DMw0OYCF.js → index-CFi9p7wR.js} +9 -9
- package/ui/dist/index.html +1 -1
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { Face } from "../common/face.js";
|
|
2
|
+
import { ExtrudeBase } from "./extrude-base.js";
|
|
3
|
+
import { ExtrudeOps } from "../oc/extrude-ops.js";
|
|
4
|
+
import { Explorer } from "../oc/explorer.js";
|
|
5
|
+
import { FaceQuery } from "../oc/face-query.js";
|
|
6
|
+
import { RibOps } from "../oc/rib-ops.js";
|
|
7
|
+
import { WireOps } from "../oc/wire-ops.js";
|
|
8
|
+
import { requireShapes } from "../common/operand-check.js";
|
|
9
|
+
export class Rib extends ExtrudeBase {
|
|
10
|
+
_thickness;
|
|
11
|
+
_spine;
|
|
12
|
+
_parallel = false;
|
|
13
|
+
_extend = false;
|
|
14
|
+
constructor(thickness, spine, extrudable) {
|
|
15
|
+
super(extrudable);
|
|
16
|
+
this._thickness = thickness;
|
|
17
|
+
this._spine = spine;
|
|
18
|
+
}
|
|
19
|
+
get thickness() {
|
|
20
|
+
return this._thickness;
|
|
21
|
+
}
|
|
22
|
+
get spine() {
|
|
23
|
+
return this._spine;
|
|
24
|
+
}
|
|
25
|
+
parallel() {
|
|
26
|
+
this._parallel = true;
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
extend() {
|
|
30
|
+
this._extend = true;
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
validate() {
|
|
34
|
+
requireShapes(this._spine, "spine", "rib");
|
|
35
|
+
}
|
|
36
|
+
build(context) {
|
|
37
|
+
const p = context.getProfiler();
|
|
38
|
+
const plane = this.extrudable.getPlane();
|
|
39
|
+
const originalSpineWire = p.record('Get spine wire', () => this.getSpineWire(this._spine));
|
|
40
|
+
let spineWire = originalSpineWire;
|
|
41
|
+
const scopeObjects = this.resolveFusionScope(context.getSceneObjects());
|
|
42
|
+
const scopeShapes = scopeObjects.flatMap(o => o.getShapes({}, 'solid'));
|
|
43
|
+
if (scopeShapes.length === 0) {
|
|
44
|
+
throw new Error("Rib requires target solids in the scene or via .scope()");
|
|
45
|
+
}
|
|
46
|
+
if (this._extend) {
|
|
47
|
+
spineWire = p.record('Extend spine', () => RibOps.extendSpineWire(spineWire, scopeShapes, plane));
|
|
48
|
+
}
|
|
49
|
+
// Sign convention:
|
|
50
|
+
// thickness > 0 → extrude OPPOSITE to the sketch normal (typical
|
|
51
|
+
// "into the cavity" direction when the sketch sits
|
|
52
|
+
// on top of a parent face).
|
|
53
|
+
// thickness < 0 → extrude IN the sketch normal direction.
|
|
54
|
+
// Same applies for parallel mode (the perpendicular-in-plane direction
|
|
55
|
+
// gets the same sign flip).
|
|
56
|
+
const dirSign = -Math.sign(this._thickness);
|
|
57
|
+
let direction;
|
|
58
|
+
let distance;
|
|
59
|
+
if (this._parallel) {
|
|
60
|
+
const perpDir = RibOps.computeSpinePerpendicularDirection(spineWire, plane);
|
|
61
|
+
direction = perpDir.multiply(dirSign);
|
|
62
|
+
distance = p.record('Compute extrude distance', () => RibOps.computeExtrudeDistanceAlongDirection(direction, plane.origin, scopeShapes));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
direction = plane.normal.multiply(dirSign);
|
|
66
|
+
distance = p.record('Compute extrude distance', () => RibOps.computeExtrudeDistance(plane, scopeShapes));
|
|
67
|
+
}
|
|
68
|
+
// Parallel + draft uses a tapered loft (firstFace lies on the spine
|
|
69
|
+
// plane by construction, exact thickness preserved). Everything else
|
|
70
|
+
// builds a plain prism from the rib profile face.
|
|
71
|
+
const useTaperedLoft = this._parallel && this.getDraft() !== null;
|
|
72
|
+
const profileFace = useTaperedLoft
|
|
73
|
+
? null
|
|
74
|
+
: p.record('Make rib profile', () => this._parallel
|
|
75
|
+
? RibOps.makeRibProfileParallel(spineWire, this._thickness, plane)
|
|
76
|
+
: RibOps.makeRibProfile(spineWire, this._thickness, plane));
|
|
77
|
+
const vec = direction.multiply(distance);
|
|
78
|
+
const { solid, firstFace, lastFace } = p.record('Extrude rib', () => {
|
|
79
|
+
if (useTaperedLoft) {
|
|
80
|
+
const draft = this.getDraft();
|
|
81
|
+
const angleRad = (draft[0] * Math.PI) / 180;
|
|
82
|
+
return RibOps.makeTaperedRibPrism(spineWire, this._thickness, plane, direction, distance, angleRad);
|
|
83
|
+
}
|
|
84
|
+
return ExtrudeOps.makePrismFromVec(profileFace, vec);
|
|
85
|
+
});
|
|
86
|
+
const ribSolid = solid;
|
|
87
|
+
const ribFirstFace = firstFace;
|
|
88
|
+
const ribLastFace = lastFace;
|
|
89
|
+
this.extrudable.removeShapes(this);
|
|
90
|
+
if (this._spine !== this.extrudable) {
|
|
91
|
+
this._spine.removeShapes(this);
|
|
92
|
+
}
|
|
93
|
+
const conformed = p.record('Conform rib', () => RibOps.conformRibToScope(ribSolid, scopeShapes, originalSpineWire, ribFirstFace, ribLastFace, direction));
|
|
94
|
+
let classified = {
|
|
95
|
+
startFaces: conformed.startFaces,
|
|
96
|
+
endFaces: conformed.endFaces,
|
|
97
|
+
sideFaces: conformed.sideFaces,
|
|
98
|
+
internalFaces: conformed.internalFaces,
|
|
99
|
+
capFaces: [],
|
|
100
|
+
};
|
|
101
|
+
let conformedSolids = conformed.solids;
|
|
102
|
+
// Draft is applied AFTER conformance so the prism walls are already
|
|
103
|
+
// bounded by the cavity. Drafting the over-extended pre-conform
|
|
104
|
+
// prism caused OCC to fail (walls would cross within the over-
|
|
105
|
+
// extension); the conformed rib is finite, so OCC handles strong
|
|
106
|
+
// drafts cleanly.
|
|
107
|
+
//
|
|
108
|
+
// Parallel mode skips this entirely — its prism was built tapered
|
|
109
|
+
// by `RibOps.makeTaperedRibPrism`, so the conformed result is
|
|
110
|
+
// already drafted with an exact (no-drift) start-face thickness.
|
|
111
|
+
// Normal mode uses OCC's draft with the sketch plane as the neutral
|
|
112
|
+
// plane (which already coincides with the prism base, so no shift
|
|
113
|
+
// is required).
|
|
114
|
+
if (!this._parallel && this.getDraft() && conformedSolids.length === 1 && classified.startFaces.length > 0) {
|
|
115
|
+
const draft = this.getDraft();
|
|
116
|
+
let angle = draft[0];
|
|
117
|
+
const draftPlane = plane;
|
|
118
|
+
// Mirrors the dirSign reversal above: in normal mode the OCC
|
|
119
|
+
// pull direction is the unsigned plane.normal, which now matches
|
|
120
|
+
// the extrude direction only when thickness < 0 (since extrude
|
|
121
|
+
// direction = plane.normal * -sign(thickness)). Negate the angle
|
|
122
|
+
// to flip OCC's outward bias to the user-facing "+ draft narrows
|
|
123
|
+
// the tip" convention.
|
|
124
|
+
if (this._thickness < 0) {
|
|
125
|
+
angle = -angle;
|
|
126
|
+
}
|
|
127
|
+
const rad = (deg) => deg * Math.PI / 180;
|
|
128
|
+
const draftResult = p.record('Apply draft', () => {
|
|
129
|
+
// Use the first start face as the OCC "firstFace" param (pivot
|
|
130
|
+
// anchor). When the conformance trimmed the original end face
|
|
131
|
+
// into the cavity (so endFaces is empty), the tip is already
|
|
132
|
+
// captured as an internal face — we pass startFaces[0] as the
|
|
133
|
+
// "lastFace" placeholder so its IsSame check is a no-op (it'll
|
|
134
|
+
// also match firstFace and so be excluded once). The actual tip
|
|
135
|
+
// surface stays excluded via the internalFaces argument.
|
|
136
|
+
const startRep = classified.startFaces[0];
|
|
137
|
+
const endRep = classified.endFaces[0] ?? startRep;
|
|
138
|
+
// Faces of the rib that sit flush with a scope face (the rib's
|
|
139
|
+
// mounting face — typically a cap that meets the cavity wall)
|
|
140
|
+
// must not be tilted. Drafting them either tears the rib away
|
|
141
|
+
// from the parent (negative draft) or makes OCC fail outright
|
|
142
|
+
// (positive draft) because there's no material outside the wall
|
|
143
|
+
// for the tilt to extend into.
|
|
144
|
+
const wallTouchingFaces = findScopeCoincidentFaces(classified.sideFaces, scopeShapes);
|
|
145
|
+
// The rib's "cap" faces (perpendicular to the spine direction)
|
|
146
|
+
// close the slab at the spine endpoints. Drafting them tilts
|
|
147
|
+
// them inward and shrinks the rib's length along the spine,
|
|
148
|
+
// which the user doesn't expect — only the long wall faces
|
|
149
|
+
// (perpendicular to spine direction) should taper.
|
|
150
|
+
const spineStart = originalSpineWire.getFirstVertex().toPoint();
|
|
151
|
+
const spineEnd = originalSpineWire.getLastVertex().toPoint();
|
|
152
|
+
const spineDir = spineStart.vectorTo(spineEnd).normalize();
|
|
153
|
+
const capFaces = findFacesAlignedWith(classified.sideFaces, spineDir);
|
|
154
|
+
const excludes = [
|
|
155
|
+
...classified.startFaces.slice(1),
|
|
156
|
+
...classified.endFaces.slice(classified.endFaces[0] === endRep ? 1 : 0),
|
|
157
|
+
...classified.internalFaces,
|
|
158
|
+
...wallTouchingFaces,
|
|
159
|
+
...capFaces,
|
|
160
|
+
];
|
|
161
|
+
return ExtrudeOps.applyDraftOnSideFaces(conformedSolids[0], startRep, endRep, draftPlane, rad(angle), excludes);
|
|
162
|
+
});
|
|
163
|
+
const remap = (faces) => {
|
|
164
|
+
const out = [];
|
|
165
|
+
for (const f of faces) {
|
|
166
|
+
for (const r of draftResult.remapFace(f)) {
|
|
167
|
+
out.push(r);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
};
|
|
172
|
+
classified = {
|
|
173
|
+
startFaces: remap(classified.startFaces),
|
|
174
|
+
endFaces: remap(classified.endFaces),
|
|
175
|
+
sideFaces: remap(classified.sideFaces),
|
|
176
|
+
internalFaces: remap(classified.internalFaces),
|
|
177
|
+
capFaces: [],
|
|
178
|
+
};
|
|
179
|
+
conformedSolids = [draftResult.solid];
|
|
180
|
+
}
|
|
181
|
+
if (this._operationMode === 'new') {
|
|
182
|
+
this.setState('start-faces', classified.startFaces);
|
|
183
|
+
this.setState('end-faces', classified.endFaces);
|
|
184
|
+
this.setState('side-faces', classified.sideFaces);
|
|
185
|
+
this.setState('internal-faces', classified.internalFaces);
|
|
186
|
+
this.setState('cap-faces', classified.capFaces);
|
|
187
|
+
this.addShapes(conformedSolids);
|
|
188
|
+
this.recordShapeFacesAndEdgesAsAdditions(conformedSolids);
|
|
189
|
+
this.classifyExtrudeEdges();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
this.finalizeAndFuse(conformedSolids, classified, context);
|
|
193
|
+
}
|
|
194
|
+
getSpineWire(pathObj) {
|
|
195
|
+
const shapes = pathObj.getShapes({ excludeMeta: false });
|
|
196
|
+
const edges = shapes.flatMap(s => s.getSubShapes('edge'));
|
|
197
|
+
return WireOps.makeWireFromEdges(edges);
|
|
198
|
+
}
|
|
199
|
+
getDependencies() {
|
|
200
|
+
const deps = [];
|
|
201
|
+
if (this.extrudable) {
|
|
202
|
+
deps.push(this.extrudable);
|
|
203
|
+
}
|
|
204
|
+
if (this._spine) {
|
|
205
|
+
deps.push(this._spine);
|
|
206
|
+
}
|
|
207
|
+
return deps;
|
|
208
|
+
}
|
|
209
|
+
createCopy(remap) {
|
|
210
|
+
const extrudable = this.extrudable
|
|
211
|
+
? (remap.get(this.extrudable) || this.extrudable)
|
|
212
|
+
: undefined;
|
|
213
|
+
const spine = remap.get(this._spine) || this._spine;
|
|
214
|
+
const copy = new Rib(this._thickness, spine, extrudable).syncWith(this);
|
|
215
|
+
copy._parallel = this._parallel;
|
|
216
|
+
copy._extend = this._extend;
|
|
217
|
+
return copy;
|
|
218
|
+
}
|
|
219
|
+
compareTo(other) {
|
|
220
|
+
if (!(other instanceof Rib)) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
if (!super.compareTo(other)) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
if (this._thickness !== other._thickness) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (!this._spine.compareTo(other._spine)) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
if (this.extrudable && other.extrudable && !this.extrudable.compareTo(other.extrudable)) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
if (this._parallel !== other._parallel) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
if (this._extend !== other._extend) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
getType() {
|
|
244
|
+
return "rib";
|
|
245
|
+
}
|
|
246
|
+
serialize() {
|
|
247
|
+
return {
|
|
248
|
+
thickness: this._thickness,
|
|
249
|
+
spine: this._spine.serialize(),
|
|
250
|
+
extrudable: this.extrudable?.serialize(),
|
|
251
|
+
operationMode: this._operationMode !== 'add' ? this._operationMode : undefined,
|
|
252
|
+
parallel: this._parallel || undefined,
|
|
253
|
+
extend: this._extend || undefined,
|
|
254
|
+
draft: this._draft,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Faces of the rib whose surface sits flush with a face of any scope
|
|
259
|
+
// shape (planar coincidence). These are the rib's mounting faces — they
|
|
260
|
+
// must not be tilted by draft; tilting them either tears the rib away
|
|
261
|
+
// from the parent (negative draft) or makes OCC's draft fail (positive
|
|
262
|
+
// draft), since there's no material outside the parent wall for the
|
|
263
|
+
// tilt to extend into.
|
|
264
|
+
// Rib faces whose surface normal is parallel to `direction` — for the
|
|
265
|
+
// rib's draft step this picks out the "cap" faces at the spine endpoints
|
|
266
|
+
// (perpendicular to the spine direction). They cap the slab length and
|
|
267
|
+
// shouldn't tilt under draft; only the wall faces, whose normals are
|
|
268
|
+
// perpendicular to spineDir, take the taper.
|
|
269
|
+
function findFacesAlignedWith(faces, direction) {
|
|
270
|
+
const out = [];
|
|
271
|
+
for (const f of faces) {
|
|
272
|
+
if (FaceQuery.getSurfaceType(f) !== "plane") {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const pl = FaceQuery.getSurfacePlane(f);
|
|
276
|
+
if (1 - Math.abs(pl.normal.dot(direction)) < 1e-4) {
|
|
277
|
+
out.push(f);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
function findScopeCoincidentFaces(ribSideFaces, scopeShapes) {
|
|
283
|
+
const out = [];
|
|
284
|
+
const scopePlanarFaces = [];
|
|
285
|
+
for (const scope of scopeShapes) {
|
|
286
|
+
const rawFaces = Explorer.findShapes(scope.getShape(), Explorer.getOcShapeType("face"));
|
|
287
|
+
for (const rf of rawFaces) {
|
|
288
|
+
const wrapped = Face.fromTopoDSFace(Explorer.toFace(rf));
|
|
289
|
+
if (FaceQuery.getSurfaceType(wrapped) !== "plane") {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const pl = FaceQuery.getSurfacePlane(wrapped);
|
|
293
|
+
scopePlanarFaces.push({ face: wrapped, origin: pl.origin, normal: pl.normal });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const tol = 1e-4;
|
|
297
|
+
for (const rf of ribSideFaces) {
|
|
298
|
+
if (FaceQuery.getSurfaceType(rf) !== "plane") {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const rPl = FaceQuery.getSurfacePlane(rf);
|
|
302
|
+
for (const { origin: sOrigin, normal: sNormal } of scopePlanarFaces) {
|
|
303
|
+
// Parallel test: |normal · normal'| ≈ 1
|
|
304
|
+
if (1 - Math.abs(rPl.normal.dot(sNormal)) > 1e-6) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
// Coincidence test: perpendicular distance from one plane's origin
|
|
308
|
+
// to the other plane is below tolerance. (FaceQuery.getSignedPlaneDistance
|
|
309
|
+
// routes through gp_Pln.Distance, which returns 0 for parallel-but-
|
|
310
|
+
// separated planes — useless for coincidence — so we compute it
|
|
311
|
+
// directly here.)
|
|
312
|
+
const offset = sOrigin.vectorTo(rPl.origin);
|
|
313
|
+
const d = Math.abs(offset.dot(sNormal));
|
|
314
|
+
if (d <= tol) {
|
|
315
|
+
out.push(rf);
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
@@ -11,6 +11,7 @@ export declare class SelectSceneObject extends SceneObject implements ISelect {
|
|
|
11
11
|
private shapes;
|
|
12
12
|
constructor(filters: FilterBuilderBase<Shape>[], constraintObject?: SceneObject);
|
|
13
13
|
build(context: BuildSceneObjectContext): void;
|
|
14
|
+
private injectFromMembershipSets;
|
|
14
15
|
private collectFromSceneObjects;
|
|
15
16
|
private getAllShapes;
|
|
16
17
|
getDependencies(): SceneObject[];
|
|
@@ -3,6 +3,7 @@ import { ShapeFilter } from "../filters/filter.js";
|
|
|
3
3
|
import { SceneObject } from "../common/scene-object.js";
|
|
4
4
|
import { BelongsToFaceFilter, NotBelongsToFaceFilter } from "../filters/edge/belongs-to-face.js";
|
|
5
5
|
import { FromSceneObjectFilter } from "../filters/from-object.js";
|
|
6
|
+
import { TopologyIndex } from "../oc/topology-index.js";
|
|
6
7
|
export class SelectSceneObject extends SceneObject {
|
|
7
8
|
filters;
|
|
8
9
|
constraintObject;
|
|
@@ -50,8 +51,39 @@ export class SelectSceneObject extends SceneObject {
|
|
|
50
51
|
if (this.type === "edge") {
|
|
51
52
|
this.injectScopeFaces(filters, sceneObjects);
|
|
52
53
|
}
|
|
53
|
-
const
|
|
54
|
-
|
|
54
|
+
const fromFilters = this.injectFromMembershipSets(filters);
|
|
55
|
+
try {
|
|
56
|
+
const filteredShapes = this.applyFilters(allShapes, filters);
|
|
57
|
+
this.addShapes(filteredShapes);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
for (const { filter, set } of fromFilters) {
|
|
61
|
+
filter.setMembershipSet(null);
|
|
62
|
+
set.delete();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
injectFromMembershipSets(filters) {
|
|
67
|
+
const allocated = [];
|
|
68
|
+
for (const builder of filters) {
|
|
69
|
+
for (const filter of builder.getFilters()) {
|
|
70
|
+
if (filter instanceof FromSceneObjectFilter) {
|
|
71
|
+
const shapeType = filter.getShapeType();
|
|
72
|
+
const rawShapes = [];
|
|
73
|
+
for (const obj of filter.getSceneObjects()) {
|
|
74
|
+
for (const owner of obj.getShapes()) {
|
|
75
|
+
for (const sub of owner.getSubShapes(shapeType)) {
|
|
76
|
+
rawShapes.push(sub.getShape());
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const set = TopologyIndex.buildShapeSet(rawShapes);
|
|
81
|
+
filter.setMembershipSet(set);
|
|
82
|
+
allocated.push({ filter, set });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return allocated;
|
|
55
87
|
}
|
|
56
88
|
collectFromSceneObjects(filters) {
|
|
57
89
|
const objects = [];
|
|
@@ -70,8 +102,17 @@ export class SelectSceneObject extends SceneObject {
|
|
|
70
102
|
}
|
|
71
103
|
getAllShapes(scope, exludedShapes) {
|
|
72
104
|
const scopeShapes = scope.flatMap(obj => obj.getShapes({}, 'solid').map(s => s.getSubShapes(this.type)).flat());
|
|
73
|
-
|
|
74
|
-
|
|
105
|
+
const flatExcluded = exludedShapes.flatMap(s => s.getSubShapes(this.type));
|
|
106
|
+
if (flatExcluded.length === 0) {
|
|
107
|
+
return scopeShapes;
|
|
108
|
+
}
|
|
109
|
+
const excludedSet = TopologyIndex.buildShapeSet(flatExcluded.map(s => s.getShape()));
|
|
110
|
+
try {
|
|
111
|
+
return scopeShapes.filter(shape => !excludedSet.Contains(shape.getShape()));
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
excludedSet.delete();
|
|
115
|
+
}
|
|
75
116
|
}
|
|
76
117
|
getDependencies() {
|
|
77
118
|
const deps = [];
|
|
@@ -97,16 +138,37 @@ export class SelectSceneObject extends SceneObject {
|
|
|
97
138
|
return new SelectSceneObject(mirroredFilters, this.constraintObject);
|
|
98
139
|
}
|
|
99
140
|
injectScopeFaces(filters, sceneObjects) {
|
|
100
|
-
let
|
|
141
|
+
let scopeSolids = null;
|
|
142
|
+
let extraFaces = null;
|
|
143
|
+
let faceByHash = null;
|
|
101
144
|
for (const builder of filters) {
|
|
102
145
|
for (const filter of builder.getFilters()) {
|
|
103
146
|
if (filter instanceof BelongsToFaceFilter || filter instanceof NotBelongsToFaceFilter) {
|
|
104
|
-
if (!
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
147
|
+
if (!scopeSolids) {
|
|
148
|
+
if (this.constraintObject) {
|
|
149
|
+
const constraintShapes = this.constraintObject.getShapes();
|
|
150
|
+
scopeSolids = constraintShapes.filter(s => s.isSolid());
|
|
151
|
+
// Faces directly in the constraint (not part of a solid) need the
|
|
152
|
+
// legacy linear-scan path since they don't have a cached index.
|
|
153
|
+
extraFaces = constraintShapes
|
|
154
|
+
.filter(s => !s.isSolid())
|
|
155
|
+
.flatMap(s => s.getSubShapes("face"));
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
scopeSolids = sceneObjects.flatMap(obj => obj.getShapes({}, 'solid'));
|
|
159
|
+
extraFaces = [];
|
|
160
|
+
}
|
|
161
|
+
faceByHash = new Map();
|
|
162
|
+
for (const solid of scopeSolids) {
|
|
163
|
+
for (const face of solid.getFaces()) {
|
|
164
|
+
addToBucket(faceByHash, face);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const face of extraFaces) {
|
|
168
|
+
addToBucket(faceByHash, face);
|
|
169
|
+
}
|
|
108
170
|
}
|
|
109
|
-
filter.
|
|
171
|
+
filter.setScopeIndex(scopeSolids, extraFaces, faceByHash);
|
|
110
172
|
}
|
|
111
173
|
}
|
|
112
174
|
}
|
|
@@ -158,3 +220,12 @@ export class SelectSceneObject extends SceneObject {
|
|
|
158
220
|
};
|
|
159
221
|
}
|
|
160
222
|
}
|
|
223
|
+
function addToBucket(faceByHash, face) {
|
|
224
|
+
const hash = face.getShape().HashCode(2147483647);
|
|
225
|
+
let bucket = faceByHash.get(hash);
|
|
226
|
+
if (!bucket) {
|
|
227
|
+
bucket = [];
|
|
228
|
+
faceByHash.set(hash, bucket);
|
|
229
|
+
}
|
|
230
|
+
bucket.push(face);
|
|
231
|
+
}
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import { Matrix4 } from "../../math/matrix4.js";
|
|
2
2
|
import { Edge, Face } from "../../common/shapes.js";
|
|
3
|
+
import { Solid } from "../../common/solid.js";
|
|
3
4
|
import { FilterBase } from "../filter-base.js";
|
|
4
5
|
import { FilterBuilderBase } from "../filter-builder-base.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
declare abstract class BelongsToFaceFilterBase extends FilterBase<Edge> {
|
|
7
|
+
protected faceFilterBuilders: FilterBuilderBase<Face>[];
|
|
8
|
+
protected scopeSolids: Solid[];
|
|
9
|
+
protected scopeFaces: Face[];
|
|
10
|
+
protected faceByHash: Map<number, Face[]>;
|
|
8
11
|
constructor(faceFilterBuilders: FilterBuilderBase<Face>[]);
|
|
9
|
-
|
|
12
|
+
setScopeIndex(solids: Solid[], extraFaces: Face[], faceByHash: Map<number, Face[]>): void;
|
|
13
|
+
protected findContainingFaces(edge: Edge): Face[];
|
|
14
|
+
}
|
|
15
|
+
export declare class BelongsToFaceFilter extends BelongsToFaceFilterBase {
|
|
10
16
|
match(shape: Edge): boolean;
|
|
11
17
|
compareTo(other: BelongsToFaceFilter): boolean;
|
|
12
18
|
transform(matrix: Matrix4): BelongsToFaceFilter;
|
|
13
19
|
}
|
|
14
|
-
export declare class NotBelongsToFaceFilter extends
|
|
15
|
-
private faceFilterBuilders;
|
|
16
|
-
private scopeFaces;
|
|
17
|
-
constructor(faceFilterBuilders: FilterBuilderBase<Face>[]);
|
|
18
|
-
setScopeFaces(faces: Face[]): void;
|
|
20
|
+
export declare class NotBelongsToFaceFilter extends BelongsToFaceFilterBase {
|
|
19
21
|
match(shape: Edge): boolean;
|
|
20
22
|
compareTo(other: NotBelongsToFaceFilter): boolean;
|
|
21
23
|
transform(matrix: Matrix4): NotBelongsToFaceFilter;
|
|
22
24
|
}
|
|
25
|
+
export {};
|
|
@@ -1,16 +1,53 @@
|
|
|
1
|
+
import { Face } from "../../common/shapes.js";
|
|
2
|
+
import { Explorer } from "../../oc/explorer.js";
|
|
3
|
+
import { TopologyIndex } from "../../oc/topology-index.js";
|
|
1
4
|
import { FilterBase } from "../filter-base.js";
|
|
2
|
-
|
|
5
|
+
class BelongsToFaceFilterBase extends FilterBase {
|
|
3
6
|
faceFilterBuilders;
|
|
7
|
+
scopeSolids = [];
|
|
4
8
|
scopeFaces = [];
|
|
9
|
+
faceByHash = new Map();
|
|
5
10
|
constructor(faceFilterBuilders) {
|
|
6
11
|
super();
|
|
7
12
|
this.faceFilterBuilders = faceFilterBuilders;
|
|
8
13
|
}
|
|
9
|
-
|
|
10
|
-
this.
|
|
14
|
+
setScopeIndex(solids, extraFaces, faceByHash) {
|
|
15
|
+
this.scopeSolids = solids;
|
|
16
|
+
this.scopeFaces = extraFaces;
|
|
17
|
+
this.faceByHash = faceByHash;
|
|
11
18
|
}
|
|
19
|
+
findContainingFaces(edge) {
|
|
20
|
+
const edgeShape = edge.getShape();
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
const result = [];
|
|
23
|
+
for (const solid of this.scopeSolids) {
|
|
24
|
+
const index = solid.getEdgeToFacesIndex();
|
|
25
|
+
const rawFaces = TopologyIndex.seekShapes(index, edgeShape);
|
|
26
|
+
for (const raw of rawFaces) {
|
|
27
|
+
const wrapper = resolveFaceWrapper(raw, this.faceByHash);
|
|
28
|
+
if (wrapper && !seen.has(wrapper)) {
|
|
29
|
+
seen.add(wrapper);
|
|
30
|
+
result.push(wrapper);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (this.scopeFaces.length > 0) {
|
|
35
|
+
for (const face of this.scopeFaces) {
|
|
36
|
+
if (seen.has(face)) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (face.hasEdge(edgeShape) !== null) {
|
|
40
|
+
seen.add(face);
|
|
41
|
+
result.push(face);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export class BelongsToFaceFilter extends BelongsToFaceFilterBase {
|
|
12
49
|
match(shape) {
|
|
13
|
-
const containingFaces = this.
|
|
50
|
+
const containingFaces = this.findContainingFaces(shape);
|
|
14
51
|
return this.faceFilterBuilders.every(builder => {
|
|
15
52
|
const filters = builder.getFilters();
|
|
16
53
|
return containingFaces.some(face => filters.every(f => f.match(face)));
|
|
@@ -32,18 +69,9 @@ export class BelongsToFaceFilter extends FilterBase {
|
|
|
32
69
|
return new BelongsToFaceFilter(transformed);
|
|
33
70
|
}
|
|
34
71
|
}
|
|
35
|
-
export class NotBelongsToFaceFilter extends
|
|
36
|
-
faceFilterBuilders;
|
|
37
|
-
scopeFaces = [];
|
|
38
|
-
constructor(faceFilterBuilders) {
|
|
39
|
-
super();
|
|
40
|
-
this.faceFilterBuilders = faceFilterBuilders;
|
|
41
|
-
}
|
|
42
|
-
setScopeFaces(faces) {
|
|
43
|
-
this.scopeFaces = faces;
|
|
44
|
-
}
|
|
72
|
+
export class NotBelongsToFaceFilter extends BelongsToFaceFilterBase {
|
|
45
73
|
match(shape) {
|
|
46
|
-
const containingFaces = this.
|
|
74
|
+
const containingFaces = this.findContainingFaces(shape);
|
|
47
75
|
return !this.faceFilterBuilders.every(builder => {
|
|
48
76
|
const filters = builder.getFilters();
|
|
49
77
|
return containingFaces.some(face => filters.every(f => f.match(face)));
|
|
@@ -65,3 +93,24 @@ export class NotBelongsToFaceFilter extends FilterBase {
|
|
|
65
93
|
return new NotBelongsToFaceFilter(transformed);
|
|
66
94
|
}
|
|
67
95
|
}
|
|
96
|
+
function resolveFaceWrapper(rawFace, faceByHash) {
|
|
97
|
+
const hash = rawFace.HashCode(2147483647);
|
|
98
|
+
const bucket = faceByHash.get(hash);
|
|
99
|
+
if (bucket) {
|
|
100
|
+
for (const candidate of bucket) {
|
|
101
|
+
if (candidate.getShape().IsSame(rawFace)) {
|
|
102
|
+
return candidate;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Not in scope (e.g. neighbor face from another part / out-of-scope solid).
|
|
107
|
+
// Wrap on the fly so the face filters can still evaluate it.
|
|
108
|
+
const wrapped = Face.fromTopoDSFace(Explorer.toFace(rawFace));
|
|
109
|
+
if (!bucket) {
|
|
110
|
+
faceByHash.set(hash, [wrapped]);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
bucket.push(wrapped);
|
|
114
|
+
}
|
|
115
|
+
return wrapped;
|
|
116
|
+
}
|
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
import { Shape } from "../common/shapes.js";
|
|
2
2
|
import { FilterBase } from "./filter-base.js";
|
|
3
|
+
export type IndexSelector = {
|
|
4
|
+
type: 'first';
|
|
5
|
+
} | {
|
|
6
|
+
type: 'last';
|
|
7
|
+
} | {
|
|
8
|
+
type: 'at';
|
|
9
|
+
index: number;
|
|
10
|
+
};
|
|
3
11
|
export declare class FilterBuilderBase<TShape extends Shape = Shape> {
|
|
4
12
|
protected filters: FilterBase<TShape>[];
|
|
5
13
|
protected _withTangents: boolean;
|
|
14
|
+
protected _indexSelector?: IndexSelector;
|
|
6
15
|
filter(filter: FilterBase<TShape>): this;
|
|
7
16
|
/**
|
|
8
17
|
* Expands the selection to include all shapes transitively connected
|
|
9
18
|
* by tangency (G1 continuity) to the initially matched shapes.
|
|
10
19
|
*/
|
|
11
20
|
withTangents(): this;
|
|
21
|
+
/**
|
|
22
|
+
* Selects the first matching shape (in iteration order). If called multiple
|
|
23
|
+
* times on the same builder, the last positional call wins.
|
|
24
|
+
*/
|
|
25
|
+
first(): this;
|
|
26
|
+
/**
|
|
27
|
+
* Selects the last matching shape (in iteration order). If called multiple
|
|
28
|
+
* times on the same builder, the last positional call wins.
|
|
29
|
+
*/
|
|
30
|
+
last(): this;
|
|
31
|
+
/**
|
|
32
|
+
* Selects the matching shape at the given zero-based index.
|
|
33
|
+
* Out-of-range indices yield no match. Negative indices are not supported;
|
|
34
|
+
* use {@link last} for the final element.
|
|
35
|
+
*/
|
|
36
|
+
at(index: number): this;
|
|
12
37
|
}
|