@octane-xplat/nano-icons 0.1.0 → 0.16.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/LICENSE +21 -0
- package/LICENSES.md +31 -0
- package/README.md +218 -1
- package/bin/nano-icons.mjs +2 -0
- package/dist/build/build-BbWkFu_9.js +4308 -0
- package/dist/build/cli.js +59 -0
- package/dist/build/index.js +2 -0
- package/package.json +103 -5
- package/src/NanoIcon.macos.tsrx +102 -0
- package/src/NanoIcon.tsrx +78 -0
- package/src/NanoIcon.web.tsrx +105 -0
- package/src/base64.ts +51 -0
- package/src/build/cli.ts +94 -0
- package/src/build/core/colors.ts +152 -0
- package/src/build/core/config.ts +30 -0
- package/src/build/core/fingerPrint.ts +68 -0
- package/src/build/core/flatten/arcs.ts +187 -0
- package/src/build/core/flatten/document.ts +237 -0
- package/src/build/core/flatten/dom.ts +268 -0
- package/src/build/core/flatten/element.ts +111 -0
- package/src/build/core/flatten/geometry.ts +95 -0
- package/src/build/core/flatten/index.ts +12 -0
- package/src/build/core/flatten/inherit.ts +281 -0
- package/src/build/core/flatten/path.ts +554 -0
- package/src/build/core/flatten/pathops.ts +294 -0
- package/src/build/core/flatten/shape.ts +513 -0
- package/src/build/core/flatten/stages/clip-path.ts +44 -0
- package/src/build/core/flatten/stages/collapse.ts +119 -0
- package/src/build/core/flatten/stages/defs.ts +60 -0
- package/src/build/core/flatten/stages/discard.ts +62 -0
- package/src/build/core/flatten/stages/fill-rule.ts +8 -0
- package/src/build/core/flatten/stages/nested-svg.ts +174 -0
- package/src/build/core/flatten/stages/opacity.ts +8 -0
- package/src/build/core/flatten/stages/shapes.ts +49 -0
- package/src/build/core/flatten/stages/stroke.ts +49 -0
- package/src/build/core/flatten/stages/styles.ts +32 -0
- package/src/build/core/flatten/stages/use.ts +98 -0
- package/src/build/core/flatten/stages/validate.ts +50 -0
- package/src/build/core/flatten/stages.ts +56 -0
- package/src/build/core/flatten/transform.ts +265 -0
- package/src/build/core/font/compile.ts +115 -0
- package/src/build/core/font/metrics.ts +56 -0
- package/src/build/core/font/quadratic.ts +61 -0
- package/src/build/core/fontIdentity.ts +24 -0
- package/src/build/core/glyph/merge.ts +74 -0
- package/src/build/core/glyph/parse.ts +147 -0
- package/src/build/core/glyph/placement.ts +82 -0
- package/src/build/core/glyph/validate.ts +57 -0
- package/src/build/core/naming.ts +18 -0
- package/src/build/core/parse.ts +64 -0
- package/src/build/core/pathkit/contours.ts +770 -0
- package/src/build/core/pathkit/evenodd.ts +49 -0
- package/src/build/core/pathkit/load.ts +34 -0
- package/src/build/core/pathkit/types.ts +81 -0
- package/src/build/core/pathkit/winding.ts +81 -0
- package/src/build/core/prepare.ts +98 -0
- package/src/build/core/prepareIcon.ts +115 -0
- package/src/build/core/runFontPipeline.ts +190 -0
- package/src/build/core/svgPatterns.ts +51 -0
- package/src/build/core/types.ts +54 -0
- package/src/build/externals.d.ts +17 -0
- package/src/build/iconify.ts +52 -0
- package/src/build/index.ts +304 -0
- package/src/build/module.ts +62 -0
- package/src/font.macos.ts +133 -0
- package/src/font.ts +137 -0
- package/src/font.web.ts +71 -0
- package/src/glyphmap.ts +129 -0
- package/src/index.macos.ts +31 -0
- package/src/index.ts +30 -0
- package/src/index.web.ts +31 -0
- package/src/props.ts +59 -0
- package/src/set.ts +140 -0
- package/types/generated/.tsrx-typegen-manifest.json +68 -0
- package/types/generated/NanoIcon.d.ts +7 -0
- package/types/generated/NanoIcon.macos.d.ts +7 -0
- package/types/generated/NanoIcon.web.d.ts +2 -0
- package/types/generated/base64.d.ts +4 -0
- package/types/generated/build/cli.d.ts +1 -0
- package/types/generated/build/core/colors.d.ts +1 -0
- package/types/generated/build/core/config.d.ts +11 -0
- package/types/generated/build/core/flatten/arcs.d.ts +2 -0
- package/types/generated/build/core/flatten/document.d.ts +35 -0
- package/types/generated/build/core/flatten/dom.d.ts +24 -0
- package/types/generated/build/core/flatten/element.d.ts +12 -0
- package/types/generated/build/core/flatten/geometry.d.ts +21 -0
- package/types/generated/build/core/flatten/index.d.ts +2 -0
- package/types/generated/build/core/flatten/inherit.d.ts +13 -0
- package/types/generated/build/core/flatten/path.d.ts +19 -0
- package/types/generated/build/core/flatten/pathops.d.ts +14 -0
- package/types/generated/build/core/flatten/shape.d.ts +29 -0
- package/types/generated/build/core/flatten/stages/clip-path.d.ts +4 -0
- package/types/generated/build/core/flatten/stages/collapse.d.ts +2 -0
- package/types/generated/build/core/flatten/stages/defs.d.ts +4 -0
- package/types/generated/build/core/flatten/stages/discard.d.ts +4 -0
- package/types/generated/build/core/flatten/stages/fill-rule.d.ts +2 -0
- package/types/generated/build/core/flatten/stages/nested-svg.d.ts +2 -0
- package/types/generated/build/core/flatten/stages/opacity.d.ts +2 -0
- package/types/generated/build/core/flatten/stages/shapes.d.ts +7 -0
- package/types/generated/build/core/flatten/stages/stroke.d.ts +3 -0
- package/types/generated/build/core/flatten/stages/styles.d.ts +2 -0
- package/types/generated/build/core/flatten/stages/use.d.ts +4 -0
- package/types/generated/build/core/flatten/stages/validate.d.ts +2 -0
- package/types/generated/build/core/flatten/stages.d.ts +2 -0
- package/types/generated/build/core/flatten/transform.d.ts +35 -0
- package/types/generated/build/core/font/compile.d.ts +21 -0
- package/types/generated/build/core/font/metrics.d.ts +1 -0
- package/types/generated/build/core/font/quadratic.d.ts +2 -0
- package/types/generated/build/core/fontIdentity.d.ts +4 -0
- package/types/generated/build/core/glyph/merge.d.ts +7 -0
- package/types/generated/build/core/glyph/parse.d.ts +27 -0
- package/types/generated/build/core/glyph/placement.d.ts +28 -0
- package/types/generated/build/core/glyph/validate.d.ts +8 -0
- package/types/generated/build/core/naming.d.ts +2 -0
- package/types/generated/build/core/parse.d.ts +1 -0
- package/types/generated/build/core/pathkit/contours.d.ts +75 -0
- package/types/generated/build/core/pathkit/evenodd.d.ts +12 -0
- package/types/generated/build/core/pathkit/load.d.ts +2 -0
- package/types/generated/build/core/pathkit/types.d.ts +69 -0
- package/types/generated/build/core/prepare.d.ts +20 -0
- package/types/generated/build/core/prepareIcon.d.ts +26 -0
- package/types/generated/build/core/runFontPipeline.d.ts +32 -0
- package/types/generated/build/core/svgPatterns.d.ts +39 -0
- package/types/generated/build/core/types.d.ts +49 -0
- package/types/generated/build/externals.d.ts +17 -0
- package/types/generated/build/iconify.d.ts +18 -0
- package/types/generated/build/index.d.ts +75 -0
- package/types/generated/build/module.d.ts +14 -0
- package/types/generated/font.d.ts +10 -0
- package/types/generated/font.macos.d.ts +9 -0
- package/types/generated/font.web.d.ts +8 -0
- package/types/generated/glyphmap.d.ts +62 -0
- package/types/generated/index.d.ts +21 -0
- package/types/generated/index.macos.d.ts +22 -0
- package/types/generated/index.web.d.ts +22 -0
- package/types/generated/props.d.ts +55 -0
- package/types/generated/set.d.ts +41 -0
|
@@ -0,0 +1,4308 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import fs$1 from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { getIcons } from "@iconify/utils";
|
|
7
|
+
import { iconToSVG } from "@iconify/utils/lib/svg/build";
|
|
8
|
+
import { DOMParser } from "@xmldom/xmldom";
|
|
9
|
+
import { Font, woff2 } from "fonteditor-core";
|
|
10
|
+
import svg2ttf from "svg2ttf";
|
|
11
|
+
import cubic2quadImport from "cubic2quad";
|
|
12
|
+
//#region src/build/core/svgPatterns.ts
|
|
13
|
+
/** Path already starts with a moveto. */
|
|
14
|
+
var SVG_MOVE_PREFIX = /^[Mm]/;
|
|
15
|
+
/** Trailing close-path command (+ optional whitespace). */
|
|
16
|
+
var SVG_TRAILING_CLOSE = /[Zz]\s*$/;
|
|
17
|
+
/** A signed int/float token. */
|
|
18
|
+
var SVG_NUMBER = /-?\d+(?:\.\d+)?/g;
|
|
19
|
+
/** Whitespace separator (e.g. viewBox parts). */
|
|
20
|
+
var WHITESPACE = /\s+/;
|
|
21
|
+
/** `<mask …>` element (unsupported). */
|
|
22
|
+
var XML_MASK = /<mask[\s/>]/i;
|
|
23
|
+
/** `<filter …>` element (unsupported). */
|
|
24
|
+
var XML_FILTER = /<filter[\s/>]/i;
|
|
25
|
+
/** `<image …>` element (unsupported). */
|
|
26
|
+
var XML_IMAGE = /<image[\s/>]/i;
|
|
27
|
+
var XML_UNCONVERTIBLE = /<(text|style|foreignObject|a|switch)[\s/>]/i;
|
|
28
|
+
/** An `xmlns=` attribute. */
|
|
29
|
+
var XML_XMLNS = /xmlns\s*=/;
|
|
30
|
+
/** Opening `<svg` tag. */
|
|
31
|
+
var SVG_OPEN_TAG = /<svg\b/;
|
|
32
|
+
var PX_ATTRIBUTE_VALUE = /=(["'])(\s*-?(?:\d+\.?\d*|\.\d+))px\s*\1/g;
|
|
33
|
+
var PX_STYLE_VALUE = /(:\s*-?(?:\d+\.?\d*|\.\d+))px(?=\s*(?:;|["']|$))/g;
|
|
34
|
+
/** viewBox: comma or whitespace separated. */
|
|
35
|
+
var VIEWBOX_SEPARATOR = /,|\s+/;
|
|
36
|
+
/** transform() arguments: comma and/or whitespace separated. */
|
|
37
|
+
var TRANSFORM_ARG_SEPARATOR = /\s*[,\s]\s*/;
|
|
38
|
+
/** stroke-dasharray: single comma or space separated. */
|
|
39
|
+
var DASHARRAY_SEPARATOR = /[, ]/;
|
|
40
|
+
/** One transform function call, e.g. `rotate(30)`; captures name and args. */
|
|
41
|
+
var TRANSFORM_FUNCTION = /(matrix|translate|scale|rotate|skewX|skewY)\s*\(([^)]*)\)/gi;
|
|
42
|
+
/** `url(#id)` reference; captures the id. */
|
|
43
|
+
var URL_ID_REFERENCE = /^url[(]#([\w-]+)[)]$/;
|
|
44
|
+
/** A well-formed XML attribute name. */
|
|
45
|
+
var XML_ATTRIBUTE_NAME = /^[A-Za-z_][\w.-]*$/;
|
|
46
|
+
var XML_AMP = /&/g;
|
|
47
|
+
var XML_LT = /</g;
|
|
48
|
+
var XML_GT = />/g;
|
|
49
|
+
var XML_QUOT = /"/g;
|
|
50
|
+
/** svg2ttf error `glyph "u<hex>"` → captures the hex codepoint. */
|
|
51
|
+
var GLYPH_CODEPOINT = /glyph\s+"u([0-9a-fA-F]+)"/;
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/build/core/flatten/dom.ts
|
|
54
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
55
|
+
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
56
|
+
function svgTag(local) {
|
|
57
|
+
return `{${SVG_NS}}${local}`;
|
|
58
|
+
}
|
|
59
|
+
function xlinkHrefAttr() {
|
|
60
|
+
return `{${XLINK_NS}}href`;
|
|
61
|
+
}
|
|
62
|
+
function splitNs(name) {
|
|
63
|
+
if (name.startsWith("{")) {
|
|
64
|
+
const end = name.indexOf("}");
|
|
65
|
+
return [name.slice(1, end), name.slice(end + 1)];
|
|
66
|
+
}
|
|
67
|
+
return [null, name];
|
|
68
|
+
}
|
|
69
|
+
function stripNs(name) {
|
|
70
|
+
return splitNs(name)[1];
|
|
71
|
+
}
|
|
72
|
+
var XEl = class XEl {
|
|
73
|
+
tag;
|
|
74
|
+
attrib;
|
|
75
|
+
children;
|
|
76
|
+
parent = null;
|
|
77
|
+
constructor(tag, attrib) {
|
|
78
|
+
this.tag = tag;
|
|
79
|
+
this.attrib = new Map(attrib ?? []);
|
|
80
|
+
this.children = [];
|
|
81
|
+
}
|
|
82
|
+
append(child) {
|
|
83
|
+
child.detach();
|
|
84
|
+
child.parent = this;
|
|
85
|
+
this.children.push(child);
|
|
86
|
+
}
|
|
87
|
+
insert(index, child) {
|
|
88
|
+
child.detach();
|
|
89
|
+
child.parent = this;
|
|
90
|
+
this.children.splice(index, 0, child);
|
|
91
|
+
}
|
|
92
|
+
detach() {
|
|
93
|
+
if (this.parent) {
|
|
94
|
+
const idx = this.parent.children.indexOf(this);
|
|
95
|
+
if (idx !== -1) this.parent.children.splice(idx, 1);
|
|
96
|
+
this.parent = null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
index(child) {
|
|
100
|
+
return this.children.indexOf(child);
|
|
101
|
+
}
|
|
102
|
+
deepClone() {
|
|
103
|
+
const copy = new XEl(this.tag, this.attrib);
|
|
104
|
+
for (const child of this.children) copy.append(child.deepClone());
|
|
105
|
+
return copy;
|
|
106
|
+
}
|
|
107
|
+
*iter() {
|
|
108
|
+
yield this;
|
|
109
|
+
for (const child of this.children) yield* child.iter();
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
function findAll(root, pred) {
|
|
113
|
+
const out = [];
|
|
114
|
+
for (const el of root.iter()) if (pred(el)) out.push(el);
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
function replaceEl(el, replacements) {
|
|
118
|
+
const parent = el.parent;
|
|
119
|
+
if (!parent) throw new Error("Cannot replace element without parent");
|
|
120
|
+
const idx = parent.index(el);
|
|
121
|
+
el.detach();
|
|
122
|
+
replacements.forEach((child, childIdx) => {
|
|
123
|
+
parent.insert(idx + childIdx, child);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function delAttrs(el, ...attrNames) {
|
|
127
|
+
for (const name of attrNames) el.attrib.delete(name);
|
|
128
|
+
}
|
|
129
|
+
function domNsToTag(node) {
|
|
130
|
+
const ns = node.namespaceURI;
|
|
131
|
+
return ns ? `{${ns}}${node.localName}` : node.nodeName;
|
|
132
|
+
}
|
|
133
|
+
function convertElement(node) {
|
|
134
|
+
const el = new XEl(domNsToTag(node));
|
|
135
|
+
const attrs = node.attributes;
|
|
136
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
137
|
+
const attr = attrs.item(i);
|
|
138
|
+
if (attr.prefix === "xmlns" || attr.nodeName === "xmlns") continue;
|
|
139
|
+
const name = attr.namespaceURI ? `{${attr.namespaceURI}}${attr.localName}` : attr.nodeName;
|
|
140
|
+
el.attrib.set(name, attr.value);
|
|
141
|
+
}
|
|
142
|
+
const children = node.childNodes;
|
|
143
|
+
for (let i = 0; i < children.length; i++) {
|
|
144
|
+
const child = children.item(i);
|
|
145
|
+
if (child.nodeType === 1) el.append(convertElement(child));
|
|
146
|
+
}
|
|
147
|
+
return el;
|
|
148
|
+
}
|
|
149
|
+
function parseSvgDocument(content) {
|
|
150
|
+
let text = content;
|
|
151
|
+
if (text.includes("xlink:") && !text.includes("xmlns:xlink")) text = text.replace(/<svg([^>]*)>/, (_m, attrs) => `<svg${attrs} xmlns:xlink="${XLINK_NS}">`);
|
|
152
|
+
let parseError = null;
|
|
153
|
+
const root = new DOMParser({ onError: (_level, message) => {
|
|
154
|
+
parseError = parseError ?? message;
|
|
155
|
+
} }).parseFromString(text, "text/xml").documentElement;
|
|
156
|
+
if (!root || parseError) throw new Error(`Unable to parse SVG: ${parseError ?? "no root element"}`);
|
|
157
|
+
return convertElement(root);
|
|
158
|
+
}
|
|
159
|
+
function escapeAttr(value) {
|
|
160
|
+
return value.replace(XML_AMP, "&").replace(XML_LT, "<").replace(XML_GT, ">").replace(XML_QUOT, """);
|
|
161
|
+
}
|
|
162
|
+
function attrDisplayName(name) {
|
|
163
|
+
const [ns, local] = splitNs(name);
|
|
164
|
+
if (ns === "http://www.w3.org/1999/xlink") return `xlink:${local}`;
|
|
165
|
+
if (ns === "http://www.w3.org/2000/svg" || ns === null) return local;
|
|
166
|
+
return local;
|
|
167
|
+
}
|
|
168
|
+
function tagDisplayName(tag) {
|
|
169
|
+
const [ns, local] = splitNs(tag);
|
|
170
|
+
if (ns === "http://www.w3.org/2000/svg" || ns === null) return local;
|
|
171
|
+
throw new Error(`Cannot serialize tag outside svg namespace: ${tag}`);
|
|
172
|
+
}
|
|
173
|
+
function usesXlink(root) {
|
|
174
|
+
for (const el of root.iter()) for (const name of el.attrib.keys()) if (splitNs(name)[0] === "http://www.w3.org/1999/xlink") return true;
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
function serializeEl(el, indent, out) {
|
|
178
|
+
const pad = " ".repeat(indent);
|
|
179
|
+
const parts = [`${pad}<${tagDisplayName(el.tag)}`];
|
|
180
|
+
for (const [name, value] of el.attrib) parts.push(` ${attrDisplayName(name)}="${escapeAttr(value)}"`);
|
|
181
|
+
if (!el.children.length) {
|
|
182
|
+
parts.push("/>");
|
|
183
|
+
out.push(parts.join(""));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
parts.push(">");
|
|
187
|
+
out.push(parts.join(""));
|
|
188
|
+
for (const child of el.children) serializeEl(child, indent + 1, out);
|
|
189
|
+
out.push(`${pad}</${tagDisplayName(el.tag)}>`);
|
|
190
|
+
}
|
|
191
|
+
function serializeSvg(root) {
|
|
192
|
+
const out = [];
|
|
193
|
+
const attrs = [`xmlns="${SVG_NS}"`];
|
|
194
|
+
if (usesXlink(root)) attrs.push(`xmlns:xlink="${XLINK_NS}"`);
|
|
195
|
+
for (const [name, value] of root.attrib) attrs.push(`${attrDisplayName(name)}="${escapeAttr(value)}"`);
|
|
196
|
+
if (!root.children.length) out.push(`<${tagDisplayName(root.tag)} ${attrs.join(" ")}/>`);
|
|
197
|
+
else {
|
|
198
|
+
out.push(`<${tagDisplayName(root.tag)} ${attrs.join(" ")}>`);
|
|
199
|
+
for (const child of root.children) serializeEl(child, 1, out);
|
|
200
|
+
out.push(`</${tagDisplayName(root.tag)}>`);
|
|
201
|
+
}
|
|
202
|
+
return out.join("\n") + "\n";
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/build/iconify.ts
|
|
206
|
+
/**
|
|
207
|
+
* Materialize an Iconify collection (e.g. `@iconify-json/heroicons`) as
|
|
208
|
+
* standalone SVG documents for `buildNanoIconFont`. Iconify geometry comes
|
|
209
|
+
* from the icon's own `width`/`height`/`left`/`top` — the artwork is not
|
|
210
|
+
* rescaled; glyph placement maps the viewBox into the font's em square.
|
|
211
|
+
* `currentColor` fills are preserved as tintable `currentColor` layers.
|
|
212
|
+
*/
|
|
213
|
+
function iconifyCollectionToSvgs(collection) {
|
|
214
|
+
const names = [...Object.keys(collection.icons), ...Object.keys(collection.aliases ?? {})];
|
|
215
|
+
const resolved = getIcons(collection, names);
|
|
216
|
+
if (!resolved) throw new Error(`Iconify collection "${collection.prefix}" has no resolvable icons`);
|
|
217
|
+
const inputs = [];
|
|
218
|
+
for (const [name, icon] of Object.entries(resolved.icons)) {
|
|
219
|
+
const doc = iconifyIconToSvg(name, icon);
|
|
220
|
+
inputs.push({
|
|
221
|
+
name: `${resolved.prefix}:${name}`,
|
|
222
|
+
file: `${resolved.prefix}:${name}.svg`,
|
|
223
|
+
svg: doc
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return inputs;
|
|
227
|
+
}
|
|
228
|
+
/** Standalone SVG document for one resolved Iconify icon. */
|
|
229
|
+
function iconifyIconToSvg(name, icon) {
|
|
230
|
+
const result = iconToSVG(icon, { height: "unset" });
|
|
231
|
+
const { viewBox } = result.attributes;
|
|
232
|
+
return `<svg xmlns="${SVG_NS}" viewBox="${viewBox}">${result.body}</svg>`;
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/build/core/config.ts
|
|
236
|
+
/** Create directory if it does not exist; do not remove existing contents (for shared output dirs). */
|
|
237
|
+
function ensureDir(dir) {
|
|
238
|
+
fs$1.mkdirSync(dir, { recursive: true });
|
|
239
|
+
}
|
|
240
|
+
function writeFileAtomic(filePath, data) {
|
|
241
|
+
const tmpPath = `${filePath}.tmp`;
|
|
242
|
+
fs$1.writeFileSync(tmpPath, data);
|
|
243
|
+
fs$1.renameSync(tmpPath, filePath);
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/build/core/naming.ts
|
|
247
|
+
/** Word separators in a set name (split → PascalCase parts). */
|
|
248
|
+
var NAME_SEPARATORS = /[^a-zA-Z0-9]+/;
|
|
249
|
+
/** Manifest identifiers must start with a letter. */
|
|
250
|
+
var STARTS_WITH_LETTER = /^[a-zA-Z]/;
|
|
251
|
+
/** "my-tab icons" → "MyTabIcons" — PascalCase base for manifest identifiers. */
|
|
252
|
+
function manifestBaseName(setName) {
|
|
253
|
+
const pascal = setName.split(NAME_SEPARATORS).filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join("");
|
|
254
|
+
return STARTS_WITH_LETTER.test(pascal) ? pascal : `Set${pascal}`;
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/build/module.ts
|
|
258
|
+
/** Escape a string for embedding inside a single-quoted TS literal. */
|
|
259
|
+
function tsString(value) {
|
|
260
|
+
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Emit `<fontFamily>.ts` beside the font assets. The module exports the
|
|
264
|
+
* glyphmap, a bound `NanoIconSet`, a `NanoIcon` alias for `set.Icon`, and a
|
|
265
|
+
* union type of icon names. `url` is a lazy getter so non-web runtimes never
|
|
266
|
+
* evaluate `import.meta.url` resolution.
|
|
267
|
+
*/
|
|
268
|
+
function emitNanoIconSetModule(options) {
|
|
269
|
+
const base = manifestBaseName(options.fontFamily);
|
|
270
|
+
const fontFile = `${options.family}.ttf`;
|
|
271
|
+
const names = Object.keys(options.glyphMap.i);
|
|
272
|
+
const urlLine = options.web ? `get url() {\n\t\treturn new URL('./${options.family}.woff2', import.meta.url).href;\n\t},` : `get url() {\n\t\treturn new URL('./${fontFile}', import.meta.url).href;\n\t},`;
|
|
273
|
+
const source = `// Generated by @octane-xplat/nano-icons. Regenerate with the nano-icons build — do not edit.
|
|
274
|
+
import { createNanoIconSet } from '@octane-xplat/nano-icons';
|
|
275
|
+
import type { NanoIconSet } from '@octane-xplat/nano-icons';
|
|
276
|
+
|
|
277
|
+
export const glyphMap = ${JSON.stringify(options.glyphMap)} as const;
|
|
278
|
+
|
|
279
|
+
/** iOS/Android: copy ${tsString(fontFile)} into the app's fonts directory (see @octane-xplat/nano-icons docs). */
|
|
280
|
+
export const ${base}Icons: NanoIconSet<${base}IconName> = createNanoIconSet(glyphMap, {
|
|
281
|
+
font: {
|
|
282
|
+
// NativeScript resolves app fonts by file name; keep it equal to the family.
|
|
283
|
+
fileName: ${tsString(fontFile)},
|
|
284
|
+
// Web font source; evaluated lazily so native bundles never touch import.meta.url.
|
|
285
|
+
${urlLine}
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
export const NanoIcon = ${base}Icons.Icon;
|
|
290
|
+
|
|
291
|
+
export type ${base}IconName =
|
|
292
|
+
${names.length ? names.map((n) => `\t| ${tsString(n)}`).join("\n") : " | never"};
|
|
293
|
+
`;
|
|
294
|
+
ensureDir(options.outputDir);
|
|
295
|
+
const modulePath = path.join(options.outputDir, `${options.fontFamily}.ts`);
|
|
296
|
+
writeFileAtomic(modulePath, source);
|
|
297
|
+
return modulePath;
|
|
298
|
+
}
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/build/core/font/metrics.ts
|
|
301
|
+
var FIXED_TIMESTAMP_MS = Date.UTC(1970, 0, 1, 0, 0, 1);
|
|
302
|
+
function forceTtfMetrics(ttfBuffer, upm, ascent, descent, lineGap) {
|
|
303
|
+
const font = Font.create(ttfBuffer, {
|
|
304
|
+
type: "ttf",
|
|
305
|
+
hinting: false,
|
|
306
|
+
compound2simple: false,
|
|
307
|
+
combinePath: false
|
|
308
|
+
});
|
|
309
|
+
const data = font.get();
|
|
310
|
+
data.head.unitsPerEm = upm;
|
|
311
|
+
data.hhea.ascent = ascent;
|
|
312
|
+
data.hhea.descent = -Math.abs(descent);
|
|
313
|
+
data.hhea.lineGap = lineGap;
|
|
314
|
+
data["OS/2"].usWinAscent = ascent;
|
|
315
|
+
data["OS/2"].usWinDescent = Math.abs(descent);
|
|
316
|
+
data["OS/2"].sTypoAscender = ascent;
|
|
317
|
+
data["OS/2"].sTypoDescender = -Math.abs(descent);
|
|
318
|
+
data["OS/2"].sTypoLineGap = lineGap;
|
|
319
|
+
data["OS/2"].fsSelection = (data["OS/2"].fsSelection || 0) | 128;
|
|
320
|
+
data.head.created = FIXED_TIMESTAMP_MS;
|
|
321
|
+
data.head.modified = FIXED_TIMESTAMP_MS;
|
|
322
|
+
data.post.format = 3;
|
|
323
|
+
for (const glyph of data.glyf) delete glyph.name;
|
|
324
|
+
font.set(data);
|
|
325
|
+
const out = font.write({
|
|
326
|
+
type: "ttf",
|
|
327
|
+
hinting: false
|
|
328
|
+
});
|
|
329
|
+
return Buffer.from(out);
|
|
330
|
+
}
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region src/build/core/font/compile.ts
|
|
333
|
+
function escapeXml(s) {
|
|
334
|
+
return s.replace(XML_AMP, "&").replace(XML_QUOT, """);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Build an SVG font XML string from pre-transformed glyph data.
|
|
338
|
+
*/
|
|
339
|
+
function buildSvgFontXml(opts) {
|
|
340
|
+
const { fontName, glyphs, upm, ascent, descent } = opts;
|
|
341
|
+
const glyphLines = glyphs.map((g) => {
|
|
342
|
+
const hex = g.codepoint.toString(16);
|
|
343
|
+
return `<glyph glyph-name="${`u${hex.padStart(4, "0")}`}" unicode="&#x${hex};" horiz-adv-x="${g.advanceWidth}" d="${escapeXml(g.d)}"/>`;
|
|
344
|
+
});
|
|
345
|
+
return `<?xml version="1.0" standalone="no"?>
|
|
346
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
347
|
+
<svg xmlns="${SVG_NS}">
|
|
348
|
+
<defs>
|
|
349
|
+
<font id="${escapeXml(fontName)}" horiz-adv-x="${upm}">
|
|
350
|
+
<font-face font-family="${escapeXml(fontName)}" units-per-em="${upm}" ascent="${ascent}" descent="${-Math.abs(descent)}"/>
|
|
351
|
+
<missing-glyph horiz-adv-x="0"/>
|
|
352
|
+
${glyphLines.join("\n")}
|
|
353
|
+
</font>
|
|
354
|
+
</defs>
|
|
355
|
+
</svg>`;
|
|
356
|
+
}
|
|
357
|
+
function parseCompileTtfFromGlyphsError(err, codepointToIcon) {
|
|
358
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
359
|
+
const cpMatch = msg.match(GLYPH_CODEPOINT);
|
|
360
|
+
if (cpMatch) {
|
|
361
|
+
const cp = parseInt(cpMatch[1], 16);
|
|
362
|
+
const iconName = codepointToIcon.get(cp);
|
|
363
|
+
const detail = iconName ? `icon "${iconName}" (codepoint u${cpMatch[1]})` : `codepoint u${cpMatch[1]}`;
|
|
364
|
+
throw new Error(`Font compilation failed for ${detail}: ${msg}`);
|
|
365
|
+
}
|
|
366
|
+
throw err;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Compile a TTF font from pre-transformed glyph data.
|
|
370
|
+
* Builds SVG font XML directly (no intermediate files), then converts via svg2ttf.
|
|
371
|
+
*/
|
|
372
|
+
async function compileTtfFromGlyphs(opts) {
|
|
373
|
+
const { glyphs, outTtfPath, fontName, upm, ascent, descent } = opts;
|
|
374
|
+
const lineGap = opts.lineGap ?? 0;
|
|
375
|
+
if (glyphs.length === 0) throw new Error("No glyphs to compile");
|
|
376
|
+
const svgFontString = buildSvgFontXml({
|
|
377
|
+
fontName,
|
|
378
|
+
glyphs,
|
|
379
|
+
upm,
|
|
380
|
+
ascent,
|
|
381
|
+
descent
|
|
382
|
+
});
|
|
383
|
+
const ttfRaw = svg2ttf(svgFontString, { ts: 0 });
|
|
384
|
+
const fixedBuf = forceTtfMetrics(Buffer.from(ttfRaw.buffer), upm, ascent, descent, lineGap);
|
|
385
|
+
fs$1.mkdirSync(path.dirname(outTtfPath), { recursive: true });
|
|
386
|
+
writeFileAtomic(outTtfPath, fixedBuf);
|
|
387
|
+
return fixedBuf;
|
|
388
|
+
}
|
|
389
|
+
var woff2Ready;
|
|
390
|
+
async function compileWoff2FromTtf(ttfBuffer) {
|
|
391
|
+
woff2Ready ??= woff2.init();
|
|
392
|
+
await woff2Ready;
|
|
393
|
+
return Buffer.from(woff2.encode(ttfBuffer));
|
|
394
|
+
}
|
|
395
|
+
//#endregion
|
|
396
|
+
//#region src/build/core/fontIdentity.ts
|
|
397
|
+
var BUILD_HASH_LENGTH = 8;
|
|
398
|
+
var BUILD_HASH = `[0-9a-f]{${BUILD_HASH_LENGTH}}`;
|
|
399
|
+
new RegExp(`-${BUILD_HASH}$`);
|
|
400
|
+
new RegExp(`-${BUILD_HASH}\\.ttf$`);
|
|
401
|
+
function buildFontFamily(fontFamily, inputHash) {
|
|
402
|
+
return `${fontFamily}-${inputHash.slice(0, BUILD_HASH_LENGTH)}`;
|
|
403
|
+
}
|
|
404
|
+
//#endregion
|
|
405
|
+
//#region src/build/core/pathkit/load.ts
|
|
406
|
+
var require = createRequire(import.meta.url);
|
|
407
|
+
var instance = null;
|
|
408
|
+
async function loadPathKit() {
|
|
409
|
+
if (instance) return instance;
|
|
410
|
+
const PathKitInit = require("pathkit-wasm/bin/pathkit.js");
|
|
411
|
+
const pathkitJsPath = require.resolve("pathkit-wasm/bin/pathkit.js");
|
|
412
|
+
const pathkitBinDir = path.dirname(pathkitJsPath);
|
|
413
|
+
const pathkitWasmPath = path.join(pathkitBinDir, "pathkit.wasm");
|
|
414
|
+
const pkInit = PathKitInit({
|
|
415
|
+
wasmBinary: await fs.readFile(pathkitWasmPath),
|
|
416
|
+
locateFile: (file) => path.join(pathkitBinDir, file)
|
|
417
|
+
});
|
|
418
|
+
instance = await (typeof pkInit?.ready === "function" ? pkInit.ready() : pkInit);
|
|
419
|
+
return instance;
|
|
420
|
+
}
|
|
421
|
+
//#endregion
|
|
422
|
+
//#region src/build/core/font/quadratic.ts
|
|
423
|
+
var cubic2quad = cubic2quadImport;
|
|
424
|
+
var COMMAND = /([MLQCZ])([^MLQCZ]*)/g;
|
|
425
|
+
var NUMBER = /-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?/gi;
|
|
426
|
+
var QUADRATIC_ERROR_BOUND_EM = 1 / 512;
|
|
427
|
+
function toQuadraticPath(d, errorBound) {
|
|
428
|
+
const out = [];
|
|
429
|
+
let x = 0;
|
|
430
|
+
let y = 0;
|
|
431
|
+
for (const [, cmd, rawArgs] of d.matchAll(COMMAND)) {
|
|
432
|
+
const args = (rawArgs.match(NUMBER) ?? []).map(Number);
|
|
433
|
+
if (cmd === "C") {
|
|
434
|
+
for (let i = 0; i + 5 < args.length; i += 6) {
|
|
435
|
+
const quads = cubic2quad(x, y, args[i], args[i + 1], args[i + 2], args[i + 3], args[i + 4], args[i + 5], errorBound);
|
|
436
|
+
for (let j = 2; j + 3 < quads.length; j += 4) out.push(`Q${quads[j]} ${quads[j + 1]} ${quads[j + 2]} ${quads[j + 3]}`);
|
|
437
|
+
x = args[i + 4];
|
|
438
|
+
y = args[i + 5];
|
|
439
|
+
}
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
out.push(`${cmd}${rawArgs.trim()}`);
|
|
443
|
+
if (cmd !== "Z" && args.length >= 2) {
|
|
444
|
+
x = args[args.length - 2];
|
|
445
|
+
y = args[args.length - 1];
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return out.join("");
|
|
449
|
+
}
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region src/build/core/colors.ts
|
|
452
|
+
var CSS_NAMED_COLORS = {
|
|
453
|
+
aliceblue: [
|
|
454
|
+
240,
|
|
455
|
+
248,
|
|
456
|
+
255
|
|
457
|
+
],
|
|
458
|
+
antiquewhite: [
|
|
459
|
+
250,
|
|
460
|
+
235,
|
|
461
|
+
215
|
|
462
|
+
],
|
|
463
|
+
aqua: [
|
|
464
|
+
0,
|
|
465
|
+
255,
|
|
466
|
+
255
|
|
467
|
+
],
|
|
468
|
+
aquamarine: [
|
|
469
|
+
127,
|
|
470
|
+
255,
|
|
471
|
+
212
|
|
472
|
+
],
|
|
473
|
+
azure: [
|
|
474
|
+
240,
|
|
475
|
+
255,
|
|
476
|
+
255
|
|
477
|
+
],
|
|
478
|
+
beige: [
|
|
479
|
+
245,
|
|
480
|
+
245,
|
|
481
|
+
220
|
|
482
|
+
],
|
|
483
|
+
bisque: [
|
|
484
|
+
255,
|
|
485
|
+
228,
|
|
486
|
+
196
|
|
487
|
+
],
|
|
488
|
+
black: [
|
|
489
|
+
0,
|
|
490
|
+
0,
|
|
491
|
+
0
|
|
492
|
+
],
|
|
493
|
+
blanchedalmond: [
|
|
494
|
+
255,
|
|
495
|
+
235,
|
|
496
|
+
205
|
|
497
|
+
],
|
|
498
|
+
blue: [
|
|
499
|
+
0,
|
|
500
|
+
0,
|
|
501
|
+
255
|
|
502
|
+
],
|
|
503
|
+
blueviolet: [
|
|
504
|
+
138,
|
|
505
|
+
43,
|
|
506
|
+
226
|
|
507
|
+
],
|
|
508
|
+
brown: [
|
|
509
|
+
165,
|
|
510
|
+
42,
|
|
511
|
+
42
|
|
512
|
+
],
|
|
513
|
+
burlywood: [
|
|
514
|
+
222,
|
|
515
|
+
184,
|
|
516
|
+
135
|
|
517
|
+
],
|
|
518
|
+
cadetblue: [
|
|
519
|
+
95,
|
|
520
|
+
158,
|
|
521
|
+
160
|
|
522
|
+
],
|
|
523
|
+
chartreuse: [
|
|
524
|
+
127,
|
|
525
|
+
255,
|
|
526
|
+
0
|
|
527
|
+
],
|
|
528
|
+
chocolate: [
|
|
529
|
+
210,
|
|
530
|
+
105,
|
|
531
|
+
30
|
|
532
|
+
],
|
|
533
|
+
coral: [
|
|
534
|
+
255,
|
|
535
|
+
127,
|
|
536
|
+
80
|
|
537
|
+
],
|
|
538
|
+
cornflowerblue: [
|
|
539
|
+
100,
|
|
540
|
+
149,
|
|
541
|
+
237
|
|
542
|
+
],
|
|
543
|
+
cornsilk: [
|
|
544
|
+
255,
|
|
545
|
+
248,
|
|
546
|
+
220
|
|
547
|
+
],
|
|
548
|
+
crimson: [
|
|
549
|
+
220,
|
|
550
|
+
20,
|
|
551
|
+
60
|
|
552
|
+
],
|
|
553
|
+
cyan: [
|
|
554
|
+
0,
|
|
555
|
+
255,
|
|
556
|
+
255
|
|
557
|
+
],
|
|
558
|
+
darkblue: [
|
|
559
|
+
0,
|
|
560
|
+
0,
|
|
561
|
+
139
|
|
562
|
+
],
|
|
563
|
+
darkcyan: [
|
|
564
|
+
0,
|
|
565
|
+
139,
|
|
566
|
+
139
|
|
567
|
+
],
|
|
568
|
+
darkgoldenrod: [
|
|
569
|
+
184,
|
|
570
|
+
134,
|
|
571
|
+
11
|
|
572
|
+
],
|
|
573
|
+
darkgray: [
|
|
574
|
+
169,
|
|
575
|
+
169,
|
|
576
|
+
169
|
|
577
|
+
],
|
|
578
|
+
darkgreen: [
|
|
579
|
+
0,
|
|
580
|
+
100,
|
|
581
|
+
0
|
|
582
|
+
],
|
|
583
|
+
darkgrey: [
|
|
584
|
+
169,
|
|
585
|
+
169,
|
|
586
|
+
169
|
|
587
|
+
],
|
|
588
|
+
darkkhaki: [
|
|
589
|
+
189,
|
|
590
|
+
183,
|
|
591
|
+
107
|
|
592
|
+
],
|
|
593
|
+
darkmagenta: [
|
|
594
|
+
139,
|
|
595
|
+
0,
|
|
596
|
+
139
|
|
597
|
+
],
|
|
598
|
+
darkolivegreen: [
|
|
599
|
+
85,
|
|
600
|
+
107,
|
|
601
|
+
47
|
|
602
|
+
],
|
|
603
|
+
darkorange: [
|
|
604
|
+
255,
|
|
605
|
+
140,
|
|
606
|
+
0
|
|
607
|
+
],
|
|
608
|
+
darkorchid: [
|
|
609
|
+
153,
|
|
610
|
+
50,
|
|
611
|
+
204
|
|
612
|
+
],
|
|
613
|
+
darkred: [
|
|
614
|
+
139,
|
|
615
|
+
0,
|
|
616
|
+
0
|
|
617
|
+
],
|
|
618
|
+
darksalmon: [
|
|
619
|
+
233,
|
|
620
|
+
150,
|
|
621
|
+
122
|
|
622
|
+
],
|
|
623
|
+
darkseagreen: [
|
|
624
|
+
143,
|
|
625
|
+
188,
|
|
626
|
+
143
|
|
627
|
+
],
|
|
628
|
+
darkslateblue: [
|
|
629
|
+
72,
|
|
630
|
+
61,
|
|
631
|
+
139
|
|
632
|
+
],
|
|
633
|
+
darkslategray: [
|
|
634
|
+
47,
|
|
635
|
+
79,
|
|
636
|
+
79
|
|
637
|
+
],
|
|
638
|
+
darkslategrey: [
|
|
639
|
+
47,
|
|
640
|
+
79,
|
|
641
|
+
79
|
|
642
|
+
],
|
|
643
|
+
darkturquoise: [
|
|
644
|
+
0,
|
|
645
|
+
206,
|
|
646
|
+
209
|
|
647
|
+
],
|
|
648
|
+
darkviolet: [
|
|
649
|
+
148,
|
|
650
|
+
0,
|
|
651
|
+
211
|
|
652
|
+
],
|
|
653
|
+
deeppink: [
|
|
654
|
+
255,
|
|
655
|
+
20,
|
|
656
|
+
147
|
|
657
|
+
],
|
|
658
|
+
deepskyblue: [
|
|
659
|
+
0,
|
|
660
|
+
191,
|
|
661
|
+
255
|
|
662
|
+
],
|
|
663
|
+
dimgray: [
|
|
664
|
+
105,
|
|
665
|
+
105,
|
|
666
|
+
105
|
|
667
|
+
],
|
|
668
|
+
dimgrey: [
|
|
669
|
+
105,
|
|
670
|
+
105,
|
|
671
|
+
105
|
|
672
|
+
],
|
|
673
|
+
dodgerblue: [
|
|
674
|
+
30,
|
|
675
|
+
144,
|
|
676
|
+
255
|
|
677
|
+
],
|
|
678
|
+
firebrick: [
|
|
679
|
+
178,
|
|
680
|
+
34,
|
|
681
|
+
34
|
|
682
|
+
],
|
|
683
|
+
floralwhite: [
|
|
684
|
+
255,
|
|
685
|
+
250,
|
|
686
|
+
240
|
|
687
|
+
],
|
|
688
|
+
forestgreen: [
|
|
689
|
+
34,
|
|
690
|
+
139,
|
|
691
|
+
34
|
|
692
|
+
],
|
|
693
|
+
fuchsia: [
|
|
694
|
+
255,
|
|
695
|
+
0,
|
|
696
|
+
255
|
|
697
|
+
],
|
|
698
|
+
gainsboro: [
|
|
699
|
+
220,
|
|
700
|
+
220,
|
|
701
|
+
220
|
|
702
|
+
],
|
|
703
|
+
ghostwhite: [
|
|
704
|
+
248,
|
|
705
|
+
248,
|
|
706
|
+
255
|
|
707
|
+
],
|
|
708
|
+
gold: [
|
|
709
|
+
255,
|
|
710
|
+
215,
|
|
711
|
+
0
|
|
712
|
+
],
|
|
713
|
+
goldenrod: [
|
|
714
|
+
218,
|
|
715
|
+
165,
|
|
716
|
+
32
|
|
717
|
+
],
|
|
718
|
+
gray: [
|
|
719
|
+
128,
|
|
720
|
+
128,
|
|
721
|
+
128
|
|
722
|
+
],
|
|
723
|
+
green: [
|
|
724
|
+
0,
|
|
725
|
+
128,
|
|
726
|
+
0
|
|
727
|
+
],
|
|
728
|
+
greenyellow: [
|
|
729
|
+
173,
|
|
730
|
+
255,
|
|
731
|
+
47
|
|
732
|
+
],
|
|
733
|
+
grey: [
|
|
734
|
+
128,
|
|
735
|
+
128,
|
|
736
|
+
128
|
|
737
|
+
],
|
|
738
|
+
honeydew: [
|
|
739
|
+
240,
|
|
740
|
+
255,
|
|
741
|
+
240
|
|
742
|
+
],
|
|
743
|
+
hotpink: [
|
|
744
|
+
255,
|
|
745
|
+
105,
|
|
746
|
+
180
|
|
747
|
+
],
|
|
748
|
+
indianred: [
|
|
749
|
+
205,
|
|
750
|
+
92,
|
|
751
|
+
92
|
|
752
|
+
],
|
|
753
|
+
indigo: [
|
|
754
|
+
75,
|
|
755
|
+
0,
|
|
756
|
+
130
|
|
757
|
+
],
|
|
758
|
+
ivory: [
|
|
759
|
+
255,
|
|
760
|
+
255,
|
|
761
|
+
240
|
|
762
|
+
],
|
|
763
|
+
khaki: [
|
|
764
|
+
240,
|
|
765
|
+
230,
|
|
766
|
+
140
|
|
767
|
+
],
|
|
768
|
+
lavender: [
|
|
769
|
+
230,
|
|
770
|
+
230,
|
|
771
|
+
250
|
|
772
|
+
],
|
|
773
|
+
lavenderblush: [
|
|
774
|
+
255,
|
|
775
|
+
240,
|
|
776
|
+
245
|
|
777
|
+
],
|
|
778
|
+
lawngreen: [
|
|
779
|
+
124,
|
|
780
|
+
252,
|
|
781
|
+
0
|
|
782
|
+
],
|
|
783
|
+
lemonchiffon: [
|
|
784
|
+
255,
|
|
785
|
+
250,
|
|
786
|
+
205
|
|
787
|
+
],
|
|
788
|
+
lightblue: [
|
|
789
|
+
173,
|
|
790
|
+
216,
|
|
791
|
+
230
|
|
792
|
+
],
|
|
793
|
+
lightcoral: [
|
|
794
|
+
240,
|
|
795
|
+
128,
|
|
796
|
+
128
|
|
797
|
+
],
|
|
798
|
+
lightcyan: [
|
|
799
|
+
224,
|
|
800
|
+
255,
|
|
801
|
+
255
|
|
802
|
+
],
|
|
803
|
+
lightgoldenrodyellow: [
|
|
804
|
+
250,
|
|
805
|
+
250,
|
|
806
|
+
210
|
|
807
|
+
],
|
|
808
|
+
lightgray: [
|
|
809
|
+
211,
|
|
810
|
+
211,
|
|
811
|
+
211
|
|
812
|
+
],
|
|
813
|
+
lightgreen: [
|
|
814
|
+
144,
|
|
815
|
+
238,
|
|
816
|
+
144
|
|
817
|
+
],
|
|
818
|
+
lightgrey: [
|
|
819
|
+
211,
|
|
820
|
+
211,
|
|
821
|
+
211
|
|
822
|
+
],
|
|
823
|
+
lightpink: [
|
|
824
|
+
255,
|
|
825
|
+
182,
|
|
826
|
+
193
|
|
827
|
+
],
|
|
828
|
+
lightsalmon: [
|
|
829
|
+
255,
|
|
830
|
+
160,
|
|
831
|
+
122
|
|
832
|
+
],
|
|
833
|
+
lightseagreen: [
|
|
834
|
+
32,
|
|
835
|
+
178,
|
|
836
|
+
170
|
|
837
|
+
],
|
|
838
|
+
lightskyblue: [
|
|
839
|
+
135,
|
|
840
|
+
206,
|
|
841
|
+
250
|
|
842
|
+
],
|
|
843
|
+
lightslategray: [
|
|
844
|
+
119,
|
|
845
|
+
136,
|
|
846
|
+
153
|
|
847
|
+
],
|
|
848
|
+
lightslategrey: [
|
|
849
|
+
119,
|
|
850
|
+
136,
|
|
851
|
+
153
|
|
852
|
+
],
|
|
853
|
+
lightsteelblue: [
|
|
854
|
+
176,
|
|
855
|
+
196,
|
|
856
|
+
222
|
|
857
|
+
],
|
|
858
|
+
lightyellow: [
|
|
859
|
+
255,
|
|
860
|
+
255,
|
|
861
|
+
224
|
|
862
|
+
],
|
|
863
|
+
lime: [
|
|
864
|
+
0,
|
|
865
|
+
255,
|
|
866
|
+
0
|
|
867
|
+
],
|
|
868
|
+
limegreen: [
|
|
869
|
+
50,
|
|
870
|
+
205,
|
|
871
|
+
50
|
|
872
|
+
],
|
|
873
|
+
linen: [
|
|
874
|
+
250,
|
|
875
|
+
240,
|
|
876
|
+
230
|
|
877
|
+
],
|
|
878
|
+
magenta: [
|
|
879
|
+
255,
|
|
880
|
+
0,
|
|
881
|
+
255
|
|
882
|
+
],
|
|
883
|
+
maroon: [
|
|
884
|
+
128,
|
|
885
|
+
0,
|
|
886
|
+
0
|
|
887
|
+
],
|
|
888
|
+
mediumaquamarine: [
|
|
889
|
+
102,
|
|
890
|
+
205,
|
|
891
|
+
170
|
|
892
|
+
],
|
|
893
|
+
mediumblue: [
|
|
894
|
+
0,
|
|
895
|
+
0,
|
|
896
|
+
205
|
|
897
|
+
],
|
|
898
|
+
mediumorchid: [
|
|
899
|
+
186,
|
|
900
|
+
85,
|
|
901
|
+
211
|
|
902
|
+
],
|
|
903
|
+
mediumpurple: [
|
|
904
|
+
147,
|
|
905
|
+
112,
|
|
906
|
+
219
|
|
907
|
+
],
|
|
908
|
+
mediumseagreen: [
|
|
909
|
+
60,
|
|
910
|
+
179,
|
|
911
|
+
113
|
|
912
|
+
],
|
|
913
|
+
mediumslateblue: [
|
|
914
|
+
123,
|
|
915
|
+
104,
|
|
916
|
+
238
|
|
917
|
+
],
|
|
918
|
+
mediumspringgreen: [
|
|
919
|
+
0,
|
|
920
|
+
250,
|
|
921
|
+
154
|
|
922
|
+
],
|
|
923
|
+
mediumturquoise: [
|
|
924
|
+
72,
|
|
925
|
+
209,
|
|
926
|
+
204
|
|
927
|
+
],
|
|
928
|
+
mediumvioletred: [
|
|
929
|
+
199,
|
|
930
|
+
21,
|
|
931
|
+
133
|
|
932
|
+
],
|
|
933
|
+
midnightblue: [
|
|
934
|
+
25,
|
|
935
|
+
25,
|
|
936
|
+
112
|
|
937
|
+
],
|
|
938
|
+
mintcream: [
|
|
939
|
+
245,
|
|
940
|
+
255,
|
|
941
|
+
250
|
|
942
|
+
],
|
|
943
|
+
mistyrose: [
|
|
944
|
+
255,
|
|
945
|
+
228,
|
|
946
|
+
225
|
|
947
|
+
],
|
|
948
|
+
moccasin: [
|
|
949
|
+
255,
|
|
950
|
+
228,
|
|
951
|
+
181
|
|
952
|
+
],
|
|
953
|
+
navajowhite: [
|
|
954
|
+
255,
|
|
955
|
+
222,
|
|
956
|
+
173
|
|
957
|
+
],
|
|
958
|
+
navy: [
|
|
959
|
+
0,
|
|
960
|
+
0,
|
|
961
|
+
128
|
|
962
|
+
],
|
|
963
|
+
oldlace: [
|
|
964
|
+
253,
|
|
965
|
+
245,
|
|
966
|
+
230
|
|
967
|
+
],
|
|
968
|
+
olive: [
|
|
969
|
+
128,
|
|
970
|
+
128,
|
|
971
|
+
0
|
|
972
|
+
],
|
|
973
|
+
olivedrab: [
|
|
974
|
+
107,
|
|
975
|
+
142,
|
|
976
|
+
35
|
|
977
|
+
],
|
|
978
|
+
orange: [
|
|
979
|
+
255,
|
|
980
|
+
165,
|
|
981
|
+
0
|
|
982
|
+
],
|
|
983
|
+
orangered: [
|
|
984
|
+
255,
|
|
985
|
+
69,
|
|
986
|
+
0
|
|
987
|
+
],
|
|
988
|
+
orchid: [
|
|
989
|
+
218,
|
|
990
|
+
112,
|
|
991
|
+
214
|
|
992
|
+
],
|
|
993
|
+
palegoldenrod: [
|
|
994
|
+
238,
|
|
995
|
+
232,
|
|
996
|
+
170
|
|
997
|
+
],
|
|
998
|
+
palegreen: [
|
|
999
|
+
152,
|
|
1000
|
+
251,
|
|
1001
|
+
152
|
|
1002
|
+
],
|
|
1003
|
+
paleturquoise: [
|
|
1004
|
+
175,
|
|
1005
|
+
238,
|
|
1006
|
+
238
|
|
1007
|
+
],
|
|
1008
|
+
palevioletred: [
|
|
1009
|
+
219,
|
|
1010
|
+
112,
|
|
1011
|
+
147
|
|
1012
|
+
],
|
|
1013
|
+
papayawhip: [
|
|
1014
|
+
255,
|
|
1015
|
+
239,
|
|
1016
|
+
213
|
|
1017
|
+
],
|
|
1018
|
+
peachpuff: [
|
|
1019
|
+
255,
|
|
1020
|
+
218,
|
|
1021
|
+
185
|
|
1022
|
+
],
|
|
1023
|
+
peru: [
|
|
1024
|
+
205,
|
|
1025
|
+
133,
|
|
1026
|
+
63
|
|
1027
|
+
],
|
|
1028
|
+
pink: [
|
|
1029
|
+
255,
|
|
1030
|
+
192,
|
|
1031
|
+
203
|
|
1032
|
+
],
|
|
1033
|
+
plum: [
|
|
1034
|
+
221,
|
|
1035
|
+
160,
|
|
1036
|
+
221
|
|
1037
|
+
],
|
|
1038
|
+
powderblue: [
|
|
1039
|
+
176,
|
|
1040
|
+
224,
|
|
1041
|
+
230
|
|
1042
|
+
],
|
|
1043
|
+
purple: [
|
|
1044
|
+
128,
|
|
1045
|
+
0,
|
|
1046
|
+
128
|
|
1047
|
+
],
|
|
1048
|
+
rebeccapurple: [
|
|
1049
|
+
102,
|
|
1050
|
+
51,
|
|
1051
|
+
153
|
|
1052
|
+
],
|
|
1053
|
+
red: [
|
|
1054
|
+
255,
|
|
1055
|
+
0,
|
|
1056
|
+
0
|
|
1057
|
+
],
|
|
1058
|
+
rosybrown: [
|
|
1059
|
+
188,
|
|
1060
|
+
143,
|
|
1061
|
+
143
|
|
1062
|
+
],
|
|
1063
|
+
royalblue: [
|
|
1064
|
+
65,
|
|
1065
|
+
105,
|
|
1066
|
+
225
|
|
1067
|
+
],
|
|
1068
|
+
saddlebrown: [
|
|
1069
|
+
139,
|
|
1070
|
+
69,
|
|
1071
|
+
19
|
|
1072
|
+
],
|
|
1073
|
+
salmon: [
|
|
1074
|
+
250,
|
|
1075
|
+
128,
|
|
1076
|
+
114
|
|
1077
|
+
],
|
|
1078
|
+
sandybrown: [
|
|
1079
|
+
244,
|
|
1080
|
+
164,
|
|
1081
|
+
96
|
|
1082
|
+
],
|
|
1083
|
+
seagreen: [
|
|
1084
|
+
46,
|
|
1085
|
+
139,
|
|
1086
|
+
87
|
|
1087
|
+
],
|
|
1088
|
+
seashell: [
|
|
1089
|
+
255,
|
|
1090
|
+
245,
|
|
1091
|
+
238
|
|
1092
|
+
],
|
|
1093
|
+
sienna: [
|
|
1094
|
+
160,
|
|
1095
|
+
82,
|
|
1096
|
+
45
|
|
1097
|
+
],
|
|
1098
|
+
silver: [
|
|
1099
|
+
192,
|
|
1100
|
+
192,
|
|
1101
|
+
192
|
|
1102
|
+
],
|
|
1103
|
+
skyblue: [
|
|
1104
|
+
135,
|
|
1105
|
+
206,
|
|
1106
|
+
235
|
|
1107
|
+
],
|
|
1108
|
+
slateblue: [
|
|
1109
|
+
106,
|
|
1110
|
+
90,
|
|
1111
|
+
205
|
|
1112
|
+
],
|
|
1113
|
+
slategray: [
|
|
1114
|
+
112,
|
|
1115
|
+
128,
|
|
1116
|
+
144
|
|
1117
|
+
],
|
|
1118
|
+
slategrey: [
|
|
1119
|
+
112,
|
|
1120
|
+
128,
|
|
1121
|
+
144
|
|
1122
|
+
],
|
|
1123
|
+
snow: [
|
|
1124
|
+
255,
|
|
1125
|
+
250,
|
|
1126
|
+
250
|
|
1127
|
+
],
|
|
1128
|
+
springgreen: [
|
|
1129
|
+
0,
|
|
1130
|
+
255,
|
|
1131
|
+
127
|
|
1132
|
+
],
|
|
1133
|
+
steelblue: [
|
|
1134
|
+
70,
|
|
1135
|
+
130,
|
|
1136
|
+
180
|
|
1137
|
+
],
|
|
1138
|
+
tan: [
|
|
1139
|
+
210,
|
|
1140
|
+
180,
|
|
1141
|
+
140
|
|
1142
|
+
],
|
|
1143
|
+
teal: [
|
|
1144
|
+
0,
|
|
1145
|
+
128,
|
|
1146
|
+
128
|
|
1147
|
+
],
|
|
1148
|
+
thistle: [
|
|
1149
|
+
216,
|
|
1150
|
+
191,
|
|
1151
|
+
216
|
|
1152
|
+
],
|
|
1153
|
+
tomato: [
|
|
1154
|
+
255,
|
|
1155
|
+
99,
|
|
1156
|
+
71
|
|
1157
|
+
],
|
|
1158
|
+
turquoise: [
|
|
1159
|
+
64,
|
|
1160
|
+
224,
|
|
1161
|
+
208
|
|
1162
|
+
],
|
|
1163
|
+
violet: [
|
|
1164
|
+
238,
|
|
1165
|
+
130,
|
|
1166
|
+
238
|
|
1167
|
+
],
|
|
1168
|
+
wheat: [
|
|
1169
|
+
245,
|
|
1170
|
+
222,
|
|
1171
|
+
179
|
|
1172
|
+
],
|
|
1173
|
+
white: [
|
|
1174
|
+
255,
|
|
1175
|
+
255,
|
|
1176
|
+
255
|
|
1177
|
+
],
|
|
1178
|
+
whitesmoke: [
|
|
1179
|
+
245,
|
|
1180
|
+
245,
|
|
1181
|
+
245
|
|
1182
|
+
],
|
|
1183
|
+
yellow: [
|
|
1184
|
+
255,
|
|
1185
|
+
255,
|
|
1186
|
+
0
|
|
1187
|
+
],
|
|
1188
|
+
yellowgreen: [
|
|
1189
|
+
154,
|
|
1190
|
+
205,
|
|
1191
|
+
50
|
|
1192
|
+
]
|
|
1193
|
+
};
|
|
1194
|
+
//#endregion
|
|
1195
|
+
//#region src/build/core/parse.ts
|
|
1196
|
+
function parseColor(color) {
|
|
1197
|
+
const c = color.trim();
|
|
1198
|
+
if (c.indexOf("rgba(") === 0) {
|
|
1199
|
+
const p = c.slice(5, c.indexOf(")")).split(",");
|
|
1200
|
+
return [
|
|
1201
|
+
+(p[0] ?? "0"),
|
|
1202
|
+
+(p[1] ?? "0"),
|
|
1203
|
+
+(p[2] ?? "0"),
|
|
1204
|
+
+(p[3] ?? "1")
|
|
1205
|
+
];
|
|
1206
|
+
}
|
|
1207
|
+
if (c.indexOf("rgb(") === 0) {
|
|
1208
|
+
const p = c.slice(4, c.indexOf(")")).split(",");
|
|
1209
|
+
return [
|
|
1210
|
+
+(p[0] ?? "0"),
|
|
1211
|
+
+(p[1] ?? "0"),
|
|
1212
|
+
+(p[2] ?? "0"),
|
|
1213
|
+
1
|
|
1214
|
+
];
|
|
1215
|
+
}
|
|
1216
|
+
if (c.charCodeAt(0) === 35) {
|
|
1217
|
+
if (c.length === 9) return [
|
|
1218
|
+
parseInt(c.slice(1, 3), 16),
|
|
1219
|
+
parseInt(c.slice(3, 5), 16),
|
|
1220
|
+
parseInt(c.slice(5, 7), 16),
|
|
1221
|
+
parseInt(c.slice(7, 9), 16) / 255
|
|
1222
|
+
];
|
|
1223
|
+
if (c.length === 7) return [
|
|
1224
|
+
parseInt(c.slice(1, 3), 16),
|
|
1225
|
+
parseInt(c.slice(3, 5), 16),
|
|
1226
|
+
parseInt(c.slice(5, 7), 16),
|
|
1227
|
+
1
|
|
1228
|
+
];
|
|
1229
|
+
if (c.length === 4) return [
|
|
1230
|
+
parseInt(c.slice(1, 2), 16) * 17,
|
|
1231
|
+
parseInt(c.slice(2, 3), 16) * 17,
|
|
1232
|
+
parseInt(c.slice(3, 4), 16) * 17,
|
|
1233
|
+
1
|
|
1234
|
+
];
|
|
1235
|
+
}
|
|
1236
|
+
const named = CSS_NAMED_COLORS[c.toLowerCase()];
|
|
1237
|
+
if (named !== void 0) return [
|
|
1238
|
+
named[0],
|
|
1239
|
+
named[1],
|
|
1240
|
+
named[2],
|
|
1241
|
+
1
|
|
1242
|
+
];
|
|
1243
|
+
return [
|
|
1244
|
+
0,
|
|
1245
|
+
0,
|
|
1246
|
+
0,
|
|
1247
|
+
1
|
|
1248
|
+
];
|
|
1249
|
+
}
|
|
1250
|
+
//#endregion
|
|
1251
|
+
//#region src/build/core/glyph/parse.ts
|
|
1252
|
+
function resolveInheritedFill(el) {
|
|
1253
|
+
let current = el.parentElement;
|
|
1254
|
+
while (current !== null) {
|
|
1255
|
+
const fill = current.getAttribute("fill");
|
|
1256
|
+
if (fill !== null && fill !== "inherit") return fill;
|
|
1257
|
+
current = current.parentElement;
|
|
1258
|
+
}
|
|
1259
|
+
return "black";
|
|
1260
|
+
}
|
|
1261
|
+
function calculateOpColor(fill, opacity, el) {
|
|
1262
|
+
const [r, g, b, a] = parseColor(fill ?? resolveInheritedFill(el));
|
|
1263
|
+
return `rgba(${r},${g},${b},${+(a * opacity).toFixed(4)})`;
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* If a flattened path lost its initial moveto (e.g. an empty `Mx y z`
|
|
1267
|
+
* subpath was dropped), prepend `M` using the path's last coordinate pair.
|
|
1268
|
+
* For closed icon shapes the endpoint equals the start point.
|
|
1269
|
+
*/
|
|
1270
|
+
function sanitizePathData(d) {
|
|
1271
|
+
const trimmed = d.trim();
|
|
1272
|
+
if (!trimmed || SVG_MOVE_PREFIX.test(trimmed)) return {
|
|
1273
|
+
d: trimmed,
|
|
1274
|
+
sanitized: false
|
|
1275
|
+
};
|
|
1276
|
+
const nums = trimmed.replace(SVG_TRAILING_CLOSE, "").match(SVG_NUMBER);
|
|
1277
|
+
if (!nums || nums.length < 2) return {
|
|
1278
|
+
d: trimmed,
|
|
1279
|
+
sanitized: false
|
|
1280
|
+
};
|
|
1281
|
+
return {
|
|
1282
|
+
d: `M${nums[nums.length - 2]},${nums[nums.length - 1]} ${trimmed}`,
|
|
1283
|
+
sanitized: true
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
var parsePath = (p) => {
|
|
1287
|
+
const d = p.getAttribute("d") ?? "";
|
|
1288
|
+
const op = p.getAttribute("opacity");
|
|
1289
|
+
const fillOp = p.getAttribute("fill-opacity");
|
|
1290
|
+
const fill = p.getAttribute("fill");
|
|
1291
|
+
const fillRule = p.getAttribute("fill-rule") === "evenodd" || p.getAttribute("clip-rule") === "evenodd" ? "evenodd" : void 0;
|
|
1292
|
+
if (op !== null || fillOp !== null) return {
|
|
1293
|
+
d,
|
|
1294
|
+
fill: calculateOpColor(fill, (op !== null ? parseFloat(op) : 1) * (fillOp !== null ? parseFloat(fillOp) : 1), p),
|
|
1295
|
+
fillRule
|
|
1296
|
+
};
|
|
1297
|
+
return {
|
|
1298
|
+
d,
|
|
1299
|
+
fill,
|
|
1300
|
+
fillRule
|
|
1301
|
+
};
|
|
1302
|
+
};
|
|
1303
|
+
function parseFlattenedSvg(flattenedSvg, options) {
|
|
1304
|
+
const svgEl = new DOMParser().parseFromString(flattenedSvg, "image/svg+xml").documentElement;
|
|
1305
|
+
const viewBoxRaw = svgEl?.getAttribute("viewBox")?.split(WHITESPACE).map(Number);
|
|
1306
|
+
let viewBox;
|
|
1307
|
+
if (viewBoxRaw?.length === 4 && viewBoxRaw.every(Number.isFinite)) viewBox = [
|
|
1308
|
+
viewBoxRaw[0],
|
|
1309
|
+
viewBoxRaw[1],
|
|
1310
|
+
viewBoxRaw[2],
|
|
1311
|
+
viewBoxRaw[3]
|
|
1312
|
+
];
|
|
1313
|
+
else {
|
|
1314
|
+
const width = parseFloat(svgEl?.getAttribute("width") ?? "");
|
|
1315
|
+
const height = parseFloat(svgEl?.getAttribute("height") ?? "");
|
|
1316
|
+
viewBox = width > 0 && height > 0 ? [
|
|
1317
|
+
0,
|
|
1318
|
+
0,
|
|
1319
|
+
width,
|
|
1320
|
+
height
|
|
1321
|
+
] : [
|
|
1322
|
+
0,
|
|
1323
|
+
0,
|
|
1324
|
+
100,
|
|
1325
|
+
100
|
|
1326
|
+
];
|
|
1327
|
+
options?.onMissingViewBox?.(viewBox);
|
|
1328
|
+
}
|
|
1329
|
+
const paths = (svgEl ? Array.from(svgEl.getElementsByTagName("path")) : []).map(parsePath).filter((p) => p.d.trim() !== "").map((p) => {
|
|
1330
|
+
const { d, sanitized } = sanitizePathData(p.d);
|
|
1331
|
+
if (sanitized) options?.onSanitize?.(p.d);
|
|
1332
|
+
return {
|
|
1333
|
+
...p,
|
|
1334
|
+
d
|
|
1335
|
+
};
|
|
1336
|
+
});
|
|
1337
|
+
return {
|
|
1338
|
+
viewBox,
|
|
1339
|
+
paths
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
function shouldSkipPath(d, fill) {
|
|
1343
|
+
if (!d || d.trim() === "") return true;
|
|
1344
|
+
const f = (fill ?? "").trim().toLowerCase();
|
|
1345
|
+
return f === "transparent" || f === "none";
|
|
1346
|
+
}
|
|
1347
|
+
//#endregion
|
|
1348
|
+
//#region src/build/core/glyph/placement.ts
|
|
1349
|
+
function roundInt(n) {
|
|
1350
|
+
return Math.round(n);
|
|
1351
|
+
}
|
|
1352
|
+
function computePlacement(opts) {
|
|
1353
|
+
const [vx, vy, vw, vh] = opts.viewBox;
|
|
1354
|
+
const safeVh = vh === 0 ? 1 : vh;
|
|
1355
|
+
const scale = opts.safeZone / safeVh;
|
|
1356
|
+
const totalPadding = (opts.upm - opts.safeZone) / 2;
|
|
1357
|
+
const scaledW = vw * scale;
|
|
1358
|
+
const scaledH = vh * scale;
|
|
1359
|
+
const adv = Math.max(1, roundInt(scaledW + totalPadding * 2));
|
|
1360
|
+
return {
|
|
1361
|
+
vx,
|
|
1362
|
+
vy,
|
|
1363
|
+
scale,
|
|
1364
|
+
xOff: (adv - scaledW) / 2,
|
|
1365
|
+
yOff: (opts.upm - scaledH) / 2,
|
|
1366
|
+
adv
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Transform an SVG path `d` string from source SVG coordinates into font
|
|
1371
|
+
* glyph coordinates (Y-up, with placement scaling and centering applied).
|
|
1372
|
+
*
|
|
1373
|
+
* Combines placement transform (translate + scale + translate) with the
|
|
1374
|
+
* SVG→font Y-axis flip into a single affine transform applied via PathKit.
|
|
1375
|
+
*/
|
|
1376
|
+
function transformPathForFont(PathKit, d, opts) {
|
|
1377
|
+
const { vx, vy, scale, xOff, yOff, upm } = opts;
|
|
1378
|
+
const p = PathKit.FromSVGString(d);
|
|
1379
|
+
if (!p) return d;
|
|
1380
|
+
const scaleX = scale;
|
|
1381
|
+
const scaleY = -scale;
|
|
1382
|
+
const transX = xOff - vx * scale;
|
|
1383
|
+
const transY = upm - yOff + vy * scale;
|
|
1384
|
+
p.transform(scaleX, 0, transX, 0, scaleY, transY, 0, 0, 1);
|
|
1385
|
+
const result = p.toSVGString();
|
|
1386
|
+
p.delete?.();
|
|
1387
|
+
return result;
|
|
1388
|
+
}
|
|
1389
|
+
//#endregion
|
|
1390
|
+
//#region src/build/core/flatten/geometry.ts
|
|
1391
|
+
var DEFAULT_ALMOST_EQUAL_TOLERANCE = 1e-9;
|
|
1392
|
+
function pt(x = 0, y = 0) {
|
|
1393
|
+
return {
|
|
1394
|
+
x,
|
|
1395
|
+
y
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
function almostEqual(c1, c2, tolerance = DEFAULT_ALMOST_EQUAL_TOLERANCE) {
|
|
1399
|
+
return Math.abs(c1 - c2) <= tolerance;
|
|
1400
|
+
}
|
|
1401
|
+
function ptEquals(a, b) {
|
|
1402
|
+
return a.x === b.x && a.y === b.y;
|
|
1403
|
+
}
|
|
1404
|
+
function ptAlmostEquals(a, b, tolerance = DEFAULT_ALMOST_EQUAL_TOLERANCE) {
|
|
1405
|
+
return almostEqual(a.x, b.x, tolerance) && almostEqual(a.y, b.y, tolerance);
|
|
1406
|
+
}
|
|
1407
|
+
function ptSub(a, b) {
|
|
1408
|
+
return {
|
|
1409
|
+
x: a.x - b.x,
|
|
1410
|
+
y: a.y - b.y
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
function ptAdd(a, b) {
|
|
1414
|
+
return {
|
|
1415
|
+
x: a.x + b.x,
|
|
1416
|
+
y: a.y + b.y
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
function ptMul(a, scalar) {
|
|
1420
|
+
return {
|
|
1421
|
+
x: a.x * scalar,
|
|
1422
|
+
y: a.y * scalar
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
function pythonRound(x, ndigits) {
|
|
1426
|
+
if (!Number.isFinite(x) || Number.isInteger(x) && ndigits >= 0) return x;
|
|
1427
|
+
const buf = /* @__PURE__ */ new DataView(/* @__PURE__ */ new ArrayBuffer(8));
|
|
1428
|
+
buf.setFloat64(0, x);
|
|
1429
|
+
const bits = buf.getBigUint64(0);
|
|
1430
|
+
const negative = bits >> 63n === 1n;
|
|
1431
|
+
const expBits = Number(bits >> 52n & 2047n);
|
|
1432
|
+
const fracBits = bits & 4503599627370495n;
|
|
1433
|
+
let mantissa;
|
|
1434
|
+
let exp;
|
|
1435
|
+
if (expBits === 0) {
|
|
1436
|
+
mantissa = fracBits;
|
|
1437
|
+
exp = -1074;
|
|
1438
|
+
} else {
|
|
1439
|
+
mantissa = fracBits | 1n << 52n;
|
|
1440
|
+
exp = expBits - 1075;
|
|
1441
|
+
}
|
|
1442
|
+
const pow10 = 10n ** BigInt(Math.abs(ndigits));
|
|
1443
|
+
let num = mantissa * (ndigits >= 0 ? pow10 : 1n);
|
|
1444
|
+
let den = ndigits >= 0 ? 1n : pow10;
|
|
1445
|
+
if (exp >= 0) num <<= BigInt(exp);
|
|
1446
|
+
else den <<= BigInt(-exp);
|
|
1447
|
+
let k = num / den;
|
|
1448
|
+
const twice = num % den * 2n;
|
|
1449
|
+
if (twice > den || twice === den && k % 2n === 1n) k += 1n;
|
|
1450
|
+
const scaled = ndigits >= 0 ? Number(k) / Number(pow10) : Number(k) * Number(pow10);
|
|
1451
|
+
return negative ? -scaled : scaled;
|
|
1452
|
+
}
|
|
1453
|
+
function ntos(n) {
|
|
1454
|
+
return Object.is(n, -0) ? "0" : String(n);
|
|
1455
|
+
}
|
|
1456
|
+
function rectEmpty(r) {
|
|
1457
|
+
return r.w === 0 || r.h === 0;
|
|
1458
|
+
}
|
|
1459
|
+
//#endregion
|
|
1460
|
+
//#region src/build/core/flatten/transform.ts
|
|
1461
|
+
var RADIAN_ARG_OPS = /* @__PURE__ */ new Set([
|
|
1462
|
+
"rotate",
|
|
1463
|
+
"skewx",
|
|
1464
|
+
"skewy"
|
|
1465
|
+
]);
|
|
1466
|
+
var Affine2D = class Affine2D {
|
|
1467
|
+
a;
|
|
1468
|
+
b;
|
|
1469
|
+
c;
|
|
1470
|
+
d;
|
|
1471
|
+
e;
|
|
1472
|
+
f;
|
|
1473
|
+
constructor(a, b, c, d, e, f) {
|
|
1474
|
+
this.a = a;
|
|
1475
|
+
this.b = b;
|
|
1476
|
+
this.c = c;
|
|
1477
|
+
this.d = d;
|
|
1478
|
+
this.e = e;
|
|
1479
|
+
this.f = f;
|
|
1480
|
+
}
|
|
1481
|
+
static _identity = new Affine2D(1, 0, 0, 1, 0, 0);
|
|
1482
|
+
static _degenerate = new Affine2D(0, 0, 0, 0, 0, 0);
|
|
1483
|
+
static identity() {
|
|
1484
|
+
return Affine2D._identity;
|
|
1485
|
+
}
|
|
1486
|
+
static degenerate() {
|
|
1487
|
+
return Affine2D._degenerate;
|
|
1488
|
+
}
|
|
1489
|
+
static fromString(rawTransform) {
|
|
1490
|
+
let transform = Affine2D.identity();
|
|
1491
|
+
const re = new RegExp(TRANSFORM_FUNCTION);
|
|
1492
|
+
let match;
|
|
1493
|
+
while ((match = re.exec(rawTransform)) !== null) {
|
|
1494
|
+
const op = match[1].toLowerCase();
|
|
1495
|
+
const args = match[2].trim().split(TRANSFORM_ARG_SEPARATOR).map((p) => parseFloat(p));
|
|
1496
|
+
if (RADIAN_ARG_OPS.has(op)) args[0] = args[0] * Math.PI / 180;
|
|
1497
|
+
switch (op) {
|
|
1498
|
+
case "matrix":
|
|
1499
|
+
transform = transform.matrix(args[0], args[1], args[2], args[3], args[4], args[5]);
|
|
1500
|
+
break;
|
|
1501
|
+
case "translate":
|
|
1502
|
+
transform = transform.translate(args[0], args[1]);
|
|
1503
|
+
break;
|
|
1504
|
+
case "scale":
|
|
1505
|
+
transform = transform.scale(args[0], args[1]);
|
|
1506
|
+
break;
|
|
1507
|
+
case "rotate":
|
|
1508
|
+
transform = transform.rotate(args[0], args[1], args[2]);
|
|
1509
|
+
break;
|
|
1510
|
+
case "skewx":
|
|
1511
|
+
transform = transform.skewx(args[0]);
|
|
1512
|
+
break;
|
|
1513
|
+
case "skewy": transform = transform.skewy(args[0]);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
return transform;
|
|
1517
|
+
}
|
|
1518
|
+
values() {
|
|
1519
|
+
return [
|
|
1520
|
+
this.a,
|
|
1521
|
+
this.b,
|
|
1522
|
+
this.c,
|
|
1523
|
+
this.d,
|
|
1524
|
+
this.e,
|
|
1525
|
+
this.f
|
|
1526
|
+
];
|
|
1527
|
+
}
|
|
1528
|
+
equals(other) {
|
|
1529
|
+
return this.a === other.a && this.b === other.b && this.c === other.c && this.d === other.d && this.e === other.e && this.f === other.f;
|
|
1530
|
+
}
|
|
1531
|
+
toString() {
|
|
1532
|
+
const [tx, ty] = this.getTranslate();
|
|
1533
|
+
if (this.equals(Affine2D.identity().translate(tx, ty))) return `translate(${[tx, ty].map(ntos).join(", ")})`;
|
|
1534
|
+
return `matrix(${this.values().map(ntos).join(" ")})`;
|
|
1535
|
+
}
|
|
1536
|
+
matMul(other) {
|
|
1537
|
+
return new Affine2D(this.a * other.a + this.c * other.b, this.b * other.a + this.d * other.b, this.a * other.c + this.c * other.d, this.b * other.c + this.d * other.d, this.a * other.e + this.c * other.f + this.e, this.b * other.e + this.d * other.f + this.f);
|
|
1538
|
+
}
|
|
1539
|
+
matrix(a, b, c, d, e, f) {
|
|
1540
|
+
return this.matMul(new Affine2D(a, b, c, d, e, f));
|
|
1541
|
+
}
|
|
1542
|
+
translate(tx, ty = 0) {
|
|
1543
|
+
if (tx === 0 && ty === 0) return this;
|
|
1544
|
+
return this.matrix(1, 0, 0, 1, tx, ty);
|
|
1545
|
+
}
|
|
1546
|
+
getTranslate() {
|
|
1547
|
+
return [this.e, this.f];
|
|
1548
|
+
}
|
|
1549
|
+
scale(sx, sy) {
|
|
1550
|
+
if (sy === void 0) sy = sx;
|
|
1551
|
+
return this.matrix(sx, 0, 0, sy, 0, 0);
|
|
1552
|
+
}
|
|
1553
|
+
rotate(a, cx = 0, cy = 0) {
|
|
1554
|
+
return this.translate(cx, cy).matrix(Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a), 0, 0).translate(-cx, -cy);
|
|
1555
|
+
}
|
|
1556
|
+
skewx(a) {
|
|
1557
|
+
return this.matrix(1, 0, Math.tan(a), 1, 0, 0);
|
|
1558
|
+
}
|
|
1559
|
+
skewy(a) {
|
|
1560
|
+
return this.matrix(1, Math.tan(a), 0, 1, 0, 0);
|
|
1561
|
+
}
|
|
1562
|
+
determinant() {
|
|
1563
|
+
return this.a * this.d - this.b * this.c;
|
|
1564
|
+
}
|
|
1565
|
+
isDegenerate() {
|
|
1566
|
+
return Math.abs(this.determinant()) <= Number.EPSILON;
|
|
1567
|
+
}
|
|
1568
|
+
inverse() {
|
|
1569
|
+
if (this.equals(Affine2D.identity())) return this;
|
|
1570
|
+
else if (this.isDegenerate()) return Affine2D.degenerate();
|
|
1571
|
+
const det = this.determinant();
|
|
1572
|
+
const a = this.d / det;
|
|
1573
|
+
const b = -this.b / det;
|
|
1574
|
+
const c = -this.c / det;
|
|
1575
|
+
const d = this.a / det;
|
|
1576
|
+
const e = -a * this.e - c * this.f;
|
|
1577
|
+
const f = -b * this.e - d * this.f;
|
|
1578
|
+
return new Affine2D(a, b, c, d, e, f);
|
|
1579
|
+
}
|
|
1580
|
+
mapPoint(p) {
|
|
1581
|
+
return {
|
|
1582
|
+
x: this.a * p.x + this.c * p.y + this.e,
|
|
1583
|
+
y: this.b * p.x + this.d * p.y + this.f
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
mapVector(v) {
|
|
1587
|
+
return {
|
|
1588
|
+
x: this.a * v.x + this.c * v.y,
|
|
1589
|
+
y: this.b * v.x + this.d * v.y
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
static composeLtr(affines) {
|
|
1593
|
+
let result = Affine2D.identity();
|
|
1594
|
+
for (let i = affines.length - 1; i >= 0; i--) result = result.matMul(affines[i]);
|
|
1595
|
+
return result;
|
|
1596
|
+
}
|
|
1597
|
+
static _ALIGN_VALUES = /* @__PURE__ */ new Set([
|
|
1598
|
+
"none",
|
|
1599
|
+
"xminymin",
|
|
1600
|
+
"xminymid",
|
|
1601
|
+
"xminymax",
|
|
1602
|
+
"xmidymin",
|
|
1603
|
+
"xmidymid",
|
|
1604
|
+
"xmidymax",
|
|
1605
|
+
"xmaxymin",
|
|
1606
|
+
"xmaxymid",
|
|
1607
|
+
"xmaxymax"
|
|
1608
|
+
]);
|
|
1609
|
+
static _MEET_OR_SLICE = /* @__PURE__ */ new Set(["meet", "slice"]);
|
|
1610
|
+
static rectToRect(src, dst, preserveAspectRatio = "none") {
|
|
1611
|
+
if (rectEmpty(src)) return Affine2D.identity();
|
|
1612
|
+
if (rectEmpty(dst)) return new Affine2D(0, 0, 0, 0, 0, 0);
|
|
1613
|
+
let sx = dst.w / src.w;
|
|
1614
|
+
let sy = dst.h / src.h;
|
|
1615
|
+
const normalized = preserveAspectRatio.toLowerCase().trim();
|
|
1616
|
+
const spaceIdx = normalized.indexOf(" ");
|
|
1617
|
+
const align = spaceIdx === -1 ? normalized : normalized.slice(0, spaceIdx);
|
|
1618
|
+
const meetOrSlice = spaceIdx === -1 ? "" : normalized.slice(spaceIdx + 1).trim();
|
|
1619
|
+
if (!Affine2D._ALIGN_VALUES.has(align) || meetOrSlice && !Affine2D._MEET_OR_SLICE.has(meetOrSlice)) throw new Error(`Invalid preserveAspectRatio: ${preserveAspectRatio}`);
|
|
1620
|
+
if (align !== "none") sx = sy = meetOrSlice.includes("slice") ? Math.max(sx, sy) : Math.min(sx, sy);
|
|
1621
|
+
let tx = dst.x - src.x * sx;
|
|
1622
|
+
let ty = dst.y - src.y * sy;
|
|
1623
|
+
if (align.includes("xmid")) tx += (dst.w - src.w * sx) / 2;
|
|
1624
|
+
else if (align.includes("xmax")) tx += dst.w - src.w * sx;
|
|
1625
|
+
if (align.includes("ymid")) ty += (dst.h - src.h * sy) / 2;
|
|
1626
|
+
else if (align.includes("ymax")) ty += dst.h - src.h * sy;
|
|
1627
|
+
return new Affine2D(sx, 0, 0, sy, tx, ty);
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
//#endregion
|
|
1631
|
+
//#region src/build/core/flatten/inherit.ts
|
|
1632
|
+
var ATTRIB_DEFAULTS = {
|
|
1633
|
+
"clip-path": "",
|
|
1634
|
+
"clip-rule": "nonzero",
|
|
1635
|
+
fill: "black",
|
|
1636
|
+
"fill-opacity": 1,
|
|
1637
|
+
"fill-rule": "nonzero",
|
|
1638
|
+
stroke: "none",
|
|
1639
|
+
"stroke-width": 1,
|
|
1640
|
+
"stroke-linecap": "butt",
|
|
1641
|
+
"stroke-linejoin": "miter",
|
|
1642
|
+
"stroke-miterlimit": 4,
|
|
1643
|
+
"stroke-dasharray": "none",
|
|
1644
|
+
"stroke-dashoffset": 0,
|
|
1645
|
+
"stroke-opacity": 1,
|
|
1646
|
+
opacity: 1,
|
|
1647
|
+
transform: "",
|
|
1648
|
+
style: "",
|
|
1649
|
+
display: "inline",
|
|
1650
|
+
d: "",
|
|
1651
|
+
id: ""
|
|
1652
|
+
};
|
|
1653
|
+
function attribDefault(name) {
|
|
1654
|
+
const value = ATTRIB_DEFAULTS[name];
|
|
1655
|
+
if (value === void 0) throw new Error(`No entry for '${name}' and no default given`);
|
|
1656
|
+
return value;
|
|
1657
|
+
}
|
|
1658
|
+
var COMMON_SHAPE_FIELDS = [
|
|
1659
|
+
"id",
|
|
1660
|
+
"clip_path",
|
|
1661
|
+
"clip_rule",
|
|
1662
|
+
"fill",
|
|
1663
|
+
"fill_opacity",
|
|
1664
|
+
"fill_rule",
|
|
1665
|
+
"stroke",
|
|
1666
|
+
"stroke_width",
|
|
1667
|
+
"stroke_linecap",
|
|
1668
|
+
"stroke_linejoin",
|
|
1669
|
+
"stroke_miterlimit",
|
|
1670
|
+
"stroke_dasharray",
|
|
1671
|
+
"stroke_dashoffset",
|
|
1672
|
+
"stroke_opacity",
|
|
1673
|
+
"opacity",
|
|
1674
|
+
"transform",
|
|
1675
|
+
"style",
|
|
1676
|
+
"display"
|
|
1677
|
+
];
|
|
1678
|
+
var VALID_FIELDS = {
|
|
1679
|
+
path: /* @__PURE__ */ new Set([...COMMON_SHAPE_FIELDS, "d"]),
|
|
1680
|
+
circle: /* @__PURE__ */ new Set([
|
|
1681
|
+
...COMMON_SHAPE_FIELDS,
|
|
1682
|
+
"r",
|
|
1683
|
+
"cx",
|
|
1684
|
+
"cy"
|
|
1685
|
+
]),
|
|
1686
|
+
ellipse: /* @__PURE__ */ new Set([
|
|
1687
|
+
...COMMON_SHAPE_FIELDS,
|
|
1688
|
+
"rx",
|
|
1689
|
+
"ry",
|
|
1690
|
+
"cx",
|
|
1691
|
+
"cy"
|
|
1692
|
+
]),
|
|
1693
|
+
line: /* @__PURE__ */ new Set([
|
|
1694
|
+
...COMMON_SHAPE_FIELDS,
|
|
1695
|
+
"x1",
|
|
1696
|
+
"y1",
|
|
1697
|
+
"x2",
|
|
1698
|
+
"y2"
|
|
1699
|
+
]),
|
|
1700
|
+
polygon: /* @__PURE__ */ new Set([...COMMON_SHAPE_FIELDS, "points"]),
|
|
1701
|
+
polyline: /* @__PURE__ */ new Set([...COMMON_SHAPE_FIELDS, "points"]),
|
|
1702
|
+
rect: /* @__PURE__ */ new Set([
|
|
1703
|
+
...COMMON_SHAPE_FIELDS,
|
|
1704
|
+
"x",
|
|
1705
|
+
"y",
|
|
1706
|
+
"width",
|
|
1707
|
+
"height",
|
|
1708
|
+
"rx",
|
|
1709
|
+
"ry"
|
|
1710
|
+
]),
|
|
1711
|
+
linearGradient: /* @__PURE__ */ new Set([
|
|
1712
|
+
"id",
|
|
1713
|
+
"x1",
|
|
1714
|
+
"y1",
|
|
1715
|
+
"x2",
|
|
1716
|
+
"y2",
|
|
1717
|
+
"gradientTransform",
|
|
1718
|
+
"gradientUnits",
|
|
1719
|
+
"spreadMethod"
|
|
1720
|
+
]),
|
|
1721
|
+
radialGradient: /* @__PURE__ */ new Set([
|
|
1722
|
+
"id",
|
|
1723
|
+
"cx",
|
|
1724
|
+
"cy",
|
|
1725
|
+
"r",
|
|
1726
|
+
"fx",
|
|
1727
|
+
"fy",
|
|
1728
|
+
"fr",
|
|
1729
|
+
"gradientTransform",
|
|
1730
|
+
"gradientUnits",
|
|
1731
|
+
"spreadMethod"
|
|
1732
|
+
]),
|
|
1733
|
+
stop: /* @__PURE__ */ new Set([
|
|
1734
|
+
"offset",
|
|
1735
|
+
"stop_color",
|
|
1736
|
+
"stop_opacity"
|
|
1737
|
+
])
|
|
1738
|
+
};
|
|
1739
|
+
function attrName(fieldName) {
|
|
1740
|
+
return fieldName.replace(/_/g, "-");
|
|
1741
|
+
}
|
|
1742
|
+
function fieldName(attr) {
|
|
1743
|
+
return attr.replace(/-/g, "_");
|
|
1744
|
+
}
|
|
1745
|
+
function attrSupported(el, attr) {
|
|
1746
|
+
const fields = VALID_FIELDS[stripNs(el.tag)];
|
|
1747
|
+
if (fields) return fields.has(fieldName(attr));
|
|
1748
|
+
return true;
|
|
1749
|
+
}
|
|
1750
|
+
function inheritCopy(attrib, child, attr) {
|
|
1751
|
+
if (child.attrib.has(attr)) return;
|
|
1752
|
+
if (attr in attrib) child.attrib.set(attr, attrib[attr]);
|
|
1753
|
+
}
|
|
1754
|
+
function inheritMultiply(attrib, child, attr) {
|
|
1755
|
+
if (!(attr in attrib) && !child.attrib.has(attr)) return;
|
|
1756
|
+
let value = parseFloat(attrib[attr] ?? "1");
|
|
1757
|
+
value *= parseFloat(child.attrib.get(attr) ?? "1");
|
|
1758
|
+
child.attrib.set(attr, ntos(value));
|
|
1759
|
+
}
|
|
1760
|
+
function inheritClipPath(attrib, child, _attr) {
|
|
1761
|
+
const clips = [...(child.attrib.get("clip-path") ?? "").split(","), attrib["clip-path"] ?? ""].sort();
|
|
1762
|
+
child.attrib.set("clip-path", clips.filter((c) => c).join(","));
|
|
1763
|
+
}
|
|
1764
|
+
function inheritNondefaultOverflow(attrib, child, attr) {
|
|
1765
|
+
if ((attrib[attr] ?? "visible") !== "visible") inheritCopy(attrib, child, attr);
|
|
1766
|
+
}
|
|
1767
|
+
function inheritNondefaultDisplay(attrib, child, attr) {
|
|
1768
|
+
const value = attrib[attr] ?? "";
|
|
1769
|
+
if (value === "none") child.attrib.set(attr, value);
|
|
1770
|
+
else inheritCopy(attrib, child, attr);
|
|
1771
|
+
}
|
|
1772
|
+
function inheritMatrixMultiply(attrib, child, attr) {
|
|
1773
|
+
let transform = Affine2D.identity();
|
|
1774
|
+
if (attr in attrib) transform = Affine2D.fromString(attrib[attr]);
|
|
1775
|
+
if (child.attrib.has(attr)) transform = Affine2D.composeLtr([Affine2D.fromString(child.attrib.get(attr)), transform]);
|
|
1776
|
+
if (!transform.equals(Affine2D.identity())) child.attrib.set(attr, transform.toString());
|
|
1777
|
+
else child.attrib.delete(attr);
|
|
1778
|
+
}
|
|
1779
|
+
var DO_NOT_INHERIT = () => {};
|
|
1780
|
+
var INHERIT_ATTRIB_HANDLERS = {
|
|
1781
|
+
"clip-rule": inheritCopy,
|
|
1782
|
+
color: inheritCopy,
|
|
1783
|
+
display: inheritNondefaultDisplay,
|
|
1784
|
+
fill: inheritCopy,
|
|
1785
|
+
"fill-rule": inheritCopy,
|
|
1786
|
+
style: inheritCopy,
|
|
1787
|
+
transform: inheritMatrixMultiply,
|
|
1788
|
+
stroke: inheritCopy,
|
|
1789
|
+
"stroke-width": inheritCopy,
|
|
1790
|
+
"stroke-linecap": inheritCopy,
|
|
1791
|
+
"stroke-linejoin": inheritCopy,
|
|
1792
|
+
"stroke-miterlimit": inheritCopy,
|
|
1793
|
+
"stroke-dasharray": inheritCopy,
|
|
1794
|
+
"stroke-dashoffset": inheritCopy,
|
|
1795
|
+
"stroke-opacity": inheritCopy,
|
|
1796
|
+
"fill-opacity": inheritCopy,
|
|
1797
|
+
opacity: inheritMultiply,
|
|
1798
|
+
"clip-path": inheritClipPath,
|
|
1799
|
+
id: DO_NOT_INHERIT,
|
|
1800
|
+
"data-name": DO_NOT_INHERIT,
|
|
1801
|
+
"enable-background": DO_NOT_INHERIT,
|
|
1802
|
+
overflow: inheritNondefaultOverflow
|
|
1803
|
+
};
|
|
1804
|
+
var INHERITABLE_ATTRIB = new Set(Object.entries(INHERIT_ATTRIB_HANDLERS).filter(([, handler]) => handler !== DO_NOT_INHERIT).map(([name]) => name));
|
|
1805
|
+
var ATTRIB_W_CUSTOM_INHERITANCE = /* @__PURE__ */ new Set([
|
|
1806
|
+
"clip-path",
|
|
1807
|
+
"opacity",
|
|
1808
|
+
"transform"
|
|
1809
|
+
]);
|
|
1810
|
+
var INHERITABLE_ATTRIB_DEFAULTS = Object.fromEntries(Object.entries(INHERIT_ATTRIB_HANDLERS).filter(([name, handler]) => name in ATTRIB_DEFAULTS && handler === inheritCopy).map(([name]) => {
|
|
1811
|
+
const value = ATTRIB_DEFAULTS[name];
|
|
1812
|
+
return [name, typeof value === "number" ? ntos(value) : value];
|
|
1813
|
+
}));
|
|
1814
|
+
function inheritAttrib(attrib, child, skipUnhandled = false, skips = /* @__PURE__ */ new Set()) {
|
|
1815
|
+
const work = attrib instanceof Map ? Object.fromEntries(attrib) : { ...attrib };
|
|
1816
|
+
for (const attr of Object.keys(work).sort()) {
|
|
1817
|
+
if (skips.has(attr) || !attrSupported(child, attr)) {
|
|
1818
|
+
delete work[attr];
|
|
1819
|
+
continue;
|
|
1820
|
+
}
|
|
1821
|
+
const handler = INHERIT_ATTRIB_HANDLERS[attr];
|
|
1822
|
+
if (!handler) continue;
|
|
1823
|
+
handler(work, child, attr);
|
|
1824
|
+
delete work[attr];
|
|
1825
|
+
}
|
|
1826
|
+
if (Object.keys(work).length && !skipUnhandled) throw new Error(`Unable to process attrib ${JSON.stringify(work)}`);
|
|
1827
|
+
}
|
|
1828
|
+
function attribToPassOn(currentAttrib, el, skips = ATTRIB_W_CUSTOM_INHERITANCE) {
|
|
1829
|
+
const attrCatcher = new XEl("dummy");
|
|
1830
|
+
inheritAttrib(el.attrib, attrCatcher, true, skips);
|
|
1831
|
+
inheritAttrib(currentAttrib, attrCatcher, false, skips);
|
|
1832
|
+
return Object.fromEntries(attrCatcher.attrib);
|
|
1833
|
+
}
|
|
1834
|
+
function dropDefaultAttrib(attrib) {
|
|
1835
|
+
for (const attr of [...attrib.keys()].sort()) {
|
|
1836
|
+
const defaultValue = ATTRIB_DEFAULTS[attr];
|
|
1837
|
+
if (defaultValue === void 0) continue;
|
|
1838
|
+
const value = attrib.get(attr);
|
|
1839
|
+
if (typeof defaultValue === "number") {
|
|
1840
|
+
if (parseFloat(value) === defaultValue) attrib.delete(attr);
|
|
1841
|
+
} else if (value === defaultValue) attrib.delete(attr);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
//#endregion
|
|
1845
|
+
//#region src/build/core/flatten/arcs.ts
|
|
1846
|
+
var TWO_PI = 2 * Math.PI;
|
|
1847
|
+
var PI_OVER_TWO = .5 * Math.PI;
|
|
1848
|
+
function isStraightLine(arc) {
|
|
1849
|
+
const rx = Math.abs(arc.rx);
|
|
1850
|
+
const ry = Math.abs(arc.ry);
|
|
1851
|
+
return !(rx && ry);
|
|
1852
|
+
}
|
|
1853
|
+
function isZeroLength(arc) {
|
|
1854
|
+
return ptEquals(arc.endPoint, arc.startPoint);
|
|
1855
|
+
}
|
|
1856
|
+
function correctOutOfRangeRadii(arc) {
|
|
1857
|
+
if (isStraightLine(arc) || isZeroLength(arc)) return arc;
|
|
1858
|
+
const midPointDistance = ptMul(ptSub(arc.startPoint, arc.endPoint), .5);
|
|
1859
|
+
const angle = arc.rotation * Math.PI / 180;
|
|
1860
|
+
const transformedMidPoint = Affine2D.identity().rotate(-angle).mapVector(midPointDistance);
|
|
1861
|
+
let rx = arc.rx;
|
|
1862
|
+
let ry = arc.ry;
|
|
1863
|
+
const squareRx = rx * rx;
|
|
1864
|
+
const squareRy = ry * ry;
|
|
1865
|
+
const squareX = transformedMidPoint.x * transformedMidPoint.x;
|
|
1866
|
+
const squareY = transformedMidPoint.y * transformedMidPoint.y;
|
|
1867
|
+
const radiiScale = squareX / squareRx + squareY / squareRy;
|
|
1868
|
+
if (radiiScale > 1) {
|
|
1869
|
+
rx *= Math.sqrt(radiiScale);
|
|
1870
|
+
ry *= Math.sqrt(radiiScale);
|
|
1871
|
+
return {
|
|
1872
|
+
...arc,
|
|
1873
|
+
rx,
|
|
1874
|
+
ry
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
return arc;
|
|
1878
|
+
}
|
|
1879
|
+
function endToCenterParametrization(arc) {
|
|
1880
|
+
const angle = arc.rotation * Math.PI / 180;
|
|
1881
|
+
const pointTransform = Affine2D.identity().scale(1 / arc.rx, 1 / arc.ry).rotate(-angle);
|
|
1882
|
+
const point1 = pointTransform.mapPoint(arc.startPoint);
|
|
1883
|
+
const point2 = pointTransform.mapPoint(arc.endPoint);
|
|
1884
|
+
let delta = ptSub(point2, point1);
|
|
1885
|
+
const d = delta.x * delta.x + delta.y * delta.y;
|
|
1886
|
+
const scaleFactorSquared = Math.max(1 / d - .25, 0);
|
|
1887
|
+
let scaleFactor = Math.sqrt(scaleFactorSquared);
|
|
1888
|
+
if (arc.sweep === arc.large) scaleFactor = -scaleFactor;
|
|
1889
|
+
delta = ptMul(delta, scaleFactor);
|
|
1890
|
+
let centerPoint = ptAdd(ptAdd(point1, ptMul(ptSub(point2, point1), .5)), pt(-delta.y, delta.x));
|
|
1891
|
+
const v1 = ptSub(point1, centerPoint);
|
|
1892
|
+
const v2 = ptSub(point2, centerPoint);
|
|
1893
|
+
const theta1 = Math.atan2(v1.y, v1.x);
|
|
1894
|
+
let thetaArc = Math.atan2(v2.y, v2.x) - theta1;
|
|
1895
|
+
if (thetaArc < 0 && arc.sweep) thetaArc += TWO_PI;
|
|
1896
|
+
else if (thetaArc > 0 && !arc.sweep) thetaArc -= TWO_PI;
|
|
1897
|
+
centerPoint = pointTransform.inverse().mapPoint(centerPoint);
|
|
1898
|
+
return {
|
|
1899
|
+
theta1,
|
|
1900
|
+
thetaArc,
|
|
1901
|
+
centerPoint
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
function* ellipticalArcToCubic(arc) {
|
|
1905
|
+
arc = correctOutOfRangeRadii(arc);
|
|
1906
|
+
const arcParams = endToCenterParametrization(arc);
|
|
1907
|
+
const pointTransform = Affine2D.identity().translate(arcParams.centerPoint.x, arcParams.centerPoint.y).rotate(arc.rotation * Math.PI / 180).scale(arc.rx, arc.ry);
|
|
1908
|
+
const numSegments = Math.ceil(Math.abs(arcParams.thetaArc / (PI_OVER_TWO + .001)));
|
|
1909
|
+
for (let i = 0; i < numSegments; i++) {
|
|
1910
|
+
const startTheta = arcParams.theta1 + i * arcParams.thetaArc / numSegments;
|
|
1911
|
+
const endTheta = arcParams.theta1 + (i + 1) * arcParams.thetaArc / numSegments;
|
|
1912
|
+
const t = 4 / 3 * Math.tan(.25 * (endTheta - startTheta));
|
|
1913
|
+
if (!Number.isFinite(t)) return;
|
|
1914
|
+
const sinStartTheta = Math.sin(startTheta);
|
|
1915
|
+
const cosStartTheta = Math.cos(startTheta);
|
|
1916
|
+
const sinEndTheta = Math.sin(endTheta);
|
|
1917
|
+
const cosEndTheta = Math.cos(endTheta);
|
|
1918
|
+
let point1 = pt(cosStartTheta - t * sinStartTheta, sinStartTheta + t * cosStartTheta);
|
|
1919
|
+
let endPoint = pt(cosEndTheta, sinEndTheta);
|
|
1920
|
+
let point2 = ptAdd(endPoint, pt(t * sinEndTheta, -t * cosEndTheta));
|
|
1921
|
+
point1 = pointTransform.mapPoint(point1);
|
|
1922
|
+
point2 = pointTransform.mapPoint(point2);
|
|
1923
|
+
if (i === numSegments - 1) endPoint = arc.endPoint;
|
|
1924
|
+
else endPoint = pointTransform.mapPoint(endPoint);
|
|
1925
|
+
yield [
|
|
1926
|
+
point1,
|
|
1927
|
+
point2,
|
|
1928
|
+
endPoint
|
|
1929
|
+
];
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
function* arcToCubic(startPoint, rx, ry, rotation, large, sweep, endPoint) {
|
|
1933
|
+
const arc = {
|
|
1934
|
+
startPoint,
|
|
1935
|
+
rx,
|
|
1936
|
+
ry,
|
|
1937
|
+
rotation,
|
|
1938
|
+
large,
|
|
1939
|
+
sweep,
|
|
1940
|
+
endPoint
|
|
1941
|
+
};
|
|
1942
|
+
if (isZeroLength(arc)) return;
|
|
1943
|
+
else if (isStraightLine(arc)) yield [
|
|
1944
|
+
null,
|
|
1945
|
+
null,
|
|
1946
|
+
arc.endPoint
|
|
1947
|
+
];
|
|
1948
|
+
else yield* ellipticalArcToCubic(arc);
|
|
1949
|
+
}
|
|
1950
|
+
//#endregion
|
|
1951
|
+
//#region src/build/core/flatten/path.ts
|
|
1952
|
+
var CMD_ARGS = {
|
|
1953
|
+
m: 2,
|
|
1954
|
+
z: 0,
|
|
1955
|
+
l: 2,
|
|
1956
|
+
h: 1,
|
|
1957
|
+
v: 1,
|
|
1958
|
+
c: 6,
|
|
1959
|
+
s: 4,
|
|
1960
|
+
q: 4,
|
|
1961
|
+
t: 2,
|
|
1962
|
+
a: 7
|
|
1963
|
+
};
|
|
1964
|
+
for (const [k, v] of Object.entries(CMD_ARGS)) CMD_ARGS[k.toUpperCase()] = v;
|
|
1965
|
+
var CMD_COORDS = {
|
|
1966
|
+
m: [[0], [1]],
|
|
1967
|
+
z: [[], []],
|
|
1968
|
+
l: [[0], [1]],
|
|
1969
|
+
h: [[0], []],
|
|
1970
|
+
v: [[], [0]],
|
|
1971
|
+
c: [[
|
|
1972
|
+
0,
|
|
1973
|
+
2,
|
|
1974
|
+
4
|
|
1975
|
+
], [
|
|
1976
|
+
1,
|
|
1977
|
+
3,
|
|
1978
|
+
5
|
|
1979
|
+
]],
|
|
1980
|
+
s: [[0, 2], [1, 3]],
|
|
1981
|
+
q: [[0, 2], [1, 3]],
|
|
1982
|
+
t: [[0], [1]],
|
|
1983
|
+
a: [[5], [6]]
|
|
1984
|
+
};
|
|
1985
|
+
for (const [k, v] of Object.entries(CMD_COORDS)) CMD_COORDS[k.toUpperCase()] = v;
|
|
1986
|
+
function numArgs(cmd) {
|
|
1987
|
+
const n = CMD_ARGS[cmd];
|
|
1988
|
+
if (n === void 0) throw new Error(`Invalid svg command "${cmd}"`);
|
|
1989
|
+
return n;
|
|
1990
|
+
}
|
|
1991
|
+
function checkCmd(cmd, args) {
|
|
1992
|
+
const cmdArgs = numArgs(cmd);
|
|
1993
|
+
if (cmdArgs === 0) {
|
|
1994
|
+
if (args.length) throw new Error(`${cmd} has no args, ${args.length} invalid`);
|
|
1995
|
+
} else if (args.length % cmdArgs !== 0) throw new Error(`${cmd} has sets of ${cmdArgs} args, ${args.length} invalid`);
|
|
1996
|
+
return cmdArgs;
|
|
1997
|
+
}
|
|
1998
|
+
function cmdCoords(cmd) {
|
|
1999
|
+
const coords = CMD_COORDS[cmd];
|
|
2000
|
+
if (coords === void 0) throw new Error(`Invalid svg command "${cmd}"`);
|
|
2001
|
+
return coords;
|
|
2002
|
+
}
|
|
2003
|
+
function pathSegment(cmd, ...args) {
|
|
2004
|
+
const argsPerCmd = checkCmd(cmd, args);
|
|
2005
|
+
const strArgs = args.map(ntos);
|
|
2006
|
+
const combinedArgs = [];
|
|
2007
|
+
const [xCoords, yCoords] = cmdCoords(cmd);
|
|
2008
|
+
const xyCoords = new Set(xCoords.map((x, i) => `${x},${yCoords[i]}`).slice(0, yCoords.length));
|
|
2009
|
+
if (argsPerCmd) for (let n = 0; n < strArgs.length / argsPerCmd; n++) {
|
|
2010
|
+
const subArgs = strArgs.slice(n * argsPerCmd, (n + 1) * argsPerCmd);
|
|
2011
|
+
let i = 0;
|
|
2012
|
+
while (i < subArgs.length) if (xyCoords.has(`${i},${i + 1}`)) {
|
|
2013
|
+
combinedArgs.push(`${subArgs[i]},${subArgs[i + 1]}`);
|
|
2014
|
+
i += 2;
|
|
2015
|
+
} else {
|
|
2016
|
+
combinedArgs.push(subArgs[i]);
|
|
2017
|
+
i += 1;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
return cmd + combinedArgs.join(" ");
|
|
2021
|
+
}
|
|
2022
|
+
var CMD_RE = /([mzlhvcsqtaMZLHVCSQTA])/;
|
|
2023
|
+
var SEPARATOR_RE = /[, ]+/;
|
|
2024
|
+
var FLOAT_RE = /* @__PURE__ */ new RegExp("^[-+]?(?:(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?|(?:\\.[0-9]+))(?:[eE][-+]?[0-9]+)?");
|
|
2025
|
+
var BOOL_RE = /^[01]/;
|
|
2026
|
+
var ARC_ARGUMENT_TYPES = [
|
|
2027
|
+
FLOAT_RE,
|
|
2028
|
+
FLOAT_RE,
|
|
2029
|
+
FLOAT_RE,
|
|
2030
|
+
BOOL_RE,
|
|
2031
|
+
BOOL_RE,
|
|
2032
|
+
FLOAT_RE,
|
|
2033
|
+
FLOAT_RE
|
|
2034
|
+
];
|
|
2035
|
+
var IMPLICIT_REPEAT_CMD = {
|
|
2036
|
+
m: "l",
|
|
2037
|
+
M: "L"
|
|
2038
|
+
};
|
|
2039
|
+
function parseArgs(cmd, rawArgsStr) {
|
|
2040
|
+
const rawArgs = rawArgsStr.split(SEPARATOR_RE).filter((s) => s);
|
|
2041
|
+
const result = [];
|
|
2042
|
+
if (!rawArgs.length) return result;
|
|
2043
|
+
const argTypes = cmd.toUpperCase() === "A" ? ARC_ARGUMENT_TYPES : [FLOAT_RE];
|
|
2044
|
+
const n = argTypes.length;
|
|
2045
|
+
let i = 0;
|
|
2046
|
+
let j = 0;
|
|
2047
|
+
while (j < rawArgs.length) {
|
|
2048
|
+
const arg = rawArgs[j];
|
|
2049
|
+
const m = argTypes[i % n].exec(arg);
|
|
2050
|
+
if (!m) throw new Error(`Invalid path data "${cmd}${rawArgsStr.slice(0, 40)}": argument #${i + 1} (${arg}) is not a number`);
|
|
2051
|
+
const end = m[0].length;
|
|
2052
|
+
result.push(parseFloat(arg.slice(0, end)));
|
|
2053
|
+
if (end < arg.length) rawArgs[j] = arg.slice(end);
|
|
2054
|
+
else j += 1;
|
|
2055
|
+
i += 1;
|
|
2056
|
+
}
|
|
2057
|
+
return result;
|
|
2058
|
+
}
|
|
2059
|
+
function explodeCmd(argsPerCmd, cmd, args) {
|
|
2060
|
+
const cmds = [];
|
|
2061
|
+
for (let i = 0; i < Math.floor(args.length / argsPerCmd); i++) {
|
|
2062
|
+
if (i > 0) cmd = IMPLICIT_REPEAT_CMD[cmd] ?? cmd;
|
|
2063
|
+
cmds.push([cmd, args.slice(i * argsPerCmd, (i + 1) * argsPerCmd)]);
|
|
2064
|
+
}
|
|
2065
|
+
return cmds;
|
|
2066
|
+
}
|
|
2067
|
+
function parseSvgPath(svgPath, exploded = false) {
|
|
2068
|
+
const commandTuples = [];
|
|
2069
|
+
const parts = svgPath.split(CMD_RE).slice(1);
|
|
2070
|
+
for (let i = 0; i < parts.length; i += 2) {
|
|
2071
|
+
const cmd = parts[i];
|
|
2072
|
+
const args = parseArgs(cmd, (parts[i + 1] ?? "").trim());
|
|
2073
|
+
const argsPerCmd = checkCmd(cmd, args);
|
|
2074
|
+
if (argsPerCmd === 0 || !exploded) commandTuples.push([cmd, args]);
|
|
2075
|
+
else commandTuples.push(...explodeCmd(argsPerCmd, cmd, args));
|
|
2076
|
+
}
|
|
2077
|
+
return commandTuples;
|
|
2078
|
+
}
|
|
2079
|
+
function addCmdToD(d, cmd, args) {
|
|
2080
|
+
const snippet = pathSegment(cmd, ...args);
|
|
2081
|
+
return d ? `${d} ${snippet}` : snippet;
|
|
2082
|
+
}
|
|
2083
|
+
function buildD(cmds) {
|
|
2084
|
+
let d = "";
|
|
2085
|
+
for (const [cmd, args] of cmds) d = addCmdToD(d, cmd, args);
|
|
2086
|
+
return d;
|
|
2087
|
+
}
|
|
2088
|
+
function nextPos(currPos, cmd, cmdArgs) {
|
|
2089
|
+
const [xCoordIdxs, yCoordIdxs] = cmdCoords(cmd);
|
|
2090
|
+
let newX = currPos.x;
|
|
2091
|
+
let newY = currPos.y;
|
|
2092
|
+
if (cmd === cmd.toUpperCase()) {
|
|
2093
|
+
if (xCoordIdxs.length) newX = 0;
|
|
2094
|
+
if (yCoordIdxs.length) newY = 0;
|
|
2095
|
+
}
|
|
2096
|
+
if (xCoordIdxs.length) newX += cmdArgs[xCoordIdxs[xCoordIdxs.length - 1]];
|
|
2097
|
+
if (yCoordIdxs.length) newY += cmdArgs[yCoordIdxs[yCoordIdxs.length - 1]];
|
|
2098
|
+
return pt(newX, newY);
|
|
2099
|
+
}
|
|
2100
|
+
function explicitLinesCallback(_subpathStart, currPos, cmd, args) {
|
|
2101
|
+
let newArgs;
|
|
2102
|
+
if (cmd === "v") newArgs = [0, args[0]];
|
|
2103
|
+
else if (cmd === "V") newArgs = [currPos.x, args[0]];
|
|
2104
|
+
else if (cmd === "h") newArgs = [args[0], 0];
|
|
2105
|
+
else if (cmd === "H") newArgs = [args[0], currPos.y];
|
|
2106
|
+
else return [[cmd, [...args]]];
|
|
2107
|
+
return [[cmd === cmd.toLowerCase() ? "l" : "L", newArgs]];
|
|
2108
|
+
}
|
|
2109
|
+
function rewriteCoords(cmdConverter, coordConverter, currPos, cmd, args) {
|
|
2110
|
+
const [xCoordIdxs, yCoordIdxs] = cmdCoords(cmd);
|
|
2111
|
+
const desiredCmd = cmdConverter(cmd);
|
|
2112
|
+
const newArgs = [...args];
|
|
2113
|
+
if (cmd !== desiredCmd) {
|
|
2114
|
+
cmd = desiredCmd;
|
|
2115
|
+
for (const xCoordIdx of xCoordIdxs) newArgs[xCoordIdx] += coordConverter(currPos.x);
|
|
2116
|
+
for (const yCoordIdx of yCoordIdxs) newArgs[yCoordIdx] += coordConverter(currPos.y);
|
|
2117
|
+
}
|
|
2118
|
+
return [cmd, newArgs];
|
|
2119
|
+
}
|
|
2120
|
+
function relativeToAbsolute(currPos, cmd, args) {
|
|
2121
|
+
return rewriteCoords((c) => c.toUpperCase(), (scaler) => scaler, currPos, cmd, args);
|
|
2122
|
+
}
|
|
2123
|
+
function moveEndpoint(currPos, cmd, cmdArgs, newEndpoint) {
|
|
2124
|
+
const [[newCmd, newArgs]] = explicitLinesCallback(null, currPos, cmd, cmdArgs);
|
|
2125
|
+
cmd = newCmd;
|
|
2126
|
+
const args = [...newArgs];
|
|
2127
|
+
const [xCoordIdxs, yCoordIdxs] = cmdCoords(cmd);
|
|
2128
|
+
if (xCoordIdxs.length || yCoordIdxs.length) {
|
|
2129
|
+
let { x: newX, y: newY } = newEndpoint;
|
|
2130
|
+
if (cmd === cmd.toLowerCase()) {
|
|
2131
|
+
newX = newX - currPos.x;
|
|
2132
|
+
newY = newY - currPos.y;
|
|
2133
|
+
}
|
|
2134
|
+
args[xCoordIdxs[xCoordIdxs.length - 1]] = newX;
|
|
2135
|
+
args[yCoordIdxs[yCoordIdxs.length - 1]] = newY;
|
|
2136
|
+
}
|
|
2137
|
+
return [cmd, args];
|
|
2138
|
+
}
|
|
2139
|
+
function walkPath(d, callback) {
|
|
2140
|
+
let currPos = pt();
|
|
2141
|
+
let subpathStartPos = currPos;
|
|
2142
|
+
const newCmds = [];
|
|
2143
|
+
const exploded = parseSvgPath(d, true);
|
|
2144
|
+
for (let idx = 0; idx < exploded.length; idx++) {
|
|
2145
|
+
let [cmd, args] = exploded[idx];
|
|
2146
|
+
checkCmd(cmd, args);
|
|
2147
|
+
if (idx === 0 && cmd === "m") cmd = "M";
|
|
2148
|
+
let prev = [
|
|
2149
|
+
null,
|
|
2150
|
+
null,
|
|
2151
|
+
null
|
|
2152
|
+
];
|
|
2153
|
+
if (newCmds.length) prev = newCmds[newCmds.length - 1];
|
|
2154
|
+
for (const [newCmd, newCmdArgs] of callback(subpathStartPos, currPos, cmd, args, ...prev)) {
|
|
2155
|
+
let next;
|
|
2156
|
+
if (newCmd.toLowerCase() !== "z") next = nextPos(currPos, newCmd, newCmdArgs);
|
|
2157
|
+
else next = subpathStartPos;
|
|
2158
|
+
const prevPos = currPos;
|
|
2159
|
+
currPos = next;
|
|
2160
|
+
if (newCmd.toUpperCase() === "M") subpathStartPos = currPos;
|
|
2161
|
+
newCmds.push([
|
|
2162
|
+
prevPos,
|
|
2163
|
+
newCmd,
|
|
2164
|
+
newCmdArgs
|
|
2165
|
+
]);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
let newD = "";
|
|
2169
|
+
for (const [, cmd, args] of newCmds) newD = addCmdToD(newD, cmd, args);
|
|
2170
|
+
return newD;
|
|
2171
|
+
}
|
|
2172
|
+
function rewritePath(d, rewriteFn) {
|
|
2173
|
+
return walkPath(d, (subpathStart, currPos, cmd, args) => {
|
|
2174
|
+
let [newCmd, newCmdArgs] = rewriteFn(currPos, cmd, args);
|
|
2175
|
+
const next = nextPos(currPos, newCmd, newCmdArgs);
|
|
2176
|
+
if (!ptEquals(next, subpathStart) && ptAlmostEquals(next, subpathStart)) [newCmd, newCmdArgs] = moveEndpoint(currPos, newCmd, newCmdArgs, subpathStart);
|
|
2177
|
+
return [[newCmd, newCmdArgs]];
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
function absolutePath(d) {
|
|
2181
|
+
return rewritePath(d, relativeToAbsolute);
|
|
2182
|
+
}
|
|
2183
|
+
function absoluteMoveto(d) {
|
|
2184
|
+
return rewritePath(d, (currPos, cmd, args) => {
|
|
2185
|
+
if (cmd === "M" || cmd === "m") return relativeToAbsolute(currPos, cmd, args);
|
|
2186
|
+
return [cmd, [...args]];
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
function explicitLines(d) {
|
|
2190
|
+
return walkPath(d, explicitLinesCallback);
|
|
2191
|
+
}
|
|
2192
|
+
function expandShorthand(d) {
|
|
2193
|
+
const shortToLong = {
|
|
2194
|
+
S: "C",
|
|
2195
|
+
T: "Q"
|
|
2196
|
+
};
|
|
2197
|
+
return walkPath(d, (_, currPos, cmd, args, prevPos, prevCmd, prevArgs) => {
|
|
2198
|
+
if (!(cmd.toUpperCase() in shortToLong)) return [[cmd, args]];
|
|
2199
|
+
if (cmd === cmd.toLowerCase()) [cmd, args] = relativeToAbsolute(currPos, cmd, args);
|
|
2200
|
+
let newCp = [currPos.x, currPos.y];
|
|
2201
|
+
if (prevCmd) {
|
|
2202
|
+
if (prevCmd === prevCmd.toLowerCase()) [prevCmd, prevArgs] = relativeToAbsolute(prevPos, prevCmd, prevArgs);
|
|
2203
|
+
if (Object.values(shortToLong).includes(prevCmd)) {
|
|
2204
|
+
const prevCpX = prevArgs[prevArgs.length - 4];
|
|
2205
|
+
const prevCpY = prevArgs[prevArgs.length - 3];
|
|
2206
|
+
newCp = [2 * currPos.x - prevCpX, 2 * currPos.y - prevCpY];
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
return [[shortToLong[cmd], [...newCp, ...args]]];
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
function arcsToCubics(d) {
|
|
2213
|
+
return walkPath(d, (_subpathStart, currPos, cmd, args) => {
|
|
2214
|
+
if (cmd !== "a" && cmd !== "A") return [[cmd, args]];
|
|
2215
|
+
const rx = args[0];
|
|
2216
|
+
const ry = args[1];
|
|
2217
|
+
const xRotation = args[2];
|
|
2218
|
+
const large = args[3];
|
|
2219
|
+
const sweep = args[4];
|
|
2220
|
+
let endX = args[5];
|
|
2221
|
+
let endY = args[6];
|
|
2222
|
+
if (cmd === "a") {
|
|
2223
|
+
endX += currPos.x;
|
|
2224
|
+
endY += currPos.y;
|
|
2225
|
+
}
|
|
2226
|
+
const result = [];
|
|
2227
|
+
for (const [p1, p2, target] of arcToCubic(currPos, rx, ry, xRotation, large, sweep, pt(endX, endY))) {
|
|
2228
|
+
const { x, y } = target;
|
|
2229
|
+
if (p1 !== null) result.push(["C", [
|
|
2230
|
+
p1.x,
|
|
2231
|
+
p1.y,
|
|
2232
|
+
p2.x,
|
|
2233
|
+
p2.y,
|
|
2234
|
+
x,
|
|
2235
|
+
y
|
|
2236
|
+
]]);
|
|
2237
|
+
else result.push(["L", [x, y]]);
|
|
2238
|
+
}
|
|
2239
|
+
return result;
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
function subpaths(d) {
|
|
2243
|
+
const result = [""];
|
|
2244
|
+
walkPath(absoluteMoveto(d), (_subpathStart, _currPos, cmd, args) => {
|
|
2245
|
+
if (cmd.toUpperCase() === "M") result.push("");
|
|
2246
|
+
result[result.length - 1] = addCmdToD(result[result.length - 1], cmd, args);
|
|
2247
|
+
if (cmd.toUpperCase() === "Z") result.push("");
|
|
2248
|
+
return [[cmd, args]];
|
|
2249
|
+
});
|
|
2250
|
+
return result.filter((s) => s);
|
|
2251
|
+
}
|
|
2252
|
+
function roundFloatsD(d, ndigits) {
|
|
2253
|
+
let out = "";
|
|
2254
|
+
for (const [cmd, args] of parseSvgPath(d)) out = addCmdToD(out, cmd, args.map((n) => pythonRound(n, ndigits)));
|
|
2255
|
+
return out;
|
|
2256
|
+
}
|
|
2257
|
+
function asCmdSeq(d) {
|
|
2258
|
+
return parseSvgPath(arcsToCubics(absolutePath(expandShorthand(explicitLines(d)))), true);
|
|
2259
|
+
}
|
|
2260
|
+
//#endregion
|
|
2261
|
+
//#region src/build/core/flatten/shape.ts
|
|
2262
|
+
var f = (name, type, dflt) => ({
|
|
2263
|
+
name,
|
|
2264
|
+
type,
|
|
2265
|
+
default: dflt
|
|
2266
|
+
});
|
|
2267
|
+
var COMMON_FIELDS = [
|
|
2268
|
+
f("id", "str", ""),
|
|
2269
|
+
f("clip_path", "str", attribDefault("clip-path")),
|
|
2270
|
+
f("clip_rule", "str", attribDefault("clip-rule")),
|
|
2271
|
+
f("fill", "str", attribDefault("fill")),
|
|
2272
|
+
f("fill_opacity", "float", attribDefault("fill-opacity")),
|
|
2273
|
+
f("fill_rule", "str", attribDefault("fill-rule")),
|
|
2274
|
+
f("stroke", "str", attribDefault("stroke")),
|
|
2275
|
+
f("stroke_width", "float", attribDefault("stroke-width")),
|
|
2276
|
+
f("stroke_linecap", "str", attribDefault("stroke-linecap")),
|
|
2277
|
+
f("stroke_linejoin", "str", attribDefault("stroke-linejoin")),
|
|
2278
|
+
f("stroke_miterlimit", "float", attribDefault("stroke-miterlimit")),
|
|
2279
|
+
f("stroke_dasharray", "str", attribDefault("stroke-dasharray")),
|
|
2280
|
+
f("stroke_dashoffset", "float", attribDefault("stroke-dashoffset")),
|
|
2281
|
+
f("stroke_opacity", "float", attribDefault("stroke-opacity")),
|
|
2282
|
+
f("opacity", "float", attribDefault("opacity")),
|
|
2283
|
+
f("transform", "str", attribDefault("transform")),
|
|
2284
|
+
f("style", "str", attribDefault("style")),
|
|
2285
|
+
f("display", "str", attribDefault("display"))
|
|
2286
|
+
];
|
|
2287
|
+
var TAG_FIELDS = {
|
|
2288
|
+
path: [f("d", "str", "")],
|
|
2289
|
+
circle: [
|
|
2290
|
+
f("r", "float", 0),
|
|
2291
|
+
f("cx", "float", 0),
|
|
2292
|
+
f("cy", "float", 0)
|
|
2293
|
+
],
|
|
2294
|
+
ellipse: [
|
|
2295
|
+
f("rx", "float", 0),
|
|
2296
|
+
f("ry", "float", 0),
|
|
2297
|
+
f("cx", "float", 0),
|
|
2298
|
+
f("cy", "float", 0)
|
|
2299
|
+
],
|
|
2300
|
+
line: [
|
|
2301
|
+
f("x1", "float", 0),
|
|
2302
|
+
f("y1", "float", 0),
|
|
2303
|
+
f("x2", "float", 0),
|
|
2304
|
+
f("y2", "float", 0)
|
|
2305
|
+
],
|
|
2306
|
+
polygon: [f("points", "str", "")],
|
|
2307
|
+
polyline: [f("points", "str", "")],
|
|
2308
|
+
rect: [
|
|
2309
|
+
f("x", "float", 0),
|
|
2310
|
+
f("y", "float", 0),
|
|
2311
|
+
f("width", "float", 0),
|
|
2312
|
+
f("height", "float", 0),
|
|
2313
|
+
f("rx", "float", 0),
|
|
2314
|
+
f("ry", "float", 0)
|
|
2315
|
+
]
|
|
2316
|
+
};
|
|
2317
|
+
var SHAPE_TAGS = new Set(Object.keys(TAG_FIELDS));
|
|
2318
|
+
function isShapeTag(tag) {
|
|
2319
|
+
return SHAPE_TAGS.has(stripNs(tag));
|
|
2320
|
+
}
|
|
2321
|
+
function shapeFields(tag) {
|
|
2322
|
+
const extra = TAG_FIELDS[tag];
|
|
2323
|
+
if (!extra) throw new Error(`Bad tag <${tag}>`);
|
|
2324
|
+
return [...COMMON_FIELDS, ...extra];
|
|
2325
|
+
}
|
|
2326
|
+
function newShape(tag) {
|
|
2327
|
+
const fields = {};
|
|
2328
|
+
for (const field of shapeFields(tag)) fields[field.name] = field.default;
|
|
2329
|
+
return {
|
|
2330
|
+
tag,
|
|
2331
|
+
fields
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
function shapeStr(shape, name) {
|
|
2335
|
+
return shape.fields[name];
|
|
2336
|
+
}
|
|
2337
|
+
function shapeNum(shape, name) {
|
|
2338
|
+
return shape.fields[name];
|
|
2339
|
+
}
|
|
2340
|
+
function cloneShape(shape) {
|
|
2341
|
+
return {
|
|
2342
|
+
tag: shape.tag,
|
|
2343
|
+
fields: { ...shape.fields }
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
function shapesEqual(a, b) {
|
|
2347
|
+
if (a.tag !== b.tag) return false;
|
|
2348
|
+
for (const field of shapeFields(a.tag)) if (a.fields[field.name] !== b.fields[field.name]) return false;
|
|
2349
|
+
return true;
|
|
2350
|
+
}
|
|
2351
|
+
function parseFloatStrict(raw, tag, attr) {
|
|
2352
|
+
const value = Number(raw.trim());
|
|
2353
|
+
if (Number.isNaN(value)) throw new Error(`${attr}="${raw}" on <${tag}> is not a number (units other than px are not supported)`);
|
|
2354
|
+
return value;
|
|
2355
|
+
}
|
|
2356
|
+
function fromElement(el, inheritedAttrib = {}) {
|
|
2357
|
+
if (!isShapeTag(el.tag)) throw new Error(`Bad tag <${el.tag}>`);
|
|
2358
|
+
const tag = stripNs(el.tag);
|
|
2359
|
+
const attrs = { ...inheritedAttrib };
|
|
2360
|
+
for (const [name, value] of el.attrib) attrs[name] = value;
|
|
2361
|
+
const shape = newShape(tag);
|
|
2362
|
+
for (const field of shapeFields(tag)) {
|
|
2363
|
+
const raw = attrs[attrName(field.name)];
|
|
2364
|
+
if (raw === void 0 || !raw.trim()) continue;
|
|
2365
|
+
shape.fields[field.name] = field.type === "float" ? parseFloatStrict(raw, tag, attrName(field.name)) : raw;
|
|
2366
|
+
}
|
|
2367
|
+
if (tag === "rect") rectPostInit(shape);
|
|
2368
|
+
return shape;
|
|
2369
|
+
}
|
|
2370
|
+
function rectPostInit(shape) {
|
|
2371
|
+
if (!shapeNum(shape, "rx")) shape.fields.rx = shapeNum(shape, "ry");
|
|
2372
|
+
if (!shapeNum(shape, "ry")) shape.fields.ry = shapeNum(shape, "rx");
|
|
2373
|
+
shape.fields.rx = Math.min(shapeNum(shape, "rx"), shapeNum(shape, "width") / 2);
|
|
2374
|
+
shape.fields.ry = Math.min(shapeNum(shape, "ry"), shapeNum(shape, "height") / 2);
|
|
2375
|
+
}
|
|
2376
|
+
function toElement(shape, inheritedAttrib = {}) {
|
|
2377
|
+
const el = new XEl(svgTag(shape.tag));
|
|
2378
|
+
for (const field of shapeFields(shape.tag)) {
|
|
2379
|
+
const attr = attrName(field.name);
|
|
2380
|
+
const fieldValue = shape.fields[field.name];
|
|
2381
|
+
const attribValue = typeof fieldValue === "number" ? ntos(fieldValue) : fieldValue;
|
|
2382
|
+
if (attr in inheritedAttrib) {
|
|
2383
|
+
if (attribValue === inheritedAttrib[attr]) continue;
|
|
2384
|
+
} else if (fieldValue === field.default) continue;
|
|
2385
|
+
el.attrib.set(attr, attribValue);
|
|
2386
|
+
}
|
|
2387
|
+
return el;
|
|
2388
|
+
}
|
|
2389
|
+
function resetFields(shape, pred) {
|
|
2390
|
+
for (const field of shapeFields(shape.tag)) if (pred(field.name)) shape.fields[field.name] = field.default;
|
|
2391
|
+
}
|
|
2392
|
+
function copyCommonFields(target, source) {
|
|
2393
|
+
for (const field of COMMON_FIELDS) target.fields[field.name] = source.fields[field.name];
|
|
2394
|
+
}
|
|
2395
|
+
function pathFromD(d) {
|
|
2396
|
+
const path = newShape("path");
|
|
2397
|
+
path.fields.d = d;
|
|
2398
|
+
return path;
|
|
2399
|
+
}
|
|
2400
|
+
function pathFromCommands(cmds) {
|
|
2401
|
+
return pathFromD(buildD(cmds));
|
|
2402
|
+
}
|
|
2403
|
+
function asPath(shape) {
|
|
2404
|
+
if (shape.tag === "path") return shape;
|
|
2405
|
+
const fields = shape.fields;
|
|
2406
|
+
let d = "";
|
|
2407
|
+
const add = (cmd, ...args) => {
|
|
2408
|
+
d = addCmdToD(d, cmd, args);
|
|
2409
|
+
};
|
|
2410
|
+
const addArc = (rx, ry, x, y, large = 0) => {
|
|
2411
|
+
const snippet = pathSegment("A", rx, ry, 0, large, 1, x, y);
|
|
2412
|
+
d = d ? `${d} ${snippet}` : snippet;
|
|
2413
|
+
};
|
|
2414
|
+
switch (shape.tag) {
|
|
2415
|
+
case "circle":
|
|
2416
|
+
case "ellipse": {
|
|
2417
|
+
const rx = shape.tag === "circle" ? fields.r : fields.rx;
|
|
2418
|
+
const ry = shape.tag === "circle" ? fields.r : fields.ry;
|
|
2419
|
+
const cx = fields.cx;
|
|
2420
|
+
const cy = fields.cy;
|
|
2421
|
+
add("M", cx + rx, cy);
|
|
2422
|
+
addArc(rx, ry, cx - rx, cy, 1);
|
|
2423
|
+
addArc(rx, ry, cx + rx, cy, 1);
|
|
2424
|
+
d = `${d} Z`;
|
|
2425
|
+
break;
|
|
2426
|
+
}
|
|
2427
|
+
case "line":
|
|
2428
|
+
add("M", fields.x1, fields.y1);
|
|
2429
|
+
add("L", fields.x2, fields.y2);
|
|
2430
|
+
break;
|
|
2431
|
+
case "polygon":
|
|
2432
|
+
d = fields.points ? `M${fields.points} Z` : "";
|
|
2433
|
+
break;
|
|
2434
|
+
case "polyline":
|
|
2435
|
+
d = fields.points ? `M${fields.points}` : "";
|
|
2436
|
+
break;
|
|
2437
|
+
case "rect": {
|
|
2438
|
+
const x = fields.x;
|
|
2439
|
+
const y = fields.y;
|
|
2440
|
+
const w = fields.width;
|
|
2441
|
+
const h = fields.height;
|
|
2442
|
+
const rx = fields.rx;
|
|
2443
|
+
const ry = fields.ry;
|
|
2444
|
+
add("M", x + rx, y);
|
|
2445
|
+
add("H", x + w - rx);
|
|
2446
|
+
if (rx > 0) addArc(rx, ry, x + w, y + ry);
|
|
2447
|
+
add("V", y + h - ry);
|
|
2448
|
+
if (rx > 0) addArc(rx, ry, x + w - rx, y + h);
|
|
2449
|
+
add("H", x + rx);
|
|
2450
|
+
if (rx > 0) addArc(rx, ry, x, y + h - ry);
|
|
2451
|
+
add("V", y + ry);
|
|
2452
|
+
if (rx > 0) addArc(rx, ry, x + rx, y);
|
|
2453
|
+
d = `${d} Z`;
|
|
2454
|
+
break;
|
|
2455
|
+
}
|
|
2456
|
+
default: throw new Error(`as_path not implemented for <${shape.tag}>`);
|
|
2457
|
+
}
|
|
2458
|
+
const path = pathFromD(d);
|
|
2459
|
+
copyCommonFields(path, shape);
|
|
2460
|
+
return path;
|
|
2461
|
+
}
|
|
2462
|
+
function shapeCmdSeq(shape) {
|
|
2463
|
+
return asCmdSeq(shapeStr(asPath(shape), "d"));
|
|
2464
|
+
}
|
|
2465
|
+
function absoluteShape(shape) {
|
|
2466
|
+
if (shape.tag !== "path") return shape;
|
|
2467
|
+
const target = cloneShape(shape);
|
|
2468
|
+
target.fields.d = absolutePath(shapeStr(target, "d"));
|
|
2469
|
+
return target;
|
|
2470
|
+
}
|
|
2471
|
+
function explicitLinesExpandShorthand(shape) {
|
|
2472
|
+
if (shape.tag !== "path") return shape;
|
|
2473
|
+
const target = cloneShape(shape);
|
|
2474
|
+
target.fields.d = expandShorthand(explicitLines(shapeStr(target, "d")));
|
|
2475
|
+
return target;
|
|
2476
|
+
}
|
|
2477
|
+
function applyTransform(shape, transform, ops) {
|
|
2478
|
+
let target = asPath(shape);
|
|
2479
|
+
if (target === shape) target = cloneShape(target);
|
|
2480
|
+
let cmds = [["M", [0, 0]]];
|
|
2481
|
+
if (!transform.isDegenerate()) cmds = ops.transformCmds(shapeCmdSeq(shape), transform);
|
|
2482
|
+
target.fields.d = buildD(cmds);
|
|
2483
|
+
return target;
|
|
2484
|
+
}
|
|
2485
|
+
function removeOverlaps(shape, ops) {
|
|
2486
|
+
const cmds = ops.removeOverlaps(shapeCmdSeq(shape), shapeStr(shape, "fill_rule"));
|
|
2487
|
+
const target = cloneShape(asPath(shape));
|
|
2488
|
+
target.fields.fill_rule = "nonzero";
|
|
2489
|
+
target.fields.clip_rule = "nonzero";
|
|
2490
|
+
target.fields.d = buildD(cmds);
|
|
2491
|
+
return target;
|
|
2492
|
+
}
|
|
2493
|
+
function strokeCommands(shape, tolerance, ops) {
|
|
2494
|
+
let dashArray = [];
|
|
2495
|
+
const dasharray = shapeStr(shape, "stroke_dasharray");
|
|
2496
|
+
if (dasharray !== "none") dashArray = dasharray.split(DASHARRAY_SEPARATOR).filter((v) => v).map((v) => parseFloatStrict(v, shape.tag, "stroke-dasharray"));
|
|
2497
|
+
if (dashArray.length % 2 !== 0) dashArray = [...dashArray, ...dashArray];
|
|
2498
|
+
return ops.strokeCmds(shapeCmdSeq(shape), shapeStr(shape, "stroke_linecap"), shapeStr(shape, "stroke_linejoin"), shapeNum(shape, "stroke_width"), shapeNum(shape, "stroke_miterlimit"), tolerance, dashArray, shapeNum(shape, "stroke_dashoffset"));
|
|
2499
|
+
}
|
|
2500
|
+
function parseCssDeclarations(style, output, propertyNames) {
|
|
2501
|
+
const unparsed = [];
|
|
2502
|
+
for (let declaration of style.split(";")) {
|
|
2503
|
+
declaration = declaration.trim();
|
|
2504
|
+
if ((declaration.match(/:/g) ?? []).length === 1) {
|
|
2505
|
+
const [rawName, rawValue] = declaration.split(":");
|
|
2506
|
+
const propertyName = rawName.trim();
|
|
2507
|
+
const value = rawValue.trim();
|
|
2508
|
+
if (propertyNames === void 0 || propertyNames.has(propertyName)) output[propertyName] = value;
|
|
2509
|
+
else unparsed.push(declaration);
|
|
2510
|
+
} else if (declaration) throw new Error(`Invalid CSS declaration syntax: ${declaration}`);
|
|
2511
|
+
}
|
|
2512
|
+
return unparsed.length ? unparsed.join("; ") + ";" : "";
|
|
2513
|
+
}
|
|
2514
|
+
function applyStyleAttribute(shape) {
|
|
2515
|
+
const target = cloneShape(shape);
|
|
2516
|
+
const style = shapeStr(target, "style");
|
|
2517
|
+
if (style) {
|
|
2518
|
+
const fields = shapeFields(target.tag);
|
|
2519
|
+
const attrTypes = new Map(fields.map((fd) => [attrName(fd.name), fd]));
|
|
2520
|
+
const rawAttrs = {};
|
|
2521
|
+
const unparsedStyle = parseCssDeclarations(style, rawAttrs, new Set(attrTypes.keys()));
|
|
2522
|
+
for (const [attr, rawValue] of Object.entries(rawAttrs)) {
|
|
2523
|
+
const field = attrTypes.get(attr);
|
|
2524
|
+
target.fields[field.name] = field.type === "float" ? parseFloatStrict(rawValue, target.tag, attr) : rawValue;
|
|
2525
|
+
}
|
|
2526
|
+
target.fields.style = unparsedStyle;
|
|
2527
|
+
}
|
|
2528
|
+
return target;
|
|
2529
|
+
}
|
|
2530
|
+
function mightPaint(shape, ops) {
|
|
2531
|
+
const styled = applyStyleAttribute(shape);
|
|
2532
|
+
if (shapeStr(styled, "display") === "none") return false;
|
|
2533
|
+
const visible = (fill, opacity) => fill !== "none" && shapeNum(styled, "opacity") * opacity !== 0;
|
|
2534
|
+
if (shapeCmdSeq(shape).every(([c]) => c.toUpperCase() === "M")) return false;
|
|
2535
|
+
if (visible(shapeStr(styled, "stroke"), shapeNum(styled, "stroke_opacity")) && shapeNum(styled, "stroke_width") !== 0) return true;
|
|
2536
|
+
if (!visible(shapeStr(styled, "fill"), shapeNum(styled, "fill_opacity"))) return false;
|
|
2537
|
+
return ops.pathArea(shapeCmdSeq(styled), shapeStr(styled, "fill_rule")) > 0;
|
|
2538
|
+
}
|
|
2539
|
+
function normalizeOpacity(shape) {
|
|
2540
|
+
if (shapeStr(shape, "fill") === "none" && shapeStr(shape, "stroke") === "none") return;
|
|
2541
|
+
for (const [fillAttr, opacityAttr] of [["fill", "stroke_opacity"], ["stroke", "fill_opacity"]]) if (shapeStr(shape, fillAttr) === "none") {
|
|
2542
|
+
shape.fields.opacity = shapeNum(shape, "opacity") * shapeNum(shape, opacityAttr);
|
|
2543
|
+
shape.fields[opacityAttr] = 1;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
function roundShapeFloats(shape, ndigits) {
|
|
2547
|
+
for (const field of shapeFields(shape.tag)) {
|
|
2548
|
+
const value = shape.fields[field.name];
|
|
2549
|
+
if (field.type === "float" && typeof value === "number") shape.fields[field.name] = pythonRound(value, ndigits);
|
|
2550
|
+
}
|
|
2551
|
+
if (shape.tag === "path") shape.fields.d = roundFloatsD(shapeStr(shape, "d"), ndigits);
|
|
2552
|
+
}
|
|
2553
|
+
function removeEmptySubpaths(shape, ops) {
|
|
2554
|
+
if (shape.tag !== "path") return;
|
|
2555
|
+
shape.fields.d = subpaths(shapeStr(shape, "d")).filter((sub) => mightPaint(pathFromD(sub), ops)).join(" ");
|
|
2556
|
+
}
|
|
2557
|
+
//#endregion
|
|
2558
|
+
//#region src/build/core/flatten/element.ts
|
|
2559
|
+
var GRADIENT_TAGS = /* @__PURE__ */ new Set(["linearGradient", "radialGradient"]);
|
|
2560
|
+
function numberAttrib(el, name, fallback) {
|
|
2561
|
+
return parseFloat(el.attrib.get(name) ?? String(fallback));
|
|
2562
|
+
}
|
|
2563
|
+
function isTag(el, local) {
|
|
2564
|
+
return stripNs(el.tag) === local;
|
|
2565
|
+
}
|
|
2566
|
+
function isDefs(tag) {
|
|
2567
|
+
return stripNs(tag) === "defs";
|
|
2568
|
+
}
|
|
2569
|
+
function isGradient(tag) {
|
|
2570
|
+
return GRADIENT_TAGS.has(stripNs(tag));
|
|
2571
|
+
}
|
|
2572
|
+
function isGroup(tag) {
|
|
2573
|
+
return stripNs(tag) === "g";
|
|
2574
|
+
}
|
|
2575
|
+
function clamp(value, minv = 0, maxv = 1) {
|
|
2576
|
+
return Math.max(Math.min(value, maxv), minv);
|
|
2577
|
+
}
|
|
2578
|
+
function elOpacity(el) {
|
|
2579
|
+
return clamp(numberAttrib(el, "opacity", 1));
|
|
2580
|
+
}
|
|
2581
|
+
function isRemovableGroup(el) {
|
|
2582
|
+
if (!isGroup(el.tag)) return false;
|
|
2583
|
+
if (el.attrib.size === 0) return true;
|
|
2584
|
+
const numChildren = el.children.length;
|
|
2585
|
+
const opacity = elOpacity(el);
|
|
2586
|
+
return numChildren <= 1 || opacity === 0 || opacity === 1;
|
|
2587
|
+
}
|
|
2588
|
+
function tryRemoveGroup(groupEl, pushOpacity = true) {
|
|
2589
|
+
const remove = isRemovableGroup(groupEl);
|
|
2590
|
+
const opacity = elOpacity(groupEl);
|
|
2591
|
+
if (remove) {
|
|
2592
|
+
const children = [...groupEl.children];
|
|
2593
|
+
if (groupEl.parent !== null) replaceEl(groupEl, children);
|
|
2594
|
+
if (pushOpacity) for (const child of children) inheritAttrib({ opacity: ntos(opacity) }, child);
|
|
2595
|
+
} else {
|
|
2596
|
+
groupEl.attrib.clear();
|
|
2597
|
+
groupEl.attrib.set("opacity", ntos(opacity));
|
|
2598
|
+
dropDefaultAttrib(groupEl.attrib);
|
|
2599
|
+
}
|
|
2600
|
+
return remove;
|
|
2601
|
+
}
|
|
2602
|
+
function elementTransform(el, currentTransform = Affine2D.identity()) {
|
|
2603
|
+
const attr = isGradient(el.tag) ? "gradientTransform" : "transform";
|
|
2604
|
+
const raw = el.attrib.get(attr);
|
|
2605
|
+
if (raw) return Affine2D.composeLtr([Affine2D.fromString(raw), currentTransform]);
|
|
2606
|
+
return currentTransform;
|
|
2607
|
+
}
|
|
2608
|
+
function idOfTarget(url) {
|
|
2609
|
+
const match = URL_ID_REFERENCE.exec(url);
|
|
2610
|
+
if (!match) throw new Error(`Unrecognized url "${url}"`);
|
|
2611
|
+
return match[1];
|
|
2612
|
+
}
|
|
2613
|
+
function parseViewBox(s) {
|
|
2614
|
+
const box = s.split(VIEWBOX_SEPARATOR).filter((v) => v).map((v) => parseFloat(v));
|
|
2615
|
+
if (box.length !== 4 || box.some((v) => Number.isNaN(v))) throw new Error(`Unable to parse viewBox: ${s}`);
|
|
2616
|
+
return {
|
|
2617
|
+
x: box[0],
|
|
2618
|
+
y: box[1],
|
|
2619
|
+
w: box[2],
|
|
2620
|
+
h: box[3]
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
2623
|
+
//#endregion
|
|
2624
|
+
//#region src/build/core/flatten/stages/use.ts
|
|
2625
|
+
function expandUses(doc, scopeEl) {
|
|
2626
|
+
const attribNotCopied = /* @__PURE__ */ new Set([
|
|
2627
|
+
"x",
|
|
2628
|
+
"y",
|
|
2629
|
+
"width",
|
|
2630
|
+
"height",
|
|
2631
|
+
"transform",
|
|
2632
|
+
xlinkHrefAttr(),
|
|
2633
|
+
"href"
|
|
2634
|
+
]);
|
|
2635
|
+
const elById = /* @__PURE__ */ new Map();
|
|
2636
|
+
for (const el of findAll(doc.root, (e) => e.attrib.has("id"))) elById.set(el.attrib.get("id"), el);
|
|
2637
|
+
for (;;) {
|
|
2638
|
+
const useEls = findAll(scopeEl, (e) => isTag(e, "use"));
|
|
2639
|
+
if (!useEls.length) break;
|
|
2640
|
+
const swaps = [];
|
|
2641
|
+
for (const useEl of useEls) {
|
|
2642
|
+
const ref = useEl.attrib.get(xlinkHrefAttr()) ?? useEl.attrib.get("href");
|
|
2643
|
+
if (ref === void 0) throw new Error("<use> has no href attribute to resolve");
|
|
2644
|
+
if (!ref.startsWith("#")) throw new Error(`<use> href "${ref}" is not supported: only same-document #id references are`);
|
|
2645
|
+
const target = elById.get(ref.slice(1));
|
|
2646
|
+
if (target === void 0) throw new Error(`<use> references ${ref}, but no element has id "${ref.slice(1)}"`);
|
|
2647
|
+
const newEl = target.deepClone();
|
|
2648
|
+
for (const el of newEl.iter()) el.attrib.delete("id");
|
|
2649
|
+
const group = new XEl(svgTag("g"));
|
|
2650
|
+
let affine = Affine2D.identity().translate(numberAttrib(useEl, "x", 0), numberAttrib(useEl, "y", 0));
|
|
2651
|
+
const useTransform = useEl.attrib.get("transform");
|
|
2652
|
+
if (useTransform !== void 0) affine = Affine2D.composeLtr([affine, Affine2D.fromString(useTransform)]);
|
|
2653
|
+
if (!affine.equals(Affine2D.identity())) group.attrib.set("transform", affine.toString());
|
|
2654
|
+
for (const [name, value] of useEl.attrib) {
|
|
2655
|
+
if (attribNotCopied.has(name)) continue;
|
|
2656
|
+
group.attrib.set(name, value);
|
|
2657
|
+
}
|
|
2658
|
+
group.append(newEl);
|
|
2659
|
+
if (tryRemoveGroup(group, false)) {
|
|
2660
|
+
inheritAttrib(group.attrib, newEl);
|
|
2661
|
+
swaps.push([useEl, newEl]);
|
|
2662
|
+
} else swaps.push([useEl, group]);
|
|
2663
|
+
}
|
|
2664
|
+
for (const [oldEl, newEl] of swaps) replaceEl(oldEl, [newEl]);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
function resolveUse(doc) {
|
|
2668
|
+
doc.syncTree();
|
|
2669
|
+
expandUses(doc, doc.root);
|
|
2670
|
+
}
|
|
2671
|
+
//#endregion
|
|
2672
|
+
//#region src/build/core/flatten/stages/clip-path.ts
|
|
2673
|
+
function resolveClipPath(doc, clipPathUrl, transform = Affine2D.identity()) {
|
|
2674
|
+
const clipPathEl = doc.resolveUrl(clipPathUrl, "clipPath");
|
|
2675
|
+
expandUses(doc, clipPathEl);
|
|
2676
|
+
const clipTransform = elementTransform(clipPathEl, transform);
|
|
2677
|
+
const clipShapes = clipPathEl.children.map((e) => applyTransform(fromElement(e), elementTransform(e, clipTransform), doc.ops));
|
|
2678
|
+
let clip = pathFromCommands(doc.ops.union(clipShapes.map((s) => shapeCmdSeq(s)), clipShapes.map((s) => shapeStr(s, "clip_rule"))));
|
|
2679
|
+
const nestedClip = clipPathEl.attrib.get("clip-path");
|
|
2680
|
+
if (nestedClip !== void 0) {
|
|
2681
|
+
const clipClop = resolveClipPath(doc, nestedClip, clipTransform);
|
|
2682
|
+
clip = pathFromCommands(doc.ops.intersection([shapeCmdSeq(clip), shapeCmdSeq(clipClop)], [shapeStr(clip, "clip_rule"), shapeStr(clipClop, "clip_rule")]));
|
|
2683
|
+
}
|
|
2684
|
+
return clip;
|
|
2685
|
+
}
|
|
2686
|
+
//#endregion
|
|
2687
|
+
//#region src/build/core/flatten/document.ts
|
|
2688
|
+
var MAX_PCT_ERROR = .1;
|
|
2689
|
+
var DEFAULT_DEFAULT_TOLERANCE = .1;
|
|
2690
|
+
var SvgDocument = class SvgDocument {
|
|
2691
|
+
root;
|
|
2692
|
+
ops;
|
|
2693
|
+
shapeCache = null;
|
|
2694
|
+
constructor(root, ops) {
|
|
2695
|
+
this.root = root;
|
|
2696
|
+
this.ops = ops;
|
|
2697
|
+
}
|
|
2698
|
+
static parse(content, ops) {
|
|
2699
|
+
return new SvgDocument(parseSvgDocument(content), ops);
|
|
2700
|
+
}
|
|
2701
|
+
hasShapes() {
|
|
2702
|
+
return this.shapeCache !== null;
|
|
2703
|
+
}
|
|
2704
|
+
shapeEntries() {
|
|
2705
|
+
if (this.shapeCache) return this.shapeCache;
|
|
2706
|
+
const entries = [];
|
|
2707
|
+
for (const context of this.depthFirst(false)) {
|
|
2708
|
+
const el = context.element;
|
|
2709
|
+
if (!isShapeTag(el.tag)) continue;
|
|
2710
|
+
entries.push([el, [fromElement(el, context.attrib)]]);
|
|
2711
|
+
}
|
|
2712
|
+
this.shapeCache = entries;
|
|
2713
|
+
return this.shapeCache;
|
|
2714
|
+
}
|
|
2715
|
+
invalidateShapes() {
|
|
2716
|
+
this.shapeCache = null;
|
|
2717
|
+
}
|
|
2718
|
+
mapShapes(fn) {
|
|
2719
|
+
const entries = this.shapeEntries();
|
|
2720
|
+
entries.forEach(([el, shapes], idx) => {
|
|
2721
|
+
entries[idx] = [el, shapes.map(fn)];
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
forEachShape(fn) {
|
|
2725
|
+
for (const [, shapes] of this.shapeEntries()) for (const shape of shapes) fn(shape);
|
|
2726
|
+
}
|
|
2727
|
+
inheritedAttrib(el) {
|
|
2728
|
+
const parents = [];
|
|
2729
|
+
let current = el.parent;
|
|
2730
|
+
while (current !== null) {
|
|
2731
|
+
parents.push(current);
|
|
2732
|
+
current = current.parent;
|
|
2733
|
+
}
|
|
2734
|
+
let attrib = { ...INHERITABLE_ATTRIB_DEFAULTS };
|
|
2735
|
+
for (const parent of parents.reverse()) attrib = attribToPassOn(attrib, parent);
|
|
2736
|
+
return attrib;
|
|
2737
|
+
}
|
|
2738
|
+
syncTree() {
|
|
2739
|
+
if (!this.shapeCache) return;
|
|
2740
|
+
for (const [oldEl, shapes] of this.shapeCache) {
|
|
2741
|
+
const inherited = this.inheritedAttrib(oldEl);
|
|
2742
|
+
replaceEl(oldEl, shapes.map((s) => toElement(s, inherited)));
|
|
2743
|
+
}
|
|
2744
|
+
this.shapeCache = null;
|
|
2745
|
+
}
|
|
2746
|
+
viewBox() {
|
|
2747
|
+
const raw = this.root.attrib.get("viewBox");
|
|
2748
|
+
if (raw === void 0) {
|
|
2749
|
+
const w = this.root.attrib.get("width");
|
|
2750
|
+
const h = this.root.attrib.get("height");
|
|
2751
|
+
if (w && h) return {
|
|
2752
|
+
x: 0,
|
|
2753
|
+
y: 0,
|
|
2754
|
+
w: parseFloat(w),
|
|
2755
|
+
h: parseFloat(h)
|
|
2756
|
+
};
|
|
2757
|
+
return null;
|
|
2758
|
+
}
|
|
2759
|
+
return parseViewBox(raw);
|
|
2760
|
+
}
|
|
2761
|
+
get tolerance() {
|
|
2762
|
+
const vbox = this.viewBox();
|
|
2763
|
+
if (vbox === null) return DEFAULT_DEFAULT_TOLERANCE;
|
|
2764
|
+
return Math.min(vbox.w, vbox.h) * MAX_PCT_ERROR / 100;
|
|
2765
|
+
}
|
|
2766
|
+
resolveUrl(url, elTag) {
|
|
2767
|
+
const id = idOfTarget(url);
|
|
2768
|
+
const matches = findAll(this.root, (el) => el.attrib.get("id") === id && (elTag === "*" || stripNs(el.tag) === elTag));
|
|
2769
|
+
if (matches.length !== 1) {
|
|
2770
|
+
const what = elTag === "*" ? "element" : `<${elTag}>`;
|
|
2771
|
+
throw new Error(matches.length === 0 ? `${url} references ${what} with id "${id}", but no such element exists` : `${url} matches ${matches.length} ${what} elements with id "${id}", expected exactly one`);
|
|
2772
|
+
}
|
|
2773
|
+
return matches[0];
|
|
2774
|
+
}
|
|
2775
|
+
*traverse(nextFn, appendFn, resolveClipPaths) {
|
|
2776
|
+
const frontier = [{
|
|
2777
|
+
nthOfType: 0,
|
|
2778
|
+
element: this.root,
|
|
2779
|
+
path: "/svg[0]",
|
|
2780
|
+
transform: Affine2D.identity(),
|
|
2781
|
+
clips: [],
|
|
2782
|
+
attrib: attribToPassOn({ ...INHERITABLE_ATTRIB_DEFAULTS }, this.root)
|
|
2783
|
+
}];
|
|
2784
|
+
while (frontier.length) {
|
|
2785
|
+
const context = nextFn(frontier);
|
|
2786
|
+
yield context;
|
|
2787
|
+
const childIdxs = {};
|
|
2788
|
+
const newEntries = [];
|
|
2789
|
+
for (const child of context.element.children) {
|
|
2790
|
+
const transform = elementTransform(child, context.transform);
|
|
2791
|
+
let clips = context.clips;
|
|
2792
|
+
const clipPathAttr = child.attrib.get("clip-path");
|
|
2793
|
+
if (resolveClipPaths && clipPathAttr && clipPathAttr !== "none") clips = [...clips, resolveClipPath(this, clipPathAttr, transform)];
|
|
2794
|
+
const localName = stripNs(child.tag);
|
|
2795
|
+
const nthOfType = childIdxs[localName] ?? 0;
|
|
2796
|
+
childIdxs[localName] = nthOfType + 1;
|
|
2797
|
+
newEntries.push({
|
|
2798
|
+
nthOfType,
|
|
2799
|
+
element: child,
|
|
2800
|
+
path: `${context.path}/${localName}[${nthOfType}]`,
|
|
2801
|
+
transform,
|
|
2802
|
+
clips,
|
|
2803
|
+
attrib: attribToPassOn(context.attrib, child)
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
appendFn(frontier, newEntries);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
depthFirst(resolveClipPaths = true) {
|
|
2810
|
+
return this.traverse((f) => f.pop(), (f, e) => f.push(...e.reverse()), resolveClipPaths);
|
|
2811
|
+
}
|
|
2812
|
+
breadthFirst(resolveClipPaths = true) {
|
|
2813
|
+
return this.traverse((f) => f.shift(), (f, e) => f.push(...e), resolveClipPaths);
|
|
2814
|
+
}
|
|
2815
|
+
serialize() {
|
|
2816
|
+
this.syncTree();
|
|
2817
|
+
return serializeSvg(this.root);
|
|
2818
|
+
}
|
|
2819
|
+
};
|
|
2820
|
+
//#endregion
|
|
2821
|
+
//#region src/build/core/pathkit/contours.ts
|
|
2822
|
+
function verbMap(PathKit) {
|
|
2823
|
+
return {
|
|
2824
|
+
MOVE: PathKit.MOVE_VERB ?? 0,
|
|
2825
|
+
LINE: PathKit.LINE_VERB ?? 1,
|
|
2826
|
+
QUAD: PathKit.QUAD_VERB ?? 2,
|
|
2827
|
+
CONIC: PathKit.CONIC_VERB ?? 3,
|
|
2828
|
+
CUBIC: PathKit.CUBIC_VERB ?? 4,
|
|
2829
|
+
CLOSE: PathKit.CLOSE_VERB ?? 5
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
function fillTypes(PathKit) {
|
|
2833
|
+
return {
|
|
2834
|
+
EVENODD: PathKit?.FillType?.EVENODD ?? PathKit?.FillType?.EVEN_ODD ?? 1,
|
|
2835
|
+
WINDING: PathKit?.FillType?.WINDING ?? PathKit?.FillType?.NONZERO ?? 0
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
var EPS = .01;
|
|
2839
|
+
function roundN(x, ndigits = 3) {
|
|
2840
|
+
const m = 10 ** ndigits;
|
|
2841
|
+
const s = x * m;
|
|
2842
|
+
if (!Number.isFinite(s)) return x;
|
|
2843
|
+
const floor = Math.floor(s);
|
|
2844
|
+
const frac = s - floor;
|
|
2845
|
+
const TIE_EPS = 1e-12;
|
|
2846
|
+
let roundedInt;
|
|
2847
|
+
if (Math.abs(frac - .5) < TIE_EPS) roundedInt = floor % 2 === 0 ? floor : floor + 1;
|
|
2848
|
+
else if (Math.abs(frac + .5) < TIE_EPS) {
|
|
2849
|
+
const ceil = Math.ceil(s);
|
|
2850
|
+
roundedInt = ceil % 2 === 0 ? ceil : ceil - 1;
|
|
2851
|
+
} else roundedInt = Math.round(s);
|
|
2852
|
+
return roundedInt / m;
|
|
2853
|
+
}
|
|
2854
|
+
function normPt(p) {
|
|
2855
|
+
return [Math.round(p[0] * 1e4) / 1e4, Math.round(p[1] * 1e4) / 1e4];
|
|
2856
|
+
}
|
|
2857
|
+
function eqPt(a, b) {
|
|
2858
|
+
const aa = normPt(a);
|
|
2859
|
+
const bb = normPt(b);
|
|
2860
|
+
return Math.abs(aa[0] - bb[0]) < EPS && Math.abs(aa[1] - bb[1]) < EPS;
|
|
2861
|
+
}
|
|
2862
|
+
function signedAreaPolyline(points) {
|
|
2863
|
+
if (points.length < 3) return 0;
|
|
2864
|
+
let a = 0;
|
|
2865
|
+
for (let i = 0; i < points.length; i++) {
|
|
2866
|
+
const [x1, y1] = points[i];
|
|
2867
|
+
const [x2, y2] = points[(i + 1) % points.length];
|
|
2868
|
+
a += x1 * y2 - x2 * y1;
|
|
2869
|
+
}
|
|
2870
|
+
return a / 2;
|
|
2871
|
+
}
|
|
2872
|
+
function approxSignedAreaFromContourCmds(contourCmds, VERB, steps = 24) {
|
|
2873
|
+
let cx = 0, cy = 0;
|
|
2874
|
+
let sx = 0, sy = 0;
|
|
2875
|
+
const pts = [];
|
|
2876
|
+
const add = (x, y) => pts.push([x, y]);
|
|
2877
|
+
for (const cmd of contourCmds) {
|
|
2878
|
+
const v = cmd[0];
|
|
2879
|
+
if (v === VERB.MOVE) {
|
|
2880
|
+
cx = cmd[1];
|
|
2881
|
+
cy = cmd[2];
|
|
2882
|
+
sx = cx;
|
|
2883
|
+
sy = cy;
|
|
2884
|
+
add(cx, cy);
|
|
2885
|
+
} else if (v === VERB.LINE) {
|
|
2886
|
+
cx = cmd[1];
|
|
2887
|
+
cy = cmd[2];
|
|
2888
|
+
add(cx, cy);
|
|
2889
|
+
} else if (v === VERB.QUAD) {
|
|
2890
|
+
const x0 = cx, y0 = cy;
|
|
2891
|
+
const x1 = cmd[1], y1 = cmd[2];
|
|
2892
|
+
const x2 = cmd[3], y2 = cmd[4];
|
|
2893
|
+
for (let i = 1; i <= steps; i++) {
|
|
2894
|
+
const t = i / steps;
|
|
2895
|
+
const mt = 1 - t;
|
|
2896
|
+
add(mt * mt * x0 + 2 * mt * t * x1 + t * t * x2, mt * mt * y0 + 2 * mt * t * y1 + t * t * y2);
|
|
2897
|
+
}
|
|
2898
|
+
cx = x2;
|
|
2899
|
+
cy = y2;
|
|
2900
|
+
} else if (v === VERB.CUBIC) {
|
|
2901
|
+
const x0 = cx, y0 = cy;
|
|
2902
|
+
const x1 = cmd[1], y1 = cmd[2];
|
|
2903
|
+
const x2 = cmd[3], y2 = cmd[4];
|
|
2904
|
+
const x3 = cmd[5], y3 = cmd[6];
|
|
2905
|
+
for (let i = 1; i <= steps; i++) {
|
|
2906
|
+
const t = i / steps;
|
|
2907
|
+
const mt = 1 - t;
|
|
2908
|
+
add(mt * mt * mt * x0 + 3 * mt * mt * t * x1 + 3 * mt * t * t * x2 + t * t * t * x3, mt * mt * mt * y0 + 3 * mt * mt * t * y1 + 3 * mt * t * t * y2 + t * t * t * y3);
|
|
2909
|
+
}
|
|
2910
|
+
cx = x3;
|
|
2911
|
+
cy = y3;
|
|
2912
|
+
} else if (v === VERB.CLOSE) add(sx, sy);
|
|
2913
|
+
}
|
|
2914
|
+
if (pts.length && (pts[0][0] !== pts[pts.length - 1][0] || pts[0][1] !== pts[pts.length - 1][1])) pts.push([pts[0][0], pts[0][1]]);
|
|
2915
|
+
return signedAreaPolyline(pts);
|
|
2916
|
+
}
|
|
2917
|
+
function splitContours(cmds, VERB) {
|
|
2918
|
+
const contours = [];
|
|
2919
|
+
let cur = null;
|
|
2920
|
+
for (const cmd of cmds) if (cmd[0] === VERB.MOVE) {
|
|
2921
|
+
if (cur && cur.length) contours.push(cur);
|
|
2922
|
+
cur = [cmd];
|
|
2923
|
+
} else if (cur) cur.push(cmd);
|
|
2924
|
+
if (cur && cur.length) contours.push(cur);
|
|
2925
|
+
return contours;
|
|
2926
|
+
}
|
|
2927
|
+
function ensureClosed(contourCmds, VERB) {
|
|
2928
|
+
return contourCmds.some((c) => c[0] === VERB.CLOSE) ? [...contourCmds] : [...contourCmds, [VERB.CLOSE]];
|
|
2929
|
+
}
|
|
2930
|
+
function explicitCloseWantedFromCmds(contourCmds, VERB) {
|
|
2931
|
+
if (!contourCmds?.length) return false;
|
|
2932
|
+
const m = contourCmds[0];
|
|
2933
|
+
if (m[0] !== VERB.MOVE) return false;
|
|
2934
|
+
const start = [m[1], m[2]];
|
|
2935
|
+
for (let i = contourCmds.length - 1; i >= 1; i--) {
|
|
2936
|
+
const cmd = contourCmds[i];
|
|
2937
|
+
const v = cmd[0];
|
|
2938
|
+
if (v === VERB.CLOSE) continue;
|
|
2939
|
+
let end = null;
|
|
2940
|
+
if (v === VERB.LINE) end = [cmd[1], cmd[2]];
|
|
2941
|
+
else if (v === VERB.QUAD) end = [cmd[3], cmd[4]];
|
|
2942
|
+
else if (v === VERB.CUBIC) end = [cmd[5], cmd[6]];
|
|
2943
|
+
else continue;
|
|
2944
|
+
return eqPt(end, start);
|
|
2945
|
+
}
|
|
2946
|
+
return false;
|
|
2947
|
+
}
|
|
2948
|
+
function contourToSegments(contourCmds, VERB) {
|
|
2949
|
+
const c = ensureClosed(contourCmds, VERB);
|
|
2950
|
+
const move = c[0];
|
|
2951
|
+
const sx = move[1], sy = move[2];
|
|
2952
|
+
let last = [sx, sy];
|
|
2953
|
+
const segs = [];
|
|
2954
|
+
for (let i = 1; i < c.length; i++) {
|
|
2955
|
+
const cmd = c[i];
|
|
2956
|
+
const v = cmd[0];
|
|
2957
|
+
if (v === VERB.LINE) {
|
|
2958
|
+
const end = [cmd[1], cmd[2]];
|
|
2959
|
+
segs.push({
|
|
2960
|
+
type: "L",
|
|
2961
|
+
start: last,
|
|
2962
|
+
end,
|
|
2963
|
+
synthetic: false
|
|
2964
|
+
});
|
|
2965
|
+
last = end;
|
|
2966
|
+
} else if (v === VERB.QUAD) {
|
|
2967
|
+
const ctrl = [cmd[1], cmd[2]];
|
|
2968
|
+
const end = [cmd[3], cmd[4]];
|
|
2969
|
+
segs.push({
|
|
2970
|
+
type: "Q",
|
|
2971
|
+
start: last,
|
|
2972
|
+
ctrl,
|
|
2973
|
+
end,
|
|
2974
|
+
synthetic: false
|
|
2975
|
+
});
|
|
2976
|
+
last = end;
|
|
2977
|
+
} else if (v === VERB.CUBIC) {
|
|
2978
|
+
const c1 = [cmd[1], cmd[2]];
|
|
2979
|
+
const c2 = [cmd[3], cmd[4]];
|
|
2980
|
+
const end = [cmd[5], cmd[6]];
|
|
2981
|
+
segs.push({
|
|
2982
|
+
type: "C",
|
|
2983
|
+
start: last,
|
|
2984
|
+
c1,
|
|
2985
|
+
c2,
|
|
2986
|
+
end,
|
|
2987
|
+
synthetic: false
|
|
2988
|
+
});
|
|
2989
|
+
last = end;
|
|
2990
|
+
} else if (v === VERB.CLOSE) {
|
|
2991
|
+
const end = [sx, sy];
|
|
2992
|
+
if (!eqPt(last, end)) segs.push({
|
|
2993
|
+
type: "L",
|
|
2994
|
+
start: last,
|
|
2995
|
+
end,
|
|
2996
|
+
synthetic: true
|
|
2997
|
+
});
|
|
2998
|
+
last = end;
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
return {
|
|
3002
|
+
start: [sx, sy],
|
|
3003
|
+
segs
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
3006
|
+
function applyClosePolicy(segs, startPt, explicitCloseWanted) {
|
|
3007
|
+
if (!segs.length) return segs;
|
|
3008
|
+
for (const s of segs) s.synthetic = false;
|
|
3009
|
+
const lastEnd = segs[segs.length - 1].end;
|
|
3010
|
+
if (!eqPt(lastEnd, startPt)) segs.push({
|
|
3011
|
+
type: "L",
|
|
3012
|
+
start: lastEnd,
|
|
3013
|
+
end: startPt,
|
|
3014
|
+
synthetic: false
|
|
3015
|
+
});
|
|
3016
|
+
if (!explicitCloseWanted) segs[segs.length - 1].synthetic = true;
|
|
3017
|
+
return segs;
|
|
3018
|
+
}
|
|
3019
|
+
function segmentsToContourCmds(startPt, segs, VERB) {
|
|
3020
|
+
const out = [[
|
|
3021
|
+
VERB.MOVE,
|
|
3022
|
+
roundN(startPt[0]),
|
|
3023
|
+
roundN(startPt[1])
|
|
3024
|
+
]];
|
|
3025
|
+
for (const s of segs) {
|
|
3026
|
+
if (s.synthetic) continue;
|
|
3027
|
+
if (s.type === "L") out.push([
|
|
3028
|
+
VERB.LINE,
|
|
3029
|
+
roundN(s.end[0]),
|
|
3030
|
+
roundN(s.end[1])
|
|
3031
|
+
]);
|
|
3032
|
+
else if (s.type === "Q") out.push([
|
|
3033
|
+
VERB.QUAD,
|
|
3034
|
+
roundN(s.ctrl[0]),
|
|
3035
|
+
roundN(s.ctrl[1]),
|
|
3036
|
+
roundN(s.end[0]),
|
|
3037
|
+
roundN(s.end[1])
|
|
3038
|
+
]);
|
|
3039
|
+
else out.push([
|
|
3040
|
+
VERB.CUBIC,
|
|
3041
|
+
roundN(s.c1[0]),
|
|
3042
|
+
roundN(s.c1[1]),
|
|
3043
|
+
roundN(s.c2[0]),
|
|
3044
|
+
roundN(s.c2[1]),
|
|
3045
|
+
roundN(s.end[0]),
|
|
3046
|
+
roundN(s.end[1])
|
|
3047
|
+
]);
|
|
3048
|
+
}
|
|
3049
|
+
out.push([VERB.CLOSE]);
|
|
3050
|
+
return out;
|
|
3051
|
+
}
|
|
3052
|
+
function reverseClosedContourKeepStart(contourCmds, explicitCloseWanted, VERB) {
|
|
3053
|
+
const { start, segs } = contourToSegments(contourCmds, VERB);
|
|
3054
|
+
const reversed = segs.slice().reverse().map((s) => {
|
|
3055
|
+
if (s.type === "L") return {
|
|
3056
|
+
type: "L",
|
|
3057
|
+
start: s.end,
|
|
3058
|
+
end: s.start,
|
|
3059
|
+
synthetic: false
|
|
3060
|
+
};
|
|
3061
|
+
if (s.type === "Q") return {
|
|
3062
|
+
type: "Q",
|
|
3063
|
+
start: s.end,
|
|
3064
|
+
ctrl: s.ctrl,
|
|
3065
|
+
end: s.start,
|
|
3066
|
+
synthetic: false
|
|
3067
|
+
};
|
|
3068
|
+
return {
|
|
3069
|
+
type: "C",
|
|
3070
|
+
start: s.end,
|
|
3071
|
+
c1: s.c2,
|
|
3072
|
+
c2: s.c1,
|
|
3073
|
+
end: s.start,
|
|
3074
|
+
synthetic: false
|
|
3075
|
+
};
|
|
3076
|
+
});
|
|
3077
|
+
applyClosePolicy(reversed, start, explicitCloseWanted);
|
|
3078
|
+
return segmentsToContourCmds(start, reversed, VERB);
|
|
3079
|
+
}
|
|
3080
|
+
function rotateClosedContourToStart(contourCmds, desiredStart, explicitCloseWanted, VERB) {
|
|
3081
|
+
const { segs } = contourToSegments(contourCmds, VERB);
|
|
3082
|
+
let idx = -1;
|
|
3083
|
+
for (let i = 0; i < segs.length; i++) if (eqPt(segs[i].start, desiredStart)) {
|
|
3084
|
+
idx = i;
|
|
3085
|
+
break;
|
|
3086
|
+
}
|
|
3087
|
+
if (idx === -1) {
|
|
3088
|
+
for (let i = 0; i < segs.length; i++) if (eqPt(segs[i].end, desiredStart)) {
|
|
3089
|
+
idx = (i + 1) % segs.length;
|
|
3090
|
+
break;
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
if (idx === -1) return [...contourCmds];
|
|
3094
|
+
const rotated = segs.slice(idx).concat(segs.slice(0, idx));
|
|
3095
|
+
applyClosePolicy(rotated, desiredStart, explicitCloseWanted);
|
|
3096
|
+
return segmentsToContourCmds(desiredStart, rotated, VERB);
|
|
3097
|
+
}
|
|
3098
|
+
function mergeMoves(aMoves, bMoves) {
|
|
3099
|
+
const out = [];
|
|
3100
|
+
const pushUnique = (pt) => {
|
|
3101
|
+
for (const existing of out) if (eqPt(existing, pt)) return;
|
|
3102
|
+
out.push(pt);
|
|
3103
|
+
};
|
|
3104
|
+
for (const m of aMoves || []) pushUnique(normPt(m));
|
|
3105
|
+
for (const m of bMoves || []) pushUnique(normPt(m));
|
|
3106
|
+
return out;
|
|
3107
|
+
}
|
|
3108
|
+
function bestStartMinYMinX(contourCmds, VERB) {
|
|
3109
|
+
let best = null;
|
|
3110
|
+
for (const cmd of contourCmds) {
|
|
3111
|
+
const v = cmd[0];
|
|
3112
|
+
const add = (x, y) => {
|
|
3113
|
+
const p = [x, y];
|
|
3114
|
+
if (!best) {
|
|
3115
|
+
best = p;
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
if (p[1] < best[1] - 1e-9) best = p;
|
|
3119
|
+
else if (Math.abs(p[1] - best[1]) < 1e-9 && p[0] < best[0] - 1e-9) best = p;
|
|
3120
|
+
};
|
|
3121
|
+
if (v === VERB.MOVE) add(cmd[1], cmd[2]);
|
|
3122
|
+
else if (v === VERB.LINE) add(cmd[1], cmd[2]);
|
|
3123
|
+
else if (v === VERB.QUAD) add(cmd[3], cmd[4]);
|
|
3124
|
+
else if (v === VERB.CUBIC) add(cmd[5], cmd[6]);
|
|
3125
|
+
}
|
|
3126
|
+
return best;
|
|
3127
|
+
}
|
|
3128
|
+
/**
|
|
3129
|
+
* Convert contour commands to a polyline by sampling curves.
|
|
3130
|
+
* Used for ray-casting containment tests.
|
|
3131
|
+
*/
|
|
3132
|
+
function contourToPolyline(contourCmds, V, steps = 8) {
|
|
3133
|
+
let cx = 0, cy = 0;
|
|
3134
|
+
let sx = 0, sy = 0;
|
|
3135
|
+
const pts = [];
|
|
3136
|
+
for (const cmd of contourCmds) {
|
|
3137
|
+
const v = cmd[0];
|
|
3138
|
+
if (v === V.MOVE) {
|
|
3139
|
+
cx = cmd[1];
|
|
3140
|
+
cy = cmd[2];
|
|
3141
|
+
sx = cx;
|
|
3142
|
+
sy = cy;
|
|
3143
|
+
pts.push([cx, cy]);
|
|
3144
|
+
} else if (v === V.LINE) {
|
|
3145
|
+
cx = cmd[1];
|
|
3146
|
+
cy = cmd[2];
|
|
3147
|
+
pts.push([cx, cy]);
|
|
3148
|
+
} else if (v === V.QUAD) {
|
|
3149
|
+
const x0 = cx, y0 = cy;
|
|
3150
|
+
const x1 = cmd[1], y1 = cmd[2];
|
|
3151
|
+
const x2 = cmd[3], y2 = cmd[4];
|
|
3152
|
+
for (let i = 1; i <= steps; i++) {
|
|
3153
|
+
const t = i / steps;
|
|
3154
|
+
const mt = 1 - t;
|
|
3155
|
+
pts.push([mt * mt * x0 + 2 * mt * t * x1 + t * t * x2, mt * mt * y0 + 2 * mt * t * y1 + t * t * y2]);
|
|
3156
|
+
}
|
|
3157
|
+
cx = x2;
|
|
3158
|
+
cy = y2;
|
|
3159
|
+
} else if (v === V.CUBIC) {
|
|
3160
|
+
const x0 = cx, y0 = cy;
|
|
3161
|
+
const x1 = cmd[1], y1 = cmd[2];
|
|
3162
|
+
const x2 = cmd[3], y2 = cmd[4];
|
|
3163
|
+
const x3 = cmd[5], y3 = cmd[6];
|
|
3164
|
+
for (let i = 1; i <= steps; i++) {
|
|
3165
|
+
const t = i / steps;
|
|
3166
|
+
const mt = 1 - t;
|
|
3167
|
+
pts.push([mt * mt * mt * x0 + 3 * mt * mt * t * x1 + 3 * mt * t * t * x2 + t * t * t * x3, mt * mt * mt * y0 + 3 * mt * mt * t * y1 + 3 * mt * t * t * y2 + t * t * t * y3]);
|
|
3168
|
+
}
|
|
3169
|
+
cx = x3;
|
|
3170
|
+
cy = y3;
|
|
3171
|
+
} else if (v === V.CLOSE) {
|
|
3172
|
+
if (cx !== sx || cy !== sy) pts.push([sx, sy]);
|
|
3173
|
+
cx = sx;
|
|
3174
|
+
cy = sy;
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
return pts;
|
|
3178
|
+
}
|
|
3179
|
+
/**
|
|
3180
|
+
* Ray-casting point-in-polygon test.
|
|
3181
|
+
*/
|
|
3182
|
+
function pointInPolygon(px, py, poly) {
|
|
3183
|
+
let inside = false;
|
|
3184
|
+
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
3185
|
+
const [xi, yi] = poly[i];
|
|
3186
|
+
const [xj, yj] = poly[j];
|
|
3187
|
+
if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi) inside = !inside;
|
|
3188
|
+
}
|
|
3189
|
+
return inside;
|
|
3190
|
+
}
|
|
3191
|
+
/**
|
|
3192
|
+
* Get a representative point on the contour boundary (midpoint of first segment).
|
|
3193
|
+
*/
|
|
3194
|
+
function getContourSamplePoint(contourCmds, V) {
|
|
3195
|
+
let cx = 0, cy = 0;
|
|
3196
|
+
for (const cmd of contourCmds) {
|
|
3197
|
+
const v = cmd[0];
|
|
3198
|
+
if (v === V.MOVE) {
|
|
3199
|
+
cx = cmd[1];
|
|
3200
|
+
cy = cmd[2];
|
|
3201
|
+
} else if (v === V.LINE) return [(cx + cmd[1]) / 2, (cy + cmd[2]) / 2];
|
|
3202
|
+
else if (v === V.QUAD) {
|
|
3203
|
+
const t = .5, mt = .5;
|
|
3204
|
+
return [mt * mt * cx + 2 * mt * t * cmd[1] + t * t * cmd[3], mt * mt * cy + 2 * mt * t * cmd[2] + t * t * cmd[4]];
|
|
3205
|
+
} else if (v === V.CUBIC) {
|
|
3206
|
+
const t = .5, mt = .5;
|
|
3207
|
+
return [mt ** 3 * cx + 3 * mt ** 2 * t * cmd[1] + 3 * mt * t ** 2 * cmd[3] + t ** 3 * cmd[5], mt ** 3 * cy + 3 * mt ** 2 * t * cmd[2] + 3 * mt * t ** 2 * cmd[4] + t ** 3 * cmd[6]];
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
return null;
|
|
3211
|
+
}
|
|
3212
|
+
/**
|
|
3213
|
+
* Apply containment-based winding fix to contour objects.
|
|
3214
|
+
* Even nesting depth = CCW (outer), odd = CW (hole).
|
|
3215
|
+
*/
|
|
3216
|
+
function applyContainmentWinding(contourObjs, V) {
|
|
3217
|
+
const n = contourObjs.length;
|
|
3218
|
+
if (n === 0) return;
|
|
3219
|
+
const polylines = contourObjs.map((obj) => contourToPolyline(obj.cmds, V));
|
|
3220
|
+
const samplePts = contourObjs.map((obj) => getContourSamplePoint(obj.cmds, V));
|
|
3221
|
+
const depths = new Array(n).fill(0);
|
|
3222
|
+
for (let i = 0; i < n; i++) {
|
|
3223
|
+
const pt = samplePts[i];
|
|
3224
|
+
if (!pt) continue;
|
|
3225
|
+
for (let j = 0; j < n; j++) {
|
|
3226
|
+
if (i === j) continue;
|
|
3227
|
+
if (pointInPolygon(pt[0], pt[1], polylines[j])) depths[i] = (depths[i] ?? 0) + 1;
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
const ensureOrient = (obj, wantCCW) => {
|
|
3231
|
+
if (wantCCW !== approxSignedAreaFromContourCmds(obj.cmds, V) > 0) obj.cmds = reverseClosedContourKeepStart(obj.cmds, obj.explicitCloseWanted, V);
|
|
3232
|
+
};
|
|
3233
|
+
for (let i = 0; i < n; i++) ensureOrient(contourObjs[i], depths[i] % 2 === 0);
|
|
3234
|
+
}
|
|
3235
|
+
function orientedContours(cmds, V) {
|
|
3236
|
+
const contours = splitContours(cmds, V).map((c) => {
|
|
3237
|
+
const explicitCloseWanted = explicitCloseWantedFromCmds(c, V);
|
|
3238
|
+
const cc = ensureClosed(c, V);
|
|
3239
|
+
const a = approxSignedAreaFromContourCmds(cc, V);
|
|
3240
|
+
return {
|
|
3241
|
+
cmds: cc,
|
|
3242
|
+
absA: Math.abs(a),
|
|
3243
|
+
explicitCloseWanted
|
|
3244
|
+
};
|
|
3245
|
+
});
|
|
3246
|
+
contours.sort((x, y) => y.absA - x.absA);
|
|
3247
|
+
applyContainmentWinding(contours, V);
|
|
3248
|
+
return contours;
|
|
3249
|
+
}
|
|
3250
|
+
function canonicalContourCmds(cmds, recordedMoves, preferMinYMinXStart, V) {
|
|
3251
|
+
const moves = recordedMoves.map((m) => normPt([m[0], m[1]]));
|
|
3252
|
+
const used = new Array(moves.length).fill(false);
|
|
3253
|
+
const contours = orientedContours(cmds, V);
|
|
3254
|
+
for (const obj of contours) {
|
|
3255
|
+
let cc = obj.cmds;
|
|
3256
|
+
for (let i = 0; i < moves.length; i++) {
|
|
3257
|
+
if (used[i]) continue;
|
|
3258
|
+
const target = moves[i];
|
|
3259
|
+
const { segs } = contourToSegments(cc, V);
|
|
3260
|
+
if (segs.some((s) => eqPt(s.start, target) || eqPt(s.end, target))) {
|
|
3261
|
+
used[i] = true;
|
|
3262
|
+
cc = rotateClosedContourToStart(cc, target, obj.explicitCloseWanted, V);
|
|
3263
|
+
break;
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
if (preferMinYMinXStart) {
|
|
3267
|
+
const best = bestStartMinYMinX(cc, V);
|
|
3268
|
+
if (best) cc = rotateClosedContourToStart(cc, best, obj.explicitCloseWanted, V);
|
|
3269
|
+
}
|
|
3270
|
+
obj.cmds = cc;
|
|
3271
|
+
}
|
|
3272
|
+
return contours.flatMap((x) => x.cmds);
|
|
3273
|
+
}
|
|
3274
|
+
function cmdsToPathData(cmds, V) {
|
|
3275
|
+
const parts = [];
|
|
3276
|
+
for (const cmd of cmds) {
|
|
3277
|
+
const v = cmd[0];
|
|
3278
|
+
if (v === V.MOVE) parts.push(`M${roundN(cmd[1])} ${roundN(cmd[2])}`);
|
|
3279
|
+
else if (v === V.LINE) parts.push(`L${roundN(cmd[1])} ${roundN(cmd[2])}`);
|
|
3280
|
+
else if (v === V.QUAD) parts.push(`Q${roundN(cmd[1])} ${roundN(cmd[2])} ${roundN(cmd[3])} ${roundN(cmd[4])}`);
|
|
3281
|
+
else if (v === V.CUBIC) parts.push(`C${roundN(cmd[1])} ${roundN(cmd[2])} ${roundN(cmd[3])} ${roundN(cmd[4])} ${roundN(cmd[5])} ${roundN(cmd[6])}`);
|
|
3282
|
+
else if (v === V.CLOSE) parts.push("Z");
|
|
3283
|
+
}
|
|
3284
|
+
return parts.join(" ");
|
|
3285
|
+
}
|
|
3286
|
+
function contoursArea(cmds, V) {
|
|
3287
|
+
let total = 0;
|
|
3288
|
+
for (const c of splitContours(cmds, V)) total += Math.abs(approxSignedAreaFromContourCmds(ensureClosed(c, V), V));
|
|
3289
|
+
return total;
|
|
3290
|
+
}
|
|
3291
|
+
//#endregion
|
|
3292
|
+
//#region src/build/core/flatten/pathops.ts
|
|
3293
|
+
var PathOpsError = class extends Error {};
|
|
3294
|
+
function createPathOps(PathKit) {
|
|
3295
|
+
const V = verbMap(PathKit);
|
|
3296
|
+
const FILL = fillTypes(PathKit);
|
|
3297
|
+
const Caps = PathKit.StrokeCap ?? {};
|
|
3298
|
+
const Joins = PathKit.StrokeJoin ?? {};
|
|
3299
|
+
const Ops = PathKit.PathOp ?? {};
|
|
3300
|
+
const LINE_CAP = {
|
|
3301
|
+
butt: Caps.BUTT ?? 0,
|
|
3302
|
+
round: Caps.ROUND ?? 1,
|
|
3303
|
+
square: Caps.SQUARE ?? 2
|
|
3304
|
+
};
|
|
3305
|
+
const LINE_JOIN = {
|
|
3306
|
+
miter: Joins.MITER ?? 0,
|
|
3307
|
+
round: Joins.ROUND ?? 1,
|
|
3308
|
+
bevel: Joins.BEVEL ?? 2
|
|
3309
|
+
};
|
|
3310
|
+
const OP_UNION = Ops.UNION ?? 0;
|
|
3311
|
+
const OP_INTERSECT = Ops.INTERSECT ?? 1;
|
|
3312
|
+
function fillTypeFor(fillRule) {
|
|
3313
|
+
if (fillRule === "nonzero") return FILL.WINDING;
|
|
3314
|
+
if (fillRule === "evenodd") return FILL.EVENODD;
|
|
3315
|
+
throw new Error(`Invalid fill rule: ${fillRule}`);
|
|
3316
|
+
}
|
|
3317
|
+
function dispose(h) {
|
|
3318
|
+
h.p.delete?.();
|
|
3319
|
+
}
|
|
3320
|
+
function fromCommands(cmds, fillRule) {
|
|
3321
|
+
const p = PathKit.NewPath();
|
|
3322
|
+
p.setFillType(fillTypeFor(fillRule));
|
|
3323
|
+
const moves = [];
|
|
3324
|
+
for (const [cmd, args] of cmds) switch (cmd) {
|
|
3325
|
+
case "M":
|
|
3326
|
+
p.moveTo(args[0], args[1]);
|
|
3327
|
+
moves.push(normPt([args[0], args[1]]));
|
|
3328
|
+
break;
|
|
3329
|
+
case "L":
|
|
3330
|
+
p.lineTo(args[0], args[1]);
|
|
3331
|
+
break;
|
|
3332
|
+
case "Q":
|
|
3333
|
+
p.quadTo(args[0], args[1], args[2], args[3]);
|
|
3334
|
+
break;
|
|
3335
|
+
case "C":
|
|
3336
|
+
p.cubicTo(args[0], args[1], args[2], args[3], args[4], args[5]);
|
|
3337
|
+
break;
|
|
3338
|
+
case "Z":
|
|
3339
|
+
p.close();
|
|
3340
|
+
break;
|
|
3341
|
+
default:
|
|
3342
|
+
p.delete?.();
|
|
3343
|
+
throw new Error(`No mapping to Skia for "${cmd} ${args}"`);
|
|
3344
|
+
}
|
|
3345
|
+
return {
|
|
3346
|
+
p,
|
|
3347
|
+
moves
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3350
|
+
function normalizedCmds(h) {
|
|
3351
|
+
const p2 = PathKit.FromSVGString(h.p.toSVGString());
|
|
3352
|
+
const cmds = p2 ? p2.toCmds() : [];
|
|
3353
|
+
p2?.delete?.();
|
|
3354
|
+
return cmds;
|
|
3355
|
+
}
|
|
3356
|
+
function simplifyToWinding(h) {
|
|
3357
|
+
try {
|
|
3358
|
+
h.p.simplify();
|
|
3359
|
+
const p2 = PathKit.FromSVGString(h.p.toSVGString());
|
|
3360
|
+
if (p2) {
|
|
3361
|
+
h.p.delete?.();
|
|
3362
|
+
h.p = p2;
|
|
3363
|
+
}
|
|
3364
|
+
} catch {}
|
|
3365
|
+
h.p.setFillType(FILL.WINDING);
|
|
3366
|
+
}
|
|
3367
|
+
function toCommands(h) {
|
|
3368
|
+
const cmds = canonicalContourCmds(normalizedCmds(h), h.moves, h.moves.length === 0, V);
|
|
3369
|
+
const out = [];
|
|
3370
|
+
for (const cmd of cmds) {
|
|
3371
|
+
const v = cmd[0];
|
|
3372
|
+
if (v === V.MOVE) out.push(["M", [cmd[1], cmd[2]]]);
|
|
3373
|
+
else if (v === V.LINE) out.push(["L", [cmd[1], cmd[2]]]);
|
|
3374
|
+
else if (v === V.QUAD) out.push(["Q", [
|
|
3375
|
+
cmd[1],
|
|
3376
|
+
cmd[2],
|
|
3377
|
+
cmd[3],
|
|
3378
|
+
cmd[4]
|
|
3379
|
+
]]);
|
|
3380
|
+
else if (v === V.CUBIC) out.push(["C", [
|
|
3381
|
+
cmd[1],
|
|
3382
|
+
cmd[2],
|
|
3383
|
+
cmd[3],
|
|
3384
|
+
cmd[4],
|
|
3385
|
+
cmd[5],
|
|
3386
|
+
cmd[6]
|
|
3387
|
+
]]);
|
|
3388
|
+
else if (v === V.CLOSE) out.push(["Z", []]);
|
|
3389
|
+
else throw new Error(`Unexpected verb in cmds: ${v}`);
|
|
3390
|
+
}
|
|
3391
|
+
return out;
|
|
3392
|
+
}
|
|
3393
|
+
function combine(a, b, op) {
|
|
3394
|
+
try {
|
|
3395
|
+
const out = PathKit.MakeFromOp(a.p, b.p, op);
|
|
3396
|
+
if (!out) return null;
|
|
3397
|
+
return {
|
|
3398
|
+
p: out,
|
|
3399
|
+
moves: mergeMoves(a.moves, b.moves)
|
|
3400
|
+
};
|
|
3401
|
+
} catch {
|
|
3402
|
+
return null;
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
function doPathop(op, cmdSeqs, fillRules) {
|
|
3406
|
+
if (!cmdSeqs.length) return [];
|
|
3407
|
+
let skPath = fromCommands(cmdSeqs[0], fillRules[0]);
|
|
3408
|
+
try {
|
|
3409
|
+
for (let i = 1; i < cmdSeqs.length; i++) {
|
|
3410
|
+
const skPath2 = fromCommands(cmdSeqs[i], fillRules[i]);
|
|
3411
|
+
const merged = combine(skPath, skPath2, op);
|
|
3412
|
+
dispose(skPath2);
|
|
3413
|
+
if (merged === null) throw new PathOpsError("operation did not succeed");
|
|
3414
|
+
dispose(skPath);
|
|
3415
|
+
skPath = merged;
|
|
3416
|
+
}
|
|
3417
|
+
simplifyToWinding(skPath);
|
|
3418
|
+
return toCommands(skPath);
|
|
3419
|
+
} finally {
|
|
3420
|
+
dispose(skPath);
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
return {
|
|
3424
|
+
union(cmdSeqs, fillRules) {
|
|
3425
|
+
return doPathop(OP_UNION, cmdSeqs, fillRules);
|
|
3426
|
+
},
|
|
3427
|
+
intersection(cmdSeqs, fillRules) {
|
|
3428
|
+
return doPathop(OP_INTERSECT, cmdSeqs, fillRules);
|
|
3429
|
+
},
|
|
3430
|
+
removeOverlaps(cmds, fillRule) {
|
|
3431
|
+
const h = fromCommands(cmds, fillRule);
|
|
3432
|
+
try {
|
|
3433
|
+
simplifyToWinding(h);
|
|
3434
|
+
return toCommands(h);
|
|
3435
|
+
} finally {
|
|
3436
|
+
dispose(h);
|
|
3437
|
+
}
|
|
3438
|
+
},
|
|
3439
|
+
transformCmds(cmds, affine) {
|
|
3440
|
+
const h = fromCommands(cmds, "nonzero");
|
|
3441
|
+
const { a, b, c, d, e, f } = affine;
|
|
3442
|
+
const p = PathKit.NewPath(h.p);
|
|
3443
|
+
p.transform(a, c, e, b, d, f, 0, 0, 1);
|
|
3444
|
+
const transformed = {
|
|
3445
|
+
p,
|
|
3446
|
+
moves: h.moves.map(([x, y]) => normPt([a * x + c * y + e, b * x + d * y + f]))
|
|
3447
|
+
};
|
|
3448
|
+
try {
|
|
3449
|
+
return toCommands(transformed);
|
|
3450
|
+
} finally {
|
|
3451
|
+
dispose(h);
|
|
3452
|
+
dispose(transformed);
|
|
3453
|
+
}
|
|
3454
|
+
},
|
|
3455
|
+
strokeCmds(cmds, svgLinecap, svgLinejoin, strokeWidth, strokeMiterlimit, _tolerance, dashArray = [], dashOffset = 0) {
|
|
3456
|
+
const cap = LINE_CAP[svgLinecap];
|
|
3457
|
+
if (cap === void 0) throw new Error(`Unsupported cap ${svgLinecap}`);
|
|
3458
|
+
const join = LINE_JOIN[svgLinejoin];
|
|
3459
|
+
if (join === void 0) throw new Error(`Unsupported join ${svgLinejoin}`);
|
|
3460
|
+
const h = fromCommands(cmds, "nonzero");
|
|
3461
|
+
const work = PathKit.NewPath(h.p);
|
|
3462
|
+
let strokedPath;
|
|
3463
|
+
try {
|
|
3464
|
+
if (dashArray.length === 2 && typeof work.dash === "function") work.dash(Number(dashArray[0]), Number(dashArray[1]), dashOffset || 0);
|
|
3465
|
+
let stroked = work.stroke({
|
|
3466
|
+
width: strokeWidth,
|
|
3467
|
+
cap,
|
|
3468
|
+
join,
|
|
3469
|
+
miter_limit: strokeMiterlimit
|
|
3470
|
+
});
|
|
3471
|
+
if (!stroked || typeof stroked.toCmds !== "function") stroked = work;
|
|
3472
|
+
if (stroked !== work) work.delete?.();
|
|
3473
|
+
strokedPath = stroked;
|
|
3474
|
+
} catch {
|
|
3475
|
+
strokedPath = work;
|
|
3476
|
+
}
|
|
3477
|
+
const result = {
|
|
3478
|
+
p: strokedPath,
|
|
3479
|
+
moves: []
|
|
3480
|
+
};
|
|
3481
|
+
try {
|
|
3482
|
+
simplifyToWinding(result);
|
|
3483
|
+
return toCommands(result);
|
|
3484
|
+
} finally {
|
|
3485
|
+
dispose(h);
|
|
3486
|
+
dispose(result);
|
|
3487
|
+
}
|
|
3488
|
+
},
|
|
3489
|
+
pathArea(cmds, fillRule) {
|
|
3490
|
+
const h = fromCommands(cmds, fillRule);
|
|
3491
|
+
try {
|
|
3492
|
+
simplifyToWinding(h);
|
|
3493
|
+
try {
|
|
3494
|
+
return contoursArea(normalizedCmds(h), V);
|
|
3495
|
+
} catch {
|
|
3496
|
+
return 0;
|
|
3497
|
+
}
|
|
3498
|
+
} finally {
|
|
3499
|
+
dispose(h);
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
};
|
|
3503
|
+
}
|
|
3504
|
+
//#endregion
|
|
3505
|
+
//#region src/build/core/flatten/stages/defs.ts
|
|
3506
|
+
function addToDefs(defs, newEl) {
|
|
3507
|
+
const newId = newEl.attrib.get("id");
|
|
3508
|
+
if (newId === void 0) return;
|
|
3509
|
+
let insertAt = defs.children.length;
|
|
3510
|
+
for (let i = 0; i < defs.children.length; i++) if (newId < (defs.children[i].attrib.get("id") ?? "")) {
|
|
3511
|
+
insertAt = i;
|
|
3512
|
+
break;
|
|
3513
|
+
}
|
|
3514
|
+
defs.insert(insertAt, newEl);
|
|
3515
|
+
}
|
|
3516
|
+
function removeOrphanedGradients(doc) {
|
|
3517
|
+
const usedGradientIds = /* @__PURE__ */ new Set();
|
|
3518
|
+
for (const [, shapes] of doc.shapeEntries()) for (const shape of shapes) {
|
|
3519
|
+
const fill = shapeStr(shape, "fill");
|
|
3520
|
+
if (!fill.startsWith("url(")) continue;
|
|
3521
|
+
let el;
|
|
3522
|
+
try {
|
|
3523
|
+
el = doc.resolveUrl(fill, "*");
|
|
3524
|
+
} catch {
|
|
3525
|
+
continue;
|
|
3526
|
+
}
|
|
3527
|
+
if (!isGradient(el.tag)) continue;
|
|
3528
|
+
const id = el.attrib.get("id");
|
|
3529
|
+
if (id !== void 0) usedGradientIds.add(id);
|
|
3530
|
+
}
|
|
3531
|
+
for (const grad of findAll(doc.root, (e) => isGradient(e.tag))) if (!usedGradientIds.has(grad.attrib.get("id") ?? "")) grad.detach();
|
|
3532
|
+
doc.invalidateShapes();
|
|
3533
|
+
}
|
|
3534
|
+
//#endregion
|
|
3535
|
+
//#region src/build/core/flatten/stages/stroke.ts
|
|
3536
|
+
function strokeToPaths(doc, shape) {
|
|
3537
|
+
const stroke = cloneShape(asPath(shape));
|
|
3538
|
+
stroke.fields.d = pathFromCommands(strokeCommands(shape, doc.tolerance, doc.ops)).fields.d;
|
|
3539
|
+
stroke.fields.fill_rule = "nonzero";
|
|
3540
|
+
stroke.fields.clip_rule = "nonzero";
|
|
3541
|
+
stroke.fields.opacity = stroke.fields.opacity * stroke.fields.stroke_opacity;
|
|
3542
|
+
stroke.fields.fill = stroke.fields.stroke;
|
|
3543
|
+
shape.fields.opacity = shape.fields.opacity * shape.fields.fill_opacity;
|
|
3544
|
+
shape.fields.fill_opacity = 1;
|
|
3545
|
+
stroke.fields.fill_opacity = 1;
|
|
3546
|
+
for (const cleanmeup of [shape, stroke]) resetFields(cleanmeup, (name) => name.startsWith("stroke"));
|
|
3547
|
+
if (!mightPaint(shape, doc.ops)) return [stroke];
|
|
3548
|
+
shape.fields.id = "";
|
|
3549
|
+
stroke.fields.id = "";
|
|
3550
|
+
return [shape, stroke];
|
|
3551
|
+
}
|
|
3552
|
+
//#endregion
|
|
3553
|
+
//#region src/build/core/flatten/stages/collapse.ts
|
|
3554
|
+
function collapse(doc) {
|
|
3555
|
+
doc.syncTree();
|
|
3556
|
+
const toProcess = [...doc.breadthFirst()].reverse();
|
|
3557
|
+
const defs = new XEl(svgTag("defs"));
|
|
3558
|
+
doc.root.insert(0, defs);
|
|
3559
|
+
for (const context of toProcess) {
|
|
3560
|
+
if (context.path.includes("clipPath")) {
|
|
3561
|
+
context.element.detach();
|
|
3562
|
+
continue;
|
|
3563
|
+
}
|
|
3564
|
+
const el = context.element;
|
|
3565
|
+
delAttrs(el, "clip-path", "transform");
|
|
3566
|
+
inheritAttrib(context.attrib, el);
|
|
3567
|
+
if (isShapeTag(el.tag)) {
|
|
3568
|
+
if (el.children.length) throw new Error("Shapes shouldn't have children");
|
|
3569
|
+
const paths = [absoluteShape(asPath(fromElement(el)))];
|
|
3570
|
+
const initialPath = cloneShape(paths[0]);
|
|
3571
|
+
if (shapeStr(paths[0], "stroke") !== "none") paths.splice(0, paths.length, ...strokeToPaths(doc, paths[0]));
|
|
3572
|
+
for (const path of paths) resetFields(path, (name) => name.startsWith("stroke"));
|
|
3573
|
+
if (!context.transform.equals(Affine2D.identity())) paths.forEach((p, i) => {
|
|
3574
|
+
paths[i] = applyTransform(p, context.transform, doc.ops);
|
|
3575
|
+
});
|
|
3576
|
+
if (context.clips.length) for (const p of paths) {
|
|
3577
|
+
const cmds = doc.ops.intersection([shapeCmdSeq(p), ...context.clips.map((c) => shapeCmdSeq(c))], [shapeStr(p, "fill_rule"), ...context.clips.map((c) => shapeStr(c, "clip_rule"))]);
|
|
3578
|
+
p.fields.d = pathFromCommands(cmds).fields.d;
|
|
3579
|
+
p.fields.fill_rule = "nonzero";
|
|
3580
|
+
}
|
|
3581
|
+
if (paths.length !== 1 || !shapesEqual(paths[0], initialPath)) replaceEl(el, paths.map((p) => toElement(p)));
|
|
3582
|
+
} else if (isGradient(el.tag)) {
|
|
3583
|
+
el.detach();
|
|
3584
|
+
addToDefs(defs, el);
|
|
3585
|
+
} else if (isDefs(el.tag)) {
|
|
3586
|
+
for (const childEl of [...el.children]) addToDefs(defs, childEl);
|
|
3587
|
+
el.detach();
|
|
3588
|
+
} else if (isGroup(el.tag)) tryRemoveGroup(el);
|
|
3589
|
+
}
|
|
3590
|
+
delAttrs(doc.root, ...INHERITABLE_ATTRIB);
|
|
3591
|
+
removeOrphanedGradients(doc);
|
|
3592
|
+
for (const unusedEl of [...defs.children]) if (!isGradient(unusedEl.tag)) unusedEl.detach();
|
|
3593
|
+
doc.invalidateShapes();
|
|
3594
|
+
}
|
|
3595
|
+
//#endregion
|
|
3596
|
+
//#region src/build/core/flatten/stages/discard.ts
|
|
3597
|
+
function removeForeignAttrs(el, goodNs) {
|
|
3598
|
+
const attrToRm = [];
|
|
3599
|
+
for (const attr of el.attrib.keys()) if (!goodNs.has(splitNs(attr)[0])) attrToRm.push(attr);
|
|
3600
|
+
delAttrs(el, ...attrToRm);
|
|
3601
|
+
}
|
|
3602
|
+
function removeNonSvgContent(doc) {
|
|
3603
|
+
doc.syncTree();
|
|
3604
|
+
const goodNs = /* @__PURE__ */ new Set([SVG_NS, XLINK_NS]);
|
|
3605
|
+
if (splitNs(doc.root.tag)[0] === "http://www.w3.org/2000/svg") goodNs.add(null);
|
|
3606
|
+
const elToRm = [];
|
|
3607
|
+
for (const el of doc.root.iter()) {
|
|
3608
|
+
if (el === doc.root) continue;
|
|
3609
|
+
if (!goodNs.has(splitNs(el.tag)[0])) {
|
|
3610
|
+
elToRm.push(el);
|
|
3611
|
+
continue;
|
|
3612
|
+
}
|
|
3613
|
+
removeForeignAttrs(el, goodNs);
|
|
3614
|
+
}
|
|
3615
|
+
removeForeignAttrs(doc.root, goodNs);
|
|
3616
|
+
for (const el of elToRm) el.detach();
|
|
3617
|
+
doc.invalidateShapes();
|
|
3618
|
+
}
|
|
3619
|
+
function removeAnonymousSymbols(doc) {
|
|
3620
|
+
doc.syncTree();
|
|
3621
|
+
for (const el of findAll(doc.root, (e) => stripNs(e.tag) === "symbol" && !e.attrib.has("id"))) el.detach();
|
|
3622
|
+
}
|
|
3623
|
+
function removeTitleMetaDesc(doc) {
|
|
3624
|
+
doc.syncTree();
|
|
3625
|
+
const tags = /* @__PURE__ */ new Set([
|
|
3626
|
+
"title",
|
|
3627
|
+
"desc",
|
|
3628
|
+
"metadata",
|
|
3629
|
+
"comment"
|
|
3630
|
+
]);
|
|
3631
|
+
for (const el of findAll(doc.root, (e) => tags.has(stripNs(e.tag)))) el.detach();
|
|
3632
|
+
}
|
|
3633
|
+
//#endregion
|
|
3634
|
+
//#region src/build/core/flatten/stages/fill-rule.ts
|
|
3635
|
+
function evenoddToNonzeroWinding(doc) {
|
|
3636
|
+
doc.mapShapes((s) => shapeStr(s, "fill_rule") === "evenodd" ? removeOverlaps(s, doc.ops) : s);
|
|
3637
|
+
}
|
|
3638
|
+
//#endregion
|
|
3639
|
+
//#region src/build/core/flatten/stages/nested-svg.ts
|
|
3640
|
+
function rectEquals(a, b) {
|
|
3641
|
+
return a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;
|
|
3642
|
+
}
|
|
3643
|
+
function nestedSvgTransform(attrs, parentWidth, parentHeight) {
|
|
3644
|
+
const num = (raw, fallback) => {
|
|
3645
|
+
if (raw === null || raw === void 0 || !raw.trim()) return fallback;
|
|
3646
|
+
const value = parseFloat(raw);
|
|
3647
|
+
return Number.isFinite(value) ? value : fallback;
|
|
3648
|
+
};
|
|
3649
|
+
const x = num(attrs.x, 0);
|
|
3650
|
+
const y = num(attrs.y, 0);
|
|
3651
|
+
const viewport = {
|
|
3652
|
+
x,
|
|
3653
|
+
y,
|
|
3654
|
+
w: num(attrs.width, parentWidth),
|
|
3655
|
+
h: num(attrs.height, parentHeight)
|
|
3656
|
+
};
|
|
3657
|
+
const viewBox = attrs.viewBox === null || attrs.viewBox === void 0 ? viewport : parseViewBox(attrs.viewBox);
|
|
3658
|
+
let transform = rectEquals(viewport, viewBox) ? Affine2D.identity().translate(x, y) : Affine2D.rectToRect(viewBox, viewport, attrs.preserveAspectRatio ?? "xMidYMid");
|
|
3659
|
+
if (attrs.transform !== null && attrs.transform !== void 0) transform = Affine2D.composeLtr([transform, Affine2D.fromString(attrs.transform)]);
|
|
3660
|
+
return {
|
|
3661
|
+
transform,
|
|
3662
|
+
viewport,
|
|
3663
|
+
viewBox
|
|
3664
|
+
};
|
|
3665
|
+
}
|
|
3666
|
+
function viewportRect(x, y, width, height) {
|
|
3667
|
+
return toElement(fromElement(new XEl(svgTag("rect"), [
|
|
3668
|
+
["x", ntos(x)],
|
|
3669
|
+
["y", ntos(y)],
|
|
3670
|
+
["width", ntos(width)],
|
|
3671
|
+
["height", ntos(height)]
|
|
3672
|
+
])));
|
|
3673
|
+
}
|
|
3674
|
+
function newId(doc, template) {
|
|
3675
|
+
for (let i = 0; i < 65536; i++) {
|
|
3676
|
+
const candidate = template(i);
|
|
3677
|
+
if (!findAll(doc.root, (el) => el.attrib.get("id") === candidate).length) return candidate;
|
|
3678
|
+
}
|
|
3679
|
+
throw new Error("No free id for nested svg viewport");
|
|
3680
|
+
}
|
|
3681
|
+
function nestedSvgs(root) {
|
|
3682
|
+
const found = [];
|
|
3683
|
+
const frontier = [...root.children];
|
|
3684
|
+
while (frontier.length) {
|
|
3685
|
+
const el = frontier.shift();
|
|
3686
|
+
if (isTag(el, "svg")) found.push(el);
|
|
3687
|
+
else if (el.children.length) frontier.push(...el.children);
|
|
3688
|
+
}
|
|
3689
|
+
return found;
|
|
3690
|
+
}
|
|
3691
|
+
function unnestSvg(doc, svg, parentWidth, parentHeight) {
|
|
3692
|
+
const { transform, viewport, viewBox } = nestedSvgTransform({
|
|
3693
|
+
x: svg.attrib.get("x") ?? null,
|
|
3694
|
+
y: svg.attrib.get("y") ?? null,
|
|
3695
|
+
width: svg.attrib.get("width") ?? null,
|
|
3696
|
+
height: svg.attrib.get("height") ?? null,
|
|
3697
|
+
viewBox: svg.attrib.get("viewBox") ?? null,
|
|
3698
|
+
transform: svg.attrib.get("transform") ?? null,
|
|
3699
|
+
preserveAspectRatio: svg.attrib.get("preserveAspectRatio") ?? null
|
|
3700
|
+
}, parentWidth, parentHeight);
|
|
3701
|
+
const { x, y, w: width, h: height } = viewport;
|
|
3702
|
+
for (const inner of nestedSvgs(svg)) replaceEl(inner, unnestSvg(doc, inner, viewBox.w, viewBox.h));
|
|
3703
|
+
const g = new XEl(svgTag("g"));
|
|
3704
|
+
for (const child of [...svg.children]) g.append(child);
|
|
3705
|
+
if (!transform.equals(Affine2D.identity())) g.attrib.set("transform", transform.toString());
|
|
3706
|
+
const overflow = svg.attrib.get("overflow") ?? "hidden";
|
|
3707
|
+
if (overflow === "visible") return [g];
|
|
3708
|
+
if (overflow !== "hidden") throw new Error(`overflow='${overflow}' is not supported`);
|
|
3709
|
+
const clipId = newId(doc, (i) => `nested-svg-viewport-${i}`);
|
|
3710
|
+
const clipPath = new XEl(svgTag("clipPath"), [["id", clipId]]);
|
|
3711
|
+
clipPath.append(viewportRect(x, y, width, height));
|
|
3712
|
+
const clippedG = new XEl(svgTag("g"), [["clip-path", `url(#${clipId})`]]);
|
|
3713
|
+
clippedG.append(g);
|
|
3714
|
+
return [clipPath, clippedG];
|
|
3715
|
+
}
|
|
3716
|
+
function resolveNestedSvgs(doc) {
|
|
3717
|
+
doc.syncTree();
|
|
3718
|
+
const nested = nestedSvgs(doc.root);
|
|
3719
|
+
if (!nested.length) return;
|
|
3720
|
+
const vbox = doc.viewBox();
|
|
3721
|
+
if (vbox === null) throw new Error("Can't determine root SVG width/height, which is required for resolving nested SVGs");
|
|
3722
|
+
for (const el of nested) replaceEl(el, unnestSvg(doc, el, vbox.w, vbox.h));
|
|
3723
|
+
doc.invalidateShapes();
|
|
3724
|
+
}
|
|
3725
|
+
//#endregion
|
|
3726
|
+
//#region src/build/core/flatten/stages/opacity.ts
|
|
3727
|
+
function normalizeOpacities(doc) {
|
|
3728
|
+
doc.forEachShape((s) => normalizeOpacity(s));
|
|
3729
|
+
}
|
|
3730
|
+
//#endregion
|
|
3731
|
+
//#region src/build/core/flatten/stages/shapes.ts
|
|
3732
|
+
function shapesToPaths(doc) {
|
|
3733
|
+
doc.mapShapes((s) => asPath(s));
|
|
3734
|
+
}
|
|
3735
|
+
function expandShorthandCommands(doc) {
|
|
3736
|
+
doc.mapShapes((s) => s.tag === "path" ? explicitLinesExpandShorthand(s) : s);
|
|
3737
|
+
}
|
|
3738
|
+
function absoluteCoordinates(doc) {
|
|
3739
|
+
doc.mapShapes((s) => absoluteShape(s));
|
|
3740
|
+
}
|
|
3741
|
+
function roundFloats(doc, ndigits) {
|
|
3742
|
+
doc.forEachShape((s) => roundShapeFloats(s, ndigits));
|
|
3743
|
+
}
|
|
3744
|
+
function dropEmptySubpaths(doc) {
|
|
3745
|
+
doc.forEachShape((s) => removeEmptySubpaths(s, doc.ops));
|
|
3746
|
+
}
|
|
3747
|
+
function removeUnpaintedShapes(doc) {
|
|
3748
|
+
doc.syncTree();
|
|
3749
|
+
const remove = [];
|
|
3750
|
+
for (const [el, shapes] of doc.shapeEntries()) if (!mightPaint(shapes[0], doc.ops)) remove.push(el);
|
|
3751
|
+
for (const el of remove) el.detach();
|
|
3752
|
+
doc.invalidateShapes();
|
|
3753
|
+
}
|
|
3754
|
+
//#endregion
|
|
3755
|
+
//#region src/build/core/flatten/stages/styles.ts
|
|
3756
|
+
function applyStyles(el) {
|
|
3757
|
+
const style = el.attrib.get("style") ?? "";
|
|
3758
|
+
el.attrib.delete("style");
|
|
3759
|
+
const parsed = {};
|
|
3760
|
+
parseCssDeclarations(style, parsed);
|
|
3761
|
+
for (const [name, value] of Object.entries(parsed)) if (XML_ATTRIBUTE_NAME.test(name)) el.attrib.set(name, value);
|
|
3762
|
+
}
|
|
3763
|
+
function applyStyleAttributes(doc) {
|
|
3764
|
+
if (doc.hasShapes()) {
|
|
3765
|
+
doc.mapShapes((s) => applyStyleAttribute(s));
|
|
3766
|
+
doc.syncTree();
|
|
3767
|
+
}
|
|
3768
|
+
const styled = [doc.root, ...findAll(doc.root, (e) => e.attrib.has("style"))];
|
|
3769
|
+
for (const el of styled) applyStyles(el);
|
|
3770
|
+
}
|
|
3771
|
+
//#endregion
|
|
3772
|
+
//#region src/build/core/flatten/stages/validate.ts
|
|
3773
|
+
var PATH_ALLOWLIST = [
|
|
3774
|
+
/^\/svg\[0\]$/,
|
|
3775
|
+
/^\/svg\[0\]\/defs\[0\]$/,
|
|
3776
|
+
/^\/svg\[0\]\/defs\[0\]\/(linear|radial)Gradient\[\d+\](\/stop\[\d+\])?$/,
|
|
3777
|
+
/^\/svg\[0\](\/(path|g)\[\d+\])+$/
|
|
3778
|
+
];
|
|
3779
|
+
function validateFlattened(doc) {
|
|
3780
|
+
doc.syncTree();
|
|
3781
|
+
const errors = [];
|
|
3782
|
+
const badPaths = /* @__PURE__ */ new Set();
|
|
3783
|
+
const pathsRequired = /* @__PURE__ */ new Set(["/svg[0]", "/svg[0]/defs[0]"]);
|
|
3784
|
+
const ids = /* @__PURE__ */ new Map();
|
|
3785
|
+
for (const context of doc.breadthFirst()) {
|
|
3786
|
+
if ([...badPaths].some((bp) => context.path.startsWith(bp))) continue;
|
|
3787
|
+
if (!PATH_ALLOWLIST.some((pat) => pat.test(context.path))) {
|
|
3788
|
+
errors.push(`Unsupported element <${stripNs(context.element.tag)}> at ${context.path}`);
|
|
3789
|
+
badPaths.add(context.path);
|
|
3790
|
+
continue;
|
|
3791
|
+
}
|
|
3792
|
+
pathsRequired.delete(context.path);
|
|
3793
|
+
const elId = context.element.attrib.get("id");
|
|
3794
|
+
if (elId !== void 0) {
|
|
3795
|
+
if (ids.has(elId)) errors.push(`Duplicate id "${elId}" at ${context.path}, first seen at ${ids.get(elId)}`);
|
|
3796
|
+
ids.set(elId, context.path);
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
for (const path of pathsRequired) errors.push(`Missing element ${path}`);
|
|
3800
|
+
return errors;
|
|
3801
|
+
}
|
|
3802
|
+
//#endregion
|
|
3803
|
+
//#region src/build/core/flatten/stages.ts
|
|
3804
|
+
function flattenDocument(doc, ndigits = 3) {
|
|
3805
|
+
doc.syncTree();
|
|
3806
|
+
removeNonSvgContent(doc);
|
|
3807
|
+
removeAnonymousSymbols(doc);
|
|
3808
|
+
removeTitleMetaDesc(doc);
|
|
3809
|
+
applyStyleAttributes(doc);
|
|
3810
|
+
resolveNestedSvgs(doc);
|
|
3811
|
+
shapesToPaths(doc);
|
|
3812
|
+
expandShorthandCommands(doc);
|
|
3813
|
+
resolveUse(doc);
|
|
3814
|
+
collapse(doc);
|
|
3815
|
+
evenoddToNonzeroWinding(doc);
|
|
3816
|
+
normalizeOpacities(doc);
|
|
3817
|
+
absoluteCoordinates(doc);
|
|
3818
|
+
roundFloats(doc, ndigits);
|
|
3819
|
+
dropEmptySubpaths(doc);
|
|
3820
|
+
removeUnpaintedShapes(doc);
|
|
3821
|
+
const violations = validateFlattened(doc);
|
|
3822
|
+
if (violations.length) throw new Error(violations.join("; "));
|
|
3823
|
+
}
|
|
3824
|
+
//#endregion
|
|
3825
|
+
//#region src/build/core/flatten/index.ts
|
|
3826
|
+
function flattenSvg(svgContent, pathkit) {
|
|
3827
|
+
const doc = SvgDocument.parse(svgContent, createPathOps(pathkit));
|
|
3828
|
+
flattenDocument(doc);
|
|
3829
|
+
return doc.serialize();
|
|
3830
|
+
}
|
|
3831
|
+
//#endregion
|
|
3832
|
+
//#region src/build/core/glyph/merge.ts
|
|
3833
|
+
/**
|
|
3834
|
+
* Concatenate multiple SVG path `d` strings into a single compound path.
|
|
3835
|
+
* This preserves the exact geometry of each path (no boolean operations)
|
|
3836
|
+
* while combining them into one glyph. Under nonzero winding, this renders
|
|
3837
|
+
* identically to drawing each path separately with the same color.
|
|
3838
|
+
*/
|
|
3839
|
+
function concatPathDs(ds) {
|
|
3840
|
+
if (ds.length === 0) return null;
|
|
3841
|
+
if (ds.length === 1) return ds[0];
|
|
3842
|
+
return ds.join(" ");
|
|
3843
|
+
}
|
|
3844
|
+
/**
|
|
3845
|
+
* Merge consecutive same-color paths into single compound paths.
|
|
3846
|
+
* Preserves z-order: only merges runs of adjacent paths with identical fill color.
|
|
3847
|
+
*/
|
|
3848
|
+
function mergeSameColorPaths(paths, logger) {
|
|
3849
|
+
if (paths.length <= 1) return paths;
|
|
3850
|
+
const result = [];
|
|
3851
|
+
let i = 0;
|
|
3852
|
+
while (i < paths.length) {
|
|
3853
|
+
const fill = paths[i].fill;
|
|
3854
|
+
const knockout = paths[i].knockout;
|
|
3855
|
+
let j = i + 1;
|
|
3856
|
+
if (!paths[i].noMerge) while (j < paths.length && paths[j].fill === fill && paths[j].knockout === knockout && !paths[j].noMerge) j++;
|
|
3857
|
+
if (j - i === 1) result.push(paths[i]);
|
|
3858
|
+
else {
|
|
3859
|
+
const group = paths.slice(i, j);
|
|
3860
|
+
const merged = concatPathDs(group.map((p) => p.d));
|
|
3861
|
+
if (merged) {
|
|
3862
|
+
logger?.info(` ⊕ Merged ${group.length} same-color paths (fill=${fill})`);
|
|
3863
|
+
result.push(knockout ? {
|
|
3864
|
+
d: merged,
|
|
3865
|
+
fill,
|
|
3866
|
+
knockout
|
|
3867
|
+
} : {
|
|
3868
|
+
d: merged,
|
|
3869
|
+
fill
|
|
3870
|
+
});
|
|
3871
|
+
} else result.push(...group);
|
|
3872
|
+
}
|
|
3873
|
+
i = j;
|
|
3874
|
+
}
|
|
3875
|
+
return result;
|
|
3876
|
+
}
|
|
3877
|
+
//#endregion
|
|
3878
|
+
//#region src/build/core/glyph/validate.ts
|
|
3879
|
+
function validateSvg(content) {
|
|
3880
|
+
if (XML_MASK.test(content)) return {
|
|
3881
|
+
valid: false,
|
|
3882
|
+
reason: "<mask> is not supported yet"
|
|
3883
|
+
};
|
|
3884
|
+
if (XML_FILTER.test(content)) return {
|
|
3885
|
+
valid: false,
|
|
3886
|
+
reason: "<filter> is not supported yet"
|
|
3887
|
+
};
|
|
3888
|
+
if (XML_IMAGE.test(content)) return {
|
|
3889
|
+
valid: false,
|
|
3890
|
+
reason: "embedded raster <image> is not supported yet"
|
|
3891
|
+
};
|
|
3892
|
+
const unconvertible = XML_UNCONVERTIBLE.exec(content);
|
|
3893
|
+
if (unconvertible) return {
|
|
3894
|
+
valid: false,
|
|
3895
|
+
reason: `<${unconvertible[1]}> is not supported yet`
|
|
3896
|
+
};
|
|
3897
|
+
return { valid: true };
|
|
3898
|
+
}
|
|
3899
|
+
function stripPxUnits(content) {
|
|
3900
|
+
return content.replace(PX_ATTRIBUTE_VALUE, "=$1$2$1").replace(PX_STYLE_VALUE, "$1");
|
|
3901
|
+
}
|
|
3902
|
+
function preprocessSvg(content) {
|
|
3903
|
+
const withUnits = stripPxUnits(content);
|
|
3904
|
+
if (XML_XMLNS.test(withUnits)) return withUnits;
|
|
3905
|
+
return withUnits.replace(SVG_OPEN_TAG, `<svg xmlns="${SVG_NS}"`);
|
|
3906
|
+
}
|
|
3907
|
+
//#endregion
|
|
3908
|
+
//#region src/build/core/pathkit/evenodd.ts
|
|
3909
|
+
/**
|
|
3910
|
+
* Convert a path `d` string with evenodd fill semantics to an equivalent
|
|
3911
|
+
* path that renders identically under nonzero winding.
|
|
3912
|
+
*
|
|
3913
|
+
* Steps:
|
|
3914
|
+
* 1. Parse via PathKit, set fill type to EVENODD, simplify (resolve topology)
|
|
3915
|
+
* 2. Split into contours, compute containment depths
|
|
3916
|
+
* 3. Fix winding: even depth = CCW (outer), odd depth = CW (hole)
|
|
3917
|
+
* 4. Reconstruct d string
|
|
3918
|
+
*/
|
|
3919
|
+
function convertEvenoddToWinding(PathKit, d) {
|
|
3920
|
+
const V = verbMap(PathKit);
|
|
3921
|
+
const p = PathKit.FromSVGString(d);
|
|
3922
|
+
if (!p) return d;
|
|
3923
|
+
p.setFillType(fillTypes(PathKit).EVENODD);
|
|
3924
|
+
p.simplify();
|
|
3925
|
+
const simplified = p.toSVGString();
|
|
3926
|
+
p.delete?.();
|
|
3927
|
+
const p2 = PathKit.FromSVGString(simplified);
|
|
3928
|
+
if (!p2) return simplified;
|
|
3929
|
+
const cmds = p2.toCmds();
|
|
3930
|
+
p2.delete?.();
|
|
3931
|
+
if (cmds.length === 0) return simplified;
|
|
3932
|
+
return cmdsToPathData(orientedContours(cmds, V).flatMap((x) => x.cmds), V);
|
|
3933
|
+
}
|
|
3934
|
+
//#endregion
|
|
3935
|
+
//#region src/build/core/prepare.ts
|
|
3936
|
+
/**
|
|
3937
|
+
* Fill values that reference a paint server (`url(#…)`) cannot be expressed
|
|
3938
|
+
* as font glyph layers — a solid-color glyph has no gradient or pattern stop
|
|
3939
|
+
* representation. Reject rather than silently painting black.
|
|
3940
|
+
*/
|
|
3941
|
+
function assertNoPaintServers(paths, fileLabel) {
|
|
3942
|
+
for (const p of paths) if (p.fill && p.fill.includes("url(")) throw new Error(`${fileLabel} uses a paint-server fill ("${p.fill}"); gradients and patterns cannot be compiled to font glyphs`);
|
|
3943
|
+
}
|
|
3944
|
+
/**
|
|
3945
|
+
* Shared per-file SVG prep: validate → preprocess → flatten → parse →
|
|
3946
|
+
* evenodd convert → same-color merge. Returns null for unsupported SVGs
|
|
3947
|
+
* (logged as a warning); throws for inputs that cannot be converted.
|
|
3948
|
+
*/
|
|
3949
|
+
async function prepareSvgLayers(opts) {
|
|
3950
|
+
const { svg, fileLabel, pathkit, logger } = opts;
|
|
3951
|
+
const validation = validateSvg(svg);
|
|
3952
|
+
if (validation.valid === false) {
|
|
3953
|
+
logger?.warn(`${fileLabel} skipped: ${validation.reason}`);
|
|
3954
|
+
return null;
|
|
3955
|
+
}
|
|
3956
|
+
const preprocessed = preprocessSvg(svg);
|
|
3957
|
+
let flattenedSvg;
|
|
3958
|
+
try {
|
|
3959
|
+
flattenedSvg = flattenSvg(preprocessed, pathkit);
|
|
3960
|
+
} catch (err) {
|
|
3961
|
+
throw new Error(`${fileLabel} failed to flatten: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
3962
|
+
}
|
|
3963
|
+
const parsed = parseFlattenedSvg(flattenedSvg, {
|
|
3964
|
+
onSanitize: (original) => {
|
|
3965
|
+
logger?.info(` ⚠ ${fileLabel} sanitized path: path was missing initial moveto (prepended M from endpoint)`);
|
|
3966
|
+
logger?.info(` Original: ${original.slice(0, 80)}…`);
|
|
3967
|
+
},
|
|
3968
|
+
onMissingViewBox: (assumed) => {
|
|
3969
|
+
logger?.warn(`${fileLabel} has no viewBox; assuming [${assumed.join(" ")}]`);
|
|
3970
|
+
}
|
|
3971
|
+
});
|
|
3972
|
+
assertNoPaintServers(parsed.paths, fileLabel);
|
|
3973
|
+
for (const p of parsed.paths) if (p.fillRule === "evenodd") {
|
|
3974
|
+
logger?.info(` ↻ ${fileLabel} converting evenodd path to nonzero winding`);
|
|
3975
|
+
p.d = convertEvenoddToWinding(pathkit, p.d);
|
|
3976
|
+
delete p.fillRule;
|
|
3977
|
+
p.noMerge = true;
|
|
3978
|
+
}
|
|
3979
|
+
const mergedPaths = mergeSameColorPaths(parsed.paths, logger);
|
|
3980
|
+
return {
|
|
3981
|
+
viewBox: parsed.viewBox,
|
|
3982
|
+
paths: mergedPaths
|
|
3983
|
+
};
|
|
3984
|
+
}
|
|
3985
|
+
//#endregion
|
|
3986
|
+
//#region src/build/core/prepareIcon.ts
|
|
3987
|
+
function collectingLogger(logs) {
|
|
3988
|
+
const push = (level) => (msg) => {
|
|
3989
|
+
logs.push([level, msg]);
|
|
3990
|
+
};
|
|
3991
|
+
return {
|
|
3992
|
+
start: () => {},
|
|
3993
|
+
update: () => {},
|
|
3994
|
+
succeed: () => {},
|
|
3995
|
+
fail: push("fail"),
|
|
3996
|
+
info: push("info"),
|
|
3997
|
+
warn: push("warn")
|
|
3998
|
+
};
|
|
3999
|
+
}
|
|
4000
|
+
async function prepareIcon(task, pathkit) {
|
|
4001
|
+
const logs = [];
|
|
4002
|
+
const logger = collectingLogger(logs);
|
|
4003
|
+
const iconName = task.file.replace(/\.svg$/i, "");
|
|
4004
|
+
const fileLabel = `[${task.fontFamily}: ${task.file}]`;
|
|
4005
|
+
const result = {
|
|
4006
|
+
file: task.file,
|
|
4007
|
+
iconName,
|
|
4008
|
+
adv: 0,
|
|
4009
|
+
layers: [],
|
|
4010
|
+
logs,
|
|
4011
|
+
error: null
|
|
4012
|
+
};
|
|
4013
|
+
logger.info(`Processing ${task.file}`);
|
|
4014
|
+
let prepared;
|
|
4015
|
+
try {
|
|
4016
|
+
prepared = await prepareSvgLayers({
|
|
4017
|
+
svg: task.svg,
|
|
4018
|
+
fileLabel,
|
|
4019
|
+
pathkit,
|
|
4020
|
+
logger
|
|
4021
|
+
});
|
|
4022
|
+
} catch (err) {
|
|
4023
|
+
result.error = err instanceof Error ? err.message : String(err);
|
|
4024
|
+
return result;
|
|
4025
|
+
}
|
|
4026
|
+
if (!prepared) return result;
|
|
4027
|
+
const { vx, vy, scale, xOff, yOff, adv } = computePlacement({
|
|
4028
|
+
upm: task.upm,
|
|
4029
|
+
safeZone: task.safeZone,
|
|
4030
|
+
viewBox: prepared.viewBox
|
|
4031
|
+
});
|
|
4032
|
+
result.adv = adv;
|
|
4033
|
+
for (const p of prepared.paths) {
|
|
4034
|
+
if (shouldSkipPath(p.d, p.fill)) continue;
|
|
4035
|
+
const placed = transformPathForFont(pathkit, p.d, {
|
|
4036
|
+
vx,
|
|
4037
|
+
vy,
|
|
4038
|
+
scale,
|
|
4039
|
+
xOff,
|
|
4040
|
+
yOff,
|
|
4041
|
+
upm: task.upm
|
|
4042
|
+
});
|
|
4043
|
+
result.layers.push({
|
|
4044
|
+
fill: p.fill,
|
|
4045
|
+
d: toQuadraticPath(placed, task.upm * QUADRATIC_ERROR_BOUND_EM)
|
|
4046
|
+
});
|
|
4047
|
+
}
|
|
4048
|
+
if (result.layers.length === 0) logger.warn(`${fileLabel} produced no glyphs: nothing in it paints`);
|
|
4049
|
+
return result;
|
|
4050
|
+
}
|
|
4051
|
+
//#endregion
|
|
4052
|
+
//#region src/build/core/runFontPipeline.ts
|
|
4053
|
+
var SKIP_REASONS = /skipped:|produced no glyphs/;
|
|
4054
|
+
/**
|
|
4055
|
+
* Run the font pipeline: prepared SVG layers → placement → glyphs → TTF +
|
|
4056
|
+
* glyphmap (+ WOFF2 when `config.web`). The PathKit WASM instance is shared
|
|
4057
|
+
* across calls.
|
|
4058
|
+
*/
|
|
4059
|
+
async function runFontPipeline(config, paths, inputs, options) {
|
|
4060
|
+
const startTime = Date.now();
|
|
4061
|
+
const logger = options?.logger;
|
|
4062
|
+
logger?.update(`Building "${config.fontFamily}"…`);
|
|
4063
|
+
ensureDir(paths.outputDir);
|
|
4064
|
+
const inputHash = options?.inputHash;
|
|
4065
|
+
const family = inputHash ? buildFontFamily(config.fontFamily, inputHash) : config.fontFamily;
|
|
4066
|
+
const glyphMap = {
|
|
4067
|
+
m: {
|
|
4068
|
+
f: family,
|
|
4069
|
+
u: config.upm,
|
|
4070
|
+
z: config.safeZone,
|
|
4071
|
+
s: config.startUnicode,
|
|
4072
|
+
...config.web ? { w: true } : {}
|
|
4073
|
+
},
|
|
4074
|
+
i: {}
|
|
4075
|
+
};
|
|
4076
|
+
let currentUnicode = config.startUnicode;
|
|
4077
|
+
const codepointToIcon = /* @__PURE__ */ new Map();
|
|
4078
|
+
const allGlyphs = [];
|
|
4079
|
+
const failed = [];
|
|
4080
|
+
const skipped = [];
|
|
4081
|
+
const pathkit = await loadPathKit();
|
|
4082
|
+
for (const input of inputs) {
|
|
4083
|
+
const result = await prepareIcon({
|
|
4084
|
+
file: input.file,
|
|
4085
|
+
svg: input.svg,
|
|
4086
|
+
fontFamily: config.fontFamily,
|
|
4087
|
+
upm: config.upm,
|
|
4088
|
+
safeZone: config.safeZone
|
|
4089
|
+
}, pathkit);
|
|
4090
|
+
for (const [level, msg] of result.logs) logger?.[level](msg);
|
|
4091
|
+
if (result.error !== null) {
|
|
4092
|
+
logger?.fail(result.error);
|
|
4093
|
+
failed.push(result.file);
|
|
4094
|
+
continue;
|
|
4095
|
+
}
|
|
4096
|
+
if (result.layers.length === 0) {
|
|
4097
|
+
skipped.push({
|
|
4098
|
+
file: result.file,
|
|
4099
|
+
reason: result.logs.filter(([, msg]) => SKIP_REASONS.test(msg)).map(([, msg]) => msg).at(-1) ?? "no paintable layers"
|
|
4100
|
+
});
|
|
4101
|
+
continue;
|
|
4102
|
+
}
|
|
4103
|
+
const layers = [];
|
|
4104
|
+
for (const layer of result.layers) {
|
|
4105
|
+
const cp = currentUnicode++;
|
|
4106
|
+
codepointToIcon.set(cp, result.iconName);
|
|
4107
|
+
allGlyphs.push({
|
|
4108
|
+
codepoint: cp,
|
|
4109
|
+
advanceWidth: result.adv,
|
|
4110
|
+
d: layer.d
|
|
4111
|
+
});
|
|
4112
|
+
layers.push([cp, layer.fill || "black"]);
|
|
4113
|
+
}
|
|
4114
|
+
glyphMap.i[result.iconName] = [result.adv, layers];
|
|
4115
|
+
}
|
|
4116
|
+
if (failed.length) throw new Error(`${failed.length} of ${inputs.length} icons in [${config.fontFamily}] could not be converted: ${failed.join(", ")}`);
|
|
4117
|
+
logger?.info(`Compiling TTF…`);
|
|
4118
|
+
const ttfPath = path.join(paths.outputDir, `${family}.ttf`);
|
|
4119
|
+
let ttfBuffer;
|
|
4120
|
+
try {
|
|
4121
|
+
ttfBuffer = await compileTtfFromGlyphs({
|
|
4122
|
+
glyphs: allGlyphs,
|
|
4123
|
+
outTtfPath: ttfPath,
|
|
4124
|
+
fontName: family,
|
|
4125
|
+
upm: config.upm,
|
|
4126
|
+
ascent: config.upm,
|
|
4127
|
+
descent: 0
|
|
4128
|
+
});
|
|
4129
|
+
} catch (err) {
|
|
4130
|
+
parseCompileTtfFromGlyphsError(err, codepointToIcon);
|
|
4131
|
+
}
|
|
4132
|
+
let woff2Path;
|
|
4133
|
+
if (config.web && ttfBuffer) {
|
|
4134
|
+
logger?.info(`Compiling WOFF2…`);
|
|
4135
|
+
woff2Path = path.join(paths.outputDir, `${family}.woff2`);
|
|
4136
|
+
writeFileAtomic(woff2Path, await compileWoff2FromTtf(ttfBuffer));
|
|
4137
|
+
}
|
|
4138
|
+
const glyphmapPath = path.join(paths.outputDir, `${family}.glyphmap.json`);
|
|
4139
|
+
if (inputHash) glyphMap.m.h = inputHash;
|
|
4140
|
+
writeFileAtomic(glyphmapPath, JSON.stringify(glyphMap));
|
|
4141
|
+
const iconCount = Object.keys(glyphMap.i).length;
|
|
4142
|
+
const elapsed = Date.now() - startTime;
|
|
4143
|
+
const products = woff2Path ? `${family}.ttf + ${family}.woff2` : `${family}.ttf`;
|
|
4144
|
+
logger?.succeed(`Built ${products} [${iconCount} icon${iconCount === 1 ? "" : "s"} in ${elapsed}ms]`);
|
|
4145
|
+
return {
|
|
4146
|
+
ttfPath,
|
|
4147
|
+
glyphmapPath,
|
|
4148
|
+
family,
|
|
4149
|
+
woff2Path,
|
|
4150
|
+
glyphMap,
|
|
4151
|
+
icons: Object.keys(glyphMap.i),
|
|
4152
|
+
skipped
|
|
4153
|
+
};
|
|
4154
|
+
}
|
|
4155
|
+
//#endregion
|
|
4156
|
+
//#region src/build/index.ts
|
|
4157
|
+
var DEFAULT_UPM = 1024;
|
|
4158
|
+
var DEFAULT_SAFE_ZONE = 1020;
|
|
4159
|
+
var DEFAULT_START_UNICODE = 59648;
|
|
4160
|
+
function generatorVersion() {
|
|
4161
|
+
try {
|
|
4162
|
+
const packageJsonPath = createRequire(import.meta.url).resolve("@octane-xplat/nano-icons/package.json");
|
|
4163
|
+
return `@octane-xplat/nano-icons@${JSON.parse(fs$1.readFileSync(packageJsonPath, "utf8")).version ?? "unknown"}`;
|
|
4164
|
+
} catch {
|
|
4165
|
+
return "@octane-xplat/nano-icons@unknown";
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
function parseStartUnicode(value) {
|
|
4169
|
+
if (value === void 0) return DEFAULT_START_UNICODE;
|
|
4170
|
+
if (typeof value === "number") return value;
|
|
4171
|
+
const parsed = value.startsWith("0x") ? parseInt(value, 16) : parseInt(value, 10);
|
|
4172
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 1114111) throw new RangeError(`Invalid startUnicode "${value}"`);
|
|
4173
|
+
return parsed;
|
|
4174
|
+
}
|
|
4175
|
+
/** sha256 over sorted inputs + pipeline settings + generator version. */
|
|
4176
|
+
function fingerprintInputs(inputs, settings) {
|
|
4177
|
+
const hash = crypto.createHash("sha256");
|
|
4178
|
+
for (const input of [...inputs].sort((a, b) => a.file.localeCompare(b.file))) {
|
|
4179
|
+
hash.update(input.file);
|
|
4180
|
+
hash.update(input.svg);
|
|
4181
|
+
}
|
|
4182
|
+
hash.update(JSON.stringify(settings));
|
|
4183
|
+
return hash.digest("hex");
|
|
4184
|
+
}
|
|
4185
|
+
/**
|
|
4186
|
+
* Build a layered icon font. Returns the emitted artifacts; throws if any
|
|
4187
|
+
* icon fails to convert. Icons that pass validation but paint nothing (or
|
|
4188
|
+
* use features skipped by validation) are listed in `skipped`.
|
|
4189
|
+
*/
|
|
4190
|
+
async function buildNanoIconFont(options) {
|
|
4191
|
+
const fontFamily = options.fontFamily ?? (options.inputDir ? path.basename(path.resolve(options.inputDir)) : "nano-icons");
|
|
4192
|
+
const web = options.web ?? true;
|
|
4193
|
+
const config = {
|
|
4194
|
+
fontFamily,
|
|
4195
|
+
upm: options.upm ?? 1024,
|
|
4196
|
+
safeZone: options.safeZone ?? 1020,
|
|
4197
|
+
startUnicode: parseStartUnicode(options.startUnicode),
|
|
4198
|
+
web
|
|
4199
|
+
};
|
|
4200
|
+
const inputs = [];
|
|
4201
|
+
const namedIcons = /* @__PURE__ */ new Map();
|
|
4202
|
+
if (options.inputDir) {
|
|
4203
|
+
const dir = path.resolve(options.inputDir);
|
|
4204
|
+
const files = (await fs.readdir(dir)).filter((f) => f.toLowerCase().endsWith(".svg"));
|
|
4205
|
+
for (const file of files.sort()) inputs.push({
|
|
4206
|
+
file,
|
|
4207
|
+
svg: await fs.readFile(path.join(dir, file), "utf8")
|
|
4208
|
+
});
|
|
4209
|
+
}
|
|
4210
|
+
for (const collection of options.iconify ?? []) for (const input of iconifyCollectionToSvgs(collection)) inputs.push({
|
|
4211
|
+
file: input.file,
|
|
4212
|
+
svg: input.svg
|
|
4213
|
+
});
|
|
4214
|
+
for (const icon of options.icons ?? []) {
|
|
4215
|
+
if (namedIcons.has(icon.name)) throw new Error(`Duplicate icon name "${icon.name}"`);
|
|
4216
|
+
namedIcons.set(icon.name, icon.svg);
|
|
4217
|
+
inputs.push({
|
|
4218
|
+
file: `${icon.name}.svg`,
|
|
4219
|
+
svg: icon.svg
|
|
4220
|
+
});
|
|
4221
|
+
}
|
|
4222
|
+
if (inputs.length === 0) throw new Error("buildNanoIconFont needs at least one of inputDir, icons, or iconify");
|
|
4223
|
+
const inputHash = fingerprintInputs(inputs, [
|
|
4224
|
+
config.upm,
|
|
4225
|
+
config.safeZone,
|
|
4226
|
+
config.startUnicode,
|
|
4227
|
+
web,
|
|
4228
|
+
generatorVersion()
|
|
4229
|
+
]);
|
|
4230
|
+
const family = `${config.fontFamily}-${inputHash.slice(0, 8)}`;
|
|
4231
|
+
const outputDir = path.resolve(options.outputDir);
|
|
4232
|
+
const glyphmapPath = path.join(outputDir, `${family}.glyphmap.json`);
|
|
4233
|
+
const ttfPath = path.join(outputDir, `${family}.ttf`);
|
|
4234
|
+
const woff2Path = path.join(outputDir, `${family}.woff2`);
|
|
4235
|
+
if (fs$1.existsSync(glyphmapPath) && fs$1.existsSync(ttfPath)) {
|
|
4236
|
+
const existing = JSON.parse(fs$1.readFileSync(glyphmapPath, "utf8"));
|
|
4237
|
+
if (existing.m?.h === inputHash && existing.m.f === family) {
|
|
4238
|
+
options.logger?.info?.(`Font "${family}" is up to date — skipping`);
|
|
4239
|
+
const modulePath = options.module === false ? void 0 : emitNanoIconSetModule({
|
|
4240
|
+
outputDir,
|
|
4241
|
+
fontFamily,
|
|
4242
|
+
family,
|
|
4243
|
+
glyphMap: existing,
|
|
4244
|
+
web
|
|
4245
|
+
});
|
|
4246
|
+
return {
|
|
4247
|
+
fontFamily,
|
|
4248
|
+
family,
|
|
4249
|
+
ttfPath,
|
|
4250
|
+
woff2Path: web && fs$1.existsSync(woff2Path) ? woff2Path : void 0,
|
|
4251
|
+
glyphmapPath,
|
|
4252
|
+
modulePath,
|
|
4253
|
+
icons: Object.keys(existing.i),
|
|
4254
|
+
skipped: [],
|
|
4255
|
+
glyphMap: existing,
|
|
4256
|
+
unchanged: true
|
|
4257
|
+
};
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
const result = await runFontPipeline(config, { outputDir }, inputs, {
|
|
4261
|
+
logger: options.logger,
|
|
4262
|
+
inputHash
|
|
4263
|
+
});
|
|
4264
|
+
let modulePath;
|
|
4265
|
+
if (options.module !== false) modulePath = emitNanoIconSetModule({
|
|
4266
|
+
outputDir,
|
|
4267
|
+
fontFamily,
|
|
4268
|
+
family: result.family,
|
|
4269
|
+
glyphMap: result.glyphMap,
|
|
4270
|
+
web
|
|
4271
|
+
});
|
|
4272
|
+
if (options.appFontsDir) {
|
|
4273
|
+
ensureDir(options.appFontsDir);
|
|
4274
|
+
writeFileAtomic(path.join(options.appFontsDir, `${result.family}.ttf`), fs$1.readFileSync(result.ttfPath));
|
|
4275
|
+
}
|
|
4276
|
+
return {
|
|
4277
|
+
fontFamily,
|
|
4278
|
+
family: result.family,
|
|
4279
|
+
ttfPath: result.ttfPath,
|
|
4280
|
+
woff2Path: result.woff2Path,
|
|
4281
|
+
glyphmapPath: result.glyphmapPath,
|
|
4282
|
+
modulePath,
|
|
4283
|
+
icons: result.icons,
|
|
4284
|
+
skipped: result.skipped,
|
|
4285
|
+
glyphMap: result.glyphMap,
|
|
4286
|
+
unchanged: false
|
|
4287
|
+
};
|
|
4288
|
+
}
|
|
4289
|
+
function loadNanoIconsConfig(configRoot) {
|
|
4290
|
+
const configPath = path.join(configRoot, ".nanoicons.json");
|
|
4291
|
+
if (!fs$1.existsSync(configPath)) throw new Error(`No .nanoicons.json found at ${configRoot}. Create one with:\n{ "iconSets": [{ "inputDir": "assets/icons", "fontFamily": "MyIcons", "outputDir": "src/generated/icons" }] }`);
|
|
4292
|
+
return JSON.parse(fs$1.readFileSync(configPath, "utf8"));
|
|
4293
|
+
}
|
|
4294
|
+
/** Run every icon set in a config file; returns per-set results. */
|
|
4295
|
+
async function buildAllNanoIconFonts(config, configRoot, logger) {
|
|
4296
|
+
if (!config.iconSets?.length) throw new Error("Config must contain an \"iconSets\" array with at least one entry");
|
|
4297
|
+
const results = [];
|
|
4298
|
+
for (const set of config.iconSets) results.push(await buildNanoIconFont({
|
|
4299
|
+
...set,
|
|
4300
|
+
outputDir: path.resolve(configRoot, set.outputDir ?? "nano-icons"),
|
|
4301
|
+
inputDir: set.inputDir ? path.resolve(configRoot, set.inputDir) : void 0,
|
|
4302
|
+
appFontsDir: set.appFontsDir ? path.resolve(configRoot, set.appFontsDir) : void 0,
|
|
4303
|
+
logger
|
|
4304
|
+
}));
|
|
4305
|
+
return results;
|
|
4306
|
+
}
|
|
4307
|
+
//#endregion
|
|
4308
|
+
export { buildNanoIconFont as a, loadPathKit as c, buildAllNanoIconFonts as i, iconifyCollectionToSvgs as l, DEFAULT_START_UNICODE as n, loadNanoIconsConfig as o, DEFAULT_UPM as r, runFontPipeline as s, DEFAULT_SAFE_ZONE as t };
|