fluidcad 0.0.17 → 0.0.19
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/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.d.ts +1 -0
- package/lib/dist/index.js +28 -13
- package/lib/dist/rendering/render.js +26 -2
- package/lib/dist/rendering/scene.d.ts +1 -0
- package/lib/dist/tests/extract-source-location.test.d.ts +1 -0
- package/lib/dist/tests/extract-source-location.test.js +74 -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 +4 -1
- package/server/dist/code-editor.d.ts +20 -0
- package/server/dist/code-editor.js +421 -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 +177 -0
- package/server/dist/routes/preferences.js +9 -0
- package/server/dist/ws-protocol.d.ts +23 -1
- package/ui/dist/assets/index-BfcNNxXr.css +2 -0
- package/ui/dist/assets/{index-CLQhpG6A.js → index-Bz0YoaQD.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
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function breakpoint(): never;
|
package/lib/dist/core/index.d.ts
CHANGED
package/lib/dist/core/index.js
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
53
|
-
const
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
57
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
for (const
|
|
31
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
177
|
-
if (
|
|
178
|
-
|
|
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
|
|
111
|
+
return outerSolids;
|
|
181
112
|
}
|
|
182
|
-
|
|
183
|
-
if (
|
|
184
|
-
return
|
|
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
|
|
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;
|
|
@@ -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;
|