fluidcad 0.0.28 → 0.0.30
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/profiler.d.ts +12 -0
- package/lib/dist/common/profiler.js +35 -0
- package/lib/dist/common/scene-object.d.ts +3 -0
- package/lib/dist/common/scene-object.js +3 -0
- package/lib/dist/common/shape-history-tracker.d.ts +9 -1
- package/lib/dist/common/shape-history-tracker.js +37 -23
- package/lib/dist/core/2d/aline.d.ts +13 -13
- package/lib/dist/core/2d/aline.js +20 -11
- package/lib/dist/core/2d/arc.d.ts +6 -6
- package/lib/dist/core/2d/arc.js +19 -15
- package/lib/dist/core/2d/back.d.ts +12 -0
- package/lib/dist/core/2d/back.js +11 -0
- package/lib/dist/core/2d/circle.d.ts +2 -2
- package/lib/dist/core/2d/circle.js +14 -10
- package/lib/dist/core/2d/ellipse.d.ts +35 -0
- package/lib/dist/core/2d/ellipse.js +65 -0
- package/lib/dist/core/2d/hline.d.ts +20 -13
- package/lib/dist/core/2d/hline.js +33 -15
- package/lib/dist/core/2d/index.d.ts +2 -0
- package/lib/dist/core/2d/index.js +2 -0
- package/lib/dist/core/2d/intersect.d.ts +2 -2
- package/lib/dist/core/2d/intersect.js +7 -3
- package/lib/dist/core/2d/line.d.ts +2 -2
- package/lib/dist/core/2d/line.js +14 -10
- package/lib/dist/core/2d/offset.d.ts +4 -4
- package/lib/dist/core/2d/offset.js +9 -5
- package/lib/dist/core/2d/polygon.d.ts +4 -4
- package/lib/dist/core/2d/polygon.js +24 -20
- package/lib/dist/core/2d/project.d.ts +2 -2
- package/lib/dist/core/2d/project.js +7 -3
- package/lib/dist/core/2d/rect.d.ts +2 -2
- package/lib/dist/core/2d/rect.js +22 -21
- package/lib/dist/core/2d/slot.d.ts +6 -6
- package/lib/dist/core/2d/slot.js +29 -32
- package/lib/dist/core/2d/vline.d.ts +20 -13
- package/lib/dist/core/2d/vline.js +29 -15
- package/lib/dist/core/interfaces.d.ts +62 -0
- package/lib/dist/core/mirror.d.ts +7 -7
- package/lib/dist/core/mirror.js +17 -11
- package/lib/dist/core/part.d.ts +3 -1
- package/lib/dist/core/part.js +1 -1
- package/lib/dist/core/rotate.d.ts +5 -5
- package/lib/dist/core/rotate.js +4 -1
- package/lib/dist/core/sketch.d.ts +3 -1
- package/lib/dist/core/sketch.js +1 -1
- package/lib/dist/core/translate.d.ts +9 -9
- package/lib/dist/features/2d/aline.d.ts +8 -5
- package/lib/dist/features/2d/aline.js +70 -18
- package/lib/dist/features/2d/back.d.ts +14 -0
- package/lib/dist/features/2d/back.js +35 -0
- package/lib/dist/features/2d/ellipse.d.ts +23 -0
- package/lib/dist/features/2d/ellipse.js +75 -0
- package/lib/dist/features/2d/hline.d.ts +9 -4
- package/lib/dist/features/2d/hline.js +65 -14
- package/lib/dist/features/2d/offset.d.ts +3 -0
- package/lib/dist/features/2d/offset.js +27 -3
- package/lib/dist/features/2d/sketch.d.ts +1 -0
- package/lib/dist/features/2d/sketch.js +15 -0
- package/lib/dist/features/2d/vline.d.ts +9 -4
- package/lib/dist/features/2d/vline.js +67 -15
- package/lib/dist/features/common.js +2 -1
- package/lib/dist/features/extrude-base.d.ts +19 -1
- package/lib/dist/features/extrude-base.js +75 -12
- package/lib/dist/features/extrude-two-distances.js +32 -27
- package/lib/dist/features/extrude.d.ts +39 -0
- package/lib/dist/features/extrude.js +196 -156
- package/lib/dist/features/fuse.js +2 -1
- package/lib/dist/features/lazy-scene-object.d.ts +1 -0
- package/lib/dist/features/lazy-scene-object.js +3 -0
- package/lib/dist/features/lazy-vertex.d.ts +1 -0
- package/lib/dist/features/lazy-vertex.js +3 -0
- package/lib/dist/features/loft.js +11 -8
- package/lib/dist/features/mirror-shape.d.ts +2 -0
- package/lib/dist/features/mirror-shape.js +16 -0
- package/lib/dist/features/mirror-shape2d.d.ts +2 -0
- package/lib/dist/features/mirror-shape2d.js +22 -1
- package/lib/dist/features/revolve.d.ts +31 -0
- package/lib/dist/features/revolve.js +178 -95
- package/lib/dist/features/rotate.d.ts +2 -0
- package/lib/dist/features/rotate.js +16 -0
- package/lib/dist/features/rotate2d.d.ts +2 -0
- package/lib/dist/features/rotate2d.js +16 -0
- package/lib/dist/features/select.js +2 -1
- package/lib/dist/features/simple-extruder.d.ts +3 -1
- package/lib/dist/features/simple-extruder.js +13 -9
- package/lib/dist/features/subtract.d.ts +2 -2
- package/lib/dist/features/subtract.js +3 -3
- package/lib/dist/features/sweep.d.ts +14 -0
- package/lib/dist/features/sweep.js +93 -80
- package/lib/dist/features/translate.d.ts +2 -0
- package/lib/dist/features/translate.js +23 -2
- package/lib/dist/filters/edge/edge-filter.d.ts +6 -0
- package/lib/dist/filters/edge/edge-filter.js +11 -0
- package/lib/dist/filters/face/face-filter.d.ts +6 -0
- package/lib/dist/filters/face/face-filter.js +11 -0
- package/lib/dist/filters/filter-base.d.ts +7 -1
- package/lib/dist/filters/filter-base.js +8 -0
- package/lib/dist/filters/filter-builder-base.js +11 -0
- package/lib/dist/filters/from-object.d.ts +14 -0
- package/lib/dist/filters/from-object.js +40 -0
- package/lib/dist/helpers/scene-helpers.d.ts +2 -0
- package/lib/dist/helpers/scene-helpers.js +68 -48
- package/lib/dist/oc/color-transfer.js +6 -0
- package/lib/dist/oc/edge-ops.d.ts +1 -0
- package/lib/dist/oc/edge-ops.js +17 -0
- package/lib/dist/oc/extrude-ops.d.ts +18 -1
- package/lib/dist/oc/extrude-ops.js +34 -1
- package/lib/dist/oc/geometry.d.ts +1 -0
- package/lib/dist/oc/geometry.js +27 -0
- package/lib/dist/oc/mesh.js +11 -9
- package/lib/dist/oc/ray-intersect.d.ts +16 -0
- package/lib/dist/oc/ray-intersect.js +91 -0
- package/lib/dist/oc/thin-face-maker.d.ts +0 -1
- package/lib/dist/oc/thin-face-maker.js +2 -20
- package/lib/dist/rendering/render.d.ts +2 -1
- package/lib/dist/rendering/render.js +72 -33
- package/lib/dist/rendering/scene.d.ts +4 -0
- package/lib/dist/tests/features/2d/back.test.d.ts +1 -0
- package/lib/dist/tests/features/2d/back.test.js +60 -0
- package/lib/dist/tests/features/2d/circle.test.js +1 -1
- package/lib/dist/tests/features/2d/constrained.test.js +4 -4
- package/lib/dist/tests/features/2d/ellipse.test.d.ts +1 -0
- package/lib/dist/tests/features/2d/ellipse.test.js +100 -0
- package/lib/dist/tests/features/2d/line.test.js +89 -3
- package/lib/dist/tests/features/2d/offset.test.js +1 -1
- package/lib/dist/tests/features/2d/polygon.test.js +2 -2
- package/lib/dist/tests/features/2d/rect.test.js +1 -1
- package/lib/dist/tests/features/2d/slot-from-edge.test.js +1 -1
- package/lib/dist/tests/features/2d/slot.test.js +1 -1
- package/lib/dist/tests/features/mirror.test.js +58 -0
- package/lib/dist/tests/features/mirror2d.test.js +63 -0
- package/lib/dist/tests/features/rotate.test.js +62 -0
- package/lib/dist/tests/features/rotate2d.test.js +47 -0
- package/lib/dist/tests/features/thin-revolve.test.js +37 -1
- package/lib/dist/tests/features/translate.test.js +63 -0
- package/lib/dist/tests/perf/record-fusion-history.bench.test.d.ts +1 -0
- package/lib/dist/tests/perf/record-fusion-history.bench.test.js +77 -0
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/server/dist/index.js +77 -45
- package/server/dist/ws-protocol.d.ts +11 -0
- package/ui/dist/assets/{index-BrW_x4uc.js → index-6Ep4GPxf.js} +131 -77
- package/ui/dist/assets/index-DRKfe6N9.css +2 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-gPoNOiIs.css +0 -2
|
@@ -15,41 +15,43 @@ export class ExtrudeTwoDistances extends ExtrudeBase {
|
|
|
15
15
|
this.distance2 = distance2;
|
|
16
16
|
}
|
|
17
17
|
build(context) {
|
|
18
|
-
const
|
|
19
|
-
const plane = this.getSourcePlane();
|
|
20
|
-
const pickedFaces = this.resolvePickedFaces(plane);
|
|
18
|
+
const p = context.getProfiler();
|
|
19
|
+
const plane = p.record('Get source plane', () => this.getSourcePlane());
|
|
20
|
+
const pickedFaces = p.record('Resolve picked faces', () => this.resolvePickedFaces(plane));
|
|
21
21
|
if (pickedFaces !== null && pickedFaces.length === 0) {
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
let faces;
|
|
25
25
|
let inwardEdges;
|
|
26
26
|
let outwardEdges;
|
|
27
|
-
|
|
28
|
-
if (this.
|
|
29
|
-
|
|
27
|
+
faces = p.record('Resolve faces', () => {
|
|
28
|
+
if (this.isFaceSourced()) {
|
|
29
|
+
if (this.isThin()) {
|
|
30
|
+
throw new Error("thin() is not supported with a face-sourced extrude");
|
|
31
|
+
}
|
|
32
|
+
return pickedFaces ?? this.getSourceFaces();
|
|
30
33
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
34
|
+
else if (this.isThin()) {
|
|
35
|
+
const thinResult = ThinFaceMaker.make(this.extrudable.getGeometries(), plane, this._thin[0], this._thin[1]);
|
|
36
|
+
inwardEdges = thinResult.inwardEdges;
|
|
37
|
+
outwardEdges = thinResult.outwardEdges;
|
|
38
|
+
return thinResult.faces;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
return pickedFaces ?? FaceMaker2.getRegions(this.extrudable.getGeometries(), plane, this.getDrill());
|
|
42
|
+
}
|
|
43
|
+
});
|
|
42
44
|
const draft = this.getDraft();
|
|
43
45
|
const draft1 = draft ? [draft[0], draft[0]] : undefined;
|
|
44
46
|
const draft2 = draft ? [draft[1], draft[1]] : undefined;
|
|
45
|
-
const extruder1 = new Extruder(faces, plane, this.distance1, draft1, this.getEndOffset());
|
|
46
|
-
const extrusions1 = extruder1.extrude();
|
|
47
|
+
const extruder1 = new Extruder(faces, plane, this.distance1, draft1, this.getEndOffset(), p);
|
|
48
|
+
const extrusions1 = p.record('Extrude direction 1', () => extruder1.extrude());
|
|
47
49
|
const startFaces = extruder1.getEndFaces();
|
|
48
|
-
const extruder2 = new Extruder(faces, plane, -this.distance2, draft2, this.getEndOffset());
|
|
49
|
-
const extrusions2 = extruder2.extrude();
|
|
50
|
+
const extruder2 = new Extruder(faces, plane, -this.distance2, draft2, this.getEndOffset(), p);
|
|
51
|
+
const extrusions2 = p.record('Extrude direction 2', () => extruder2.extrude());
|
|
50
52
|
const endFaces = extruder2.getEndFaces();
|
|
51
53
|
const all = [...extrusions1, ...extrusions2];
|
|
52
|
-
const halvesFuse = BooleanOps.fuse(all);
|
|
54
|
+
const halvesFuse = p.record('Fuse halves', () => BooleanOps.fuse(all));
|
|
53
55
|
const extrusions = halvesFuse.result;
|
|
54
56
|
halvesFuse.dispose();
|
|
55
57
|
const remainingFaces = [];
|
|
@@ -122,13 +124,16 @@ export class ExtrudeTwoDistances extends ExtrudeBase {
|
|
|
122
124
|
this.setState('cap-faces', capFaces);
|
|
123
125
|
this.getSource()?.removeShapes(this);
|
|
124
126
|
if (this._operationMode === 'remove') {
|
|
125
|
-
const scope = this.resolveFusionScope(context.getSceneObjects());
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
const scope = p.record('Resolve fusion scope', () => this.resolveFusionScope(context.getSceneObjects()));
|
|
128
|
+
p.record('Cut with scene objects', () => {
|
|
129
|
+
cutWithSceneObjects(scope, extrusions, plane, this.distance1 + this.distance2, this, {
|
|
130
|
+
recordHistoryFor: this,
|
|
131
|
+
});
|
|
128
132
|
});
|
|
129
133
|
this.setFinalShapes(this.getShapes());
|
|
130
134
|
return;
|
|
131
135
|
}
|
|
136
|
+
const sceneObjects = p.record('Resolve fusion scope', () => this.resolveFusionScope(context.getSceneObjects()));
|
|
132
137
|
if (extrusions.length === 0 || sceneObjects.length === 0) {
|
|
133
138
|
this.addShapes(extrusions);
|
|
134
139
|
this.recordShapeFacesAndEdgesAsAdditions(extrusions);
|
|
@@ -136,9 +141,9 @@ export class ExtrudeTwoDistances extends ExtrudeBase {
|
|
|
136
141
|
this.setFinalShapes(this.getShapes());
|
|
137
142
|
return;
|
|
138
143
|
}
|
|
139
|
-
const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions, {
|
|
144
|
+
const fusionResult = p.record('Fuse with scene objects', () => fuseWithSceneObjects(sceneObjects, extrusions, {
|
|
140
145
|
recordHistoryFor: this,
|
|
141
|
-
});
|
|
146
|
+
}));
|
|
142
147
|
for (const modifiedShape of fusionResult.modifiedShapes) {
|
|
143
148
|
if (!modifiedShape.object) {
|
|
144
149
|
continue;
|
|
@@ -5,8 +5,47 @@ export declare class Extrude extends ExtrudeBase {
|
|
|
5
5
|
distance: number;
|
|
6
6
|
constructor(distance: number, source?: Extrudable | SceneObject);
|
|
7
7
|
build(context: BuildSceneObjectContext): void;
|
|
8
|
+
/** Resolve the source faces for a non-thin extrude (add / symmetric / remove). */
|
|
9
|
+
private resolveSourceFaces;
|
|
10
|
+
/** Plain extrude: one direction, regular face classification. */
|
|
8
11
|
private buildAdd;
|
|
12
|
+
/** Thin extrude: shell-like profile with inward/outward offsets. */
|
|
13
|
+
private buildAddThin;
|
|
14
|
+
/**
|
|
15
|
+
* Classify a thin extrusion's faces. For OPEN profiles (`inwardEdges`
|
|
16
|
+
* non-empty) we reclassify side/internal/cap via the inward/outward edge
|
|
17
|
+
* matching. For CLOSED profiles (no inward edges, e.g. a rect with `thin(5)`
|
|
18
|
+
* creating an annulus) the Extruder's own inner-wire detection already
|
|
19
|
+
* separated start/end/side/internal correctly — keep it as-is.
|
|
20
|
+
*/
|
|
21
|
+
private classifyThinExtrusion;
|
|
22
|
+
/** Symmetric extrude: two halves fused together, regular face classification. */
|
|
9
23
|
private buildSymmetric;
|
|
24
|
+
/**
|
|
25
|
+
* Classify symmetric-extrusion remaining faces using inner-wire detection.
|
|
26
|
+
* Used by both `buildSymmetric` and the closed-profile fallback in
|
|
27
|
+
* `buildSymmetricThin`. Faces of the fused solid that share an edge with a
|
|
28
|
+
* detected inner wire (a hole) are internal; the rest are side.
|
|
29
|
+
*/
|
|
30
|
+
private classifySymmetricByInnerWires;
|
|
31
|
+
/** Symmetric thin extrude: two halves of a shell-like profile fused. */
|
|
32
|
+
private buildSymmetricThin;
|
|
33
|
+
/**
|
|
34
|
+
* Build the two half-extrusions for a symmetric op, fuse them together, and
|
|
35
|
+
* remap start/end faces onto the fused solid. The "remaining" faces (not
|
|
36
|
+
* start, not end) are returned for the caller to classify per-mode (regular
|
|
37
|
+
* inner-wire detection vs thin reclassification).
|
|
38
|
+
*/
|
|
39
|
+
private buildSymmetricHalves;
|
|
40
|
+
/**
|
|
41
|
+
* Find post-fusion edges that came from inner wires of the start face's
|
|
42
|
+
* pre-fusion profile (holes). Used to classify the symmetric fused solid's
|
|
43
|
+
* remaining faces into side vs internal. Inner wires are detected as
|
|
44
|
+
* counter-clockwise wires on the sketch plane; their edges are mapped onto
|
|
45
|
+
* the fused solid by 2D midpoint matching (SimplifyResult breaks TShape
|
|
46
|
+
* identity for merged half-faces).
|
|
47
|
+
*/
|
|
48
|
+
private detectFusedInnerEdges;
|
|
10
49
|
private buildRemove;
|
|
11
50
|
getDependencies(): SceneObject[];
|
|
12
51
|
createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Extruder } from "./simple-extruder.js";
|
|
2
|
-
import {
|
|
2
|
+
import { cutWithSceneObjects } from "../helpers/scene-helpers.js";
|
|
3
3
|
import { ExtrudeBase } from "./extrude-base.js";
|
|
4
4
|
import { FaceMaker2 } from "../oc/face-maker2.js";
|
|
5
5
|
import { BooleanOps } from "../oc/boolean-ops.js";
|
|
@@ -14,122 +14,193 @@ export class Extrude extends ExtrudeBase {
|
|
|
14
14
|
this.distance = distance;
|
|
15
15
|
}
|
|
16
16
|
build(context) {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
console.log(`[perf] Extrude.getSourcePlane: ${(performance.now() - t).toFixed(1)} ms`);
|
|
21
|
-
t = performance.now();
|
|
22
|
-
const pickedFaces = this.resolvePickedFaces(plane);
|
|
23
|
-
console.log(`[perf] Extrude.resolvePickedFaces: ${(performance.now() - t).toFixed(1)} ms`);
|
|
17
|
+
const p = context.getProfiler();
|
|
18
|
+
const plane = p.record('Get source plane', () => this.getSourcePlane());
|
|
19
|
+
const pickedFaces = p.record('Resolve picked faces', () => this.resolvePickedFaces(plane));
|
|
24
20
|
if (pickedFaces !== null && pickedFaces.length === 0) {
|
|
25
21
|
return;
|
|
26
22
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
let outwardEdges;
|
|
30
|
-
t = performance.now();
|
|
31
|
-
if (this.isFaceSourced()) {
|
|
32
|
-
if (this.isThin()) {
|
|
23
|
+
if (this.isThin()) {
|
|
24
|
+
if (this.isFaceSourced()) {
|
|
33
25
|
throw new Error("thin() is not supported with a face-sourced extrude");
|
|
34
26
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (this._operationMode === 'remove') {
|
|
48
|
-
this.buildRemove(faces, plane, context);
|
|
49
|
-
}
|
|
50
|
-
else if (this._symmetric) {
|
|
51
|
-
this.buildSymmetric(faces, plane, context, inwardEdges, outwardEdges);
|
|
27
|
+
const thinResult = p.record('Resolve thin faces', () => ThinFaceMaker.make(this.extrudable.getGeometries(), plane, this._thin[0], this._thin[1]));
|
|
28
|
+
if (this._operationMode === 'remove') {
|
|
29
|
+
// Thin + remove: use the thin profile faces as the cut tool source,
|
|
30
|
+
// but the cut path doesn't apply thin face reclassification.
|
|
31
|
+
this.buildRemove(thinResult.faces, plane, context);
|
|
32
|
+
}
|
|
33
|
+
else if (this._symmetric) {
|
|
34
|
+
this.buildSymmetricThin(thinResult, plane, context);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
this.buildAddThin(thinResult, plane, context);
|
|
38
|
+
}
|
|
52
39
|
}
|
|
53
40
|
else {
|
|
54
|
-
|
|
41
|
+
const faces = p.record('Resolve faces', () => this.resolveSourceFaces(plane, pickedFaces));
|
|
42
|
+
if (this._operationMode === 'remove') {
|
|
43
|
+
this.buildRemove(faces, plane, context);
|
|
44
|
+
}
|
|
45
|
+
else if (this._symmetric) {
|
|
46
|
+
this.buildSymmetric(faces, plane, context);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
this.buildAdd(faces, plane, context);
|
|
50
|
+
}
|
|
55
51
|
}
|
|
56
52
|
this.setFinalShapes(this.getShapes());
|
|
57
|
-
console.log(`[perf] Extrude.build TOTAL: ${(performance.now() - tBuild).toFixed(1)} ms`);
|
|
58
53
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
t = performance.now();
|
|
64
|
-
const extruder = new Extruder(faces, plane, this.distance, this.getDraft(), this.getEndOffset());
|
|
65
|
-
let extrusions = extruder.extrude();
|
|
66
|
-
console.log(`[perf] Extrude.buildAdd.extruder.extrude (extrusions=${extrusions.length}): ${(performance.now() - t).toFixed(1)} ms`);
|
|
67
|
-
let sideFaces = extruder.getSideFaces();
|
|
68
|
-
let internalFaces = extruder.getInternalFaces();
|
|
69
|
-
let capFaces = [];
|
|
70
|
-
if (inwardEdges && inwardEdges.length > 0) {
|
|
71
|
-
const result = this.reclassifyThinFaces([...sideFaces, ...internalFaces], extruder.getStartFaces(), plane, inwardEdges, outwardEdges || []);
|
|
72
|
-
sideFaces = result.sideFaces;
|
|
73
|
-
internalFaces = result.internalFaces;
|
|
74
|
-
capFaces = result.capFaces;
|
|
54
|
+
/** Resolve the source faces for a non-thin extrude (add / symmetric / remove). */
|
|
55
|
+
resolveSourceFaces(plane, pickedFaces) {
|
|
56
|
+
if (this.isFaceSourced()) {
|
|
57
|
+
return pickedFaces ?? this.getSourceFaces();
|
|
75
58
|
}
|
|
76
|
-
this.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
59
|
+
return pickedFaces ?? FaceMaker2.getRegions(this.extrudable.getGeometries(), plane, this.getDrill());
|
|
60
|
+
}
|
|
61
|
+
/** Plain extrude: one direction, regular face classification. */
|
|
62
|
+
buildAdd(faces, plane, context) {
|
|
63
|
+
const p = context.getProfiler();
|
|
64
|
+
const extruder = new Extruder(faces, plane, this.distance, this.getDraft(), this.getEndOffset(), p);
|
|
65
|
+
const extrusions = p.record('Extrude faces', () => extruder.extrude());
|
|
66
|
+
const classified = {
|
|
67
|
+
startFaces: extruder.getStartFaces(),
|
|
68
|
+
endFaces: extruder.getEndFaces(),
|
|
69
|
+
sideFaces: extruder.getSideFaces(),
|
|
70
|
+
internalFaces: extruder.getInternalFaces(),
|
|
71
|
+
capFaces: [],
|
|
72
|
+
};
|
|
81
73
|
this.getSource()?.removeShapes(this);
|
|
82
|
-
|
|
83
|
-
if (extrusions.length === 0 || sceneObjects.length === 0) {
|
|
84
|
-
this.addShapes(extrusions);
|
|
85
|
-
this.recordShapeFacesAndEdgesAsAdditions(extrusions);
|
|
86
|
-
this.classifyExtrudeEdges();
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
const tFuse = performance.now();
|
|
90
|
-
const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions, {
|
|
74
|
+
this.finalizeAndFuse(extrusions, classified, context, {
|
|
91
75
|
glue: this.isFaceSourced() ? 'full' : undefined,
|
|
92
|
-
recordHistoryFor: this,
|
|
93
76
|
});
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
77
|
+
}
|
|
78
|
+
/** Thin extrude: shell-like profile with inward/outward offsets. */
|
|
79
|
+
buildAddThin(thinResult, plane, context) {
|
|
80
|
+
const p = context.getProfiler();
|
|
81
|
+
const extruder = new Extruder(thinResult.faces, plane, this.distance, this.getDraft(), this.getEndOffset(), p);
|
|
82
|
+
const extrusions = p.record('Extrude thin faces', () => extruder.extrude());
|
|
83
|
+
const classified = this.classifyThinExtrusion(extruder.getStartFaces(), extruder.getEndFaces(), extruder.getSideFaces(), extruder.getInternalFaces(), extruder.getStartFaces(), plane, thinResult);
|
|
84
|
+
this.getSource()?.removeShapes(this);
|
|
85
|
+
this.finalizeAndFuse(extrusions, classified, context, {
|
|
86
|
+
glue: this.isFaceSourced() ? 'full' : undefined,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Classify a thin extrusion's faces. For OPEN profiles (`inwardEdges`
|
|
91
|
+
* non-empty) we reclassify side/internal/cap via the inward/outward edge
|
|
92
|
+
* matching. For CLOSED profiles (no inward edges, e.g. a rect with `thin(5)`
|
|
93
|
+
* creating an annulus) the Extruder's own inner-wire detection already
|
|
94
|
+
* separated start/end/side/internal correctly — keep it as-is.
|
|
95
|
+
*/
|
|
96
|
+
classifyThinExtrusion(startFaces, endFaces, sideFaces, internalFaces, referenceFaces, plane, thinResult) {
|
|
97
|
+
if (thinResult.inwardEdges.length === 0) {
|
|
98
|
+
return {
|
|
99
|
+
startFaces,
|
|
100
|
+
endFaces,
|
|
101
|
+
sideFaces,
|
|
102
|
+
internalFaces,
|
|
103
|
+
capFaces: [],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const reclass = this.reclassifyThinFaces([...sideFaces, ...internalFaces], referenceFaces, plane, thinResult.inwardEdges, thinResult.outwardEdges);
|
|
107
|
+
return {
|
|
108
|
+
startFaces,
|
|
109
|
+
endFaces,
|
|
110
|
+
sideFaces: reclass.sideFaces,
|
|
111
|
+
internalFaces: reclass.internalFaces,
|
|
112
|
+
capFaces: reclass.capFaces,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** Symmetric extrude: two halves fused together, regular face classification. */
|
|
116
|
+
buildSymmetric(faces, plane, context) {
|
|
117
|
+
const halves = this.buildSymmetricHalves(faces, plane, context);
|
|
118
|
+
const classified = this.classifySymmetricByInnerWires(halves, plane);
|
|
119
|
+
this.getSource()?.removeShapes(this);
|
|
120
|
+
this.finalizeAndFuse(halves.extrusions, classified, context);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Classify symmetric-extrusion remaining faces using inner-wire detection.
|
|
124
|
+
* Used by both `buildSymmetric` and the closed-profile fallback in
|
|
125
|
+
* `buildSymmetricThin`. Faces of the fused solid that share an edge with a
|
|
126
|
+
* detected inner wire (a hole) are internal; the rest are side.
|
|
127
|
+
*/
|
|
128
|
+
classifySymmetricByInnerWires(halves, plane) {
|
|
129
|
+
const fusedInnerEdges = this.detectFusedInnerEdges(halves.extruder1, halves.fusedStartFaces, plane);
|
|
130
|
+
const sideFaces = [];
|
|
131
|
+
const internalFaces = [];
|
|
132
|
+
for (const f of halves.remainingFaces) {
|
|
133
|
+
const isInternal = fusedInnerEdges.length > 0 && f.getEdges().some(fe => fusedInnerEdges.some(ie => fe.getShape().IsPartner(ie.getShape())));
|
|
134
|
+
if (isInternal) {
|
|
135
|
+
internalFaces.push(f);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
sideFaces.push(f);
|
|
98
139
|
}
|
|
99
|
-
modifiedShape.object.removeShape(modifiedShape.shape, this);
|
|
100
140
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
141
|
+
return {
|
|
142
|
+
startFaces: halves.startFaces,
|
|
143
|
+
endFaces: halves.endFaces,
|
|
144
|
+
sideFaces,
|
|
145
|
+
internalFaces,
|
|
146
|
+
capFaces: [],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** Symmetric thin extrude: two halves of a shell-like profile fused. */
|
|
150
|
+
buildSymmetricThin(thinResult, plane, context) {
|
|
151
|
+
const halves = this.buildSymmetricHalves(thinResult.faces, plane, context);
|
|
152
|
+
let classified;
|
|
153
|
+
if (thinResult.inwardEdges.length > 0) {
|
|
154
|
+
// Open profile: reclassify side/internal/cap via inward/outward edge matching.
|
|
155
|
+
const reclass = this.reclassifyThinFaces(halves.remainingFaces, [...halves.fusedStartFaces, ...halves.fusedEndFaces], plane, thinResult.inwardEdges, thinResult.outwardEdges);
|
|
156
|
+
classified = {
|
|
157
|
+
startFaces: halves.startFaces,
|
|
158
|
+
endFaces: halves.endFaces,
|
|
159
|
+
sideFaces: reclass.sideFaces,
|
|
160
|
+
internalFaces: reclass.internalFaces,
|
|
161
|
+
capFaces: reclass.capFaces,
|
|
162
|
+
};
|
|
104
163
|
}
|
|
105
|
-
|
|
164
|
+
else {
|
|
165
|
+
// Closed profile (e.g. rect.thin(5) producing an annulus): fall back to
|
|
166
|
+
// the regular symmetric inner-wire detection — the Extruder's wire
|
|
167
|
+
// orientation already encoded the hole correctly.
|
|
168
|
+
classified = this.classifySymmetricByInnerWires(halves, plane);
|
|
169
|
+
}
|
|
170
|
+
this.getSource()?.removeShapes(this);
|
|
171
|
+
this.finalizeAndFuse(halves.extrusions, classified, context);
|
|
106
172
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Build the two half-extrusions for a symmetric op, fuse them together, and
|
|
175
|
+
* remap start/end faces onto the fused solid. The "remaining" faces (not
|
|
176
|
+
* start, not end) are returned for the caller to classify per-mode (regular
|
|
177
|
+
* inner-wire detection vs thin reclassification).
|
|
178
|
+
*/
|
|
179
|
+
buildSymmetricHalves(faces, plane, context) {
|
|
180
|
+
const p = context.getProfiler();
|
|
181
|
+
const extruder1 = new Extruder(faces, plane, this.distance / 2, this.getDraft(), this.getEndOffset(), p);
|
|
182
|
+
const extrusions1 = p.record('Extrude direction 1', () => extruder1.extrude());
|
|
111
183
|
const startFaces = extruder1.getEndFaces();
|
|
112
|
-
const extruder2 = new Extruder(faces, plane, -this.distance / 2, this.getDraft(), this.getEndOffset());
|
|
113
|
-
const extrusions2 = extruder2.extrude();
|
|
184
|
+
const extruder2 = new Extruder(faces, plane, -this.distance / 2, this.getDraft(), this.getEndOffset(), p);
|
|
185
|
+
const extrusions2 = p.record('Extrude direction 2', () => extruder2.extrude());
|
|
114
186
|
const endFaces = extruder2.getEndFaces();
|
|
115
187
|
const all = [...extrusions1, ...extrusions2];
|
|
116
|
-
const halvesFuse = BooleanOps.fuse(all);
|
|
188
|
+
const halvesFuse = p.record('Fuse halves', () => BooleanOps.fuse(all));
|
|
117
189
|
const extrusions = halvesFuse.result;
|
|
118
190
|
halvesFuse.dispose();
|
|
119
|
-
//
|
|
120
|
-
//
|
|
191
|
+
// Re-find start/end faces in the fused solid (NonDestructive preserves
|
|
192
|
+
// their TShape, so IsSame matching works) and collect everything else as
|
|
193
|
+
// "remaining" for per-mode classification.
|
|
121
194
|
const remainingFaces = [];
|
|
122
195
|
const fusedStartFaces = [];
|
|
123
196
|
const fusedEndFaces = [];
|
|
124
197
|
for (const solid of extrusions) {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const isEnd = endFaces.some(ef => f.getShape().IsSame(ef.getShape()));
|
|
129
|
-
if (isStart) {
|
|
198
|
+
for (const f of Explorer.findFacesWrapped(solid)) {
|
|
199
|
+
const raw = f.getShape();
|
|
200
|
+
if (startFaces.some(sf => raw.IsSame(sf.getShape()))) {
|
|
130
201
|
fusedStartFaces.push(f);
|
|
131
202
|
}
|
|
132
|
-
else if (
|
|
203
|
+
else if (endFaces.some(ef => raw.IsSame(ef.getShape()))) {
|
|
133
204
|
fusedEndFaces.push(f);
|
|
134
205
|
}
|
|
135
206
|
else {
|
|
@@ -137,81 +208,50 @@ export class Extrude extends ExtrudeBase {
|
|
|
137
208
|
}
|
|
138
209
|
}
|
|
139
210
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (preInnerEdges.length > 0) {
|
|
167
|
-
const innerMids = preInnerEdges.map(e => plane.worldToLocal(EdgeOps.getEdgeMidPointRaw(e.getShape())));
|
|
168
|
-
for (const sf of fusedStartFaces) {
|
|
169
|
-
for (const sfe of sf.getEdges()) {
|
|
170
|
-
const mid = plane.worldToLocal(EdgeOps.getEdgeMidPointRaw(sfe.getShape()));
|
|
171
|
-
if (innerMids.some(im => mid.distanceTo(im) < 1e-4)) {
|
|
172
|
-
fusedInnerEdges.push(sfe);
|
|
173
|
-
}
|
|
211
|
+
return {
|
|
212
|
+
extrusions,
|
|
213
|
+
extruder1,
|
|
214
|
+
extruder2,
|
|
215
|
+
startFaces,
|
|
216
|
+
endFaces,
|
|
217
|
+
fusedStartFaces,
|
|
218
|
+
fusedEndFaces,
|
|
219
|
+
remainingFaces,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Find post-fusion edges that came from inner wires of the start face's
|
|
224
|
+
* pre-fusion profile (holes). Used to classify the symmetric fused solid's
|
|
225
|
+
* remaining faces into side vs internal. Inner wires are detected as
|
|
226
|
+
* counter-clockwise wires on the sketch plane; their edges are mapped onto
|
|
227
|
+
* the fused solid by 2D midpoint matching (SimplifyResult breaks TShape
|
|
228
|
+
* identity for merged half-faces).
|
|
229
|
+
*/
|
|
230
|
+
detectFusedInnerEdges(extruder1, fusedStartFaces, plane) {
|
|
231
|
+
const preInnerEdges = [];
|
|
232
|
+
for (const sf of extruder1.getStartFaces()) {
|
|
233
|
+
for (const wire of sf.getWires()) {
|
|
234
|
+
if (!wire.isCW(plane.normal)) {
|
|
235
|
+
for (const edge of wire.getEdges()) {
|
|
236
|
+
preInnerEdges.push(edge);
|
|
174
237
|
}
|
|
175
238
|
}
|
|
176
239
|
}
|
|
177
|
-
sideFaces = [];
|
|
178
|
-
internalFaces = [];
|
|
179
|
-
for (const f of remainingFaces) {
|
|
180
|
-
const isInternal = fusedInnerEdges.length > 0 && f.getEdges().some(fe => fusedInnerEdges.some(ie => fe.getShape().IsPartner(ie.getShape())));
|
|
181
|
-
if (isInternal) {
|
|
182
|
-
internalFaces.push(f);
|
|
183
|
-
}
|
|
184
|
-
else {
|
|
185
|
-
sideFaces.push(f);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
240
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
this.setState('side-faces', sideFaces);
|
|
192
|
-
this.setState('internal-faces', internalFaces);
|
|
193
|
-
this.setState('cap-faces', capFaces);
|
|
194
|
-
this.getSource()?.removeShapes(this);
|
|
195
|
-
if (extrusions.length === 0 || sceneObjects.length === 0) {
|
|
196
|
-
this.addShapes(extrusions);
|
|
197
|
-
this.recordShapeFacesAndEdgesAsAdditions(extrusions);
|
|
198
|
-
this.classifyExtrudeEdges();
|
|
199
|
-
return;
|
|
241
|
+
if (preInnerEdges.length === 0) {
|
|
242
|
+
return [];
|
|
200
243
|
}
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
244
|
+
const innerMids = preInnerEdges.map(e => plane.worldToLocal(EdgeOps.getEdgeMidPointRaw(e.getShape())));
|
|
245
|
+
const fusedInnerEdges = [];
|
|
246
|
+
for (const sf of fusedStartFaces) {
|
|
247
|
+
for (const sfe of sf.getEdges()) {
|
|
248
|
+
const mid = plane.worldToLocal(EdgeOps.getEdgeMidPointRaw(sfe.getShape()));
|
|
249
|
+
if (innerMids.some(im => mid.distanceTo(im) < 1e-4)) {
|
|
250
|
+
fusedInnerEdges.push(sfe);
|
|
251
|
+
}
|
|
207
252
|
}
|
|
208
|
-
modifiedShape.object.removeShape(modifiedShape.shape, this);
|
|
209
|
-
}
|
|
210
|
-
this.addShapes(fusionResult.newShapes);
|
|
211
|
-
if (fusionResult.toolHistory) {
|
|
212
|
-
this.remapClassifiedFaces(fusionResult.toolHistory);
|
|
213
253
|
}
|
|
214
|
-
|
|
254
|
+
return fusedInnerEdges;
|
|
215
255
|
}
|
|
216
256
|
buildRemove(faces, plane, context) {
|
|
217
257
|
const scope = this.resolveFusionScope(context.getSceneObjects());
|
|
@@ -11,6 +11,7 @@ export class Fuse extends SceneObject {
|
|
|
11
11
|
return this._sceneObjects;
|
|
12
12
|
}
|
|
13
13
|
build(context) {
|
|
14
|
+
const p = context.getProfiler();
|
|
14
15
|
let sceneObjects = this.sceneObjects;
|
|
15
16
|
if (sceneObjects?.length === 0) {
|
|
16
17
|
sceneObjects = context.getSceneObjects();
|
|
@@ -25,7 +26,7 @@ export class Fuse extends SceneObject {
|
|
|
25
26
|
if (allShapes.length < 2) {
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
|
-
const fuseResult = BooleanOps.fuse(allShapes);
|
|
29
|
+
const fuseResult = p.record('Fuse solids', () => BooleanOps.fuse(allShapes));
|
|
29
30
|
if (fuseResult.result.length === allShapes.length) {
|
|
30
31
|
fuseResult.dispose();
|
|
31
32
|
return;
|
|
@@ -7,6 +7,7 @@ export declare class LazySelectionSceneObject extends SceneObject {
|
|
|
7
7
|
private _originalParent;
|
|
8
8
|
constructor(uniqueName: string, getShapesFn: (parent: SceneObject) => Shape[], sourceParent: SceneObject);
|
|
9
9
|
build(): void;
|
|
10
|
+
isLazy(): boolean;
|
|
10
11
|
getDependencies(): SceneObject[];
|
|
11
12
|
createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
|
|
12
13
|
compareTo(other: LazySelectionSceneObject): boolean;
|
|
@@ -8,6 +8,7 @@ export declare class LazyVertex extends SceneObject {
|
|
|
8
8
|
private _isBuilt;
|
|
9
9
|
constructor(uniqueName: string, getShapesFn: () => Shape[]);
|
|
10
10
|
build(): void;
|
|
11
|
+
isLazy(): boolean;
|
|
11
12
|
getShapes(filter?: ShapeFilter, type?: ShapeType): Shape[];
|
|
12
13
|
asPoint(): import("../math/point.js").Point;
|
|
13
14
|
asPoint2D(): import("../math/point.js").Point2D;
|