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.
Files changed (58) hide show
  1. package/README.md +44 -0
  2. package/bin/fluidcad.js +6 -1
  3. package/bin/watcher.js +19 -1
  4. package/lib/dist/common/breakpoint-hit.d.ts +5 -0
  5. package/lib/dist/common/breakpoint-hit.js +9 -0
  6. package/lib/dist/core/breakpoint.d.ts +1 -0
  7. package/lib/dist/core/breakpoint.js +5 -0
  8. package/lib/dist/core/index.d.ts +1 -0
  9. package/lib/dist/core/index.js +1 -0
  10. package/lib/dist/core/interfaces.d.ts +100 -0
  11. package/lib/dist/features/copy-linear.d.ts +2 -2
  12. package/lib/dist/features/copy-linear.js +17 -11
  13. package/lib/dist/features/copy-linear2d.js +17 -11
  14. package/lib/dist/features/extrude-base.js +1 -1
  15. package/lib/dist/features/loft.d.ts +6 -12
  16. package/lib/dist/features/loft.js +55 -128
  17. package/lib/dist/features/repeat-circular.d.ts +1 -0
  18. package/lib/dist/features/repeat-circular.js +3 -0
  19. package/lib/dist/features/repeat-linear.d.ts +1 -0
  20. package/lib/dist/features/repeat-linear.js +3 -0
  21. package/lib/dist/features/revolve.d.ts +1 -0
  22. package/lib/dist/features/revolve.js +47 -22
  23. package/lib/dist/features/sweep.d.ts +1 -0
  24. package/lib/dist/features/sweep.js +47 -2
  25. package/lib/dist/index.js +15 -10
  26. package/lib/dist/rendering/render.js +26 -2
  27. package/lib/dist/rendering/scene.d.ts +1 -0
  28. package/lib/dist/tests/features/copy-linear.test.js +2 -2
  29. package/lib/dist/tests/features/thin-loft.test.d.ts +1 -0
  30. package/lib/dist/tests/features/thin-loft.test.js +151 -0
  31. package/lib/dist/tests/features/thin-revolve.test.d.ts +1 -0
  32. package/lib/dist/tests/features/thin-revolve.test.js +153 -0
  33. package/lib/dist/tests/features/thin-sweep.test.d.ts +1 -0
  34. package/lib/dist/tests/features/thin-sweep.test.js +121 -0
  35. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +3 -1
  37. package/server/dist/code-editor.d.ts +13 -0
  38. package/server/dist/code-editor.js +71 -0
  39. package/server/dist/fluidcad-server.d.ts +1 -0
  40. package/server/dist/fluidcad-server.js +14 -1
  41. package/server/dist/index.js +2 -0
  42. package/server/dist/preferences.d.ts +3 -0
  43. package/server/dist/preferences.js +3 -0
  44. package/server/dist/routes/actions.js +34 -0
  45. package/server/dist/routes/preferences.js +9 -0
  46. package/server/dist/ws-protocol.d.ts +10 -1
  47. package/ui/dist/assets/index-BfcNNxXr.css +2 -0
  48. package/ui/dist/assets/{index-CLQhpG6A.js → index-BoKbrDML.js} +150 -56
  49. package/ui/dist/icons/.claude/settings.local.json +8 -0
  50. package/ui/dist/icons/hmove.png +0 -0
  51. package/ui/dist/icons/loft.png +0 -0
  52. package/ui/dist/icons/offset.png +0 -0
  53. package/ui/dist/icons/pmove.png +0 -0
  54. package/ui/dist/icons/projection.png +0 -0
  55. package/ui/dist/icons/remove.png +0 -0
  56. package/ui/dist/icons/vmove.png +0 -0
  57. package/ui/dist/index.html +2 -2
  58. package/ui/dist/assets/index-BpvgjPLm.css +0 -2
@@ -10,6 +10,9 @@ export class RepeatCircular extends SceneObject {
10
10
  this.targetObjects = targetObjects;
11
11
  this.setAlwaysVisible();
12
12
  }
13
+ isContainer() {
14
+ return true;
15
+ }
13
16
  build(context) {
14
17
  this.saveShapesSnapshot(context);
15
18
  }
@@ -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;
@@ -10,6 +10,9 @@ export class RepeatLinear extends SceneObject {
10
10
  this.targetObjects = targetObjects;
11
11
  this.setAlwaysVisible();
12
12
  }
13
+ isContainer() {
14
+ return true;
15
+ }
13
16
  build(context) {
14
17
  this.saveShapesSnapshot(context);
15
18
  }
@@ -26,5 +26,6 @@ export declare class Revolve extends ExtrudeBase implements IRevolve {
26
26
  axis: any;
27
27
  operationMode: "new" | "remove";
28
28
  symmetric: true;
29
+ thin: [number, number] | [number];
29
30
  };
30
31
  }
@@ -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
- const allSideFaces = [];
29
- const allInternalFaces = [];
30
- const faces = pickedFaces ?? FaceMaker2.getRegions(this.extrudable.getGeometries(), plane);
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 if (!isOnSourcePlane) {
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
  }
@@ -25,5 +25,6 @@ export declare class Sweep extends ExtrudeBase implements ISweep {
25
25
  path: any;
26
26
  extrudable: any;
27
27
  operationMode: "new" | "remove";
28
+ thin: [number, number] | [number];
28
29
  };
29
30
  }
@@ -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
- const profileFaces = pickedFaces ?? FaceMaker2.getRegions(this.extrudable.getGeometries(), plane, this.getDrill());
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
- const sideFaces = [];
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
- const match = frame.match(/((?:[A-Za-z]:)?\/[^\s]+?\.fluid\.js):(\d+):(\d+)/);
11
- if (match) {
12
- let filePath = match[1];
13
- const virtualIdx = filePath.indexOf('virtual:live-render:');
14
- if (virtualIdx !== -1) {
15
- filePath = filePath.substring(virtualIdx + 'virtual:live-render:'.length);
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(match[2], 10),
20
- column: parseInt(match[3], 10),
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);
@@ -39,6 +39,7 @@ export type SceneObjectRender = {
39
39
  line: number;
40
40
  column: number;
41
41
  };
42
+ buildDurationMs?: number;
42
43
  };
43
44
  export declare class Scene {
44
45
  private sceneObjects;
@@ -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 = 30
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(30, 0);
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 {};