@weasel-js/svg 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { pathFromD, PATH_M, PATH_L, PATH_Z, boundsOfPath, resolveScreenLength, SCRIPT_METRICS, dwarn, getMarker, registerMarker, createInsertOp, PathBuilder, parseColor, rgbaToHex, resolveTextStyle, layoutRuns, resolveRuns, resolveAlign, PATH_C, PATH_Q, solid, fillToBoundsFrame, getPaintKind } from '@weasel-js/core';
1
+ import { pathFromD, PATH_M, PATH_L, PATH_Z, boundsOfPath, resolveScreenLength, SCRIPT_METRICS, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, createInsertOp, fillInPoseFrame, DEFAULT_SHAPE_FILL, pathInPoseFrame, DEFAULT_TEXT_STYLE, PathBuilder, parseColor, rgbaToHex, resolveTextStyle, layoutRuns, resolveRuns, resolveAlign, PATH_C, PATH_Q, resolveMarkerSize, getPaintKind } from '@weasel-js/core';
2
2
  import { identity, multiply, invert } from '@weasel-js/geom';
3
3
  import { tileGeometry } from '@weasel-js/core/patterns-builtin';
4
4
 
@@ -682,13 +682,18 @@ function serializePathD(path) {
682
682
  // src/gradients.ts
683
683
  var WEASEL_NS = "urn:weasel-js:svg";
684
684
  var WEASEL_NS_PREFIX = "wzl";
685
- function hasNativeSvgForm(paint) {
686
- const kind = paint.fill ?? "solid";
685
+ function nativeSvgKind(kind) {
687
686
  return kind === "solid" || kind === "linear-gradient" || kind === "radial-gradient" || kind === "pattern";
688
687
  }
688
+ function hasNativeSvgForm(paint) {
689
+ return nativeSvgKind(paint.fill ?? "solid");
690
+ }
689
691
  var INTERPOLATE_SPACES = /* @__PURE__ */ new Set(["rgb", "oklab", "oklch"]);
692
+ function nativeSvgSpace(space) {
693
+ return !space || space === "rgb";
694
+ }
690
695
  function interpolateAttr(space) {
691
- if (!space || space === "rgb") return "";
696
+ if (nativeSvgSpace(space)) return "";
692
697
  return ` ${WEASEL_NS_PREFIX}:interpolate="${space}"`;
693
698
  }
694
699
  function readInterpolate(el, elements) {
@@ -698,7 +703,7 @@ function readInterpolate(el, elements) {
698
703
  }
699
704
  function usesInterpolateAttr(paint) {
700
705
  const space = paint.interpolate;
701
- return space != null && space !== "rgb" && INTERPOLATE_SPACES.has(space);
706
+ return !nativeSvgSpace(space) && INTERPOLATE_SPACES.has(space);
702
707
  }
703
708
  function localName(tag) {
704
709
  const i = tag.indexOf(":");
@@ -773,8 +778,8 @@ function readStops(el, onWarn) {
773
778
  const colorAttr = ownProp(c, "stop-color") ?? "#000000";
774
779
  const parsed = parsePaintAttr(colorAttr);
775
780
  if (parsed && parsed.kind === "solid") {
776
- const opacityAttr = ownProp(c, "stop-opacity");
777
- const own = opacityAttr != null ? parseRatio(opacityAttr) : NaN;
781
+ const opacityAttr2 = ownProp(c, "stop-opacity");
782
+ const own = opacityAttr2 != null ? parseRatio(opacityAttr2) : NaN;
778
783
  const alpha = Number.isFinite(own) ? own : parsed.alpha;
779
784
  stops.push({ offset, color: alpha < 1 ? applyAlpha(parsed.color, alpha) : parsed.color });
780
785
  } else {
@@ -877,12 +882,13 @@ var PaintServerRegistry = class {
877
882
  byPaint = /* @__PURE__ */ new Map();
878
883
  order = [];
879
884
  counter = 0;
880
- // Marker keys referenced by any stroke, in first-use order. The `<defs>`
881
- // id is the marker key itself, not a minted counter id: `parseSvg` reads a
882
- // `marker-end="url(#id)"` fragment back as the marker key directly when the
883
- // registry knows it, so a synthetic id would come back as a stranger.
884
- markerKeys = [];
885
- markerKeySet = /* @__PURE__ */ new Set();
885
+ // Marker defs referenced by any stroke, by `<defs>` id in first-use order.
886
+ // An unsized reference's id is the marker key itself, not a minted counter
887
+ // id: `parseSvg` reads a `marker-end="url(#id)"` fragment back as the marker
888
+ // key directly when the registry knows it, so a synthetic id would come
889
+ // back as a stranger. A sized one gets a def of its own, stamped with its
890
+ // key and size so the parser can read the reference back whole.
891
+ markers = /* @__PURE__ */ new Map();
886
892
  // Clip paths, in first-use order. Identity, not value: two groups clipped by
887
893
  // equal outlines get a def each, which costs bytes and misleads nobody.
888
894
  clips = [];
@@ -910,19 +916,19 @@ var PaintServerRegistry = class {
910
916
  /** Whether any registered paint serializes outside SVG's own vocabulary, so
911
917
  * the root has to declare {@link WEASEL_NS}. */
912
918
  usesPrivateNamespace() {
913
- return this.order.some((paint) => !hasNativeSvgForm(paint) || usesInterpolateAttr(paint));
919
+ return this.order.some((paint) => !hasNativeSvgForm(paint) || usesInterpolateAttr(paint)) || [...this.markers.values()].some((m) => m.size !== void 0 || writesInset(getMarker(m.key)));
914
920
  }
915
- /** The `<defs>` id for a marker key — the key itself, minting nothing.
916
- * `undefined` when nothing is registered under `key`, so a caller emits no
917
- * attribute at all rather than a `url(#…)` pointing at a def that will
918
- * never be written. */
919
- markerId(key) {
921
+ /** The `<defs>` id for a marker reference — the key itself when it has no
922
+ * size, else the key suffixed with the size. `undefined` when nothing is
923
+ * registered under the key, so a caller emits no attribute at all rather
924
+ * than a `url(#…)` pointing at a def that will never be written. */
925
+ markerId(ref) {
926
+ const key = typeof ref === "string" ? ref : ref.key;
927
+ const size = typeof ref === "string" ? void 0 : ref.size;
920
928
  if (getMarker(key) === void 0) return void 0;
921
- if (!this.markerKeySet.has(key)) {
922
- this.markerKeySet.add(key);
923
- this.markerKeys.push(key);
924
- }
925
- return key;
929
+ const id = size === void 0 ? key : `${key}-s${typeof size === "number" ? trimNumber(size) : `${trimNumber(size.px)}px`}`;
930
+ if (!this.markers.has(id)) this.markers.set(id, size === void 0 ? { key } : { key, size });
931
+ return id;
926
932
  }
927
933
  /** Mint (or recall) the `<defs>` id for a clip outline. */
928
934
  clipId(path) {
@@ -936,17 +942,22 @@ var PaintServerRegistry = class {
936
942
  /** Emit `<defs>...</defs>` XML for every registered paint server, marker
937
943
  * and clip path. */
938
944
  toDefsXml(onWarn) {
939
- if (this.order.length === 0 && this.markerKeys.length === 0 && this.clips.length === 0) {
945
+ if (this.order.length === 0 && this.markers.size === 0 && this.clips.length === 0) {
940
946
  return "";
941
947
  }
942
948
  const parts = ["<defs>"];
943
949
  for (const paint of this.order) {
944
950
  parts.push(paintServerXml(this.byPaint.get(paint), paint, onWarn));
945
951
  }
946
- for (const key of this.markerKeys) {
952
+ for (const [id, { key, size }] of this.markers) {
947
953
  const entry = getMarker(key);
948
954
  if (!entry) continue;
949
- parts.push(entry.toSvg ? entry.toSvg(key, entry) : defaultMarkerXml(key, entry, onWarn));
955
+ if (entry.toSvg) {
956
+ if (size !== void 0) onWarn?.(`marker "${key}" writes its own <marker> def, which cannot carry a reference's size; written at its own size`);
957
+ parts.push(entry.toSvg(id, entry));
958
+ } else {
959
+ parts.push(defaultMarkerXml(id, entry, onWarn, key, size));
960
+ }
950
961
  }
951
962
  for (const path of this.clips) {
952
963
  parts.push(
@@ -957,13 +968,19 @@ var PaintServerRegistry = class {
957
968
  return parts.join("");
958
969
  }
959
970
  };
960
- function defaultMarkerXml(id, entry, onWarn) {
961
- const path = entry.path({ size: 1, stroke: { paint: { fill: "solid", color: "#000" } } });
971
+ function writesInset(entry) {
972
+ return entry !== void 0 && !entry.toSvg && (entry.inset ?? 0) !== 0;
973
+ }
974
+ function defaultMarkerXml(id, entry, onWarn, key, size) {
975
+ const unit = size === void 0 ? 1 : resolveMarkerSize({ key: key ?? id, size }, 1);
976
+ const path = entry.path({ size: unit, stroke: { paint: { fill: "solid", color: "#000" } } });
962
977
  const d = serializePathD(path);
963
978
  const fill = markerPaintAttrs("fill", entry.fill, id, onWarn);
964
- const outline = entry.outline ? ` ${markerPaintAttrs("stroke", entry.outline.paint, id, onWarn)} stroke-width="${trimNumber(entry.outline.width)}" stroke-linecap="round" stroke-linejoin="round"` : "";
979
+ const outline = entry.outline ? ` ${markerPaintAttrs("stroke", entry.outline.paint, id, onWarn)} stroke-width="${trimNumber(entry.outline.width * unit)}" stroke-linecap="round" stroke-linejoin="round"` : "";
965
980
  const orient = typeof entry.orient === "number" ? trimNumber(entry.orient * 180 / Math.PI) : "auto-start-reverse";
966
- return `<marker id="${id}" markerUnits="strokeWidth" markerWidth="8" markerHeight="8" refX="0" refY="0" orient="${orient}" overflow="visible"><path d="${d}" ${fill}${outline}/></marker>`;
981
+ const units = size === void 0 ? 'markerUnits="strokeWidth" markerWidth="8" markerHeight="8"' : `markerUnits="userSpaceOnUse" markerWidth="${trimNumber(8 * unit)}" markerHeight="${trimNumber(8 * unit)}" ${WEASEL_NS_PREFIX}:key="${key}" ${WEASEL_NS_PREFIX}:size="${typeof size === "number" ? trimNumber(size) : `${trimNumber(size.px)}px`}"`;
982
+ const inset = writesInset(entry) ? ` ${WEASEL_NS_PREFIX}:inset="${trimNumber(entry.inset)}"` : "";
983
+ return `<marker id="${id}" ${units}${inset} refX="0" refY="0" orient="${orient}" overflow="visible"><path d="${d}" ${fill}${outline}/></marker>`;
967
984
  }
968
985
  function markerPaintAttrs(attr, paint, id, onWarn) {
969
986
  if (paint === "none") return `${attr}="none"`;
@@ -1415,7 +1432,7 @@ function readPaint(style, attr, defaultColor, gradients, onWarn) {
1415
1432
  onWarn(`${attr} references unknown gradient #${parsed.id}`);
1416
1433
  return { kind: "solid", color: defaultColor };
1417
1434
  }
1418
- return { kind: "gradient", paint };
1435
+ return { kind: "gradient", paint: opacity != null ? { ...paint, opacity } : paint };
1419
1436
  }
1420
1437
  const out = { kind: "solid", color: parsed.color };
1421
1438
  const a = opacity ?? (parsed.alpha < 1 ? parsed.alpha : void 0);
@@ -1923,6 +1940,16 @@ function ingestMarkers(root, nodes, gradients, onWarn) {
1923
1940
  delete stroke[field];
1924
1941
  continue;
1925
1942
  }
1943
+ const sized = sizedMarkerRef(el);
1944
+ if (sized) {
1945
+ if (getMarker(sized.key) === void 0 && !minted.has(sized.key)) {
1946
+ const entry2 = markerEntryFrom(el, id, role, resolveMarkerSize(sized, 1), gradients, onWarn);
1947
+ minted.set(sized.key, entry2 && { ...entry2, id: sized.key });
1948
+ }
1949
+ if (minted.get(sized.key) === null) delete stroke[field];
1950
+ else stroke[field] = sized;
1951
+ continue;
1952
+ }
1926
1953
  const cacheKey = `${id}\0${role}\0${w}`;
1927
1954
  if (!minted.has(cacheKey)) minted.set(cacheKey, markerEntryFrom(el, id, role, w, gradients, onWarn));
1928
1955
  const entry = minted.get(cacheKey);
@@ -1945,6 +1972,22 @@ function ingestMarkers(root, nodes, gradients, onWarn) {
1945
1972
  for (const entry of minted.values()) if (entry) out.set(entry.id, entry);
1946
1973
  return [...out.values()];
1947
1974
  }
1975
+ function weaselAttr(el, name) {
1976
+ return el.getAttributeNS(WEASEL_NS, name) ?? el.getAttribute(`${WEASEL_NS_PREFIX}:${name}`);
1977
+ }
1978
+ function sizedMarkerRef(el) {
1979
+ const key = weaselAttr(el, "key");
1980
+ const raw = weaselAttr(el, "size")?.trim();
1981
+ if (!key || !raw) return void 0;
1982
+ const px = raw.endsWith("px");
1983
+ const n = parseFloat(px ? raw.slice(0, -2) : raw);
1984
+ if (!Number.isFinite(n) || n <= 0) return void 0;
1985
+ return { key, size: px ? { px: n } : n };
1986
+ }
1987
+ function markerInsetAttr(el) {
1988
+ const n = parseFloat(weaselAttr(el, "inset") ?? "");
1989
+ return Number.isFinite(n) && n > 0 ? n : 0;
1990
+ }
1948
1991
  function styleAt(el) {
1949
1992
  const chain = [];
1950
1993
  for (let e = el; e; e = e.parentElement) chain.unshift(e);
@@ -2066,6 +2109,7 @@ function markerEntryFrom(el, id, role, strokeWidth, gradients, onWarn) {
2066
2109
  pi += p.path.coords.length;
2067
2110
  }
2068
2111
  const fillRule = parts[0].path.fillRule ?? "nonzero";
2112
+ const inset = markerInsetAttr(el);
2069
2113
  const round = (v) => Math.round(v * 1e4) / 1e4 + 0;
2070
2114
  const hash = shortHash(JSON.stringify([
2071
2115
  Array.from(commands),
@@ -2073,11 +2117,13 @@ function markerEntryFrom(el, id, role, strokeWidth, gradients, onWarn) {
2073
2117
  fillRule,
2074
2118
  fill,
2075
2119
  outline,
2076
- typeof orient === "number" ? round(orient) : orient
2120
+ typeof orient === "number" ? round(orient) : orient,
2121
+ ...inset !== 0 ? [round(inset)] : []
2077
2122
  ]));
2078
2123
  const key = id.endsWith(`-${hash}`) ? id : `${id}-${hash}`;
2079
2124
  return {
2080
2125
  id: key,
2126
+ reads: [],
2081
2127
  path: ({ size }) => {
2082
2128
  const scaled = new Float32Array(coords.length);
2083
2129
  for (let i = 0; i < coords.length; i++) scaled[i] = coords[i] * size;
@@ -2086,7 +2132,7 @@ function markerEntryFrom(el, id, role, strokeWidth, gradients, onWarn) {
2086
2132
  fill,
2087
2133
  outline,
2088
2134
  orient,
2089
- inset: 0
2135
+ inset
2090
2136
  };
2091
2137
  }
2092
2138
  function serializeSvg(nodes, opts = {}) {
@@ -2198,16 +2244,10 @@ function registerPaintServers(nodes, registry) {
2198
2244
  function registerTextPaint(paint, registry) {
2199
2245
  if (paint && !("color" in paint)) registry.register(paint);
2200
2246
  }
2201
- function markerKeyOfRef(ref) {
2202
- if (typeof ref === "string") return ref;
2203
- if (ref && typeof ref === "object" && "key" in ref) return String(ref.key);
2204
- return void 0;
2205
- }
2206
2247
  function registerMarkers(stroke, registry) {
2207
2248
  if (!stroke) return;
2208
2249
  for (const ref of [stroke.markerStart, stroke.markerMid, stroke.markerEnd]) {
2209
- const key = markerKeyOfRef(ref);
2210
- if (key) registry.markerId(key);
2250
+ if (ref !== void 0) registry.markerId(ref);
2211
2251
  }
2212
2252
  }
2213
2253
  function nodeXml(node, registry, namespaces, warn) {
@@ -2296,14 +2336,16 @@ function pathXml(node, registry, namespaces, warn) {
2296
2336
  }
2297
2337
  function paintAttrs(paint, name, registry, includeOpacity = true) {
2298
2338
  if (paint.kind === "none") return [`${name}="none"`];
2299
- if (paint.kind === "solid") {
2300
- const out = [`${name}="${paint.color}"`];
2301
- if (includeOpacity && paint.opacity != null && paint.opacity !== 1) {
2302
- out.push(`${name}-opacity="${trimNumber(paint.opacity)}"`);
2303
- }
2304
- return out;
2305
- }
2306
- return [`${name}="${registry.ref(paint.paint)}"`];
2339
+ const out = [`${name}="${paint.kind === "solid" ? paint.color : registry.ref(paint.paint)}"`];
2340
+ if (includeOpacity) out.push(...opacityAttr(`${name}-opacity`, svgPaintOpacity(paint)));
2341
+ return out;
2342
+ }
2343
+ function svgPaintOpacity(paint) {
2344
+ if (paint.kind === "solid") return paint.opacity;
2345
+ return paint.kind === "gradient" ? paint.paint.opacity : void 0;
2346
+ }
2347
+ function opacityAttr(name, v) {
2348
+ return v != null && v !== 1 ? [`${name}="${trimNumber(v)}"`] : [];
2307
2349
  }
2308
2350
  function strokeWidthAttrs(width) {
2309
2351
  if (typeof width === "number") return [`stroke-width="${trimNumber(width)}"`];
@@ -2315,15 +2357,8 @@ function coreStrokeAttrs(stroke, registry, warn) {
2315
2357
  const width = stroke.width ?? 1;
2316
2358
  if (!((typeof width === "object" ? width.px : width) > 0)) return [];
2317
2359
  warnStrokeAlign(stroke.align, warn);
2318
- const attrs = [];
2319
- if ("color" in paint) {
2320
- attrs.push(`stroke="${paint.color}"`);
2321
- if (paint.opacity != null && paint.opacity !== 1) {
2322
- attrs.push(`stroke-opacity="${trimNumber(paint.opacity)}"`);
2323
- }
2324
- } else {
2325
- attrs.push(`stroke="${registry.ref(paint)}"`);
2326
- }
2360
+ const attrs = [`stroke="${"color" in paint ? paint.color : registry.ref(paint)}"`];
2361
+ attrs.push(...opacityAttr("stroke-opacity", paint.opacity));
2327
2362
  attrs.push(...strokeWidthAttrs(width));
2328
2363
  if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
2329
2364
  if (stroke.join) attrs.push(`stroke-linejoin="${stroke.join}"`);
@@ -2338,8 +2373,7 @@ function coreStrokeAttrs(stroke, registry, warn) {
2338
2373
  ]) {
2339
2374
  const ref = stroke[field];
2340
2375
  if (ref === void 0) continue;
2341
- const key = typeof ref === "string" ? ref : ref.key;
2342
- const id = registry.markerId(key);
2376
+ const id = registry.markerId(ref);
2343
2377
  if (id === void 0) continue;
2344
2378
  attrs.push(`${attr}="url(#${id})"`);
2345
2379
  }
@@ -2349,10 +2383,7 @@ function strokeAttrsFor(stroke, registry, warn) {
2349
2383
  warnStrokeAlign(stroke.align, warn);
2350
2384
  const attrs = paintAttrs(stroke.paint, "stroke", registry, false);
2351
2385
  attrs.push(...strokeWidthAttrs(stroke.width));
2352
- const opacity = stroke.opacity ?? (stroke.paint.kind === "solid" ? stroke.paint.opacity : void 0);
2353
- if (opacity != null && opacity !== 1) {
2354
- attrs.push(`stroke-opacity="${trimNumber(opacity)}"`);
2355
- }
2386
+ attrs.push(...opacityAttr("stroke-opacity", stroke.opacity ?? svgPaintOpacity(stroke.paint)));
2356
2387
  if (stroke.cap) {
2357
2388
  attrs.push(`stroke-linecap="${stroke.cap}"`);
2358
2389
  }
@@ -2441,6 +2472,7 @@ function textXml(node, registry, namespaces, warn) {
2441
2472
  }
2442
2473
  const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
2443
2474
  if (decoration) attrs.push(`text-decoration="${decoration}"`);
2475
+ if (style?.script) attrs.push(`baseline-shift="${style.script}"`);
2444
2476
  if (style?.textTransform && style.textTransform !== "none") {
2445
2477
  attrs.push(`style="text-transform:${style.textTransform}"`);
2446
2478
  }
@@ -2455,14 +2487,8 @@ function textXml(node, registry, namespaces, warn) {
2455
2487
  if (node.fill === null) {
2456
2488
  attrs.push('fill="none"');
2457
2489
  } else if (node.fill) {
2458
- if ("color" in node.fill) {
2459
- attrs.push(`fill="${node.fill.color}"`);
2460
- if (node.fill.opacity != null && node.fill.opacity !== 1) {
2461
- attrs.push(`fill-opacity="${trimNumber(node.fill.opacity)}"`);
2462
- }
2463
- } else {
2464
- attrs.push(`fill="${registry.ref(node.fill)}"`);
2465
- }
2490
+ attrs.push(`fill="${"color" in node.fill ? node.fill.color : registry.ref(node.fill)}"`);
2491
+ attrs.push(...opacityAttr("fill-opacity", node.fill.opacity));
2466
2492
  }
2467
2493
  for (const a of coreStrokeAttrs(node.stroke, registry, warn)) attrs.push(a);
2468
2494
  if (node.opacity != null && node.opacity !== 1) {
@@ -2549,17 +2575,31 @@ var svgIdCounter = 0;
2549
2575
  function freshSvgNodeId() {
2550
2576
  return `n${(svgIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
2551
2577
  }
2552
- function fillFromPaint(paint, box) {
2578
+ function fillDataFromSvg(paint, box) {
2553
2579
  if (!paint) return void 0;
2554
2580
  if (paint.kind === "none") return null;
2555
- if (paint.kind === "solid") return solid(paint.color);
2556
- const g = paint.paint;
2557
- const units = "units" in g ? g.units : void 0;
2558
- return units === "world" ? fillToBoundsFrame(g, box) : g;
2581
+ if (paint.kind === "solid") {
2582
+ return paint.opacity != null ? { ...solid(paint.color), opacity: paint.opacity } : solid(paint.color);
2583
+ }
2584
+ return fillInBoxFrame(paint.paint, box);
2585
+ }
2586
+ function fadeFill(fill, k) {
2587
+ return k === 1 ? fill : { ...fill, opacity: (fill.opacity ?? 1) * k };
2588
+ }
2589
+ function fadeStroke(stroke, k) {
2590
+ return k === 1 || !stroke.paint ? stroke : { ...stroke, paint: fadeFill(stroke.paint, k) };
2591
+ }
2592
+ function fadeRun(run, k) {
2593
+ if (k === 1 || !run.fill && !run.stroke) return run;
2594
+ return {
2595
+ ...run,
2596
+ ...run.fill ? { fill: fadeFill(run.fill, k) } : {},
2597
+ ...run.stroke ? { stroke: fadeStroke(run.stroke, k) } : {}
2598
+ };
2559
2599
  }
2560
2600
  function strokeDataFromSvg(stroke, box) {
2561
2601
  if (!stroke || stroke.paint.kind === "none") return void 0;
2562
- const paint = fillFromPaint(stroke.paint, box);
2602
+ const paint = fillDataFromSvg(stroke.paint, box);
2563
2603
  if (paint === void 0 || paint === null) return void 0;
2564
2604
  return {
2565
2605
  paint: stroke.opacity !== void 0 ? { ...paint, opacity: stroke.opacity } : paint,
@@ -2573,22 +2613,6 @@ function strokeDataFromSvg(stroke, box) {
2573
2613
  ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {}
2574
2614
  };
2575
2615
  }
2576
- function svgImageFromKit(image, pose) {
2577
- const node = {
2578
- kind: "image",
2579
- href: image.src,
2580
- x: pose.x,
2581
- y: pose.y,
2582
- width: pose.width,
2583
- height: pose.height
2584
- };
2585
- if (image.source) node.source = { ...image.source };
2586
- if (image.flipX) node.flipX = true;
2587
- if (image.flipY) node.flipY = true;
2588
- if (image.opacity != null) node.opacity = image.opacity;
2589
- if (pose.rotation) node.rotation = pose.rotation;
2590
- return node;
2591
- }
2592
2616
  function fillInBoxFrame(fill, box) {
2593
2617
  const units = "units" in fill ? fill.units : void 0;
2594
2618
  return units === "world" ? fillToBoundsFrame(fill, box) : fill;
@@ -2604,28 +2628,36 @@ function strokeInBoxFrame(stroke, box) {
2604
2628
  const paint = fillInBoxFrame(stroke.paint, box);
2605
2629
  return paint === stroke.paint ? stroke : { ...stroke, paint };
2606
2630
  }
2607
- function svgNodesToKitDrafts(nodes, nextId) {
2631
+ function svgNodesToKitDrafts(input, nextId, options = {}) {
2632
+ const nodes = isNodeList(input) ? input : input.nodes;
2633
+ if (!isNodeList(input)) {
2634
+ for (const m of input.markers ?? []) if (getMarker(m.id) === void 0) registerMarker(m);
2635
+ }
2608
2636
  const drafts = [];
2609
- const visit = (n, parentId) => {
2637
+ const pushLeaf = (source, parentId, pose, data) => {
2638
+ const id = nextId(source);
2639
+ const out = options.leaf ? options.leaf(id, { pose, data }, source) : data;
2640
+ if (out === null) return null;
2641
+ drafts.push({ kind: "leaf", id, parentId, pose, data: out });
2642
+ return pose;
2643
+ };
2644
+ const visit = (n, parentId, inherited) => {
2645
+ const k = inherited * (n.opacity ?? 1);
2610
2646
  if (n.kind === "group") {
2611
2647
  const draft = {
2612
2648
  kind: "container",
2613
- id: nextId(),
2649
+ id: nextId(n),
2614
2650
  parentId,
2615
2651
  pose: { x: 0, y: 0, width: 0, height: 0 }
2616
2652
  };
2617
2653
  drafts.push(draft);
2618
- let acc = null;
2619
- for (const c of n.children) {
2620
- const b2 = visit(c, draft.id);
2621
- if (b2) acc = acc ? unionRect(acc, b2) : b2;
2622
- }
2654
+ const acc = unionAABB(n.children.map((c) => visit(c, draft.id, k)));
2623
2655
  if (!acc) {
2624
2656
  drafts.splice(drafts.indexOf(draft), 1);
2625
2657
  return null;
2626
2658
  }
2627
2659
  draft.pose = acc;
2628
- return acc;
2660
+ return draft.pose;
2629
2661
  }
2630
2662
  if (n.kind === "text") {
2631
2663
  const pose2 = {
@@ -2641,65 +2673,48 @@ function svgNodesToKitDrafts(nodes, nextId) {
2641
2673
  width: pose2.width,
2642
2674
  height: pose2.height
2643
2675
  };
2644
- drafts.push({
2645
- kind: "leaf",
2646
- id: nextId(),
2647
- parentId,
2648
- pose: pose2,
2649
- data: {
2650
- text: n.text,
2651
- ...n.style ? { style: n.style } : {},
2652
- ...n.verticalAlign ? { verticalAlign: n.verticalAlign } : {},
2653
- ...n.runs ? { runs: n.runs.map((r) => runInBoxFrame(r, box)) } : {},
2654
- // `!== undefined`, not a truthiness test: `null` is the document
2655
- // saying `fill="none"`, and absent takes the painter's default.
2656
- ...n.fill !== void 0 ? { fill: n.fill === null ? null : fillInBoxFrame(n.fill, box) } : {},
2657
- ...n.stroke !== void 0 ? { stroke: strokeInBoxFrame(n.stroke, box) } : {}
2658
- }
2676
+ const fill2 = n.fill === void 0 && k !== 1 ? DEFAULT_TEXT_STYLE.fill : n.fill;
2677
+ return pushLeaf(n, parentId, pose2, {
2678
+ text: n.text,
2679
+ ...n.style ? { style: n.style } : {},
2680
+ ...n.verticalAlign ? { verticalAlign: n.verticalAlign } : {},
2681
+ ...n.runs ? { runs: n.runs.map((r) => fadeRun(runInBoxFrame(r, box), k)) } : {},
2682
+ // `!== undefined`, not a truthiness test: `null` is the document
2683
+ // saying `fill="none"`, and absent takes the painter's default.
2684
+ ...fill2 !== void 0 ? { fill: fill2 === null ? null : fadeFill(fillInBoxFrame(fill2, box), k) } : {},
2685
+ ...n.stroke !== void 0 ? { stroke: fadeStroke(strokeInBoxFrame(n.stroke, box), k) } : {}
2659
2686
  });
2660
- return pose2;
2661
2687
  }
2662
2688
  if (n.kind === "image") {
2663
2689
  const pose2 = { x: n.x, y: n.y, width: n.width, height: n.height };
2664
2690
  if (n.rotation) pose2.rotation = n.rotation;
2665
- drafts.push({
2666
- kind: "leaf",
2667
- id: nextId(),
2668
- parentId,
2669
- pose: pose2,
2670
- data: {
2671
- image: {
2672
- src: n.href,
2673
- ...n.opacity != null ? { opacity: n.opacity } : {},
2674
- ...n.source ? { source: { ...n.source } } : {},
2675
- ...n.flipX ? { flipX: true } : {},
2676
- ...n.flipY ? { flipY: true } : {}
2677
- }
2691
+ return pushLeaf(n, parentId, pose2, {
2692
+ image: {
2693
+ src: n.href,
2694
+ ...k !== 1 ? { opacity: k } : {},
2695
+ ...n.source ? { source: { ...n.source } } : {},
2696
+ ...n.flipX ? { flipX: true } : {},
2697
+ ...n.flipY ? { flipY: true } : {}
2678
2698
  }
2679
2699
  });
2680
- return pose2;
2681
2700
  }
2682
2701
  const b = n.path.kind === "rect" ? { x: n.path.x, y: n.path.y, width: n.path.width, height: n.path.height } : boundsOfPath(n.path);
2683
2702
  const pose = { x: b.x, y: b.y, width: b.width, height: b.height };
2684
2703
  if (n.rotation) pose.rotation = n.rotation;
2685
- const fill = fillFromPaint(n.fill, b);
2704
+ const fill = fillDataFromSvg(n.fill, b);
2686
2705
  const strokeFromSvg = strokeDataFromSvg(n.stroke, b);
2687
- drafts.push({
2688
- kind: "leaf",
2689
- id: nextId(),
2690
- parentId,
2691
- pose,
2692
- data: {
2693
- path: n.path,
2694
- ...fill !== void 0 ? { fill } : {},
2695
- ...strokeFromSvg !== void 0 ? { stroke: strokeFromSvg } : {}
2696
- }
2706
+ return pushLeaf(n, parentId, pose, {
2707
+ path: n.path,
2708
+ ...fill !== void 0 ? { fill: fill && fadeFill(fill, k) } : {},
2709
+ ...strokeFromSvg !== void 0 ? { stroke: fadeStroke(strokeFromSvg, k) } : {}
2697
2710
  });
2698
- return pose;
2699
2711
  };
2700
- for (const n of nodes) visit(n, null);
2712
+ for (const n of nodes) visit(n, null, 1);
2701
2713
  return drafts;
2702
2714
  }
2715
+ function isNodeList(input) {
2716
+ return Array.isArray(input);
2717
+ }
2703
2718
  function scaleTextData(data, scale) {
2704
2719
  if (scale === 1 || typeof data.text !== "string") return data;
2705
2720
  const style = data.style ?? {};
@@ -2713,13 +2728,6 @@ function scaleTextData(data, scale) {
2713
2728
  style: { ...style, fontSize: scaleSize(style.fontSize ?? resolveTextStyle(style).fontSize) }
2714
2729
  };
2715
2730
  }
2716
- function unionRect(a, b) {
2717
- const minX = Math.min(a.x, b.x);
2718
- const minY = Math.min(a.y, b.y);
2719
- const maxX = Math.max(a.x + a.width, b.x + b.width);
2720
- const maxY = Math.max(a.y + a.height, b.y + b.height);
2721
- return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
2722
- }
2723
2731
  async function unpackSvgFiles(files, ctx) {
2724
2732
  const layer = ctx.scene.layers[0]?.id ?? "default";
2725
2733
  let index = 0;
@@ -2728,8 +2736,7 @@ async function unpackSvgFiles(files, ctx) {
2728
2736
  const text = await readFileText(file);
2729
2737
  const parsed = parseSvg(text);
2730
2738
  for (const w of parsed.warnings) dwarn("ingest", `svg "${file.name}":`, w);
2731
- for (const m of parsed.markers ?? []) if (getMarker(m.id) === void 0) registerMarker(m);
2732
- let drafts = svgNodesToKitDrafts(parsed.nodes, freshSvgNodeId);
2739
+ let drafts = svgNodesToKitDrafts(parsed, freshSvgNodeId);
2733
2740
  if (drafts.length === 0) {
2734
2741
  console.warn(`weasel ingest: svg "${file.name}" parsed to no drawable nodes`);
2735
2742
  continue;
@@ -2737,13 +2744,13 @@ async function unpackSvgFiles(files, ctx) {
2737
2744
  const roots = drafts.filter((d) => d.parentId === null);
2738
2745
  if (roots.length > 1) {
2739
2746
  const wrapperId = freshSvgNodeId();
2740
- const union2 = roots.map((d) => d.pose).reduce(unionRect);
2747
+ const union2 = unionAABB(roots.map((d) => d.pose));
2741
2748
  drafts = [
2742
2749
  { kind: "container", id: wrapperId, parentId: null, pose: union2 },
2743
2750
  ...drafts.map((d) => d.parentId === null ? { ...d, parentId: wrapperId } : d)
2744
2751
  ];
2745
2752
  }
2746
- const union = drafts.filter((d) => d.parentId === null).map((d) => d.pose).reduce(unionRect);
2753
+ const union = unionAABB(drafts.filter((d) => d.parentId === null).map((d) => d.pose));
2747
2754
  const view = ctx.viewportWorldRect();
2748
2755
  const scale = Math.min(
2749
2756
  1,
@@ -2784,7 +2791,115 @@ async function unpackSvgFiles(files, ctx) {
2784
2791
  }
2785
2792
  }
2786
2793
  }
2794
+ function svgPaintFromKit(fill, box) {
2795
+ if (fill === null) return { kind: "none" };
2796
+ if (fill.fill === void 0 || fill.fill === "solid") {
2797
+ return { kind: "solid", color: fill.color, ...fill.opacity != null ? { opacity: fill.opacity } : {} };
2798
+ }
2799
+ return { kind: "gradient", paint: fillInPoseFrame(fill, box) };
2800
+ }
2801
+ function svgStrokeFromKit(stroke, box) {
2802
+ if (!stroke?.paint) return void 0;
2803
+ const width = stroke.width ?? 1;
2804
+ if (!((typeof width === "object" ? width.px : width) > 0)) return void 0;
2805
+ const opacity = stroke.paint.opacity;
2806
+ return {
2807
+ paint: svgPaintFromKit(stroke.paint, box),
2808
+ width,
2809
+ ...opacity != null ? { opacity } : {},
2810
+ ...stroke.cap !== void 0 ? { cap: stroke.cap } : {},
2811
+ ...stroke.join !== void 0 ? { join: stroke.join } : {},
2812
+ ...stroke.dash !== void 0 ? { dash: stroke.dash } : {},
2813
+ ...stroke.miterLimit !== void 0 ? { miterLimit: stroke.miterLimit } : {},
2814
+ ...stroke.align !== void 0 ? { align: stroke.align } : {},
2815
+ ...stroke.markerStart !== void 0 ? { markerStart: stroke.markerStart } : {},
2816
+ ...stroke.markerMid !== void 0 ? { markerMid: stroke.markerMid } : {},
2817
+ ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {}
2818
+ };
2819
+ }
2820
+ function svgImageFromKit(image, pose) {
2821
+ const node = {
2822
+ kind: "image",
2823
+ href: image.src,
2824
+ x: pose.x,
2825
+ y: pose.y,
2826
+ width: pose.width,
2827
+ height: pose.height
2828
+ };
2829
+ if (image.source) node.source = { ...image.source };
2830
+ if (image.flipX) node.flipX = true;
2831
+ if (image.flipY) node.flipY = true;
2832
+ if (image.opacity != null) node.opacity = image.opacity;
2833
+ if (pose.rotation) node.rotation = pose.rotation;
2834
+ return node;
2835
+ }
2836
+ function strokeInPoseFrame(stroke, box) {
2837
+ return stroke.paint ? { ...stroke, paint: fillInPoseFrame(stroke.paint, box) } : stroke;
2838
+ }
2839
+ function runInPoseFrame(run, box) {
2840
+ if (!run.fill && !run.stroke) return run;
2841
+ return {
2842
+ ...run,
2843
+ ...run.fill ? { fill: fillInPoseFrame(run.fill, box) } : {},
2844
+ ...run.stroke ? { stroke: strokeInPoseFrame(run.stroke, box) } : {}
2845
+ };
2846
+ }
2847
+ function svgLeafFromKit(data, pose) {
2848
+ if (data.image) return svgImageFromKit(data.image, pose);
2849
+ if (data.text != null) {
2850
+ const node2 = {
2851
+ kind: "text",
2852
+ x: pose.x,
2853
+ y: pose.y,
2854
+ width: pose.width,
2855
+ height: pose.height,
2856
+ text: data.text
2857
+ };
2858
+ if (data.style) node2.style = data.style;
2859
+ if (data.runs) node2.runs = data.runs.map((r) => runInPoseFrame(r, pose));
2860
+ if (data.verticalAlign) node2.verticalAlign = data.verticalAlign;
2861
+ if (data.fill !== void 0) node2.fill = data.fill && fillInPoseFrame(data.fill, pose);
2862
+ if (data.stroke) node2.stroke = strokeInPoseFrame(data.stroke, pose);
2863
+ if (pose.rotation) node2.rotation = pose.rotation;
2864
+ return node2;
2865
+ }
2866
+ if (!data.path) return null;
2867
+ const stroke = svgStrokeFromKit(data.stroke, pose);
2868
+ const fill = data.fill !== void 0 ? data.fill : stroke ? null : DEFAULT_SHAPE_FILL;
2869
+ const node = {
2870
+ kind: "path",
2871
+ path: pathInPoseFrame(data.path, pose),
2872
+ fill: svgPaintFromKit(fill, pose)
2873
+ };
2874
+ if (stroke) node.stroke = stroke;
2875
+ if (pose.rotation) node.rotation = pose.rotation;
2876
+ return node;
2877
+ }
2878
+ function svgNodesFromKit(tree, options = {}) {
2879
+ const { include, leaf, group } = options;
2880
+ const emit = (id) => {
2881
+ if (include && !include(id)) return null;
2882
+ const node = tree.get(id);
2883
+ if (!node) return null;
2884
+ if (node.kind === "container") {
2885
+ const children = [];
2886
+ for (const c of tree.childrenOf(id)) {
2887
+ const child = emit(c);
2888
+ if (child) children.push(child);
2889
+ }
2890
+ const g = { kind: "group", children };
2891
+ return group ? group(id, g) : g;
2892
+ }
2893
+ return leaf ? leaf(id, node) : svgLeafFromKit(node.data, node.pose);
2894
+ };
2895
+ const out = [];
2896
+ for (const id of options.roots ?? tree.roots) {
2897
+ const n = emit(id);
2898
+ if (n) out.push(n);
2899
+ }
2900
+ return out;
2901
+ }
2787
2902
 
2788
- export { IDENTITY_MATRIX, parseSvg, serializeSvg, strokeDataFromSvg, svgImageFromKit, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
2903
+ export { IDENTITY_MATRIX, fillDataFromSvg, nativeSvgKind, nativeSvgSpace, parseSvg, serializeSvg, strokeDataFromSvg, svgImageFromKit, svgLeafFromKit, svgNodesFromKit, svgNodesToKitDrafts, svgPaintFromKit, svgStrokeFromKit, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
2789
2904
  //# sourceMappingURL=index.js.map
2790
2905
  //# sourceMappingURL=index.js.map