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
|
@@ -4,6 +4,7 @@ import { AxisObjectBase } from "../features/axis-renderable-base.js";
|
|
|
4
4
|
import { Sketch } from "../features/2d/sketch.js";
|
|
5
5
|
import { transformMeshes } from "./mesh-transform.js";
|
|
6
6
|
import { ShapeOps } from "../oc/shape-ops.js";
|
|
7
|
+
import { Profiler } from "../common/profiler.js";
|
|
7
8
|
export class SceneRenderer {
|
|
8
9
|
meshBuilder = new MeshBuilder();
|
|
9
10
|
render(scene) {
|
|
@@ -11,6 +12,7 @@ export class SceneRenderer {
|
|
|
11
12
|
console.log("============ Rendering ==============", sceneObjects.length);
|
|
12
13
|
const skippedContainers = new Set();
|
|
13
14
|
const buildDurations = new Map();
|
|
15
|
+
const profilers = new Map();
|
|
14
16
|
for (const object of sceneObjects) {
|
|
15
17
|
// Skip descendants of cloned sketches — their edges are already
|
|
16
18
|
// computed by the parent sketch's clone-mode build.
|
|
@@ -21,7 +23,9 @@ export class SceneRenderer {
|
|
|
21
23
|
}
|
|
22
24
|
console.log("Rendering object:", object.getUniqueType());
|
|
23
25
|
if (!scene.isCached(object)) {
|
|
24
|
-
|
|
26
|
+
const result = this.buildObject(object, scene);
|
|
27
|
+
buildDurations.set(object, result.totalMs);
|
|
28
|
+
profilers.set(object, result.profiler);
|
|
25
29
|
}
|
|
26
30
|
// After building, mark cloned sketches so their children are skipped —
|
|
27
31
|
// the sketch's build() already populated them with transformed shapes.
|
|
@@ -35,9 +39,20 @@ export class SceneRenderer {
|
|
|
35
39
|
}
|
|
36
40
|
object.clean(scene.getPartScopedAllObjects(object));
|
|
37
41
|
}
|
|
42
|
+
const prepared = new Map();
|
|
43
|
+
for (const object of sceneObjects) {
|
|
44
|
+
const profiler = profilers.get(object);
|
|
45
|
+
const start = performance.now();
|
|
46
|
+
prepared.set(object, this.prepareRenderedShapes(object, profiler));
|
|
47
|
+
const meshMs = performance.now() - start;
|
|
48
|
+
const existing = buildDurations.get(object);
|
|
49
|
+
if (existing !== undefined) {
|
|
50
|
+
buildDurations.set(object, existing + meshMs);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
38
53
|
this.aggregateContainerDurations(sceneObjects, scene, buildDurations);
|
|
39
54
|
for (const object of sceneObjects) {
|
|
40
|
-
this.
|
|
55
|
+
this.emitRenderObject(object, scene, prepared.get(object) ?? { renderedSceneShapes: [], ownShapeCount: 0 }, buildDurations.get(object), profilers.get(object));
|
|
41
56
|
}
|
|
42
57
|
return scene;
|
|
43
58
|
}
|
|
@@ -50,7 +65,7 @@ export class SceneRenderer {
|
|
|
50
65
|
}
|
|
51
66
|
scene.clearRenderedObjects();
|
|
52
67
|
for (const obj of allObjects) {
|
|
53
|
-
if (!scope.has(obj)) {
|
|
68
|
+
if (!scope.has(obj) || obj.isLazy()) {
|
|
54
69
|
this.emitRendered(obj, scene, {
|
|
55
70
|
sceneShapes: [],
|
|
56
71
|
visible: false,
|
|
@@ -59,7 +74,7 @@ export class SceneRenderer {
|
|
|
59
74
|
});
|
|
60
75
|
continue;
|
|
61
76
|
}
|
|
62
|
-
const sceneShapes = obj.getOwnShapes({ excludeMeta: false }, scope);
|
|
77
|
+
const sceneShapes = obj.getOwnShapes({ excludeMeta: false, excludeGuide: false }, scope);
|
|
63
78
|
const renderedSceneShapes = sceneShapes.map(s => this.toRenderedShape(s));
|
|
64
79
|
this.emitRendered(obj, scene, {
|
|
65
80
|
sceneShapes: renderedSceneShapes,
|
|
@@ -72,40 +87,53 @@ export class SceneRenderer {
|
|
|
72
87
|
console.table(result);
|
|
73
88
|
return scene;
|
|
74
89
|
}
|
|
75
|
-
|
|
76
|
-
const sceneShapes = obj.getOwnShapes({ excludeMeta: false, excludeGuide: false });
|
|
90
|
+
prepareRenderedShapes(obj, profiler) {
|
|
77
91
|
const renderedSceneShapes = [];
|
|
92
|
+
if (obj.isLazy()) {
|
|
93
|
+
return { renderedSceneShapes, ownShapeCount: 0 };
|
|
94
|
+
}
|
|
78
95
|
try {
|
|
96
|
+
const sceneShapes = obj.getOwnShapes({ excludeMeta: false, excludeGuide: false });
|
|
79
97
|
if (sceneShapes.length) {
|
|
80
98
|
console.log(` - Scene shapes: ${sceneShapes.length}`);
|
|
81
99
|
for (const shape of sceneShapes) {
|
|
82
|
-
renderedSceneShapes.push(this.toRenderedShape(shape));
|
|
100
|
+
renderedSceneShapes.push(this.toRenderedShape(shape, profiler));
|
|
83
101
|
}
|
|
84
102
|
}
|
|
85
|
-
|
|
86
|
-
this.emitRendered(obj, scene, {
|
|
87
|
-
sceneShapes: renderedSceneShapes,
|
|
88
|
-
visible: this.computeVisibility(obj, scene, sceneShapes.length),
|
|
89
|
-
hasError: !!errorMessage,
|
|
90
|
-
errorMessage: errorMessage || undefined,
|
|
91
|
-
buildDurationMs,
|
|
92
|
-
});
|
|
103
|
+
return { renderedSceneShapes, ownShapeCount: sceneShapes.length };
|
|
93
104
|
}
|
|
94
105
|
catch (error) {
|
|
95
106
|
const message = error instanceof Error ? error.message : String(error);
|
|
96
107
|
console.error(`Error rendering object ${obj.getUniqueType()}:`, error);
|
|
108
|
+
return { renderedSceneShapes, ownShapeCount: renderedSceneShapes.length, prepError: message };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
emitRenderObject(obj, scene, prepared, buildDurationMs, profiler) {
|
|
112
|
+
if (prepared.prepError) {
|
|
97
113
|
this.emitRendered(obj, scene, {
|
|
98
|
-
sceneShapes: renderedSceneShapes,
|
|
114
|
+
sceneShapes: prepared.renderedSceneShapes,
|
|
99
115
|
visible: false,
|
|
100
116
|
hasError: true,
|
|
101
|
-
errorMessage:
|
|
117
|
+
errorMessage: prepared.prepError,
|
|
102
118
|
buildDurationMs,
|
|
119
|
+
profiler,
|
|
103
120
|
});
|
|
121
|
+
return;
|
|
104
122
|
}
|
|
123
|
+
const errorMessage = obj.getError();
|
|
124
|
+
this.emitRendered(obj, scene, {
|
|
125
|
+
sceneShapes: prepared.renderedSceneShapes,
|
|
126
|
+
visible: this.computeVisibility(obj, scene, prepared.ownShapeCount),
|
|
127
|
+
hasError: !!errorMessage,
|
|
128
|
+
errorMessage: errorMessage || undefined,
|
|
129
|
+
buildDurationMs,
|
|
130
|
+
profiler,
|
|
131
|
+
});
|
|
105
132
|
}
|
|
106
133
|
buildObject(object, scene) {
|
|
107
134
|
object.clearError();
|
|
108
135
|
const start = performance.now();
|
|
136
|
+
const profiler = new Profiler();
|
|
109
137
|
try {
|
|
110
138
|
object.build({
|
|
111
139
|
getSceneObjects: () => scene.getPartScopedObjectsUpTo(object),
|
|
@@ -122,6 +150,7 @@ export class SceneRenderer {
|
|
|
122
150
|
}
|
|
123
151
|
return null;
|
|
124
152
|
},
|
|
153
|
+
getProfiler: () => profiler,
|
|
125
154
|
});
|
|
126
155
|
const appliedTransform = object.getAppliedTransform();
|
|
127
156
|
if (appliedTransform && !object.isContainer()) {
|
|
@@ -136,33 +165,40 @@ export class SceneRenderer {
|
|
|
136
165
|
console.error(`Error building object ${object.getUniqueType()}:`, error);
|
|
137
166
|
object.setError(message);
|
|
138
167
|
}
|
|
139
|
-
|
|
168
|
+
const totalMs = performance.now() - start;
|
|
169
|
+
return { totalMs, profiler };
|
|
140
170
|
}
|
|
141
|
-
getOrBuildMeshes(shape) {
|
|
171
|
+
getOrBuildMeshes(shape, profiler) {
|
|
142
172
|
const existing = shape.getMeshes();
|
|
143
173
|
if (existing) {
|
|
144
174
|
return existing;
|
|
145
175
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
sourceMeshes =
|
|
152
|
-
|
|
176
|
+
profiler?.start("Triangulation");
|
|
177
|
+
try {
|
|
178
|
+
let meshes;
|
|
179
|
+
const meshSource = shape.getMeshSource();
|
|
180
|
+
if (meshSource) {
|
|
181
|
+
let sourceMeshes = meshSource.shape.getMeshes();
|
|
182
|
+
if (!sourceMeshes) {
|
|
183
|
+
sourceMeshes = this.meshBuilder.build(meshSource.shape);
|
|
184
|
+
meshSource.shape.setMeshes(sourceMeshes);
|
|
185
|
+
}
|
|
186
|
+
meshes = sourceMeshes ? transformMeshes(sourceMeshes, meshSource.matrix) : this.meshBuilder.build(shape);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
meshes = this.meshBuilder.build(shape);
|
|
153
190
|
}
|
|
154
|
-
|
|
191
|
+
shape.setMeshes(meshes);
|
|
192
|
+
return meshes;
|
|
155
193
|
}
|
|
156
|
-
|
|
157
|
-
|
|
194
|
+
finally {
|
|
195
|
+
profiler?.end("Triangulation");
|
|
158
196
|
}
|
|
159
|
-
shape.setMeshes(meshes);
|
|
160
|
-
return meshes;
|
|
161
197
|
}
|
|
162
|
-
toRenderedShape(shape) {
|
|
198
|
+
toRenderedShape(shape, profiler) {
|
|
163
199
|
return {
|
|
164
200
|
shapeId: shape.id,
|
|
165
|
-
meshes: this.getOrBuildMeshes(shape),
|
|
201
|
+
meshes: this.getOrBuildMeshes(shape, profiler),
|
|
166
202
|
shapeType: shape.getType(),
|
|
167
203
|
isMetaShape: shape.isMetaShape() || undefined,
|
|
168
204
|
isGuide: shape.isGuideShape() || undefined,
|
|
@@ -209,6 +245,8 @@ export class SceneRenderer {
|
|
|
209
245
|
}
|
|
210
246
|
}
|
|
211
247
|
emitRendered(obj, scene, opts) {
|
|
248
|
+
const categories = opts.profiler?.getCategories();
|
|
249
|
+
const profileCategories = categories && categories.length > 0 ? categories : undefined;
|
|
212
250
|
const rendered = {
|
|
213
251
|
id: obj.id,
|
|
214
252
|
name: obj.getName(),
|
|
@@ -224,6 +262,7 @@ export class SceneRenderer {
|
|
|
224
262
|
errorMessage: opts.errorMessage,
|
|
225
263
|
sourceLocation: obj.getSourceLocation() || undefined,
|
|
226
264
|
buildDurationMs: opts.buildDurationMs,
|
|
265
|
+
profileCategories,
|
|
227
266
|
};
|
|
228
267
|
scene.addRenderedObject(obj, rendered);
|
|
229
268
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { setupOC, render } from "../../setup.js";
|
|
3
|
+
import sketch from "../../../core/sketch.js";
|
|
4
|
+
import extrude from "../../../core/extrude.js";
|
|
5
|
+
import { move, hMove, back, rect } from "../../../core/2d/index.js";
|
|
6
|
+
import { ShapeOps } from "../../../oc/shape-ops.js";
|
|
7
|
+
describe("back", () => {
|
|
8
|
+
setupOC();
|
|
9
|
+
it("should revert cursor to the previous position", () => {
|
|
10
|
+
sketch("xy", () => {
|
|
11
|
+
move([30, 20]);
|
|
12
|
+
move([100, 100]);
|
|
13
|
+
back();
|
|
14
|
+
rect(10, 10);
|
|
15
|
+
});
|
|
16
|
+
const e = extrude(5);
|
|
17
|
+
render();
|
|
18
|
+
const bbox = ShapeOps.getBoundingBox(e.getShapes()[0]);
|
|
19
|
+
expect(bbox.minX).toBeCloseTo(30, 0);
|
|
20
|
+
expect(bbox.minY).toBeCloseTo(20, 0);
|
|
21
|
+
});
|
|
22
|
+
it("should revert N positions back when given a count", () => {
|
|
23
|
+
sketch("xy", () => {
|
|
24
|
+
hMove(10);
|
|
25
|
+
hMove(20);
|
|
26
|
+
hMove(40);
|
|
27
|
+
back(2);
|
|
28
|
+
rect(5, 5);
|
|
29
|
+
});
|
|
30
|
+
const e = extrude(5);
|
|
31
|
+
render();
|
|
32
|
+
const bbox = ShapeOps.getBoundingBox(e.getShapes()[0]);
|
|
33
|
+
expect(bbox.minX).toBeCloseTo(10, 0);
|
|
34
|
+
expect(bbox.minY).toBeCloseTo(0, 0);
|
|
35
|
+
});
|
|
36
|
+
it("should fall back to sketch start point when count exceeds history", () => {
|
|
37
|
+
sketch("xy", () => {
|
|
38
|
+
hMove(50);
|
|
39
|
+
back(99);
|
|
40
|
+
rect(10, 10);
|
|
41
|
+
});
|
|
42
|
+
const e = extrude(5);
|
|
43
|
+
render();
|
|
44
|
+
const bbox = ShapeOps.getBoundingBox(e.getShapes()[0]);
|
|
45
|
+
expect(bbox.minX).toBeCloseTo(0, 0);
|
|
46
|
+
expect(bbox.minY).toBeCloseTo(0, 0);
|
|
47
|
+
});
|
|
48
|
+
it("should toggle on consecutive back() calls", () => {
|
|
49
|
+
sketch("xy", () => {
|
|
50
|
+
hMove(40);
|
|
51
|
+
back();
|
|
52
|
+
back();
|
|
53
|
+
rect(10, 10);
|
|
54
|
+
});
|
|
55
|
+
const e = extrude(5);
|
|
56
|
+
render();
|
|
57
|
+
const bbox = ShapeOps.getBoundingBox(e.getShapes()[0]);
|
|
58
|
+
expect(bbox.minX).toBeCloseTo(40, 0);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -53,7 +53,7 @@ describe("circle", () => {
|
|
|
53
53
|
});
|
|
54
54
|
describe("standalone with targetPlane", () => {
|
|
55
55
|
it("should create a circle on a specific plane", () => {
|
|
56
|
-
circle(
|
|
56
|
+
circle("xy", 60);
|
|
57
57
|
const e = extrude(10);
|
|
58
58
|
render();
|
|
59
59
|
const solid = e.getShapes()[0];
|
|
@@ -89,7 +89,7 @@ describe("constrained geometries", () => {
|
|
|
89
89
|
describe("tCircle between circle and line", () => {
|
|
90
90
|
it("should create a tangent circle to a circle and a line", () => {
|
|
91
91
|
const s = sketch("xy", () => {
|
|
92
|
-
const l = aLine(
|
|
92
|
+
const l = aLine(45, 150);
|
|
93
93
|
const c = circle([100, 0], 60);
|
|
94
94
|
tCircle(c, l, 100).guide();
|
|
95
95
|
});
|
|
@@ -101,7 +101,7 @@ describe("constrained geometries", () => {
|
|
|
101
101
|
describe("tCircle between two lines", () => {
|
|
102
102
|
it("should create a tangent circle between two lines", () => {
|
|
103
103
|
const s = sketch("xy", () => {
|
|
104
|
-
const l1 = aLine(
|
|
104
|
+
const l1 = aLine(45, 300);
|
|
105
105
|
move([-50, 0]);
|
|
106
106
|
const l2 = vLine(300);
|
|
107
107
|
tCircle(l1, l2, 200, true).guide();
|
|
@@ -136,7 +136,7 @@ describe("constrained geometries", () => {
|
|
|
136
136
|
describe("tArc between circle and line", () => {
|
|
137
137
|
it("should create a tangent arc to a circle and a line", () => {
|
|
138
138
|
const s = sketch("xy", () => {
|
|
139
|
-
const l = aLine(
|
|
139
|
+
const l = aLine(45, 150);
|
|
140
140
|
const c = circle([100, 0], 40);
|
|
141
141
|
tArc(c, l, 50).guide();
|
|
142
142
|
});
|
|
@@ -148,7 +148,7 @@ describe("constrained geometries", () => {
|
|
|
148
148
|
describe("tArc between two lines", () => {
|
|
149
149
|
it("should create a fillet arc between two lines", () => {
|
|
150
150
|
const s = sketch("xy", () => {
|
|
151
|
-
const l1 = aLine(
|
|
151
|
+
const l1 = aLine(45, 150);
|
|
152
152
|
move([-50, 0]);
|
|
153
153
|
const l2 = vLine(100);
|
|
154
154
|
tArc(l1, l2, 50).guide();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { setupOC, render } from "../../setup.js";
|
|
3
|
+
import sketch from "../../../core/sketch.js";
|
|
4
|
+
import extrude from "../../../core/extrude.js";
|
|
5
|
+
import { ellipse } from "../../../core/2d/index.js";
|
|
6
|
+
import { ShapeOps } from "../../../oc/shape-ops.js";
|
|
7
|
+
describe("ellipse", () => {
|
|
8
|
+
setupOC();
|
|
9
|
+
describe("in sketch", () => {
|
|
10
|
+
it("creates an ellipse with rx along X and ry along Y", () => {
|
|
11
|
+
sketch("xy", () => {
|
|
12
|
+
ellipse(50, 30);
|
|
13
|
+
});
|
|
14
|
+
const e = extrude(10);
|
|
15
|
+
render();
|
|
16
|
+
const solid = e.getShapes()[0];
|
|
17
|
+
const bbox = ShapeOps.getBoundingBox(solid);
|
|
18
|
+
expect(bbox.maxX - bbox.minX).toBeCloseTo(100, 0);
|
|
19
|
+
expect(bbox.maxY - bbox.minY).toBeCloseTo(60, 0);
|
|
20
|
+
});
|
|
21
|
+
it("handles ry > rx (axis-swap path)", () => {
|
|
22
|
+
sketch("xy", () => {
|
|
23
|
+
ellipse(30, 50);
|
|
24
|
+
});
|
|
25
|
+
const e = extrude(10);
|
|
26
|
+
render();
|
|
27
|
+
const solid = e.getShapes()[0];
|
|
28
|
+
const bbox = ShapeOps.getBoundingBox(solid);
|
|
29
|
+
expect(bbox.maxX - bbox.minX).toBeCloseTo(60, 0);
|
|
30
|
+
expect(bbox.maxY - bbox.minY).toBeCloseTo(100, 0);
|
|
31
|
+
});
|
|
32
|
+
it("creates an ellipse at a given center", () => {
|
|
33
|
+
sketch("xy", () => {
|
|
34
|
+
ellipse([50, 30], 40, 20);
|
|
35
|
+
});
|
|
36
|
+
const e = extrude(10);
|
|
37
|
+
render();
|
|
38
|
+
const bbox = ShapeOps.getBoundingBox(e.getShapes()[0]);
|
|
39
|
+
expect(bbox.centerX).toBeCloseTo(50, 0);
|
|
40
|
+
expect(bbox.centerY).toBeCloseTo(30, 0);
|
|
41
|
+
expect(bbox.maxX - bbox.minX).toBeCloseTo(80, 0);
|
|
42
|
+
expect(bbox.maxY - bbox.minY).toBeCloseTo(40, 0);
|
|
43
|
+
});
|
|
44
|
+
it("falls through to a circle when rx == ry", () => {
|
|
45
|
+
sketch("xy", () => {
|
|
46
|
+
ellipse(25, 25);
|
|
47
|
+
});
|
|
48
|
+
const e = extrude(10);
|
|
49
|
+
render();
|
|
50
|
+
const solid = e.getShapes()[0];
|
|
51
|
+
const bbox = ShapeOps.getBoundingBox(solid);
|
|
52
|
+
expect(bbox.maxX - bbox.minX).toBeCloseTo(50, 0);
|
|
53
|
+
expect(bbox.maxY - bbox.minY).toBeCloseTo(50, 0);
|
|
54
|
+
});
|
|
55
|
+
it("rejects zero or negative radii", () => {
|
|
56
|
+
let zeroEllipse;
|
|
57
|
+
sketch("xy", () => {
|
|
58
|
+
zeroEllipse = ellipse(0, 30);
|
|
59
|
+
});
|
|
60
|
+
render();
|
|
61
|
+
expect(zeroEllipse?.getError()).toMatch(/positive/i);
|
|
62
|
+
let negEllipse;
|
|
63
|
+
sketch("xy", () => {
|
|
64
|
+
negEllipse = ellipse(-10, 5);
|
|
65
|
+
});
|
|
66
|
+
render();
|
|
67
|
+
expect(negEllipse?.getError()).toMatch(/positive/i);
|
|
68
|
+
});
|
|
69
|
+
it("throws when given a plane inside a sketch", () => {
|
|
70
|
+
expect(() => {
|
|
71
|
+
sketch("xy", () => {
|
|
72
|
+
ellipse("xy", 30, 20);
|
|
73
|
+
});
|
|
74
|
+
render();
|
|
75
|
+
}).toThrow();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe("standalone with targetPlane", () => {
|
|
79
|
+
it("creates an ellipse on a specific plane", () => {
|
|
80
|
+
ellipse("xy", 60, 40);
|
|
81
|
+
const e = extrude(10);
|
|
82
|
+
render();
|
|
83
|
+
const solid = e.getShapes()[0];
|
|
84
|
+
const bbox = ShapeOps.getBoundingBox(solid);
|
|
85
|
+
expect(bbox.maxX - bbox.minX).toBeCloseTo(120, 0);
|
|
86
|
+
expect(bbox.maxY - bbox.minY).toBeCloseTo(80, 0);
|
|
87
|
+
});
|
|
88
|
+
it("creates an ellipse on a plane at a given center", () => {
|
|
89
|
+
ellipse("xy", [10, 20], 30, 15);
|
|
90
|
+
const e = extrude(10);
|
|
91
|
+
render();
|
|
92
|
+
const solid = e.getShapes()[0];
|
|
93
|
+
const bbox = ShapeOps.getBoundingBox(solid);
|
|
94
|
+
expect(bbox.centerX).toBeCloseTo(10, 0);
|
|
95
|
+
expect(bbox.centerY).toBeCloseTo(20, 0);
|
|
96
|
+
expect(bbox.maxX - bbox.minX).toBeCloseTo(60, 0);
|
|
97
|
+
expect(bbox.maxY - bbox.minY).toBeCloseTo(30, 0);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -2,8 +2,9 @@ import { describe, it, expect } from "vitest";
|
|
|
2
2
|
import { setupOC, render } from "../../setup.js";
|
|
3
3
|
import sketch from "../../../core/sketch.js";
|
|
4
4
|
import extrude from "../../../core/extrude.js";
|
|
5
|
-
import { line, hLine, vLine, aLine } from "../../../core/2d/index.js";
|
|
5
|
+
import { line, hLine, vLine, aLine, circle, move } from "../../../core/2d/index.js";
|
|
6
6
|
import { ShapeOps } from "../../../oc/shape-ops.js";
|
|
7
|
+
import { Edge } from "../../../common/edge.js";
|
|
7
8
|
describe("line functions", () => {
|
|
8
9
|
setupOC();
|
|
9
10
|
describe("line", () => {
|
|
@@ -32,7 +33,7 @@ describe("line functions", () => {
|
|
|
32
33
|
expect(bbox.maxY - bbox.minY).toBeCloseTo(40, 0);
|
|
33
34
|
});
|
|
34
35
|
it("should support standalone mode with targetPlane", () => {
|
|
35
|
-
hLine(
|
|
36
|
+
hLine("xy", 50);
|
|
36
37
|
render();
|
|
37
38
|
// Just verify no error — standalone line doesn't form a closed shape
|
|
38
39
|
});
|
|
@@ -57,7 +58,7 @@ describe("line functions", () => {
|
|
|
57
58
|
it("should create an angled line", () => {
|
|
58
59
|
sketch("xy", () => {
|
|
59
60
|
hLine(50);
|
|
60
|
-
aLine(
|
|
61
|
+
aLine(90, 50);
|
|
61
62
|
hLine(-50);
|
|
62
63
|
vLine(-50);
|
|
63
64
|
});
|
|
@@ -66,6 +67,91 @@ describe("line functions", () => {
|
|
|
66
67
|
expect(e.getShapes()).toHaveLength(1);
|
|
67
68
|
});
|
|
68
69
|
});
|
|
70
|
+
describe("hLine to target geometry", () => {
|
|
71
|
+
it("should end at the nearest intersection with a circle", () => {
|
|
72
|
+
let h;
|
|
73
|
+
sketch("xy", () => {
|
|
74
|
+
const c = circle([100, 0], 50);
|
|
75
|
+
h = hLine([0, 0], c);
|
|
76
|
+
});
|
|
77
|
+
render();
|
|
78
|
+
const edges = h.getOwnShapes().filter((sh) => sh instanceof Edge);
|
|
79
|
+
expect(edges).toHaveLength(1);
|
|
80
|
+
const endPoint = edges[0].getLastVertex().toPoint();
|
|
81
|
+
// Circle at (100, 0) with diameter 50 → radius 25 → near edge at x=75
|
|
82
|
+
expect(endPoint.x).toBeCloseTo(75, 1);
|
|
83
|
+
expect(endPoint.y).toBeCloseTo(0, 1);
|
|
84
|
+
});
|
|
85
|
+
it("should pick nearest intersection when target is behind the start", () => {
|
|
86
|
+
let h;
|
|
87
|
+
sketch("xy", () => {
|
|
88
|
+
const c = circle([-100, 0], 50);
|
|
89
|
+
h = hLine([0, 0], c);
|
|
90
|
+
});
|
|
91
|
+
render();
|
|
92
|
+
const edges = h.getOwnShapes().filter((sh) => sh instanceof Edge);
|
|
93
|
+
const endPoint = edges[0].getLastVertex().toPoint();
|
|
94
|
+
// Circle at (-100, 0) with diameter 50 → near edge at x=-75
|
|
95
|
+
expect(endPoint.x).toBeCloseTo(-75, 1);
|
|
96
|
+
expect(endPoint.y).toBeCloseTo(0, 1);
|
|
97
|
+
});
|
|
98
|
+
it("should record an error when there is no intersection", () => {
|
|
99
|
+
let h;
|
|
100
|
+
sketch("xy", () => {
|
|
101
|
+
const c = circle([0, 100], 20);
|
|
102
|
+
h = hLine([0, 0], c);
|
|
103
|
+
});
|
|
104
|
+
render();
|
|
105
|
+
expect(h.getError()).toMatch(/does not intersect/);
|
|
106
|
+
});
|
|
107
|
+
it("should record an error when .centered() is combined with a target", () => {
|
|
108
|
+
let h;
|
|
109
|
+
sketch("xy", () => {
|
|
110
|
+
const c = circle([100, 0], 40);
|
|
111
|
+
h = hLine([0, 0], c).centered();
|
|
112
|
+
});
|
|
113
|
+
render();
|
|
114
|
+
expect(h.getError()).toMatch(/centered/);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe("vLine to target geometry", () => {
|
|
118
|
+
it("should end at the nearest intersection with a circle above", () => {
|
|
119
|
+
let v;
|
|
120
|
+
sketch("xy", () => {
|
|
121
|
+
const c = circle([0, 100], 50);
|
|
122
|
+
v = vLine([0, 0], c);
|
|
123
|
+
});
|
|
124
|
+
render();
|
|
125
|
+
const edges = v.getOwnShapes().filter((sh) => sh instanceof Edge);
|
|
126
|
+
const endPoint = edges[0].getLastVertex().toPoint();
|
|
127
|
+
expect(endPoint.x).toBeCloseTo(0, 1);
|
|
128
|
+
expect(endPoint.y).toBeCloseTo(75, 1);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe("aLine to target geometry", () => {
|
|
132
|
+
it("should end where the angled line meets a horizontal line", () => {
|
|
133
|
+
let a;
|
|
134
|
+
sketch("xy", () => {
|
|
135
|
+
// A horizontal segment at y = 50 (drawn as guide for intersection).
|
|
136
|
+
// Use hLine starting at (-100, 50) with length 200 so it spans x ∈ [-100, 100].
|
|
137
|
+
const h = hLine([-100, 50], 200);
|
|
138
|
+
// Now place a 45° line starting at the origin; previous tangent is (1,0)
|
|
139
|
+
// (left over from h's hLine). Rotated 45° CCW that's direction (√2/2, √2/2).
|
|
140
|
+
// Starting from (100, 50)? Actually current position after h is (100, 50).
|
|
141
|
+
// We want aLine at angle 45° from current direction (1,0) rotated by 45°
|
|
142
|
+
// → direction (√2/2, √2/2). Starting from (100, 50), going at 45° → never
|
|
143
|
+
// hits the segment again. Use move() to reset start.
|
|
144
|
+
move([0, 0]);
|
|
145
|
+
a = aLine(45, h);
|
|
146
|
+
});
|
|
147
|
+
render();
|
|
148
|
+
const edges = a.getOwnShapes().filter((sh) => sh instanceof Edge);
|
|
149
|
+
const endPoint = edges[0].getLastVertex().toPoint();
|
|
150
|
+
// 45° line from (0,0) hits y=50 at x=50.
|
|
151
|
+
expect(endPoint.x).toBeCloseTo(50, 1);
|
|
152
|
+
expect(endPoint.y).toBeCloseTo(50, 1);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
69
155
|
describe("combined line functions", () => {
|
|
70
156
|
it("should create an L-shape with hLine and vLine", () => {
|
|
71
157
|
sketch("xy", () => {
|
|
@@ -133,14 +133,14 @@ describe("polygon", () => {
|
|
|
133
133
|
});
|
|
134
134
|
describe("standalone with targetPlane", () => {
|
|
135
135
|
it("should create a polygon on a specific plane", () => {
|
|
136
|
-
polygon(5, 50
|
|
136
|
+
polygon("xy", 5, 50);
|
|
137
137
|
const e = extrude(10);
|
|
138
138
|
render();
|
|
139
139
|
const solid = e.getShapes()[0];
|
|
140
140
|
expect(solid.getFaces()).toHaveLength(7);
|
|
141
141
|
});
|
|
142
142
|
it("should create a circumscribed polygon on a specific plane", () => {
|
|
143
|
-
polygon(6, 60, "circumscribed"
|
|
143
|
+
polygon("xy", 6, 60, "circumscribed");
|
|
144
144
|
const e = extrude(10);
|
|
145
145
|
render();
|
|
146
146
|
const solid = e.getShapes()[0];
|
|
@@ -54,7 +54,7 @@ describe("rect", () => {
|
|
|
54
54
|
});
|
|
55
55
|
describe("standalone with targetPlane", () => {
|
|
56
56
|
it("should create a rectangle on a specific plane", () => {
|
|
57
|
-
rect(80, 40
|
|
57
|
+
rect("xy", 80, 40);
|
|
58
58
|
const e = extrude(10);
|
|
59
59
|
render();
|
|
60
60
|
const solid = e.getShapes()[0];
|
|
@@ -35,7 +35,7 @@ describe("slot", () => {
|
|
|
35
35
|
});
|
|
36
36
|
describe("standalone with targetPlane", () => {
|
|
37
37
|
it("should create a slot on a specific plane", () => {
|
|
38
|
-
slot(60, 10
|
|
38
|
+
slot("xy", 60, 10);
|
|
39
39
|
const e = extrude(10);
|
|
40
40
|
render();
|
|
41
41
|
expect(e.getShapes()).toHaveLength(1);
|