@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.
- package/CHANGELOG.md +111 -0
- package/LICENSE +200 -0
- package/README.md +376 -2
- package/THIRD_PARTY_NOTICES.md +153 -0
- package/dist/api/engine.d.ts +71 -0
- package/dist/api/geometry.d.ts +3136 -0
- package/dist/api/index.d.ts +2087 -0
- package/dist/api/text.d.ts +93 -0
- package/dist/engine/construction-operations.js +189 -0
- package/dist/engine/execution.js +227 -0
- package/dist/engine/export-3mf.js +100 -0
- package/dist/engine/extrusion-finishing.js +82 -0
- package/dist/engine/finalization.js +214 -0
- package/dist/engine/index.js +83 -0
- package/dist/engine/inspection-operations.js +52 -0
- package/dist/engine/kernel/LICENSE +201 -0
- package/dist/engine/kernel/manifold.js +2 -0
- package/dist/engine/kernel/manifold.wasm +0 -0
- package/dist/engine/loft-operations.js +156 -0
- package/dist/engine/mechanical-operations.js +51 -0
- package/dist/engine/modeling-context.js +38 -0
- package/dist/engine/native/adapter.js +20 -0
- package/dist/engine/native/context.js +209 -0
- package/dist/engine/native/copied-views.js +46 -0
- package/dist/engine/native/index.js +13 -0
- package/dist/engine/native/operations.js +115 -0
- package/dist/engine/native/ownership.js +98 -0
- package/dist/engine/native/regions.js +138 -0
- package/dist/engine/native/scoped-primitives.js +127 -0
- package/dist/engine/native/scoped-threads.js +34 -0
- package/dist/engine/native/threads.js +52 -0
- package/dist/engine/native/transforms.js +111 -0
- package/dist/engine/operation-environment.js +20 -0
- package/dist/engine/pattern-operations.js +36 -0
- package/dist/engine/pattern-placements.js +176 -0
- package/dist/engine/primitive-anchors.js +70 -0
- package/dist/engine/primitive-correspondence.js +74 -0
- package/dist/engine/primitive-operations.js +151 -0
- package/dist/engine/primitive-profile-types.js +1 -0
- package/dist/engine/primitive-profiles.js +250 -0
- package/dist/engine/profile-mesh.js +279 -0
- package/dist/engine/section-operations.js +93 -0
- package/dist/engine/shape-anchors.js +88 -0
- package/dist/engine/transform-operations.js +225 -0
- package/dist/engine/types.js +1 -0
- package/dist/geometry/anchors.js +226 -0
- package/dist/geometry/curves/evaluation.js +162 -0
- package/dist/geometry/curves/sampling.js +266 -0
- package/dist/geometry/curves/types.js +1 -0
- package/dist/geometry/cylinder.js +117 -0
- package/dist/geometry/edge-finishing.js +160 -0
- package/dist/geometry/errors.js +76 -0
- package/dist/geometry/extrusion.js +150 -0
- package/dist/geometry/heightfield.js +115 -0
- package/dist/geometry/index.js +42 -0
- package/dist/geometry/loft-correspondence.js +204 -0
- package/dist/geometry/loft-geometry.js +184 -0
- package/dist/geometry/loft-options.js +56 -0
- package/dist/geometry/loft-simple-mesh.js +92 -0
- package/dist/geometry/loft-triangulation.js +64 -0
- package/dist/geometry/loft.js +95 -0
- package/dist/geometry/math/vector3.js +71 -0
- package/dist/geometry/mechanical/gear-motion.js +94 -0
- package/dist/geometry/mechanical/gears.js +484 -0
- package/dist/geometry/mechanical/index.js +13 -0
- package/dist/geometry/mechanical/standards.js +67 -0
- package/dist/geometry/mechanical/thread-helices.js +44 -0
- package/dist/geometry/mechanical/thread-mesh-plan.js +358 -0
- package/dist/geometry/mechanical/thread-mesh.js +142 -0
- package/dist/geometry/mechanical/thread-profiles.js +373 -0
- package/dist/geometry/mechanical/thread-solids.js +230 -0
- package/dist/geometry/mechanical/thread-surface.js +433 -0
- package/dist/geometry/mechanical/threads.js +138 -0
- package/dist/geometry/mesh-edge-table.js +126 -0
- package/dist/geometry/mesh-planar.js +408 -0
- package/dist/geometry/mesh-quality.js +396 -0
- package/dist/geometry/mesh-signature.js +137 -0
- package/dist/geometry/mesh.js +283 -0
- package/dist/geometry/model-limits.js +50 -0
- package/dist/geometry/numeric.js +11 -0
- package/dist/geometry/paths/frames.js +118 -0
- package/dist/geometry/paths/helix.js +49 -0
- package/dist/geometry/paths/path.js +32 -0
- package/dist/geometry/paths/planar.js +135 -0
- package/dist/geometry/paths/resampling.js +125 -0
- package/dist/geometry/polygon-clipping.js +52 -0
- package/dist/geometry/primitive-options.js +1 -0
- package/dist/geometry/primitive-resolution.js +17 -0
- package/dist/geometry/primitive-resolvers.js +235 -0
- package/dist/geometry/profiles/matched.js +192 -0
- package/dist/geometry/profiles/open-profile.js +137 -0
- package/dist/geometry/profiles/profiles.js +91 -0
- package/dist/geometry/profiles/rectangle.js +249 -0
- package/dist/geometry/profiles/shapes.js +41 -0
- package/dist/geometry/profiles/span-sampling.js +28 -0
- package/dist/geometry/profiles/types.js +1 -0
- package/dist/geometry/profiles/validation.js +20 -0
- package/dist/geometry/regions.js +371 -0
- package/dist/geometry/resolution.js +236 -0
- package/dist/geometry/revolve/revolve-mesh.js +129 -0
- package/dist/geometry/sampling-limits.js +29 -0
- package/dist/geometry/stroke-path.js +181 -0
- package/dist/geometry/svg/errors.js +23 -0
- package/dist/geometry/svg/fill.js +172 -0
- package/dist/geometry/svg/index.js +1 -0
- package/dist/geometry/svg/matrix.js +130 -0
- package/dist/geometry/svg/parser.js +536 -0
- package/dist/geometry/svg/path.js +349 -0
- package/dist/geometry/svg/types.js +1 -0
- package/dist/geometry/svg-import.js +662 -0
- package/dist/geometry/sweeps/options.js +28 -0
- package/dist/geometry/sweeps/orientation.js +106 -0
- package/dist/geometry/sweeps/stations.js +161 -0
- package/dist/geometry/sweeps/sweep-closed.js +96 -0
- package/dist/geometry/sweeps/sweep-mesh.js +30 -0
- package/dist/geometry/sweeps/sweep-open.js +76 -0
- package/dist/geometry/sweeps/validation.js +116 -0
- package/dist/geometry/text-contours.js +32 -0
- package/dist/geometry/text-layout.js +1 -0
- package/dist/geometry/tolerance.js +53 -0
- package/dist/geometry/transform-geometry.js +41 -0
- package/dist/geometry/transform3.js +340 -0
- package/dist/geometry/types.js +1 -0
- package/dist/index.js +1 -0
- package/dist/model/execution-binding.js +30 -0
- package/dist/model/operations.js +154 -0
- package/dist/model/options.js +1 -0
- package/dist/model/types.js +1 -0
- package/dist/text/index.js +58 -0
- package/dist/text/vector-text-layout.js +279 -0
- package/package.json +100 -3
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { planarCubicControl } from "../curves/evaluation.js";
|
|
2
|
+
import { resolveLineSegmentCount, resolveUniformCubicSegments } from "../curves/sampling.js";
|
|
3
|
+
import { GeometryError } from "../errors.js";
|
|
4
|
+
import { normalizeRegion2, signedAreaLoop2 } from "../regions.js";
|
|
5
|
+
import { resolveArcSegments, resolveGeometryResolution } from "../resolution.js";
|
|
6
|
+
import { assertSamplingCount, resolveGeometrySamplingLimits } from "../sampling-limits.js";
|
|
7
|
+
import { createProfileSpanSampler } from "./span-sampling.js";
|
|
8
|
+
const isArray = (value) => Array.isArray(value);
|
|
9
|
+
const fail = (message) => {
|
|
10
|
+
throw new GeometryError("INVALID_ARGUMENT", message, { operation: "matchProfiles" });
|
|
11
|
+
};
|
|
12
|
+
const point = (value) => {
|
|
13
|
+
if (!isArray(value) || value.length !== 2 || !value.every(Number.isFinite))
|
|
14
|
+
fail("Profile points must contain two finite coordinates.");
|
|
15
|
+
};
|
|
16
|
+
const identity = (value) => {
|
|
17
|
+
if (typeof value !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value) || value.length > 64)
|
|
18
|
+
fail("Profile identities must be canonical identifiers of at most 64 characters.");
|
|
19
|
+
};
|
|
20
|
+
const validateContour = (contour) => {
|
|
21
|
+
identity(contour.id);
|
|
22
|
+
if (!isArray(contour.spans) || contour.spans.length === 0 || contour.spans.length > 1000)
|
|
23
|
+
fail("Each profile contour needs one through 1000 identified spans.");
|
|
24
|
+
const ids = new Set();
|
|
25
|
+
for (const span of contour.spans) {
|
|
26
|
+
identity(span.id);
|
|
27
|
+
if (ids.has(span.id))
|
|
28
|
+
fail("Span identities must be unique within a contour.");
|
|
29
|
+
ids.add(span.id);
|
|
30
|
+
if (span.segments !== undefined && (!Number.isSafeInteger(span.segments) || span.segments < 1))
|
|
31
|
+
fail("Exact span intervals must be positive safe integers.");
|
|
32
|
+
if (span.kind === "line" || span.kind === "cubic") {
|
|
33
|
+
point(span.start);
|
|
34
|
+
point(span.end);
|
|
35
|
+
if (span.kind === "cubic") {
|
|
36
|
+
point(span.control1);
|
|
37
|
+
point(span.control2);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else if (span.kind === "arc") {
|
|
41
|
+
point(span.center);
|
|
42
|
+
if (![span.radius, span.startDegrees, span.sweepDegrees].every(Number.isFinite) ||
|
|
43
|
+
span.radius < 0 ||
|
|
44
|
+
Math.abs(span.sweepDegrees) > 360 ||
|
|
45
|
+
span.sweepDegrees === 0)
|
|
46
|
+
fail("Arc spans need nonnegative radius and a nonzero finite sweep no larger than 360 degrees.");
|
|
47
|
+
}
|
|
48
|
+
else
|
|
49
|
+
fail("Profile spans must be identified lines, circular arcs, or cubic Béziers.");
|
|
50
|
+
}
|
|
51
|
+
for (let index = 0; index < contour.spans.length; index += 1) {
|
|
52
|
+
const currentSpan = contour.spans[index];
|
|
53
|
+
const nextSpan = contour.spans[(index + 1) % contour.spans.length];
|
|
54
|
+
const end = currentSpan.kind === "cubic" ? currentSpan.end : createProfileSpanSampler(currentSpan)(1);
|
|
55
|
+
const next = nextSpan.kind === "cubic" ? nextSpan.start : createProfileSpanSampler(nextSpan)(0);
|
|
56
|
+
const scale = Math.max(1, ...end.map(Math.abs), ...next.map(Math.abs));
|
|
57
|
+
if (Math.hypot(end[0] - next[0], end[1] - next[1]) > scale * 1e-12)
|
|
58
|
+
fail("Ordered profile spans must meet at their paired landmarks.");
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const contours = (component) => [
|
|
62
|
+
component.outer,
|
|
63
|
+
...component.holes,
|
|
64
|
+
];
|
|
65
|
+
/**
|
|
66
|
+
* Explicitly matches identified component/contour/span topology across a family.
|
|
67
|
+
* Every paired span uses the maximum physical subdivision count requested by any member.
|
|
68
|
+
* Cubics share a checked uniform parameter grid, including grids selected by exact paired requests.
|
|
69
|
+
* Input order and winding are preserved. Collapsed spans retain repeated logical slots, which
|
|
70
|
+
* the engine maps to shared physical vertices while omitting degenerate faces.
|
|
71
|
+
*
|
|
72
|
+
* @example Match named rectangle landmarks
|
|
73
|
+
* ```ts
|
|
74
|
+
* matchProfiles([
|
|
75
|
+
* rectangleProfile({ size: [20, 12] }),
|
|
76
|
+
* rectangleProfile({ size: [30, 20], treatment: { kind: "round", radius: 3 } }),
|
|
77
|
+
* ]);
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export function matchProfiles(profiles, options = {}) {
|
|
81
|
+
if (!isArray(profiles) || profiles.length === 0 || profiles.length > 1000)
|
|
82
|
+
fail("A profile family needs one through 1000 members.");
|
|
83
|
+
const limits = resolveGeometrySamplingLimits(options.limits);
|
|
84
|
+
const resolution = resolveGeometryResolution({ ...options.resolution }, limits);
|
|
85
|
+
// This family-wide work bound is independent of configurable per-profile output ceilings.
|
|
86
|
+
const maximumFamilyPoints = 1_000_000;
|
|
87
|
+
const cubicBudget = Math.min(limits.maxProfilePoints, Math.floor(maximumFamilyPoints / profiles.length));
|
|
88
|
+
const first = profiles[0];
|
|
89
|
+
for (const profile of profiles) {
|
|
90
|
+
if (!isArray(profile.components) ||
|
|
91
|
+
profile.components.length === 0 ||
|
|
92
|
+
profile.components.length > 1000)
|
|
93
|
+
fail("Profiles need one through 1000 components.");
|
|
94
|
+
const componentIds = new Set();
|
|
95
|
+
for (const component of profile.components) {
|
|
96
|
+
identity(component.id);
|
|
97
|
+
if (componentIds.has(component.id))
|
|
98
|
+
fail("Component identities must be unique.");
|
|
99
|
+
componentIds.add(component.id);
|
|
100
|
+
if (!isArray(component.holes) || component.holes.length > 1000)
|
|
101
|
+
fail("Components may have at most 1000 holes.");
|
|
102
|
+
const contourIds = new Set();
|
|
103
|
+
for (const contour of contours(component)) {
|
|
104
|
+
if (contourIds.has(contour.id))
|
|
105
|
+
fail("Contour identities must be unique within each component.");
|
|
106
|
+
contourIds.add(contour.id);
|
|
107
|
+
validateContour(contour);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (profile.components.length !== first.components.length)
|
|
111
|
+
fail("Profile components cannot appear or disappear within a family.");
|
|
112
|
+
profile.components.forEach((component, componentIndex) => {
|
|
113
|
+
const expected = first.components[componentIndex];
|
|
114
|
+
if (component.id !== expected.id || component.holes.length !== expected.holes.length)
|
|
115
|
+
fail("Profile component identities and hole counts must retain the same order.");
|
|
116
|
+
contours(component).forEach((contour, contourIndex) => {
|
|
117
|
+
const paired = contours(expected)[contourIndex];
|
|
118
|
+
if (contour.id !== paired.id ||
|
|
119
|
+
contour.spans.length !== paired.spans.length ||
|
|
120
|
+
contour.spans.some((span, index) => span.id !== paired.spans[index].id))
|
|
121
|
+
fail("Profile contour and paired span identities must retain the same order.");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
let pointsPerProfile = 0;
|
|
126
|
+
const sampling = first.components.map((component, componentIndex) => contours(component).map((contour, contourIndex) => contour.spans.map((_, spanIndex) => {
|
|
127
|
+
let count = Math.max(...profiles.map((profile) => {
|
|
128
|
+
const span = contours(profile.components[componentIndex])[contourIndex].spans[spanIndex];
|
|
129
|
+
if (span.segments !== undefined)
|
|
130
|
+
return span.segments;
|
|
131
|
+
const selectedResolution = resolveGeometryResolution({ ...resolution, ...span.resolution }, limits);
|
|
132
|
+
if (span.kind === "arc")
|
|
133
|
+
return span.radius === 0
|
|
134
|
+
? 1
|
|
135
|
+
: resolveArcSegments(span.radius, Math.abs(span.sweepDegrees), selectedResolution, limits);
|
|
136
|
+
if (span.kind === "cubic")
|
|
137
|
+
return resolveUniformCubicSegments(planarCubicControl(span), selectedResolution, cubicBudget);
|
|
138
|
+
return resolveLineSegmentCount(Math.hypot(span.end[0] - span.start[0], span.end[1] - span.start[1]), selectedResolution.maxSegmentLength);
|
|
139
|
+
}));
|
|
140
|
+
// An exact request in another member may choose a non-dyadic grid. Recheck adaptive
|
|
141
|
+
// cubics on that actual shared grid instead of assuming a larger count retains the bound.
|
|
142
|
+
while (true) {
|
|
143
|
+
let checkedCount = count;
|
|
144
|
+
for (const profile of profiles) {
|
|
145
|
+
const span = contours(profile.components[componentIndex])[contourIndex].spans[spanIndex];
|
|
146
|
+
if (span.kind === "cubic" && span.segments === undefined)
|
|
147
|
+
checkedCount = Math.max(checkedCount, resolveUniformCubicSegments(planarCubicControl(span), resolveGeometryResolution({ ...resolution, ...span.resolution }, limits), cubicBudget, count));
|
|
148
|
+
}
|
|
149
|
+
if (checkedCount === count)
|
|
150
|
+
break;
|
|
151
|
+
count = checkedCount;
|
|
152
|
+
}
|
|
153
|
+
pointsPerProfile += count;
|
|
154
|
+
assertSamplingCount(pointsPerProfile, "matchProfiles", limits.maxProfilePoints, "Profile sampling exceeds its selected per-profile point ceiling.");
|
|
155
|
+
assertSamplingCount(pointsPerProfile * profiles.length, "matchProfiles", maximumFamilyPoints, "Profile family sampling exceeds its one-million-logical-slot work bound.");
|
|
156
|
+
return count;
|
|
157
|
+
})));
|
|
158
|
+
return Object.freeze(profiles.map((profile) => {
|
|
159
|
+
const components = profile.components.map((component, componentIndex) => {
|
|
160
|
+
const sampled = contours(component).map((contour, contourIndex) => {
|
|
161
|
+
const points = [], spans = [];
|
|
162
|
+
contour.spans.forEach((span, spanIndex) => {
|
|
163
|
+
const count = sampling[componentIndex][contourIndex][spanIndex];
|
|
164
|
+
const sample = createProfileSpanSampler(span);
|
|
165
|
+
for (let index = 0; index < count; index += 1) {
|
|
166
|
+
points.push(Object.freeze(sample(index / count)));
|
|
167
|
+
spans.push(span.id);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
const area = signedAreaLoop2(points);
|
|
171
|
+
if (!(contourIndex === 0 ? area > 0 : area < 0))
|
|
172
|
+
fail("Outer contours must be CCW and holes CW; matching never reorders winding.");
|
|
173
|
+
return Object.freeze({
|
|
174
|
+
id: contour.id,
|
|
175
|
+
points: Object.freeze(points),
|
|
176
|
+
spans: Object.freeze(spans),
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
return Object.freeze({
|
|
180
|
+
id: component.id,
|
|
181
|
+
outer: sampled[0],
|
|
182
|
+
holes: Object.freeze(sampled.slice(1)),
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
// This validates intersections and containment, without changing logical slots or topology.
|
|
186
|
+
normalizeRegion2(components.map((component) => ({
|
|
187
|
+
outer: component.outer.points,
|
|
188
|
+
holes: component.holes.map((hole) => hole.points),
|
|
189
|
+
})));
|
|
190
|
+
return Object.freeze({ components: Object.freeze(components) });
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { GeometryError } from "../errors.js";
|
|
2
|
+
import { resolveGeometryResolution } from "../resolution.js";
|
|
3
|
+
import { resolveGeometrySamplingLimits } from "../sampling-limits.js";
|
|
4
|
+
import { requireProfileVertexCount } from "./validation.js";
|
|
5
|
+
const OPERATION = "sampleOpenProfile";
|
|
6
|
+
const MAX_SEGMENTS = 31;
|
|
7
|
+
const MAX_DEPTH = 24;
|
|
8
|
+
function invalid(message) {
|
|
9
|
+
throw new GeometryError("INVALID_ARGUMENT", message, { operation: OPERATION });
|
|
10
|
+
}
|
|
11
|
+
function midpoint(a, b) {
|
|
12
|
+
return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
13
|
+
}
|
|
14
|
+
function distanceToChord(point, start, end) {
|
|
15
|
+
const dx = end[0] - start[0];
|
|
16
|
+
const dy = end[1] - start[1];
|
|
17
|
+
const squared = dx * dx + dy * dy;
|
|
18
|
+
const t = squared === 0
|
|
19
|
+
? 0
|
|
20
|
+
: Math.max(0, Math.min(1, ((point[0] - start[0]) * dx + (point[1] - start[1]) * dy) / squared));
|
|
21
|
+
return Math.hypot(point[0] - start[0] - t * dx, point[1] - start[1] - t * dy);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Samples 1–31 line/cubic segments into a deeply immutable open [radius,height] polyline.
|
|
25
|
+
* Anchors must progress strictly upward; nonnegative-radius cubic controls must have ordered
|
|
26
|
+
* heights between their endpoints. Cubics use de Casteljau subdivision with a conservative
|
|
27
|
+
* control-hull chord-error bound. Lines also obey the optional physical edge-length ceiling.
|
|
28
|
+
* Neither normalized coordinates nor editor metadata are required. Endpoints are retained once;
|
|
29
|
+
* no axis closure, winding normalization, or manufacturing repair is performed.
|
|
30
|
+
*
|
|
31
|
+
* @param curve - Open outline in millimetres unless the positive scales convert its coordinates.
|
|
32
|
+
* @param options - Physical resolution and output budget; defaults to 4096 vertices.
|
|
33
|
+
* @returns Frozen open polyline in millimetres, ordered from lowest to highest anchor.
|
|
34
|
+
* @throws {@link GeometryError} for invalid coordinates, order, scale, or exceeded subdivision budget.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const side = sampleOpenProfile({
|
|
39
|
+
* start: { radius: 0.5, height: 0 },
|
|
40
|
+
* segments: [{ type: "line", end: { radius: 1, height: 1 } }],
|
|
41
|
+
* }, { radiusScale: 12, heightScale: 30 });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function sampleOpenProfile(curve, options = {}) {
|
|
45
|
+
const radiusScale = options.radiusScale ?? 1;
|
|
46
|
+
const heightScale = options.heightScale ?? 1;
|
|
47
|
+
if (![radiusScale, heightScale].every((scale) => Number.isFinite(scale) && scale > 0)) {
|
|
48
|
+
invalid("Profile scales must be finite and positive.");
|
|
49
|
+
}
|
|
50
|
+
const maximumVertices = options.maximumVertices ??
|
|
51
|
+
Math.min(4096, resolveGeometrySamplingLimits(options.limits).maxProfilePoints);
|
|
52
|
+
requireProfileVertexCount(maximumVertices, 2, OPERATION, options.limits);
|
|
53
|
+
const maximumProfilePoints = resolveGeometrySamplingLimits(options.limits).maxProfilePoints;
|
|
54
|
+
const segmentInput = curve.segments;
|
|
55
|
+
if (!Array.isArray(segmentInput) ||
|
|
56
|
+
curve.segments.length < 1 ||
|
|
57
|
+
curve.segments.length > MAX_SEGMENTS) {
|
|
58
|
+
invalid("An open profile must contain 1–31 segments.");
|
|
59
|
+
}
|
|
60
|
+
const resolution = resolveGeometryResolution(options.resolution, options.limits);
|
|
61
|
+
const physical = (point) => {
|
|
62
|
+
if (!point || !Number.isFinite(point.radius) || !Number.isFinite(point.height)) {
|
|
63
|
+
invalid("Profile coordinates must be finite.");
|
|
64
|
+
}
|
|
65
|
+
const radius = point.radius * radiusScale;
|
|
66
|
+
const height = point.height * heightScale;
|
|
67
|
+
if (!Number.isFinite(radius) || !Number.isFinite(height) || radius < 0) {
|
|
68
|
+
invalid("Scaled coordinates must be finite with a nonnegative radius.");
|
|
69
|
+
}
|
|
70
|
+
return [radius, height];
|
|
71
|
+
};
|
|
72
|
+
const output = [];
|
|
73
|
+
const append = (point) => {
|
|
74
|
+
if (output.length >= maximumVertices || output.length >= maximumProfilePoints) {
|
|
75
|
+
throw new GeometryError("COMPLEXITY_LIMIT", "Profile sampling exceeds its vertex budget.", {
|
|
76
|
+
operation: OPERATION,
|
|
77
|
+
details: { maximumVertices },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
output.push(Object.freeze(point));
|
|
81
|
+
};
|
|
82
|
+
const flatten = (a, b, c, d, depth) => {
|
|
83
|
+
if (Math.max(distanceToChord(b, a, d), distanceToChord(c, a, d)) <= resolution.maxChordError &&
|
|
84
|
+
(resolution.maxSegmentLength === undefined ||
|
|
85
|
+
Math.hypot(d[0] - a[0], d[1] - a[1]) <= resolution.maxSegmentLength)) {
|
|
86
|
+
append(d);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (depth >= MAX_DEPTH) {
|
|
90
|
+
throw new GeometryError("COMPLEXITY_LIMIT", "Profile subdivision exceeds its recursion limit.", {
|
|
91
|
+
operation: OPERATION,
|
|
92
|
+
details: { maximumDepth: MAX_DEPTH },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const ab = midpoint(a, b), bc = midpoint(b, c), cd = midpoint(c, d);
|
|
96
|
+
const abc = midpoint(ab, bc), bcd = midpoint(bc, cd), split = midpoint(abc, bcd);
|
|
97
|
+
flatten(a, ab, abc, split, depth + 1);
|
|
98
|
+
flatten(split, bcd, cd, d, depth + 1);
|
|
99
|
+
};
|
|
100
|
+
let start = physical(curve.start);
|
|
101
|
+
append(start);
|
|
102
|
+
for (const segment of curve.segments) {
|
|
103
|
+
const end = physical(segment.end);
|
|
104
|
+
if (!(end[1] > start[1]))
|
|
105
|
+
invalid("Profile anchor heights must increase strictly.");
|
|
106
|
+
if (segment.type === "line") {
|
|
107
|
+
const length = Math.hypot(end[0] - start[0], end[1] - start[1]);
|
|
108
|
+
const count = resolution.maxSegmentLength === undefined
|
|
109
|
+
? 1
|
|
110
|
+
: Math.max(1, Math.ceil(length / resolution.maxSegmentLength));
|
|
111
|
+
if (count > maximumVertices - output.length) {
|
|
112
|
+
throw new GeometryError("COMPLEXITY_LIMIT", "Profile line exceeds its vertex budget.", {
|
|
113
|
+
operation: OPERATION,
|
|
114
|
+
details: { maximumVertices },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
for (let index = 1; index <= count; index++) {
|
|
118
|
+
const t = index / count;
|
|
119
|
+
append(index === count
|
|
120
|
+
? end
|
|
121
|
+
: [start[0] + (end[0] - start[0]) * t, start[1] + (end[1] - start[1]) * t]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if (segment.type === "cubic") {
|
|
125
|
+
const first = physical(segment.control1), second = physical(segment.control2);
|
|
126
|
+
if (!(start[1] <= first[1] && first[1] <= second[1] && second[1] <= end[1])) {
|
|
127
|
+
invalid("Cubic control heights must progress between their anchors.");
|
|
128
|
+
}
|
|
129
|
+
flatten(start, first, second, end, 0);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
invalid("Profile segments must be lines or cubics.");
|
|
133
|
+
}
|
|
134
|
+
start = end;
|
|
135
|
+
}
|
|
136
|
+
return Object.freeze(output);
|
|
137
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { GeometryError } from "../errors.js";
|
|
2
|
+
import { resolveCircularPrimitiveSegments } from "../primitive-resolution.js";
|
|
3
|
+
import { assertPrimitiveOptions } from "../primitive-resolvers.js";
|
|
4
|
+
import { requireFiniteProfileValues, requireProfileVertexCount } from "./validation.js";
|
|
5
|
+
export { rectangleLoop } from "./rectangle.js";
|
|
6
|
+
/**
|
|
7
|
+
* Creates an immutable centered CCW circle with +X seam and vertex or face radial sizing.
|
|
8
|
+
*
|
|
9
|
+
* @example Explicit circular point data
|
|
10
|
+
* ```ts
|
|
11
|
+
* circleLoop({ diameter: 20, segments: 32 });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export const circleLoop = (options) => {
|
|
15
|
+
const operation = "circleLoop";
|
|
16
|
+
assertPrimitiveOptions(options, operation);
|
|
17
|
+
if ((options.radius === undefined) === (options.diameter === undefined)) {
|
|
18
|
+
throw new GeometryError("INVALID_ARGUMENT", "Specify exactly one radius or diameter.", {
|
|
19
|
+
operation,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const radius = options.radius ?? options.diameter / 2;
|
|
23
|
+
requireFiniteProfileValues([radius], operation);
|
|
24
|
+
if (!(radius > 0)) {
|
|
25
|
+
throw new GeometryError("INVALID_ARGUMENT", "Circle radius must be positive.", { operation });
|
|
26
|
+
}
|
|
27
|
+
const segments = resolveCircularPrimitiveSegments(radius, options, operation);
|
|
28
|
+
requireProfileVertexCount(segments, 3, operation, options.limits);
|
|
29
|
+
const sizing = options.sizing ?? "vertex";
|
|
30
|
+
if (sizing !== "vertex" && sizing !== "face") {
|
|
31
|
+
throw new GeometryError("INVALID_ARGUMENT", "Circle sizing must be vertex or face.", {
|
|
32
|
+
operation,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const sampledRadius = sizing === "face" ? radius / Math.cos(Math.PI / segments) : radius;
|
|
36
|
+
requireFiniteProfileValues([sampledRadius], operation);
|
|
37
|
+
return Object.freeze(Array.from({ length: segments }, (_, index) => {
|
|
38
|
+
const angle = (Math.PI * 2 * index) / segments;
|
|
39
|
+
return Object.freeze([sampledRadius * Math.cos(angle), sampledRadius * Math.sin(angle)]);
|
|
40
|
+
}));
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Creates a centered CCW point loop with vertex or face radial sizing and an implicit seam.
|
|
44
|
+
*
|
|
45
|
+
* @example Hexagon sized to its faces
|
|
46
|
+
* ```ts
|
|
47
|
+
* regularPolygonLoop({ radius: 10, sides: 6, sizing: "face" });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export const regularPolygonLoop = (options) => {
|
|
51
|
+
const operation = "regularPolygonLoop";
|
|
52
|
+
const { radius, sides, rotationDegrees = 0, sizing = "vertex" } = options;
|
|
53
|
+
requireFiniteProfileValues([radius, rotationDegrees], operation);
|
|
54
|
+
if (!(radius > 0)) {
|
|
55
|
+
throw new GeometryError("INVALID_ARGUMENT", "Polygon radius must be positive.", { operation });
|
|
56
|
+
}
|
|
57
|
+
requireProfileVertexCount(sides, 3, operation, options.limits);
|
|
58
|
+
if (sizing !== "vertex" && sizing !== "face")
|
|
59
|
+
throw new GeometryError("INVALID_ARGUMENT", "Polygon sizing must be vertex or face.", {
|
|
60
|
+
operation,
|
|
61
|
+
});
|
|
62
|
+
const sampledRadius = sizing === "face" ? radius / Math.cos(Math.PI / sides) : radius;
|
|
63
|
+
requireFiniteProfileValues([sampledRadius], operation);
|
|
64
|
+
const rotation = ((rotationDegrees % 360) * Math.PI) / 180;
|
|
65
|
+
return Array.from({ length: sides }, (_, index) => {
|
|
66
|
+
const angle = rotation + (Math.PI * 2 * index) / sides;
|
|
67
|
+
return [sampledRadius * Math.cos(angle), sampledRadius * Math.sin(angle)];
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Creates a centered CCW alternating-radius star within the selected profile sampling ceiling.
|
|
72
|
+
*
|
|
73
|
+
* @example Five-point outline
|
|
74
|
+
* ```ts
|
|
75
|
+
* starLoop(12, 5, 6);
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export const starLoop = (outerRadius, points, innerRadius = outerRadius / 2, limits = {}) => {
|
|
79
|
+
const operation = "starLoop";
|
|
80
|
+
requireFiniteProfileValues([outerRadius, innerRadius], operation);
|
|
81
|
+
if (!(outerRadius > 0) || !(innerRadius > 0) || innerRadius >= outerRadius) {
|
|
82
|
+
throw new GeometryError("INVALID_ARGUMENT", "Star radii must be positive, with the inner radius smaller than the outer.", { operation });
|
|
83
|
+
}
|
|
84
|
+
requireProfileVertexCount(points, 3, operation, limits);
|
|
85
|
+
requireProfileVertexCount(points * 2, 6, operation, limits);
|
|
86
|
+
return Array.from({ length: points * 2 }, (_, index) => {
|
|
87
|
+
const radius = index % 2 === 0 ? outerRadius : innerRadius;
|
|
88
|
+
const angle = (Math.PI * index) / points;
|
|
89
|
+
return [radius * Math.cos(angle), radius * Math.sin(angle)];
|
|
90
|
+
});
|
|
91
|
+
};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { edgeTreatmentPointCount, edgeTreatmentSetbacks, sampleEdgeTreatment, } from "../edge-finishing.js";
|
|
2
|
+
import { GeometryError } from "../errors.js";
|
|
3
|
+
import { resolveModelingLimits } from "../model-limits.js";
|
|
4
|
+
import { assertPrimitiveOptions } from "../primitive-resolvers.js";
|
|
5
|
+
import { resolveAlignedCircularSegments } from "../resolution.js";
|
|
6
|
+
import { assertSamplingCount } from "../sampling-limits.js";
|
|
7
|
+
import { requireFiniteProfileValues, requireProfileVertexCount } from "./validation.js";
|
|
8
|
+
const fail = (operation, message) => {
|
|
9
|
+
throw new GeometryError("INVALID_ARGUMENT", message, { operation });
|
|
10
|
+
};
|
|
11
|
+
/** @internal Validates the shared rectangle family before native or pure allocation. */
|
|
12
|
+
export function resolveRectangle(options, operation) {
|
|
13
|
+
assertPrimitiveOptions(options, operation);
|
|
14
|
+
if (typeof options.size !== "number" &&
|
|
15
|
+
(!Array.isArray(options.size) || options.size.length !== 2))
|
|
16
|
+
fail(operation, "size must be a number or an exact XY tuple.");
|
|
17
|
+
if (options.center !== undefined && typeof options.center !== "boolean")
|
|
18
|
+
fail(operation, "center must be boolean.");
|
|
19
|
+
const [width, height] = typeof options.size === "number" ? [options.size, options.size] : options.size;
|
|
20
|
+
requireFiniteProfileValues([width, height], operation);
|
|
21
|
+
if (!(width > 0) || !(height > 0))
|
|
22
|
+
fail(operation, "Rectangle dimensions must be positive.");
|
|
23
|
+
const treatment = options.treatment;
|
|
24
|
+
const square = { kind: "round", radius: 0 };
|
|
25
|
+
const corners = treatment === undefined
|
|
26
|
+
? [square, square, square, square]
|
|
27
|
+
: Array.isArray(treatment)
|
|
28
|
+
? treatment
|
|
29
|
+
: [treatment, treatment, treatment, treatment];
|
|
30
|
+
if (corners.length !== 4 ||
|
|
31
|
+
corners.some((corner) => typeof corner !== "object" || corner === null))
|
|
32
|
+
fail(operation, "Rectangle treatments require a uniform treatment or four corner objects.");
|
|
33
|
+
const setbacks = corners.map((corner) => {
|
|
34
|
+
if (corner.kind === "square") {
|
|
35
|
+
if (!Array.isArray(treatment))
|
|
36
|
+
fail(operation, "An untreated corner is available only in a four-corner treatment tuple.");
|
|
37
|
+
return [0, 0];
|
|
38
|
+
}
|
|
39
|
+
return edgeTreatmentSetbacks(corner, operation, Math.PI / 2, options.limits);
|
|
40
|
+
});
|
|
41
|
+
const [tr, tl, bl, br] = setbacks;
|
|
42
|
+
if (tr[1] + tl[0] > width ||
|
|
43
|
+
bl[1] + br[0] > width ||
|
|
44
|
+
tl[1] + bl[0] > height ||
|
|
45
|
+
br[1] + tr[0] > height)
|
|
46
|
+
fail(operation, "Adjacent rectangle corner treatments must fit without overlap.");
|
|
47
|
+
if (!Array.isArray(treatment) &&
|
|
48
|
+
treatment?.kind === "chamfer" &&
|
|
49
|
+
setbacks.some((pair) => pair.some((value) => 2 * value >= Math.min(width, height))))
|
|
50
|
+
fail(operation, "Uniform chamfers must be smaller than half every dimension.");
|
|
51
|
+
const baseLimits = resolveModelingLimits(options.limits);
|
|
52
|
+
const limits = { ...baseLimits };
|
|
53
|
+
for (const corner of corners)
|
|
54
|
+
if (corner.kind === "round" || corner.kind === "smooth") {
|
|
55
|
+
const local = resolveModelingLimits(corner.limits, baseLimits);
|
|
56
|
+
for (const key of Object.keys(baseLimits))
|
|
57
|
+
limits[key] = Math.min(limits[key], local[key]);
|
|
58
|
+
}
|
|
59
|
+
return { width, height, corners, setbacks, limits };
|
|
60
|
+
}
|
|
61
|
+
/** @internal Resolves one quarter arc under the same exact and physical convention as native rectangles. */
|
|
62
|
+
export function rectangleCornerSegments(corner, limits) {
|
|
63
|
+
if (corner.kind !== "round")
|
|
64
|
+
return 1;
|
|
65
|
+
const selected = resolveModelingLimits(limits);
|
|
66
|
+
const exact = corner.segments ?? corner.resolution?.circularSegments;
|
|
67
|
+
if (exact !== undefined && Number.isSafeInteger(exact) && exact >= 3)
|
|
68
|
+
assertSamplingCount(exact, "rectangleLoop", selected.maxCircularSegments);
|
|
69
|
+
if (corner.radius === 0 &&
|
|
70
|
+
corner.segments === undefined &&
|
|
71
|
+
corner.resolution?.circularSegments === undefined)
|
|
72
|
+
return 1;
|
|
73
|
+
return (resolveAlignedCircularSegments(Math.max(corner.radius, Number.MIN_VALUE), {
|
|
74
|
+
alignTo: [4],
|
|
75
|
+
minimumSegments: 4,
|
|
76
|
+
...(corner.segments === undefined ? {} : { segments: corner.segments }),
|
|
77
|
+
...(corner.resolution === undefined ? {} : { resolution: corner.resolution }),
|
|
78
|
+
limits: selected,
|
|
79
|
+
}) / 4);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Creates an immutable CCW rectangle loop. Round radius zero is square; tangent saturated endpoints occur once.
|
|
83
|
+
*
|
|
84
|
+
* @example Rounded rectangle outline
|
|
85
|
+
* ```ts
|
|
86
|
+
* rectangleLoop({ size: [30, 20], treatment: { kind: "round", radius: 3 } });
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function rectangleLoop(options) {
|
|
90
|
+
const { width, height, corners, limits } = resolveRectangle(options, "rectangleLoop");
|
|
91
|
+
const counts = corners.map((corner) => {
|
|
92
|
+
if (corner.kind === "square")
|
|
93
|
+
return 1;
|
|
94
|
+
if (corner.kind === "round") {
|
|
95
|
+
const intervals = rectangleCornerSegments(corner, limits);
|
|
96
|
+
return corner.radius > 0 ? intervals + 1 : 1;
|
|
97
|
+
}
|
|
98
|
+
return edgeTreatmentPointCount(corner, "rectangleLoop", { limits });
|
|
99
|
+
});
|
|
100
|
+
requireProfileVertexCount(counts.reduce((sum, count) => sum + count, 0), 3, "rectangleLoop", limits);
|
|
101
|
+
const x = width / 2, y = height / 2;
|
|
102
|
+
const vertices = [
|
|
103
|
+
[x, y],
|
|
104
|
+
[-x, y],
|
|
105
|
+
[-x, -y],
|
|
106
|
+
[x, -y],
|
|
107
|
+
];
|
|
108
|
+
const incoming = [
|
|
109
|
+
[0, -1],
|
|
110
|
+
[1, 0],
|
|
111
|
+
[0, 1],
|
|
112
|
+
[-1, 0],
|
|
113
|
+
];
|
|
114
|
+
const outgoing = [
|
|
115
|
+
[-1, 0],
|
|
116
|
+
[0, -1],
|
|
117
|
+
[1, 0],
|
|
118
|
+
[0, 1],
|
|
119
|
+
];
|
|
120
|
+
const points = [];
|
|
121
|
+
corners.forEach((corner, index) => {
|
|
122
|
+
if (corner.kind === "round" && corner.radius > 0) {
|
|
123
|
+
const start = (index * Math.PI) / 2, size = corner.radius, [vx, vy] = vertices[index];
|
|
124
|
+
const cx = vx - Math.cos(start + Math.PI / 4) * Math.SQRT2 * size;
|
|
125
|
+
const cy = vy - Math.sin(start + Math.PI / 4) * Math.SQRT2 * size;
|
|
126
|
+
for (let step = 0; step < counts[index]; step++) {
|
|
127
|
+
const angle = start + (step * Math.PI) / (2 * (counts[index] - 1));
|
|
128
|
+
points.push([cx + Math.cos(angle) * size, cy + Math.sin(angle) * size]);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const samples = corner.kind === "square"
|
|
133
|
+
? [[0, 0]]
|
|
134
|
+
: sampleEdgeTreatment(corner, "rectangleLoop", { limits });
|
|
135
|
+
for (const [a, b] of samples)
|
|
136
|
+
points.push([
|
|
137
|
+
vertices[index][0] + incoming[index][0] * a + outgoing[index][0] * b,
|
|
138
|
+
vertices[index][1] + incoming[index][1] * a + outgoing[index][1] * b,
|
|
139
|
+
]);
|
|
140
|
+
});
|
|
141
|
+
const epsilon = Math.max(width, height) * Number.EPSILON * 8;
|
|
142
|
+
const same = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]) <= epsilon;
|
|
143
|
+
const clean = points.filter((point, index) => index === 0 || !same(point, points[index - 1]));
|
|
144
|
+
if (clean.length > 1 && same(clean[0], clean.at(-1)))
|
|
145
|
+
clean.pop();
|
|
146
|
+
return Object.freeze(clean.map(([px, py]) => Object.freeze(options.center === false ? [px + x, py + y] : [px, py])));
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Creates an identified rectangle whose edge/corner spans survive zero and saturated round radii. Outer winding is CCW.
|
|
150
|
+
*
|
|
151
|
+
* @example Retain rectangle landmarks
|
|
152
|
+
* ```ts
|
|
153
|
+
* rectangleProfile({ size: [30, 20], treatment: { kind: "round", radius: 3 } });
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export function rectangleProfile(options) {
|
|
157
|
+
const { width, height, corners, setbacks, limits } = resolveRectangle(options, "rectangleProfile");
|
|
158
|
+
const id = options.id ?? "body";
|
|
159
|
+
const spanCount = corners.reduce((sum, corner) => sum +
|
|
160
|
+
(corner.kind === "profile"
|
|
161
|
+
? edgeTreatmentPointCount(corner, "rectangleProfile", { limits }) - 1
|
|
162
|
+
: 1), 4);
|
|
163
|
+
// A square's four collapsed corner landmarks add four metadata slots without adding physical points.
|
|
164
|
+
assertSamplingCount(spanCount, "rectangleProfile", limits.maxProfilePoints + 4);
|
|
165
|
+
if (typeof id !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id) || id.length > 64)
|
|
166
|
+
fail("rectangleProfile", "Profile identities must be canonical identifiers of at most 64 characters.");
|
|
167
|
+
const x = width / 2, y = height / 2;
|
|
168
|
+
const vertices = [
|
|
169
|
+
[x, y, 0],
|
|
170
|
+
[-x, y, 90],
|
|
171
|
+
[-x, -y, 180],
|
|
172
|
+
[x, -y, 270],
|
|
173
|
+
];
|
|
174
|
+
const names = ["top-right", "top-left", "bottom-left", "bottom-right"];
|
|
175
|
+
const edges = ["top", "left", "bottom", "right"];
|
|
176
|
+
const point = (px, py) => options.center === false ? [px + x, py + y] : [px, py];
|
|
177
|
+
const spans = [];
|
|
178
|
+
const incoming = [
|
|
179
|
+
[0, -1],
|
|
180
|
+
[1, 0],
|
|
181
|
+
[0, 1],
|
|
182
|
+
[-1, 0],
|
|
183
|
+
];
|
|
184
|
+
const outgoing = [
|
|
185
|
+
[-1, 0],
|
|
186
|
+
[0, -1],
|
|
187
|
+
[1, 0],
|
|
188
|
+
[0, 1],
|
|
189
|
+
];
|
|
190
|
+
const map = (index, a, b) => point(vertices[index][0] + incoming[index][0] * a + outgoing[index][0] * b, vertices[index][1] + incoming[index][1] * a + outgoing[index][1] * b);
|
|
191
|
+
const endpoints = setbacks.map(([a, b], index) => ({
|
|
192
|
+
start: map(index, a, 0),
|
|
193
|
+
end: map(index, 0, b),
|
|
194
|
+
center: map(index, a, b),
|
|
195
|
+
}));
|
|
196
|
+
corners.forEach((corner, index) => {
|
|
197
|
+
const shape = endpoints[index], startDegrees = vertices[index][2];
|
|
198
|
+
if (corner.kind === "chamfer")
|
|
199
|
+
spans.push({ id: names[index], kind: "line", start: shape.start, end: shape.end });
|
|
200
|
+
else if (corner.kind === "smooth") {
|
|
201
|
+
sampleEdgeTreatment(corner, "rectangleProfile", { limits });
|
|
202
|
+
spans.push({
|
|
203
|
+
id: names[index],
|
|
204
|
+
kind: "cubic",
|
|
205
|
+
start: shape.start,
|
|
206
|
+
control1: map(index, 0, 0),
|
|
207
|
+
control2: map(index, 0, 0),
|
|
208
|
+
end: shape.end,
|
|
209
|
+
...(corner.segments === undefined ? {} : { segments: corner.segments }),
|
|
210
|
+
...(corner.resolution === undefined ? {} : { resolution: corner.resolution }),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
else if (corner.kind === "profile") {
|
|
214
|
+
const samples = sampleEdgeTreatment(corner, "rectangleProfile", { limits });
|
|
215
|
+
for (let slot = 0; slot < samples.length - 1; slot++)
|
|
216
|
+
spans.push({
|
|
217
|
+
id: `${names[index]}-${slot}`,
|
|
218
|
+
kind: "line",
|
|
219
|
+
start: map(index, ...samples[slot]),
|
|
220
|
+
end: map(index, ...samples[slot + 1]),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const exact = corner.kind === "round" &&
|
|
225
|
+
(corner.segments !== undefined || corner.resolution?.circularSegments !== undefined);
|
|
226
|
+
if (corner.kind === "round")
|
|
227
|
+
rectangleCornerSegments(corner, limits);
|
|
228
|
+
spans.push({
|
|
229
|
+
id: names[index],
|
|
230
|
+
kind: "arc",
|
|
231
|
+
center: shape.center,
|
|
232
|
+
radius: setbacks[index][0],
|
|
233
|
+
startDegrees,
|
|
234
|
+
sweepDegrees: 90,
|
|
235
|
+
...(exact ? { segments: rectangleCornerSegments(corner, limits) } : {}),
|
|
236
|
+
...(corner.kind === "round" && corner.resolution !== undefined
|
|
237
|
+
? { resolution: { ...corner.resolution } }
|
|
238
|
+
: {}),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
spans.push({
|
|
242
|
+
id: edges[index],
|
|
243
|
+
kind: "line",
|
|
244
|
+
start: shape.end,
|
|
245
|
+
end: endpoints[(index + 1) % 4].start,
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
return { components: [{ id, holes: [], outer: { id: "outline", spans } }] };
|
|
249
|
+
}
|