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
package/README.md CHANGED
@@ -117,6 +117,50 @@ FluidCAD ships official extensions for **VS Code** and **Neovim**, but works wit
117
117
 
118
118
  ---
119
119
 
120
+ ## Tutorials
121
+
122
+ Step-by-step tutorials from simple shapes to exam-level parts. [Browse all tutorials →](https://fluidcad.io/docs/tutorials/)
123
+
124
+ <table>
125
+ <tr>
126
+ <td align="center" width="33%">
127
+ <a href="https://fluidcad.io/docs/tutorials/lantern">
128
+ <img src="https://fluidcad.io/img/docs/tutorials/lantern-final.png" alt="Lantern" height="180" /><br />
129
+ <strong>Lantern</strong>
130
+ </a>
131
+ </td>
132
+ <td align="center" width="33%">
133
+ <a href="https://fluidcad.io/docs/tutorials/ice-cube-tray">
134
+ <img src="https://fluidcad.io/img/docs/tutorials/ice-cube-tray-final.png" alt="Ice Cube Tray" height="180" /><br />
135
+ <strong>Ice Cube Tray</strong>
136
+ </a>
137
+ </td>
138
+ <td align="center" width="33%">
139
+ <a href="https://fluidcad.io/docs/tutorials/grooved-box">
140
+ <img src="https://fluidcad.io/img/docs/tutorials/grooved-box-final.png" alt="Grooved Box" height="180" /><br />
141
+ <strong>Grooved Box</strong>
142
+ </a>
143
+ </td>
144
+ </tr>
145
+ <tr>
146
+ <td align="center" width="33%">
147
+ <a href="https://fluidcad.io/docs/tutorials/flange-with-notch">
148
+ <img src="https://fluidcad.io/img/docs/tutorials/flange-with-notch-final.png" alt="Flange With Notch" height="180" /><br />
149
+ <strong>Flange With Notch</strong>
150
+ </a>
151
+ </td>
152
+ <td align="center" width="33%">
153
+ <a href="https://fluidcad.io/docs/tutorials/cswp-sample-exam">
154
+ <img src="https://fluidcad.io/img/docs/tutorials/cswp-sample-exam-final.png" alt="CSWP Sample Exam" height="180" /><br />
155
+ <strong>CSWP Sample Exam</strong>
156
+ </a>
157
+ </td>
158
+ <td width="33%"></td>
159
+ </tr>
160
+ </table>
161
+
162
+ ---
163
+
120
164
  ## Getting Started
121
165
 
122
166
  ### 1. Create a New Project
package/bin/fluidcad.js CHANGED
@@ -5,7 +5,7 @@ import { resolve, dirname } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { parseArgs } from 'util';
7
7
  import { writeFileSync, existsSync } from 'fs';
8
- import { createFileWatcher } from './watcher.js';
8
+ import { createFileWatcher, findFluidFiles } from './watcher.js';
9
9
 
10
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
11
 
@@ -88,6 +88,11 @@ server.on('message', (msg) => {
88
88
  if (msg.success) {
89
89
  console.log('FluidCAD initialized successfully.');
90
90
  watcher = createFileWatcher(workspacePath, server);
91
+
92
+ const files = findFluidFiles(workspacePath);
93
+ if (files.length > 0) {
94
+ server.send({ type: 'process-file', filePath: files[0] });
95
+ }
91
96
  } else {
92
97
  console.error(`FluidCAD initialization failed: ${msg.error}`);
93
98
  process.exit(1);
package/bin/watcher.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import chokidar from 'chokidar';
2
- import { readFileSync } from 'fs';
2
+ import { readFileSync, readdirSync } from 'fs';
3
+ import { join } from 'path';
3
4
 
4
5
  /**
5
6
  * Creates a file watcher that monitors .fluid.js files in the workspace
@@ -53,3 +54,20 @@ export function createFileWatcher(workspacePath, server) {
53
54
 
54
55
  return watcher;
55
56
  }
57
+
58
+ /**
59
+ * Finds `.fluid.js` files in the top level of the workspace directory,
60
+ * ignoring node_modules and .git.
61
+ *
62
+ * @param {string} workspacePath - Absolute path to the workspace directory
63
+ * @returns {string[]} Absolute paths to discovered `.fluid.js` files
64
+ */
65
+ export function findFluidFiles(workspacePath) {
66
+ try {
67
+ return readdirSync(workspacePath)
68
+ .filter((f) => f.endsWith('.fluid.js'))
69
+ .map((f) => join(workspacePath, f));
70
+ } catch {
71
+ return [];
72
+ }
73
+ }
@@ -0,0 +1,5 @@
1
+ import { SourceLocation } from "./scene-object.js";
2
+ export declare class BreakpointHit extends Error {
3
+ readonly sourceLocation: SourceLocation | null;
4
+ constructor(sourceLocation: SourceLocation | null);
5
+ }
@@ -0,0 +1,9 @@
1
+ export class BreakpointHit extends Error {
2
+ sourceLocation;
3
+ constructor(sourceLocation) {
4
+ super('FluidCAD breakpoint hit');
5
+ this.name = 'BreakpointHit';
6
+ this.sourceLocation = sourceLocation;
7
+ Object.setPrototypeOf(this, BreakpointHit.prototype);
8
+ }
9
+ }
@@ -0,0 +1 @@
1
+ export declare function breakpoint(): never;
@@ -0,0 +1,5 @@
1
+ import { captureSourceLocation } from "../index.js";
2
+ import { BreakpointHit } from "../common/breakpoint-hit.js";
3
+ export function breakpoint() {
4
+ throw new BreakpointHit(captureSourceLocation());
5
+ }
@@ -30,3 +30,4 @@ export { default as part } from "./part.js";
30
30
  export { default as use } from "./use.js";
31
31
  export type { PartHandle } from "./part.js";
32
32
  export * from "./2d/index.js";
33
+ export { breakpoint } from "./breakpoint.js";
@@ -28,3 +28,4 @@ export { default as trim } from "./trim.js";
28
28
  export { default as part } from "./part.js";
29
29
  export { default as use } from "./use.js";
30
30
  export * from "./2d/index.js";
31
+ export { breakpoint } from "./breakpoint.js";
@@ -367,6 +367,43 @@ export interface IRevolve extends IFuseable {
367
367
  * @param points - 2D points in the sketch plane identifying regions to revolve.
368
368
  */
369
369
  pick(...points: Point2DLike[]): this;
370
+ /**
371
+ * Enables thin revolve mode — offsets the profile edges to create a thin-walled
372
+ * solid of revolution instead of revolving filled faces. Positive values offset
373
+ * outward, negative values offset inward.
374
+ * @param offset - The wall offset distance. Positive = outward, negative = inward.
375
+ */
376
+ thin(offset: number): this;
377
+ /**
378
+ * Enables thin revolve mode with two offset directions.
379
+ * The two offsets must go in opposite directions. If both have the same sign,
380
+ * the second offset is automatically flipped.
381
+ * @param offset1 - The first wall offset distance. Positive = outward, negative = inward.
382
+ * @param offset2 - The second wall offset distance, in the opposite direction of offset1.
383
+ */
384
+ thin(offset1: number, offset2: number): this;
385
+ /**
386
+ * Selects faces created inside the solid during revolution (e.g., the inner
387
+ * wall of a thin-walled revolve from a closed profile).
388
+ * @param args - Numeric indices or {@link FaceFilterBuilder} instances to filter the selection.
389
+ */
390
+ internalFaces(...args: (number | FaceFilterBuilder)[]): ISceneObject;
391
+ /**
392
+ * Selects edges bounding the internal geometry created during revolution.
393
+ * @param args - Numeric indices or {@link EdgeFilterBuilder} instances to filter the selection.
394
+ */
395
+ internalEdges(...args: (number | EdgeFilterBuilder)[]): ISceneObject;
396
+ /**
397
+ * Selects the cap faces at the open ends of a thin-walled revolve from an open profile.
398
+ * These are the small faces connecting the inner and outer walls at the profile endpoints.
399
+ * @param args - Numeric indices or {@link FaceFilterBuilder} instances to filter the selection.
400
+ */
401
+ capFaces(...args: (number | FaceFilterBuilder)[]): ISceneObject;
402
+ /**
403
+ * Selects edges on the cap faces of a thin-walled revolve from an open profile.
404
+ * @param args - Numeric indices or {@link EdgeFilterBuilder} instances to filter the selection.
405
+ */
406
+ capEdges(...args: (number | EdgeFilterBuilder)[]): ISceneObject;
370
407
  }
371
408
  export interface ILoft extends IFuseable {
372
409
  /**
@@ -399,6 +436,43 @@ export interface ILoft extends IFuseable {
399
436
  * @param args - Numeric indices or {@link EdgeFilterBuilder} instances to filter the selection.
400
437
  */
401
438
  sideEdges(...args: (number | EdgeFilterBuilder)[]): ISceneObject;
439
+ /**
440
+ * Enables thin loft mode — offsets the profile edges of each section to create a
441
+ * thin-walled shell instead of lofting filled faces. All profiles must be sketches
442
+ * and share the same topology. Positive values offset outward, negative offsets inward.
443
+ * @param offset - The wall offset distance. Positive = outward, negative = inward.
444
+ */
445
+ thin(offset: number): this;
446
+ /**
447
+ * Enables thin loft mode with two offset directions.
448
+ * The two offsets must go in opposite directions. If both have the same sign,
449
+ * the second offset is automatically flipped.
450
+ * @param offset1 - The first wall offset distance. Positive = outward, negative = inward.
451
+ * @param offset2 - The second wall offset distance, in the opposite direction of offset1.
452
+ */
453
+ thin(offset1: number, offset2: number): this;
454
+ /**
455
+ * Selects faces created inside the solid during loft (e.g., the inner
456
+ * wall of a thin-walled loft from closed profiles).
457
+ * @param args - Numeric indices or {@link FaceFilterBuilder} instances to filter the selection.
458
+ */
459
+ internalFaces(...args: (number | FaceFilterBuilder)[]): ISceneObject;
460
+ /**
461
+ * Selects edges bounding the internal geometry created during loft.
462
+ * @param args - Numeric indices or {@link EdgeFilterBuilder} instances to filter the selection.
463
+ */
464
+ internalEdges(...args: (number | EdgeFilterBuilder)[]): ISceneObject;
465
+ /**
466
+ * Selects the cap faces at the open ends of a thin-walled loft from open profiles.
467
+ * These are the small faces connecting the inner and outer walls at the profile endpoints.
468
+ * @param args - Numeric indices or {@link FaceFilterBuilder} instances to filter the selection.
469
+ */
470
+ capFaces(...args: (number | FaceFilterBuilder)[]): ISceneObject;
471
+ /**
472
+ * Selects edges on the cap faces of a thin-walled loft from open profiles.
473
+ * @param args - Numeric indices or {@link EdgeFilterBuilder} instances to filter the selection.
474
+ */
475
+ capEdges(...args: (number | EdgeFilterBuilder)[]): ISceneObject;
402
476
  }
403
477
  export interface ISweep extends IFuseable {
404
478
  /**
@@ -461,6 +535,32 @@ export interface ISweep extends IFuseable {
461
535
  * @param points - 2D points in the sketch plane identifying regions to sweep.
462
536
  */
463
537
  pick(...points: Point2DLike[]): this;
538
+ /**
539
+ * Enables thin sweep mode — offsets the profile edges to create a thin-walled
540
+ * swept shell instead of sweeping filled faces. Positive values offset outward,
541
+ * negative values offset inward.
542
+ * @param offset - The wall offset distance. Positive = outward, negative = inward.
543
+ */
544
+ thin(offset: number): this;
545
+ /**
546
+ * Enables thin sweep mode with two offset directions.
547
+ * The two offsets must go in opposite directions. If both have the same sign,
548
+ * the second offset is automatically flipped.
549
+ * @param offset1 - The first wall offset distance. Positive = outward, negative = inward.
550
+ * @param offset2 - The second wall offset distance, in the opposite direction of offset1.
551
+ */
552
+ thin(offset1: number, offset2: number): this;
553
+ /**
554
+ * Selects the cap faces at the open ends of a thin-walled sweep from an open profile.
555
+ * These are the small faces connecting the inner and outer walls at the profile endpoints.
556
+ * @param args - Numeric indices or {@link FaceFilterBuilder} instances to filter the selection.
557
+ */
558
+ capFaces(...args: (number | FaceFilterBuilder)[]): ISceneObject;
559
+ /**
560
+ * Selects edges on the cap faces of a thin-walled sweep from an open profile.
561
+ * @param args - Numeric indices or {@link EdgeFilterBuilder} instances to filter the selection.
562
+ */
563
+ capEdges(...args: (number | EdgeFilterBuilder)[]): ISceneObject;
464
564
  }
465
565
  export interface IShell extends ISceneObject {
466
566
  /**
@@ -5,10 +5,10 @@ export type LinearCopyOptions = {
5
5
  centered?: boolean;
6
6
  skip?: number[][];
7
7
  } & ({
8
- offset: number;
8
+ offset: number | number[];
9
9
  length?: never;
10
10
  } | {
11
- length: number;
11
+ length: number | number[];
12
12
  offset?: never;
13
13
  });
14
14
  export declare class CopyLinear extends SceneObject {
@@ -16,19 +16,24 @@ export class CopyLinear extends SceneObject {
16
16
  if (!this.targetObjects) {
17
17
  objects = context.getActiveSceneObjects();
18
18
  }
19
- let length = this.options.length || 1;
20
19
  const { count, centered, skip } = this.options;
21
- // Normalize count to per-axis array
22
20
  const counts = Array.isArray(count)
23
21
  ? count
24
22
  : this.axes.map(() => count);
25
- let offset;
26
- if ('offset' in this.options && this.options.offset !== undefined) {
27
- offset = this.options.offset;
28
- }
29
- else {
30
- offset = length / Math.max(...counts);
31
- }
23
+ const offsets = 'offset' in this.options && this.options.offset !== undefined
24
+ ? (Array.isArray(this.options.offset) ? this.options.offset : this.axes.map(() => this.options.offset))
25
+ : null;
26
+ const lengths = 'length' in this.options && this.options.length !== undefined
27
+ ? (Array.isArray(this.options.length) ? this.options.length : this.axes.map(() => this.options.length))
28
+ : null;
29
+ const axisOffsets = this.axes.map((_, a) => {
30
+ if (offsets) {
31
+ return offsets[a] ?? offsets[0];
32
+ }
33
+ const len = lengths ? (lengths[a] ?? lengths[0]) : 1;
34
+ const axisCount = counts[a];
35
+ return axisCount > 1 ? len / (axisCount - 1) : 0;
36
+ });
32
37
  // Build grid positions as cartesian product of per-axis indices (0..counts[a]-1)
33
38
  let positions = [[]];
34
39
  for (let a = 0; a < this.axes.length; a++) {
@@ -49,8 +54,9 @@ export class CopyLinear extends SceneObject {
49
54
  let matrix = Matrix4.identity();
50
55
  for (let a = 0; a < this.axes.length; a++) {
51
56
  const axisCount = counts[a];
52
- const startOffset = centered ? -(axisCount * offset) / 2 : 0;
53
- const distance = startOffset + offset * pos[a];
57
+ const axisOffset = axisOffsets[a];
58
+ const startOffset = centered ? -(axisCount * axisOffset) / 2 : 0;
59
+ const distance = startOffset + axisOffset * pos[a];
54
60
  const translation = this.axes[a].direction.multiply(distance);
55
61
  matrix = matrix.multiply(Matrix4.fromTranslationVector(translation));
56
62
  }
@@ -20,19 +20,24 @@ export class CopyLinear2D extends GeometrySceneObject {
20
20
  else {
21
21
  objects = allSiblings;
22
22
  }
23
- let length = this.options.length || 1;
24
23
  const { count, centered, skip } = this.options;
25
- // Normalize count to per-axis array
26
24
  const counts = Array.isArray(count)
27
25
  ? count
28
26
  : this.axes.map(() => count);
29
- let offset;
30
- if ('offset' in this.options && this.options.offset !== undefined) {
31
- offset = this.options.offset;
32
- }
33
- else {
34
- offset = length / Math.max(...counts);
35
- }
27
+ const offsets = 'offset' in this.options && this.options.offset !== undefined
28
+ ? (Array.isArray(this.options.offset) ? this.options.offset : this.axes.map(() => this.options.offset))
29
+ : null;
30
+ const lengths = 'length' in this.options && this.options.length !== undefined
31
+ ? (Array.isArray(this.options.length) ? this.options.length : this.axes.map(() => this.options.length))
32
+ : null;
33
+ const axisOffsets = this.axes.map((_, a) => {
34
+ if (offsets) {
35
+ return offsets[a] ?? offsets[0];
36
+ }
37
+ const len = lengths ? (lengths[a] ?? lengths[0]) : 1;
38
+ const axisCount = counts[a];
39
+ return axisCount > 1 ? len / (axisCount - 1) : 0;
40
+ });
36
41
  // Build grid positions as cartesian product of per-axis indices (0..counts[a]-1)
37
42
  let positions = [[]];
38
43
  for (let a = 0; a < this.axes.length; a++) {
@@ -53,8 +58,9 @@ export class CopyLinear2D extends GeometrySceneObject {
53
58
  let matrix = Matrix4.identity();
54
59
  for (let a = 0; a < this.axes.length; a++) {
55
60
  const axisCount = counts[a];
56
- const startOffset = centered ? -(axisCount * offset) / 2 : 0;
57
- const distance = startOffset + offset * pos[a];
61
+ const axisOffset = axisOffsets[a];
62
+ const startOffset = centered ? -(axisCount * axisOffset) / 2 : 0;
63
+ const distance = startOffset + axisOffset * pos[a];
58
64
  const translation = this.axes[a].direction.multiply(distance);
59
65
  matrix = matrix.multiply(Matrix4.fromTranslationVector(translation));
60
66
  }
@@ -236,7 +236,7 @@ export class ExtrudeBase extends SceneObject {
236
236
  pickPoints: this.isPicking()
237
237
  ? this._pickPoints.map(p => { const pt = p.asPoint2D(); return [pt.x, pt.y]; })
238
238
  : undefined,
239
- trigger: 'region-picking',
239
+ trigger: this.isThin() ? undefined : 'region-picking',
240
240
  pickPlane: plane ? {
241
241
  origin: plane.origin,
242
242
  xDirection: plane.xDirection,
@@ -1,27 +1,21 @@
1
1
  import { BuildSceneObjectContext, SceneObject } from "../common/scene-object.js";
2
- import { FaceFilterBuilder } from "../filters/face/face-filter.js";
3
- import { EdgeFilterBuilder } from "../filters/edge/edge-filter.js";
4
2
  import { ILoft } from "../core/interfaces.js";
5
- export declare class Loft extends SceneObject implements ILoft {
3
+ import { ExtrudeBase } from "./extrude-base.js";
4
+ export declare class Loft extends ExtrudeBase implements ILoft {
6
5
  private _profiles;
7
6
  constructor(...profiles: SceneObject[]);
8
7
  get profiles(): SceneObject[];
9
8
  build(context: BuildSceneObjectContext): void;
9
+ private buildThinLoft;
10
10
  private getProfilePlane;
11
- startFaces(...args: (number | FaceFilterBuilder)[]): SceneObject;
12
- endFaces(...args: (number | FaceFilterBuilder)[]): SceneObject;
13
- sideFaces(...args: (number | FaceFilterBuilder)[]): SceneObject;
14
- startEdges(...args: (number | EdgeFilterBuilder)[]): SceneObject;
15
- endEdges(...args: (number | EdgeFilterBuilder)[]): SceneObject;
16
- sideEdges(...args: (number | EdgeFilterBuilder)[]): SceneObject;
17
- private buildSuffix;
18
- private resolveEdges;
19
- private resolveFaces;
20
11
  private getWiresFromSceneObject;
12
+ getDependencies(): SceneObject[];
13
+ createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
21
14
  compareTo(other: Loft): boolean;
22
15
  getType(): string;
23
16
  serialize(): {
24
17
  profiles: any[];
25
18
  operationMode: "new" | "remove";
19
+ thin: [number, number] | [number];
26
20
  };
27
21
  }
@@ -1,14 +1,12 @@
1
- import { SceneObject } from "../common/scene-object.js";
2
1
  import { Explorer } from "../oc/explorer.js";
3
2
  import { LoftOps } from "../oc/loft-ops.js";
4
3
  import { FaceMaker2 } from "../oc/face-maker2.js";
5
- import { LazySelectionSceneObject } from "./lazy-scene-object.js";
6
- import { FaceFilterBuilder } from "../filters/face/face-filter.js";
7
- import { EdgeFilterBuilder } from "../filters/edge/edge-filter.js";
8
- import { ShapeFilter } from "../filters/filter.js";
9
4
  import { FaceOps } from "../oc/face-ops.js";
5
+ import { BooleanOps } from "../oc/boolean-ops.js";
10
6
  import { fuseWithSceneObjects, cutWithSceneObjects } from "../helpers/scene-helpers.js";
11
- export class Loft extends SceneObject {
7
+ import { ExtrudeBase } from "./extrude-base.js";
8
+ import { ThinFaceMaker } from "../oc/thin-face-maker.js";
9
+ export class Loft extends ExtrudeBase {
12
10
  _profiles = [];
13
11
  constructor(...profiles) {
14
12
  super();
@@ -21,17 +19,23 @@ export class Loft extends SceneObject {
21
19
  if (this.profiles.length < 2) {
22
20
  throw new Error("Loft requires at least two profiles.");
23
21
  }
24
- const allWires = [];
25
- for (const profile of this.profiles) {
26
- const wires = this.getWiresFromSceneObject(profile);
27
- if (wires.length === 0) {
28
- throw new Error("Could not extract wire from profile.");
29
- }
30
- for (const wire of wires) {
31
- allWires.push(wire);
22
+ let newShapes;
23
+ if (this.isThin()) {
24
+ newShapes = this.buildThinLoft();
25
+ }
26
+ else {
27
+ const allWires = [];
28
+ for (const profile of this.profiles) {
29
+ const wires = this.getWiresFromSceneObject(profile);
30
+ if (wires.length === 0) {
31
+ throw new Error("Could not extract wire from profile.");
32
+ }
33
+ for (const wire of wires) {
34
+ allWires.push(wire);
35
+ }
32
36
  }
37
+ newShapes = LoftOps.makeLoft(allWires);
33
38
  }
34
- const newShapes = LoftOps.makeLoft(allWires);
35
39
  for (const profile of this.profiles) {
36
40
  profile.removeShapes(this);
37
41
  }
@@ -78,124 +82,39 @@ export class Loft extends SceneObject {
78
82
  }
79
83
  this.addShapes(fusionResult.newShapes);
80
84
  }
81
- getProfilePlane(profile) {
82
- if ('getPlane' in profile && typeof profile.getPlane === 'function') {
83
- return profile.getPlane();
84
- }
85
- return null;
86
- }
87
- startFaces(...args) {
88
- const suffix = this.buildSuffix('start-faces', args);
89
- return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
90
- const faces = parent.getState('start-faces') || [];
91
- const transform = parent.getTransform();
92
- const originalFaces = transform
93
- ? (this.getState('start-faces') || [])
94
- : null;
95
- return this.resolveFaces(faces, args, transform, originalFaces);
96
- }, this);
97
- }
98
- endFaces(...args) {
99
- const suffix = this.buildSuffix('end-faces', args);
100
- return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
101
- const faces = parent.getState('end-faces') || [];
102
- const transform = parent.getTransform();
103
- const originalFaces = transform
104
- ? (this.getState('end-faces') || [])
105
- : null;
106
- return this.resolveFaces(faces, args, transform, originalFaces);
107
- }, this);
108
- }
109
- sideFaces(...args) {
110
- const suffix = this.buildSuffix('side-faces', args);
111
- return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
112
- const faces = parent.getState('side-faces') || [];
113
- const transform = parent.getTransform();
114
- const originalFaces = transform
115
- ? (this.getState('side-faces') || [])
116
- : null;
117
- return this.resolveFaces(faces, args, transform, originalFaces);
118
- }, this);
119
- }
120
- startEdges(...args) {
121
- const suffix = this.buildSuffix('start-edges', args);
122
- return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
123
- const faces = parent.getState('start-faces') || [];
124
- const edges = faces.flatMap(f => f.getEdges());
125
- const transform = parent.getTransform();
126
- const originalEdges = transform
127
- ? (this.getState('start-faces') || []).flatMap(f => f.getEdges())
128
- : null;
129
- return this.resolveEdges(edges, args, transform, originalEdges);
130
- }, this);
131
- }
132
- endEdges(...args) {
133
- const suffix = this.buildSuffix('end-edges', args);
134
- return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
135
- const faces = parent.getState('end-faces') || [];
136
- const edges = faces.flatMap(f => f.getEdges());
137
- const transform = parent.getTransform();
138
- const originalEdges = transform
139
- ? (this.getState('end-faces') || []).flatMap(f => f.getEdges())
140
- : null;
141
- return this.resolveEdges(edges, args, transform, originalEdges);
142
- }, this);
143
- }
144
- sideEdges(...args) {
145
- const suffix = this.buildSuffix('side-edges', args);
146
- return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
147
- const sideFaces = parent.getState('side-faces') || [];
148
- const startFaces = parent.getState('start-faces') || [];
149
- const endFaces = parent.getState('end-faces') || [];
150
- const excludedEdges = [...startFaces, ...endFaces].flatMap(f => f.getEdges());
151
- const edges = sideFaces.flatMap(f => f.getEdges())
152
- .filter(e => !excludedEdges.some(ex => e.getShape().IsSame(ex.getShape())))
153
- .filter((e, i, arr) => arr.findIndex(o => o.getShape().IsSame(e.getShape())) === i);
154
- return this.resolveEdges(edges, args);
155
- }, this);
156
- }
157
- buildSuffix(prefix, args) {
158
- if (args.length === 0) {
159
- return prefix;
160
- }
161
- const key = args.map(a => typeof a === 'number' ? a : 'f').join('-');
162
- return `${prefix}-${key}`;
163
- }
164
- resolveEdges(shapes, args, transform = null, originalShapes = null) {
165
- if (args.length === 0) {
166
- return shapes;
167
- }
168
- if (args.every(a => typeof a === 'number')) {
169
- const indices = args;
170
- let filters = indices.map(i => new EdgeFilterBuilder().atIndex(i, shapes, originalShapes));
171
- if (transform) {
172
- filters = filters.map(f => f.transform(transform));
85
+ buildThinLoft() {
86
+ const outerWires = [];
87
+ const innerWires = [];
88
+ for (const profile of this.profiles) {
89
+ if (!profile.isExtrudable()) {
90
+ throw new Error("Thin loft requires all profiles to be sketches.");
91
+ }
92
+ const extrudable = profile;
93
+ const profilePlane = extrudable.getPlane();
94
+ const thinResult = ThinFaceMaker.make(extrudable.getGeometries(), profilePlane, this._thin[0], this._thin[1]);
95
+ for (const face of thinResult.faces) {
96
+ const wires = face.getWires();
97
+ outerWires.push(wires[0]);
98
+ if (wires.length > 1) {
99
+ innerWires.push(wires[1]);
100
+ }
173
101
  }
174
- return new ShapeFilter(shapes, ...filters).apply();
175
102
  }
176
- let filters = args.filter(a => a instanceof EdgeFilterBuilder);
177
- if (transform) {
178
- filters = filters.map(f => f.transform(transform));
103
+ const outerSolids = LoftOps.makeLoft(outerWires);
104
+ if (innerWires.length > 0 && innerWires.length === outerWires.length) {
105
+ const innerSolids = LoftOps.makeLoft(innerWires);
106
+ const { result: outerFused } = BooleanOps.fuse(outerSolids);
107
+ const { result: innerFused } = BooleanOps.fuse(innerSolids);
108
+ const cutResult = BooleanOps.cutShapes(outerFused[0], innerFused[0]);
109
+ return [cutResult];
179
110
  }
180
- return new ShapeFilter(shapes, ...filters).apply();
111
+ return outerSolids;
181
112
  }
182
- resolveFaces(shapes, args, transform = null, originalShapes = null) {
183
- if (args.length === 0) {
184
- return shapes;
185
- }
186
- if (args.every(a => typeof a === 'number')) {
187
- const indices = args;
188
- let filters = indices.map(i => new FaceFilterBuilder().atIndex(i, shapes, originalShapes));
189
- if (transform) {
190
- filters = filters.map(f => f.transform(transform));
191
- }
192
- return new ShapeFilter(shapes, ...filters).apply();
193
- }
194
- let filters = args.filter(a => a instanceof FaceFilterBuilder);
195
- if (transform) {
196
- filters = filters.map(f => f.transform(transform));
113
+ getProfilePlane(profile) {
114
+ if ('getPlane' in profile && typeof profile.getPlane === 'function') {
115
+ return profile.getPlane();
197
116
  }
198
- return new ShapeFilter(shapes, ...filters).apply();
117
+ return null;
199
118
  }
200
119
  getWiresFromSceneObject(obj) {
201
120
  const shapes = obj.getShapes({ excludeMeta: false });
@@ -245,6 +164,13 @@ export class Loft extends SceneObject {
245
164
  }
246
165
  return [];
247
166
  }
167
+ getDependencies() {
168
+ return [...this._profiles];
169
+ }
170
+ createCopy(remap) {
171
+ const profiles = this._profiles.map(p => remap.get(p) || p);
172
+ return new Loft(...profiles).syncWith(this);
173
+ }
248
174
  compareTo(other) {
249
175
  if (!(other instanceof Loft)) {
250
176
  return false;
@@ -269,6 +195,7 @@ export class Loft extends SceneObject {
269
195
  return {
270
196
  profiles: this.profiles.map(f => f.serialize()),
271
197
  operationMode: this._operationMode !== 'add' ? this._operationMode : undefined,
198
+ thin: this._thin,
272
199
  };
273
200
  }
274
201
  }
@@ -16,6 +16,7 @@ export declare class RepeatCircular extends SceneObject {
16
16
  options: CircularRepeatOptions;
17
17
  targetObjects: SceneObject[] | null;
18
18
  constructor(axis: Axis, options: CircularRepeatOptions, targetObjects?: SceneObject[] | null);
19
+ isContainer(): boolean;
19
20
  build(context: BuildSceneObjectContext): void;
20
21
  compareTo(other: RepeatCircular): boolean;
21
22
  getType(): string;