fluidcad 0.0.37 → 0.0.39
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/2d/aline.d.ts +0 -8
- package/lib/dist/core/2d/aline.js +3 -22
- package/lib/dist/core/2d/arc.d.ts +1 -23
- package/lib/dist/core/2d/arc.js +11 -28
- package/lib/dist/core/2d/circle.d.ts +1 -8
- package/lib/dist/core/2d/circle.js +7 -24
- package/lib/dist/core/2d/ellipse.d.ts +1 -17
- package/lib/dist/core/2d/ellipse.js +7 -45
- package/lib/dist/core/2d/hline.d.ts +0 -7
- package/lib/dist/core/2d/hline.js +5 -27
- package/lib/dist/core/2d/intersect.d.ts +0 -7
- package/lib/dist/core/2d/intersect.js +0 -13
- package/lib/dist/core/2d/line.d.ts +1 -8
- package/lib/dist/core/2d/line.js +6 -24
- package/lib/dist/core/2d/offset.d.ts +1 -11
- package/lib/dist/core/2d/offset.js +0 -16
- package/lib/dist/core/2d/polygon.d.ts +1 -17
- package/lib/dist/core/2d/polygon.js +18 -37
- package/lib/dist/core/2d/project.d.ts +0 -7
- package/lib/dist/core/2d/project.js +0 -13
- package/lib/dist/core/2d/rect.d.ts +1 -9
- package/lib/dist/core/2d/rect.js +0 -19
- package/lib/dist/core/2d/slot.d.ts +0 -23
- package/lib/dist/core/2d/slot.js +18 -37
- package/lib/dist/core/2d/text.d.ts +0 -7
- package/lib/dist/core/2d/text.js +0 -14
- package/lib/dist/core/2d/vline.d.ts +0 -7
- package/lib/dist/core/2d/vline.js +5 -23
- package/lib/dist/core/fillet.js +7 -0
- package/lib/dist/core/index.d.ts +1 -1
- package/lib/dist/core/interfaces.d.ts +43 -0
- package/lib/dist/core/plane.d.ts +26 -6
- package/lib/dist/core/plane.js +21 -44
- package/lib/dist/features/2d/connect.d.ts +2 -0
- package/lib/dist/features/2d/connect.js +3 -0
- package/lib/dist/features/2d/intersect.js +4 -1
- package/lib/dist/features/2d/offset.js +2 -2
- package/lib/dist/features/frozen-geometry.d.ts +29 -0
- package/lib/dist/features/frozen-geometry.js +62 -0
- package/lib/dist/features/loft.d.ts +38 -1
- package/lib/dist/features/loft.js +133 -8
- package/lib/dist/features/plane-from-object.d.ts +16 -4
- package/lib/dist/features/plane-from-object.js +101 -8
- package/lib/dist/features/rib.js +11 -1
- package/lib/dist/features/shell.d.ts +1 -1
- package/lib/dist/features/shell.js +9 -0
- package/lib/dist/features/sweep.d.ts +11 -1
- package/lib/dist/features/sweep.js +43 -7
- package/lib/dist/helpers/clone-transform.js +62 -5
- package/lib/dist/helpers/scene-helpers.d.ts +12 -0
- package/lib/dist/helpers/scene-helpers.js +26 -0
- package/lib/dist/math/bspline-interpolation.d.ts +33 -6
- package/lib/dist/math/bspline-interpolation.js +187 -84
- package/lib/dist/oc/loft/constrained-loft.d.ts +32 -0
- package/lib/dist/oc/loft/constrained-loft.js +61 -0
- package/lib/dist/oc/loft/curve-data.d.ts +18 -0
- package/lib/dist/oc/loft/curve-data.js +54 -0
- package/lib/dist/oc/loft/curve-eval.d.ts +23 -0
- package/lib/dist/oc/loft/curve-eval.js +84 -0
- package/lib/dist/oc/loft/guided-loft.d.ts +89 -0
- package/lib/dist/oc/loft/guided-loft.js +368 -0
- package/lib/dist/oc/loft/section-compatibility.d.ts +131 -0
- package/lib/dist/oc/loft/section-compatibility.js +481 -0
- package/lib/dist/oc/loft/section-curve.d.ts +53 -0
- package/lib/dist/oc/loft/section-curve.js +264 -0
- package/lib/dist/oc/loft/skinning.d.ts +104 -0
- package/lib/dist/oc/loft/skinning.js +299 -0
- package/lib/dist/oc/loft-ops.d.ts +28 -1
- package/lib/dist/oc/loft-ops.js +31 -1
- package/lib/dist/oc/ncollection.d.ts +19 -0
- package/lib/dist/oc/ncollection.js +69 -0
- package/lib/dist/oc/rib-ops.js +5 -17
- 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-extend-ops.d.ts +12 -0
- package/lib/dist/oc/wire-extend-ops.js +42 -0
- package/lib/dist/oc/wire-ops.d.ts +28 -2
- package/lib/dist/oc/wire-ops.js +88 -4
- package/lib/dist/oc/wrap-ops.js +8 -15
- package/lib/dist/tests/features/2d/circle.test.js +0 -10
- package/lib/dist/tests/features/2d/ellipse.test.js +0 -30
- package/lib/dist/tests/features/2d/line.test.js +0 -5
- package/lib/dist/tests/features/2d/offset.test.js +74 -11
- package/lib/dist/tests/features/2d/polygon.test.js +0 -16
- package/lib/dist/tests/features/2d/rect.test.js +0 -11
- package/lib/dist/tests/features/2d/slot.test.js +0 -8
- package/lib/dist/tests/features/fillet2d.test.js +37 -1
- package/lib/dist/tests/features/loft-conditions.test.d.ts +1 -0
- package/lib/dist/tests/features/loft-conditions.test.js +246 -0
- package/lib/dist/tests/features/loft-guides.test.d.ts +1 -0
- package/lib/dist/tests/features/loft-guides.test.js +276 -0
- package/lib/dist/tests/features/plane.test.js +95 -0
- package/lib/dist/tests/features/repeat-sweep-borrowed-path.test.d.ts +1 -0
- package/lib/dist/tests/features/repeat-sweep-borrowed-path.test.js +35 -0
- package/lib/dist/tests/features/rib.test.js +50 -0
- package/lib/dist/tests/features/shell.test.js +35 -1
- package/lib/dist/tests/features/sweep.test.js +127 -1
- package/lib/dist/tests/features/text.test.js +33 -14
- package/lib/dist/tests/math/bspline-interpolation.test.js +97 -1
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/llm-docs/api/index.json +1 -1
- package/llm-docs/api/types/loft.md +36 -1
- package/llm-docs/api/types/sweep.md +13 -1
- package/llm-docs/api/types/text.md +2 -2
- package/llm-docs/index.json +5 -5
- 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
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
import { Solid } from "../common/solid.js";
|
|
2
2
|
import { Wire } from "../common/wire.js";
|
|
3
|
+
import { LoftEndCondition, LoftConditionKind } from "./loft/constrained-loft.js";
|
|
4
|
+
export type { LoftEndCondition, LoftConditionKind };
|
|
5
|
+
export interface LoftOptions {
|
|
6
|
+
/** Constrains how the surface leaves the first profile. */
|
|
7
|
+
startCondition?: LoftEndCondition;
|
|
8
|
+
/** Constrains how the surface arrives at the last profile. */
|
|
9
|
+
endCondition?: LoftEndCondition;
|
|
10
|
+
/** One or two side rails the loft surface must follow. */
|
|
11
|
+
guides?: Wire[];
|
|
12
|
+
}
|
|
3
13
|
export declare class LoftOps {
|
|
4
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Lofts through the section wires. Plain lofts run OCC's ThruSections;
|
|
16
|
+
* guides dispatch to `GuidedLoft` (virtual sections carried onto the
|
|
17
|
+
* rails) and start/end conditions to `ConstrainedLoft` (derivative-pinned
|
|
18
|
+
* skinning) — both in-house skins, since OCC can neither constrain end
|
|
19
|
+
* tangency nor follow rails without distorting sections. Conditions
|
|
20
|
+
* compose with guides: the condition fades out around each guide contact
|
|
21
|
+
* (the rails own their sides of the surface).
|
|
22
|
+
*/
|
|
23
|
+
static makeLoft(wires: Wire[], options?: LoftOptions): Solid[];
|
|
24
|
+
/**
|
|
25
|
+
* Thin-walled loft with start/end conditions: both walls are skinned with
|
|
26
|
+
* the same conditions and assembled directly with ring caps — see
|
|
27
|
+
* `ConstrainedLoft.buildThin`. (The condition-less thin loft stays on the
|
|
28
|
+
* legacy ThruSections + boolean path in the feature layer.)
|
|
29
|
+
*/
|
|
30
|
+
static makeThinLoft(outerWires: Wire[], innerWires: Wire[], options: LoftOptions): Solid[];
|
|
31
|
+
private static makeThruSectionsLoft;
|
|
5
32
|
}
|
package/lib/dist/oc/loft-ops.js
CHANGED
|
@@ -1,8 +1,38 @@
|
|
|
1
1
|
import { getOC } from "./init.js";
|
|
2
2
|
import { Explorer } from "./explorer.js";
|
|
3
3
|
import { Solid } from "../common/solid.js";
|
|
4
|
+
import { ConstrainedLoft } from "./loft/constrained-loft.js";
|
|
5
|
+
import { GuidedLoft } from "./loft/guided-loft.js";
|
|
4
6
|
export class LoftOps {
|
|
5
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Lofts through the section wires. Plain lofts run OCC's ThruSections;
|
|
9
|
+
* guides dispatch to `GuidedLoft` (virtual sections carried onto the
|
|
10
|
+
* rails) and start/end conditions to `ConstrainedLoft` (derivative-pinned
|
|
11
|
+
* skinning) — both in-house skins, since OCC can neither constrain end
|
|
12
|
+
* tangency nor follow rails without distorting sections. Conditions
|
|
13
|
+
* compose with guides: the condition fades out around each guide contact
|
|
14
|
+
* (the rails own their sides of the surface).
|
|
15
|
+
*/
|
|
16
|
+
static makeLoft(wires, options) {
|
|
17
|
+
const guides = options?.guides ?? [];
|
|
18
|
+
if (guides.length > 0) {
|
|
19
|
+
return GuidedLoft.build(wires, guides, options?.startCondition, options?.endCondition);
|
|
20
|
+
}
|
|
21
|
+
if (options?.startCondition || options?.endCondition) {
|
|
22
|
+
return ConstrainedLoft.build(wires, options.startCondition, options.endCondition);
|
|
23
|
+
}
|
|
24
|
+
return LoftOps.makeThruSectionsLoft(wires);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Thin-walled loft with start/end conditions: both walls are skinned with
|
|
28
|
+
* the same conditions and assembled directly with ring caps — see
|
|
29
|
+
* `ConstrainedLoft.buildThin`. (The condition-less thin loft stays on the
|
|
30
|
+
* legacy ThruSections + boolean path in the feature layer.)
|
|
31
|
+
*/
|
|
32
|
+
static makeThinLoft(outerWires, innerWires, options) {
|
|
33
|
+
return ConstrainedLoft.buildThin(outerWires, innerWires, options.startCondition, options.endCondition);
|
|
34
|
+
}
|
|
35
|
+
static makeThruSectionsLoft(wires) {
|
|
6
36
|
const oc = getOC();
|
|
7
37
|
const thruSections = new oc.BRepOffsetAPI_ThruSections(true, false, 1e-6);
|
|
8
38
|
for (const wire of wires) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { NCollection_Array1_double, NCollection_Array1_int, NCollection_Array1_gp_Pnt, NCollection_Array1_gp_Pnt2d, NCollection_Array2_gp_Pnt, NCollection_Array2_double } from "ocjs-fluidcad";
|
|
2
|
+
import type { Disposable } from "./convert.js";
|
|
3
|
+
import { Point2D } from "../math/point.js";
|
|
4
|
+
/**
|
|
5
|
+
* Builders for the 1-based NCollection arrays that OCC geometry constructors
|
|
6
|
+
* consume. Each returns the codebase's `Disposable` pair so call sites free
|
|
7
|
+
* the native array (and nothing else) once the consumer has copied it.
|
|
8
|
+
*/
|
|
9
|
+
export declare class NCollections {
|
|
10
|
+
static toArray1Double(values: ReadonlyArray<number>): Disposable<NCollection_Array1_double>;
|
|
11
|
+
static toArray1Int(values: ReadonlyArray<number>): Disposable<NCollection_Array1_int>;
|
|
12
|
+
/** Each point is an [x, y, z] triple. */
|
|
13
|
+
static toArray1Pnt(points: ReadonlyArray<ReadonlyArray<number>>): Disposable<NCollection_Array1_gp_Pnt>;
|
|
14
|
+
static toArray1Pnt2d(points: ReadonlyArray<Point2D>): Disposable<NCollection_Array1_gp_Pnt2d>;
|
|
15
|
+
/** Grid of [x, y, z] triples, indexed [row][column]. */
|
|
16
|
+
static toArray2Pnt(grid: ReadonlyArray<ReadonlyArray<ReadonlyArray<number>>>): Disposable<NCollection_Array2_gp_Pnt>;
|
|
17
|
+
/** Grid of numbers, indexed [row][column]. */
|
|
18
|
+
static toArray2Double(grid: ReadonlyArray<ReadonlyArray<number>>): Disposable<NCollection_Array2_double>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { getOC } from "./init.js";
|
|
2
|
+
/**
|
|
3
|
+
* Builders for the 1-based NCollection arrays that OCC geometry constructors
|
|
4
|
+
* consume. Each returns the codebase's `Disposable` pair so call sites free
|
|
5
|
+
* the native array (and nothing else) once the consumer has copied it.
|
|
6
|
+
*/
|
|
7
|
+
export class NCollections {
|
|
8
|
+
static toArray1Double(values) {
|
|
9
|
+
const oc = getOC();
|
|
10
|
+
const array = new oc.NCollection_Array1_double(1, values.length);
|
|
11
|
+
for (let i = 0; i < values.length; i++) {
|
|
12
|
+
array.SetValue(i + 1, values[i]);
|
|
13
|
+
}
|
|
14
|
+
return [array, () => array.delete()];
|
|
15
|
+
}
|
|
16
|
+
static toArray1Int(values) {
|
|
17
|
+
const oc = getOC();
|
|
18
|
+
const array = new oc.NCollection_Array1_int(1, values.length);
|
|
19
|
+
for (let i = 0; i < values.length; i++) {
|
|
20
|
+
array.SetValue(i + 1, values[i]);
|
|
21
|
+
}
|
|
22
|
+
return [array, () => array.delete()];
|
|
23
|
+
}
|
|
24
|
+
/** Each point is an [x, y, z] triple. */
|
|
25
|
+
static toArray1Pnt(points) {
|
|
26
|
+
const oc = getOC();
|
|
27
|
+
const array = new oc.NCollection_Array1_gp_Pnt(1, points.length);
|
|
28
|
+
for (let i = 0; i < points.length; i++) {
|
|
29
|
+
const point = new oc.gp_Pnt(points[i][0], points[i][1], points[i][2]);
|
|
30
|
+
array.SetValue(i + 1, point);
|
|
31
|
+
point.delete();
|
|
32
|
+
}
|
|
33
|
+
return [array, () => array.delete()];
|
|
34
|
+
}
|
|
35
|
+
static toArray1Pnt2d(points) {
|
|
36
|
+
const oc = getOC();
|
|
37
|
+
const array = new oc.NCollection_Array1_gp_Pnt2d(1, points.length);
|
|
38
|
+
for (let i = 0; i < points.length; i++) {
|
|
39
|
+
const point = new oc.gp_Pnt2d(points[i].x, points[i].y);
|
|
40
|
+
array.SetValue(i + 1, point);
|
|
41
|
+
point.delete();
|
|
42
|
+
}
|
|
43
|
+
return [array, () => array.delete()];
|
|
44
|
+
}
|
|
45
|
+
/** Grid of [x, y, z] triples, indexed [row][column]. */
|
|
46
|
+
static toArray2Pnt(grid) {
|
|
47
|
+
const oc = getOC();
|
|
48
|
+
const array = new oc.NCollection_Array2_gp_Pnt(1, grid.length, 1, grid[0].length);
|
|
49
|
+
for (let row = 0; row < grid.length; row++) {
|
|
50
|
+
for (let col = 0; col < grid[row].length; col++) {
|
|
51
|
+
const point = new oc.gp_Pnt(grid[row][col][0], grid[row][col][1], grid[row][col][2]);
|
|
52
|
+
array.SetValue(row + 1, col + 1, point);
|
|
53
|
+
point.delete();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return [array, () => array.delete()];
|
|
57
|
+
}
|
|
58
|
+
/** Grid of numbers, indexed [row][column]. */
|
|
59
|
+
static toArray2Double(grid) {
|
|
60
|
+
const oc = getOC();
|
|
61
|
+
const array = new oc.NCollection_Array2_double(1, grid.length, 1, grid[0].length);
|
|
62
|
+
for (let row = 0; row < grid.length; row++) {
|
|
63
|
+
for (let col = 0; col < grid[row].length; col++) {
|
|
64
|
+
array.SetValue(row + 1, col + 1, grid[row][col]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return [array, () => array.delete()];
|
|
68
|
+
}
|
|
69
|
+
}
|
package/lib/dist/oc/rib-ops.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Point } from "../math/point.js";
|
|
|
4
4
|
import { Vector3d } from "../math/vector3d.js";
|
|
5
5
|
import { Matrix4 } from "../math/matrix4.js";
|
|
6
6
|
import { WireOps } from "./wire-ops.js";
|
|
7
|
+
import { WireExtendOps } from "./wire-extend-ops.js";
|
|
7
8
|
import { FaceOps } from "./face-ops.js";
|
|
8
9
|
import { EdgeOps } from "./edge-ops.js";
|
|
9
10
|
import { ShapeOps } from "./shape-ops.js";
|
|
@@ -144,17 +145,9 @@ export class RibOps {
|
|
|
144
145
|
// overlaps every cavity boundary regardless of curvature (drafted cones,
|
|
145
146
|
// fillets, etc.) — so the cut produces a clean blend on every face it touches.
|
|
146
147
|
static extendSpineWire(spineWire, scopeShapes, _plane) {
|
|
147
|
-
|
|
148
|
-
if (edges.length === 0) {
|
|
148
|
+
if (spineWire.getEdges().length === 0) {
|
|
149
149
|
return spineWire;
|
|
150
150
|
}
|
|
151
|
-
const firstVertex = spineWire.getFirstVertex().toPoint();
|
|
152
|
-
const lastVertex = spineWire.getLastVertex().toPoint();
|
|
153
|
-
const lastEdge = edges[edges.length - 1];
|
|
154
|
-
const endTangent = EdgeOps.getEdgeTangentAtEnd(lastEdge).normalize();
|
|
155
|
-
const firstEdge = edges[0];
|
|
156
|
-
const firstEdgeEnd = EdgeOps.getLastVertex(firstEdge).toPoint();
|
|
157
|
-
const startTangent = firstVertex.vectorTo(firstEdgeEnd).normalize();
|
|
158
151
|
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
159
152
|
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
160
153
|
for (const s of scopeShapes) {
|
|
@@ -184,14 +177,9 @@ export class RibOps {
|
|
|
184
177
|
return spineWire;
|
|
185
178
|
}
|
|
186
179
|
const ext = 2 * diag;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
EdgeOps.makeLineEdge(startExtPoint, firstVertex),
|
|
191
|
-
...edges,
|
|
192
|
-
EdgeOps.makeLineEdge(lastVertex, endExtPoint),
|
|
193
|
-
];
|
|
194
|
-
return WireOps.makeWireFromEdges(newEdges);
|
|
180
|
+
let extended = WireExtendOps.extendWire(spineWire, "start", ext);
|
|
181
|
+
extended = WireExtendOps.extendWire(extended, "end", ext);
|
|
182
|
+
return extended;
|
|
195
183
|
}
|
|
196
184
|
static computeExtrudeDistanceAlongDirection(direction, origin, scopeShapes) {
|
|
197
185
|
let maxDist = 0;
|
|
@@ -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,
|
|
@@ -4,7 +4,6 @@ import { WireOps } from "./wire-ops.js";
|
|
|
4
4
|
import { FaceOps } from "./face-ops.js";
|
|
5
5
|
import { EdgeOps } from "./edge-ops.js";
|
|
6
6
|
import { Explorer } from "./explorer.js";
|
|
7
|
-
import { Convert } from "./convert.js";
|
|
8
7
|
import { getOC } from "./init.js";
|
|
9
8
|
export class ThinFaceMaker {
|
|
10
9
|
static make(edges, plane, offset1, offset2) {
|
|
@@ -48,7 +47,7 @@ export class ThinFaceMaker {
|
|
|
48
47
|
return { faces, inwardEdges, outwardEdges };
|
|
49
48
|
}
|
|
50
49
|
static makeSingleOffsetFace(wire, isClosed, plane, offset) {
|
|
51
|
-
const offsetWire =
|
|
50
|
+
const offsetWire = WireOps.offsetWireOnPlane(wire, offset, isClosed, plane);
|
|
52
51
|
if (isClosed) {
|
|
53
52
|
// Determine which wire is outer (larger) vs inner based on offset sign
|
|
54
53
|
const [outer, inner] = offset >= 0
|
|
@@ -77,8 +76,8 @@ export class ThinFaceMaker {
|
|
|
77
76
|
if (Math.sign(offset1) === Math.sign(offset2)) {
|
|
78
77
|
offset2 = -offset2;
|
|
79
78
|
}
|
|
80
|
-
const wire1 =
|
|
81
|
-
const wire2 =
|
|
79
|
+
const wire1 = WireOps.offsetWireOnPlane(wire, offset1, isClosed, plane);
|
|
80
|
+
const wire2 = WireOps.offsetWireOnPlane(wire, offset2, isClosed, plane);
|
|
82
81
|
if (isClosed) {
|
|
83
82
|
// The wire with the larger offset is the outer boundary
|
|
84
83
|
const [outer, inner] = offset1 >= offset2
|
|
@@ -102,34 +101,6 @@ export class ThinFaceMaker {
|
|
|
102
101
|
const outwardEdges = this.matchFaceEdgesByMidpoint(face, outwardWireEdges);
|
|
103
102
|
return { face, inwardEdges, outwardEdges };
|
|
104
103
|
}
|
|
105
|
-
/**
|
|
106
|
-
* Offsets a wire by the given distance, handling both closed and open wires.
|
|
107
|
-
* For closed wires, WireOps.offsetWire handles negative distances natively.
|
|
108
|
-
* For open wires, negative distances are handled by reversing the wire,
|
|
109
|
-
* offsetting with the absolute value, then reversing back.
|
|
110
|
-
*
|
|
111
|
-
* If the wire-only offset throws (e.g. "Offset wire is not closed." on
|
|
112
|
-
* wires whose corners are GeomAbs_OffsetCurve segments from `offset()` over
|
|
113
|
-
* a drafted body's filleted bottom), retries with a planar face as the
|
|
114
|
-
* offset spine — that path supplies an explicit normal which keeps the
|
|
115
|
-
* algorithm stable on the same input.
|
|
116
|
-
*/
|
|
117
|
-
static doOffset(wire, plane, distance, isClosed) {
|
|
118
|
-
if (!isClosed) {
|
|
119
|
-
if (distance < 0) {
|
|
120
|
-
const reversed = WireOps.reverseWire(wire);
|
|
121
|
-
const offsetResult = this.offsetWireOnPlane(reversed, plane, -distance, true);
|
|
122
|
-
return WireOps.reverseWire(offsetResult);
|
|
123
|
-
}
|
|
124
|
-
return this.offsetWireOnPlane(wire, plane, distance, true);
|
|
125
|
-
}
|
|
126
|
-
try {
|
|
127
|
-
return WireOps.offsetWire(wire, distance, false);
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
return this.offsetWireOnPlane(wire, plane, distance, false);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
104
|
/**
|
|
134
105
|
* Merges adjacent edges that share the same underlying curve into a single
|
|
135
106
|
* edge (e.g. two conic-arc segments at a filleted corner produced by
|
|
@@ -154,42 +125,6 @@ export class ThinFaceMaker {
|
|
|
154
125
|
}
|
|
155
126
|
return Wire.fromTopoDSWire(oc.TopoDS.Wire(wires[0]));
|
|
156
127
|
}
|
|
157
|
-
/**
|
|
158
|
-
* Offsets an open wire on a given plane, using a planar face as reference
|
|
159
|
-
* so that BRepOffsetAPI_MakeOffset knows the offset direction.
|
|
160
|
-
* Only handles positive distances — use doOffset for sign handling.
|
|
161
|
-
*/
|
|
162
|
-
static offsetWireOnPlane(wire, plane, distance, isOpen) {
|
|
163
|
-
const oc = getOC();
|
|
164
|
-
const [pln, disposePlane] = Convert.toGpPln(plane);
|
|
165
|
-
const faceMaker = new oc.BRepBuilderAPI_MakeFace(pln);
|
|
166
|
-
if (!faceMaker.IsDone()) {
|
|
167
|
-
faceMaker.delete();
|
|
168
|
-
disposePlane();
|
|
169
|
-
throw new Error("Failed to create reference face for thin offset");
|
|
170
|
-
}
|
|
171
|
-
const face = faceMaker.Face();
|
|
172
|
-
faceMaker.delete();
|
|
173
|
-
disposePlane();
|
|
174
|
-
const maker = new oc.BRepOffsetAPI_MakeOffset();
|
|
175
|
-
maker.Init(face, oc.GeomAbs_JoinType.GeomAbs_Arc, isOpen);
|
|
176
|
-
maker.AddWire(wire.getShape());
|
|
177
|
-
maker.Perform(distance, 0);
|
|
178
|
-
if (!maker.IsDone()) {
|
|
179
|
-
maker.delete();
|
|
180
|
-
throw new Error("Failed to offset wire for thin extrude");
|
|
181
|
-
}
|
|
182
|
-
const result = maker.Shape();
|
|
183
|
-
maker.delete();
|
|
184
|
-
if (Explorer.isWire(result)) {
|
|
185
|
-
return Wire.fromTopoDSWire(oc.TopoDS.Wire(result));
|
|
186
|
-
}
|
|
187
|
-
const wires = Explorer.findShapes(result, oc.TopAbs_ShapeEnum.TopAbs_WIRE);
|
|
188
|
-
if (wires.length === 0) {
|
|
189
|
-
throw new Error("Thin offset produced no usable wire");
|
|
190
|
-
}
|
|
191
|
-
return Wire.fromTopoDSWire(oc.TopoDS.Wire(wires[0]));
|
|
192
|
-
}
|
|
193
128
|
/**
|
|
194
129
|
* Finds face edges that geometrically match the given wire edges by comparing midpoints.
|
|
195
130
|
* This is needed because wire reversal (ShapeExtend_WireData.Reverse) creates new TShapes,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Wire } from "../common/wire.js";
|
|
2
|
+
export type WireEnd = "start" | "end";
|
|
3
|
+
export declare class WireExtendOps {
|
|
4
|
+
/**
|
|
5
|
+
* Extends an open wire beyond one end by `length`, continuing straight along
|
|
6
|
+
* the wire's true tangent there. The new straight edge joins the existing
|
|
7
|
+
* first/last vertex, so the result is a single connected wire.
|
|
8
|
+
*
|
|
9
|
+
* No-op for closed wires, empty wires, or `length <= 0`.
|
|
10
|
+
*/
|
|
11
|
+
static extendWire(wire: Wire, side: WireEnd, length: number): Wire;
|
|
12
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { EdgeOps } from "./edge-ops.js";
|
|
2
|
+
import { WireOps } from "./wire-ops.js";
|
|
3
|
+
import { PathSampler } from "./path-sampler.js";
|
|
4
|
+
export class WireExtendOps {
|
|
5
|
+
/**
|
|
6
|
+
* Extends an open wire beyond one end by `length`, continuing straight along
|
|
7
|
+
* the wire's true tangent there. The new straight edge joins the existing
|
|
8
|
+
* first/last vertex, so the result is a single connected wire.
|
|
9
|
+
*
|
|
10
|
+
* No-op for closed wires, empty wires, or `length <= 0`.
|
|
11
|
+
*/
|
|
12
|
+
static extendWire(wire, side, length) {
|
|
13
|
+
if (length <= 0) {
|
|
14
|
+
return wire;
|
|
15
|
+
}
|
|
16
|
+
const edges = wire.getEdges();
|
|
17
|
+
if (edges.length === 0) {
|
|
18
|
+
return wire;
|
|
19
|
+
}
|
|
20
|
+
const sampler = new PathSampler(wire);
|
|
21
|
+
try {
|
|
22
|
+
if (sampler.closed) {
|
|
23
|
+
return wire;
|
|
24
|
+
}
|
|
25
|
+
if (side === "start") {
|
|
26
|
+
// Tangent at the start points into the wire; extend the opposite way.
|
|
27
|
+
const startPt = wire.getFirstVertex().toPoint();
|
|
28
|
+
const inward = sampler.evalAt(0).tangent;
|
|
29
|
+
const extPt = startPt.add(inward.multiply(-length));
|
|
30
|
+
return WireOps.makeWireFromEdges([EdgeOps.makeLineEdge(extPt, startPt), ...edges]);
|
|
31
|
+
}
|
|
32
|
+
// Tangent at the end already points out of the wire.
|
|
33
|
+
const endPt = wire.getLastVertex().toPoint();
|
|
34
|
+
const outward = sampler.evalAt(sampler.length).tangent;
|
|
35
|
+
const extPt = endPt.add(outward.multiply(length));
|
|
36
|
+
return WireOps.makeWireFromEdges([...edges, EdgeOps.makeLineEdge(endPt, extPt)]);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
sampler.dispose();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|