fluidcad 0.0.16 → 0.0.18
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/README.md +44 -0
- package/bin/fluidcad.js +6 -1
- package/bin/watcher.js +19 -1
- package/lib/dist/common/breakpoint-hit.d.ts +5 -0
- package/lib/dist/common/breakpoint-hit.js +9 -0
- package/lib/dist/core/breakpoint.d.ts +1 -0
- package/lib/dist/core/breakpoint.js +5 -0
- package/lib/dist/core/index.d.ts +1 -0
- package/lib/dist/core/index.js +1 -0
- package/lib/dist/core/interfaces.d.ts +100 -0
- package/lib/dist/features/copy-linear.d.ts +2 -2
- package/lib/dist/features/copy-linear.js +17 -11
- package/lib/dist/features/copy-linear2d.js +17 -11
- package/lib/dist/features/extrude-base.js +1 -1
- package/lib/dist/features/loft.d.ts +6 -12
- package/lib/dist/features/loft.js +55 -128
- package/lib/dist/features/repeat-circular.d.ts +1 -0
- package/lib/dist/features/repeat-circular.js +3 -0
- package/lib/dist/features/repeat-linear.d.ts +1 -0
- package/lib/dist/features/repeat-linear.js +3 -0
- package/lib/dist/features/revolve.d.ts +1 -0
- package/lib/dist/features/revolve.js +47 -22
- package/lib/dist/features/sweep.d.ts +1 -0
- package/lib/dist/features/sweep.js +47 -2
- package/lib/dist/index.js +15 -10
- package/lib/dist/rendering/render.js +26 -2
- package/lib/dist/rendering/scene.d.ts +1 -0
- package/lib/dist/tests/features/copy-linear.test.js +2 -2
- package/lib/dist/tests/features/thin-loft.test.d.ts +1 -0
- package/lib/dist/tests/features/thin-loft.test.js +151 -0
- package/lib/dist/tests/features/thin-revolve.test.d.ts +1 -0
- package/lib/dist/tests/features/thin-revolve.test.js +153 -0
- package/lib/dist/tests/features/thin-sweep.test.d.ts +1 -0
- package/lib/dist/tests/features/thin-sweep.test.js +121 -0
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -1
- package/server/dist/code-editor.d.ts +13 -0
- package/server/dist/code-editor.js +71 -0
- package/server/dist/fluidcad-server.d.ts +1 -0
- package/server/dist/fluidcad-server.js +14 -1
- package/server/dist/index.js +2 -0
- package/server/dist/preferences.d.ts +3 -0
- package/server/dist/preferences.js +3 -0
- package/server/dist/routes/actions.js +34 -0
- package/server/dist/routes/preferences.js +9 -0
- package/server/dist/ws-protocol.d.ts +10 -1
- package/ui/dist/assets/index-BfcNNxXr.css +2 -0
- package/ui/dist/assets/{index-CLQhpG6A.js → index-BoKbrDML.js} +150 -56
- package/ui/dist/icons/.claude/settings.local.json +8 -0
- package/ui/dist/icons/hmove.png +0 -0
- package/ui/dist/icons/loft.png +0 -0
- package/ui/dist/icons/offset.png +0 -0
- package/ui/dist/icons/pmove.png +0 -0
- package/ui/dist/icons/projection.png +0 -0
- package/ui/dist/icons/remove.png +0 -0
- package/ui/dist/icons/vmove.png +0 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-BpvgjPLm.css +0 -2
|
@@ -16,6 +16,7 @@ export declare class RepeatLinear extends SceneObject {
|
|
|
16
16
|
options: LinearRepeatOptions;
|
|
17
17
|
targetObjects: SceneObject[] | null;
|
|
18
18
|
constructor(axes: Axis[], options: LinearRepeatOptions, targetObjects?: SceneObject[] | null);
|
|
19
|
+
isContainer(): boolean;
|
|
19
20
|
build(context: BuildSceneObjectContext): void;
|
|
20
21
|
compareTo(other: RepeatLinear): boolean;
|
|
21
22
|
getType(): string;
|
|
@@ -8,6 +8,7 @@ import { FaceMaker2 } from "../oc/face-maker2.js";
|
|
|
8
8
|
import { ExtrudeBase } from "./extrude-base.js";
|
|
9
9
|
import { BooleanOps } from "../oc/boolean-ops.js";
|
|
10
10
|
import { FaceOps } from "../oc/face-ops.js";
|
|
11
|
+
import { ThinFaceMaker } from "../oc/thin-face-maker.js";
|
|
11
12
|
export class Revolve extends ExtrudeBase {
|
|
12
13
|
axis;
|
|
13
14
|
angle;
|
|
@@ -25,24 +26,23 @@ export class Revolve extends ExtrudeBase {
|
|
|
25
26
|
const solids = [];
|
|
26
27
|
const allStartFaces = [];
|
|
27
28
|
const allEndFaces = [];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
let allSideFaces = [];
|
|
30
|
+
let allInternalFaces = [];
|
|
31
|
+
let allCapFaces = [];
|
|
32
|
+
let faces = pickedFaces ?? FaceMaker2.getRegions(this.extrudable.getGeometries(), plane);
|
|
33
|
+
let inwardEdges;
|
|
34
|
+
let outwardEdges;
|
|
35
|
+
if (this.isThin()) {
|
|
36
|
+
const thinResult = ThinFaceMaker.make(this.extrudable.getGeometries(), plane, this._thin[0], this._thin[1]);
|
|
37
|
+
faces = thinResult.faces;
|
|
38
|
+
inwardEdges = thinResult.inwardEdges;
|
|
39
|
+
outwardEdges = thinResult.outwardEdges;
|
|
40
|
+
}
|
|
31
41
|
const { result: fusedFaces } = BooleanOps.fuseFaces(faces);
|
|
32
42
|
const axis = this.axis.getAxis();
|
|
33
43
|
const isFullRevolution = Math.abs(this.angle) >= 360;
|
|
34
44
|
for (const face of fusedFaces) {
|
|
35
45
|
const solid = ExtrudeOps.makeRevol(face, axis, rad(this.angle));
|
|
36
|
-
// Collect inner wire edges for internal face detection
|
|
37
|
-
const innerWireEdges = [];
|
|
38
|
-
const wires = face.getWires();
|
|
39
|
-
for (const wire of wires) {
|
|
40
|
-
if (!wire.isCW(plane.normal)) {
|
|
41
|
-
for (const edge of wire.getEdges()) {
|
|
42
|
-
innerWireEdges.push(edge);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
46
|
let resultSolid;
|
|
47
47
|
if (this._symmetric) {
|
|
48
48
|
const rotated = ShapeOps.rotateShape(solid.getShape(), axis, -rad(this.angle) / 2);
|
|
@@ -59,15 +59,7 @@ export class Revolve extends ExtrudeBase {
|
|
|
59
59
|
if (isOnSourcePlane && !isFullRevolution) {
|
|
60
60
|
allStartFaces.push(f);
|
|
61
61
|
}
|
|
62
|
-
else
|
|
63
|
-
if (innerWireEdges.length > 0) {
|
|
64
|
-
const faceEdges = f.getEdges();
|
|
65
|
-
const isInternal = faceEdges.some(fe => innerWireEdges.some(iwe => fe.getShape().IsPartner(iwe.getShape())));
|
|
66
|
-
if (isInternal) {
|
|
67
|
-
allInternalFaces.push(f);
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
62
|
+
else {
|
|
71
63
|
allSideFaces.push(f);
|
|
72
64
|
}
|
|
73
65
|
}
|
|
@@ -81,10 +73,42 @@ export class Revolve extends ExtrudeBase {
|
|
|
81
73
|
allStartFaces.push(...startSlice);
|
|
82
74
|
allEndFaces.push(...endSlice);
|
|
83
75
|
}
|
|
76
|
+
if (inwardEdges && inwardEdges.length > 0) {
|
|
77
|
+
const result = this.reclassifyThinFaces(allSideFaces, allStartFaces, plane, inwardEdges, outwardEdges || []);
|
|
78
|
+
allSideFaces = result.sideFaces;
|
|
79
|
+
allInternalFaces = result.internalFaces;
|
|
80
|
+
allCapFaces = result.capFaces;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
const innerWireEdges = [];
|
|
84
|
+
for (const sf of allStartFaces) {
|
|
85
|
+
for (const wire of sf.getWires()) {
|
|
86
|
+
if (!wire.isCW(plane.normal)) {
|
|
87
|
+
for (const edge of wire.getEdges()) {
|
|
88
|
+
innerWireEdges.push(edge);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (innerWireEdges.length > 0) {
|
|
94
|
+
const remaining = [];
|
|
95
|
+
for (const f of allSideFaces) {
|
|
96
|
+
const isInternal = f.getEdges().some(fe => innerWireEdges.some(iwe => fe.getShape().IsPartner(iwe.getShape())));
|
|
97
|
+
if (isInternal) {
|
|
98
|
+
allInternalFaces.push(f);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
remaining.push(f);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
allSideFaces = remaining;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
84
107
|
this.setState('start-faces', allStartFaces);
|
|
85
108
|
this.setState('end-faces', allEndFaces);
|
|
86
109
|
this.setState('side-faces', allSideFaces);
|
|
87
110
|
this.setState('internal-faces', allInternalFaces);
|
|
111
|
+
this.setState('cap-faces', allCapFaces);
|
|
88
112
|
this.extrudable.removeShapes(this);
|
|
89
113
|
this.axis.removeShapes(this);
|
|
90
114
|
if (this._operationMode === 'remove') {
|
|
@@ -141,6 +165,7 @@ export class Revolve extends ExtrudeBase {
|
|
|
141
165
|
axis: this.axis.serialize(),
|
|
142
166
|
operationMode: this._operationMode !== 'add' ? this._operationMode : undefined,
|
|
143
167
|
symmetric: this._symmetric || undefined,
|
|
168
|
+
thin: this._thin,
|
|
144
169
|
...this.serializePickFields(),
|
|
145
170
|
};
|
|
146
171
|
}
|
|
@@ -4,6 +4,7 @@ import { WireOps } from "../oc/wire-ops.js";
|
|
|
4
4
|
import { FaceMaker2 } from "../oc/face-maker2.js";
|
|
5
5
|
import { ExtrudeBase } from "./extrude-base.js";
|
|
6
6
|
import { fuseWithSceneObjects, cutWithSceneObjects } from "../helpers/scene-helpers.js";
|
|
7
|
+
import { ThinFaceMaker } from "../oc/thin-face-maker.js";
|
|
7
8
|
export class Sweep extends ExtrudeBase {
|
|
8
9
|
_path;
|
|
9
10
|
constructor(path, extrudable) {
|
|
@@ -22,7 +23,15 @@ export class Sweep extends ExtrudeBase {
|
|
|
22
23
|
// Extract spine wire from path
|
|
23
24
|
const spineWire = this.getSpineWire(this._path);
|
|
24
25
|
// Extract profile faces from extrudable
|
|
25
|
-
|
|
26
|
+
let profileFaces = pickedFaces ?? FaceMaker2.getRegions(this.extrudable.getGeometries(), plane, this.getDrill());
|
|
27
|
+
let inwardEdges;
|
|
28
|
+
let outwardEdges;
|
|
29
|
+
if (this.isThin()) {
|
|
30
|
+
const thinResult = ThinFaceMaker.make(this.extrudable.getGeometries(), plane, this._thin[0], this._thin[1]);
|
|
31
|
+
profileFaces = thinResult.faces;
|
|
32
|
+
inwardEdges = thinResult.inwardEdges;
|
|
33
|
+
outwardEdges = thinResult.outwardEdges;
|
|
34
|
+
}
|
|
26
35
|
if (profileFaces.length === 0) {
|
|
27
36
|
throw new Error("Could not extract profile faces from extrudable.");
|
|
28
37
|
}
|
|
@@ -32,7 +41,7 @@ export class Sweep extends ExtrudeBase {
|
|
|
32
41
|
// Classify faces using FirstShape/LastShape from the OC result
|
|
33
42
|
const startFaces = [];
|
|
34
43
|
const endFaces = [];
|
|
35
|
-
|
|
44
|
+
let sideFaces = [];
|
|
36
45
|
const firstShapeFromOC = sweepResult.firstShape;
|
|
37
46
|
const lastShapeFromOC = sweepResult.lastShape;
|
|
38
47
|
for (const shape of newShapes) {
|
|
@@ -49,9 +58,44 @@ export class Sweep extends ExtrudeBase {
|
|
|
49
58
|
}
|
|
50
59
|
}
|
|
51
60
|
}
|
|
61
|
+
let internalFaces = [];
|
|
62
|
+
let capFaces = [];
|
|
63
|
+
if (inwardEdges && inwardEdges.length > 0) {
|
|
64
|
+
const result = this.reclassifyThinFaces(sideFaces, startFaces, plane, inwardEdges, outwardEdges || []);
|
|
65
|
+
sideFaces = result.sideFaces;
|
|
66
|
+
internalFaces = result.internalFaces;
|
|
67
|
+
capFaces = result.capFaces;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const innerWireEdges = [];
|
|
71
|
+
for (const sf of startFaces) {
|
|
72
|
+
for (const wire of sf.getWires()) {
|
|
73
|
+
if (!wire.isCW(plane.normal)) {
|
|
74
|
+
for (const edge of wire.getEdges()) {
|
|
75
|
+
innerWireEdges.push(edge);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (innerWireEdges.length > 0) {
|
|
81
|
+
const remaining = [];
|
|
82
|
+
for (const f of sideFaces) {
|
|
83
|
+
const isInternal = f.getEdges().some(fe => innerWireEdges.some(iwe => fe.getShape().IsPartner(iwe.getShape())));
|
|
84
|
+
if (isInternal) {
|
|
85
|
+
internalFaces.push(f);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
remaining.push(f);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
sideFaces = remaining;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
52
94
|
this.setState('start-faces', startFaces);
|
|
53
95
|
this.setState('end-faces', endFaces);
|
|
54
96
|
this.setState('side-faces', sideFaces);
|
|
97
|
+
this.setState('internal-faces', internalFaces);
|
|
98
|
+
this.setState('cap-faces', capFaces);
|
|
55
99
|
// Remove consumed input shapes
|
|
56
100
|
this.extrudable.removeShapes(this);
|
|
57
101
|
this._path.removeShapes(this);
|
|
@@ -120,6 +164,7 @@ export class Sweep extends ExtrudeBase {
|
|
|
120
164
|
path: this._path.serialize(),
|
|
121
165
|
extrudable: this.extrudable.serialize(),
|
|
122
166
|
operationMode: this._operationMode !== 'add' ? this._operationMode : undefined,
|
|
167
|
+
thin: this._thin,
|
|
123
168
|
...this.serializePickFields(),
|
|
124
169
|
};
|
|
125
170
|
}
|
package/lib/dist/index.js
CHANGED
|
@@ -7,17 +7,22 @@ export function captureSourceLocation() {
|
|
|
7
7
|
}
|
|
8
8
|
const lines = stack.split('\n');
|
|
9
9
|
for (const frame of lines) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
filePath
|
|
16
|
-
|
|
10
|
+
// Match the Vite live-render virtual prefix first so the path regex
|
|
11
|
+
// does not accidentally match `r:/…` inside the word `render:`.
|
|
12
|
+
const virtualMatch = frame.match(/virtual:live-render:((?:[A-Za-z]:)?\/[^\s]+?\.fluid\.js):(\d+):(\d+)/);
|
|
13
|
+
if (virtualMatch) {
|
|
14
|
+
return {
|
|
15
|
+
filePath: virtualMatch[1],
|
|
16
|
+
line: parseInt(virtualMatch[2], 10),
|
|
17
|
+
column: parseInt(virtualMatch[3], 10),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const realMatch = frame.match(/((?:[A-Za-z]:)?\/[^\s]+?\.fluid\.js):(\d+):(\d+)/);
|
|
21
|
+
if (realMatch) {
|
|
17
22
|
return {
|
|
18
|
-
filePath,
|
|
19
|
-
line: parseInt(
|
|
20
|
-
column: parseInt(
|
|
23
|
+
filePath: realMatch[1],
|
|
24
|
+
line: parseInt(realMatch[2], 10),
|
|
25
|
+
column: parseInt(realMatch[3], 10),
|
|
21
26
|
};
|
|
22
27
|
}
|
|
23
28
|
}
|
|
@@ -3,7 +3,7 @@ import { PlaneObjectBase } from "../features/plane-renderable-base.js";
|
|
|
3
3
|
import { AxisObjectBase } from "../features/axis-renderable-base.js";
|
|
4
4
|
import { Sketch } from "../features/2d/sketch.js";
|
|
5
5
|
const meshBuilder = new MeshBuilder();
|
|
6
|
-
function renderSceneObject(obj, scene) {
|
|
6
|
+
function renderSceneObject(obj, scene, buildDurationMs) {
|
|
7
7
|
const hasError = !!obj.getError();
|
|
8
8
|
const sceneShapes = obj.getOwnShapes({ excludeMeta: false, excludeGuide: false });
|
|
9
9
|
const renderedSceneShapes = [];
|
|
@@ -52,6 +52,7 @@ function renderSceneObject(obj, scene) {
|
|
|
52
52
|
hasError,
|
|
53
53
|
errorMessage: obj.getError() || undefined,
|
|
54
54
|
sourceLocation: obj.getSourceLocation() || undefined,
|
|
55
|
+
buildDurationMs,
|
|
55
56
|
});
|
|
56
57
|
}
|
|
57
58
|
export function renderSceneRollback(scene, rollbackIndex) {
|
|
@@ -136,6 +137,7 @@ export function renderScene(scene) {
|
|
|
136
137
|
const sceneObjects = scene.getAllSceneObjects();
|
|
137
138
|
console.log("============ Rendering ==============", sceneObjects.length);
|
|
138
139
|
const skippedContainers = new Set();
|
|
140
|
+
const buildDurations = new Map();
|
|
139
141
|
for (const object of sceneObjects) {
|
|
140
142
|
// Skip descendants of cloned sketches — their edges are already
|
|
141
143
|
// computed by the parent sketch's clone-mode build.
|
|
@@ -148,6 +150,7 @@ export function renderScene(scene) {
|
|
|
148
150
|
const isCached = scene.isCached(object);
|
|
149
151
|
if (!isCached) {
|
|
150
152
|
object.clearError();
|
|
153
|
+
const buildStart = performance.now();
|
|
151
154
|
try {
|
|
152
155
|
object.build({
|
|
153
156
|
getSceneObjects() {
|
|
@@ -179,6 +182,7 @@ export function renderScene(scene) {
|
|
|
179
182
|
console.error(`Error building object ${object.getUniqueType()}:`, error);
|
|
180
183
|
object.setError(message);
|
|
181
184
|
}
|
|
185
|
+
buildDurations.set(object, performance.now() - buildStart);
|
|
182
186
|
}
|
|
183
187
|
// After building, mark cloned sketches so their children are skipped
|
|
184
188
|
if (object instanceof Sketch && object.getState('cloned-edges')) {
|
|
@@ -192,8 +196,28 @@ export function renderScene(scene) {
|
|
|
192
196
|
}
|
|
193
197
|
object.clean(scene.getPartScopedAllObjects(object));
|
|
194
198
|
}
|
|
199
|
+
// Roll up container durations: include own build time plus all descendants.
|
|
200
|
+
// Iterate in reverse so nested containers are aggregated before their parents.
|
|
201
|
+
for (let i = sceneObjects.length - 1; i >= 0; i--) {
|
|
202
|
+
const object = sceneObjects[i];
|
|
203
|
+
if (!object.isContainer()) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const own = buildDurations.get(object);
|
|
207
|
+
if (own === undefined) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
let total = own;
|
|
211
|
+
for (const child of scene.getChildren(object)) {
|
|
212
|
+
const childDuration = buildDurations.get(child);
|
|
213
|
+
if (childDuration !== undefined) {
|
|
214
|
+
total += childDuration;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
buildDurations.set(object, total);
|
|
218
|
+
}
|
|
195
219
|
for (const object of sceneObjects) {
|
|
196
|
-
renderSceneObject(object, scene);
|
|
220
|
+
renderSceneObject(object, scene, buildDurations.get(object));
|
|
197
221
|
}
|
|
198
222
|
const result = scene.getRenderedObjects();
|
|
199
223
|
console.table(result);
|
|
@@ -62,13 +62,13 @@ describe("copy linear", () => {
|
|
|
62
62
|
rect(20, 20);
|
|
63
63
|
});
|
|
64
64
|
const e = extrude(10).new();
|
|
65
|
-
// 4 copies over length 120 → offset = 120/4 =
|
|
65
|
+
// 4 copies over length 120 → offset = 120/(4-1) = 40
|
|
66
66
|
const c = copy("linear", "x", { count: 4, length: 120 }, e);
|
|
67
67
|
render();
|
|
68
68
|
const shapes = c.getShapes();
|
|
69
69
|
expect(shapes).toHaveLength(3);
|
|
70
70
|
const bbox = ShapeOps.getBoundingBox(shapes[0]);
|
|
71
|
-
expect(bbox.minX).toBeCloseTo(
|
|
71
|
+
expect(bbox.minX).toBeCloseTo(40, 0);
|
|
72
72
|
});
|
|
73
73
|
it("should create a 2D grid with multiple axes", () => {
|
|
74
74
|
sketch("xy", () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { setupOC, render } from "../setup.js";
|
|
3
|
+
import sketch from "../../core/sketch.js";
|
|
4
|
+
import plane from "../../core/plane.js";
|
|
5
|
+
import loft from "../../core/loft.js";
|
|
6
|
+
import extrude from "../../core/extrude.js";
|
|
7
|
+
import { rect, circle } from "../../core/2d/index.js";
|
|
8
|
+
import { ShapeOps } from "../../oc/shape-ops.js";
|
|
9
|
+
import { ShapeProps } from "../../oc/props.js";
|
|
10
|
+
import { EdgeQuery } from "../../oc/edge-query.js";
|
|
11
|
+
describe("thin loft", () => {
|
|
12
|
+
setupOC();
|
|
13
|
+
describe("closed profile - same size", () => {
|
|
14
|
+
it("should create a thin-walled loft between two rects", () => {
|
|
15
|
+
const s1 = sketch("xy", () => {
|
|
16
|
+
rect(40, 40);
|
|
17
|
+
});
|
|
18
|
+
const s2 = sketch(plane("xy", { offset: 30 }), () => {
|
|
19
|
+
rect(40, 40);
|
|
20
|
+
});
|
|
21
|
+
const l = loft(s1, s2).thin(5);
|
|
22
|
+
render();
|
|
23
|
+
const shapes = l.getShapes();
|
|
24
|
+
expect(shapes).toHaveLength(1);
|
|
25
|
+
expect(shapes[0].getType()).toBe("solid");
|
|
26
|
+
const bbox = ShapeOps.getBoundingBox(shapes[0]);
|
|
27
|
+
expect(bbox.maxZ - bbox.minZ).toBeCloseTo(30, 0);
|
|
28
|
+
});
|
|
29
|
+
it("should have less volume than a solid loft", () => {
|
|
30
|
+
const s1 = sketch("xy", () => {
|
|
31
|
+
rect(60, 60);
|
|
32
|
+
});
|
|
33
|
+
const s2 = sketch(plane("xy", { offset: 40 }), () => {
|
|
34
|
+
rect(60, 60);
|
|
35
|
+
});
|
|
36
|
+
const solidS1 = sketch("xy", () => {
|
|
37
|
+
rect(60, 60);
|
|
38
|
+
});
|
|
39
|
+
const solidS2 = sketch(plane("xy", { offset: 40 }), () => {
|
|
40
|
+
rect(60, 60);
|
|
41
|
+
});
|
|
42
|
+
const thinLoft = loft(s1, s2).thin(3).new();
|
|
43
|
+
const solidLoft = loft(solidS1, solidS2).new();
|
|
44
|
+
render();
|
|
45
|
+
const thinVolume = ShapeProps.getProperties(thinLoft.getShapes()[0].getShape()).volumeMm3;
|
|
46
|
+
const solidVolume = ShapeProps.getProperties(solidLoft.getShapes()[0].getShape()).volumeMm3;
|
|
47
|
+
expect(thinVolume).toBeLessThan(solidVolume * 0.9);
|
|
48
|
+
expect(thinVolume).toBeGreaterThan(0);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe("closed profile - different sizes", () => {
|
|
52
|
+
it("should create a thin-walled tapered loft", () => {
|
|
53
|
+
const s1 = sketch("xy", () => {
|
|
54
|
+
rect(40, 40);
|
|
55
|
+
});
|
|
56
|
+
const s2 = sketch(plane("xy", { offset: 30 }), () => {
|
|
57
|
+
rect(20, 20);
|
|
58
|
+
});
|
|
59
|
+
const l = loft(s1, s2).thin(3);
|
|
60
|
+
render();
|
|
61
|
+
const shapes = l.getShapes();
|
|
62
|
+
expect(shapes).toHaveLength(1);
|
|
63
|
+
expect(shapes[0].getType()).toBe("solid");
|
|
64
|
+
});
|
|
65
|
+
it("should create a thin-walled loft between circle and rect", () => {
|
|
66
|
+
const s1 = sketch("xy", () => {
|
|
67
|
+
circle(20);
|
|
68
|
+
});
|
|
69
|
+
const s2 = sketch(plane("xy", { offset: 30 }), () => {
|
|
70
|
+
rect(30, 30);
|
|
71
|
+
});
|
|
72
|
+
const l = loft(s1, s2).thin(3);
|
|
73
|
+
render();
|
|
74
|
+
const shapes = l.getShapes();
|
|
75
|
+
expect(shapes).toHaveLength(1);
|
|
76
|
+
expect(shapes[0].getType()).toBe("solid");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe("dual offset", () => {
|
|
80
|
+
it("should create a thin-walled loft with dual offset", () => {
|
|
81
|
+
const s1 = sketch("xy", () => {
|
|
82
|
+
circle(40);
|
|
83
|
+
});
|
|
84
|
+
const s2 = sketch(plane("xy", { offset: 30 }), () => {
|
|
85
|
+
circle(40);
|
|
86
|
+
});
|
|
87
|
+
const l = loft(s1, s2).thin(5, -3).new();
|
|
88
|
+
render();
|
|
89
|
+
const shapes = l.getShapes();
|
|
90
|
+
expect(shapes).toHaveLength(1);
|
|
91
|
+
expect(shapes[0].getType()).toBe("solid");
|
|
92
|
+
const edges = shapes[0].getSubShapes('edge');
|
|
93
|
+
const circleEdges = edges.filter(e => EdgeQuery.isCircleEdge(e));
|
|
94
|
+
expect(circleEdges).toHaveLength(4);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe("face classification", () => {
|
|
98
|
+
it("should classify start and end faces", () => {
|
|
99
|
+
const s1 = sketch("xy", () => {
|
|
100
|
+
rect(40, 40);
|
|
101
|
+
});
|
|
102
|
+
const s2 = sketch(plane("xy", { offset: 30 }), () => {
|
|
103
|
+
rect(40, 40);
|
|
104
|
+
});
|
|
105
|
+
const l = loft(s1, s2).thin(5);
|
|
106
|
+
render();
|
|
107
|
+
const startFaces = l.getState('start-faces');
|
|
108
|
+
const endFaces = l.getState('end-faces');
|
|
109
|
+
const sideFaces = l.getState('side-faces');
|
|
110
|
+
expect(startFaces.length).toBeGreaterThan(0);
|
|
111
|
+
expect(endFaces.length).toBeGreaterThan(0);
|
|
112
|
+
expect(sideFaces.length).toBeGreaterThan(0);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe("three profiles", () => {
|
|
116
|
+
it("should create a thin-walled loft through three profiles", () => {
|
|
117
|
+
const s1 = sketch("xy", () => {
|
|
118
|
+
rect(40, 40);
|
|
119
|
+
});
|
|
120
|
+
const s2 = sketch(plane("xy", { offset: 20 }), () => {
|
|
121
|
+
rect(30, 30);
|
|
122
|
+
});
|
|
123
|
+
const s3 = sketch(plane("xy", { offset: 40 }), () => {
|
|
124
|
+
rect(40, 40);
|
|
125
|
+
});
|
|
126
|
+
const l = loft(s1, s2, s3).thin(3);
|
|
127
|
+
render();
|
|
128
|
+
const shapes = l.getShapes();
|
|
129
|
+
expect(shapes).toHaveLength(1);
|
|
130
|
+
expect(shapes[0].getType()).toBe("solid");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe("remove mode", () => {
|
|
134
|
+
it("should cut a thin-walled loft from existing geometry", () => {
|
|
135
|
+
sketch("xy", () => {
|
|
136
|
+
rect(200, 200);
|
|
137
|
+
});
|
|
138
|
+
extrude(50);
|
|
139
|
+
const s1 = sketch("xy", () => {
|
|
140
|
+
rect(40, 40);
|
|
141
|
+
});
|
|
142
|
+
const s2 = sketch(plane("xy", { offset: 40 }), () => {
|
|
143
|
+
rect(40, 40);
|
|
144
|
+
});
|
|
145
|
+
const l = loft(s1, s2).thin(5).remove();
|
|
146
|
+
render();
|
|
147
|
+
const shapes = l.getShapes();
|
|
148
|
+
expect(shapes.length).toBeGreaterThan(0);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|