@metamesh/modeling 0.7.0 → 0.9.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +86 -4
  3. package/dist/api/engine.d.ts +4 -0
  4. package/dist/api/geometry.d.ts +1067 -908
  5. package/dist/api/index.d.ts +203 -18
  6. package/dist/api/text.d.ts +21 -7
  7. package/dist/engine/construction-operations.js +52 -10
  8. package/dist/engine/execution.js +55 -2
  9. package/dist/engine/export-3mf.js +17 -1
  10. package/dist/engine/finalization.js +27 -18
  11. package/dist/engine/inspection-operations.js +26 -22
  12. package/dist/engine/loft-operations.js +1 -1
  13. package/dist/engine/mechanical-operations.js +5 -8
  14. package/dist/engine/mesh-view-cache.js +23 -0
  15. package/dist/engine/native/adapter.js +2 -4
  16. package/dist/engine/native/distance.js +42 -0
  17. package/dist/engine/native/status.js +16 -0
  18. package/dist/engine/operation-environment.js +2 -1
  19. package/dist/engine/section-operations.js +3 -3
  20. package/dist/geometry/heightfield.js +101 -42
  21. package/dist/geometry/index.js +3 -0
  22. package/dist/geometry/loft-simple-mesh.js +2 -3
  23. package/dist/geometry/loft-triangulation.js +12 -9
  24. package/dist/geometry/loft.js +2 -6
  25. package/dist/geometry/mechanical/standards.js +2 -3
  26. package/dist/geometry/mechanical/threads.js +2 -3
  27. package/dist/geometry/mesh-centroid.js +36 -0
  28. package/dist/geometry/mesh-solid-intersections.js +123 -0
  29. package/dist/geometry/mesh-solid.js +284 -0
  30. package/dist/geometry/profiles/cross-section.js +102 -0
  31. package/dist/geometry/svg-import.js +8 -1
  32. package/dist/geometry/sweeps/sweep-closed.js +4 -4
  33. package/dist/geometry/sweeps/sweep-mesh.js +2 -6
  34. package/dist/geometry/sweeps/sweep-open.js +4 -4
  35. package/dist/geometry/sweeps/validation.js +1 -2
  36. package/dist/model/operations.js +10 -3
  37. package/dist/text/vector-text-layout.js +46 -3
  38. package/package.json +1 -1
@@ -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 regularly sampled heightfield during the active synchronous build. */
409
+ /** Constructs a closed rectilinear heightfield during the active synchronous build. */
407
410
  export declare const heightfield: ModelingContext["heightfield"];
408
411
 
409
- /** A regularly sampled, closed heightfield above a flat base, in millimetres. */
410
- export declare interface HeightfieldOptions {
411
- /** Positive XY footprint. */
412
- readonly size: Vec2;
413
- /** Exact positive integer X/Y interval counts. */
414
- readonly segments: readonly [x: number, y: number];
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
- /** Explicit nonnegative native geometric tolerance in millimetres. */
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
- * Copies solid bounds and volume; temporary components are owned by the build.
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
- /** Returns same-build disconnected solid handles. */
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
- /** Removes features within the explicit millimetre tolerance. */
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 regular-grid top above a flat base; every height is sampled once.
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
- * Copied native filled section facts in millimetres and square millimetres.
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
- * Copied solid bounds, connected component count, and cubic millimetre volume.
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 native solid components. */
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 pen displacement: final line advance and negative baseline displacement, in mm. */
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; distinct from visible ink width. */
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;
@@ -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 and position up to 512 UTF-16 units and 16 lines; supports empty or whitespace text. */
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 multiline outlines as bounded path-only SVG in physical millimetres. */
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
- * Horizontal multiline text, bounded to 512 UTF-16 units and 16 lines without wrapping.
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 each line's advance at the origin; defaults to left. Ink overhang is preserved. */
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 the complete ink bounds at Y=0; defaults to baseline. */
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 pen displacement: final line advance and negative baseline displacement, in mm. */
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; distinct from visible ink width. */
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, preflightHeightfield } from "../geometry/heightfield.js";
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 { buildSweepMesh } from "../geometry/sweeps/sweep-mesh.js";
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
- return wrapSolid(kernel.hull(scope, value.solids === undefined
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(buildSweepMesh(contours[0], path, selected, assertMeshAllocation));
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) => {
@@ -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
- if (native.status() !== "NoError")
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&apos;" })[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
- const facts = analyzeTriangleMesh(part, { requireClosed: true, includeTopology: true });
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;