@metamesh/modeling 0.7.0 → 0.8.0
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/CHANGELOG.md +24 -0
- package/README.md +76 -3
- package/dist/api/engine.d.ts +4 -0
- package/dist/api/geometry.d.ts +104 -19
- package/dist/api/index.d.ts +203 -18
- package/dist/api/text.d.ts +21 -7
- package/dist/engine/construction-operations.js +52 -10
- package/dist/engine/execution.js +55 -2
- package/dist/engine/export-3mf.js +17 -1
- package/dist/engine/finalization.js +27 -18
- package/dist/engine/inspection-operations.js +26 -22
- package/dist/engine/loft-operations.js +1 -1
- package/dist/engine/mechanical-operations.js +5 -8
- package/dist/engine/mesh-view-cache.js +23 -0
- package/dist/engine/native/adapter.js +2 -4
- package/dist/engine/native/distance.js +42 -0
- package/dist/engine/native/status.js +16 -0
- package/dist/engine/operation-environment.js +2 -1
- package/dist/engine/section-operations.js +3 -3
- package/dist/geometry/heightfield.js +101 -42
- package/dist/geometry/index.js +2 -0
- package/dist/geometry/loft-simple-mesh.js +2 -3
- package/dist/geometry/loft-triangulation.js +12 -9
- package/dist/geometry/loft.js +2 -6
- package/dist/geometry/mechanical/standards.js +2 -3
- package/dist/geometry/mechanical/threads.js +2 -3
- package/dist/geometry/mesh-centroid.js +36 -0
- package/dist/geometry/mesh-solid-intersections.js +123 -0
- package/dist/geometry/mesh-solid.js +284 -0
- package/dist/geometry/sweeps/sweep-closed.js +4 -4
- package/dist/geometry/sweeps/sweep-mesh.js +2 -6
- package/dist/geometry/sweeps/sweep-open.js +4 -4
- package/dist/geometry/sweeps/validation.js +1 -2
- package/dist/model/operations.js +10 -3
- package/dist/text/vector-text-layout.js +46 -3
- package/package.json +1 -1
package/dist/api/index.d.ts
CHANGED
|
@@ -225,6 +225,9 @@ export declare type CylinderOptions = CircularPrimitiveOptions & RadialSizingOpt
|
|
|
225
225
|
readonly radius?: never;
|
|
226
226
|
});
|
|
227
227
|
|
|
228
|
+
/** Measures the minimum separation between two nonempty solids in millimetres. */
|
|
229
|
+
export declare const distance: ModelingContext["distance"];
|
|
230
|
+
|
|
228
231
|
/**
|
|
229
232
|
* Uniform finishing vocabulary shared by applicable solid edges and profile corners.
|
|
230
233
|
*/
|
|
@@ -403,22 +406,57 @@ export declare type GridPatternOptions = {
|
|
|
403
406
|
readonly spacing: Vec3;
|
|
404
407
|
});
|
|
405
408
|
|
|
406
|
-
/** Constructs a closed
|
|
409
|
+
/** Constructs a closed rectilinear heightfield during the active synchronous build. */
|
|
407
410
|
export declare const heightfield: ModelingContext["heightfield"];
|
|
408
411
|
|
|
409
|
-
/**
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
412
|
+
/**
|
|
413
|
+
* A closed rectilinear heightfield above a flat base, with uniform or explicit XY samples in millimetres.
|
|
414
|
+
*
|
|
415
|
+
* Uniform sampling requires positive `size: [width, depth]` and positive integer `segments: [x, y]`.
|
|
416
|
+
* Optional `origin` sets the lower-left XY corner; omitting it centres the footprint on XY.
|
|
417
|
+
* Alternatively, supply `coordinates: { x, y }` with at least two strictly increasing finite
|
|
418
|
+
* absolute samples per axis, distinct in Float32. This infers the footprint and interval counts;
|
|
419
|
+
* `size`, `segments`, and `origin` cannot be combined with explicit coordinates.
|
|
420
|
+
*/
|
|
421
|
+
export declare type HeightfieldOptions = {
|
|
415
422
|
/** Absolute top Z at the supplied XY point. Every sample must be above baseZ. */
|
|
416
423
|
readonly height: (point: Vec2) => number;
|
|
417
424
|
/** Flat bottom Z. Defaults to zero. */
|
|
418
425
|
readonly baseZ?: number;
|
|
426
|
+
/**
|
|
427
|
+
* Top-cell diagonal policy. Defaults to fixed (lower-left to upper-right).
|
|
428
|
+
* min-edge selects the shorter 3D diagonal; center-error selects the diagonal whose endpoint
|
|
429
|
+
* average is closest to one extra height sample at the physical cell centre. Ties use fixed.
|
|
430
|
+
* All policies retain two triangles per cell; neither estimates error elsewhere in the cell.
|
|
431
|
+
*/
|
|
432
|
+
readonly triangulation?: "fixed" | "min-edge" | "center-error";
|
|
433
|
+
} & ({
|
|
434
|
+
/** Positive XY footprint for uniform sampling. */
|
|
435
|
+
readonly size: Vec2;
|
|
436
|
+
/** Exact positive integer X/Y interval counts. */
|
|
437
|
+
readonly segments: readonly [x: number, y: number];
|
|
419
438
|
/** Lower-left footprint XY. Omitted centers the footprint on XY. */
|
|
420
439
|
readonly origin?: Vec2;
|
|
421
|
-
|
|
440
|
+
/** Explicit coordinates cannot be combined with uniform sampling. */
|
|
441
|
+
readonly coordinates?: never;
|
|
442
|
+
} | {
|
|
443
|
+
/**
|
|
444
|
+
* Absolute XY coordinates, each with at least two strictly increasing finite values that
|
|
445
|
+
* remain distinct in Float32. Uneven spacing permits caller-owned local refinement.
|
|
446
|
+
*/
|
|
447
|
+
readonly coordinates: {
|
|
448
|
+
/** Absolute X samples in ascending order. */
|
|
449
|
+
readonly x: readonly number[];
|
|
450
|
+
/** Absolute Y samples in ascending order. */
|
|
451
|
+
readonly y: readonly number[];
|
|
452
|
+
};
|
|
453
|
+
/** The footprint is inferred from explicit coordinates. */
|
|
454
|
+
readonly size?: never;
|
|
455
|
+
/** Interval counts are inferred from explicit coordinates. */
|
|
456
|
+
readonly segments?: never;
|
|
457
|
+
/** Explicit coordinates already include their origin. */
|
|
458
|
+
readonly origin?: never;
|
|
459
|
+
});
|
|
422
460
|
|
|
423
461
|
/** Runtime-neutral options for the dedicated helical ridge mesher. */
|
|
424
462
|
export declare interface HelicalThreadRidgeMeshOptions {
|
|
@@ -488,7 +526,7 @@ export declare interface ImplicitOptions {
|
|
|
488
526
|
readonly edgeLength: number;
|
|
489
527
|
/** Field isosurface value; defaults to zero. */
|
|
490
528
|
readonly level?: number;
|
|
491
|
-
/**
|
|
529
|
+
/** Positive native geometric tolerance in millimetres; omitted uses interpolated grid crossings. */
|
|
492
530
|
readonly tolerance?: number;
|
|
493
531
|
}
|
|
494
532
|
|
|
@@ -512,6 +550,9 @@ export declare interface InternalThreadCutterOptions extends Omit<HelicalThreadR
|
|
|
512
550
|
/** Host-bound intersect operation. Valid only during a synchronous build. */
|
|
513
551
|
export declare const intersect: ModelingContext["intersect"];
|
|
514
552
|
|
|
553
|
+
/** Tests a solid or filled section in the active build, including expected empty Boolean results. */
|
|
554
|
+
export declare const isEmpty: ModelingContext["isEmpty"];
|
|
555
|
+
|
|
515
556
|
/** Host-bound linearPattern operation. Valid only during a synchronous build. */
|
|
516
557
|
export declare const linearPattern: ModelingContext["linearPattern"];
|
|
517
558
|
|
|
@@ -733,6 +774,44 @@ export declare interface ModelingContext {
|
|
|
733
774
|
intersect(this: void, solids: readonly Solid[]): Solid;
|
|
734
775
|
/** Native filled section intersection; dimensions cannot be mixed. */
|
|
735
776
|
intersect(this: void, sections: readonly Section[]): Section;
|
|
777
|
+
/**
|
|
778
|
+
* Splits a solid into [inside the cutter, outside the cutter].
|
|
779
|
+
*
|
|
780
|
+
* The first result is the intersection; the second is the subtraction. The cutter itself
|
|
781
|
+
* is not returned. Either result may be empty or contain multiple disconnected components.
|
|
782
|
+
*
|
|
783
|
+
* @example Keep the plug and the surrounding body
|
|
784
|
+
* ```ts
|
|
785
|
+
* const body = box({ size: [30, 20, 8] });
|
|
786
|
+
* const cutter = cylinder({ height: 10, diameter: 8 });
|
|
787
|
+
* const [plug, remainder] = split(body, cutter);
|
|
788
|
+
* // Move the plug beside the body to show both results.
|
|
789
|
+
* union([translate(plug, [0, -20, 0]), remainder]);
|
|
790
|
+
* ```
|
|
791
|
+
*/
|
|
792
|
+
split(this: void, solid: Solid, cutter: Solid): readonly [Solid, Solid];
|
|
793
|
+
/**
|
|
794
|
+
* Splits a solid into [positive-normal side, negative side] at a world-space plane.
|
|
795
|
+
*
|
|
796
|
+
* The nonzero normal is normalized; offset is the signed distance from the origin in
|
|
797
|
+
* millimetres, defaulting to zero. Both cut faces are closed. Select either tuple result
|
|
798
|
+
* to retain one side; either result may be empty.
|
|
799
|
+
*
|
|
800
|
+
* @example Separate two halves at X = 0
|
|
801
|
+
* ```ts
|
|
802
|
+
* const body = box({ size: [30, 20, 8] });
|
|
803
|
+
* const [right, left] = splitByPlane(body, { normal: [1, 0, 0] });
|
|
804
|
+
* // The gap is only for display; the cut itself removes no material.
|
|
805
|
+
* union([translate(right, [4, 0, 0]), translate(left, [-4, 0, 0])]);
|
|
806
|
+
* ```
|
|
807
|
+
*
|
|
808
|
+
* @example Keep only the positive half
|
|
809
|
+
* ```ts
|
|
810
|
+
* const [right] = splitByPlane(box({ size: [30, 20, 8] }), { normal: [1, 0, 0] });
|
|
811
|
+
* right;
|
|
812
|
+
* ```
|
|
813
|
+
*/
|
|
814
|
+
splitByPlane(this: void, solid: Solid, plane: PlaneOptions): readonly [Solid, Solid];
|
|
736
815
|
/** Combines disjoint components without resolving overlap. */
|
|
737
816
|
compose(this: void, solids: readonly Solid[]): Solid;
|
|
738
817
|
/**
|
|
@@ -746,6 +825,8 @@ export declare interface ModelingContext {
|
|
|
746
825
|
* ```
|
|
747
826
|
*/
|
|
748
827
|
hull(this: void, options: HullOptions): Solid;
|
|
828
|
+
/** Convex filled XY hull of finite points or same-build sections. */
|
|
829
|
+
hull(this: void, options: SectionHullOptions): Section;
|
|
749
830
|
/** Three-dimensional Minkowski sum. */
|
|
750
831
|
minkowskiSum(this: void, a: Solid, b: Solid): Solid;
|
|
751
832
|
/**
|
|
@@ -966,7 +1047,7 @@ export declare interface ModelingContext {
|
|
|
966
1047
|
*/
|
|
967
1048
|
inspectSection(this: void, section: Section): SectionInspection;
|
|
968
1049
|
/**
|
|
969
|
-
*
|
|
1050
|
+
* Reads immutable native facts once per solid; temporary components are owned by the build.
|
|
970
1051
|
*
|
|
971
1052
|
* @example Check dimensions and volume
|
|
972
1053
|
* ```ts
|
|
@@ -975,18 +1056,85 @@ export declare interface ModelingContext {
|
|
|
975
1056
|
* ```
|
|
976
1057
|
*/
|
|
977
1058
|
inspectSolid(this: void, solid: Solid): SolidInspection;
|
|
1059
|
+
/**
|
|
1060
|
+
* Checks whether a same-build solid or filled section contains no geometry.
|
|
1061
|
+
*
|
|
1062
|
+
* Accepts a 3D Solid or 2D Section handle, not a finalized model or raw triangle mesh.
|
|
1063
|
+
* Useful after a Boolean operation whose inputs may not overlap; no mesh is extracted.
|
|
1064
|
+
*
|
|
1065
|
+
* @example Check a missed intersection
|
|
1066
|
+
* ```ts
|
|
1067
|
+
* const body = box({ size: 10 });
|
|
1068
|
+
* const distant = translate(box({ size: 10 }), [30, 0, 0]);
|
|
1069
|
+
* const empty = intersect([body, distant]);
|
|
1070
|
+
* const missed = isEmpty(empty); // true
|
|
1071
|
+
* ```
|
|
1072
|
+
*/
|
|
1073
|
+
isEmpty(this: void, shape: Solid | Section): boolean;
|
|
1074
|
+
/**
|
|
1075
|
+
* Measures the minimum separation between two nonempty solids in millimetres.
|
|
1076
|
+
*
|
|
1077
|
+
* Uses actual geometry, not bounding-box separation or distance between object origins.
|
|
1078
|
+
* Contact or overlapping material returns zero, including one solid inside another.
|
|
1079
|
+
* The kernel's search range is managed internally; the result is not capped at a caller limit.
|
|
1080
|
+
* Empty, expired or foreign-build handles reject. Results follow native numeric precision.
|
|
1081
|
+
*
|
|
1082
|
+
* @example Measure the gap between two blocks
|
|
1083
|
+
* ```ts
|
|
1084
|
+
* const left = box({ size: 10 });
|
|
1085
|
+
* const right = translate(box({ size: 10 }), [13, 0, 0]);
|
|
1086
|
+
* const gap = distance(left, right); // 3 mm
|
|
1087
|
+
* ```
|
|
1088
|
+
*/
|
|
1089
|
+
distance(this: void, solid: Solid, other: Solid): number;
|
|
978
1090
|
/** Copies intersections along the finite directed origin-to-endpoint segment. */
|
|
979
1091
|
rayCast(this: void, solid: Solid, origin: Vec3, endpoint: Vec3): readonly SolidRayHit[];
|
|
980
|
-
/**
|
|
1092
|
+
/**
|
|
1093
|
+
* Separates a solid's disconnected surface shells into same-build solid handles.
|
|
1094
|
+
*
|
|
1095
|
+
* For solids without enclosed cavities, these shells are the disconnected material islands.
|
|
1096
|
+
* Enclosed cavities produce separate inward shells with negative signed volume; these cannot
|
|
1097
|
+
* be finalized alone as manufacturing parts. This does not cut connected geometry.
|
|
1098
|
+
*
|
|
1099
|
+
* @example Move disconnected pieces independently
|
|
1100
|
+
* ```ts
|
|
1101
|
+
* const islands = union([box({ size: 8 }), translate(box({ size: 8 }), [12, 0, 0])]);
|
|
1102
|
+
* const pieces = components(islands);
|
|
1103
|
+
* // Component order is unspecified; the index only sets a display height here.
|
|
1104
|
+
* union(pieces.map((piece, index) => translate(piece, [0, 0, index * 6])));
|
|
1105
|
+
* ```
|
|
1106
|
+
*/
|
|
981
1107
|
components(this: void, solid: Solid): readonly Solid[];
|
|
982
|
-
/**
|
|
1108
|
+
/** Separates filled 2D islands into sections, retaining each island's holes; empty gives []. */
|
|
1109
|
+
components(this: void, section: Section): readonly Section[];
|
|
1110
|
+
/**
|
|
1111
|
+
* Simplifies a solid using an explicit positive millimetre tolerance.
|
|
1112
|
+
*
|
|
1113
|
+
* Removes redundant vertices while keeping surfaces within the greater of this tolerance
|
|
1114
|
+
* and the solid's existing native tolerance. Choose it from the detail you can afford to lose.
|
|
1115
|
+
*/
|
|
983
1116
|
simplify(this: void, solid: Solid, tolerance: number): Solid;
|
|
1117
|
+
/**
|
|
1118
|
+
* Simplifies filled 2D contours using an explicit positive millimetre tolerance.
|
|
1119
|
+
*
|
|
1120
|
+
* Useful for dense imported outlines before extrusion. This changes the outline rather
|
|
1121
|
+
* than selecting a lower rendering quality; fine features may be removed.
|
|
1122
|
+
*
|
|
1123
|
+
* @example Clean a dense outline before extrusion
|
|
1124
|
+
* ```ts
|
|
1125
|
+
* const outline = circle({ diameter: 20, segments: 256 });
|
|
1126
|
+
* const cleaned = simplify(outline, 0.05); // 0.05 mm cleanup tolerance
|
|
1127
|
+
* const body = extrude(cleaned, { height: 3 });
|
|
1128
|
+
* ```
|
|
1129
|
+
*/
|
|
1130
|
+
simplify(this: void, section: Section, tolerance: number): Section;
|
|
984
1131
|
/** Bounded positive-inside implicit field constructor. */
|
|
985
1132
|
implicit(this: void, options: ImplicitOptions): Solid;
|
|
986
1133
|
/** Replaces native tolerance on an immutable copy; zero selects the kernel minimum. */
|
|
987
1134
|
setTolerance(this: void, solid: Solid, tolerance: number): Solid;
|
|
988
1135
|
/**
|
|
989
|
-
* Closed
|
|
1136
|
+
* Closed rectilinear top above a flat base, with uniform or explicit XY coordinates.
|
|
1137
|
+
* Samples each grid height once; center-error triangulation also samples each cell centre.
|
|
990
1138
|
*
|
|
991
1139
|
* @example Heightfield
|
|
992
1140
|
* ```ts
|
|
@@ -1171,6 +1319,14 @@ export declare interface PathPatternOptions {
|
|
|
1171
1319
|
*/
|
|
1172
1320
|
export declare type PatternSource = Solid | ((index: number) => Solid);
|
|
1173
1321
|
|
|
1322
|
+
/** A world-space cutting plane, retaining the half-space in the normal's direction. */
|
|
1323
|
+
export declare interface PlaneOptions {
|
|
1324
|
+
/** Finite nonzero plane normal; magnitude does not affect the plane. */
|
|
1325
|
+
readonly normal: Vec3;
|
|
1326
|
+
/** Signed millimetre distance from the origin along the unit normal. Defaults to zero. */
|
|
1327
|
+
readonly offset?: number;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1174
1330
|
/**
|
|
1175
1331
|
* One bounded two-dimensional polygon.
|
|
1176
1332
|
*
|
|
@@ -1473,10 +1629,26 @@ export declare interface SectionFromContoursOptions {
|
|
|
1473
1629
|
readonly fillRule?: "nonzero" | "evenodd";
|
|
1474
1630
|
}
|
|
1475
1631
|
|
|
1632
|
+
/** Planar convex hull inputs are finite XY points or same-build filled sections. */
|
|
1633
|
+
export declare type SectionHullOptions = {
|
|
1634
|
+
/** At least three finite millimetre XY points spanning positive area. */
|
|
1635
|
+
readonly points: readonly Vec2[];
|
|
1636
|
+
/** Point input excludes section input. */
|
|
1637
|
+
readonly sections?: never;
|
|
1638
|
+
} | {
|
|
1639
|
+
/** One through 1000 same-build sections to include in the convex hull. */
|
|
1640
|
+
readonly sections: readonly Section[];
|
|
1641
|
+
/** Section input excludes point input. */
|
|
1642
|
+
readonly points?: never;
|
|
1643
|
+
};
|
|
1644
|
+
|
|
1476
1645
|
/**
|
|
1477
|
-
*
|
|
1646
|
+
* Immutable native filled section facts in millimetres and square millimetres.
|
|
1647
|
+
* Repeated inspections of the same native shape share these facts within one build.
|
|
1478
1648
|
*/
|
|
1479
1649
|
export declare interface SectionInspection {
|
|
1650
|
+
/** Whether this filled section has no area or boundary. */
|
|
1651
|
+
readonly isEmpty: boolean;
|
|
1480
1652
|
/** Filled section area in square millimetres. */
|
|
1481
1653
|
readonly area: number;
|
|
1482
1654
|
/** Number of extracted outer and hole contours. */
|
|
@@ -1526,17 +1698,24 @@ declare const solidBrand: unique symbol;
|
|
|
1526
1698
|
export declare const solidFromMesh: ModelingContext["solidFromMesh"];
|
|
1527
1699
|
|
|
1528
1700
|
/**
|
|
1529
|
-
*
|
|
1701
|
+
* Immutable solid bounds, emptiness, component count, volume, surface area and native tolerance.
|
|
1702
|
+
* Repeated inspections of the same native shape share these facts within one build.
|
|
1530
1703
|
*/
|
|
1531
1704
|
export declare interface SolidInspection {
|
|
1705
|
+
/** Whether this native solid contains no material. */
|
|
1706
|
+
readonly isEmpty: boolean;
|
|
1532
1707
|
/** Inclusive maximum coordinates in millimetres. */
|
|
1533
1708
|
readonly max: Vec3;
|
|
1534
1709
|
/** Inclusive minimum coordinates in millimetres. */
|
|
1535
1710
|
readonly min: Vec3;
|
|
1536
|
-
/** Number of connected
|
|
1711
|
+
/** Number of connected surface shells; inward cavity shells count separately. */
|
|
1537
1712
|
readonly parts: number;
|
|
1538
1713
|
/** Enclosed native solid volume in cubic millimetres. */
|
|
1539
1714
|
readonly volume: number;
|
|
1715
|
+
/** Total native surface area in square millimetres. */
|
|
1716
|
+
readonly surfaceArea: number;
|
|
1717
|
+
/** Effective native geometric tolerance in millimetres. */
|
|
1718
|
+
readonly tolerance: number;
|
|
1540
1719
|
}
|
|
1541
1720
|
|
|
1542
1721
|
/** Named same-build solid manufacturing part. */
|
|
@@ -1581,6 +1760,12 @@ export declare type SphereOptions = CircularPrimitiveOptions & PrimitivePlacemen
|
|
|
1581
1760
|
readonly radius?: never;
|
|
1582
1761
|
});
|
|
1583
1762
|
|
|
1763
|
+
/** Returns intersection and difference with a same-build cutter, including empty results. */
|
|
1764
|
+
export declare const split: ModelingContext["split"];
|
|
1765
|
+
|
|
1766
|
+
/** Splits a solid into the normal-side and opposite-side halves of a world-space plane. */
|
|
1767
|
+
export declare const splitByPlane: ModelingContext["splitByPlane"];
|
|
1768
|
+
|
|
1584
1769
|
/** Square profile with equal crest and root widths. */
|
|
1585
1770
|
declare interface SquareThreadProfileSpec extends ThreadProfileBaseSpec {
|
|
1586
1771
|
/** Selects the square profile family. */
|
|
@@ -2067,9 +2252,9 @@ export declare interface VectorTextLayout {
|
|
|
2067
2252
|
export declare interface VectorTextMetrics {
|
|
2068
2253
|
/** Visible curve bounds after alignment, or null when the text has no ink. */
|
|
2069
2254
|
readonly bounds: VectorTextBounds | null;
|
|
2070
|
-
/** Unaligned
|
|
2255
|
+
/** Unaligned logical advance and negative baseline displacement in mm; arcs retain [arc length, 0]. */
|
|
2071
2256
|
readonly advance: readonly [number, number];
|
|
2072
|
-
/** Maximum line advance in millimetres, including spaces;
|
|
2257
|
+
/** Maximum logical line advance in millimetres, including spaces; arc length on circular baselines. */
|
|
2073
2258
|
readonly logicalWidth: number;
|
|
2074
2259
|
/** Selected font's ascender distance above a baseline in millimetres, independent of the text. */
|
|
2075
2260
|
readonly ascent: number;
|
package/dist/api/text.d.ts
CHANGED
|
@@ -18,11 +18,11 @@ export declare function prepareVectorFont(bytes: Uint8Array, options?: VectorFon
|
|
|
18
18
|
* A prepared font owns its parsed bytes; keep handles local to their Worker or invocation.
|
|
19
19
|
*/
|
|
20
20
|
export declare interface VectorFont {
|
|
21
|
-
/** Shape
|
|
21
|
+
/** Shape horizontal lines or one circular line; bounded to 512 UTF-16 units and supports blank text. */
|
|
22
22
|
readonly layout: (text: string, options: VectorTextLayoutOptions) => VectorTextLayout;
|
|
23
23
|
/** Measure the same cached vector layout; all returned lengths are in millimetres. */
|
|
24
24
|
readonly measure: (text: string, options: VectorTextLayoutOptions) => VectorTextMetrics;
|
|
25
|
-
/** Serialize visible
|
|
25
|
+
/** Serialize visible horizontal or circular outlines as bounded path-only SVG in physical millimetres. */
|
|
26
26
|
readonly toSvg: (text: string, options: VectorTextLayoutOptions) => string;
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -32,6 +32,18 @@ export declare interface VectorFontOptions {
|
|
|
32
32
|
readonly staticWeight?: number;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** A circular baseline centered at the XY origin, with rigid shaped glyph groups. */
|
|
36
|
+
export declare interface VectorTextArcBaseline {
|
|
37
|
+
/** Selects a circular baseline; glyph outlines rotate without bending or stretching. */
|
|
38
|
+
readonly kind: "arc";
|
|
39
|
+
/** Baseline radius in millimetres, greater than zero and at most 10000. */
|
|
40
|
+
readonly radius: number;
|
|
41
|
+
/** Alignment datum in degrees counterclockwise from +X; defaults to 90. */
|
|
42
|
+
readonly centerAngleDegrees?: number;
|
|
43
|
+
/** Reading direction around the circle; defaults to clockwise. */
|
|
44
|
+
readonly direction?: "clockwise" | "counterclockwise";
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
/** Exact curve bounds in millimetres; positive Y points upward. */
|
|
36
48
|
export declare interface VectorTextBounds {
|
|
37
49
|
/** Minimum X and Y of visible outlines, including accents, descenders, and overhangs. */
|
|
@@ -51,14 +63,16 @@ export declare interface VectorTextLayout {
|
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
/**
|
|
54
|
-
*
|
|
66
|
+
* Text bounded to 512 UTF-16 units: horizontal lines or one circular line without wrapping.
|
|
55
67
|
*/
|
|
56
68
|
export declare interface VectorTextLayoutOptions extends VectorTextOptions {
|
|
57
|
-
/** Place
|
|
69
|
+
/** Place the advance's start, midpoint or end at the datum; defaults to center on arcs, otherwise left. */
|
|
58
70
|
readonly align?: "left" | "center" | "right";
|
|
71
|
+
/** Omitted uses horizontal lines. Arcs require one line and baseline vertical alignment. */
|
|
72
|
+
readonly baseline?: VectorTextArcBaseline;
|
|
59
73
|
/** Baseline spacing in ems, 0.5–5; omitted uses the selected face's natural vertical metrics. */
|
|
60
74
|
readonly lineHeight?: number;
|
|
61
|
-
/** Place the first baseline or
|
|
75
|
+
/** Place the first baseline or complete ink bounds at Y=0; arcs only accept baseline. */
|
|
62
76
|
readonly verticalAlign?: "baseline" | "top" | "center" | "bottom";
|
|
63
77
|
}
|
|
64
78
|
|
|
@@ -66,9 +80,9 @@ export declare interface VectorTextLayoutOptions extends VectorTextOptions {
|
|
|
66
80
|
export declare interface VectorTextMetrics {
|
|
67
81
|
/** Visible curve bounds after alignment, or null when the text has no ink. */
|
|
68
82
|
readonly bounds: VectorTextBounds | null;
|
|
69
|
-
/** Unaligned
|
|
83
|
+
/** Unaligned logical advance and negative baseline displacement in mm; arcs retain [arc length, 0]. */
|
|
70
84
|
readonly advance: readonly [number, number];
|
|
71
|
-
/** Maximum line advance in millimetres, including spaces;
|
|
85
|
+
/** Maximum logical line advance in millimetres, including spaces; arc length on circular baselines. */
|
|
72
86
|
readonly logicalWidth: number;
|
|
73
87
|
/** Selected font's ascender distance above a baseline in millimetres, independent of the text. */
|
|
74
88
|
readonly ascent: number;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { GeometryError } from "../geometry/errors.js";
|
|
2
2
|
import { resolveExtrusionContourDivisions, resolveExtrusionControls, } from "../geometry/extrusion.js";
|
|
3
|
-
import { heightfieldMesh
|
|
3
|
+
import { heightfieldMesh } from "../geometry/heightfield.js";
|
|
4
4
|
import { assertMeshAllocationWithinLimits } from "../geometry/model-limits.js";
|
|
5
5
|
import { assertPrimitiveOptions } from "../geometry/primitive-resolvers.js";
|
|
6
6
|
import { matchProfiles } from "../geometry/profiles/matched.js";
|
|
7
7
|
import { resolveGeometryResolution } from "../geometry/resolution.js";
|
|
8
8
|
import { assertSamplingCount } from "../geometry/sampling-limits.js";
|
|
9
|
-
import {
|
|
9
|
+
import { sweepMesh } from "../geometry/sweeps/sweep-mesh.js";
|
|
10
10
|
import { buildFinishedExtrusion } from "./extrusion-finishing.js";
|
|
11
11
|
import { createLoftOperation } from "./loft-operations.js";
|
|
12
12
|
import { triangleMeshToManifold } from "./native/adapter.js";
|
|
@@ -15,7 +15,18 @@ import * as kernel from "./native/index.js";
|
|
|
15
15
|
import { buildProfileSweepMesh } from "./profile-mesh.js";
|
|
16
16
|
/** Internal host-bound construction operations. */
|
|
17
17
|
export function createConstructionOperations(environment, sections) {
|
|
18
|
-
const { module, scope, limits, assertActive, throwGeometryError, resolveHandle, nativeSolid, nativeSection, wrapSolid, ownSolid, ownSection, validateFiniteNumber, requirePositiveNumber, validateCoordinates, assertOperandCount, assertMeshAllocation, withResolutionDefaults, resolveCircularSegments, } = environment;
|
|
18
|
+
const { module, scope, limits, assertActive, throwGeometryError, resolveHandle, nativeSolid, nativeSection, wrapSolid, wrapSection, ownSolid, ownSection, validateFiniteNumber, requirePositiveNumber, validateCoordinates, assertOperandCount, assertMeshAllocation, withResolutionDefaults, resolveCircularSegments, } = environment;
|
|
19
|
+
const cuttingPlane = (value, operation) => {
|
|
20
|
+
assertPrimitiveOptions(value, operation);
|
|
21
|
+
validateCoordinates(value.normal, 3, operation);
|
|
22
|
+
const magnitude = Math.max(...value.normal.map(Math.abs));
|
|
23
|
+
if (magnitude === 0)
|
|
24
|
+
throwGeometryError("DEGENERATE_GEOMETRY", operation, "Plane normals must be nonzero.");
|
|
25
|
+
const scaled = value.normal.map((coordinate) => coordinate / magnitude);
|
|
26
|
+
const length = Math.hypot(...scaled);
|
|
27
|
+
const normal = [scaled[0] / length, scaled[1] / length, scaled[2] / length];
|
|
28
|
+
return { normal, offset: validateFiniteNumber(value.offset ?? 0, operation) };
|
|
29
|
+
};
|
|
19
30
|
const fromContours = sections.sectionFromContours;
|
|
20
31
|
const corresponding = (value) => typeof value === "object" && value !== null && "components" in value;
|
|
21
32
|
const shapeSection = (shape) => corresponding(shape)
|
|
@@ -50,7 +61,6 @@ export function createConstructionOperations(environment, sections) {
|
|
|
50
61
|
// a hole also bounds disconnected Sections, without extracting native point data.
|
|
51
62
|
triangles = Math.max(0, vertices * 2 + 4 * native.numContour() - 8);
|
|
52
63
|
assertMeshAllocationWithinLimits(vertices, triangles, vertices * 12 + triangles * 12, "extrude", resolved.limits);
|
|
53
|
-
assertMeshAllocation(vertices, triangles, vertices * 12 + triangles * 12, "extrude");
|
|
54
64
|
return ownSolid(native.extrude(value.height, divisions, controls.twistDegrees, controls.scales, controls.center));
|
|
55
65
|
};
|
|
56
66
|
const boolean = (values, operation) => {
|
|
@@ -90,13 +100,47 @@ export function createConstructionOperations(environment, sections) {
|
|
|
90
100
|
union: ((values) => boolean(values, "union")),
|
|
91
101
|
subtract,
|
|
92
102
|
intersect: ((values) => boolean(values, "intersect")),
|
|
103
|
+
split: (solid, cutter) => {
|
|
104
|
+
const results = nativeSolid(solid).split(nativeSolid(cutter));
|
|
105
|
+
return Object.freeze([ownSolid(results[0]), ownSolid(results[1])]);
|
|
106
|
+
},
|
|
107
|
+
splitByPlane: (solid, value) => {
|
|
108
|
+
const native = nativeSolid(solid);
|
|
109
|
+
const plane = cuttingPlane(value, "splitByPlane");
|
|
110
|
+
const results = native.splitByPlane(plane.normal, plane.offset);
|
|
111
|
+
return Object.freeze([ownSolid(results[0]), ownSolid(results[1])]);
|
|
112
|
+
},
|
|
93
113
|
compose: (values) => wrapSolid(kernel.compose(scope, values.map(nativeSolid))),
|
|
94
|
-
hull: (value) => {
|
|
114
|
+
hull: ((value) => {
|
|
95
115
|
assertActive();
|
|
96
|
-
|
|
116
|
+
assertPrimitiveOptions(value, "hull");
|
|
117
|
+
const hasSolids = "solids" in value && value.solids !== undefined;
|
|
118
|
+
const hasSections = "sections" in value && value.sections !== undefined;
|
|
119
|
+
if (Number(value.points !== undefined) + Number(hasSolids) + Number(hasSections) !== 1)
|
|
120
|
+
throwGeometryError("INVALID_ARGUMENT", "hull", "Supply exactly one hull input family.");
|
|
121
|
+
if (hasSections) {
|
|
122
|
+
assertOperandCount(value.sections, "hull");
|
|
123
|
+
return ownSection(module.CrossSection.hull(value.sections.map(nativeSection)));
|
|
124
|
+
}
|
|
125
|
+
if (value.points !== undefined && value.points[0]?.length === 2) {
|
|
126
|
+
const points = value.points;
|
|
127
|
+
const pointInputs = points;
|
|
128
|
+
if (!Array.isArray(points) || points.length < 3)
|
|
129
|
+
throwGeometryError("INVALID_ARGUMENT", "hull", "A planar hull needs at least three points.");
|
|
130
|
+
assertSamplingCount(points.length, "hull", Math.min(100_000, limits.maxProfilePoints));
|
|
131
|
+
const copied = pointInputs.map((point) => {
|
|
132
|
+
validateCoordinates(point, 2, "hull");
|
|
133
|
+
return [point[0], point[1]];
|
|
134
|
+
});
|
|
135
|
+
const native = ownScopedManifoldValue(scope, module.CrossSection.hull([copied]));
|
|
136
|
+
if (!(native.area() > 0))
|
|
137
|
+
throwGeometryError("DEGENERATE_GEOMETRY", "hull", "Hull points must span positive area.");
|
|
138
|
+
return wrapSection(native);
|
|
139
|
+
}
|
|
140
|
+
return wrapSolid(kernel.hull(scope, !hasSolids
|
|
97
141
|
? { points: value.points }
|
|
98
142
|
: { solids: value.solids.map(nativeSolid) }));
|
|
99
|
-
},
|
|
143
|
+
}),
|
|
100
144
|
minkowskiSum: (a, b) => wrapSolid(kernel.minkowskiSum(scope, nativeSolid(a), nativeSolid(b))),
|
|
101
145
|
solidFromMesh,
|
|
102
146
|
extrude,
|
|
@@ -147,13 +191,11 @@ export function createConstructionOperations(environment, sections) {
|
|
|
147
191
|
const singleComponent = profile.components.length === 1 ? profile.components[0] : undefined;
|
|
148
192
|
const contours = singleComponent?.holes.length === 0 ? [singleComponent.outer.points] : [];
|
|
149
193
|
if (contours.length === 1)
|
|
150
|
-
return solidFromMesh(
|
|
194
|
+
return solidFromMesh(sweepMesh(contours[0], path, selected));
|
|
151
195
|
return solidFromMesh(buildProfileSweepMesh(module, profile, path, selected, selected.limits));
|
|
152
196
|
},
|
|
153
197
|
heightfield: (value) => {
|
|
154
198
|
assertActive();
|
|
155
|
-
const facts = preflightHeightfield(value, limits);
|
|
156
|
-
assertMeshAllocation(facts.vertexCount, facts.triangleCount, facts.byteCount, "heightfield");
|
|
157
199
|
return solidFromMesh(heightfieldMesh(value, limits));
|
|
158
200
|
},
|
|
159
201
|
implicit: (value) => {
|
package/dist/engine/execution.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createModelingContext } from "./modeling-context.js";
|
|
|
7
7
|
import { ownScopedManifoldValue } from "./native/context.js";
|
|
8
8
|
import { copiedFloat32Array, copiedUint32Array } from "./native/copied-views.js";
|
|
9
9
|
import * as kernel from "./native/index.js";
|
|
10
|
+
import { assertNativeSolidStatus } from "./native/status.js";
|
|
10
11
|
import { createShapeAnchorOwner } from "./shape-anchors.js";
|
|
11
12
|
/** Internal execution owner; no native value or constructor crosses the public facade. */
|
|
12
13
|
export function createExecutionContext(module, scope, limits, initialResolution = {}) {
|
|
@@ -15,6 +16,8 @@ export function createExecutionContext(module, scope, limits, initialResolution
|
|
|
15
16
|
// mesh inspection receives independent copies, and this run-owned cache is cleared on exit.
|
|
16
17
|
const canonicalSolidMeshes = new Map();
|
|
17
18
|
const canonicalSectionProfiles = new Map();
|
|
19
|
+
let solidInspections;
|
|
20
|
+
let sectionInspections;
|
|
18
21
|
const primitiveSectionProfiles = new Map();
|
|
19
22
|
let active = true;
|
|
20
23
|
const throwGeometryError = (code, operation, message) => {
|
|
@@ -60,6 +63,50 @@ export function createExecutionContext(module, scope, limits, initialResolution
|
|
|
60
63
|
const owned = ownScopedManifoldValue(scope, value);
|
|
61
64
|
return wrapSection(owned);
|
|
62
65
|
};
|
|
66
|
+
// Geometry is immutable for a native value. Validate build ownership before every cache hit,
|
|
67
|
+
// and retain only frozen plain facts so callers cannot corrupt later inspections.
|
|
68
|
+
const solidInspectionFor = (handle) => {
|
|
69
|
+
const native = nativeSolid(handle);
|
|
70
|
+
const cached = solidInspections?.get(native);
|
|
71
|
+
if (cached !== undefined)
|
|
72
|
+
return cached;
|
|
73
|
+
assertNativeSolidStatus(native, "inspectSolid");
|
|
74
|
+
const bounds = native.boundingBox();
|
|
75
|
+
const parts = native.decompose().map((part) => ownScopedManifoldValue(scope, part));
|
|
76
|
+
const facts = Object.freeze({
|
|
77
|
+
min: Object.freeze([...bounds.min]),
|
|
78
|
+
max: Object.freeze([...bounds.max]),
|
|
79
|
+
isEmpty: native.isEmpty(),
|
|
80
|
+
parts: parts.length,
|
|
81
|
+
volume: native.volume(),
|
|
82
|
+
surfaceArea: native.surfaceArea(),
|
|
83
|
+
tolerance: native.tolerance(),
|
|
84
|
+
});
|
|
85
|
+
(solidInspections ??= new Map()).set(native, facts);
|
|
86
|
+
return facts;
|
|
87
|
+
};
|
|
88
|
+
const sectionInspectionFor = (handle) => {
|
|
89
|
+
const native = nativeSection(handle);
|
|
90
|
+
const cached = sectionInspections?.get(native);
|
|
91
|
+
if (cached !== undefined)
|
|
92
|
+
return cached;
|
|
93
|
+
const bounds = native.bounds();
|
|
94
|
+
const empty = native.isEmpty();
|
|
95
|
+
const parts = empty
|
|
96
|
+
? []
|
|
97
|
+
: native.decompose().map((part) => ownScopedManifoldValue(scope, part));
|
|
98
|
+
const facts = Object.freeze({
|
|
99
|
+
min: Object.freeze([...bounds.min]),
|
|
100
|
+
max: Object.freeze([...bounds.max]),
|
|
101
|
+
isEmpty: empty,
|
|
102
|
+
area: native.area(),
|
|
103
|
+
contours: native.numContour(),
|
|
104
|
+
parts: parts.length,
|
|
105
|
+
vertices: native.numVert(),
|
|
106
|
+
});
|
|
107
|
+
(sectionInspections ??= new Map()).set(native, facts);
|
|
108
|
+
return facts;
|
|
109
|
+
};
|
|
63
110
|
const shapeAnchors = createShapeAnchorOwner((shape) => {
|
|
64
111
|
resolveHandle(shape);
|
|
65
112
|
}, (shape) => {
|
|
@@ -94,6 +141,7 @@ export function createExecutionContext(module, scope, limits, initialResolution
|
|
|
94
141
|
assertMeshAllocationWithinLimits(vertices, triangles, bytes, operation, limits);
|
|
95
142
|
};
|
|
96
143
|
const copyNativeMesh = (native) => {
|
|
144
|
+
assertNativeSolidStatus(native, "toTriangleMesh");
|
|
97
145
|
const raw = native.getMesh();
|
|
98
146
|
assertMeshAllocation(raw.numVert, raw.numTri, (raw.numVert + raw.numTri) * 12, "copyMesh");
|
|
99
147
|
const positions = copiedFloat32Array(raw.numVert * 3);
|
|
@@ -194,6 +242,8 @@ export function createExecutionContext(module, scope, limits, initialResolution
|
|
|
194
242
|
assertOperandCount,
|
|
195
243
|
assertMeshAllocation,
|
|
196
244
|
analyzedMeshFor,
|
|
245
|
+
solidInspectionFor,
|
|
246
|
+
sectionInspectionFor,
|
|
197
247
|
sectionProfileFor,
|
|
198
248
|
primitiveProfileFor,
|
|
199
249
|
rememberPrimitiveProfile,
|
|
@@ -213,13 +263,16 @@ export function createExecutionContext(module, scope, limits, initialResolution
|
|
|
213
263
|
triVerts: mesh.indices,
|
|
214
264
|
});
|
|
215
265
|
const native = ownScopedManifoldValue(scope, new module.Manifold(input));
|
|
216
|
-
|
|
217
|
-
throwGeometryError("INVALID_TOPOLOGY", "finalize", "The geometry kernel rejected the finalized manufacturing mesh.");
|
|
266
|
+
assertNativeSolidStatus(native, "finalize");
|
|
218
267
|
},
|
|
219
268
|
dispose: () => {
|
|
220
269
|
active = false;
|
|
221
270
|
canonicalSolidMeshes.clear();
|
|
222
271
|
canonicalSectionProfiles.clear();
|
|
272
|
+
solidInspections?.clear();
|
|
273
|
+
sectionInspections?.clear();
|
|
274
|
+
solidInspections = undefined;
|
|
275
|
+
sectionInspections = undefined;
|
|
223
276
|
primitiveSectionProfiles.clear();
|
|
224
277
|
shapeAnchors.dispose();
|
|
225
278
|
},
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { Zip, ZipDeflate } from "fflate";
|
|
2
2
|
import { GeometryError } from "../geometry/errors.js";
|
|
3
|
+
import { analyzeSolidMesh } from "../geometry/mesh-solid.js";
|
|
3
4
|
import { analyzeTriangleMesh } from "../geometry/mesh.js";
|
|
5
|
+
import { MeshViewCache } from "./mesh-view-cache.js";
|
|
4
6
|
const namespace = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02";
|
|
5
7
|
const declaration = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
6
8
|
const encoder = new TextEncoder();
|
|
7
9
|
const escapeXml = (value) => value.replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]);
|
|
8
10
|
/** Private core 3MF serializer. It preserves the selected buffers and streams bounded XML chunks. */
|
|
9
11
|
export function meshesTo3mf(parts, options) {
|
|
12
|
+
if (options.meshValidation !== undefined &&
|
|
13
|
+
options.meshValidation !== "topology" &&
|
|
14
|
+
options.meshValidation !== "solid")
|
|
15
|
+
throw new GeometryError("INVALID_ARGUMENT", "Unsupported mesh validation mode.", {
|
|
16
|
+
operation: "export3mf",
|
|
17
|
+
});
|
|
10
18
|
const name = options.name ?? "MetaMesh model";
|
|
11
19
|
if (typeof name !== "string" ||
|
|
12
20
|
name.length === 0 ||
|
|
@@ -14,10 +22,18 @@ export function meshesTo3mf(parts, options) {
|
|
|
14
22
|
// eslint-disable-next-line no-control-regex -- XML 1.0 disallows these control characters.
|
|
15
23
|
/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ud800-\udfff\ufffe\uffff]/u.test(name))
|
|
16
24
|
throw new GeometryError("INVALID_ARGUMENT", "3MF names require one through 1024 valid XML characters.", { operation: "export3mf" });
|
|
25
|
+
const analyzed = new MeshViewCache();
|
|
17
26
|
for (const part of parts) {
|
|
18
|
-
|
|
27
|
+
let facts = analyzed.get(part);
|
|
28
|
+
if (facts !== undefined)
|
|
29
|
+
continue;
|
|
30
|
+
facts =
|
|
31
|
+
options.meshValidation === "solid"
|
|
32
|
+
? analyzeSolidMesh(part)
|
|
33
|
+
: analyzeTriangleMesh(part, { requireClosed: true, includeTopology: true });
|
|
19
34
|
if (!(facts.signedVolume > 0) || facts.vertexCount >= 2 ** 31 || facts.triangleCount >= 2 ** 31)
|
|
20
35
|
throw new GeometryError("INVALID_TOPOLOGY", "3MF parts require positive oriented volume and valid core resource counts.", { operation: "export3mf" });
|
|
36
|
+
analyzed.set(part, facts);
|
|
21
37
|
}
|
|
22
38
|
const chunks = [];
|
|
23
39
|
let bytes = 0, complete = false;
|