@metamesh/modeling 0.0.0-stage → 0.7.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 (131) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/LICENSE +200 -0
  3. package/README.md +376 -2
  4. package/THIRD_PARTY_NOTICES.md +153 -0
  5. package/dist/api/engine.d.ts +71 -0
  6. package/dist/api/geometry.d.ts +3136 -0
  7. package/dist/api/index.d.ts +2087 -0
  8. package/dist/api/text.d.ts +93 -0
  9. package/dist/engine/construction-operations.js +189 -0
  10. package/dist/engine/execution.js +227 -0
  11. package/dist/engine/export-3mf.js +100 -0
  12. package/dist/engine/extrusion-finishing.js +82 -0
  13. package/dist/engine/finalization.js +214 -0
  14. package/dist/engine/index.js +83 -0
  15. package/dist/engine/inspection-operations.js +52 -0
  16. package/dist/engine/kernel/LICENSE +201 -0
  17. package/dist/engine/kernel/manifold.js +2 -0
  18. package/dist/engine/kernel/manifold.wasm +0 -0
  19. package/dist/engine/loft-operations.js +156 -0
  20. package/dist/engine/mechanical-operations.js +51 -0
  21. package/dist/engine/modeling-context.js +38 -0
  22. package/dist/engine/native/adapter.js +20 -0
  23. package/dist/engine/native/context.js +209 -0
  24. package/dist/engine/native/copied-views.js +46 -0
  25. package/dist/engine/native/index.js +13 -0
  26. package/dist/engine/native/operations.js +115 -0
  27. package/dist/engine/native/ownership.js +98 -0
  28. package/dist/engine/native/regions.js +138 -0
  29. package/dist/engine/native/scoped-primitives.js +127 -0
  30. package/dist/engine/native/scoped-threads.js +34 -0
  31. package/dist/engine/native/threads.js +52 -0
  32. package/dist/engine/native/transforms.js +111 -0
  33. package/dist/engine/operation-environment.js +20 -0
  34. package/dist/engine/pattern-operations.js +36 -0
  35. package/dist/engine/pattern-placements.js +176 -0
  36. package/dist/engine/primitive-anchors.js +70 -0
  37. package/dist/engine/primitive-correspondence.js +74 -0
  38. package/dist/engine/primitive-operations.js +151 -0
  39. package/dist/engine/primitive-profile-types.js +1 -0
  40. package/dist/engine/primitive-profiles.js +250 -0
  41. package/dist/engine/profile-mesh.js +279 -0
  42. package/dist/engine/section-operations.js +93 -0
  43. package/dist/engine/shape-anchors.js +88 -0
  44. package/dist/engine/transform-operations.js +225 -0
  45. package/dist/engine/types.js +1 -0
  46. package/dist/geometry/anchors.js +226 -0
  47. package/dist/geometry/curves/evaluation.js +162 -0
  48. package/dist/geometry/curves/sampling.js +266 -0
  49. package/dist/geometry/curves/types.js +1 -0
  50. package/dist/geometry/cylinder.js +117 -0
  51. package/dist/geometry/edge-finishing.js +160 -0
  52. package/dist/geometry/errors.js +76 -0
  53. package/dist/geometry/extrusion.js +150 -0
  54. package/dist/geometry/heightfield.js +115 -0
  55. package/dist/geometry/index.js +42 -0
  56. package/dist/geometry/loft-correspondence.js +204 -0
  57. package/dist/geometry/loft-geometry.js +184 -0
  58. package/dist/geometry/loft-options.js +56 -0
  59. package/dist/geometry/loft-simple-mesh.js +92 -0
  60. package/dist/geometry/loft-triangulation.js +64 -0
  61. package/dist/geometry/loft.js +95 -0
  62. package/dist/geometry/math/vector3.js +71 -0
  63. package/dist/geometry/mechanical/gear-motion.js +94 -0
  64. package/dist/geometry/mechanical/gears.js +484 -0
  65. package/dist/geometry/mechanical/index.js +13 -0
  66. package/dist/geometry/mechanical/standards.js +67 -0
  67. package/dist/geometry/mechanical/thread-helices.js +44 -0
  68. package/dist/geometry/mechanical/thread-mesh-plan.js +358 -0
  69. package/dist/geometry/mechanical/thread-mesh.js +142 -0
  70. package/dist/geometry/mechanical/thread-profiles.js +373 -0
  71. package/dist/geometry/mechanical/thread-solids.js +230 -0
  72. package/dist/geometry/mechanical/thread-surface.js +433 -0
  73. package/dist/geometry/mechanical/threads.js +138 -0
  74. package/dist/geometry/mesh-edge-table.js +126 -0
  75. package/dist/geometry/mesh-planar.js +408 -0
  76. package/dist/geometry/mesh-quality.js +396 -0
  77. package/dist/geometry/mesh-signature.js +137 -0
  78. package/dist/geometry/mesh.js +283 -0
  79. package/dist/geometry/model-limits.js +50 -0
  80. package/dist/geometry/numeric.js +11 -0
  81. package/dist/geometry/paths/frames.js +118 -0
  82. package/dist/geometry/paths/helix.js +49 -0
  83. package/dist/geometry/paths/path.js +32 -0
  84. package/dist/geometry/paths/planar.js +135 -0
  85. package/dist/geometry/paths/resampling.js +125 -0
  86. package/dist/geometry/polygon-clipping.js +52 -0
  87. package/dist/geometry/primitive-options.js +1 -0
  88. package/dist/geometry/primitive-resolution.js +17 -0
  89. package/dist/geometry/primitive-resolvers.js +235 -0
  90. package/dist/geometry/profiles/matched.js +192 -0
  91. package/dist/geometry/profiles/open-profile.js +137 -0
  92. package/dist/geometry/profiles/profiles.js +91 -0
  93. package/dist/geometry/profiles/rectangle.js +249 -0
  94. package/dist/geometry/profiles/shapes.js +41 -0
  95. package/dist/geometry/profiles/span-sampling.js +28 -0
  96. package/dist/geometry/profiles/types.js +1 -0
  97. package/dist/geometry/profiles/validation.js +20 -0
  98. package/dist/geometry/regions.js +371 -0
  99. package/dist/geometry/resolution.js +236 -0
  100. package/dist/geometry/revolve/revolve-mesh.js +129 -0
  101. package/dist/geometry/sampling-limits.js +29 -0
  102. package/dist/geometry/stroke-path.js +181 -0
  103. package/dist/geometry/svg/errors.js +23 -0
  104. package/dist/geometry/svg/fill.js +172 -0
  105. package/dist/geometry/svg/index.js +1 -0
  106. package/dist/geometry/svg/matrix.js +130 -0
  107. package/dist/geometry/svg/parser.js +536 -0
  108. package/dist/geometry/svg/path.js +349 -0
  109. package/dist/geometry/svg/types.js +1 -0
  110. package/dist/geometry/svg-import.js +662 -0
  111. package/dist/geometry/sweeps/options.js +28 -0
  112. package/dist/geometry/sweeps/orientation.js +106 -0
  113. package/dist/geometry/sweeps/stations.js +161 -0
  114. package/dist/geometry/sweeps/sweep-closed.js +96 -0
  115. package/dist/geometry/sweeps/sweep-mesh.js +30 -0
  116. package/dist/geometry/sweeps/sweep-open.js +76 -0
  117. package/dist/geometry/sweeps/validation.js +116 -0
  118. package/dist/geometry/text-contours.js +32 -0
  119. package/dist/geometry/text-layout.js +1 -0
  120. package/dist/geometry/tolerance.js +53 -0
  121. package/dist/geometry/transform-geometry.js +41 -0
  122. package/dist/geometry/transform3.js +340 -0
  123. package/dist/geometry/types.js +1 -0
  124. package/dist/index.js +1 -0
  125. package/dist/model/execution-binding.js +30 -0
  126. package/dist/model/operations.js +154 -0
  127. package/dist/model/options.js +1 -0
  128. package/dist/model/types.js +1 -0
  129. package/dist/text/index.js +58 -0
  130. package/dist/text/vector-text-layout.js +279 -0
  131. package/package.json +100 -3
@@ -0,0 +1,74 @@
1
+ import { GeometryError } from "../geometry/errors.js";
2
+ import { matchProfiles, } from "../geometry/profiles/matched.js";
3
+ import { createProfileSpanSampler } from "../geometry/profiles/span-sampling.js";
4
+ import { assertSamplingCount } from "../geometry/sampling-limits.js";
5
+ /** Primitive exact counts describe physical facets; identified user Profile counts describe analytic grids. */
6
+ export function samplePrimitiveFamily(sources, profiles, resolution, limits) {
7
+ const matched = matchProfiles(profiles, { resolution, limits });
8
+ const first = profiles[0].components[0].outer;
9
+ const baseline = new Map();
10
+ for (const id of matched[0].components[0].outer.spans)
11
+ baseline.set(id, (baseline.get(id) ?? 0) + 1);
12
+ const needsFacets = sources.some((source, index) => source.profile.components[0].outer.spans.some((span, slot) => span.kind !== "line" &&
13
+ (span.segments !== undefined ||
14
+ (span.kind === "arc" && resolution.circularSegments !== undefined)) &&
15
+ profiles[index].components[0].outer.spans[slot].segments !== baseline.get(span.id)));
16
+ if (!needsFacets)
17
+ return matched;
18
+ let count = 0;
19
+ const grids = first.spans.map((span, slot) => {
20
+ const common = baseline.get(span.id);
21
+ const knots = new Set(Array.from({ length: common }, (_, i) => i / common));
22
+ const facets = sources.map((source, index) => {
23
+ const original = source.profile.components[0].outer.spans[slot];
24
+ const exact = original.segments !== undefined ||
25
+ (original.kind === "arc" && resolution.circularSegments !== undefined);
26
+ const intervals = profiles[index].components[0].outer.spans[slot].segments;
27
+ if (exact)
28
+ for (let i = 0; i < intervals; i++)
29
+ knots.add(i / intervals);
30
+ return exact ? intervals : undefined;
31
+ });
32
+ const parameters = [...knots].sort((a, b) => a - b);
33
+ count += parameters.length;
34
+ assertSamplingCount(count, "loft", limits.maxProfilePoints);
35
+ assertSamplingCount(count * sources.length, "loft", 1_000_000);
36
+ return { parameters, facets };
37
+ });
38
+ return profiles.map((profile, index) => {
39
+ const contour = profile.components[0].outer;
40
+ const points = [], spans = [];
41
+ contour.spans.forEach((span, slot) => {
42
+ const sample = createProfileSpanSampler(span);
43
+ const plan = grids[slot];
44
+ const intervals = plan.facets[index];
45
+ const vertices = intervals === undefined
46
+ ? undefined
47
+ : Array.from({ length: intervals + 1 }, (_, i) => sample(i / intervals));
48
+ for (const parameter of plan.parameters) {
49
+ if (vertices === undefined)
50
+ points.push(sample(parameter));
51
+ else {
52
+ const at = parameter * intervals;
53
+ const nearest = Math.round(at);
54
+ if (Math.abs(at - nearest) <= Number.EPSILON * 8 * Math.max(1, intervals)) {
55
+ points.push(vertices[nearest]);
56
+ spans.push(span.id);
57
+ continue;
58
+ }
59
+ const start = Math.min(Math.floor(at), intervals - 1);
60
+ const a = vertices[start], b = vertices[start + 1], amount = at - start;
61
+ points.push(amount === 0 ? a : [a[0] + (b[0] - a[0]) * amount, a[1] + (b[1] - a[1]) * amount]);
62
+ }
63
+ spans.push(span.id);
64
+ }
65
+ });
66
+ if (points.length !== count)
67
+ throw new GeometryError("INVALID_TOPOLOGY", "Primitive sampling grid must retain every shared knot.", { operation: "loft" });
68
+ return {
69
+ components: [
70
+ { id: profile.components[0].id, holes: [], outer: { id: contour.id, points, spans } },
71
+ ],
72
+ };
73
+ });
74
+ }
@@ -0,0 +1,151 @@
1
+ import { resolveCylinderRadii } from "../geometry/cylinder.js";
2
+ import { resolveModelingLimits } from "../geometry/model-limits.js";
3
+ import { assertPrimitiveOptions } from "../geometry/primitive-resolvers.js";
4
+ import { regularPolygonLoop, starLoop } from "../geometry/profiles/profiles.js";
5
+ import { rectangleLoop, resolveRectangle, } from "../geometry/profiles/rectangle.js";
6
+ import { ellipseLoop } from "../geometry/profiles/shapes.js";
7
+ import { requireProfileVertexCount } from "../geometry/profiles/validation.js";
8
+ import * as kernel from "./native/index.js";
9
+ import { boxAnchors, cylinderAnchors, originAnchor, planarAnchors, sphereAnchors, torusAnchors, tubeAnchors, } from "./primitive-anchors.js";
10
+ import { ellipticalSectionProfile, polygonSectionProfile, rectangleSectionProfile, } from "./primitive-profiles.js";
11
+ /** Internal host-bound primitive operations. */
12
+ export function createPrimitiveOperations(environment, transforms, sections) {
13
+ const { module, scope, assertActive, throwGeometryError, ownSection, validateFiniteNumber, requirePositiveNumber, validateCoordinates, withResolutionDefaults, resolveCircularSegments, } = environment;
14
+ const { placedSolid, placedSection } = transforms;
15
+ const fromContours = sections.sectionFromContours;
16
+ const operations = {
17
+ box: (value) => {
18
+ assertActive();
19
+ assertPrimitiveOptions(value, "box");
20
+ const resolved = withResolutionDefaults(value);
21
+ const resolveTreatment = (treatment) => {
22
+ assertPrimitiveOptions(treatment, "box");
23
+ return treatment.kind === "round" || treatment.kind === "smooth"
24
+ ? withResolutionDefaults({
25
+ ...treatment,
26
+ limits: resolveModelingLimits(treatment.limits, resolved.limits),
27
+ })
28
+ : treatment;
29
+ };
30
+ if (value.treatment !== undefined)
31
+ assertPrimitiveOptions(value.treatment, "box");
32
+ const treatment = value.treatment === undefined
33
+ ? undefined
34
+ : "kind" in value.treatment
35
+ ? resolveTreatment(value.treatment)
36
+ : Object.fromEntries(Object.entries(value.treatment).map(([edge, treatment]) => [
37
+ edge,
38
+ resolveTreatment(treatment),
39
+ ]));
40
+ return placedSolid(value, () => kernel.box(scope, treatment === undefined ? resolved : { ...resolved, treatment }), () => boxAnchors(value));
41
+ },
42
+ cylinder: (value) => {
43
+ assertActive();
44
+ assertPrimitiveOptions(value, "cylinder");
45
+ const resolved = withResolutionDefaults(value);
46
+ if (value.sizing !== undefined && value.sizing !== "vertex" && value.sizing !== "face")
47
+ throwGeometryError("INVALID_ARGUMENT", "cylinder", "sizing must be vertex or face.");
48
+ if (value.sizing !== "face")
49
+ return placedSolid(value, () => kernel.cylinder(scope, resolved), () => cylinderAnchors(value));
50
+ const radii = resolveCylinderRadii(value);
51
+ const count = resolveCircularSegments(Math.max(...radii), value), correction = 1 / Math.cos(Math.PI / count);
52
+ return placedSolid(value, () => kernel.cylinder(scope, {
53
+ height: resolved.height,
54
+ radius: [radii[0] * correction, radii[1] * correction],
55
+ segments: count,
56
+ resolution: resolved.resolution,
57
+ limits: resolved.limits,
58
+ ...(resolved.center === undefined ? {} : { center: resolved.center }),
59
+ ...(resolved.ends === undefined ? {} : { ends: resolved.ends }),
60
+ }), () => cylinderAnchors(value));
61
+ },
62
+ tube: (value) => {
63
+ assertActive();
64
+ return placedSolid(value, () => kernel.tube(scope, withResolutionDefaults(value)), () => tubeAnchors(value));
65
+ },
66
+ sphere: (value) => {
67
+ assertActive();
68
+ return placedSolid(value, () => kernel.sphere(scope, withResolutionDefaults(value)), () => sphereAnchors(value));
69
+ },
70
+ torus: (value) => {
71
+ assertActive();
72
+ return placedSolid(value, () => kernel.torus(scope, withResolutionDefaults(value)), () => torusAnchors(value));
73
+ },
74
+ rectangle: (value) => placedSection(value, () => {
75
+ assertActive();
76
+ const resolved = withResolutionDefaults(value);
77
+ resolveRectangle(resolved, "rectangle");
78
+ const treatment = value.treatment;
79
+ const resolvedCorner = (corner) => corner.kind === "round" || corner.kind === "smooth"
80
+ ? withResolutionDefaults({
81
+ ...corner,
82
+ limits: resolveModelingLimits(corner.limits, resolved.limits),
83
+ })
84
+ : corner;
85
+ const options = {
86
+ ...resolved,
87
+ ...(treatment === undefined
88
+ ? {}
89
+ : {
90
+ treatment: Array.isArray(treatment)
91
+ ? treatment.map(resolvedCorner)
92
+ : resolvedCorner(treatment),
93
+ }),
94
+ };
95
+ const points = rectangleLoop(options);
96
+ const dimensions = typeof value.size === "number" ? [value.size, value.size] : value.size;
97
+ const section = treatment === undefined
98
+ ? ownSection(module.CrossSection.square(dimensions, value.center ?? true))
99
+ : fromContours([points]);
100
+ environment.rememberAnchors(section, planarAnchors(dimensions, value.center ?? true));
101
+ return environment.rememberPrimitiveProfile(section, rectangleSectionProfile(options, resolved.resolution, resolved.limits));
102
+ }),
103
+ circle: (value) => placedSection(value, () => {
104
+ assertActive();
105
+ if ((value.radius === undefined) === (value.diameter === undefined))
106
+ throwGeometryError("INVALID_ARGUMENT", "circle", "Specify exactly one radius or diameter.");
107
+ const radius = value.radius ?? value.diameter / 2;
108
+ requirePositiveNumber(radius, "circle");
109
+ const count = resolveCircularSegments(radius, value);
110
+ requireProfileVertexCount(count, 3, "circle", withResolutionDefaults(value).limits);
111
+ if (value.sizing !== undefined && value.sizing !== "vertex" && value.sizing !== "face")
112
+ throwGeometryError("INVALID_ARGUMENT", "circle", "sizing must be vertex or face.");
113
+ const section = ownSection(module.CrossSection.circle(value.sizing === "face" ? radius / Math.cos(Math.PI / count) : radius, count));
114
+ const resolved = withResolutionDefaults(value);
115
+ environment.rememberAnchors(section, planarAnchors([2 * radius, 2 * radius]));
116
+ return environment.rememberPrimitiveProfile(section, ellipticalSectionProfile([radius, radius], value, resolved.resolution, resolved.limits, value.sizing === "face"));
117
+ }),
118
+ ellipse: (value) => placedSection(value, () => {
119
+ if ((value.radius === undefined) === (value.size === undefined))
120
+ throwGeometryError("INVALID_ARGUMENT", "ellipse", "Specify exactly one radius or size tuple.");
121
+ const radii = value.radius ?? [value.size[0] / 2, value.size[1] / 2];
122
+ validateCoordinates(radii, 2, "ellipse");
123
+ const resolved = withResolutionDefaults(value);
124
+ const section = fromContours([
125
+ ellipseLoop({
126
+ radius: radii,
127
+ segments: resolveCircularSegments(Math.max(...radii), value),
128
+ limits: resolved.limits,
129
+ }),
130
+ ]);
131
+ environment.rememberAnchors(section, planarAnchors([2 * radii[0], 2 * radii[1]]));
132
+ return environment.rememberPrimitiveProfile(section, ellipticalSectionProfile(radii, value, resolved.resolution, resolved.limits));
133
+ }),
134
+ regularPolygon: (value) => placedSection(value, () => {
135
+ const resolved = withResolutionDefaults(value), loop = regularPolygonLoop(resolved);
136
+ const section = environment.rememberAnchors(fromContours([loop]), originAnchor());
137
+ return environment.rememberPrimitiveProfile(section, polygonSectionProfile(`polygon-${value.sides}`, loop, resolved.resolution, resolved.limits));
138
+ }),
139
+ star: (value) => placedSection(value, () => {
140
+ const angle = (validateFiniteNumber(value.rotationDegrees ?? 0, "star") * Math.PI) / 180;
141
+ const resolved = withResolutionDefaults(value);
142
+ const loop = starLoop(value.outerRadius, value.points, value.innerRadius, resolved.limits).map(([x, y]) => [
143
+ x * Math.cos(angle) - y * Math.sin(angle),
144
+ x * Math.sin(angle) + y * Math.cos(angle),
145
+ ]);
146
+ const section = environment.rememberAnchors(fromContours([loop]), originAnchor());
147
+ return environment.rememberPrimitiveProfile(section, polygonSectionProfile(`star-${value.points}`, loop, resolved.resolution, resolved.limits));
148
+ }),
149
+ };
150
+ return operations;
151
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,250 @@
1
+ import { planarCubicControl } from "../geometry/curves/evaluation.js";
2
+ import { resolveLineSegmentCount, resolveUniformCubicSegments, } from "../geometry/curves/sampling.js";
3
+ import { GeometryError } from "../geometry/errors.js";
4
+ import {} from "../geometry/profiles/matched.js";
5
+ import { rectangleProfile, resolveRectangle, } from "../geometry/profiles/rectangle.js";
6
+ import { resolveArcSegments, resolveGeometryResolution, } from "../geometry/resolution.js";
7
+ import { samplePrimitiveFamily } from "./primitive-correspondence.js";
8
+ const identity = [1, 0, 0, 0, 1, 0, 0, 0, 1];
9
+ const cornerIds = ["top-right", "top-left", "bottom-left", "bottom-right"];
10
+ /** Captures rectangle landmarks separately from its existing native filled geometry. */
11
+ export function rectangleSectionProfile(options, resolution, limits) {
12
+ const { corners, width, height, limits: rectangleLimits, } = resolveRectangle({ ...options, limits }, "rectangle");
13
+ const spanResolution = {};
14
+ corners.forEach((corner, index) => {
15
+ if (corner.kind === "round" || corner.kind === "smooth")
16
+ spanResolution[cornerIds[index]] = { ...resolution, ...corner.resolution };
17
+ });
18
+ return {
19
+ family: "rectangle",
20
+ profile: rectangleProfile(options),
21
+ resolution: { ...resolution },
22
+ spanResolution,
23
+ limits: rectangleLimits,
24
+ matrix: identity,
25
+ origin: options.center === false ? [width / 2, height / 2] : [0, 0],
26
+ };
27
+ }
28
+ /** Circle and ellipse share the same explicit +X seam and counterclockwise ring family. */
29
+ export function ellipticalSectionProfile(radii, options, resolution, limits, faceSizing = false) {
30
+ const radius = Math.max(...radii);
31
+ const segments = options.segments ?? resolution.circularSegments;
32
+ return {
33
+ family: "ellipse",
34
+ profile: {
35
+ components: [
36
+ {
37
+ id: "body",
38
+ holes: [],
39
+ outer: {
40
+ id: "outline",
41
+ spans: [
42
+ {
43
+ id: "ring",
44
+ kind: "arc",
45
+ center: [0, 0],
46
+ radius,
47
+ startDegrees: 0,
48
+ sweepDegrees: 360,
49
+ ...(segments === undefined ? {} : { segments }),
50
+ },
51
+ ],
52
+ },
53
+ },
54
+ ],
55
+ },
56
+ resolution: { ...resolution },
57
+ limits,
58
+ matrix: [radii[0] / radius, 0, 0, 0, radii[1] / radius, 0, 0, 0, 1],
59
+ origin: [0, 0],
60
+ ...(faceSizing ? { faceSizing: true } : {}),
61
+ };
62
+ }
63
+ /** Captures the explicitly indexed edges of a polygon or star; its topology is its family key. */
64
+ export function polygonSectionProfile(family, loop, resolution, limits) {
65
+ return {
66
+ family,
67
+ profile: {
68
+ components: [
69
+ {
70
+ id: "body",
71
+ holes: [],
72
+ outer: {
73
+ id: "outline",
74
+ spans: loop.map((start, index) => ({
75
+ id: `edge-${index}`,
76
+ kind: "line",
77
+ start,
78
+ end: loop[(index + 1) % loop.length],
79
+ })),
80
+ },
81
+ },
82
+ ],
83
+ },
84
+ resolution: { ...resolution },
85
+ limits,
86
+ matrix: identity,
87
+ origin: [0, 0],
88
+ };
89
+ }
90
+ /** Preserves a primitive's authored identity through an invertible XY affine transform. */
91
+ export function transformPrimitiveSectionProfile(source, transform) {
92
+ const a = transform, b = source.matrix;
93
+ const matrix = [
94
+ a[0] * b[0] + a[1] * b[3],
95
+ a[0] * b[1] + a[1] * b[4],
96
+ a[0] * b[2] + a[1] * b[5] + a[2],
97
+ a[3] * b[0] + a[4] * b[3],
98
+ a[3] * b[1] + a[4] * b[4],
99
+ a[3] * b[2] + a[4] * b[5] + a[5],
100
+ 0,
101
+ 0,
102
+ 1,
103
+ ];
104
+ if (!matrix.every(Number.isFinite))
105
+ fail("Primitive profile transforms must remain finite.");
106
+ return { ...source, matrix };
107
+ }
108
+ const fail = (message) => {
109
+ throw new GeometryError("INVALID_ARGUMENT", message, { operation: "loft" });
110
+ };
111
+ const determinant = (matrix) => matrix[0] * matrix[4] - matrix[1] * matrix[3];
112
+ /** Circle/ellipse quadrants are the same corner landmarks as a fully rounded square. */
113
+ export function rectangularPrimitiveFamily(sources) {
114
+ if (!sources.some((source) => source.family === "rectangle") ||
115
+ sources.some((source) => source.family !== "rectangle" && source.family !== "ellipse"))
116
+ return undefined;
117
+ return sources.map((source) => {
118
+ if (source.family === "rectangle")
119
+ return source;
120
+ const ring = source.profile.components[0].outer.spans[0];
121
+ if (ring.kind !== "arc")
122
+ return fail("Retained circular primitives require their analytic ring.");
123
+ if (ring.segments !== undefined && ring.segments % 4 !== 0)
124
+ fail("Circle/ellipse to rectangle landmark lofts require exact circular segments divisible by four. Select explicit correspondence to retain other faceted counts.");
125
+ const rectangular = rectangleSectionProfile({
126
+ size: 2 * ring.radius,
127
+ limits: source.limits,
128
+ treatment: {
129
+ kind: "round",
130
+ radius: ring.radius,
131
+ resolution: source.resolution,
132
+ ...(ring.segments === undefined ? {} : { segments: ring.segments }),
133
+ },
134
+ }, source.resolution, source.limits);
135
+ return {
136
+ ...rectangular,
137
+ matrix: source.matrix,
138
+ ...(source.faceSizing === true ? { faceSizing: true } : {}),
139
+ };
140
+ });
141
+ }
142
+ // Compute the greatest singular value after normalizing coefficients, avoiding squared overflow.
143
+ const magnification = (matrix) => {
144
+ const maximum = Math.max(Math.abs(matrix[0]), Math.abs(matrix[1]), Math.abs(matrix[3]), Math.abs(matrix[4]));
145
+ const a = matrix[0] / maximum, b = matrix[1] / maximum, c = matrix[3] / maximum, d = matrix[4] / maximum;
146
+ const trace = a * a + b * b + c * c + d * d;
147
+ const det = a * d - b * c;
148
+ const result = maximum * Math.sqrt((trace + Math.sqrt(Math.max(0, trace * trace - 4 * det * det))) / 2);
149
+ if (!Number.isFinite(result) || result <= 0)
150
+ fail("Primitive profile transforms must be finite and nonsingular.");
151
+ return result;
152
+ };
153
+ /** Samples only compatible, retained primitive identities; arbitrary sections never enter this path. */
154
+ export function correspondingPrimitiveProfiles(sources, options, limits) {
155
+ const first = sources[0];
156
+ if (sources.some((source) => source.family !== first.family))
157
+ fail("Primitive loft stations must retain the same identified shape family. Use explicit profiles for unlike shapes.");
158
+ const reflected = determinant(first.matrix) < 0;
159
+ if (sources.some((source) => determinant(source.matrix) < 0 !== reflected))
160
+ fail("Primitive loft stations must retain the same transform handedness.");
161
+ const selected = { ...limits };
162
+ for (const source of sources) {
163
+ for (const key of Object.keys(source.limits))
164
+ selected[key] = Math.min(selected[key], source.limits[key]);
165
+ }
166
+ const target = resolveGeometryResolution(options.resolution, selected);
167
+ if (first.family === "rectangle" &&
168
+ target.circularSegments !== undefined &&
169
+ target.circularSegments % 4 !== 0)
170
+ fail("Exact rectangle round segments describe a complete circle and must divide by four.");
171
+ const profiles = sources.map((source) => {
172
+ const scale = magnification(source.matrix);
173
+ return {
174
+ components: source.profile.components.map((component) => ({
175
+ ...component,
176
+ outer: {
177
+ ...component.outer,
178
+ spans: component.outer.spans.map((span) => {
179
+ if (span.segments !== undefined)
180
+ return span;
181
+ const policy = source.spanResolution?.[span.id] ?? source.resolution;
182
+ const original = resolveGeometryResolution({
183
+ ...policy,
184
+ maximumSegments: Math.min(policy.maximumSegments ?? selected.maxCircularSegments, selected.maxCircularSegments),
185
+ minimumSegments: Math.min(policy.minimumSegments ?? 16, selected.maxCircularSegments),
186
+ }, selected);
187
+ const edgeLength = Math.min(original.maxSegmentLength ?? Infinity, target.maxSegmentLength ?? Infinity) /
188
+ scale;
189
+ const resolution = {
190
+ maxChordError: Math.min(original.maxChordError, target.maxChordError) / scale,
191
+ maximumSegments: Math.min(original.maximumSegments, target.maximumSegments),
192
+ minimumSegments: Math.min(Math.max(original.minimumSegments, target.minimumSegments), Math.min(original.maximumSegments, target.maximumSegments)),
193
+ ...(Number.isFinite(edgeLength) ? { maxSegmentLength: edgeLength } : {}),
194
+ ...(target.circularSegments === undefined
195
+ ? {}
196
+ : { circularSegments: target.circularSegments }),
197
+ };
198
+ const segments = span.kind === "arc"
199
+ ? span.radius === 0
200
+ ? 1
201
+ : resolveArcSegments(span.radius, Math.abs(span.sweepDegrees), resolution, selected)
202
+ : span.kind === "line"
203
+ ? resolveLineSegmentCount(Math.hypot(span.end[0] - span.start[0], span.end[1] - span.start[1]), resolution.maxSegmentLength)
204
+ : resolveUniformCubicSegments(planarCubicControl(span), resolveGeometryResolution(resolution, selected), selected.maxProfilePoints);
205
+ return { ...span, segments };
206
+ }),
207
+ },
208
+ })),
209
+ };
210
+ });
211
+ const sampled = samplePrimitiveFamily(sources, profiles, target, selected);
212
+ return sampled.map((profile, index) => {
213
+ const source = sources[index], matrix = source.matrix;
214
+ return {
215
+ components: profile.components.map((component) => {
216
+ const contour = component.outer;
217
+ // Collapsed rectangle edge slots are landmarks rather than circular facets.
218
+ let radialScale = 1;
219
+ if (source.faceSizing) {
220
+ const ring = source.profile.components[0].outer.spans.find((span) => span.kind === "arc");
221
+ if (ring?.kind !== "arc" || !(ring.radius > 0))
222
+ return fail("Face sizing requires a positive retained circular primitive.");
223
+ let support = Infinity;
224
+ for (let slot = 0; slot < contour.points.length; slot++) {
225
+ const point = contour.points[slot], next = contour.points[(slot + 1) % contour.points.length];
226
+ const ax = point[0] / ring.radius, ay = point[1] / ring.radius;
227
+ const dx = next[0] / ring.radius - ax, dy = next[1] / ring.radius - ay, length = Math.hypot(dx, dy);
228
+ if (length > 0)
229
+ support = Math.min(support, Math.abs(ax * (dy / length) - ay * (dx / length)));
230
+ }
231
+ if (!Number.isFinite(support) || !(support > 0))
232
+ return fail("Face-sized primitive facets must enclose their local origin.");
233
+ radialScale = 1 / support;
234
+ }
235
+ const points = contour.points.map(([x, y]) => [
236
+ matrix[0] * x * radialScale + matrix[1] * y * radialScale + matrix[2],
237
+ matrix[3] * x * radialScale + matrix[4] * y * radialScale + matrix[5],
238
+ ]);
239
+ return {
240
+ ...component,
241
+ outer: {
242
+ ...contour,
243
+ points: reflected ? points.reverse() : points,
244
+ spans: reflected ? [...contour.spans].reverse() : contour.spans,
245
+ },
246
+ };
247
+ }),
248
+ };
249
+ });
250
+ }