@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,266 @@
|
|
|
1
|
+
import { resolveArcSegments, resolveGeometryResolution, } from "../resolution.js";
|
|
2
|
+
import { assertSamplingCount, resolveGeometrySamplingLimits } from "../sampling-limits.js";
|
|
3
|
+
import { CubicEvaluator, curveDirection, curveDistance, curveFailure, distanceToCurveChord, interpolateCurvePoint, liftPointTo3D, normalizeCurveDirection, planarCubicControl, splitCubicControl, validateCurvePoint3, } from "./evaluation.js";
|
|
4
|
+
/** @internal Final planar results never create a discarded immutable 3D snapshot. */
|
|
5
|
+
export function createPlanarSampleWriter() {
|
|
6
|
+
const points = [];
|
|
7
|
+
const tangents = [];
|
|
8
|
+
return {
|
|
9
|
+
append(point, tangent) {
|
|
10
|
+
points.push(Object.freeze([point[0], point[1]]));
|
|
11
|
+
tangents.push(Object.freeze([tangent[0], tangent[1]]));
|
|
12
|
+
},
|
|
13
|
+
finish: () => Object.freeze({ points: Object.freeze(points), tangents: Object.freeze(tangents) }),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** @internal Scratch buffers and caller-owned endpoints cannot escape through the returned vectors. */
|
|
17
|
+
export function createSpatialSampleWriter() {
|
|
18
|
+
const points = [];
|
|
19
|
+
const tangents = [];
|
|
20
|
+
return {
|
|
21
|
+
append(point, tangent) {
|
|
22
|
+
points.push(Object.freeze([...point]));
|
|
23
|
+
tangents.push(Object.freeze([...tangent]));
|
|
24
|
+
},
|
|
25
|
+
finish: () => Object.freeze({ points: Object.freeze(points), tangents: Object.freeze(tangents) }),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** @internal Exact counts override physical resolution, but never the finite output budget. */
|
|
29
|
+
export function resolveSamplingBudget(options) {
|
|
30
|
+
const selected = resolveGeometrySamplingLimits(options.limits).maxPathPoints;
|
|
31
|
+
const maximum = options.maximumVertices ?? selected;
|
|
32
|
+
if (options.maximumVertices !== undefined &&
|
|
33
|
+
(!Number.isSafeInteger(maximum) || maximum < 2 || maximum > selected))
|
|
34
|
+
curveFailure("INVALID_ARGUMENT", "maximumVertices must be at least two and cannot raise maxPathPoints.");
|
|
35
|
+
if (options.segments !== undefined &&
|
|
36
|
+
(!Number.isSafeInteger(options.segments) || options.segments < 1))
|
|
37
|
+
curveFailure("INVALID_ARGUMENT", "segments must be a positive safe integer.");
|
|
38
|
+
return maximum;
|
|
39
|
+
}
|
|
40
|
+
/** @internal Count actual output vertices explicitly, including or omitting the seam as appropriate. */
|
|
41
|
+
export function assertSampleVertexCount(vertexCount, maximum) {
|
|
42
|
+
assertSamplingCount(vertexCount, "sampleCurve", maximum, "Curve sampling exceeds its selected vertex allocation ceiling.");
|
|
43
|
+
}
|
|
44
|
+
/** @internal A shared physical interval count for straight curves and identified spans. */
|
|
45
|
+
export function resolveLineSegmentCount(length, maximumSegmentLength) {
|
|
46
|
+
return maximumSegmentLength === undefined
|
|
47
|
+
? 1
|
|
48
|
+
: Math.max(1, Math.ceil(length / maximumSegmentLength));
|
|
49
|
+
}
|
|
50
|
+
function sampleLine(curve, options, maximum, createWriter) {
|
|
51
|
+
const start = validateCurvePoint3(curve.start);
|
|
52
|
+
const end = validateCurvePoint3(curve.end);
|
|
53
|
+
const length = curveDistance(start, end);
|
|
54
|
+
const tangent = [0, 0, 0];
|
|
55
|
+
curveDirection(start, end, tangent);
|
|
56
|
+
const policy = options.segments === undefined
|
|
57
|
+
? resolveGeometryResolution(options.resolution, options.limits)
|
|
58
|
+
: undefined;
|
|
59
|
+
const segments = options.segments ?? resolveLineSegmentCount(length, policy?.maxSegmentLength);
|
|
60
|
+
assertSampleVertexCount(segments + 1, maximum);
|
|
61
|
+
const writer = createWriter();
|
|
62
|
+
const point = [0, 0, 0];
|
|
63
|
+
for (let i = 0; i <= segments; i++) {
|
|
64
|
+
if (i === 0 || i === segments)
|
|
65
|
+
writer.append(i === 0 ? start : end, tangent);
|
|
66
|
+
else {
|
|
67
|
+
interpolateCurvePoint(start, end, i / segments, point);
|
|
68
|
+
writer.append(point, tangent);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return writer.finish();
|
|
72
|
+
}
|
|
73
|
+
function validateArc(curve) {
|
|
74
|
+
validateCurvePoint3(curve.center);
|
|
75
|
+
validateCurvePoint3(curve.xAxis);
|
|
76
|
+
validateCurvePoint3(curve.yAxis);
|
|
77
|
+
if (!Number.isFinite(curve.radius) ||
|
|
78
|
+
curve.radius <= 0 ||
|
|
79
|
+
!Number.isFinite(curve.sweepDegrees) ||
|
|
80
|
+
curve.sweepDegrees === 0 ||
|
|
81
|
+
Math.abs(curve.sweepDegrees) > 360 ||
|
|
82
|
+
!Number.isFinite(curve.startDegrees ?? 0))
|
|
83
|
+
curveFailure("INVALID_ARGUMENT", "Arc radius must be positive and its nonzero degree sweep at most 360.");
|
|
84
|
+
if (Math.abs(Math.hypot(...curve.xAxis) - 1) > 1e-10 ||
|
|
85
|
+
Math.abs(Math.hypot(...curve.yAxis) - 1) > 1e-10 ||
|
|
86
|
+
Math.abs(curve.xAxis[0] * curve.yAxis[0] +
|
|
87
|
+
curve.xAxis[1] * curve.yAxis[1] +
|
|
88
|
+
curve.xAxis[2] * curve.yAxis[2]) > 1e-10)
|
|
89
|
+
curveFailure("INVALID_ARGUMENT", "Spatial arc axes must be orthogonal unit directions.");
|
|
90
|
+
}
|
|
91
|
+
function sampleArc(curve, options, maximum, createWriter) {
|
|
92
|
+
validateArc(curve);
|
|
93
|
+
const segments = options.segments ??
|
|
94
|
+
resolveArcSegments(curve.radius, curve.sweepDegrees, options.resolution, options.limits);
|
|
95
|
+
assertSampleVertexCount(segments + 1, maximum);
|
|
96
|
+
const writer = createWriter();
|
|
97
|
+
const point = [0, 0, 0];
|
|
98
|
+
const tangent = [0, 0, 0];
|
|
99
|
+
const firstPoint = [0, 0, 0];
|
|
100
|
+
const sign = Math.sign(curve.sweepDegrees);
|
|
101
|
+
for (let i = 0; i <= segments; i++) {
|
|
102
|
+
const angle = (((curve.startDegrees ?? 0) + (curve.sweepDegrees * i) / segments) * Math.PI) / 180;
|
|
103
|
+
const cos = Math.cos(angle), sin = Math.sin(angle);
|
|
104
|
+
if (i === segments && Math.abs(curve.sweepDegrees) === 360) {
|
|
105
|
+
point[0] = firstPoint[0];
|
|
106
|
+
point[1] = firstPoint[1];
|
|
107
|
+
point[2] = firstPoint[2];
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
point[0] = curve.center[0] + curve.radius * (curve.xAxis[0] * cos + curve.yAxis[0] * sin);
|
|
111
|
+
point[1] = curve.center[1] + curve.radius * (curve.xAxis[1] * cos + curve.yAxis[1] * sin);
|
|
112
|
+
point[2] = curve.center[2] + curve.radius * (curve.xAxis[2] * cos + curve.yAxis[2] * sin);
|
|
113
|
+
validateCurvePoint3(point);
|
|
114
|
+
if (i === 0) {
|
|
115
|
+
firstPoint[0] = point[0];
|
|
116
|
+
firstPoint[1] = point[1];
|
|
117
|
+
firstPoint[2] = point[2];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
tangent[0] = sign * (-curve.xAxis[0] * sin + curve.yAxis[0] * cos);
|
|
121
|
+
tangent[1] = sign * (-curve.xAxis[1] * sin + curve.yAxis[1] * cos);
|
|
122
|
+
tangent[2] = sign * (-curve.xAxis[2] * sin + curve.yAxis[2] * cos);
|
|
123
|
+
normalizeCurveDirection(tangent, tangent);
|
|
124
|
+
writer.append(point, tangent);
|
|
125
|
+
}
|
|
126
|
+
return writer.finish();
|
|
127
|
+
}
|
|
128
|
+
/** @internal The control hull bounds curve deviation and each sampled edge's physical length. */
|
|
129
|
+
export function cubicControlMeetsPolicy(control, policy) {
|
|
130
|
+
const length = curveDistance(control[0], control[1]) +
|
|
131
|
+
curveDistance(control[1], control[2]) +
|
|
132
|
+
curveDistance(control[2], control[3]);
|
|
133
|
+
const flat = Math.max(distanceToCurveChord(control[1], control[0], control[3]), distanceToCurveChord(control[2], control[0], control[3]));
|
|
134
|
+
return (flat <= policy.maxChordError &&
|
|
135
|
+
(policy.maxSegmentLength === undefined || length <= policy.maxSegmentLength));
|
|
136
|
+
}
|
|
137
|
+
/** @internal Profiles use a checked uniform parameter grid instead of an adaptive leaf count.
|
|
138
|
+
* Every control interval must meet the physical policy, including flatter regions subdivided to
|
|
139
|
+
* match curved ones. The depth-first traversal retains only one bounded stack, without output points.
|
|
140
|
+
*/
|
|
141
|
+
export function resolveUniformCubicSegments(control, policy, maximum, minimumSegments = 1) {
|
|
142
|
+
let segments = minimumSegments;
|
|
143
|
+
for (let depth = 0; depth <= 32; depth += 1) {
|
|
144
|
+
assertSampleVertexCount(segments, maximum);
|
|
145
|
+
const pending = [{ control, intervals: segments }];
|
|
146
|
+
let accepted = true;
|
|
147
|
+
while (pending.length > 0) {
|
|
148
|
+
const part = pending.pop();
|
|
149
|
+
if (part.intervals === 1) {
|
|
150
|
+
if (!cubicControlMeetsPolicy(part.control, policy)) {
|
|
151
|
+
accepted = false;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const leftIntervals = Math.floor(part.intervals / 2);
|
|
157
|
+
const [left, right] = splitCubicControl(part.control, leftIntervals / part.intervals);
|
|
158
|
+
pending.push({ control: right, intervals: part.intervals - leftIntervals }, { control: left, intervals: leftIntervals });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (accepted)
|
|
162
|
+
return segments;
|
|
163
|
+
segments *= 2;
|
|
164
|
+
}
|
|
165
|
+
return curveFailure("COMPLEXITY_LIMIT", "Bézier correspondence cannot meet its physical tolerance.");
|
|
166
|
+
}
|
|
167
|
+
function resolveCubicParameters(control, options, maximum) {
|
|
168
|
+
const parameters = [0];
|
|
169
|
+
if (options.segments !== undefined) {
|
|
170
|
+
assertSampleVertexCount(options.segments + 1, maximum);
|
|
171
|
+
for (let i = 1; i <= options.segments; i++)
|
|
172
|
+
parameters.push(i / options.segments);
|
|
173
|
+
return parameters;
|
|
174
|
+
}
|
|
175
|
+
const policy = resolveGeometryResolution(options.resolution, options.limits);
|
|
176
|
+
const pending = [{ control, start: 0, end: 1, depth: 0 }];
|
|
177
|
+
while (pending.length > 0) {
|
|
178
|
+
const part = pending.pop();
|
|
179
|
+
if (cubicControlMeetsPolicy(part.control, policy)) {
|
|
180
|
+
assertSampleVertexCount(parameters.length + 1, maximum);
|
|
181
|
+
parameters.push(part.end);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// Depth bounds pathological subdivision and parameter midpoints that lose precision.
|
|
185
|
+
if (part.depth >= 32 || parameters.length + pending.length + 2 > maximum)
|
|
186
|
+
curveFailure("COMPLEXITY_LIMIT", "Bézier sampling cannot meet the requested physical tolerance within its budget.");
|
|
187
|
+
const [left, right] = splitCubicControl(part.control);
|
|
188
|
+
const middle = (part.start + part.end) / 2;
|
|
189
|
+
// The LIFO stack visits the left interval first, preserving increasing output parameters.
|
|
190
|
+
pending.push({ control: right, start: middle, end: part.end, depth: part.depth + 1 }, { control: left, start: part.start, end: middle, depth: part.depth + 1 });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return parameters;
|
|
194
|
+
}
|
|
195
|
+
function sampleCubicBezier(curve, options, maximum, createWriter) {
|
|
196
|
+
const control = [
|
|
197
|
+
validateCurvePoint3(curve.start),
|
|
198
|
+
validateCurvePoint3(curve.control1),
|
|
199
|
+
validateCurvePoint3(curve.control2),
|
|
200
|
+
validateCurvePoint3(curve.end),
|
|
201
|
+
];
|
|
202
|
+
const parameters = resolveCubicParameters(control, options, maximum);
|
|
203
|
+
assertSampleVertexCount(parameters.length, maximum);
|
|
204
|
+
const evaluator = new CubicEvaluator(control);
|
|
205
|
+
const writer = createWriter();
|
|
206
|
+
const point = [0, 0, 0], tangent = [0, 0, 0];
|
|
207
|
+
for (const t of parameters) {
|
|
208
|
+
evaluator.point(t, point);
|
|
209
|
+
evaluator.tangent(t, point, tangent);
|
|
210
|
+
writer.append(point, tangent);
|
|
211
|
+
}
|
|
212
|
+
return writer.finish();
|
|
213
|
+
}
|
|
214
|
+
function sampleCurve(curve, options, createWriter) {
|
|
215
|
+
const maximum = resolveSamplingBudget(options);
|
|
216
|
+
switch (curve.kind) {
|
|
217
|
+
case "line":
|
|
218
|
+
return sampleLine(curve, options, maximum, createWriter);
|
|
219
|
+
case "arc":
|
|
220
|
+
return sampleArc(curve, options, maximum, createWriter);
|
|
221
|
+
case "cubic":
|
|
222
|
+
return sampleCubicBezier(curve, options, maximum, createWriter);
|
|
223
|
+
default:
|
|
224
|
+
return curveFailure("INVALID_ARGUMENT", "Unsupported curve kind.");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Samples a spatial curve, preserving endpoints and directed unit tangents.
|
|
229
|
+
* @remarks Exact counts use uniform curve parameters, not equal physical distances. Adaptive cubics
|
|
230
|
+
* bound the full control hull. Full-circle arcs repeat their endpoint; closed polylines do not.
|
|
231
|
+
* @example
|
|
232
|
+
* ```ts
|
|
233
|
+
* const path = sampleCurve3({kind: "cubic", start: [0,0,0], control1: [5,0,0], control2: [5,5,5], end: [10,5,5]});
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
export function sampleCurve3(curve, options = {}) {
|
|
237
|
+
return sampleCurve(curve, options, createSpatialSampleWriter);
|
|
238
|
+
}
|
|
239
|
+
/** Samples a planar curve with the same guarantees and policy as spatial sampling.
|
|
240
|
+
* @remarks Private spatial scratch math is shared, while only final planar vectors are copied and
|
|
241
|
+
* frozen. At stationary tangents, outgoing secants are preferred and endpoints use a backward fallback.
|
|
242
|
+
* @example
|
|
243
|
+
* ```ts
|
|
244
|
+
* const turn = sampleCurve2({kind: "arc", center: [0,0], radius: 10, sweepDegrees: 90});
|
|
245
|
+
* ```
|
|
246
|
+
*/
|
|
247
|
+
export function sampleCurve2(curve, options = {}) {
|
|
248
|
+
switch (curve.kind) {
|
|
249
|
+
case "line":
|
|
250
|
+
return sampleCurve({ ...curve, start: liftPointTo3D(curve.start), end: liftPointTo3D(curve.end) }, options, createPlanarSampleWriter);
|
|
251
|
+
case "arc":
|
|
252
|
+
return sampleCurve({ ...curve, center: liftPointTo3D(curve.center), xAxis: [1, 0, 0], yAxis: [0, 1, 0] }, options, createPlanarSampleWriter);
|
|
253
|
+
case "cubic": {
|
|
254
|
+
const [start, control1, control2, end] = planarCubicControl(curve);
|
|
255
|
+
return sampleCurve({
|
|
256
|
+
...curve,
|
|
257
|
+
start,
|
|
258
|
+
control1,
|
|
259
|
+
control2,
|
|
260
|
+
end,
|
|
261
|
+
}, options, createPlanarSampleWriter);
|
|
262
|
+
}
|
|
263
|
+
default:
|
|
264
|
+
return curveFailure("INVALID_ARGUMENT", "Unsupported curve kind.");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { edgeTreatmentPointCount, edgeTreatmentSetbacks, sampleEdgeTreatment, } from "./edge-finishing.js";
|
|
2
|
+
import { GeometryError } from "./errors.js";
|
|
3
|
+
import { resolveCircularPrimitiveSegments } from "./primitive-resolution.js";
|
|
4
|
+
import { assertSamplingCount, resolveGeometrySamplingLimits } from "./sampling-limits.js";
|
|
5
|
+
const invalid = (message) => {
|
|
6
|
+
throw new GeometryError("INVALID_ARGUMENT", message, { operation: "cylinder" });
|
|
7
|
+
};
|
|
8
|
+
/** @internal Resolves exactly one cylinder radius or diameter dimension before faceting correction. */
|
|
9
|
+
export const resolveCylinderRadii = (options) => {
|
|
10
|
+
if ((options.radius === undefined) === (options.diameter === undefined)) {
|
|
11
|
+
return invalid("Specify exactly one radius or diameter.");
|
|
12
|
+
}
|
|
13
|
+
const dimension = options.radius ?? options.diameter;
|
|
14
|
+
const factor = options.radius === undefined ? 0.5 : 1;
|
|
15
|
+
if (typeof dimension === "number")
|
|
16
|
+
return [dimension * factor, dimension * factor];
|
|
17
|
+
if (!Array.isArray(dimension) || dimension.length !== 2) {
|
|
18
|
+
return invalid("radius or diameter must be a number or an exact [bottom, top] tuple.");
|
|
19
|
+
}
|
|
20
|
+
return [dimension[0] * factor, dimension[1] * factor];
|
|
21
|
+
};
|
|
22
|
+
const pushDistinct = (profile, point) => {
|
|
23
|
+
const previous = profile.at(-1);
|
|
24
|
+
if (previous?.[0] !== point[0] || previous[1] !== point[1])
|
|
25
|
+
profile.push(point);
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Validates and resolves the canonical cylinder contract without allocating kernel objects.
|
|
29
|
+
*
|
|
30
|
+
* Exact `segments` are resolved against package limits only and deliberately ignore both an
|
|
31
|
+
* operation-level `resolution` object and the ambient resolution scope. Treatments use cap inset
|
|
32
|
+
* and sloped-side setback coordinates; circular roundovers account for the actual corner angle.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
export function resolveCylinderPrimitive(options) {
|
|
37
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
38
|
+
invalid("options must be an object.");
|
|
39
|
+
}
|
|
40
|
+
if (!Number.isFinite(options.height) || options.height <= 0) {
|
|
41
|
+
invalid("height must be finite and positive.");
|
|
42
|
+
}
|
|
43
|
+
if (options.center !== undefined && typeof options.center !== "boolean") {
|
|
44
|
+
invalid("center must be boolean when supplied.");
|
|
45
|
+
}
|
|
46
|
+
const [bottomRadius, topRadius] = resolveCylinderRadii(options);
|
|
47
|
+
if (!Number.isFinite(bottomRadius) ||
|
|
48
|
+
!Number.isFinite(topRadius) ||
|
|
49
|
+
bottomRadius < 0 ||
|
|
50
|
+
topRadius < 0 ||
|
|
51
|
+
(bottomRadius === 0 && topRadius === 0)) {
|
|
52
|
+
invalid("radius values must be finite and non-negative, with at least one positive value.");
|
|
53
|
+
}
|
|
54
|
+
if (options.ends !== undefined &&
|
|
55
|
+
(typeof options.ends !== "object" || options.ends === null || Array.isArray(options.ends)))
|
|
56
|
+
invalid("ends must be an object when supplied.");
|
|
57
|
+
const radialChange = topRadius - bottomRadius;
|
|
58
|
+
const sideLength = Math.hypot(radialChange, options.height);
|
|
59
|
+
if (!Number.isFinite(radialChange) || !Number.isFinite(sideLength))
|
|
60
|
+
invalid("Cylinder side dimensions must remain finite.");
|
|
61
|
+
const sideX = radialChange / sideLength, sideZ = options.height / sideLength;
|
|
62
|
+
const bottomAngle = Math.acos(-sideX), topAngle = Math.acos(sideX);
|
|
63
|
+
const bottom = options.ends?.bottom, top = options.ends?.top;
|
|
64
|
+
const bottomSetbacks = bottom === undefined
|
|
65
|
+
? [0, 0]
|
|
66
|
+
: edgeTreatmentSetbacks(bottom, "cylinder", bottomAngle, options.limits);
|
|
67
|
+
const topSetbacks = top === undefined ? [0, 0] : edgeTreatmentSetbacks(top, "cylinder", topAngle, options.limits);
|
|
68
|
+
if ((bottom && bottomRadius === 0) || (top && topRadius === 0))
|
|
69
|
+
invalid("A zero-radius cone apex cannot receive an end treatment.");
|
|
70
|
+
if ((bottom && bottomSetbacks[0] >= bottomRadius) || (top && topSetbacks[0] >= topRadius))
|
|
71
|
+
invalid("Each cap setback must be smaller than its corresponding end radius.");
|
|
72
|
+
if (bottomSetbacks[1] + topSetbacks[1] >= sideLength)
|
|
73
|
+
invalid("End treatment setbacks must leave positive length on the sloped side.");
|
|
74
|
+
const maximumRadius = Math.max(bottomRadius, topRadius);
|
|
75
|
+
const segments = resolveCircularPrimitiveSegments(maximumRadius, options, "cylinder");
|
|
76
|
+
const center = options.center ?? true;
|
|
77
|
+
if (bottom === undefined && top === undefined)
|
|
78
|
+
return { bottomRadius, center, height: options.height, segments, topRadius };
|
|
79
|
+
const sampling = {
|
|
80
|
+
...(options.resolution === undefined ? {} : { resolution: options.resolution }),
|
|
81
|
+
...(options.limits === undefined ? {} : { limits: options.limits }),
|
|
82
|
+
};
|
|
83
|
+
const profileCount = 2 +
|
|
84
|
+
(bottom === undefined
|
|
85
|
+
? 1
|
|
86
|
+
: edgeTreatmentPointCount(bottom, "cylinder", { ...sampling, angle: bottomAngle })) +
|
|
87
|
+
(top === undefined
|
|
88
|
+
? 1
|
|
89
|
+
: edgeTreatmentPointCount(top, "cylinder", { ...sampling, angle: topAngle }));
|
|
90
|
+
assertSamplingCount(profileCount, "cylinder", resolveGeometrySamplingLimits(options.limits).maxProfilePoints);
|
|
91
|
+
const bottomPoints = bottom === undefined
|
|
92
|
+
? [[0, 0]]
|
|
93
|
+
: sampleEdgeTreatment(bottom, "cylinder", { ...sampling, angle: bottomAngle });
|
|
94
|
+
const topPoints = top === undefined
|
|
95
|
+
? [[0, 0]]
|
|
96
|
+
: sampleEdgeTreatment(top, "cylinder", { ...sampling, angle: topAngle });
|
|
97
|
+
assertSamplingCount(bottomPoints.length + topPoints.length + 2, "cylinder", resolveGeometrySamplingLimits(options.limits).maxProfilePoints);
|
|
98
|
+
const bottomZ = center ? -options.height / 2 : 0;
|
|
99
|
+
const topZ = bottomZ + options.height;
|
|
100
|
+
const profile = [];
|
|
101
|
+
pushDistinct(profile, [0, bottomZ]);
|
|
102
|
+
for (const [cap, side] of bottomPoints)
|
|
103
|
+
pushDistinct(profile, [bottomRadius - cap + sideX * side, bottomZ + sideZ * side]);
|
|
104
|
+
for (const [cap, side] of [...topPoints].reverse())
|
|
105
|
+
pushDistinct(profile, [topRadius - cap - sideX * side, topZ - sideZ * side]);
|
|
106
|
+
pushDistinct(profile, [0, topZ]);
|
|
107
|
+
if (profile.some(([radius, z]) => !Number.isFinite(radius) || !Number.isFinite(z) || radius < 0))
|
|
108
|
+
invalid("End treatment profile must remain finite and cannot cross the revolution axis.");
|
|
109
|
+
return {
|
|
110
|
+
bottomRadius,
|
|
111
|
+
center,
|
|
112
|
+
height: options.height,
|
|
113
|
+
profile,
|
|
114
|
+
segments,
|
|
115
|
+
topRadius,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { GeometryError } from "./errors.js";
|
|
2
|
+
import { resolveModelingLimits } from "./model-limits.js";
|
|
3
|
+
import { resolveCircularPrimitiveSegments } from "./primitive-resolution.js";
|
|
4
|
+
import { resolveGeometryResolution } from "./resolution.js";
|
|
5
|
+
import { assertSamplingCount, resolveGeometrySamplingLimits, } from "./sampling-limits.js";
|
|
6
|
+
const fail = (operation, message) => {
|
|
7
|
+
throw new GeometryError("INVALID_ARGUMENT", message, { operation });
|
|
8
|
+
};
|
|
9
|
+
const isArray = (value) => Array.isArray(value);
|
|
10
|
+
/** @internal Resolves the actual tangent setbacks before allocation or fit checks. */
|
|
11
|
+
export function edgeTreatmentSetbacks(treatment, operation, angle = Math.PI / 2, limits) {
|
|
12
|
+
if (typeof treatment !== "object" || treatment === null || Array.isArray(treatment))
|
|
13
|
+
fail(operation, "An edge treatment must be an object.");
|
|
14
|
+
let result;
|
|
15
|
+
if (treatment.kind === "round") {
|
|
16
|
+
if (!Number.isFinite(treatment.radius))
|
|
17
|
+
throw new GeometryError("NON_FINITE_VALUE", "Round radius must be finite.", { operation });
|
|
18
|
+
if (treatment.radius < 0)
|
|
19
|
+
fail(operation, "Round radius must be nonnegative.");
|
|
20
|
+
const setback = angle === Math.PI / 2 ? treatment.radius : treatment.radius / Math.tan(angle / 2);
|
|
21
|
+
result = [setback, setback];
|
|
22
|
+
}
|
|
23
|
+
else if (treatment.kind === "chamfer" || treatment.kind === "smooth") {
|
|
24
|
+
const distance = treatment.distance;
|
|
25
|
+
if (typeof distance !== "number" && (!Array.isArray(distance) || distance.length !== 2))
|
|
26
|
+
fail(operation, "Treatment distance must be a number or an exact [first, second] tuple.");
|
|
27
|
+
result = typeof distance === "number" ? [distance, distance] : distance;
|
|
28
|
+
if (result.some((value) => !Number.isFinite(value)))
|
|
29
|
+
throw new GeometryError("NON_FINITE_VALUE", "Treatment setbacks must be finite.", {
|
|
30
|
+
operation,
|
|
31
|
+
});
|
|
32
|
+
if (result.some((value) => value <= 0))
|
|
33
|
+
fail(operation, "Treatment setbacks must be positive.");
|
|
34
|
+
}
|
|
35
|
+
else if (treatment.kind === "profile") {
|
|
36
|
+
const points = treatment.profile;
|
|
37
|
+
if (!isArray(points) || points.length < 2)
|
|
38
|
+
fail(operation, "A treatment profile needs at least two points.");
|
|
39
|
+
assertSamplingCount(points.length, operation, resolveGeometrySamplingLimits(limits).maxProfilePoints);
|
|
40
|
+
let previous;
|
|
41
|
+
for (const point of points) {
|
|
42
|
+
if (!isArray(point) || point.length !== 2)
|
|
43
|
+
fail(operation, "Treatment profile points must be XY pairs.");
|
|
44
|
+
if (point.some((value) => !Number.isFinite(value)))
|
|
45
|
+
throw new GeometryError("NON_FINITE_VALUE", "Treatment profile points must be finite.", {
|
|
46
|
+
operation,
|
|
47
|
+
});
|
|
48
|
+
if (point.some((value) => value < 0))
|
|
49
|
+
fail(operation, "Treatment profile coordinates must be nonnegative.");
|
|
50
|
+
if (previous &&
|
|
51
|
+
(point[0] > previous[0] ||
|
|
52
|
+
point[1] < previous[1] ||
|
|
53
|
+
(point[0] === previous[0] && point[1] === previous[1])))
|
|
54
|
+
fail(operation, "Treatment profile must decrease its first setback and increase its second without repeated points.");
|
|
55
|
+
previous = point;
|
|
56
|
+
}
|
|
57
|
+
const first = points[0], last = points.at(-1);
|
|
58
|
+
if (!(first[0] > 0) || first[1] !== 0 || last[0] !== 0 || !(last[1] > 0))
|
|
59
|
+
fail(operation, "Treatment profile must run from [positive, 0] to [0, positive].");
|
|
60
|
+
result = [first[0], last[1]];
|
|
61
|
+
}
|
|
62
|
+
else
|
|
63
|
+
return fail(operation, "Unsupported edge treatment kind.");
|
|
64
|
+
if (result.some((value) => !Number.isFinite(value)))
|
|
65
|
+
throw new GeometryError("NON_FINITE_VALUE", "Treatment setbacks must remain finite.", {
|
|
66
|
+
operation,
|
|
67
|
+
});
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
/** @internal Plans one corner before profile or mesh arrays are allocated. */
|
|
71
|
+
export function edgeTreatmentPointCount(treatment, operation, options = {}) {
|
|
72
|
+
const angle = options.angle ?? Math.PI / 2;
|
|
73
|
+
if (!Number.isFinite(angle) || angle <= 0 || angle >= Math.PI)
|
|
74
|
+
fail(operation, "Treatment corner angle must be strictly between zero and 180 degrees.");
|
|
75
|
+
const [first, second] = edgeTreatmentSetbacks(treatment, operation, angle, options.limits);
|
|
76
|
+
const limits = resolveGeometrySamplingLimits(options.limits);
|
|
77
|
+
if (treatment.kind === "profile") {
|
|
78
|
+
assertSamplingCount(treatment.profile.length, operation, limits.maxProfilePoints);
|
|
79
|
+
return treatment.profile.length;
|
|
80
|
+
}
|
|
81
|
+
if (first === 0 && second === 0)
|
|
82
|
+
return 1;
|
|
83
|
+
if (treatment.kind === "chamfer")
|
|
84
|
+
return 2;
|
|
85
|
+
const selectedResolution = { ...options.resolution, ...treatment.resolution };
|
|
86
|
+
const magnification = options.magnification ?? 1;
|
|
87
|
+
if (!Number.isFinite(magnification) || magnification < 1)
|
|
88
|
+
fail(operation, "Treatment magnification must be finite and at least one.");
|
|
89
|
+
selectedResolution.maxChordError = (selectedResolution.maxChordError ?? 0.05) / magnification;
|
|
90
|
+
if (selectedResolution.maxSegmentLength !== undefined)
|
|
91
|
+
selectedResolution.maxSegmentLength /= magnification;
|
|
92
|
+
const selected = {
|
|
93
|
+
...treatment,
|
|
94
|
+
resolution: selectedResolution,
|
|
95
|
+
limits: resolveModelingLimits(treatment.limits, resolveModelingLimits(limits)),
|
|
96
|
+
};
|
|
97
|
+
let intervals;
|
|
98
|
+
if (treatment.kind === "round") {
|
|
99
|
+
const full = resolveCircularPrimitiveSegments(Math.max(treatment.radius, Number.MIN_VALUE), selected, operation);
|
|
100
|
+
intervals = Math.max(1, Math.ceil((full * (Math.PI - angle)) / (2 * Math.PI)));
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const exact = treatment.segments;
|
|
104
|
+
const cubicResolution = { ...selected.resolution };
|
|
105
|
+
delete cubicResolution.circularSegments;
|
|
106
|
+
const policy = exact === undefined ? resolveGeometryResolution(cubicResolution, selected.limits) : undefined;
|
|
107
|
+
const curvature = 6 * Math.hypot(first, second); // cubic second derivative bounded by this norm
|
|
108
|
+
intervals =
|
|
109
|
+
exact === undefined
|
|
110
|
+
? Math.max(1, Math.ceil(Math.sqrt(curvature / (8 * policy.maxChordError))), policy.maxSegmentLength === undefined
|
|
111
|
+
? 1
|
|
112
|
+
: Math.ceil((3 * Math.hypot(first, second)) / policy.maxSegmentLength))
|
|
113
|
+
: exact;
|
|
114
|
+
if (!Number.isSafeInteger(intervals) || intervals < 1)
|
|
115
|
+
fail(operation, "Smooth treatment segments must be a positive safe integer.");
|
|
116
|
+
assertSamplingCount(intervals, operation, Math.min(policy?.maximumSegments ?? selected.limits.maxCircularSegments, selected.limits.maxCircularSegments));
|
|
117
|
+
}
|
|
118
|
+
assertSamplingCount(intervals + 1, operation, selected.limits.maxProfilePoints);
|
|
119
|
+
return intervals + 1;
|
|
120
|
+
}
|
|
121
|
+
/** @internal Samples a convex corner in its two inward surface directions, preserving endpoints. */
|
|
122
|
+
export function sampleEdgeTreatment(treatment, operation, options = {}) {
|
|
123
|
+
const count = edgeTreatmentPointCount(treatment, operation, options);
|
|
124
|
+
const angle = options.angle ?? Math.PI / 2;
|
|
125
|
+
const [first, second] = edgeTreatmentSetbacks(treatment, operation, angle, options.limits);
|
|
126
|
+
if (treatment.kind === "profile")
|
|
127
|
+
return treatment.profile.map(([x, y]) => [x, y]);
|
|
128
|
+
if (count === 1)
|
|
129
|
+
return [[0, 0]];
|
|
130
|
+
if (treatment.kind === "chamfer")
|
|
131
|
+
return [
|
|
132
|
+
[first, 0],
|
|
133
|
+
[0, second],
|
|
134
|
+
];
|
|
135
|
+
const intervals = count - 1;
|
|
136
|
+
const points = [];
|
|
137
|
+
const cosine = Math.cos(angle), sine = Math.sin(angle);
|
|
138
|
+
const cx = first, cy = (first * (1 - cosine)) / sine;
|
|
139
|
+
const start = Math.atan2(-cy, first - cx);
|
|
140
|
+
for (let index = 0; index <= intervals; index++) {
|
|
141
|
+
if (index === 0) {
|
|
142
|
+
points.push([first, 0]);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (index === intervals) {
|
|
146
|
+
points.push([0, second]);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const t = index / intervals;
|
|
150
|
+
if (treatment.kind === "smooth")
|
|
151
|
+
points.push([first * (1 - t) ** 3, second * t ** 3]);
|
|
152
|
+
else {
|
|
153
|
+
const theta = start - t * (Math.PI - angle);
|
|
154
|
+
const x = cx + treatment.radius * Math.cos(theta), y = cy + treatment.radius * Math.sin(theta);
|
|
155
|
+
const b = y / sine;
|
|
156
|
+
points.push([x - b * cosine, b]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return points;
|
|
160
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const geometryErrorKey = Symbol.for("@metamesh/modeling/error/2");
|
|
2
|
+
const geometryErrorCodes = {
|
|
3
|
+
COMPLEXITY_LIMIT: true,
|
|
4
|
+
DEGENERATE_GEOMETRY: true,
|
|
5
|
+
INVALID_ARGUMENT: true,
|
|
6
|
+
INVALID_MESH: true,
|
|
7
|
+
INVALID_TOPOLOGY: true,
|
|
8
|
+
INVALID_TOLERANCE: true,
|
|
9
|
+
NON_FINITE_VALUE: true,
|
|
10
|
+
SELF_INTERSECTION: true,
|
|
11
|
+
SINGULAR_TRANSFORM: true,
|
|
12
|
+
UNSUPPORTED_RUNTIME: true,
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Error raised when a geometry precondition or invariant is not satisfied.
|
|
16
|
+
*
|
|
17
|
+
* Callers may use `code` for coarse recovery decisions. Human-facing details
|
|
18
|
+
* remain in `message`; messages are descriptive but are not a stable API.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const error = new GeometryError("INVALID_ARGUMENT", "Radius must be positive.", {
|
|
23
|
+
* operation: "example",
|
|
24
|
+
* details: { radius: 0 },
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export class GeometryError extends Error {
|
|
29
|
+
/** Machine-readable failure category. */
|
|
30
|
+
code;
|
|
31
|
+
/** Operation and scalar facts associated with the failure. */
|
|
32
|
+
context;
|
|
33
|
+
/**
|
|
34
|
+
* Creates a geometry error with a stable category, operation, and descriptive message.
|
|
35
|
+
*
|
|
36
|
+
* @param code - Machine-readable failure category.
|
|
37
|
+
* @param message - Human-readable diagnostic message.
|
|
38
|
+
* @param context - Operation and optional scalar facts. Defaults to `{ operation: "geometry" }`.
|
|
39
|
+
*/
|
|
40
|
+
constructor(code, message, context = { operation: "geometry" }) {
|
|
41
|
+
super(message);
|
|
42
|
+
Object.defineProperty(this, geometryErrorKey, { value: 2 });
|
|
43
|
+
this.name = "GeometryError";
|
|
44
|
+
this.code = code;
|
|
45
|
+
this.context = Object.freeze({
|
|
46
|
+
...context,
|
|
47
|
+
...(context.details === undefined ? {} : { details: Object.freeze({ ...context.details }) }),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Recognizes structured geometry errors from separately bundled Modeling copies in the same realm.
|
|
53
|
+
* Ordinary errors and unmarked code-shaped objects are rejected. The marker is execution-local and
|
|
54
|
+
* does not survive structured cloning; worker protocols carry their own serialized error envelope.
|
|
55
|
+
*/
|
|
56
|
+
export function isGeometryError(value) {
|
|
57
|
+
if (!(value instanceof Error))
|
|
58
|
+
return false;
|
|
59
|
+
const candidate = value;
|
|
60
|
+
if (candidate[geometryErrorKey] !== 2 ||
|
|
61
|
+
typeof candidate.code !== "string" ||
|
|
62
|
+
!Object.hasOwn(geometryErrorCodes, candidate.code) ||
|
|
63
|
+
typeof candidate.context !== "object" ||
|
|
64
|
+
candidate.context === null ||
|
|
65
|
+
Array.isArray(candidate.context))
|
|
66
|
+
return false;
|
|
67
|
+
const context = candidate.context;
|
|
68
|
+
if (typeof context.operation !== "string")
|
|
69
|
+
return false;
|
|
70
|
+
if (context.details === undefined)
|
|
71
|
+
return true;
|
|
72
|
+
return (typeof context.details === "object" &&
|
|
73
|
+
context.details !== null &&
|
|
74
|
+
!Array.isArray(context.details) &&
|
|
75
|
+
Object.values(context.details).every((detail) => typeof detail === "boolean" || typeof detail === "number" || typeof detail === "string"));
|
|
76
|
+
}
|