vizcore 1.0.0 → 1.1.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.
- checksums.yaml +4 -4
- data/README.md +66 -648
- data/docs/assets/playground-worker.js +373 -0
- data/docs/assets/playground.css +440 -0
- data/docs/assets/playground.js +652 -0
- data/docs/index.html +2 -1
- data/docs/playground.html +81 -0
- data/docs/shape_dsl.md +269 -0
- data/frontend/index.html +26 -0
- data/frontend/src/custom-shape-param-controls.js +106 -0
- data/frontend/src/main.js +268 -0
- data/frontend/src/mapping-target-selector.js +109 -0
- data/frontend/src/renderer/engine.js +10 -1
- data/frontend/src/renderer/layer-manager.js +18 -4
- data/frontend/src/shape-editor-controls.js +157 -0
- data/frontend/src/visuals/geometry.js +425 -27
- data/frontend/src/visuals/shape-renderer.js +475 -0
- data/frontend/src/visuals/svg-arc.js +104 -0
- data/lib/vizcore/cli/dsl_reference.rb +1 -1
- data/lib/vizcore/cli/scene_validator.rb +92 -0
- data/lib/vizcore/dsl/layer_builder.rb +795 -7
- data/lib/vizcore/dsl/mapping_resolver.rb +158 -4
- data/lib/vizcore/layer_catalog.rb +4 -2
- data/lib/vizcore/renderer/scene_frame_source.rb +14 -1
- data/lib/vizcore/renderer/snapshot_renderer.rb +507 -15
- data/lib/vizcore/server/frame_broadcaster.rb +53 -4
- data/lib/vizcore/server/runner.rb +21 -0
- data/lib/vizcore/shape.rb +719 -0
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +1 -0
- data/sig/vizcore.rbs +100 -1
- metadata +12 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { describeSvgArc, svgArcPoint, svgArcSegmentCount } from "./svg-arc.js";
|
|
2
|
+
|
|
1
3
|
const BASE_VERTICES = [
|
|
2
4
|
[-1.0, -1.0, -1.0],
|
|
3
5
|
[1.0, -1.0, -1.0],
|
|
@@ -66,6 +68,11 @@ const MESH_PRESETS = {
|
|
|
66
68
|
octahedron: { vertices: OCTAHEDRON_VERTICES, edges: OCTAHEDRON_EDGES },
|
|
67
69
|
icosahedron: { vertices: ICOSAHEDRON_VERTICES, edges: ICOSAHEDRON_EDGES }
|
|
68
70
|
};
|
|
71
|
+
const SHAPE_HALF_WIDTH = 640;
|
|
72
|
+
const SHAPE_HALF_HEIGHT = 360;
|
|
73
|
+
const PATH_DEFAULT_MAX_SEGMENTS = 4096;
|
|
74
|
+
const PATH_HARD_MAX_SEGMENTS = 65536;
|
|
75
|
+
const PATH_MAX_RECURSION = 12;
|
|
69
76
|
|
|
70
77
|
export const buildWireframeLines = ({ rotationY, rotationX, deform }) => {
|
|
71
78
|
const amount = clamp(Number(deform || 0), 0, 1);
|
|
@@ -185,14 +192,23 @@ export const buildWaveformLines = ({ time = 0, params = {}, audio = {} } = {}) =
|
|
|
185
192
|
|
|
186
193
|
export const buildShapeLines = ({ params = {} } = {}) => {
|
|
187
194
|
const shapes = Array.isArray(params.shapes) ? params.shapes : [];
|
|
195
|
+
const context = shapeCoordinateContext(params);
|
|
188
196
|
const points = [];
|
|
189
197
|
|
|
190
198
|
shapes.forEach((shape) => {
|
|
191
199
|
const kind = String(shape?.kind || shape?.type || "").toLowerCase();
|
|
192
200
|
if (kind === "circle") {
|
|
193
|
-
appendCircleShape(points, shape);
|
|
201
|
+
appendCircleShape(points, shape, context);
|
|
194
202
|
} else if (kind === "line") {
|
|
195
|
-
appendLineShape(points, shape);
|
|
203
|
+
appendLineShape(points, shape, context);
|
|
204
|
+
} else if (kind === "rect") {
|
|
205
|
+
appendRectShape(points, shape, context);
|
|
206
|
+
} else if (kind === "polygon" || kind === "polyline") {
|
|
207
|
+
appendPolygonShape(points, shape, context, kind === "polygon");
|
|
208
|
+
} else if (kind === "path") {
|
|
209
|
+
appendPathShape(points, shape, context);
|
|
210
|
+
} else if (kind === "star") {
|
|
211
|
+
appendStarShape(points, shape, context);
|
|
196
212
|
}
|
|
197
213
|
});
|
|
198
214
|
|
|
@@ -223,56 +239,438 @@ const appendLineSegments = (points, samples) => {
|
|
|
223
239
|
}
|
|
224
240
|
};
|
|
225
241
|
|
|
226
|
-
const appendCircleShape = (points, shape) => {
|
|
242
|
+
const appendCircleShape = (points, shape, context) => {
|
|
227
243
|
const count = clampInt(shape.count || 1, 1, 64);
|
|
228
244
|
const segments = clampInt(shape.segments || 96, 12, 256);
|
|
229
|
-
const radius =
|
|
230
|
-
const
|
|
231
|
-
const y = normalizeShapeCoordinate(shape.y ?? 0, "y");
|
|
245
|
+
const radius = normalizeShapeLength(shape.radius ?? 100, context, "radius");
|
|
246
|
+
const center = normalizeShapePoint(shape.x ?? 0, shape.y ?? 0, context);
|
|
232
247
|
|
|
233
248
|
for (let ring = 0; ring < count; ring += 1) {
|
|
234
249
|
const ringRadius = radius * ((ring + 1) / count);
|
|
235
250
|
for (let index = 0; index < segments; index += 1) {
|
|
236
|
-
|
|
237
|
-
|
|
251
|
+
appendSegment(
|
|
252
|
+
points,
|
|
253
|
+
circlePoint(center, ringRadius, index, segments),
|
|
254
|
+
circlePoint(center, ringRadius, index + 1, segments),
|
|
255
|
+
shape,
|
|
256
|
+
context
|
|
257
|
+
);
|
|
238
258
|
}
|
|
239
259
|
}
|
|
240
260
|
};
|
|
241
261
|
|
|
242
|
-
const
|
|
262
|
+
const circlePoint = (center, radius, index, segments) => {
|
|
243
263
|
const angle = (index / segments) * Math.PI * 2;
|
|
244
|
-
|
|
245
|
-
clamp(x + Math.cos(angle) * radius, -1.2, 1.2),
|
|
246
|
-
clamp(y + Math.sin(angle) * radius, -1.2, 1.2)
|
|
247
|
-
);
|
|
264
|
+
return [center[0] + Math.cos(angle) * radius, center[1] + Math.sin(angle) * radius];
|
|
248
265
|
};
|
|
249
266
|
|
|
250
|
-
const appendLineShape = (points, shape) => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
267
|
+
const appendLineShape = (points, shape, context) => {
|
|
268
|
+
const [x1, y1, x2, y2] = lineDefaults(context);
|
|
269
|
+
appendSegment(
|
|
270
|
+
points,
|
|
271
|
+
normalizeShapePoint(shape.x1 ?? x1, shape.y1 ?? y1, context),
|
|
272
|
+
normalizeShapePoint(shape.x2 ?? x2, shape.y2 ?? y2, context),
|
|
273
|
+
shape,
|
|
274
|
+
context
|
|
256
275
|
);
|
|
257
276
|
};
|
|
258
277
|
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
|
|
278
|
+
const appendRectShape = (points, shape, context) => {
|
|
279
|
+
const center = normalizeShapePoint(shape.x ?? 0, shape.y ?? 0, context);
|
|
280
|
+
const halfWidth = normalizeShapeLength(shape.width ?? 100, context, "x") / 2;
|
|
281
|
+
const halfHeight = normalizeShapeLength(shape.height ?? 100, context, "y") / 2;
|
|
282
|
+
const vertices = [
|
|
283
|
+
[center[0] - halfWidth, center[1] - halfHeight],
|
|
284
|
+
[center[0] + halfWidth, center[1] - halfHeight],
|
|
285
|
+
[center[0] + halfWidth, center[1] + halfHeight],
|
|
286
|
+
[center[0] - halfWidth, center[1] + halfHeight]
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
appendPolylineSegments(points, vertices, shape, context, true);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const appendPolygonShape = (points, shape, context, defaultClosed) => {
|
|
293
|
+
const vertices = normalizeShapePoints(shape.points, context);
|
|
294
|
+
if (vertices.length < (defaultClosed ? 3 : 2)) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
appendPolylineSegments(points, vertices, shape, context, shape.closed ?? defaultClosed);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const appendStarShape = (points, shape, context) => {
|
|
302
|
+
const tips = clampInt(shape.points || 5, 3, 128);
|
|
303
|
+
const center = normalizeShapePoint(shape.x ?? 0, shape.y ?? 0, context);
|
|
304
|
+
const radius = normalizeShapeLength(shape.radius ?? 100, context, "radius");
|
|
305
|
+
const innerRadius = normalizeShapeLength(shape.inner_radius ?? radiusToRawHalf(shape.radius ?? 100), context, "radius");
|
|
306
|
+
const rotation = (finiteNumber(shape.rotation, -90) / 180) * Math.PI;
|
|
307
|
+
const vertices = [];
|
|
308
|
+
|
|
309
|
+
for (let index = 0; index < tips * 2; index += 1) {
|
|
310
|
+
const angle = rotation + (index / (tips * 2)) * Math.PI * 2;
|
|
311
|
+
const pointRadius = index % 2 === 0 ? radius : innerRadius;
|
|
312
|
+
vertices.push([center[0] + Math.cos(angle) * pointRadius, center[1] + Math.sin(angle) * pointRadius]);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
appendPolylineSegments(points, vertices, shape, context, true);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const appendPathShape = (points, shape, context) => {
|
|
319
|
+
const commands = Array.isArray(shape.commands) ? shape.commands : [];
|
|
320
|
+
const detail = clampInt(shape.detail || 32, 4, 128);
|
|
321
|
+
const tolerance = pathTolerance(shape);
|
|
322
|
+
const budget = pathSegmentBudget(shape);
|
|
323
|
+
let current = null;
|
|
324
|
+
let subpathStart = null;
|
|
325
|
+
|
|
326
|
+
commands.forEach((entry) => {
|
|
327
|
+
const command = Array.isArray(entry) ? String(entry[0] || "").toUpperCase() : "";
|
|
328
|
+
const values = Array.isArray(entry) ? entry.slice(1).map((value) => finiteNumber(value, 0)) : [];
|
|
329
|
+
|
|
330
|
+
if (command === "M" && values.length >= 2) {
|
|
331
|
+
current = [values[0], values[1]];
|
|
332
|
+
subpathStart = current;
|
|
333
|
+
} else if (command === "L" && current && values.length >= 2) {
|
|
334
|
+
const next = [values[0], values[1]];
|
|
335
|
+
appendRawSegment(points, current, next, shape, context, budget);
|
|
336
|
+
current = next;
|
|
337
|
+
} else if (command === "H" && current && values.length >= 1) {
|
|
338
|
+
const next = [values[0], current[1]];
|
|
339
|
+
appendRawSegment(points, current, next, shape, context, budget);
|
|
340
|
+
current = next;
|
|
341
|
+
} else if (command === "V" && current && values.length >= 1) {
|
|
342
|
+
const next = [current[0], values[0]];
|
|
343
|
+
appendRawSegment(points, current, next, shape, context, budget);
|
|
344
|
+
current = next;
|
|
345
|
+
} else if (command === "Q" && current && values.length >= 4) {
|
|
346
|
+
current = appendQuadraticPath(points, current, values, detail, tolerance, shape, context, budget);
|
|
347
|
+
} else if (command === "C" && current && values.length >= 6) {
|
|
348
|
+
current = appendCubicPath(points, current, values, detail, tolerance, shape, context, budget);
|
|
349
|
+
} else if (command === "A" && current && values.length >= 7) {
|
|
350
|
+
current = appendArcPath(points, current, values, detail, shape, context, budget);
|
|
351
|
+
} else if (command === "Z" && current && subpathStart) {
|
|
352
|
+
appendRawSegment(points, current, subpathStart, shape, context, budget);
|
|
353
|
+
current = subpathStart;
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const appendQuadraticPath = (points, current, values, detail, tolerance, shape, context, budget) => {
|
|
359
|
+
let previous = current;
|
|
360
|
+
const control = [values[0], values[1]];
|
|
361
|
+
const end = [values[2], values[3]];
|
|
362
|
+
|
|
363
|
+
if (tolerance !== null) {
|
|
364
|
+
appendAdaptiveQuadraticPath(points, current, control, end, tolerance, shape, context, budget);
|
|
365
|
+
return end;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (let step = 1; step <= detail; step += 1) {
|
|
369
|
+
const t = step / detail;
|
|
370
|
+
const next = [
|
|
371
|
+
quadraticPoint(current[0], control[0], end[0], t),
|
|
372
|
+
quadraticPoint(current[1], control[1], end[1], t)
|
|
373
|
+
];
|
|
374
|
+
if (!appendRawSegment(points, previous, next, shape, context, budget)) break;
|
|
375
|
+
previous = next;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return end;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const appendCubicPath = (points, current, values, detail, tolerance, shape, context, budget) => {
|
|
382
|
+
let previous = current;
|
|
383
|
+
const c1 = [values[0], values[1]];
|
|
384
|
+
const c2 = [values[2], values[3]];
|
|
385
|
+
const end = [values[4], values[5]];
|
|
386
|
+
|
|
387
|
+
if (tolerance !== null) {
|
|
388
|
+
appendAdaptiveCubicPath(points, current, c1, c2, end, tolerance, shape, context, budget);
|
|
389
|
+
return end;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
for (let step = 1; step <= detail; step += 1) {
|
|
393
|
+
const t = step / detail;
|
|
394
|
+
const next = [
|
|
395
|
+
cubicPoint(current[0], c1[0], c2[0], end[0], t),
|
|
396
|
+
cubicPoint(current[1], c1[1], c2[1], end[1], t)
|
|
397
|
+
];
|
|
398
|
+
if (!appendRawSegment(points, previous, next, shape, context, budget)) break;
|
|
399
|
+
previous = next;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return end;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const appendArcPath = (points, current, values, detail, shape, context, budget) => {
|
|
406
|
+
const end = [values[5], values[6]];
|
|
407
|
+
const arc = describeSvgArc({
|
|
408
|
+
from: current,
|
|
409
|
+
to: end,
|
|
410
|
+
rx: values[0],
|
|
411
|
+
ry: values[1],
|
|
412
|
+
xAxisRotation: values[2],
|
|
413
|
+
largeArc: !!values[3],
|
|
414
|
+
sweep: !!values[4]
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
if (!arc) {
|
|
418
|
+
appendRawSegment(points, current, end, shape, context, budget);
|
|
419
|
+
return end;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let previous = current;
|
|
423
|
+
const segments = svgArcSegmentCount(arc, detail);
|
|
424
|
+
for (let step = 1; step <= segments; step += 1) {
|
|
425
|
+
const next = svgArcPoint(arc, step / segments);
|
|
426
|
+
if (!appendRawSegment(points, previous, next, shape, context, budget)) break;
|
|
427
|
+
previous = next;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return end;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const appendRawSegment = (points, from, to, shape, context, budget) => {
|
|
434
|
+
if (budget && budget.remaining <= 0) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
appendSegment(points, normalizeShapePoint(from[0], from[1], context), normalizeShapePoint(to[0], to[1], context), shape, context);
|
|
439
|
+
if (budget) {
|
|
440
|
+
budget.remaining -= 1;
|
|
441
|
+
}
|
|
442
|
+
return true;
|
|
263
443
|
};
|
|
264
444
|
|
|
265
|
-
const
|
|
445
|
+
const appendAdaptiveQuadraticPath = (points, from, control, to, tolerance, shape, context, budget, depth = 0) => {
|
|
446
|
+
if (budget && budget.remaining <= 0) return;
|
|
447
|
+
|
|
448
|
+
if (depth >= PATH_MAX_RECURSION || pointLineDistance(control, from, to) <= tolerance) {
|
|
449
|
+
appendRawSegment(points, from, to, shape, context, budget);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const leftControl = midpoint(from, control);
|
|
454
|
+
const rightControl = midpoint(control, to);
|
|
455
|
+
const center = midpoint(leftControl, rightControl);
|
|
456
|
+
appendAdaptiveQuadraticPath(points, from, leftControl, center, tolerance, shape, context, budget, depth + 1);
|
|
457
|
+
appendAdaptiveQuadraticPath(points, center, rightControl, to, tolerance, shape, context, budget, depth + 1);
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const appendAdaptiveCubicPath = (points, from, c1, c2, to, tolerance, shape, context, budget, depth = 0) => {
|
|
461
|
+
if (budget && budget.remaining <= 0) return;
|
|
462
|
+
|
|
463
|
+
const flatness = Math.max(pointLineDistance(c1, from, to), pointLineDistance(c2, from, to));
|
|
464
|
+
if (depth >= PATH_MAX_RECURSION || flatness <= tolerance) {
|
|
465
|
+
appendRawSegment(points, from, to, shape, context, budget);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const p01 = midpoint(from, c1);
|
|
470
|
+
const p12 = midpoint(c1, c2);
|
|
471
|
+
const p23 = midpoint(c2, to);
|
|
472
|
+
const p012 = midpoint(p01, p12);
|
|
473
|
+
const p123 = midpoint(p12, p23);
|
|
474
|
+
const center = midpoint(p012, p123);
|
|
475
|
+
appendAdaptiveCubicPath(points, from, p01, p012, center, tolerance, shape, context, budget, depth + 1);
|
|
476
|
+
appendAdaptiveCubicPath(points, center, p123, p23, to, tolerance, shape, context, budget, depth + 1);
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const appendPolylineSegments = (points, vertices, shape, context, closed) => {
|
|
480
|
+
for (let index = 1; index < vertices.length; index += 1) {
|
|
481
|
+
appendSegment(points, vertices[index - 1], vertices[index], shape, context);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (closed && vertices.length > 2) {
|
|
485
|
+
appendSegment(points, vertices[vertices.length - 1], vertices[0], shape, context);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const appendSegment = (points, from, to, shape, context) => {
|
|
490
|
+
const start = applyShapeTransform(from, shape, context);
|
|
491
|
+
const end = applyShapeTransform(to, shape, context);
|
|
492
|
+
points.push(start[0], start[1], end[0], end[1]);
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const normalizeShapePoints = (value, context) => {
|
|
496
|
+
if (!Array.isArray(value)) {
|
|
497
|
+
return [];
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return value
|
|
501
|
+
.filter((point) => Array.isArray(point) && point.length >= 2)
|
|
502
|
+
.map((point) => normalizeShapePoint(point[0], point[1], context));
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const shapeCoordinateContext = (params) => {
|
|
506
|
+
const requestedUnits = String(params.units || "").trim().toLowerCase();
|
|
507
|
+
if (requestedUnits) {
|
|
508
|
+
return { units: requestedUnits };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const version = Number(params.shape_schema_version ?? params.shapeSchemaVersion ?? 1);
|
|
512
|
+
return { units: version >= 2 ? "logical" : "legacy" };
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const normalizeShapePoint = (x, y, context) => {
|
|
516
|
+
return [
|
|
517
|
+
normalizeShapeCoordinate(x, context, "x"),
|
|
518
|
+
normalizeShapeCoordinate(y, context, "y")
|
|
519
|
+
];
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const normalizeShapeCoordinate = (value, context, axis) => {
|
|
266
523
|
const numeric = finiteNumber(value, 0);
|
|
524
|
+
if (context.units === "ndc") {
|
|
525
|
+
return clamp(numeric, -1.2, 1.2);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (logicalShapeUnits(context.units)) {
|
|
529
|
+
return clamp(numeric / shapeAxisHalf(axis), -1.2, 1.2);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (screenShapeUnits(context.units)) {
|
|
533
|
+
return axis === "y"
|
|
534
|
+
? clamp(1 - numeric / SHAPE_HALF_HEIGHT, -1.2, 1.2)
|
|
535
|
+
: clamp(numeric / SHAPE_HALF_WIDTH - 1, -1.2, 1.2);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return normalizeLegacyShapeCoordinate(numeric, axis);
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const normalizeLegacyShapeCoordinate = (numeric, axis) => {
|
|
267
542
|
if (Math.abs(numeric) <= 1.5) {
|
|
268
543
|
return clamp(numeric, -1.2, 1.2);
|
|
269
544
|
}
|
|
270
545
|
|
|
271
|
-
|
|
272
|
-
|
|
546
|
+
return axis === "y"
|
|
547
|
+
? clamp(1 - numeric / SHAPE_HALF_HEIGHT, -1.2, 1.2)
|
|
548
|
+
: clamp(numeric / SHAPE_HALF_WIDTH - 1, -1.2, 1.2);
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const normalizeShapeLength = (value, context, axis) => {
|
|
552
|
+
const numeric = Math.abs(finiteNumber(value, 0));
|
|
553
|
+
if (context.units === "ndc" || Math.abs(numeric) <= 2) {
|
|
554
|
+
return clamp(numeric, 0.005, 1.4);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return clamp(numeric / shapeAxisHalf(axis === "radius" ? "y" : axis), 0.005, 1.4);
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const normalizeShapeVector = (value, context, axis) => {
|
|
561
|
+
const numeric = finiteNumber(value, 0);
|
|
562
|
+
if (context.units === "ndc") {
|
|
563
|
+
return clamp(numeric, -2.0, 2.0);
|
|
273
564
|
}
|
|
274
565
|
|
|
275
|
-
return clamp(numeric /
|
|
566
|
+
return clamp(numeric / shapeAxisHalf(axis), -2.0, 2.0);
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const applyShapeTransform = (point, shape, context) => {
|
|
570
|
+
const transform = shapeTransform(shape, context);
|
|
571
|
+
const shiftedX = (point[0] - transform.origin.x) * transform.scale.x;
|
|
572
|
+
const shiftedY = (point[1] - transform.origin.y) * transform.scale.y;
|
|
573
|
+
const radians = (transform.rotate / 180) * Math.PI;
|
|
574
|
+
const cos = Math.cos(radians);
|
|
575
|
+
const sin = Math.sin(radians);
|
|
576
|
+
const rotatedX = shiftedX * cos - shiftedY * sin;
|
|
577
|
+
const rotatedY = shiftedX * sin + shiftedY * cos;
|
|
578
|
+
|
|
579
|
+
return [
|
|
580
|
+
clamp(rotatedX + transform.origin.x + transform.translate.x, -1.2, 1.2),
|
|
581
|
+
clamp(rotatedY + transform.origin.y + transform.translate.y, -1.2, 1.2)
|
|
582
|
+
];
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const shapeTransform = (shape, context) => {
|
|
586
|
+
const transform = shape?.transform || {};
|
|
587
|
+
return {
|
|
588
|
+
translate: normalizeShapeVectorObject(transform.translate || shape.translate, context, { x: 0, y: 0 }),
|
|
589
|
+
origin: normalizeShapeVectorObject(transform.origin || shape.origin, context, { x: 0, y: 0 }),
|
|
590
|
+
rotate: finiteNumber(transform.rotate ?? shape.rotate ?? shape.rotation, 0),
|
|
591
|
+
scale: normalizeShapeScale(transform.scale ?? shape.scale)
|
|
592
|
+
};
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const normalizeShapeVectorObject = (value, context, fallback) => {
|
|
596
|
+
if (Array.isArray(value)) {
|
|
597
|
+
return {
|
|
598
|
+
x: normalizeShapeVector(value[0] ?? fallback.x, context, "x"),
|
|
599
|
+
y: normalizeShapeVector(value[1] ?? fallback.y, context, "y")
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (value && typeof value === "object") {
|
|
604
|
+
return {
|
|
605
|
+
x: normalizeShapeVector(value.x ?? fallback.x, context, "x"),
|
|
606
|
+
y: normalizeShapeVector(value.y ?? fallback.y, context, "y")
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return fallback;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const normalizeShapeScale = (value) => {
|
|
614
|
+
if (value && typeof value === "object") {
|
|
615
|
+
return {
|
|
616
|
+
x: clamp(finiteNumber(value.x, 1), -8, 8),
|
|
617
|
+
y: clamp(finiteNumber(value.y, 1), -8, 8)
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const scale = clamp(finiteNumber(value, 1), -8, 8);
|
|
622
|
+
return { x: scale, y: scale };
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const lineDefaults = (context) => {
|
|
626
|
+
if (context.units === "legacy" || context.units === "ndc") {
|
|
627
|
+
return [-0.8, 0, 0.8, 0];
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return [-100, 0, 100, 0];
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const shapeAxisHalf = (axis) => axis === "x" ? SHAPE_HALF_WIDTH : SHAPE_HALF_HEIGHT;
|
|
634
|
+
|
|
635
|
+
const logicalShapeUnits = (value) => ["logical", "center", "center_origin", "px"].includes(value);
|
|
636
|
+
|
|
637
|
+
const screenShapeUnits = (value) => ["screen", "canvas", "viewport"].includes(value);
|
|
638
|
+
|
|
639
|
+
const radiusToRawHalf = (value) => finiteNumber(value, 100) * 0.5;
|
|
640
|
+
|
|
641
|
+
const pathSegmentBudget = (shape) => {
|
|
642
|
+
const value = shape.max_segments ?? shape.maxSegments ?? PATH_DEFAULT_MAX_SEGMENTS;
|
|
643
|
+
return { remaining: clampInt(value, 1, PATH_HARD_MAX_SEGMENTS) };
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const pathTolerance = (shape) => {
|
|
647
|
+
if (shape.tolerance === undefined && shape.tolerancePx === undefined) return null;
|
|
648
|
+
|
|
649
|
+
const numeric = Number(shape.tolerance ?? shape.tolerancePx);
|
|
650
|
+
return Number.isFinite(numeric) && numeric >= 0 ? numeric : null;
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const midpoint = (from, to) => [
|
|
654
|
+
(from[0] + to[0]) * 0.5,
|
|
655
|
+
(from[1] + to[1]) * 0.5
|
|
656
|
+
];
|
|
657
|
+
|
|
658
|
+
const pointLineDistance = (point, from, to) => {
|
|
659
|
+
const dx = to[0] - from[0];
|
|
660
|
+
const dy = to[1] - from[1];
|
|
661
|
+
const length = Math.hypot(dx, dy);
|
|
662
|
+
if (length <= 0) return Math.hypot(point[0] - from[0], point[1] - from[1]);
|
|
663
|
+
return Math.abs(dy * point[0] - dx * point[1] + to[0] * from[1] - to[1] * from[0]) / length;
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const quadraticPoint = (from, control, to, t) => {
|
|
667
|
+
const inv = 1 - t;
|
|
668
|
+
return inv * inv * from + 2 * inv * t * control + t * t * to;
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
const cubicPoint = (from, c1, c2, to, t) => {
|
|
672
|
+
const inv = 1 - t;
|
|
673
|
+
return inv * inv * inv * from + 3 * inv * inv * t * c1 + 3 * inv * t * t * c2 + t * t * t * to;
|
|
276
674
|
};
|
|
277
675
|
|
|
278
676
|
const sampleSpectrum = (spectrum, progress) => {
|