sketchmark 1.2.0 → 1.3.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/README.md +55 -73
- package/dist/animation/index.d.ts.map +1 -1
- package/dist/ast/types.d.ts +3 -0
- package/dist/ast/types.d.ts.map +1 -1
- package/dist/index.cjs +595 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +595 -34
- package/dist/index.js.map +1 -1
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/renderer/canvas/index.d.ts.map +1 -1
- package/dist/renderer/shapes/path-geometry.d.ts +8 -0
- package/dist/renderer/shapes/path-geometry.d.ts.map +1 -0
- package/dist/renderer/shapes/path.d.ts.map +1 -1
- package/dist/renderer/svg/index.d.ts.map +1 -1
- package/dist/scene/index.d.ts +3 -0
- package/dist/scene/index.d.ts.map +1 -1
- package/dist/sketchmark.iife.js +595 -34
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -377,6 +377,7 @@ function parse(src, options = {}) {
|
|
|
377
377
|
const ast = {
|
|
378
378
|
kind: "diagram",
|
|
379
379
|
layout: "column",
|
|
380
|
+
style: {},
|
|
380
381
|
nodes: [],
|
|
381
382
|
edges: [],
|
|
382
383
|
groups: [],
|
|
@@ -433,6 +434,54 @@ function parse(src, options = {}) {
|
|
|
433
434
|
}
|
|
434
435
|
return props;
|
|
435
436
|
}
|
|
437
|
+
function parseConfigValue(value) {
|
|
438
|
+
if (value === "true" || value === "on")
|
|
439
|
+
return true;
|
|
440
|
+
if (value === "false" || value === "off")
|
|
441
|
+
return false;
|
|
442
|
+
const numeric = Number(value);
|
|
443
|
+
return Number.isNaN(numeric) ? value : numeric;
|
|
444
|
+
}
|
|
445
|
+
function applyRootProps(props) {
|
|
446
|
+
const styleProps = propsToStyle(props);
|
|
447
|
+
const styleKeys = new Set([
|
|
448
|
+
"fill",
|
|
449
|
+
"stroke",
|
|
450
|
+
"stroke-width",
|
|
451
|
+
"color",
|
|
452
|
+
"opacity",
|
|
453
|
+
"font-size",
|
|
454
|
+
"font-weight",
|
|
455
|
+
"font",
|
|
456
|
+
"dash",
|
|
457
|
+
"stroke-dash",
|
|
458
|
+
"padding",
|
|
459
|
+
"text-align",
|
|
460
|
+
"vertical-align",
|
|
461
|
+
"line-height",
|
|
462
|
+
"letter-spacing",
|
|
463
|
+
]);
|
|
464
|
+
ast.style = { ...(ast.style ?? {}), ...styleProps };
|
|
465
|
+
if (props.layout) {
|
|
466
|
+
ast.layout = props.layout;
|
|
467
|
+
}
|
|
468
|
+
if (props.width !== undefined) {
|
|
469
|
+
ast.width = parseFloat(props.width);
|
|
470
|
+
}
|
|
471
|
+
if (props.height !== undefined) {
|
|
472
|
+
ast.height = parseFloat(props.height);
|
|
473
|
+
}
|
|
474
|
+
if (props.font !== undefined) {
|
|
475
|
+
ast.config.font = parseConfigValue(props.font);
|
|
476
|
+
}
|
|
477
|
+
for (const [key, value] of Object.entries(props)) {
|
|
478
|
+
if (key === "layout" || key === "width" || key === "height")
|
|
479
|
+
continue;
|
|
480
|
+
if (styleKeys.has(key))
|
|
481
|
+
continue;
|
|
482
|
+
ast.config[key] = parseConfigValue(value);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
436
485
|
function parseGroupProps(toks, startIndex) {
|
|
437
486
|
const props = {};
|
|
438
487
|
const itemIds = [];
|
|
@@ -914,8 +963,12 @@ function parse(src, options = {}) {
|
|
|
914
963
|
};
|
|
915
964
|
}
|
|
916
965
|
skipNL();
|
|
917
|
-
if (cur().value === "diagram")
|
|
966
|
+
if (cur().value === "diagram") {
|
|
918
967
|
skip();
|
|
968
|
+
const toks = lineTokens();
|
|
969
|
+
const props = parseSimpleProps(toks, 0);
|
|
970
|
+
applyRootProps(props);
|
|
971
|
+
}
|
|
919
972
|
skipNL();
|
|
920
973
|
while (cur().type !== "EOF" && cur().value !== "end") {
|
|
921
974
|
skipNL();
|
|
@@ -926,7 +979,7 @@ function parse(src, options = {}) {
|
|
|
926
979
|
continue;
|
|
927
980
|
}
|
|
928
981
|
if (v === "diagram") {
|
|
929
|
-
|
|
982
|
+
lineTokens();
|
|
930
983
|
continue;
|
|
931
984
|
}
|
|
932
985
|
if (v === "end")
|
|
@@ -936,10 +989,7 @@ function parse(src, options = {}) {
|
|
|
936
989
|
continue;
|
|
937
990
|
}
|
|
938
991
|
if (v === "layout") {
|
|
939
|
-
|
|
940
|
-
ast.layout = cur().value ?? "column";
|
|
941
|
-
skip();
|
|
942
|
-
continue;
|
|
992
|
+
throw new ParseError(`Root layout must be declared on the diagram line, e.g. diagram layout=absolute`, t.line, t.col);
|
|
943
993
|
}
|
|
944
994
|
if (v === "title") {
|
|
945
995
|
skip();
|
|
@@ -963,15 +1013,7 @@ function parse(src, options = {}) {
|
|
|
963
1013
|
continue;
|
|
964
1014
|
}
|
|
965
1015
|
if (v === "config") {
|
|
966
|
-
|
|
967
|
-
const key = cur().value;
|
|
968
|
-
skip();
|
|
969
|
-
if (cur().type === "EQUALS")
|
|
970
|
-
skip();
|
|
971
|
-
const value = cur().value;
|
|
972
|
-
skip();
|
|
973
|
-
ast.config[key] = value;
|
|
974
|
-
continue;
|
|
1016
|
+
throw new ParseError(`Root config must be declared on the diagram line, e.g. diagram gap=40 margin=0 tts=true`, t.line, t.col);
|
|
975
1017
|
}
|
|
976
1018
|
if (v === "style") {
|
|
977
1019
|
skip();
|
|
@@ -3624,6 +3666,7 @@ function buildSceneGraph(ast) {
|
|
|
3624
3666
|
title: ast.title,
|
|
3625
3667
|
description: ast.description,
|
|
3626
3668
|
layout: ast.layout,
|
|
3669
|
+
style: ast.style ?? {},
|
|
3627
3670
|
nodes,
|
|
3628
3671
|
edges,
|
|
3629
3672
|
groups,
|
|
@@ -3636,6 +3679,8 @@ function buildSceneGraph(ast) {
|
|
|
3636
3679
|
rootOrder: ast.rootOrder ?? [],
|
|
3637
3680
|
width: 0,
|
|
3638
3681
|
height: 0,
|
|
3682
|
+
fixedWidth: ast.width,
|
|
3683
|
+
fixedHeight: ast.height,
|
|
3639
3684
|
};
|
|
3640
3685
|
}
|
|
3641
3686
|
// ── Helpers ───────────────────────────────────────────────
|
|
@@ -4297,37 +4342,510 @@ const lineShape = {
|
|
|
4297
4342
|
},
|
|
4298
4343
|
};
|
|
4299
4344
|
|
|
4345
|
+
const COMMAND_RE = /^[AaCcHhLlMmQqSsTtVvZz]$/;
|
|
4346
|
+
const TOKEN_RE = /[AaCcHhLlMmQqSsTtVvZz]|[-+]?(?:\d*\.\d+|\d+)(?:[eE][-+]?\d+)?/g;
|
|
4347
|
+
const EPSILON = 1e-6;
|
|
4348
|
+
const PARAM_COUNTS = {
|
|
4349
|
+
A: 7,
|
|
4350
|
+
C: 6,
|
|
4351
|
+
H: 1,
|
|
4352
|
+
L: 2,
|
|
4353
|
+
M: 2,
|
|
4354
|
+
Q: 4,
|
|
4355
|
+
S: 4,
|
|
4356
|
+
T: 2,
|
|
4357
|
+
V: 1,
|
|
4358
|
+
Z: 0,
|
|
4359
|
+
};
|
|
4360
|
+
function isCommandToken(token) {
|
|
4361
|
+
return COMMAND_RE.test(token);
|
|
4362
|
+
}
|
|
4363
|
+
function formatNumber(value) {
|
|
4364
|
+
const rounded = Math.abs(value) < EPSILON ? 0 : Number(value.toFixed(3));
|
|
4365
|
+
return Object.is(rounded, -0) ? "0" : String(rounded);
|
|
4366
|
+
}
|
|
4367
|
+
function parseRawSegments(pathData) {
|
|
4368
|
+
const tokens = pathData.match(TOKEN_RE) ?? [];
|
|
4369
|
+
if (!tokens.length)
|
|
4370
|
+
return [];
|
|
4371
|
+
const segments = [];
|
|
4372
|
+
let index = 0;
|
|
4373
|
+
let currentCommand = null;
|
|
4374
|
+
while (index < tokens.length) {
|
|
4375
|
+
const token = tokens[index];
|
|
4376
|
+
if (isCommandToken(token)) {
|
|
4377
|
+
currentCommand = token;
|
|
4378
|
+
index += 1;
|
|
4379
|
+
if (token === "Z" || token === "z") {
|
|
4380
|
+
segments.push({ command: "Z", values: [] });
|
|
4381
|
+
}
|
|
4382
|
+
continue;
|
|
4383
|
+
}
|
|
4384
|
+
if (!currentCommand)
|
|
4385
|
+
break;
|
|
4386
|
+
const upper = currentCommand.toUpperCase();
|
|
4387
|
+
const paramCount = PARAM_COUNTS[upper];
|
|
4388
|
+
if (!paramCount) {
|
|
4389
|
+
index += 1;
|
|
4390
|
+
continue;
|
|
4391
|
+
}
|
|
4392
|
+
let isFirstMove = upper === "M";
|
|
4393
|
+
while (index < tokens.length && !isCommandToken(tokens[index])) {
|
|
4394
|
+
if (index + paramCount > tokens.length)
|
|
4395
|
+
return segments;
|
|
4396
|
+
const values = tokens
|
|
4397
|
+
.slice(index, index + paramCount)
|
|
4398
|
+
.map((value) => Number(value));
|
|
4399
|
+
if (values.some((value) => Number.isNaN(value))) {
|
|
4400
|
+
return segments;
|
|
4401
|
+
}
|
|
4402
|
+
if (upper === "M") {
|
|
4403
|
+
const moveCommand = isFirstMove
|
|
4404
|
+
? currentCommand
|
|
4405
|
+
: currentCommand === "m"
|
|
4406
|
+
? "l"
|
|
4407
|
+
: "L";
|
|
4408
|
+
segments.push({ command: moveCommand, values });
|
|
4409
|
+
isFirstMove = false;
|
|
4410
|
+
}
|
|
4411
|
+
else {
|
|
4412
|
+
segments.push({ command: currentCommand, values });
|
|
4413
|
+
}
|
|
4414
|
+
index += paramCount;
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
return segments;
|
|
4418
|
+
}
|
|
4419
|
+
function reflect(control, around) {
|
|
4420
|
+
return {
|
|
4421
|
+
x: around.x * 2 - control.x,
|
|
4422
|
+
y: around.y * 2 - control.y,
|
|
4423
|
+
};
|
|
4424
|
+
}
|
|
4425
|
+
function toAbsoluteSegments(rawSegments) {
|
|
4426
|
+
const segments = [];
|
|
4427
|
+
let current = { x: 0, y: 0 };
|
|
4428
|
+
let subpathStart = { x: 0, y: 0 };
|
|
4429
|
+
let previousCubicControl = null;
|
|
4430
|
+
let previousQuadraticControl = null;
|
|
4431
|
+
for (const segment of rawSegments) {
|
|
4432
|
+
const isRelative = segment.command === segment.command.toLowerCase();
|
|
4433
|
+
const command = segment.command.toUpperCase();
|
|
4434
|
+
const values = segment.values;
|
|
4435
|
+
switch (command) {
|
|
4436
|
+
case "M": {
|
|
4437
|
+
const x = isRelative ? current.x + values[0] : values[0];
|
|
4438
|
+
const y = isRelative ? current.y + values[1] : values[1];
|
|
4439
|
+
current = { x, y };
|
|
4440
|
+
subpathStart = { x, y };
|
|
4441
|
+
previousCubicControl = null;
|
|
4442
|
+
previousQuadraticControl = null;
|
|
4443
|
+
segments.push({ command: "M", values: [x, y] });
|
|
4444
|
+
break;
|
|
4445
|
+
}
|
|
4446
|
+
case "L": {
|
|
4447
|
+
const x = isRelative ? current.x + values[0] : values[0];
|
|
4448
|
+
const y = isRelative ? current.y + values[1] : values[1];
|
|
4449
|
+
current = { x, y };
|
|
4450
|
+
previousCubicControl = null;
|
|
4451
|
+
previousQuadraticControl = null;
|
|
4452
|
+
segments.push({ command: "L", values: [x, y] });
|
|
4453
|
+
break;
|
|
4454
|
+
}
|
|
4455
|
+
case "H": {
|
|
4456
|
+
const x = isRelative ? current.x + values[0] : values[0];
|
|
4457
|
+
current = { x, y: current.y };
|
|
4458
|
+
previousCubicControl = null;
|
|
4459
|
+
previousQuadraticControl = null;
|
|
4460
|
+
segments.push({ command: "L", values: [x, current.y] });
|
|
4461
|
+
break;
|
|
4462
|
+
}
|
|
4463
|
+
case "V": {
|
|
4464
|
+
const y = isRelative ? current.y + values[0] : values[0];
|
|
4465
|
+
current = { x: current.x, y };
|
|
4466
|
+
previousCubicControl = null;
|
|
4467
|
+
previousQuadraticControl = null;
|
|
4468
|
+
segments.push({ command: "L", values: [current.x, y] });
|
|
4469
|
+
break;
|
|
4470
|
+
}
|
|
4471
|
+
case "C": {
|
|
4472
|
+
const x1 = isRelative ? current.x + values[0] : values[0];
|
|
4473
|
+
const y1 = isRelative ? current.y + values[1] : values[1];
|
|
4474
|
+
const x2 = isRelative ? current.x + values[2] : values[2];
|
|
4475
|
+
const y2 = isRelative ? current.y + values[3] : values[3];
|
|
4476
|
+
const x = isRelative ? current.x + values[4] : values[4];
|
|
4477
|
+
const y = isRelative ? current.y + values[5] : values[5];
|
|
4478
|
+
current = { x, y };
|
|
4479
|
+
previousCubicControl = { x: x2, y: y2 };
|
|
4480
|
+
previousQuadraticControl = null;
|
|
4481
|
+
segments.push({ command: "C", values: [x1, y1, x2, y2, x, y] });
|
|
4482
|
+
break;
|
|
4483
|
+
}
|
|
4484
|
+
case "S": {
|
|
4485
|
+
const control1 = previousCubicControl
|
|
4486
|
+
? reflect(previousCubicControl, current)
|
|
4487
|
+
: { ...current };
|
|
4488
|
+
const x2 = isRelative ? current.x + values[0] : values[0];
|
|
4489
|
+
const y2 = isRelative ? current.y + values[1] : values[1];
|
|
4490
|
+
const x = isRelative ? current.x + values[2] : values[2];
|
|
4491
|
+
const y = isRelative ? current.y + values[3] : values[3];
|
|
4492
|
+
current = { x, y };
|
|
4493
|
+
previousCubicControl = { x: x2, y: y2 };
|
|
4494
|
+
previousQuadraticControl = null;
|
|
4495
|
+
segments.push({
|
|
4496
|
+
command: "C",
|
|
4497
|
+
values: [control1.x, control1.y, x2, y2, x, y],
|
|
4498
|
+
});
|
|
4499
|
+
break;
|
|
4500
|
+
}
|
|
4501
|
+
case "Q": {
|
|
4502
|
+
const x1 = isRelative ? current.x + values[0] : values[0];
|
|
4503
|
+
const y1 = isRelative ? current.y + values[1] : values[1];
|
|
4504
|
+
const x = isRelative ? current.x + values[2] : values[2];
|
|
4505
|
+
const y = isRelative ? current.y + values[3] : values[3];
|
|
4506
|
+
current = { x, y };
|
|
4507
|
+
previousCubicControl = null;
|
|
4508
|
+
previousQuadraticControl = { x: x1, y: y1 };
|
|
4509
|
+
segments.push({ command: "Q", values: [x1, y1, x, y] });
|
|
4510
|
+
break;
|
|
4511
|
+
}
|
|
4512
|
+
case "T": {
|
|
4513
|
+
const control = previousQuadraticControl
|
|
4514
|
+
? reflect(previousQuadraticControl, current)
|
|
4515
|
+
: { ...current };
|
|
4516
|
+
const x = isRelative ? current.x + values[0] : values[0];
|
|
4517
|
+
const y = isRelative ? current.y + values[1] : values[1];
|
|
4518
|
+
current = { x, y };
|
|
4519
|
+
previousCubicControl = null;
|
|
4520
|
+
previousQuadraticControl = control;
|
|
4521
|
+
segments.push({ command: "Q", values: [control.x, control.y, x, y] });
|
|
4522
|
+
break;
|
|
4523
|
+
}
|
|
4524
|
+
case "A": {
|
|
4525
|
+
const rx = Math.abs(values[0]);
|
|
4526
|
+
const ry = Math.abs(values[1]);
|
|
4527
|
+
const rotation = values[2];
|
|
4528
|
+
const largeArc = values[3];
|
|
4529
|
+
const sweep = values[4];
|
|
4530
|
+
const x = isRelative ? current.x + values[5] : values[5];
|
|
4531
|
+
const y = isRelative ? current.y + values[6] : values[6];
|
|
4532
|
+
current = { x, y };
|
|
4533
|
+
previousCubicControl = null;
|
|
4534
|
+
previousQuadraticControl = null;
|
|
4535
|
+
segments.push({
|
|
4536
|
+
command: "A",
|
|
4537
|
+
values: [rx, ry, rotation, largeArc, sweep, x, y],
|
|
4538
|
+
});
|
|
4539
|
+
break;
|
|
4540
|
+
}
|
|
4541
|
+
case "Z": {
|
|
4542
|
+
current = { ...subpathStart };
|
|
4543
|
+
previousCubicControl = null;
|
|
4544
|
+
previousQuadraticControl = null;
|
|
4545
|
+
segments.push({ command: "Z", values: [] });
|
|
4546
|
+
break;
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
return segments;
|
|
4551
|
+
}
|
|
4552
|
+
function cubicAt(p0, p1, p2, p3, t) {
|
|
4553
|
+
const mt = 1 - t;
|
|
4554
|
+
return (mt * mt * mt * p0 +
|
|
4555
|
+
3 * mt * mt * t * p1 +
|
|
4556
|
+
3 * mt * t * t * p2 +
|
|
4557
|
+
t * t * t * p3);
|
|
4558
|
+
}
|
|
4559
|
+
function quadraticAt(p0, p1, p2, t) {
|
|
4560
|
+
const mt = 1 - t;
|
|
4561
|
+
return mt * mt * p0 + 2 * mt * t * p1 + t * t * p2;
|
|
4562
|
+
}
|
|
4563
|
+
function cubicExtrema(p0, p1, p2, p3) {
|
|
4564
|
+
const a = -p0 + 3 * p1 - 3 * p2 + p3;
|
|
4565
|
+
const b = 3 * p0 - 6 * p1 + 3 * p2;
|
|
4566
|
+
const c = -3 * p0 + 3 * p1;
|
|
4567
|
+
if (Math.abs(a) < EPSILON) {
|
|
4568
|
+
if (Math.abs(b) < EPSILON)
|
|
4569
|
+
return [];
|
|
4570
|
+
return [-c / (2 * b)].filter((t) => t > 0 && t < 1);
|
|
4571
|
+
}
|
|
4572
|
+
const discriminant = 4 * b * b - 12 * a * c;
|
|
4573
|
+
if (discriminant < 0)
|
|
4574
|
+
return [];
|
|
4575
|
+
const sqrtDiscriminant = Math.sqrt(discriminant);
|
|
4576
|
+
return [
|
|
4577
|
+
(-2 * b + sqrtDiscriminant) / (6 * a),
|
|
4578
|
+
(-2 * b - sqrtDiscriminant) / (6 * a),
|
|
4579
|
+
].filter((t) => t > 0 && t < 1);
|
|
4580
|
+
}
|
|
4581
|
+
function quadraticExtrema(p0, p1, p2) {
|
|
4582
|
+
const denominator = p0 - 2 * p1 + p2;
|
|
4583
|
+
if (Math.abs(denominator) < EPSILON)
|
|
4584
|
+
return [];
|
|
4585
|
+
const t = (p0 - p1) / denominator;
|
|
4586
|
+
return t > 0 && t < 1 ? [t] : [];
|
|
4587
|
+
}
|
|
4588
|
+
function angleBetween(u, v) {
|
|
4589
|
+
const magnitude = Math.hypot(u.x, u.y) * Math.hypot(v.x, v.y);
|
|
4590
|
+
if (magnitude < EPSILON)
|
|
4591
|
+
return 0;
|
|
4592
|
+
const sign = u.x * v.y - u.y * v.x < 0 ? -1 : 1;
|
|
4593
|
+
const cosine = Math.min(1, Math.max(-1, (u.x * v.x + u.y * v.y) / magnitude));
|
|
4594
|
+
return sign * Math.acos(cosine);
|
|
4595
|
+
}
|
|
4596
|
+
function sampleArc(start, values) {
|
|
4597
|
+
let [rx, ry, rotation, largeArcFlag, sweepFlag, endX, endY] = values;
|
|
4598
|
+
if ((Math.abs(start.x - endX) < EPSILON && Math.abs(start.y - endY) < EPSILON) || rx < EPSILON || ry < EPSILON) {
|
|
4599
|
+
return [start, { x: endX, y: endY }];
|
|
4600
|
+
}
|
|
4601
|
+
rx = Math.abs(rx);
|
|
4602
|
+
ry = Math.abs(ry);
|
|
4603
|
+
const phi = (rotation * Math.PI) / 180;
|
|
4604
|
+
const cosPhi = Math.cos(phi);
|
|
4605
|
+
const sinPhi = Math.sin(phi);
|
|
4606
|
+
const dx2 = (start.x - endX) / 2;
|
|
4607
|
+
const dy2 = (start.y - endY) / 2;
|
|
4608
|
+
const x1p = cosPhi * dx2 + sinPhi * dy2;
|
|
4609
|
+
const y1p = -sinPhi * dx2 + cosPhi * dy2;
|
|
4610
|
+
const lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
|
|
4611
|
+
if (lambda > 1) {
|
|
4612
|
+
const scale = Math.sqrt(lambda);
|
|
4613
|
+
rx *= scale;
|
|
4614
|
+
ry *= scale;
|
|
4615
|
+
}
|
|
4616
|
+
const rx2 = rx * rx;
|
|
4617
|
+
const ry2 = ry * ry;
|
|
4618
|
+
const x1p2 = x1p * x1p;
|
|
4619
|
+
const y1p2 = y1p * y1p;
|
|
4620
|
+
const numerator = rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2;
|
|
4621
|
+
const denominator = rx2 * y1p2 + ry2 * x1p2;
|
|
4622
|
+
const factor = denominator < EPSILON ? 0 : Math.sqrt(Math.max(0, numerator / denominator));
|
|
4623
|
+
const sign = largeArcFlag === sweepFlag ? -1 : 1;
|
|
4624
|
+
const cxp = sign * factor * ((rx * y1p) / ry);
|
|
4625
|
+
const cyp = sign * factor * (-(ry * x1p) / rx);
|
|
4626
|
+
const cx = cosPhi * cxp - sinPhi * cyp + (start.x + endX) / 2;
|
|
4627
|
+
const cy = sinPhi * cxp + cosPhi * cyp + (start.y + endY) / 2;
|
|
4628
|
+
const startVector = {
|
|
4629
|
+
x: (x1p - cxp) / rx,
|
|
4630
|
+
y: (y1p - cyp) / ry,
|
|
4631
|
+
};
|
|
4632
|
+
const endVector = {
|
|
4633
|
+
x: (-x1p - cxp) / rx,
|
|
4634
|
+
y: (-y1p - cyp) / ry,
|
|
4635
|
+
};
|
|
4636
|
+
let deltaTheta = angleBetween(startVector, endVector);
|
|
4637
|
+
if (!sweepFlag && deltaTheta > 0)
|
|
4638
|
+
deltaTheta -= Math.PI * 2;
|
|
4639
|
+
if (sweepFlag && deltaTheta < 0)
|
|
4640
|
+
deltaTheta += Math.PI * 2;
|
|
4641
|
+
const theta1 = angleBetween({ x: 1, y: 0 }, startVector);
|
|
4642
|
+
const steps = Math.max(12, Math.ceil(Math.abs(deltaTheta) / (Math.PI / 8)));
|
|
4643
|
+
const points = [];
|
|
4644
|
+
for (let index = 0; index <= steps; index += 1) {
|
|
4645
|
+
const theta = theta1 + (deltaTheta * index) / steps;
|
|
4646
|
+
const cosTheta = Math.cos(theta);
|
|
4647
|
+
const sinTheta = Math.sin(theta);
|
|
4648
|
+
points.push({
|
|
4649
|
+
x: cx + rx * cosPhi * cosTheta - ry * sinPhi * sinTheta,
|
|
4650
|
+
y: cy + rx * sinPhi * cosTheta + ry * cosPhi * sinTheta,
|
|
4651
|
+
});
|
|
4652
|
+
}
|
|
4653
|
+
return points;
|
|
4654
|
+
}
|
|
4655
|
+
function boundsFromAbsoluteSegments(segments) {
|
|
4656
|
+
if (!segments.length)
|
|
4657
|
+
return null;
|
|
4658
|
+
let minX = Infinity;
|
|
4659
|
+
let minY = Infinity;
|
|
4660
|
+
let maxX = -Infinity;
|
|
4661
|
+
let maxY = -Infinity;
|
|
4662
|
+
const include = (point) => {
|
|
4663
|
+
minX = Math.min(minX, point.x);
|
|
4664
|
+
minY = Math.min(minY, point.y);
|
|
4665
|
+
maxX = Math.max(maxX, point.x);
|
|
4666
|
+
maxY = Math.max(maxY, point.y);
|
|
4667
|
+
};
|
|
4668
|
+
let current = { x: 0, y: 0 };
|
|
4669
|
+
let subpathStart = { x: 0, y: 0 };
|
|
4670
|
+
for (const segment of segments) {
|
|
4671
|
+
switch (segment.command) {
|
|
4672
|
+
case "M": {
|
|
4673
|
+
current = { x: segment.values[0], y: segment.values[1] };
|
|
4674
|
+
subpathStart = { ...current };
|
|
4675
|
+
include(current);
|
|
4676
|
+
break;
|
|
4677
|
+
}
|
|
4678
|
+
case "L": {
|
|
4679
|
+
include(current);
|
|
4680
|
+
current = { x: segment.values[0], y: segment.values[1] };
|
|
4681
|
+
include(current);
|
|
4682
|
+
break;
|
|
4683
|
+
}
|
|
4684
|
+
case "C": {
|
|
4685
|
+
const [x1, y1, x2, y2, x, y] = segment.values;
|
|
4686
|
+
const ts = new Set([0, 1]);
|
|
4687
|
+
cubicExtrema(current.x, x1, x2, x).forEach((value) => ts.add(value));
|
|
4688
|
+
cubicExtrema(current.y, y1, y2, y).forEach((value) => ts.add(value));
|
|
4689
|
+
for (const t of ts) {
|
|
4690
|
+
include({
|
|
4691
|
+
x: cubicAt(current.x, x1, x2, x, t),
|
|
4692
|
+
y: cubicAt(current.y, y1, y2, y, t),
|
|
4693
|
+
});
|
|
4694
|
+
}
|
|
4695
|
+
current = { x, y };
|
|
4696
|
+
break;
|
|
4697
|
+
}
|
|
4698
|
+
case "Q": {
|
|
4699
|
+
const [x1, y1, x, y] = segment.values;
|
|
4700
|
+
const ts = new Set([0, 1]);
|
|
4701
|
+
quadraticExtrema(current.x, x1, x).forEach((value) => ts.add(value));
|
|
4702
|
+
quadraticExtrema(current.y, y1, y).forEach((value) => ts.add(value));
|
|
4703
|
+
for (const t of ts) {
|
|
4704
|
+
include({
|
|
4705
|
+
x: quadraticAt(current.x, x1, x, t),
|
|
4706
|
+
y: quadraticAt(current.y, y1, y, t),
|
|
4707
|
+
});
|
|
4708
|
+
}
|
|
4709
|
+
current = { x, y };
|
|
4710
|
+
break;
|
|
4711
|
+
}
|
|
4712
|
+
case "A": {
|
|
4713
|
+
for (const point of sampleArc(current, segment.values)) {
|
|
4714
|
+
include(point);
|
|
4715
|
+
}
|
|
4716
|
+
current = { x: segment.values[5], y: segment.values[6] };
|
|
4717
|
+
break;
|
|
4718
|
+
}
|
|
4719
|
+
case "Z": {
|
|
4720
|
+
include(current);
|
|
4721
|
+
include(subpathStart);
|
|
4722
|
+
current = { ...subpathStart };
|
|
4723
|
+
break;
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
}
|
|
4727
|
+
if (!Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY)) {
|
|
4728
|
+
return null;
|
|
4729
|
+
}
|
|
4730
|
+
return { minX, minY, maxX, maxY };
|
|
4731
|
+
}
|
|
4732
|
+
function transformX(x, bounds, scaleX) {
|
|
4733
|
+
return (x - bounds.minX) * scaleX;
|
|
4734
|
+
}
|
|
4735
|
+
function transformY(y, bounds, scaleY) {
|
|
4736
|
+
return (y - bounds.minY) * scaleY;
|
|
4737
|
+
}
|
|
4738
|
+
function buildScaledPathData(segments, bounds, width, height) {
|
|
4739
|
+
const sourceWidth = Math.max(bounds.maxX - bounds.minX, EPSILON);
|
|
4740
|
+
const sourceHeight = Math.max(bounds.maxY - bounds.minY, EPSILON);
|
|
4741
|
+
const scaleX = width / sourceWidth;
|
|
4742
|
+
const scaleY = height / sourceHeight;
|
|
4743
|
+
return segments
|
|
4744
|
+
.map((segment) => {
|
|
4745
|
+
switch (segment.command) {
|
|
4746
|
+
case "M":
|
|
4747
|
+
case "L":
|
|
4748
|
+
return [
|
|
4749
|
+
segment.command,
|
|
4750
|
+
formatNumber(transformX(segment.values[0], bounds, scaleX)),
|
|
4751
|
+
formatNumber(transformY(segment.values[1], bounds, scaleY)),
|
|
4752
|
+
].join(" ");
|
|
4753
|
+
case "C":
|
|
4754
|
+
return [
|
|
4755
|
+
"C",
|
|
4756
|
+
formatNumber(transformX(segment.values[0], bounds, scaleX)),
|
|
4757
|
+
formatNumber(transformY(segment.values[1], bounds, scaleY)),
|
|
4758
|
+
formatNumber(transformX(segment.values[2], bounds, scaleX)),
|
|
4759
|
+
formatNumber(transformY(segment.values[3], bounds, scaleY)),
|
|
4760
|
+
formatNumber(transformX(segment.values[4], bounds, scaleX)),
|
|
4761
|
+
formatNumber(transformY(segment.values[5], bounds, scaleY)),
|
|
4762
|
+
].join(" ");
|
|
4763
|
+
case "Q":
|
|
4764
|
+
return [
|
|
4765
|
+
"Q",
|
|
4766
|
+
formatNumber(transformX(segment.values[0], bounds, scaleX)),
|
|
4767
|
+
formatNumber(transformY(segment.values[1], bounds, scaleY)),
|
|
4768
|
+
formatNumber(transformX(segment.values[2], bounds, scaleX)),
|
|
4769
|
+
formatNumber(transformY(segment.values[3], bounds, scaleY)),
|
|
4770
|
+
].join(" ");
|
|
4771
|
+
case "A":
|
|
4772
|
+
return [
|
|
4773
|
+
"A",
|
|
4774
|
+
formatNumber(segment.values[0] * scaleX),
|
|
4775
|
+
formatNumber(segment.values[1] * scaleY),
|
|
4776
|
+
formatNumber(segment.values[2]),
|
|
4777
|
+
formatNumber(segment.values[3]),
|
|
4778
|
+
formatNumber(segment.values[4]),
|
|
4779
|
+
formatNumber(transformX(segment.values[5], bounds, scaleX)),
|
|
4780
|
+
formatNumber(transformY(segment.values[6], bounds, scaleY)),
|
|
4781
|
+
].join(" ");
|
|
4782
|
+
case "Z":
|
|
4783
|
+
return "Z";
|
|
4784
|
+
}
|
|
4785
|
+
})
|
|
4786
|
+
.join(" ");
|
|
4787
|
+
}
|
|
4788
|
+
function intrinsicSizeFromBounds(bounds) {
|
|
4789
|
+
if (!bounds)
|
|
4790
|
+
return { width: 100, height: 100 };
|
|
4791
|
+
return {
|
|
4792
|
+
width: Math.max(1, Math.ceil(bounds.maxX - bounds.minX)),
|
|
4793
|
+
height: Math.max(1, Math.ceil(bounds.maxY - bounds.minY)),
|
|
4794
|
+
};
|
|
4795
|
+
}
|
|
4796
|
+
function parsePathGeometry(pathData) {
|
|
4797
|
+
const segments = toAbsoluteSegments(parseRawSegments(pathData));
|
|
4798
|
+
return {
|
|
4799
|
+
segments,
|
|
4800
|
+
bounds: boundsFromAbsoluteSegments(segments),
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
function getPathIntrinsicSize(pathData) {
|
|
4804
|
+
if (!pathData)
|
|
4805
|
+
return { width: 100, height: 100 };
|
|
4806
|
+
return intrinsicSizeFromBounds(parsePathGeometry(pathData).bounds);
|
|
4807
|
+
}
|
|
4808
|
+
function getRenderablePathData(pathData, width, height) {
|
|
4809
|
+
if (!pathData)
|
|
4810
|
+
return null;
|
|
4811
|
+
const { segments, bounds } = parsePathGeometry(pathData);
|
|
4812
|
+
if (!segments.length || !bounds)
|
|
4813
|
+
return pathData;
|
|
4814
|
+
return buildScaledPathData(segments, bounds, Math.max(1, width), Math.max(1, height));
|
|
4815
|
+
}
|
|
4816
|
+
function getRenderableNodePathData(node) {
|
|
4817
|
+
return getRenderablePathData(node.pathData, node.w, node.h);
|
|
4818
|
+
}
|
|
4819
|
+
|
|
4300
4820
|
const pathShape = {
|
|
4301
4821
|
size(n, labelW) {
|
|
4302
|
-
|
|
4303
|
-
const w = n.width ?? Math.max(
|
|
4822
|
+
const intrinsic = getPathIntrinsicSize(n.pathData);
|
|
4823
|
+
const w = n.width ?? Math.max(intrinsic.width, Math.min(300, labelW + 20));
|
|
4304
4824
|
n.w = w;
|
|
4305
4825
|
if (!n.h) {
|
|
4306
|
-
if (!n.width && labelW + 20 > w) {
|
|
4826
|
+
if (!n.width && !n.height && labelW + 20 > w) {
|
|
4307
4827
|
const fontSize = Number(n.style?.fontSize ?? 14);
|
|
4308
4828
|
const lines = Math.ceil(labelW / (w - 20));
|
|
4309
|
-
n.h = Math.max(
|
|
4829
|
+
n.h = Math.max(intrinsic.height, lines * fontSize * 1.5 + 20);
|
|
4310
4830
|
}
|
|
4311
4831
|
else {
|
|
4312
|
-
n.h = n.height ??
|
|
4832
|
+
n.h = n.height ?? intrinsic.height;
|
|
4313
4833
|
}
|
|
4314
4834
|
}
|
|
4315
4835
|
},
|
|
4316
4836
|
renderSVG(rc, n, _palette, opts) {
|
|
4317
|
-
const d = n
|
|
4837
|
+
const d = getRenderableNodePathData(n);
|
|
4318
4838
|
if (!d) {
|
|
4319
|
-
// No path data — render placeholder box
|
|
4320
4839
|
return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
|
|
4321
4840
|
}
|
|
4322
4841
|
const el = rc.path(d, opts);
|
|
4323
|
-
// Wrap in a group to translate the user's path to the node position
|
|
4324
4842
|
const g = document.createElementNS(SVG_NS, "g");
|
|
4325
4843
|
g.setAttribute("transform", `translate(${n.x},${n.y})`);
|
|
4326
4844
|
g.appendChild(el);
|
|
4327
4845
|
return [g];
|
|
4328
4846
|
},
|
|
4329
4847
|
renderCanvas(rc, ctx, n, _palette, opts) {
|
|
4330
|
-
const d = n
|
|
4848
|
+
const d = getRenderableNodePathData(n);
|
|
4331
4849
|
if (!d) {
|
|
4332
4850
|
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts);
|
|
4333
4851
|
return;
|
|
@@ -4739,8 +5257,10 @@ function computeBounds(sg, margin) {
|
|
|
4739
5257
|
...sg.charts.map((c) => c.y + c.h),
|
|
4740
5258
|
...sg.markdowns.map((m) => m.y + m.h),
|
|
4741
5259
|
];
|
|
4742
|
-
|
|
4743
|
-
|
|
5260
|
+
const autoWidth = (allX.length ? Math.max(...allX) : 400) + margin;
|
|
5261
|
+
const autoHeight = (allY.length ? Math.max(...allY) : 300) + margin;
|
|
5262
|
+
sg.width = sg.fixedWidth ?? autoWidth;
|
|
5263
|
+
sg.height = sg.fixedHeight ?? autoHeight;
|
|
4744
5264
|
}
|
|
4745
5265
|
// ── Public entry point ────────────────────────────────────
|
|
4746
5266
|
function layout(sg) {
|
|
@@ -7776,7 +8296,7 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
7776
8296
|
const palette = resolvePalette(themeName);
|
|
7777
8297
|
// ── Diagram-level font ──────────────────────────────────
|
|
7778
8298
|
const diagramFont = (() => {
|
|
7779
|
-
const raw = String(sg.config["font"] ?? "");
|
|
8299
|
+
const raw = String(sg.style?.font ?? sg.config["font"] ?? "");
|
|
7780
8300
|
if (raw) {
|
|
7781
8301
|
loadFont(raw);
|
|
7782
8302
|
return resolveFont(raw);
|
|
@@ -7806,8 +8326,22 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
7806
8326
|
bgRect.setAttribute("y", "0");
|
|
7807
8327
|
bgRect.setAttribute("width", String(sg.width));
|
|
7808
8328
|
bgRect.setAttribute("height", String(sg.height));
|
|
7809
|
-
bgRect.setAttribute("fill", palette.background);
|
|
8329
|
+
bgRect.setAttribute("fill", String(sg.style?.fill ?? palette.background));
|
|
7810
8330
|
svg.appendChild(bgRect);
|
|
8331
|
+
const rootStroke = sg.style?.stroke;
|
|
8332
|
+
const rootStrokeWidth = Number(sg.style?.strokeWidth ?? 0);
|
|
8333
|
+
if (rootStroke && rootStroke !== "none" && rootStrokeWidth > 0) {
|
|
8334
|
+
const frame = se("rect");
|
|
8335
|
+
const inset = rootStrokeWidth / 2;
|
|
8336
|
+
frame.setAttribute("x", String(inset));
|
|
8337
|
+
frame.setAttribute("y", String(inset));
|
|
8338
|
+
frame.setAttribute("width", String(Math.max(0, sg.width - rootStrokeWidth)));
|
|
8339
|
+
frame.setAttribute("height", String(Math.max(0, sg.height - rootStrokeWidth)));
|
|
8340
|
+
frame.setAttribute("fill", "none");
|
|
8341
|
+
frame.setAttribute("stroke", String(rootStroke));
|
|
8342
|
+
frame.setAttribute("stroke-width", String(rootStrokeWidth));
|
|
8343
|
+
svg.appendChild(frame);
|
|
8344
|
+
}
|
|
7811
8345
|
}
|
|
7812
8346
|
const rc = rough.svg(svg);
|
|
7813
8347
|
// ── Title ────────────────────────────────────────────────
|
|
@@ -7939,7 +8473,7 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
7939
8473
|
ng.dataset.w = String(n.w);
|
|
7940
8474
|
ng.dataset.h = String(n.h);
|
|
7941
8475
|
if (n.pathData)
|
|
7942
|
-
ng.dataset.pathData = n.pathData;
|
|
8476
|
+
ng.dataset.pathData = getRenderableNodePathData(n) ?? n.pathData;
|
|
7943
8477
|
if (n.meta?.animationParent)
|
|
7944
8478
|
ng.dataset.animationParent = n.meta.animationParent;
|
|
7945
8479
|
if (n.style?.opacity != null)
|
|
@@ -8535,7 +9069,7 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
8535
9069
|
const palette = resolvePalette(themeName);
|
|
8536
9070
|
// ── Diagram-level font ───────────────────────────────────
|
|
8537
9071
|
const diagramFont = (() => {
|
|
8538
|
-
const raw = String(sg.config['font'] ?? '');
|
|
9072
|
+
const raw = String(sg.style?.font ?? sg.config['font'] ?? '');
|
|
8539
9073
|
if (raw) {
|
|
8540
9074
|
loadFont(raw);
|
|
8541
9075
|
return resolveFont(raw);
|
|
@@ -8544,8 +9078,18 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
8544
9078
|
})();
|
|
8545
9079
|
// ── Background ───────────────────────────────────────────
|
|
8546
9080
|
if (!options.transparent) {
|
|
8547
|
-
ctx.fillStyle = options.background ?? palette.background;
|
|
9081
|
+
ctx.fillStyle = options.background ?? String(sg.style?.fill ?? palette.background);
|
|
8548
9082
|
ctx.fillRect(0, 0, sg.width, sg.height);
|
|
9083
|
+
const rootStroke = sg.style?.stroke;
|
|
9084
|
+
const rootStrokeWidth = Number(sg.style?.strokeWidth ?? 0);
|
|
9085
|
+
if (rootStroke && rootStroke !== 'none' && rootStrokeWidth > 0) {
|
|
9086
|
+
const inset = rootStrokeWidth / 2;
|
|
9087
|
+
ctx.save();
|
|
9088
|
+
ctx.strokeStyle = String(rootStroke);
|
|
9089
|
+
ctx.lineWidth = rootStrokeWidth;
|
|
9090
|
+
ctx.strokeRect(inset, inset, Math.max(0, sg.width - rootStrokeWidth), Math.max(0, sg.height - rootStrokeWidth));
|
|
9091
|
+
ctx.restore();
|
|
9092
|
+
}
|
|
8549
9093
|
}
|
|
8550
9094
|
else {
|
|
8551
9095
|
ctx.clearRect(0, 0, sg.width, sg.height);
|
|
@@ -9595,6 +10139,10 @@ class AnimationController {
|
|
|
9595
10139
|
if (!el.id.startsWith("group-")) {
|
|
9596
10140
|
const ids = new Set([el.id]);
|
|
9597
10141
|
this._relatedElementIdsByPrimaryId.get(el.id)?.forEach((id) => ids.add(id));
|
|
10142
|
+
this.svg.querySelectorAll(POSITIONABLE_SELECTOR).forEach((candidate) => {
|
|
10143
|
+
if (candidate.dataset.animationParent === target)
|
|
10144
|
+
ids.add(candidate.id);
|
|
10145
|
+
});
|
|
9598
10146
|
return Array.from(ids)
|
|
9599
10147
|
.map((id) => getEl(this.svg, id))
|
|
9600
10148
|
.filter((candidate) => candidate != null);
|
|
@@ -9795,6 +10343,7 @@ class AnimationController {
|
|
|
9795
10343
|
el.classList.remove("hl", "faded", "hidden");
|
|
9796
10344
|
el.style.opacity = el.style.filter = "";
|
|
9797
10345
|
if (this.drawTargetNodes.has(el.id)) {
|
|
10346
|
+
hideDrawEl(el);
|
|
9798
10347
|
prepareNodeForDraw(el);
|
|
9799
10348
|
}
|
|
9800
10349
|
else {
|
|
@@ -10293,6 +10842,15 @@ class AnimationController {
|
|
|
10293
10842
|
_doColor(target, color) {
|
|
10294
10843
|
if (!color)
|
|
10295
10844
|
return;
|
|
10845
|
+
const applyTextColor = (root) => {
|
|
10846
|
+
root.querySelectorAll("text").forEach((t) => {
|
|
10847
|
+
t.style.fill = color;
|
|
10848
|
+
const existingStyle = t.getAttribute("style") ?? "";
|
|
10849
|
+
const nextStyle = `${existingStyle.replace(/(?:^|;)\s*fill\s*:[^;]*/g, "").trim().replace(/;?$/, ";")}fill:${color};`;
|
|
10850
|
+
t.setAttribute("style", nextStyle);
|
|
10851
|
+
t.setAttribute("fill", color);
|
|
10852
|
+
});
|
|
10853
|
+
};
|
|
10296
10854
|
for (const el of this._resolveCascadeTargets(target)) {
|
|
10297
10855
|
if (parseEdgeTarget(target)) {
|
|
10298
10856
|
el.querySelectorAll("path, line, polyline").forEach((p) => {
|
|
@@ -10315,11 +10873,14 @@ class AnimationController {
|
|
|
10315
10873
|
hit = true;
|
|
10316
10874
|
});
|
|
10317
10875
|
if (!hit) {
|
|
10318
|
-
el
|
|
10319
|
-
t.style.fill = color;
|
|
10320
|
-
});
|
|
10876
|
+
applyTextColor(el);
|
|
10321
10877
|
}
|
|
10322
10878
|
}
|
|
10879
|
+
this.svg.querySelectorAll(`${POSITIONABLE_SELECTOR}[data-animation-parent]`).forEach((el) => {
|
|
10880
|
+
if (el.dataset.animationParent === target) {
|
|
10881
|
+
applyTextColor(el);
|
|
10882
|
+
}
|
|
10883
|
+
});
|
|
10323
10884
|
}
|
|
10324
10885
|
// ── narration ───────────────────────────────────────────
|
|
10325
10886
|
_initCaption() {
|