@weasel-js/labkit 1.6.0 → 1.6.1
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/dist/_dts/{CanvasStackContext-DQgcH1ny.d.ts → CanvasStackContext-Wo6OwIVM.d.ts} +1 -1
- package/dist/_dts/FloatingPanel-7j0IcXwz.d.ts +30 -0
- package/dist/_dts/{LayerList.d-2c9yup5Y.d.ts → LayerList.d-BoELAMsM.d.ts} +2 -2
- package/dist/_dts/{index-DINmCuLA.d.ts → PropertyField.d-D0ndfIIt.d.ts} +34 -83
- package/dist/_dts/{Select.d-BchGMC2Y.d.ts → Select.d-Cr8-zoup.d.ts} +4 -5
- package/dist/_dts/{usePanZoom-Bq73pwFe.d.ts → Stage-DWBBi8IU.d.ts} +80 -35
- package/dist/_dts/{index-B0--8PEM.d.ts → index-CKyX0ME_.d.ts} +12 -13
- package/dist/_dts/{frac-DTRq7Xpb.d.ts → types-B7yBTTF9.d.ts} +699 -649
- package/dist/_dts/{types-B7y8hX1D.d.ts → types-DTuViBvc.d.ts} +20 -1
- package/dist/_dts/{useTrialState-CP4jfuKl.d.ts → useTrialState-CMaV6UD4.d.ts} +2 -3
- package/dist/canvas/index.d.ts +39 -7
- package/dist/canvas/index.js +5 -3
- package/dist/chrome/index.d.ts +4 -5
- package/dist/chrome/index.js +8 -6
- package/dist/chunk-24W5VEJW.js +18 -0
- package/dist/chunk-24W5VEJW.js.map +1 -0
- package/dist/chunk-3RY6WL5T.js +38 -0
- package/dist/chunk-3RY6WL5T.js.map +1 -0
- package/dist/{chunk-H5VIV7MQ.js → chunk-4TT3PNPR.js} +60 -51
- package/dist/chunk-4TT3PNPR.js.map +1 -0
- package/dist/{chunk-ZORVNOKX.js → chunk-6FLXL3I7.js} +9 -145
- package/dist/chunk-6FLXL3I7.js.map +1 -0
- package/dist/{chunk-HFRE4QQV.js → chunk-ASOK76ZH.js} +53 -22
- package/dist/chunk-ASOK76ZH.js.map +1 -0
- package/dist/chunk-AWQG7FCC.js +19 -0
- package/dist/chunk-AWQG7FCC.js.map +1 -0
- package/dist/chunk-CGHF6RZV.js +300 -0
- package/dist/chunk-CGHF6RZV.js.map +1 -0
- package/dist/chunk-DWMSB4CA.js +228 -0
- package/dist/chunk-DWMSB4CA.js.map +1 -0
- package/dist/{chunk-USO6TERP.js → chunk-GHGO52SO.js} +11 -9
- package/dist/chunk-GHGO52SO.js.map +1 -0
- package/dist/{chunk-MOLIUNVH.js → chunk-HKV7LRHY.js} +4 -4
- package/dist/{chunk-MOLIUNVH.js.map → chunk-HKV7LRHY.js.map} +1 -1
- package/dist/{chunk-VQ47KWD5.js → chunk-IRTGUQ5O.js} +5 -4
- package/dist/chunk-IRTGUQ5O.js.map +1 -0
- package/dist/{chunk-H6ZAOWNE.js → chunk-JVBYV6TL.js} +3 -3
- package/dist/{chunk-H6ZAOWNE.js.map → chunk-JVBYV6TL.js.map} +1 -1
- package/dist/chunk-KHK5FGER.js +145 -0
- package/dist/chunk-KHK5FGER.js.map +1 -0
- package/dist/chunk-NEXR37YJ.js +144 -0
- package/dist/chunk-NEXR37YJ.js.map +1 -0
- package/dist/{chunk-BVL7Y574.js → chunk-O6FFFOHS.js} +14 -6
- package/dist/chunk-O6FFFOHS.js.map +1 -0
- package/dist/chunk-QGMOA2YE.js +731 -0
- package/dist/chunk-QGMOA2YE.js.map +1 -0
- package/dist/{chunk-ERFMC6WU.js → chunk-S4NCI32V.js} +19 -10
- package/dist/chunk-S4NCI32V.js.map +1 -0
- package/dist/{chunk-3FI7Y4R4.js → chunk-SLAQPUI4.js} +4 -4
- package/dist/{chunk-3FI7Y4R4.js.map → chunk-SLAQPUI4.js.map} +1 -1
- package/dist/{chunk-AWH3SRPW.js → chunk-TJ77A4A3.js} +3 -31
- package/dist/chunk-TJ77A4A3.js.map +1 -0
- package/dist/{chunk-WUJXJC5O.js → chunk-TYN6MLXX.js} +691 -1395
- package/dist/chunk-TYN6MLXX.js.map +1 -0
- package/dist/{chunk-RS3HRQWU.js → chunk-WPLAANH4.js} +4 -3
- package/dist/chunk-WPLAANH4.js.map +1 -0
- package/dist/config/index.d.ts +36 -35
- package/dist/config/index.js +4 -5
- package/dist/controls/index.d.ts +91 -7
- package/dist/controls/index.js +5 -4
- package/dist/dragdrop/index.d.ts +8 -6
- package/dist/dragdrop/index.js +3 -2
- package/dist/index.d.ts +50 -77
- package/dist/index.js +138 -254
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +3 -4
- package/dist/layers/index.js +2 -1
- package/dist/loupe/index.d.ts +19 -16
- package/dist/loupe/index.js +5 -3
- package/dist/overview/index.d.ts +56 -0
- package/dist/overview/index.js +288 -0
- package/dist/overview/index.js.map +1 -0
- package/dist/passthrough/weasel-ui.d.ts +38 -38
- package/dist/passthrough/weasel-ui.js +2 -1
- package/dist/primitives/index.d.ts +19 -43
- package/dist/primitives/index.js +5 -3
- package/dist/state/index.d.ts +7 -7
- package/dist/state/index.js +4 -5
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +63 -1
- package/dist/undo/index.d.ts +3 -3
- package/package.json +13 -9
- package/src/annotations/index.ts +1 -0
- package/src/annotations/store.test.ts +26 -0
- package/src/annotations/store.ts +25 -6
- package/src/annotations/toolMap.test.ts +37 -2
- package/src/annotations/toolMap.ts +13 -0
- package/src/annotations/types.ts +27 -0
- package/src/canvas/AGENTS.md +8 -10
- package/src/canvas/CameraInput.test.tsx +136 -0
- package/src/canvas/CameraInput.tsx +311 -0
- package/src/canvas/CanvasStack.tsx +42 -58
- package/src/canvas/LinkedCursor.less +16 -0
- package/src/canvas/LinkedCursor.tsx +54 -0
- package/src/canvas/Stage.tsx +42 -37
- package/src/canvas/cameraView.test.ts +60 -0
- package/src/canvas/cameraView.ts +61 -0
- package/src/canvas/index.ts +11 -2
- package/src/chrome/regions/SidebarRegion.test.tsx +9 -2
- package/src/config/builder.ts +3 -3
- package/src/config/entry.test.ts +3 -1
- package/src/config/fromConfigField.ts +1 -0
- package/src/config/index.ts +4 -1
- package/src/config/resolve.ts +11 -1
- package/src/config/sectionTree.test.ts +12 -6
- package/src/config/sectionTree.ts +15 -5
- package/src/config/types.ts +21 -0
- package/src/controls/ControlPanel.test.tsx +12 -2
- package/src/controls/ControlPanel.tsx +28 -9
- package/src/controls/inDialog.test.tsx +18 -0
- package/src/controls/inDialog.tsx +2 -10
- package/src/index.test.ts +1 -1
- package/src/index.ts +8 -4
- package/src/instrument/SineWave.smoke.test.tsx +2 -0
- package/src/instrument/types.ts +10 -1
- package/src/lab/Lab.nebula.test.tsx +11 -2
- package/src/lab/Lab.test.tsx +3 -1
- package/src/lab/Lab.tsx +4 -4
- package/src/loupe/AGENTS.md +7 -6
- package/src/loupe/LoupeGestures.tsx +14 -9
- package/src/loupe/TrialLoupe.tsx +5 -5
- package/src/loupe/index.ts +1 -0
- package/src/loupe/loupeActions.ts +6 -2
- package/src/overview/AGENTS.md +38 -0
- package/src/overview/OverviewMarks.test.tsx +100 -0
- package/src/overview/OverviewMarks.tsx +93 -0
- package/src/overview/TrialOverview.less +53 -0
- package/src/overview/TrialOverview.stories.tsx +50 -0
- package/src/overview/TrialOverview.test.tsx +116 -0
- package/src/overview/TrialOverview.tsx +374 -0
- package/src/overview/index.ts +2 -0
- package/src/passthrough/weasel-ui.ts +7 -0
- package/src/specimen/Specimen.tsx +19 -2
- package/src/state/labRecords.test.ts +4 -1
- package/src/state/store.test.ts +4 -4
- package/src/state/useTrialState.test.tsx +1 -1
- package/src/styles.less +2 -0
- package/src/theme/Interstellar.stories.tsx +1 -5
- package/src/tools/labTool.ts +6 -2
- package/src/trial/Trial.annotations.test.tsx +16 -0
- package/src/trial/Trial.loupe.test.tsx +73 -1
- package/src/trial/Trial.stories.tsx +2 -0
- package/src/trial/Trial.tsx +17 -1
- package/src/trial/trialOps.test.ts +4 -4
- package/dist/_dts/inDialog-D5ZKGjiD.d.ts +0 -23
- package/dist/_dts/number.d-OeQYQoUO.d.ts +0 -28
- package/dist/chunk-2AYGEN57.js +0 -32
- package/dist/chunk-2AYGEN57.js.map +0 -1
- package/dist/chunk-AWH3SRPW.js.map +0 -1
- package/dist/chunk-BVL7Y574.js.map +0 -1
- package/dist/chunk-ERFMC6WU.js.map +0 -1
- package/dist/chunk-H5VIV7MQ.js.map +0 -1
- package/dist/chunk-HFRE4QQV.js.map +0 -1
- package/dist/chunk-RS3HRQWU.js.map +0 -1
- package/dist/chunk-TT5C5S6R.js +0 -368
- package/dist/chunk-TT5C5S6R.js.map +0 -1
- package/dist/chunk-USO6TERP.js.map +0 -1
- package/dist/chunk-VQ47KWD5.js.map +0 -1
- package/dist/chunk-WUJXJC5O.js.map +0 -1
- package/dist/chunk-ZORVNOKX.js.map +0 -1
- package/src/canvas/usePanZoom.test.ts +0 -244
- package/src/canvas/usePanZoom.ts +0 -147
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import { resolveScreenLength, SCRIPT_METRICS, boundsOfPath, getMarker, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, resolveAlign, getPaintKind } from '@weasel-js/core';
|
|
2
|
+
import { tileGeometry } from '@weasel-js/core/patterns-builtin';
|
|
3
|
+
|
|
4
|
+
// ../svg/dist/index.js
|
|
5
|
+
|
|
6
|
+
// ../geom/dist/index.js
|
|
7
|
+
function identity() {
|
|
8
|
+
return [1, 0, 0, 1, 0, 0];
|
|
9
|
+
}
|
|
10
|
+
function multiply(m, n) {
|
|
11
|
+
return [
|
|
12
|
+
m[0] * n[0] + m[2] * n[1],
|
|
13
|
+
m[1] * n[0] + m[3] * n[1],
|
|
14
|
+
m[0] * n[2] + m[2] * n[3],
|
|
15
|
+
m[1] * n[2] + m[3] * n[3],
|
|
16
|
+
m[0] * n[4] + m[2] * n[5] + m[4],
|
|
17
|
+
m[1] * n[4] + m[3] * n[5] + m[5]
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
var IDENTITY_MATRIX = identity();
|
|
21
|
+
function isIdentity(m, eps = 1e-9) {
|
|
22
|
+
return Math.abs(m[0] - 1) < eps && Math.abs(m[1]) < eps && Math.abs(m[2]) < eps && Math.abs(m[3] - 1) < eps && Math.abs(m[4]) < eps && Math.abs(m[5]) < eps;
|
|
23
|
+
}
|
|
24
|
+
function formatMatrix(m) {
|
|
25
|
+
if (isIdentity(m)) return null;
|
|
26
|
+
return `matrix(${m.map((n) => trimNumber(n)).join(" ")})`;
|
|
27
|
+
}
|
|
28
|
+
function trimNumber(n) {
|
|
29
|
+
if (!Number.isFinite(n)) return "0";
|
|
30
|
+
const r = Math.round(n * 1e6) / 1e6;
|
|
31
|
+
if (Number.isInteger(r)) return String(r);
|
|
32
|
+
return String(r);
|
|
33
|
+
}
|
|
34
|
+
function anchorOffset(style, width) {
|
|
35
|
+
const edge = resolveAlign(style?.align ?? "left", style?.direction ?? "ltr");
|
|
36
|
+
return edge === "center" ? width / 2 : edge === "right" ? width : 0;
|
|
37
|
+
}
|
|
38
|
+
function patternSpecOf(paint) {
|
|
39
|
+
return "tile" in paint.pattern ? paint.pattern : null;
|
|
40
|
+
}
|
|
41
|
+
function patternXml(id, paint, onWarn) {
|
|
42
|
+
const spec = patternSpecOf(paint);
|
|
43
|
+
if (!spec) {
|
|
44
|
+
onWarn?.("pattern fill carries a TextureHandle, which has no vector form \u2014 omitted from <defs>");
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
const geometry = tileGeometry(spec);
|
|
48
|
+
const body = geometry.shapes.map(shapeXml).join("");
|
|
49
|
+
const origin = paint.origin ?? { x: 0, y: 0 };
|
|
50
|
+
const attrs = [
|
|
51
|
+
`id="${id}"`,
|
|
52
|
+
'patternUnits="userSpaceOnUse"',
|
|
53
|
+
`width="${trimNumber(geometry.size)}"`,
|
|
54
|
+
`height="${trimNumber(geometry.size)}"`
|
|
55
|
+
];
|
|
56
|
+
if (origin.x !== 0 || origin.y !== 0) {
|
|
57
|
+
attrs.push(`x="${trimNumber(origin.x)}"`, `y="${trimNumber(origin.y)}"`);
|
|
58
|
+
}
|
|
59
|
+
attrs.push(`data-weasel-tile="${escapeAttr(JSON.stringify(spec))}"`);
|
|
60
|
+
return `<pattern ${attrs.join(" ")}>${body}</pattern>`;
|
|
61
|
+
}
|
|
62
|
+
function tilePreviewSvg(spec, background) {
|
|
63
|
+
const geometry = tileGeometry(spec);
|
|
64
|
+
const s = trimNumber(geometry.size);
|
|
65
|
+
const bg = background ? `<rect width="${s}" height="${s}" fill="${background}"/>` : "";
|
|
66
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 ${s} ${s}">${bg}${geometry.shapes.map(shapeXml).join("")}</svg>`;
|
|
67
|
+
}
|
|
68
|
+
function tilePreviewCssUrl(spec, background) {
|
|
69
|
+
return `url("data:image/svg+xml,${encodeURIComponent(tilePreviewSvg(spec, background))}")`;
|
|
70
|
+
}
|
|
71
|
+
function shapeXml(shape) {
|
|
72
|
+
switch (shape.kind) {
|
|
73
|
+
case "line":
|
|
74
|
+
return `<line x1="${trimNumber(shape.x1)}" y1="${trimNumber(shape.y1)}" x2="${trimNumber(shape.x2)}" y2="${trimNumber(shape.y2)}" stroke="${shape.color}" stroke-width="${trimNumber(shape.width)}"/>`;
|
|
75
|
+
case "circle":
|
|
76
|
+
return `<circle cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" r="${trimNumber(shape.r)}" fill="${shape.color}"/>`;
|
|
77
|
+
case "ellipse": {
|
|
78
|
+
const deg = shape.rotation * 180 / Math.PI;
|
|
79
|
+
const transform = deg === 0 ? "" : ` transform="rotate(${trimNumber(deg)} ${trimNumber(shape.cx)} ${trimNumber(shape.cy)})"`;
|
|
80
|
+
return `<ellipse cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" rx="${trimNumber(shape.rx)}" ry="${trimNumber(shape.ry)}" fill="${shape.color}"${transform}/>`;
|
|
81
|
+
}
|
|
82
|
+
case "rect":
|
|
83
|
+
return `<rect x="${trimNumber(shape.x)}" y="${trimNumber(shape.y)}" width="${trimNumber(shape.width)}" height="${trimNumber(shape.height)}" fill="${shape.color}"/>`;
|
|
84
|
+
default:
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function escapeAttr(s) {
|
|
89
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
90
|
+
}
|
|
91
|
+
function serializePathD(path) {
|
|
92
|
+
if (path.kind === "rect") {
|
|
93
|
+
const { x, y, width, height } = path;
|
|
94
|
+
return `M${trimNumber(x)} ${trimNumber(y)}h${trimNumber(width)}v${trimNumber(height)}h${trimNumber(-width)}Z`;
|
|
95
|
+
}
|
|
96
|
+
const cmds = path.commands;
|
|
97
|
+
const xs = path.coords;
|
|
98
|
+
const out = [];
|
|
99
|
+
let ci = 0;
|
|
100
|
+
for (let i = 0; i < cmds.length; i++) {
|
|
101
|
+
const c = cmds[i];
|
|
102
|
+
if (c === PATH_M) {
|
|
103
|
+
out.push(`M${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])}`);
|
|
104
|
+
ci += 2;
|
|
105
|
+
} else if (c === PATH_L) {
|
|
106
|
+
out.push(`L${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])}`);
|
|
107
|
+
ci += 2;
|
|
108
|
+
} else if (c === PATH_C) {
|
|
109
|
+
out.push(
|
|
110
|
+
`C${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])} ${trimNumber(xs[ci + 2])} ${trimNumber(xs[ci + 3])} ${trimNumber(xs[ci + 4])} ${trimNumber(xs[ci + 5])}`
|
|
111
|
+
);
|
|
112
|
+
ci += 6;
|
|
113
|
+
} else if (c === PATH_Q) {
|
|
114
|
+
out.push(
|
|
115
|
+
`Q${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])} ${trimNumber(xs[ci + 2])} ${trimNumber(xs[ci + 3])}`
|
|
116
|
+
);
|
|
117
|
+
ci += 4;
|
|
118
|
+
} else if (c === PATH_Z) {
|
|
119
|
+
out.push("Z");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out.join("");
|
|
123
|
+
}
|
|
124
|
+
var WEASEL_NS = "urn:weasel-js:svg";
|
|
125
|
+
var WEASEL_NS_PREFIX = "wzl";
|
|
126
|
+
function hasNativeSvgForm(paint) {
|
|
127
|
+
const kind = paint.fill ?? "solid";
|
|
128
|
+
return kind === "solid" || kind === "linear-gradient" || kind === "radial-gradient" || kind === "pattern";
|
|
129
|
+
}
|
|
130
|
+
var INTERPOLATE_SPACES = /* @__PURE__ */ new Set(["rgb", "oklab", "oklch"]);
|
|
131
|
+
function interpolateAttr(space) {
|
|
132
|
+
if (!space || space === "rgb") return "";
|
|
133
|
+
return ` ${WEASEL_NS_PREFIX}:interpolate="${space}"`;
|
|
134
|
+
}
|
|
135
|
+
function usesInterpolateAttr(paint) {
|
|
136
|
+
const space = paint.interpolate;
|
|
137
|
+
return space != null && space !== "rgb" && INTERPOLATE_SPACES.has(space);
|
|
138
|
+
}
|
|
139
|
+
var PaintServerRegistry = class {
|
|
140
|
+
byPaint = /* @__PURE__ */ new Map();
|
|
141
|
+
order = [];
|
|
142
|
+
counter = 0;
|
|
143
|
+
// Marker keys referenced by any stroke, in first-use order. The `<defs>`
|
|
144
|
+
// id is the marker key itself, not a minted counter id: `parseSvg` reads a
|
|
145
|
+
// `marker-end="url(#id)"` fragment back as the marker key directly when the
|
|
146
|
+
// registry knows it, so a synthetic id would come back as a stranger.
|
|
147
|
+
markerKeys = [];
|
|
148
|
+
markerKeySet = /* @__PURE__ */ new Set();
|
|
149
|
+
// Clip paths, in first-use order. Identity, not value: two groups clipped by
|
|
150
|
+
// equal outlines get a def each, which costs bytes and misleads nobody.
|
|
151
|
+
clips = [];
|
|
152
|
+
clipIds = /* @__PURE__ */ new Map();
|
|
153
|
+
register(paint) {
|
|
154
|
+
const existing = this.byPaint.get(paint);
|
|
155
|
+
if (existing) return existing;
|
|
156
|
+
const id = paint.fill === "pattern" ? `pat${this.counter++}` : `grad${this.counter++}`;
|
|
157
|
+
this.byPaint.set(paint, id);
|
|
158
|
+
this.order.push(paint);
|
|
159
|
+
return id;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The `fill` / `stroke` attribute value referencing this paint server: a
|
|
163
|
+
* `url(#id)` reference, plus the fallback paint SVG's own grammar allows
|
|
164
|
+
* after one for any kind SVG cannot express. A renderer that cannot resolve
|
|
165
|
+
* the reference takes the fallback instead of dropping the fill, which is
|
|
166
|
+
* the difference between a flat shape and a vanished one.
|
|
167
|
+
*/
|
|
168
|
+
ref(paint) {
|
|
169
|
+
const id = this.register(paint);
|
|
170
|
+
if (hasNativeSvgForm(paint)) return `url(#${id})`;
|
|
171
|
+
return `url(#${id}) ${paintFallback(paint)}`;
|
|
172
|
+
}
|
|
173
|
+
/** Whether any registered paint serializes outside SVG's own vocabulary, so
|
|
174
|
+
* the root has to declare {@link WEASEL_NS}. */
|
|
175
|
+
usesPrivateNamespace() {
|
|
176
|
+
return this.order.some((paint) => !hasNativeSvgForm(paint) || usesInterpolateAttr(paint));
|
|
177
|
+
}
|
|
178
|
+
/** The `<defs>` id for a marker key — the key itself, minting nothing.
|
|
179
|
+
* `undefined` when nothing is registered under `key`, so a caller emits no
|
|
180
|
+
* attribute at all rather than a `url(#…)` pointing at a def that will
|
|
181
|
+
* never be written. */
|
|
182
|
+
markerId(key) {
|
|
183
|
+
if (getMarker(key) === void 0) return void 0;
|
|
184
|
+
if (!this.markerKeySet.has(key)) {
|
|
185
|
+
this.markerKeySet.add(key);
|
|
186
|
+
this.markerKeys.push(key);
|
|
187
|
+
}
|
|
188
|
+
return key;
|
|
189
|
+
}
|
|
190
|
+
/** Mint (or recall) the `<defs>` id for a clip outline. */
|
|
191
|
+
clipId(path) {
|
|
192
|
+
const existing = this.clipIds.get(path);
|
|
193
|
+
if (existing) return existing;
|
|
194
|
+
const id = `clip${this.clips.length}`;
|
|
195
|
+
this.clipIds.set(path, id);
|
|
196
|
+
this.clips.push(path);
|
|
197
|
+
return id;
|
|
198
|
+
}
|
|
199
|
+
/** Emit `<defs>...</defs>` XML for every registered paint server, marker
|
|
200
|
+
* and clip path. */
|
|
201
|
+
toDefsXml(onWarn) {
|
|
202
|
+
if (this.order.length === 0 && this.markerKeys.length === 0 && this.clips.length === 0) {
|
|
203
|
+
return "";
|
|
204
|
+
}
|
|
205
|
+
const parts = ["<defs>"];
|
|
206
|
+
for (const paint of this.order) {
|
|
207
|
+
parts.push(paintServerXml(this.byPaint.get(paint), paint, onWarn));
|
|
208
|
+
}
|
|
209
|
+
for (const key of this.markerKeys) {
|
|
210
|
+
const entry = getMarker(key);
|
|
211
|
+
if (!entry) continue;
|
|
212
|
+
parts.push(entry.toSvg ? entry.toSvg(key, entry) : defaultMarkerXml(key, entry, onWarn));
|
|
213
|
+
}
|
|
214
|
+
for (const path of this.clips) {
|
|
215
|
+
parts.push(
|
|
216
|
+
`<clipPath id="${this.clipIds.get(path)}"><path d="${serializePathD(path)}"/></clipPath>`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
parts.push("</defs>");
|
|
220
|
+
return parts.join("");
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
function defaultMarkerXml(id, entry, onWarn) {
|
|
224
|
+
const path = entry.path({ size: 1, stroke: { paint: { fill: "solid", color: "#000" } } });
|
|
225
|
+
const d = serializePathD(path);
|
|
226
|
+
const fill = markerPaintAttrs("fill", entry.fill, id, onWarn);
|
|
227
|
+
const outline = entry.outline ? ` ${markerPaintAttrs("stroke", entry.outline.paint, id, onWarn)} stroke-width="${trimNumber(entry.outline.width)}" stroke-linecap="round" stroke-linejoin="round"` : "";
|
|
228
|
+
const orient = typeof entry.orient === "number" ? trimNumber(entry.orient * 180 / Math.PI) : "auto-start-reverse";
|
|
229
|
+
return `<marker id="${id}" markerUnits="strokeWidth" markerWidth="8" markerHeight="8" refX="0" refY="0" orient="${orient}" overflow="visible"><path d="${d}" ${fill}${outline}/></marker>`;
|
|
230
|
+
}
|
|
231
|
+
function markerPaintAttrs(attr, paint, id, onWarn) {
|
|
232
|
+
if (paint === "none") return `${attr}="none"`;
|
|
233
|
+
if (paint === void 0 || paint === "line") return `${attr}="context-stroke"`;
|
|
234
|
+
if ("color" in paint && (paint.fill === void 0 || paint.fill === "solid")) {
|
|
235
|
+
return paint.opacity != null && paint.opacity !== 1 ? `${attr}="${paint.color}" ${attr}-opacity="${trimNumber(paint.opacity)}"` : `${attr}="${paint.color}"`;
|
|
236
|
+
}
|
|
237
|
+
onWarn?.(`marker "${id}" ${attr} is a paint server, which a <marker> def cannot reference here; written as the line's paint`);
|
|
238
|
+
return `${attr}="context-stroke"`;
|
|
239
|
+
}
|
|
240
|
+
function paintServerXml(id, paint, onWarn) {
|
|
241
|
+
if (paint.fill === "pattern") return patternXml(id, paint, onWarn);
|
|
242
|
+
const builtin = gradientXml(id, paint);
|
|
243
|
+
if (builtin) return builtin;
|
|
244
|
+
const custom = getPaintKind(paint.fill)?.toSvg?.(id, paint);
|
|
245
|
+
if (custom) return custom;
|
|
246
|
+
onWarn?.(`${paint.fill} fill has no vector form \u2014 omitted from <defs>`);
|
|
247
|
+
return "";
|
|
248
|
+
}
|
|
249
|
+
function gradientUnitsAttr(units) {
|
|
250
|
+
return units === "bounds" ? "objectBoundingBox" : "userSpaceOnUse";
|
|
251
|
+
}
|
|
252
|
+
function gradientXml(id, paint) {
|
|
253
|
+
if (paint.fill === "linear-gradient") {
|
|
254
|
+
const stops = paint.stops.map(stopXml).join("");
|
|
255
|
+
return `<linearGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" x1="${trimNumber(paint.from.x)}" y1="${trimNumber(paint.from.y)}" x2="${trimNumber(paint.to.x)}" y2="${trimNumber(paint.to.y)}"${interpolateAttr(paint.interpolate)}>${stops}</linearGradient>`;
|
|
256
|
+
}
|
|
257
|
+
if (paint.fill === "conic-gradient") {
|
|
258
|
+
const stops = paint.stops.map(stopXml).join("");
|
|
259
|
+
const P = WEASEL_NS_PREFIX;
|
|
260
|
+
return `<${P}:conicGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" cx="${trimNumber(paint.center.x)}" cy="${trimNumber(paint.center.y)}" angle="${trimNumber(paint.angle)}"${interpolateAttr(paint.interpolate)}>${stops}</${P}:conicGradient>`;
|
|
261
|
+
}
|
|
262
|
+
if (paint.fill === "radial-gradient") {
|
|
263
|
+
const stops = paint.stops.map(stopXml).join("");
|
|
264
|
+
return `<radialGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" cx="${trimNumber(paint.center.x)}" cy="${trimNumber(paint.center.y)}" r="${trimNumber(paint.radius)}"${interpolateAttr(paint.interpolate)}>${stops}</radialGradient>`;
|
|
265
|
+
}
|
|
266
|
+
return "";
|
|
267
|
+
}
|
|
268
|
+
function paintFallback(paint) {
|
|
269
|
+
const color = getPaintKind(paint.fill)?.colorOf(paint);
|
|
270
|
+
if (!color) return "none";
|
|
271
|
+
const packed = /^#([0-9a-f]{6})[0-9a-f]{2}$/i.exec(color);
|
|
272
|
+
return packed ? `#${packed[1].toLowerCase()}` : color;
|
|
273
|
+
}
|
|
274
|
+
function stopXml(s) {
|
|
275
|
+
const m = /^#([0-9a-f]{6})([0-9a-f]{2})$/i.exec(s.color);
|
|
276
|
+
if (m) {
|
|
277
|
+
const alpha = parseInt(m[2], 16) / 255;
|
|
278
|
+
return `<stop offset="${trimNumber(s.offset)}" stop-color="#${m[1].toLowerCase()}" stop-opacity="${trimNumber(alpha)}"/>`;
|
|
279
|
+
}
|
|
280
|
+
return `<stop offset="${trimNumber(s.offset)}" stop-color="${s.color}"/>`;
|
|
281
|
+
}
|
|
282
|
+
function serializeSvg(nodes, opts = {}) {
|
|
283
|
+
const registry = new PaintServerRegistry();
|
|
284
|
+
registerPaintServers(nodes, registry);
|
|
285
|
+
const bounds = opts.viewBox ?? computeBounds(nodes);
|
|
286
|
+
const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
|
|
287
|
+
const namespaces = opts.namespaces ?? {};
|
|
288
|
+
const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
|
|
289
|
+
if (registry.usesPrivateNamespace() && namespaces[WEASEL_NS_PREFIX] == null) {
|
|
290
|
+
rootAttrs.push(`xmlns:${WEASEL_NS_PREFIX}="${WEASEL_NS}"`);
|
|
291
|
+
}
|
|
292
|
+
for (const [prefix, uri] of Object.entries(namespaces)) {
|
|
293
|
+
rootAttrs.push(`xmlns:${prefix}="${escapeAttr2(uri)}"`);
|
|
294
|
+
}
|
|
295
|
+
rootAttrs.push(`viewBox="${vb}"`);
|
|
296
|
+
if (opts.width != null) rootAttrs.push(`width="${trimNumber(opts.width)}"`);
|
|
297
|
+
if (opts.height != null) rootAttrs.push(`height="${trimNumber(opts.height)}"`);
|
|
298
|
+
if (opts.documentMeta) {
|
|
299
|
+
for (const prefix of Object.keys(namespaces)) {
|
|
300
|
+
const bucket = opts.documentMeta[prefix];
|
|
301
|
+
if (!bucket?.attrs) continue;
|
|
302
|
+
for (const [name, value] of Object.entries(bucket.attrs)) {
|
|
303
|
+
rootAttrs.push(`${prefix}:${name}="${escapeAttr2(value)}"`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
let docMetaXml = "";
|
|
308
|
+
if (opts.documentMeta) {
|
|
309
|
+
for (const prefix of Object.keys(namespaces)) {
|
|
310
|
+
const bucket = opts.documentMeta[prefix];
|
|
311
|
+
if (!bucket?.elements) continue;
|
|
312
|
+
for (const [localName2, list] of Object.entries(bucket.elements)) {
|
|
313
|
+
for (const el of list) docMetaXml += namespacedElementXml(prefix, localName2, el);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const defsXml = registry.toDefsXml(opts.onWarn);
|
|
318
|
+
const said = /* @__PURE__ */ new Set();
|
|
319
|
+
const warn = (m) => {
|
|
320
|
+
if (!opts.onWarn || said.has(m)) return;
|
|
321
|
+
said.add(m);
|
|
322
|
+
opts.onWarn(m);
|
|
323
|
+
};
|
|
324
|
+
const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces, warn)).join("");
|
|
325
|
+
const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
|
|
326
|
+
return `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
|
|
327
|
+
}
|
|
328
|
+
function namespacedElementXml(prefix, localName2, el) {
|
|
329
|
+
const attrs = [];
|
|
330
|
+
for (const [name, value] of Object.entries(el.attrs)) {
|
|
331
|
+
attrs.push(`${name}="${escapeAttr2(value)}"`);
|
|
332
|
+
}
|
|
333
|
+
const head = attrs.length > 0 ? `<${prefix}:${localName2} ${attrs.join(" ")}>` : `<${prefix}:${localName2}>`;
|
|
334
|
+
let body = "";
|
|
335
|
+
if (el.children) {
|
|
336
|
+
for (const [childName, list] of Object.entries(el.children)) {
|
|
337
|
+
for (const child of list) body += namespacedElementXml(prefix, childName, child);
|
|
338
|
+
}
|
|
339
|
+
} else if (el.text != null) {
|
|
340
|
+
body = escapeText(el.text);
|
|
341
|
+
}
|
|
342
|
+
return `${head}${body}</${prefix}:${localName2}>`;
|
|
343
|
+
}
|
|
344
|
+
function metaAttrsXml(meta, namespaces) {
|
|
345
|
+
if (!meta) return "";
|
|
346
|
+
const parts = [];
|
|
347
|
+
for (const prefix of Object.keys(namespaces)) {
|
|
348
|
+
const bucket = meta[prefix];
|
|
349
|
+
if (!bucket?.attrs) continue;
|
|
350
|
+
for (const [name, value] of Object.entries(bucket.attrs)) {
|
|
351
|
+
parts.push(`${prefix}:${name}="${escapeAttr2(value)}"`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return parts.length > 0 ? ` ${parts.join(" ")}` : "";
|
|
355
|
+
}
|
|
356
|
+
function metaElementsXml(meta, namespaces) {
|
|
357
|
+
if (!meta) return "";
|
|
358
|
+
let out = "";
|
|
359
|
+
for (const prefix of Object.keys(namespaces)) {
|
|
360
|
+
const bucket = meta[prefix];
|
|
361
|
+
if (!bucket?.elements) continue;
|
|
362
|
+
for (const [localName2, list] of Object.entries(bucket.elements)) {
|
|
363
|
+
for (const el of list) out += namespacedElementXml(prefix, localName2, el);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
function registerPaintServers(nodes, registry) {
|
|
369
|
+
for (const n of nodes) {
|
|
370
|
+
if (n.kind === "group") {
|
|
371
|
+
if (n.clip) registry.clipId(n.clip);
|
|
372
|
+
registerPaintServers(n.children, registry);
|
|
373
|
+
} else if (n.kind === "path") {
|
|
374
|
+
if (n.fill.kind === "gradient") registry.register(n.fill.paint);
|
|
375
|
+
if (n.stroke && n.stroke.paint.kind === "gradient") registry.register(n.stroke.paint.paint);
|
|
376
|
+
registerMarkers(n.stroke, registry);
|
|
377
|
+
} else if (n.kind === "text") {
|
|
378
|
+
registerTextPaint(n.fill, registry);
|
|
379
|
+
registerTextPaint(n.stroke?.paint, registry);
|
|
380
|
+
registerMarkers(n.stroke, registry);
|
|
381
|
+
for (const run of n.runs ?? []) {
|
|
382
|
+
registerTextPaint(run.fill, registry);
|
|
383
|
+
registerTextPaint(run.stroke?.paint, registry);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function registerTextPaint(paint, registry) {
|
|
389
|
+
if (paint && !("color" in paint)) registry.register(paint);
|
|
390
|
+
}
|
|
391
|
+
function markerKeyOfRef(ref) {
|
|
392
|
+
if (typeof ref === "string") return ref;
|
|
393
|
+
if (ref && typeof ref === "object" && "key" in ref) return String(ref.key);
|
|
394
|
+
return void 0;
|
|
395
|
+
}
|
|
396
|
+
function registerMarkers(stroke, registry) {
|
|
397
|
+
if (!stroke) return;
|
|
398
|
+
for (const ref of [stroke.markerStart, stroke.markerMid, stroke.markerEnd]) {
|
|
399
|
+
const key = markerKeyOfRef(ref);
|
|
400
|
+
if (key) registry.markerId(key);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function nodeXml(node, registry, namespaces, warn) {
|
|
404
|
+
if (node.kind === "group") return groupXml(node, registry, namespaces, warn);
|
|
405
|
+
if (node.kind === "text") return textXml(node, registry, namespaces, warn);
|
|
406
|
+
if (node.kind === "image") return imageXml(node, namespaces);
|
|
407
|
+
return pathXml(node, registry, namespaces, warn);
|
|
408
|
+
}
|
|
409
|
+
function warnStrokeAlign(align, warn) {
|
|
410
|
+
if (align && align !== "center") {
|
|
411
|
+
warn(`a stroke aligned "${align}" of its edge is written centered: SVG has no stroke alignment`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function imageXml(node, namespaces) {
|
|
415
|
+
const box = `x="${trimNumber(node.x)}" y="${trimNumber(node.y)}" width="${trimNumber(node.width)}" height="${trimNumber(node.height)}"`;
|
|
416
|
+
const outer = [];
|
|
417
|
+
if (node.opacity != null && node.opacity !== 1) {
|
|
418
|
+
outer.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
419
|
+
}
|
|
420
|
+
if (node.rotation != null && node.rotation !== 0) {
|
|
421
|
+
const cx = node.x + node.width / 2;
|
|
422
|
+
const cy = node.y + node.height / 2;
|
|
423
|
+
const deg = node.rotation * 180 / Math.PI;
|
|
424
|
+
outer.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
|
|
425
|
+
}
|
|
426
|
+
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
427
|
+
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
428
|
+
const href = `href="${escapeAttr2(node.href)}"`;
|
|
429
|
+
if (!node.source && !node.flipX && !node.flipY) {
|
|
430
|
+
const attrs = [href, box, 'preserveAspectRatio="none"', ...outer].join(" ");
|
|
431
|
+
if (metaEls) return `<image ${attrs}${metaAttrs}>${metaEls}</image>`;
|
|
432
|
+
return `<image ${attrs}${metaAttrs}/>`;
|
|
433
|
+
}
|
|
434
|
+
const src = node.source ?? { x: 0, y: 0, width: 1, height: 1 };
|
|
435
|
+
const viewBox = [src.x, src.y, src.width, src.height].map(trimNumber).join(" ");
|
|
436
|
+
const flip = node.flipX || node.flipY ? ` transform="matrix(${node.flipX ? -1 : 1} 0 0 ${node.flipY ? -1 : 1} ${trimNumber(node.flipX ? 2 * src.x + src.width : 0)} ${trimNumber(node.flipY ? 2 * src.y + src.height : 0)})"` : "";
|
|
437
|
+
const head = ['<g data-weasel-image="true"', ...outer].join(" ");
|
|
438
|
+
return `${head}${metaAttrs}><svg ${box} viewBox="${viewBox}" preserveAspectRatio="none"><image ${href} x="0" y="0" width="1" height="1" preserveAspectRatio="none"${flip}/></svg>${metaEls}</g>`;
|
|
439
|
+
}
|
|
440
|
+
function groupXml(node, registry, namespaces, warn) {
|
|
441
|
+
const attrs = [];
|
|
442
|
+
if (node.transform) {
|
|
443
|
+
const m = formatMatrix(node.transform);
|
|
444
|
+
if (m) attrs.push(`transform="${m}"`);
|
|
445
|
+
}
|
|
446
|
+
if (node.clip) {
|
|
447
|
+
attrs.push(`clip-path="url(#${registry.clipId(node.clip)})"`);
|
|
448
|
+
}
|
|
449
|
+
if (node.opacity != null && node.opacity !== 1) {
|
|
450
|
+
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
451
|
+
}
|
|
452
|
+
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
453
|
+
const head = attrs.length > 0 ? `<g ${attrs.join(" ")}${metaAttrs}>` : `<g${metaAttrs}>`;
|
|
454
|
+
const body = node.children.map((c) => nodeXml(c, registry, namespaces, warn)).join("");
|
|
455
|
+
return `${head}${body}${metaElementsXml(node.meta, namespaces)}</g>`;
|
|
456
|
+
}
|
|
457
|
+
function pathXml(node, registry, namespaces, warn) {
|
|
458
|
+
const attrs = [`d="${serializePathD(node.path)}"`];
|
|
459
|
+
const fillAttrs = paintAttrs(node.fill, "fill", registry);
|
|
460
|
+
for (const a of fillAttrs) attrs.push(a);
|
|
461
|
+
if (node.path.kind === "polygon" && node.path.fillRule === "evenodd") {
|
|
462
|
+
attrs.push(`fill-rule="evenodd"`);
|
|
463
|
+
}
|
|
464
|
+
if (node.stroke) {
|
|
465
|
+
const strokeAttrs = strokeAttrsFor(node.stroke, registry, warn);
|
|
466
|
+
for (const a of strokeAttrs) attrs.push(a);
|
|
467
|
+
} else {
|
|
468
|
+
attrs.push('stroke="none"');
|
|
469
|
+
}
|
|
470
|
+
if (node.opacity != null && node.opacity !== 1) {
|
|
471
|
+
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
472
|
+
}
|
|
473
|
+
if (node.rotation != null && node.rotation !== 0) {
|
|
474
|
+
const b = pathBounds(node.path);
|
|
475
|
+
const cx = b.minX + (b.maxX - b.minX) / 2;
|
|
476
|
+
const cy = b.minY + (b.maxY - b.minY) / 2;
|
|
477
|
+
const deg = node.rotation * 180 / Math.PI;
|
|
478
|
+
attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
|
|
479
|
+
}
|
|
480
|
+
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
481
|
+
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
482
|
+
if (metaEls) {
|
|
483
|
+
return `<path ${attrs.join(" ")}${metaAttrs}>${metaEls}</path>`;
|
|
484
|
+
}
|
|
485
|
+
return `<path ${attrs.join(" ")}${metaAttrs}/>`;
|
|
486
|
+
}
|
|
487
|
+
function paintAttrs(paint, name, registry, includeOpacity = true) {
|
|
488
|
+
if (paint.kind === "none") return [`${name}="none"`];
|
|
489
|
+
if (paint.kind === "solid") {
|
|
490
|
+
const out = [`${name}="${paint.color}"`];
|
|
491
|
+
if (includeOpacity && paint.opacity != null && paint.opacity !== 1) {
|
|
492
|
+
out.push(`${name}-opacity="${trimNumber(paint.opacity)}"`);
|
|
493
|
+
}
|
|
494
|
+
return out;
|
|
495
|
+
}
|
|
496
|
+
return [`${name}="${registry.ref(paint.paint)}"`];
|
|
497
|
+
}
|
|
498
|
+
function strokeWidthAttrs(width) {
|
|
499
|
+
if (typeof width === "number") return [`stroke-width="${trimNumber(width)}"`];
|
|
500
|
+
return [`stroke-width="${trimNumber(width.px)}"`, 'vector-effect="non-scaling-stroke"'];
|
|
501
|
+
}
|
|
502
|
+
function coreStrokeAttrs(stroke, registry, warn) {
|
|
503
|
+
const paint = stroke?.paint;
|
|
504
|
+
if (!stroke || !paint) return [];
|
|
505
|
+
const width = stroke.width ?? 1;
|
|
506
|
+
if (!((typeof width === "object" ? width.px : width) > 0)) return [];
|
|
507
|
+
warnStrokeAlign(stroke.align, warn);
|
|
508
|
+
const attrs = [];
|
|
509
|
+
if ("color" in paint) {
|
|
510
|
+
attrs.push(`stroke="${paint.color}"`);
|
|
511
|
+
if (paint.opacity != null && paint.opacity !== 1) {
|
|
512
|
+
attrs.push(`stroke-opacity="${trimNumber(paint.opacity)}"`);
|
|
513
|
+
}
|
|
514
|
+
} else {
|
|
515
|
+
attrs.push(`stroke="${registry.ref(paint)}"`);
|
|
516
|
+
}
|
|
517
|
+
attrs.push(...strokeWidthAttrs(width));
|
|
518
|
+
if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
|
|
519
|
+
if (stroke.join) attrs.push(`stroke-linejoin="${stroke.join}"`);
|
|
520
|
+
if (stroke.dash && stroke.dash.length > 0) {
|
|
521
|
+
attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
|
|
522
|
+
}
|
|
523
|
+
if (stroke.miterLimit != null) attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
|
|
524
|
+
for (const [field, attr] of [
|
|
525
|
+
["markerStart", "marker-start"],
|
|
526
|
+
["markerMid", "marker-mid"],
|
|
527
|
+
["markerEnd", "marker-end"]
|
|
528
|
+
]) {
|
|
529
|
+
const ref = stroke[field];
|
|
530
|
+
if (ref === void 0) continue;
|
|
531
|
+
const key = typeof ref === "string" ? ref : ref.key;
|
|
532
|
+
const id = registry.markerId(key);
|
|
533
|
+
if (id === void 0) continue;
|
|
534
|
+
attrs.push(`${attr}="url(#${id})"`);
|
|
535
|
+
}
|
|
536
|
+
return attrs;
|
|
537
|
+
}
|
|
538
|
+
function strokeAttrsFor(stroke, registry, warn) {
|
|
539
|
+
warnStrokeAlign(stroke.align, warn);
|
|
540
|
+
const attrs = paintAttrs(stroke.paint, "stroke", registry, false);
|
|
541
|
+
attrs.push(...strokeWidthAttrs(stroke.width));
|
|
542
|
+
const opacity = stroke.opacity ?? (stroke.paint.kind === "solid" ? stroke.paint.opacity : void 0);
|
|
543
|
+
if (opacity != null && opacity !== 1) {
|
|
544
|
+
attrs.push(`stroke-opacity="${trimNumber(opacity)}"`);
|
|
545
|
+
}
|
|
546
|
+
if (stroke.cap) {
|
|
547
|
+
attrs.push(`stroke-linecap="${stroke.cap}"`);
|
|
548
|
+
}
|
|
549
|
+
if (stroke.join) {
|
|
550
|
+
attrs.push(`stroke-linejoin="${stroke.join}"`);
|
|
551
|
+
}
|
|
552
|
+
if (stroke.dash && stroke.dash.length > 0) {
|
|
553
|
+
attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
|
|
554
|
+
}
|
|
555
|
+
if (stroke.miterLimit != null) {
|
|
556
|
+
attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
|
|
557
|
+
}
|
|
558
|
+
for (const [field, attr] of [
|
|
559
|
+
["markerStart", "marker-start"],
|
|
560
|
+
["markerMid", "marker-mid"],
|
|
561
|
+
["markerEnd", "marker-end"]
|
|
562
|
+
]) {
|
|
563
|
+
const ref = stroke[field];
|
|
564
|
+
if (ref === void 0) continue;
|
|
565
|
+
const id = registry.markerId(ref);
|
|
566
|
+
if (id === void 0) continue;
|
|
567
|
+
attrs.push(`${attr}="url(#${id})"`);
|
|
568
|
+
}
|
|
569
|
+
return attrs;
|
|
570
|
+
}
|
|
571
|
+
function computeBounds(nodes) {
|
|
572
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
573
|
+
const visit = (n, ctm) => {
|
|
574
|
+
if (n.kind === "group") {
|
|
575
|
+
const childCtm = n.transform ? multiply(ctm, n.transform) : ctm;
|
|
576
|
+
n.children.forEach((c) => visit(c, childCtm));
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const b = n.kind === "text" || n.kind === "image" ? { minX: n.x, minY: n.y, maxX: n.x + n.width, maxY: n.y + n.height } : pathBounds(n.path);
|
|
580
|
+
const local = n.rotation ? rotateAboutCenter(ctm, b, n.rotation) : ctm;
|
|
581
|
+
for (const [x, y] of corners(b)) {
|
|
582
|
+
const px = local[0] * x + local[2] * y + local[4];
|
|
583
|
+
const py = local[1] * x + local[3] * y + local[5];
|
|
584
|
+
if (px < minX) minX = px;
|
|
585
|
+
if (py < minY) minY = py;
|
|
586
|
+
if (px > maxX) maxX = px;
|
|
587
|
+
if (py > maxY) maxY = py;
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
nodes.forEach((n) => visit(n, IDENTITY_MATRIX));
|
|
591
|
+
if (!Number.isFinite(minX)) {
|
|
592
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
593
|
+
}
|
|
594
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
595
|
+
}
|
|
596
|
+
function corners(b) {
|
|
597
|
+
return [[b.minX, b.minY], [b.maxX, b.minY], [b.maxX, b.maxY], [b.minX, b.maxY]];
|
|
598
|
+
}
|
|
599
|
+
function rotateAboutCenter(ctm, b, angle) {
|
|
600
|
+
const cx = (b.minX + b.maxX) / 2;
|
|
601
|
+
const cy = (b.minY + b.maxY) / 2;
|
|
602
|
+
const c = Math.cos(angle);
|
|
603
|
+
const s = Math.sin(angle);
|
|
604
|
+
return multiply(ctm, [c, s, -s, c, cx - cx * c + cy * s, cy - cx * s - cy * c]);
|
|
605
|
+
}
|
|
606
|
+
function textXml(node, registry, namespaces, warn) {
|
|
607
|
+
const attrs = [
|
|
608
|
+
`x="${trimNumber(node.x + anchorOffset(node.style, node.width))}"`,
|
|
609
|
+
`y="${trimNumber(node.y)}"`,
|
|
610
|
+
`dominant-baseline="text-before-edge"`,
|
|
611
|
+
// The runs model stores real line breaks; SVG's default whitespace
|
|
612
|
+
// handling would collapse them (and any indentation) away on re-import.
|
|
613
|
+
`xml:space="preserve"`,
|
|
614
|
+
`data-weasel-width="${trimNumber(node.width)}"`,
|
|
615
|
+
`data-weasel-height="${trimNumber(node.height)}"`
|
|
616
|
+
];
|
|
617
|
+
const style = node.style;
|
|
618
|
+
if (style?.fontSize != null) attrs.push(`font-size="${trimNumber(resolveScreenLength(style.fontSize, 1))}"`);
|
|
619
|
+
if (style?.fontFamily) attrs.push(`font-family="${escapeAttr2(style.fontFamily)}"`);
|
|
620
|
+
if (style?.fontWeight != null) attrs.push(`font-weight="${String(style.fontWeight)}"`);
|
|
621
|
+
if (style?.fontStyle && style.fontStyle !== "normal") attrs.push(`font-style="${style.fontStyle}"`);
|
|
622
|
+
const direction = style?.direction ?? "ltr";
|
|
623
|
+
if (direction === "rtl") attrs.push('direction="rtl"');
|
|
624
|
+
if (style?.align != null || direction === "rtl") {
|
|
625
|
+
const align = style?.align ?? "left";
|
|
626
|
+
const anchor = align === "center" ? "middle" : align === "start" ? "start" : align === "end" ? "end" : align === "left" === (direction === "ltr") ? "start" : "end";
|
|
627
|
+
if (anchor !== "start") attrs.push(`text-anchor="${anchor}"`);
|
|
628
|
+
}
|
|
629
|
+
if (style?.letterSpacing != null && style.letterSpacing !== 0) {
|
|
630
|
+
attrs.push(`letter-spacing="${trimNumber(resolveScreenLength(style.letterSpacing, 1))}"`);
|
|
631
|
+
}
|
|
632
|
+
const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
|
|
633
|
+
if (decoration) attrs.push(`text-decoration="${decoration}"`);
|
|
634
|
+
if (style?.textTransform && style.textTransform !== "none") {
|
|
635
|
+
attrs.push(`style="text-transform:${style.textTransform}"`);
|
|
636
|
+
}
|
|
637
|
+
if (style?.wrap) {
|
|
638
|
+
attrs.push('data-weasel-wrap="true"');
|
|
639
|
+
warn("text that wraps at its box width is written unwrapped: SVG text does not wrap, so other readers draw one line per hard break (weasel reads the wrap back)");
|
|
640
|
+
}
|
|
641
|
+
if (node.verticalAlign && node.verticalAlign !== "top") {
|
|
642
|
+
attrs.push(`data-weasel-vertical-align="${node.verticalAlign}"`);
|
|
643
|
+
warn(`text aligned to the ${node.verticalAlign} of its box is written top-aligned: SVG text has no box, so other readers draw it at the top (weasel reads the alignment back)`);
|
|
644
|
+
}
|
|
645
|
+
if (node.fill === null) {
|
|
646
|
+
attrs.push('fill="none"');
|
|
647
|
+
} else if (node.fill) {
|
|
648
|
+
if ("color" in node.fill) {
|
|
649
|
+
attrs.push(`fill="${node.fill.color}"`);
|
|
650
|
+
if (node.fill.opacity != null && node.fill.opacity !== 1) {
|
|
651
|
+
attrs.push(`fill-opacity="${trimNumber(node.fill.opacity)}"`);
|
|
652
|
+
}
|
|
653
|
+
} else {
|
|
654
|
+
attrs.push(`fill="${registry.ref(node.fill)}"`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
for (const a of coreStrokeAttrs(node.stroke, registry, warn)) attrs.push(a);
|
|
658
|
+
if (node.opacity != null && node.opacity !== 1) {
|
|
659
|
+
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
660
|
+
}
|
|
661
|
+
if (node.rotation != null && node.rotation !== 0) {
|
|
662
|
+
const cx = node.x + node.width / 2;
|
|
663
|
+
const cy = node.y + node.height / 2;
|
|
664
|
+
const deg = node.rotation * 180 / Math.PI;
|
|
665
|
+
attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
|
|
666
|
+
}
|
|
667
|
+
const body = node.runs && node.runs.length > 0 ? node.runs.map((r) => runXml(r, registry, warn)).join("") : escapeText(node.text);
|
|
668
|
+
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
669
|
+
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
670
|
+
return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
|
|
671
|
+
}
|
|
672
|
+
function runXml(run, registry, warn) {
|
|
673
|
+
const attrs = [];
|
|
674
|
+
if (run.bold) attrs.push(`font-weight="700"`);
|
|
675
|
+
if (run.italic) attrs.push(`font-style="italic"`);
|
|
676
|
+
if (run.fontFamily) attrs.push(`font-family="${escapeAttr2(run.fontFamily)}"`);
|
|
677
|
+
const presetScale = run.script != null && run.baselineShift != null ? SCRIPT_METRICS[run.script].size : void 0;
|
|
678
|
+
const scale = run.fontScale ?? presetScale;
|
|
679
|
+
if (run.fontSize != null) attrs.push(`font-size="${trimNumber(resolveScreenLength(run.fontSize, 1))}"`);
|
|
680
|
+
else if (scale != null) attrs.push(`font-size="${trimNumber(scale * 100)}%"`);
|
|
681
|
+
if (run.baselineShift != null) {
|
|
682
|
+
attrs.push(`baseline-shift="${trimNumber(run.baselineShift * 100)}%"`);
|
|
683
|
+
} else if (run.script) {
|
|
684
|
+
attrs.push(`baseline-shift="${run.script}"`);
|
|
685
|
+
}
|
|
686
|
+
if (run.letterSpacing != null) {
|
|
687
|
+
attrs.push(`letter-spacing="${trimNumber(resolveScreenLength(run.letterSpacing, 1))}"`);
|
|
688
|
+
}
|
|
689
|
+
const runDecoration = textDecorationValue(run.underline, run.strikethrough, run.overline);
|
|
690
|
+
if (runDecoration) attrs.push(`text-decoration="${runDecoration}"`);
|
|
691
|
+
if (run.fill) {
|
|
692
|
+
if ("color" in run.fill) {
|
|
693
|
+
attrs.push(`fill="${run.fill.color}"`);
|
|
694
|
+
} else {
|
|
695
|
+
attrs.push(`fill="${registry.ref(run.fill)}"`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
for (const a of coreStrokeAttrs(run.stroke, registry, warn)) attrs.push(a);
|
|
699
|
+
if (run.textTransform) attrs.push(`style="text-transform:${run.textTransform}"`);
|
|
700
|
+
const head = attrs.length > 0 ? `<tspan ${attrs.join(" ")}>` : "<tspan>";
|
|
701
|
+
return `${head}${escapeText(run.text)}</tspan>`;
|
|
702
|
+
}
|
|
703
|
+
function textDecorationValue(underline, strikethrough, overline) {
|
|
704
|
+
const tokens = [];
|
|
705
|
+
if (underline) tokens.push("underline");
|
|
706
|
+
if (strikethrough) tokens.push("line-through");
|
|
707
|
+
if (overline) tokens.push("overline");
|
|
708
|
+
return tokens.length > 0 ? tokens.join(" ") : null;
|
|
709
|
+
}
|
|
710
|
+
function escapeAttr2(s) {
|
|
711
|
+
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
712
|
+
}
|
|
713
|
+
function escapeText(s) {
|
|
714
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
715
|
+
}
|
|
716
|
+
function pathBounds(path) {
|
|
717
|
+
if (path.kind === "rect") {
|
|
718
|
+
return {
|
|
719
|
+
minX: path.x,
|
|
720
|
+
minY: path.y,
|
|
721
|
+
maxX: path.x + path.width,
|
|
722
|
+
maxY: path.y + path.height
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
const b = boundsOfPath(path);
|
|
726
|
+
return { minX: b.x, minY: b.y, maxX: b.x + b.width, maxY: b.y + b.height };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export { serializeSvg, tilePreviewCssUrl };
|
|
730
|
+
//# sourceMappingURL=chunk-QGMOA2YE.js.map
|
|
731
|
+
//# sourceMappingURL=chunk-QGMOA2YE.js.map
|