fluidcad 0.0.37 → 0.0.38
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/core/plane.d.ts +26 -6
- package/lib/dist/core/plane.js +21 -44
- package/lib/dist/features/2d/offset.js +2 -2
- package/lib/dist/features/plane-from-object.d.ts +16 -4
- package/lib/dist/features/plane-from-object.js +101 -8
- package/lib/dist/oc/sweep-ops.d.ts +7 -16
- package/lib/dist/oc/sweep-ops.js +67 -99
- package/lib/dist/oc/thin-face-maker.d.ts +0 -19
- package/lib/dist/oc/thin-face-maker.js +3 -68
- package/lib/dist/oc/wire-ops.d.ts +17 -2
- package/lib/dist/oc/wire-ops.js +55 -4
- package/lib/dist/tests/features/2d/offset.test.js +74 -1
- package/lib/dist/tests/features/plane.test.js +95 -0
- package/lib/dist/tests/features/sweep.test.js +46 -1
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/llm-docs/api/index.json +1 -1
- package/llm-docs/index.json +1 -1
- package/package.json +1 -1
- package/server/dist/fluidcad-server.d.ts +1 -1
- package/server/dist/fluidcad-server.js +11 -1
- package/server/dist/routes/params.js +1 -1
- package/ui/dist/assets/{index-no7mtr5s.js → index-D8zV21wB.js} +83 -83
- package/ui/dist/index.html +1 -1
package/lib/dist/core/plane.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { PlaneLike, PlaneTransformOptions } from "../math/plane.js";
|
|
2
2
|
import { IPlane, ISceneObject } from "./interfaces.js";
|
|
3
3
|
export type PlaneRenderableOptions = PlaneTransformOptions;
|
|
4
|
+
/**
|
|
5
|
+
* Where to place a plane along an edge: a named position, or a normalized
|
|
6
|
+
* distance from the edge start (`0`) to its end (`1`). `0.5` is the midpoint.
|
|
7
|
+
*/
|
|
8
|
+
export type EdgePlanePosition = 'start' | 'middle' | 'end' | number;
|
|
4
9
|
interface PlaneFunction {
|
|
5
10
|
/**
|
|
6
11
|
* Creates a plane from a standard plane or normal vector.
|
|
@@ -20,8 +25,10 @@ interface PlaneFunction {
|
|
|
20
25
|
*/
|
|
21
26
|
(plane: PlaneLike, offset: number): IPlane;
|
|
22
27
|
/**
|
|
23
|
-
* Creates a plane from a face
|
|
24
|
-
*
|
|
28
|
+
* Creates a plane from a selection. A selected face yields that face's
|
|
29
|
+
* plane; a selected edge yields a plane at the edge start, oriented normal
|
|
30
|
+
* to the edge and facing outward (away from the edge body), like a start cap.
|
|
31
|
+
* @param selection - The selected face or edge to create a plane from
|
|
25
32
|
*/
|
|
26
33
|
(selection: ISceneObject): IPlane;
|
|
27
34
|
/**
|
|
@@ -31,11 +38,24 @@ interface PlaneFunction {
|
|
|
31
38
|
*/
|
|
32
39
|
(selection: ISceneObject, options: PlaneRenderableOptions): IPlane;
|
|
33
40
|
/**
|
|
34
|
-
* Creates a plane from a
|
|
35
|
-
*
|
|
36
|
-
*
|
|
41
|
+
* Creates a plane from a selection with a numeric second argument. For a
|
|
42
|
+
* face, the number is the offset distance along the face normal. For an
|
|
43
|
+
* edge, it is a normalized position from `0` (start) to `1` (end), and the
|
|
44
|
+
* plane is created at that point oriented normal to the edge. The normal
|
|
45
|
+
* follows the edge's forward direction, except at the start (`0`) where it
|
|
46
|
+
* faces outward (away from the edge body), so both ends read like caps.
|
|
47
|
+
* @param selection - The selected face or edge
|
|
48
|
+
* @param offsetOrPosition - Face: offset distance. Edge: normalized 0–1 position.
|
|
49
|
+
*/
|
|
50
|
+
(selection: ISceneObject, offsetOrPosition: number): IPlane;
|
|
51
|
+
/**
|
|
52
|
+
* Creates a plane at a named position along an edge, oriented normal to the
|
|
53
|
+
* edge. At `'start'` the normal faces outward (away from the edge body);
|
|
54
|
+
* `'middle'` and `'end'` follow the edge's forward direction.
|
|
55
|
+
* @param edge - The selected edge
|
|
56
|
+
* @param position - `'start'`, `'middle'`, or `'end'`
|
|
37
57
|
*/
|
|
38
|
-
(
|
|
58
|
+
(edge: ISceneObject, position: 'start' | 'middle' | 'end'): IPlane;
|
|
39
59
|
/**
|
|
40
60
|
* Transforms an existing plane with options.
|
|
41
61
|
* @param plane - The existing plane to transform
|
package/lib/dist/core/plane.js
CHANGED
|
@@ -23,56 +23,33 @@ function build(context) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
if (arguments.length === 2) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const pln = new PlaneObject(axis, options);
|
|
37
|
-
context.addSceneObject(pln);
|
|
38
|
-
return pln;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
if ((arguments[0] instanceof PlaneObjectBase || isPlaneLike(arguments[0])) &&
|
|
42
|
-
(arguments[1] instanceof PlaneObjectBase || isPlaneLike(arguments[1]))) {
|
|
43
|
-
// axis between two others
|
|
44
|
-
let a1;
|
|
45
|
-
let a2;
|
|
46
|
-
if (arguments[0] instanceof PlaneObjectBase) {
|
|
47
|
-
a1 = arguments[0];
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
const axis = normalizePlane(arguments[0]);
|
|
51
|
-
a1 = new PlaneObject(axis);
|
|
52
|
-
}
|
|
53
|
-
if (arguments[1] instanceof PlaneObjectBase) {
|
|
54
|
-
a2 = arguments[1];
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
const axis = normalizePlane(arguments[1]);
|
|
58
|
-
a2 = new PlaneObject(axis);
|
|
59
|
-
}
|
|
60
|
-
context.addSceneObject(a1);
|
|
61
|
-
context.addSceneObject(a2);
|
|
62
|
-
const pln = new PlaneMiddleRenderable(a1, a2);
|
|
26
|
+
const a0 = arguments[0];
|
|
27
|
+
const a1 = arguments[1];
|
|
28
|
+
// Plane midway between two planes / plane-likes.
|
|
29
|
+
if ((a0 instanceof PlaneObjectBase || isPlaneLike(a0)) &&
|
|
30
|
+
(a1 instanceof PlaneObjectBase || isPlaneLike(a1))) {
|
|
31
|
+
const p1 = a0 instanceof PlaneObjectBase ? a0 : new PlaneObject(normalizePlane(a0));
|
|
32
|
+
const p2 = a1 instanceof PlaneObjectBase ? a1 : new PlaneObject(normalizePlane(a1));
|
|
33
|
+
context.addSceneObject(p1);
|
|
34
|
+
context.addSceneObject(p2);
|
|
35
|
+
const pln = new PlaneMiddleRenderable(p1, p2);
|
|
63
36
|
context.addSceneObject(pln);
|
|
64
37
|
return pln;
|
|
65
38
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
39
|
+
// From a scene object. A face reads the second argument as an offset
|
|
40
|
+
// distance / transform options; an edge reads it as a normalized 0–1
|
|
41
|
+
// position (or 'start'/'middle'/'end'). The face-vs-edge decision is
|
|
42
|
+
// deferred to PlaneFromObject.build(), where the source shape is known.
|
|
43
|
+
if (a0 instanceof SceneObject) {
|
|
44
|
+
context.addSceneObject(a0);
|
|
45
|
+
const pln = new PlaneFromObject(a0, a1);
|
|
69
46
|
context.addSceneObject(pln);
|
|
70
47
|
return pln;
|
|
71
48
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const options =
|
|
75
|
-
const pln = new PlaneObject(
|
|
49
|
+
// From a plane-like: number → offset, object → transform options.
|
|
50
|
+
if (isPlaneLike(a0)) {
|
|
51
|
+
const options = typeof a1 === 'number' ? { offset: a1 } : a1;
|
|
52
|
+
const pln = new PlaneObject(normalizePlane(a0), options);
|
|
76
53
|
context.addSceneObject(pln);
|
|
77
54
|
return pln;
|
|
78
55
|
}
|
|
@@ -55,9 +55,9 @@ export class Offset extends ExtrudableGeometryBase {
|
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
let lastOffsetWire = null;
|
|
58
|
+
const plane = this.getPlane();
|
|
58
59
|
for (const wireInfo of wires) {
|
|
59
|
-
const
|
|
60
|
-
const offsetWire = WireOps.offsetWire(wireInfo.wire, this.distance, isOpen);
|
|
60
|
+
const offsetWire = WireOps.offsetWireOnPlane(wireInfo.wire, this.distance, wireInfo.wire.isClosed(), plane);
|
|
61
61
|
lastOffsetWire = offsetWire;
|
|
62
62
|
const edges = offsetWire.getEdges();
|
|
63
63
|
for (const edge of edges) {
|
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import { BuildSceneObjectContext, SceneObject } from "../common/scene-object.js";
|
|
2
|
-
import { PlaneRenderableOptions } from "../core/plane.js";
|
|
2
|
+
import { EdgePlanePosition, PlaneRenderableOptions } from "../core/plane.js";
|
|
3
3
|
import { PlaneObjectBase } from "./plane-renderable-base.js";
|
|
4
4
|
import { Face } from "../common/face.js";
|
|
5
5
|
import { Point } from "../math/point.js";
|
|
6
6
|
import { Plane } from "../math/plane.js";
|
|
7
7
|
export declare class PlaneFromObject extends PlaneObjectBase {
|
|
8
8
|
sourceObject: SceneObject;
|
|
9
|
-
|
|
10
|
-
constructor(sourceObject: SceneObject,
|
|
9
|
+
optionsOrPosition?: PlaneRenderableOptions | EdgePlanePosition;
|
|
10
|
+
constructor(sourceObject: SceneObject, optionsOrPosition?: PlaneRenderableOptions | EdgePlanePosition);
|
|
11
11
|
validate(): void;
|
|
12
12
|
build(context?: BuildSceneObjectContext): void;
|
|
13
|
+
/**
|
|
14
|
+
* Builds a plane normal to `edge` at the configured position. The edge
|
|
15
|
+
* tangent at that point becomes the plane normal; the in-plane axes are
|
|
16
|
+
* an arbitrary (but deterministic) basis around it.
|
|
17
|
+
*/
|
|
18
|
+
private buildFromEdge;
|
|
19
|
+
/**
|
|
20
|
+
* Resolves the second argument for a face/plane source. A bare number is a
|
|
21
|
+
* normal-offset distance; a string position is only meaningful for edges
|
|
22
|
+
* and is rejected here.
|
|
23
|
+
*/
|
|
24
|
+
private faceOptions;
|
|
13
25
|
getFromSceneObject(sceneObject: SceneObject): {
|
|
14
26
|
plane: Plane;
|
|
15
27
|
sourceFace: Face;
|
|
@@ -24,7 +36,7 @@ export declare class PlaneFromObject extends PlaneObjectBase {
|
|
|
24
36
|
xDirection: import("../math/vector3d.js").Vector3d;
|
|
25
37
|
yDirection: import("../math/vector3d.js").Vector3d;
|
|
26
38
|
normal: import("../math/vector3d.js").Vector3d;
|
|
27
|
-
options: import("../math/plane.js").PlaneTransformOptions;
|
|
39
|
+
options: import("../math/plane.js").PlaneTransformOptions | EdgePlanePosition;
|
|
28
40
|
center: any;
|
|
29
41
|
};
|
|
30
42
|
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { PlaneObjectBase } from "./plane-renderable-base.js";
|
|
2
2
|
import { FaceOps } from "../oc/face-ops.js";
|
|
3
3
|
import { ShapeOps } from "../oc/shape-ops.js";
|
|
4
|
+
import { WireOps } from "../oc/wire-ops.js";
|
|
5
|
+
import { PathSampler } from "../oc/path-sampler.js";
|
|
4
6
|
import { Point } from "../math/point.js";
|
|
7
|
+
import { Plane } from "../math/plane.js";
|
|
5
8
|
import { requireShapes } from "../common/operand-check.js";
|
|
6
9
|
export class PlaneFromObject extends PlaneObjectBase {
|
|
7
10
|
sourceObject;
|
|
8
|
-
|
|
9
|
-
constructor(sourceObject,
|
|
11
|
+
optionsOrPosition;
|
|
12
|
+
constructor(sourceObject, optionsOrPosition) {
|
|
10
13
|
super();
|
|
11
14
|
this.sourceObject = sourceObject;
|
|
12
|
-
this.
|
|
15
|
+
this.optionsOrPosition = optionsOrPosition;
|
|
13
16
|
}
|
|
14
17
|
validate() {
|
|
15
18
|
// PlaneObjectBase sources expose the plane directly — no shapes required.
|
|
@@ -19,6 +22,17 @@ export class PlaneFromObject extends PlaneObjectBase {
|
|
|
19
22
|
requireShapes(this.sourceObject, "source", "plane");
|
|
20
23
|
}
|
|
21
24
|
build(context) {
|
|
25
|
+
// An edge source produces a plane normal to the edge at a position along
|
|
26
|
+
// it. The face-vs-edge decision is deferred to here (rather than the
|
|
27
|
+
// plane() builder) because the source shape type is only known once the
|
|
28
|
+
// selection has been resolved.
|
|
29
|
+
if (!(this.sourceObject instanceof PlaneObjectBase)) {
|
|
30
|
+
const shapes = this.sourceObject.getShapes({ excludeGuide: false });
|
|
31
|
+
if (shapes.length === 1 && shapes[0].isEdge()) {
|
|
32
|
+
this.buildFromEdge(context, shapes[0]);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
22
36
|
let plane;
|
|
23
37
|
let sourceFace;
|
|
24
38
|
let center;
|
|
@@ -36,10 +50,11 @@ export class PlaneFromObject extends PlaneObjectBase {
|
|
|
36
50
|
const bbox = ShapeOps.getBoundingBox(sourceFace.getShape());
|
|
37
51
|
center = new Point(bbox.centerX, bbox.centerY, bbox.centerZ);
|
|
38
52
|
}
|
|
39
|
-
|
|
53
|
+
const options = this.faceOptions();
|
|
54
|
+
if (options) {
|
|
40
55
|
// Apply the same transform to the center so the preview face stays on
|
|
41
56
|
// the rotated plane instead of floating at its pre-rotation position.
|
|
42
|
-
const matrix = plane.getTransformMatrix(
|
|
57
|
+
const matrix = plane.getTransformMatrix(options);
|
|
43
58
|
plane = plane.applyMatrix(matrix);
|
|
44
59
|
if (center) {
|
|
45
60
|
center = center.transform(matrix);
|
|
@@ -60,6 +75,53 @@ export class PlaneFromObject extends PlaneObjectBase {
|
|
|
60
75
|
face.markAsMetaShape();
|
|
61
76
|
this.addShape(face);
|
|
62
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Builds a plane normal to `edge` at the configured position. The edge
|
|
80
|
+
* tangent at that point becomes the plane normal; the in-plane axes are
|
|
81
|
+
* an arbitrary (but deterministic) basis around it.
|
|
82
|
+
*/
|
|
83
|
+
buildFromEdge(context, edge) {
|
|
84
|
+
const t = normalizeEdgePosition(this.optionsOrPosition);
|
|
85
|
+
const frame = sampleEdgeFrame(edge, t);
|
|
86
|
+
// The forward tangent points *into* the edge at the start, so the plane
|
|
87
|
+
// would face inward there. Flip it at the start endpoint so it faces
|
|
88
|
+
// outward — like an extrude's start cap (the end already faces outward via
|
|
89
|
+
// the forward tangent). Interior/end positions keep the forward tangent.
|
|
90
|
+
const normal = t <= 0 ? frame.tangent.negate() : frame.tangent;
|
|
91
|
+
let plane = Plane.fromPointAndNormal(frame.point, normal);
|
|
92
|
+
let center = frame.point;
|
|
93
|
+
// Unlike the face path, an edge is only *referenced* to derive the plane —
|
|
94
|
+
// it is not consumed, so it stays available to its owning solid and to
|
|
95
|
+
// other features.
|
|
96
|
+
const transform = context?.getTransform() ?? null;
|
|
97
|
+
if (transform) {
|
|
98
|
+
plane = plane.applyMatrix(transform);
|
|
99
|
+
center = center.transform(transform);
|
|
100
|
+
}
|
|
101
|
+
this.setState('plane-center', center);
|
|
102
|
+
this.setState('plane', plane);
|
|
103
|
+
const face = FaceOps.planeToFace(plane, center);
|
|
104
|
+
face.markAsMetaShape();
|
|
105
|
+
this.addShape(face);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Resolves the second argument for a face/plane source. A bare number is a
|
|
109
|
+
* normal-offset distance; a string position is only meaningful for edges
|
|
110
|
+
* and is rejected here.
|
|
111
|
+
*/
|
|
112
|
+
faceOptions() {
|
|
113
|
+
const value = this.optionsOrPosition;
|
|
114
|
+
if (value == null) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
if (typeof value === 'number') {
|
|
118
|
+
return { offset: value };
|
|
119
|
+
}
|
|
120
|
+
if (typeof value === 'string') {
|
|
121
|
+
throw new Error(`Plane: position '${value}' is only valid for an edge source`);
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
63
125
|
getFromSceneObject(sceneObject) {
|
|
64
126
|
const shapes = sceneObject.getShapes();
|
|
65
127
|
console.log(`Plane: Retrieved ${shapes.length} shapes from selection`, shapes);
|
|
@@ -82,7 +144,7 @@ export class PlaneFromObject extends PlaneObjectBase {
|
|
|
82
144
|
return [];
|
|
83
145
|
}
|
|
84
146
|
createCopy(remap) {
|
|
85
|
-
return new PlaneFromObject(this, this.
|
|
147
|
+
return new PlaneFromObject(this, this.optionsOrPosition);
|
|
86
148
|
}
|
|
87
149
|
compareTo(other) {
|
|
88
150
|
if (!(other instanceof PlaneFromObject)) {
|
|
@@ -94,7 +156,7 @@ export class PlaneFromObject extends PlaneObjectBase {
|
|
|
94
156
|
if (!this.sourceObject.compareTo(other.sourceObject)) {
|
|
95
157
|
return false;
|
|
96
158
|
}
|
|
97
|
-
if (JSON.stringify(this.
|
|
159
|
+
if (JSON.stringify(this.optionsOrPosition) !== JSON.stringify(other.optionsOrPosition)) {
|
|
98
160
|
return false;
|
|
99
161
|
}
|
|
100
162
|
return true;
|
|
@@ -109,8 +171,39 @@ export class PlaneFromObject extends PlaneObjectBase {
|
|
|
109
171
|
xDirection: plane.xDirection,
|
|
110
172
|
yDirection: plane.yDirection,
|
|
111
173
|
normal: plane.normal,
|
|
112
|
-
options: this.
|
|
174
|
+
options: this.optionsOrPosition,
|
|
113
175
|
center: this.getState('plane-center') || plane.origin,
|
|
114
176
|
};
|
|
115
177
|
}
|
|
116
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Evaluates the point and unit (forward) tangent on `edge` at a normalized
|
|
181
|
+
* position `t` (`0` = start, `1` = end), measured by arc length.
|
|
182
|
+
*/
|
|
183
|
+
function sampleEdgeFrame(edge, t) {
|
|
184
|
+
const wire = WireOps.makeWireFromEdges([edge]);
|
|
185
|
+
const sampler = new PathSampler(wire);
|
|
186
|
+
try {
|
|
187
|
+
return sampler.evalAt(t * sampler.length);
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
sampler.dispose();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function normalizeEdgePosition(position) {
|
|
194
|
+
if (position === undefined) {
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
if (typeof position === 'number') {
|
|
198
|
+
return position;
|
|
199
|
+
}
|
|
200
|
+
switch (position) {
|
|
201
|
+
case 'start':
|
|
202
|
+
return 0;
|
|
203
|
+
case 'middle':
|
|
204
|
+
return 0.5;
|
|
205
|
+
case 'end':
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
throw new Error("Plane: an edge plane takes a 0–1 position or 'start'/'middle'/'end', not transform options");
|
|
209
|
+
}
|
|
@@ -12,22 +12,13 @@ export declare class SweepOps {
|
|
|
12
12
|
static makeSweep(spineWire: Wire, profileFaces: Face[]): SweepResult;
|
|
13
13
|
/** Sweep a single wire along the spine with a fixed binormal. */
|
|
14
14
|
private static sweepWire;
|
|
15
|
-
/** Unit tangent of the spine wire at its first parameter. */
|
|
16
|
-
private static getSpineTangent;
|
|
17
|
-
/** World-space centroid of a planar face (uses surface-area properties). */
|
|
18
|
-
private static getFaceCentroid;
|
|
19
15
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* frame A onto frame B (i.e., A's origin → B's origin, A's axes → B's
|
|
25
|
-
* axes). To move the profile *from* its current frame *to* the canonical
|
|
26
|
-
* frame, we pass the canonical frame as A and the profile's frame as B —
|
|
27
|
-
* counterintuitive but matches OCC's convention as verified empirically.
|
|
28
|
-
*
|
|
29
|
-
* The canonical frame is what OCC's MakePipeShell expects when
|
|
30
|
-
* WithContact/WithCorrection are both false: `Saxe = gp_Ax3(0, +Z, +X)`.
|
|
16
|
+
* The axis the spine's tangent rotates around, = normalize(Σ Tᵢ × Tᵢ₊₁) over
|
|
17
|
+
* tangents sampled along the spine. For a planar spine this is the plane
|
|
18
|
+
* normal; for a helix it is the coil axis. For a straight spine the tangent
|
|
19
|
+
* is constant, every cross product vanishes, and it returns null.
|
|
31
20
|
*/
|
|
32
|
-
private static
|
|
21
|
+
private static tangentRotationAxis;
|
|
22
|
+
/** Unit tangent of the spine wire at its first parameter. */
|
|
23
|
+
private static getSpineTangent;
|
|
33
24
|
}
|
package/lib/dist/oc/sweep-ops.js
CHANGED
|
@@ -17,58 +17,40 @@ export class SweepOps {
|
|
|
17
17
|
let firstShape = null;
|
|
18
18
|
let lastShape = null;
|
|
19
19
|
const profilePlane = profileFaces[0].getPlane();
|
|
20
|
-
// Fixed binormal
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
const [binormalDir, disposeBinormal] = Convert.toGpDir(binormalVec);
|
|
27
|
-
// Decide whether to pre-canonicalize the profile.
|
|
28
|
-
//
|
|
29
|
-
// OCC's `MakePipeShell.Add(_, false, true)` (no contact, with correction)
|
|
30
|
-
// works for most profile orientations: it rotates the profile to align
|
|
31
|
-
// its plane normal with the spine tangent, around an axis given by
|
|
32
|
-
// `profile.normal × spine.tangent`. That axis is well-defined unless
|
|
33
|
-
// the two are *anti-parallel* — in which case the cross product is
|
|
34
|
-
// zero, the rotation axis is undefined, and OCC produces a degenerate
|
|
35
|
-
// sweep.
|
|
20
|
+
// Fixed binormal for MakePipeShell's `SetMode`: it locks the section's
|
|
21
|
+
// "up", so the profile keeps a constant angle to it instead of twisting
|
|
22
|
+
// along the spine. The correct direction is the axis the spine's tangent
|
|
23
|
+
// rotates around — the plane normal for a planar spine, the coil axis for a
|
|
24
|
+
// helix. The tangent keeps a constant, non-zero angle to that axis, so the
|
|
25
|
+
// section never flips and the result is a clean coil.
|
|
36
26
|
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// `
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
27
|
+
// The profile plane's own "up" (used previously) only works when it happens
|
|
28
|
+
// to equal that axis — true for a profile sketched on a world plane, but
|
|
29
|
+
// NOT for a plane built off a helix, whose in-plane axes are arbitrary.
|
|
30
|
+
// A wrong (e.g. roughly horizontal) binormal lets the helix tangent rotate
|
|
31
|
+
// into it, collapsing `Normal = BiNormal × Tangent` ~twice per turn and
|
|
32
|
+
// shredding the section into a self-intersecting ribbon. A straight spine
|
|
33
|
+
// has no rotation axis (the cross products vanish); there the profile's up
|
|
34
|
+
// is well-defined and never aligns with the constant tangent, so use it.
|
|
35
|
+
const spineAxis = SweepOps.tangentRotationAxis(spineWire.getShape());
|
|
36
|
+
const binormalVec = spineAxis ?? profilePlane.yDirection;
|
|
37
|
+
const [binormalDir, disposeBinormal] = Convert.toGpDir(binormalVec);
|
|
38
|
+
// `Add(_, false, true)` (no contact, with correction) rotates the profile
|
|
39
|
+
// to sit perpendicular to the spine tangent, about an axis given by
|
|
40
|
+
// `profile.normal × spine.tangent`. That axis is undefined when the two are
|
|
41
|
+
// anti-parallel — but then the profile plane is *already* perpendicular to
|
|
42
|
+
// the spine (its normal is ∥ -tangent), so no correction is needed: skip it
|
|
43
|
+
// and keep the profile's drawn position.
|
|
45
44
|
const spineTangent = SweepOps.getSpineTangent(spineWire.getShape());
|
|
46
45
|
const isAntiParallel = profilePlane.normal.dot(spineTangent) < -0.999;
|
|
47
|
-
|
|
48
|
-
let withCorrection = true;
|
|
49
|
-
if (isAntiParallel) {
|
|
50
|
-
const profileCentroid = SweepOps.getFaceCentroid(profileFaces[0].getShape());
|
|
51
|
-
trsf = SweepOps.profileToCanonicalFrameTrsf(profileCentroid, profilePlane.normal, profilePlane.xDirection);
|
|
52
|
-
withCorrection = false;
|
|
53
|
-
}
|
|
46
|
+
const withCorrection = !isAntiParallel;
|
|
54
47
|
try {
|
|
55
48
|
for (const face of profileFaces) {
|
|
56
|
-
|
|
57
|
-
let transformer = null;
|
|
58
|
-
if (trsf) {
|
|
59
|
-
transformer = new oc.BRepBuilderAPI_Transform(trsf);
|
|
60
|
-
transformer.Perform(face.getShape(), true);
|
|
61
|
-
workingFace = transformer.Shape();
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
workingFace = face.getShape();
|
|
65
|
-
}
|
|
66
|
-
const ocFace = oc.TopoDS.Face(workingFace);
|
|
49
|
+
const ocFace = oc.TopoDS.Face(face.getShape());
|
|
67
50
|
const outerWire = oc.BRepTools.OuterWire(ocFace);
|
|
68
|
-
const innerWires =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
: face.getWires().map(w => w.getShape()).filter(w => !w.IsSame(outerWire));
|
|
51
|
+
const innerWires = face.getWires()
|
|
52
|
+
.map(w => w.getShape())
|
|
53
|
+
.filter(w => !w.IsSame(outerWire));
|
|
72
54
|
const outer = SweepOps.sweepWire(spineWire.getShape(), outerWire, binormalDir, withCorrection);
|
|
73
55
|
let resultSolid = outer.solid;
|
|
74
56
|
let resultFirst = outer.firstFace;
|
|
@@ -115,11 +97,9 @@ export class SweepOps {
|
|
|
115
97
|
for (const s of solids) {
|
|
116
98
|
allSolids.push(Solid.fromTopoDSSolid(Explorer.toSolid(s)));
|
|
117
99
|
}
|
|
118
|
-
transformer?.delete();
|
|
119
100
|
}
|
|
120
101
|
}
|
|
121
102
|
finally {
|
|
122
|
-
trsf?.delete();
|
|
123
103
|
disposeBinormal();
|
|
124
104
|
}
|
|
125
105
|
if (allSolids.length === 0) {
|
|
@@ -135,10 +115,10 @@ export class SweepOps {
|
|
|
135
115
|
static sweepWire(spine, profile, binormalDir, withCorrection) {
|
|
136
116
|
const oc = getOC();
|
|
137
117
|
const pipe = new oc.BRepOffsetAPI_MakePipeShell(spine);
|
|
138
|
-
// Fixed binormal (the
|
|
139
|
-
//
|
|
140
|
-
// and is well-defined on straight spines, where Frenet is not
|
|
141
|
-
// curvature ⇒ undefined normal).
|
|
118
|
+
// Fixed binormal (the spine's tangent-rotation axis; see makeSweep): keeps
|
|
119
|
+
// the swept section from twisting — a clean coil rather than a wobbling
|
|
120
|
+
// ribbon — and is well-defined on straight spines, where Frenet is not
|
|
121
|
+
// (zero curvature ⇒ undefined normal).
|
|
142
122
|
pipe.SetMode(binormalDir);
|
|
143
123
|
// Give the swept-surface approximation enough spans for tapered/tight
|
|
144
124
|
// helical spines (see MAX_PIPE_SEGMENTS) — at OCCT's default budget the
|
|
@@ -162,6 +142,41 @@ export class SweepOps {
|
|
|
162
142
|
pipe.delete();
|
|
163
143
|
return { solid, firstFace, lastFace };
|
|
164
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* The axis the spine's tangent rotates around, = normalize(Σ Tᵢ × Tᵢ₊₁) over
|
|
147
|
+
* tangents sampled along the spine. For a planar spine this is the plane
|
|
148
|
+
* normal; for a helix it is the coil axis. For a straight spine the tangent
|
|
149
|
+
* is constant, every cross product vanishes, and it returns null.
|
|
150
|
+
*/
|
|
151
|
+
static tangentRotationAxis(spine) {
|
|
152
|
+
const oc = getOC();
|
|
153
|
+
const adaptor = new oc.BRepAdaptor_CompCurve(spine, false);
|
|
154
|
+
const u0 = adaptor.FirstParameter();
|
|
155
|
+
const u1 = adaptor.LastParameter();
|
|
156
|
+
const SAMPLES = 64;
|
|
157
|
+
const tangents = [];
|
|
158
|
+
const pnt = new oc.gp_Pnt();
|
|
159
|
+
const vec = new oc.gp_Vec();
|
|
160
|
+
for (let i = 0; i <= SAMPLES; i++) {
|
|
161
|
+
const u = u0 + ((u1 - u0) * i) / SAMPLES;
|
|
162
|
+
adaptor.D1(u, pnt, vec);
|
|
163
|
+
const t = new Vector3d(vec.X(), vec.Y(), vec.Z());
|
|
164
|
+
if (t.length() > 1e-9) {
|
|
165
|
+
tangents.push(t.normalize());
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
pnt.delete();
|
|
169
|
+
vec.delete();
|
|
170
|
+
adaptor.delete();
|
|
171
|
+
let axis = new Vector3d(0, 0, 0);
|
|
172
|
+
for (let i = 0; i + 1 < tangents.length; i++) {
|
|
173
|
+
axis = axis.add(tangents[i].cross(tangents[i + 1]));
|
|
174
|
+
}
|
|
175
|
+
if (axis.length() < 1e-6) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
return axis.normalize();
|
|
179
|
+
}
|
|
165
180
|
/** Unit tangent of the spine wire at its first parameter. */
|
|
166
181
|
static getSpineTangent(spine) {
|
|
167
182
|
const oc = getOC();
|
|
@@ -177,51 +192,4 @@ export class SweepOps {
|
|
|
177
192
|
adaptor.delete();
|
|
178
193
|
return tangent;
|
|
179
194
|
}
|
|
180
|
-
/** World-space centroid of a planar face (uses surface-area properties). */
|
|
181
|
-
static getFaceCentroid(face) {
|
|
182
|
-
const oc = getOC();
|
|
183
|
-
const ocFace = oc.TopoDS.Face(face);
|
|
184
|
-
const props = new oc.GProp_GProps();
|
|
185
|
-
oc.BRepGProp.SurfaceProperties(ocFace, props, false, false);
|
|
186
|
-
const c = props.CentreOfMass();
|
|
187
|
-
const out = new Vector3d(c.X(), c.Y(), c.Z());
|
|
188
|
-
c.delete();
|
|
189
|
-
props.delete();
|
|
190
|
-
return out;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Build a world-to-world trsf that lays a planar face flat in world XY
|
|
194
|
-
* with its centroid at the world origin.
|
|
195
|
-
*
|
|
196
|
-
* `gp_Trsf::SetTransformation(A, B)` builds the transformation that maps
|
|
197
|
-
* frame A onto frame B (i.e., A's origin → B's origin, A's axes → B's
|
|
198
|
-
* axes). To move the profile *from* its current frame *to* the canonical
|
|
199
|
-
* frame, we pass the canonical frame as A and the profile's frame as B —
|
|
200
|
-
* counterintuitive but matches OCC's convention as verified empirically.
|
|
201
|
-
*
|
|
202
|
-
* The canonical frame is what OCC's MakePipeShell expects when
|
|
203
|
-
* WithContact/WithCorrection are both false: `Saxe = gp_Ax3(0, +Z, +X)`.
|
|
204
|
-
*/
|
|
205
|
-
static profileToCanonicalFrameTrsf(centroid, normal, xDir) {
|
|
206
|
-
const oc = getOC();
|
|
207
|
-
const [originPnt, disposeOriginPnt] = Convert.toGpPnt(centroid);
|
|
208
|
-
const [normalDirGp, disposeNormalDir] = Convert.toGpDir(normal);
|
|
209
|
-
const [xDirGp, disposeXDirGp] = Convert.toGpDir(xDir);
|
|
210
|
-
const profileAx3 = new oc.gp_Ax3(originPnt, normalDirGp, xDirGp);
|
|
211
|
-
const zeroPnt = new oc.gp_Pnt(0, 0, 0);
|
|
212
|
-
const zDir = new oc.gp_Dir(0, 0, 1);
|
|
213
|
-
const xDirWorld = new oc.gp_Dir(1, 0, 0);
|
|
214
|
-
const canonicalAx3 = new oc.gp_Ax3(zeroPnt, zDir, xDirWorld);
|
|
215
|
-
const trsf = new oc.gp_Trsf();
|
|
216
|
-
trsf.SetTransformation(canonicalAx3, profileAx3);
|
|
217
|
-
profileAx3.delete();
|
|
218
|
-
canonicalAx3.delete();
|
|
219
|
-
zeroPnt.delete();
|
|
220
|
-
zDir.delete();
|
|
221
|
-
xDirWorld.delete();
|
|
222
|
-
disposeOriginPnt();
|
|
223
|
-
disposeNormalDir();
|
|
224
|
-
disposeXDirGp();
|
|
225
|
-
return trsf;
|
|
226
|
-
}
|
|
227
195
|
}
|
|
@@ -11,19 +11,6 @@ export declare class ThinFaceMaker {
|
|
|
11
11
|
static make(edges: (Wire | Edge)[], plane: Plane, offset1: number, offset2?: number): ThinFaceResult;
|
|
12
12
|
private static makeSingleOffsetFace;
|
|
13
13
|
private static makeDualOffsetFace;
|
|
14
|
-
/**
|
|
15
|
-
* Offsets a wire by the given distance, handling both closed and open wires.
|
|
16
|
-
* For closed wires, WireOps.offsetWire handles negative distances natively.
|
|
17
|
-
* For open wires, negative distances are handled by reversing the wire,
|
|
18
|
-
* offsetting with the absolute value, then reversing back.
|
|
19
|
-
*
|
|
20
|
-
* If the wire-only offset throws (e.g. "Offset wire is not closed." on
|
|
21
|
-
* wires whose corners are GeomAbs_OffsetCurve segments from `offset()` over
|
|
22
|
-
* a drafted body's filleted bottom), retries with a planar face as the
|
|
23
|
-
* offset spine — that path supplies an explicit normal which keeps the
|
|
24
|
-
* algorithm stable on the same input.
|
|
25
|
-
*/
|
|
26
|
-
private static doOffset;
|
|
27
14
|
/**
|
|
28
15
|
* Merges adjacent edges that share the same underlying curve into a single
|
|
29
16
|
* edge (e.g. two conic-arc segments at a filleted corner produced by
|
|
@@ -33,12 +20,6 @@ export declare class ThinFaceMaker {
|
|
|
33
20
|
* upgrader produces no usable result.
|
|
34
21
|
*/
|
|
35
22
|
private static unifyWireEdges;
|
|
36
|
-
/**
|
|
37
|
-
* Offsets an open wire on a given plane, using a planar face as reference
|
|
38
|
-
* so that BRepOffsetAPI_MakeOffset knows the offset direction.
|
|
39
|
-
* Only handles positive distances — use doOffset for sign handling.
|
|
40
|
-
*/
|
|
41
|
-
private static offsetWireOnPlane;
|
|
42
23
|
/**
|
|
43
24
|
* Finds face edges that geometrically match the given wire edges by comparing midpoints.
|
|
44
25
|
* This is needed because wire reversal (ShapeExtend_WireData.Reverse) creates new TShapes,
|