@weasel-js/svg 1.12.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -105,13 +105,16 @@ drafts the kit's built-in path, text and image painters draw, and registers
105
105
  the document's markers. Containers carry no opacity, so an element or group
106
106
  `opacity` is multiplied into the paints of the leaves under it. `nextId` is
107
107
  handed the node each id is for, and `options.leaf` maps each leaf's kit data
108
- into data of your own shape, from the source node's metadata too.
108
+ into data of your own shape, from the source node's metadata too. `options.mark`
109
+ says which `<g>`s are marks with children, as the export's `mark` decorator
110
+ tagged them.
109
111
 
110
112
  `svgNodesFromKit(scene)` goes the other way: containers become groups, and
111
113
  each leaf is written the way its painter draws it, with the pose baked into
112
114
  the geometry and box-relative paints resolved into that box. Hand
113
115
  `serializeSvg` the result. Options pick the roots, skip nodes (a hidden
114
- layer), replace a leaf's lowering or decorate a group. The per-leaf pieces are
116
+ layer), replace a leaf's lowering, or decorate a group or a mark with children
117
+ so the import can tell the two apart. The per-leaf pieces are
115
118
  public too: `svgLeafFromKit`, `svgPaintFromKit`, `svgStrokeFromKit`,
116
119
  `svgImageFromKit`.
117
120
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Affine } from '@weasel-js/geom';
2
2
  export { pathToD as serializePathD } from '@weasel-js/geom';
3
- import { Path, Paint, ScreenLength, StrokeAlign, MarkerRef, VertexWidth, StrokeNib, StyledRun, TextStyle, TextVerticalAlign, Fill, Stroke, PoseLinear, MarkerEntry, TextPoseLayout, RenderNeeds, TilePatternSpec, Inherit, ImageNodeData, IngestCtx } from '@weasel-js/core';
3
+ import { Path, Paint, ScreenLength, StrokeAlign, MarkerRef, VertexWidth, StrokeNib, StyledRun, TextStyle, TextVerticalAlign, Fill, Stroke, PoseLinear, MarkerEntry, TextPoseLayout, RenderNeeds, TilePatternSpec, IngestCtx, Inherit, ImageNodeData, NodeClip } from '@weasel-js/core';
4
4
 
5
5
  /**
6
6
  * Media Queries 4 evaluation against a fixed environment.
@@ -542,6 +542,19 @@ interface SupportsOptions {
542
542
  */
543
543
  declare function evaluateSupports(condition: string, opts?: SupportsOptions): boolean;
544
544
 
545
+ /**
546
+ * Parse each file and insert its node tree, one undo step per file. Pass it
547
+ * as `<SceneCanvas ingestion={{ svg: { unpack: unpackSvgFiles } }}>`.
548
+ *
549
+ * Each file is fit-clamped to 90% of the visible viewport and centered on the
550
+ * drop point (or the viewport center), with later files cascading by a fixed
551
+ * offset. A multi-root file is wrapped in one container so it arrives as a
552
+ * single selectable unit. Content the file hides arrives hidden
553
+ * (`SceneNode.hidden`) rather than being left out. A file that fails to parse, or parses to nothing,
554
+ * is skipped with a `console.warn`.
555
+ */
556
+ declare function unpackSvgFiles(files: File[], ctx: IngestCtx): Promise<void>;
557
+
545
558
  /**
546
559
  * The kit-to-SVG direction: scene leaves back to `SvgNode`s, the inverse of
547
560
  * `svgNodesToKitDrafts`. Each leaf is written as the built-in painter that
@@ -613,8 +626,11 @@ interface SvgKitTreeNode {
613
626
  /** The kit's `SceneNode.hidden`: written with `display="none"`, which
614
627
  * `parseSvg(…, { keepHidden: true })` reads back as `hidden`. */
615
628
  hidden?: boolean;
616
- /** A container's copies, as the kit's `ContainerNode.copiesFromPose`
617
- * draws them: each is written as a transformed group of its children. */
629
+ /** The kit's `SceneNode.clip`: `'outline'` writes the node's children
630
+ * clipped to its outline, as core's `clipOutline` reads it. */
631
+ clip?: NodeClip;
632
+ /** The node's copies, as the kit's `SceneNode.copiesFromPose` draws them:
633
+ * each is written as a transformed group of its children. */
618
634
  copiesFromPose?(pose: SvgKitPose, node: never): readonly Matrix[] | null;
619
635
  }
620
636
  /** The read side of a scene that {@link svgNodesFromKit} walks. A kit
@@ -635,14 +651,24 @@ interface SvgNodesFromKitOptions<TId extends string = string> {
635
651
  * as these, and the walk marks the result as inheriting where SVG's
636
652
  * cascade can carry them. */
637
653
  leaf?(id: TId, node: SvgKitTreeNode, inherited: SvgKitInherited): SvgNode | null;
638
- /** Decorate the group written for a container, e.g. with `meta`. */
654
+ /** Decorate the `<g>` written for a scene group, e.g. with `meta`. */
639
655
  group?(id: TId, group: SvgGroupNode): SvgGroupNode;
656
+ /** Decorate the `<g>` written for a mark with children, so a reader can
657
+ * tell it from a group: `svgNodesToKitDrafts`'s `mark` option reads one
658
+ * back. Given, the `<g>` holds the mark's own element, then one `<g>` of its
659
+ * children carrying the mark's clip, then a transformed `<g>` of that for
660
+ * each copy; a mark whose children are all left out holds its own element
661
+ * alone. Not called for a mark whose own element is itself a group, or that
662
+ * draws nothing. */
663
+ mark?(id: TId, group: SvgGroupNode): SvgGroupNode;
640
664
  }
641
665
  /**
642
- * Walk a scene's container tree into `SvgNode`s ready for `serializeSvg`:
643
- * every `container` becomes a group of its children, in z-order, and every
644
- * leaf goes through {@link svgLeafFromKit} unless `options.leaf` says
645
- * otherwise. A hidden node is written hidden, with its subtree as it is.
666
+ * Walk a scene's tree into `SvgNode`s ready for `serializeSvg`: a group
667
+ * becomes a `<g>` of its children, in z-order, drawing nothing of its own; a
668
+ * mark is written by {@link svgLeafFromKit} unless `options.leaf` says
669
+ * otherwise, and a mark with children is a `<g>` holding that element and then
670
+ * its children, or one `<g>` of them when `options.mark` decorates it. A hidden
671
+ * node is written hidden, with its subtree as it is.
646
672
  */
647
673
  declare function svgNodesFromKit<TId extends string>(tree: SvgKitTree<TId>, options?: SvgNodesFromKitOptions<TId>): SvgNode[];
648
674
  /** What a leaf's `'inherit'` fill and stroke resolve to — its nearest
@@ -666,30 +692,45 @@ type SvgDraftPose = SvgDraftBounds & {
666
692
  transform?: PoseLinear;
667
693
  };
668
694
 
695
+ /**
696
+ * Parse an SVG file into **native scene nodes** — path/text leaves under
697
+ * containers that mirror the source `<g>` structure — instead of the default
698
+ * single embedded-image node.
699
+ *
700
+ * Leaf data targets the kit's built-in painters. Poses are absolute AABBs, and
701
+ * the renderer rebases stored geometry into the pose box, so placement and
702
+ * fit-clamping operate on poses alone and never rewrite path coordinates.
703
+ * Stroke widths, dashes, font sizes, and letter spacing live in data, so
704
+ * they take the clamp separately.
705
+ */
706
+
669
707
  /**
670
708
  * One node the unpack wants the scene to materialize, in parent-before-child
671
709
  * order (a draft's `parentId` always names an earlier draft, or `null` for
672
710
  * roots). `hidden` is the source node's, for the caller to map onto whatever
673
711
  * it hides a node with.
674
712
  *
675
- * A container's `data` is empty unless its `<g>` is clipped. A scene container
676
- * clips to what its painter draws, so a clipped one holds the clip as an
677
- * unpainted path, `{ path, fill: null }`, and its `pose` is the clip's box
678
- * rather than its children's.
713
+ * A group's `data` is empty unless its `<g>` is clipped. A clipped one declares
714
+ * `clip: 'outline'` and holds the clip as an unpainted path,
715
+ * `{ path, fill: null }`, which is the outline a group clips to; its `pose` is
716
+ * the clip's box rather than its children's. A mark declares a clip only when
717
+ * it is read from a `<g>` `options.mark` claims.
679
718
  */
680
719
  type SvgSceneDraft<TData = Record<string, unknown>> = {
681
- kind: 'container';
720
+ kind: 'group';
682
721
  id: string;
683
722
  parentId: string | null;
684
723
  pose: SvgDraftPose;
685
724
  data: SvgKitLeafData;
725
+ clip?: NodeClip;
686
726
  hidden?: true;
687
727
  } | {
688
- kind: 'leaf';
728
+ kind: 'mark';
689
729
  id: string;
690
730
  parentId: string | null;
691
731
  pose: SvgDraftPose;
692
732
  data: TData;
733
+ clip?: NodeClip;
693
734
  hidden?: true;
694
735
  };
695
736
  /** A leaf `SvgNode`: everything but a group. */
@@ -707,6 +748,14 @@ interface SvgNodesToKitDraftsOptions<TData> {
707
748
  * whose metadata means something with no children under it. It is drafted
708
749
  * with its clip's box, or a zero box at the origin. */
709
750
  keepEmpty?(source: SvgGroupNode): boolean;
751
+ /** Whether a `<g>` is a mark with children, as `svgNodesFromKit`'s `mark`
752
+ * option writes one: its first element is drafted as the mark, the
753
+ * children of the `<g>` after it as the mark's, hidden where that `<g>` is,
754
+ * with its clip as the mark's `clip: 'outline'`, and anything after that,
755
+ * such as baked copies, as the mark's further children. A clip on the
756
+ * claimed `<g>` itself clips the mark as a group around it. A `<g>` whose
757
+ * first element is a group, or whose second is not, is a group. */
758
+ mark?(source: SvgGroupNode): boolean;
710
759
  }
711
760
  /** Lower an `SvgPaint` onto the paint of the `kit:path` painter's
712
761
  * `data.fill` — a `Paint`, or `null` for SVG's `fill="none"` (the painter
@@ -736,17 +785,5 @@ declare function strokeDataFromSvg(stroke: SvgStroke | undefined, box: SvgDraftB
736
785
  * data; a group whose leaves it all leaves out is dropped like an empty one.
737
786
  */
738
787
  declare function svgNodesToKitDrafts<TData = Record<string, unknown>>(input: ParseResult | readonly SvgNode[], nextId: (source: SvgNode) => string, options?: SvgNodesToKitDraftsOptions<TData>): SvgSceneDraft<TData>[];
739
- /**
740
- * Parse each file and insert its node tree, one undo step per file. Pass it
741
- * as `<SceneCanvas ingestion={{ svg: { unpack: unpackSvgFiles } }}>`.
742
- *
743
- * Each file is fit-clamped to 90% of the visible viewport and centered on the
744
- * drop point (or the viewport center), with later files cascading by a fixed
745
- * offset. A multi-root file is wrapped in one container so it arrives as a
746
- * single selectable unit. Content the file hides arrives hidden
747
- * (`SceneNode.hidden`) rather than being left out. A file that fails to parse, or parses to nothing,
748
- * is skipped with a `console.warn`.
749
- */
750
- declare function unpackSvgFiles(files: File[], ctx: IngestCtx): Promise<void>;
751
788
 
752
789
  export { DEFAULT_MEDIA_ENVIRONMENT, IDENTITY_MATRIX, type Matrix, type NamespaceMeta, type NamespacedElement, type ParseOptions, type ParseResult, type SerializeOptions, type Specificity, type SupportsOptions, type SvgDraftBounds, type SvgDraftPose, type SvgGroupNode, type SvgImageNode, type SvgKitLeafData, type SvgKitPose, type SvgKitTree, type SvgKitTreeNode, type SvgLeafNode, type SvgMediaEnvironment, type SvgNode, type SvgNodesFromKitOptions, type SvgNodesToKitDraftsOptions, type SvgPaint, type SvgPathNode, type SvgSceneDraft, type SvgStroke, type SvgTextNode, compareSpecificity, escapeAttr, escapeText, evaluateMediaQuery, evaluateSupports, fillDataFromSvg, nativeSvgKind, nativeSvgSpace, parseSvg, serializeSvg, specificity, splitTopLevel, strokeDataFromSvg, svgImageFromKit, svgLeafFromKit, svgNeeds, svgNodesFromKit, svgNodesToKitDrafts, svgPaintFromKit, svgStrokeFromKit, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles, warmSvg };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { identity, multiply, pathToD, invert, splitByClosure, fillsOpenSubpaths, rotateAboutPoint, hasOpenSubpath, splitSubpaths } from '@weasel-js/geom';
2
2
  export { pathToD as serializePathD } from '@weasel-js/geom';
3
- import { isTileKind, boundsOfPath, transformPath, isSolidPaint, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, createInsertOp, fillInPoseFrame, resolveStroke, pathThroughPoseFrame, resolveFill, pathInPoseFrame, poseLinearOf, poseFrameOf, INHERIT, DEFAULT_TEXT_FILL, resolveScreenLength, pathFromD, isPatternPaint, resolveTextStyle, strokeToPath, toRuns, resolveRunFace, parseColor, rgbaToHex, layoutRuns, resolveRuns, resolveAlign, PathBuilder, PATH_M, PATH_L, PATH_Z, scaleVertexWidth, getPaintKind, resolveMarkerSize, paragraphDirection, scriptMetricsFor, vertexHalfWidths, isTilePaint, tileSpecOf } from '@weasel-js/core';
3
+ import { isTileKind, boundsOfPath, transformPath, isSolidPaint, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, createInsertOp, fillInPoseFrame, resolveStroke, pathThroughPoseFrame, resolveFill, pathInPoseFrame, poseLinearOf, poseFrameOf, INHERIT, clipOutline, resolveScreenLength, pathFromD, isPatternPaint, resolveTextStyle, strokeToPath, toRuns, resolveRunFace, DEFAULT_TEXT_FILL, parseColor, rgbaToHex, layoutRuns, resolveRuns, resolveAlign, PathBuilder, PATH_M, PATH_L, PATH_Z, scaleVertexWidth, getPaintKind, resolveMarkerSize, paragraphDirection, scriptMetricsFor, vertexHalfWidths, isTilePaint, tileSpecOf } from '@weasel-js/core';
4
4
  import { tileGeometry } from '@weasel-js/core/patterns-builtin';
5
5
 
6
6
  // src/types.ts
@@ -3971,16 +3971,6 @@ function groupTakes(group, takes, k) {
3971
3971
  const stroke = group.inherits?.stroke ? takes.stroke : solidOrNone(group.stroke?.paint);
3972
3972
  return { ...fill ? { fill: k } : {}, ...stroke ? { stroke: k } : {} };
3973
3973
  }
3974
- var CASCADE_OFFSET_PX = 24;
3975
- var VIEWPORT_FIT = 0.9;
3976
- function readFileText(file) {
3977
- return new Promise((resolve, reject) => {
3978
- const reader = new FileReader();
3979
- reader.onload = () => resolve(String(reader.result ?? ""));
3980
- reader.onerror = () => reject(reader.error);
3981
- reader.readAsText(file);
3982
- });
3983
- }
3984
3974
  function fillDataFromSvg(paint, box) {
3985
3975
  if (!paint) return void 0;
3986
3976
  if (paint.kind === "none") return null;
@@ -4032,19 +4022,25 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
4032
4022
  const id = nextId(source);
4033
4023
  const out = options.leaf ? options.leaf(id, { pose, data }, source) : data;
4034
4024
  if (out === null) return null;
4035
- drafts.push({ kind: "leaf", id, parentId, pose, data: out, ...source.hidden ? { hidden: true } : {} });
4025
+ drafts.push({ kind: "mark", id, parentId, pose, data: out, ...source.hidden ? { hidden: true } : {} });
4036
4026
  return pose;
4037
4027
  };
4038
4028
  const visit = (n, parentId, inherited2, from) => {
4039
4029
  const k = inherited2 * (n.opacity ?? 1);
4040
4030
  const takes = takesOf(n, from, k);
4041
4031
  if (n.kind === "group") {
4032
+ const [own2, kids, ...more] = n.children;
4033
+ if (own2 && own2.kind !== "group" && (kids === void 0 || kids.kind === "group") && options.mark?.(n)) {
4034
+ if (n.clip) return visit(clippingGroup(n), parentId, inherited2, from);
4035
+ return visitMark(n, own2, kids, more, parentId, k, takes);
4036
+ }
4042
4037
  const draft = {
4043
- kind: "container",
4038
+ kind: "group",
4044
4039
  id: nextId(n),
4045
4040
  parentId,
4046
4041
  pose: { x: 0, y: 0, width: 0, height: 0 },
4047
4042
  data: n.clip ? { path: n.clip, fill: null } : {},
4043
+ ...n.clip ? { clip: "outline" } : {},
4048
4044
  ...n.hidden ? { hidden: true } : {}
4049
4045
  };
4050
4046
  drafts.push(draft);
@@ -4060,60 +4056,95 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
4060
4056
  Object.assign(draft.data, containerPaint(n, takes, draft.pose, k));
4061
4057
  return draft.pose;
4062
4058
  }
4063
- if (n.kind === "text") {
4064
- const pose2 = {
4065
- x: n.x,
4066
- y: n.y,
4067
- width: n.width,
4068
- height: n.height
4069
- };
4070
- if (n.rotation) pose2.rotation = n.rotation;
4071
- if (n.transform) pose2.transform = n.transform;
4072
- const box = {
4073
- x: pose2.x,
4074
- y: pose2.y,
4075
- width: pose2.width,
4076
- height: pose2.height
4077
- };
4078
- const fill2 = n.fill === void 0 ? DEFAULT_TEXT_FILL : n.fill;
4079
- return pushLeaf(n, parentId, pose2, {
4080
- text: n.text,
4081
- ...n.style ? { style: n.style } : {},
4082
- ...n.verticalAlign ? { verticalAlign: n.verticalAlign } : {},
4083
- ...n.runs ? { runs: n.runs.map((r) => fadeRun(runInBoxFrame(r, box), k)) } : {},
4084
- // `null` is the document saying `fill="none"`.
4085
- ...takes.fill ? { fill: INHERIT } : { fill: fill2 && mapFillPaint(fill2, (p) => fadeFill(fillInBoxFrame(p, box), k)) },
4086
- ...takes.stroke ? { stroke: INHERIT } : n.stroke !== void 0 ? { stroke: fadeStroke(strokeInBoxFrame(n.stroke, box), k) } : {}
4087
- });
4088
- }
4089
- if (n.kind === "image") {
4090
- const pose2 = { x: n.x, y: n.y, width: n.width, height: n.height };
4091
- if (n.rotation) pose2.rotation = n.rotation;
4092
- if (n.transform) pose2.transform = n.transform;
4093
- return pushLeaf(n, parentId, pose2, {
4094
- image: {
4095
- src: n.href,
4096
- ...k !== 1 ? { opacity: k } : {},
4097
- ...n.source ? { source: { ...n.source } } : {},
4098
- ...n.flipX ? { flipX: true } : {},
4099
- ...n.flipY ? { flipY: true } : {}
4100
- }
4101
- });
4059
+ const { pose, data } = leafDraft(n, takes, k);
4060
+ return pushLeaf(n, parentId, pose, data);
4061
+ };
4062
+ const visitMark = (n, own2, kids, more, parentId, k, takes) => {
4063
+ const inner = groupTakes(n, takes, k);
4064
+ const pose = visit(own2, parentId, k, inner);
4065
+ const mark = pose === null ? null : drafts.at(-1);
4066
+ const under = mark?.id ?? parentId;
4067
+ if (mark && n.hidden) mark.hidden = true;
4068
+ if (mark && kids?.clip) mark.clip = "outline";
4069
+ const poses = [pose];
4070
+ if (kids) {
4071
+ const at = drafts.length;
4072
+ const kk = k * (kids.opacity ?? 1);
4073
+ const below = groupTakes(kids, takesOf(kids, inner, kk), kk);
4074
+ for (const c of kids.children) poses.push(visit(c, under, kk, below));
4075
+ if (kids.hidden) {
4076
+ for (const d of drafts.slice(at)) if (d.parentId === under) d.hidden = true;
4077
+ }
4102
4078
  }
4103
- const b = boxOfPath(n.path);
4104
- const pose = { x: b.x, y: b.y, width: b.width, height: b.height };
4105
- if (n.rotation) pose.rotation = n.rotation;
4106
- const fill = fillDataFromSvg(n.fill, b);
4107
- const strokeFromSvg = strokeDataFromSvg(n.stroke, b);
4108
- return pushLeaf(n, parentId, pose, {
4109
- path: n.path,
4110
- ...takes.fill ? { fill: INHERIT } : fill !== void 0 ? { fill: fill && { paint: fadeFill(fill, k) } } : {},
4111
- ...takes.stroke ? { stroke: INHERIT } : strokeFromSvg !== void 0 ? { stroke: fadeStroke(strokeFromSvg, k) } : {}
4112
- });
4079
+ for (const c of more) poses.push(visit(c, under, k, inner));
4080
+ return unionAABB(poses);
4113
4081
  };
4114
4082
  for (const n of nodes) visit(n, null, 1, NO_TAKES);
4115
4083
  return drafts;
4116
4084
  }
4085
+ function leafDraft(n, takes, k) {
4086
+ if (n.kind === "text") {
4087
+ const pose2 = {
4088
+ x: n.x,
4089
+ y: n.y,
4090
+ width: n.width,
4091
+ height: n.height
4092
+ };
4093
+ if (n.rotation) pose2.rotation = n.rotation;
4094
+ if (n.transform) pose2.transform = n.transform;
4095
+ const box = {
4096
+ x: pose2.x,
4097
+ y: pose2.y,
4098
+ width: pose2.width,
4099
+ height: pose2.height
4100
+ };
4101
+ const fill2 = n.fill === void 0 ? DEFAULT_TEXT_FILL : n.fill;
4102
+ return { pose: pose2, data: {
4103
+ text: n.text,
4104
+ ...n.style ? { style: n.style } : {},
4105
+ ...n.verticalAlign ? { verticalAlign: n.verticalAlign } : {},
4106
+ ...n.runs ? { runs: n.runs.map((r) => fadeRun(runInBoxFrame(r, box), k)) } : {},
4107
+ // `null` is the document saying `fill="none"`.
4108
+ ...takes.fill ? { fill: INHERIT } : { fill: fill2 && mapFillPaint(fill2, (p) => fadeFill(fillInBoxFrame(p, box), k)) },
4109
+ ...takes.stroke ? { stroke: INHERIT } : n.stroke !== void 0 ? { stroke: fadeStroke(strokeInBoxFrame(n.stroke, box), k) } : {}
4110
+ } };
4111
+ }
4112
+ if (n.kind === "image") {
4113
+ const pose2 = { x: n.x, y: n.y, width: n.width, height: n.height };
4114
+ if (n.rotation) pose2.rotation = n.rotation;
4115
+ if (n.transform) pose2.transform = n.transform;
4116
+ return { pose: pose2, data: {
4117
+ image: {
4118
+ src: n.href,
4119
+ ...k !== 1 ? { opacity: k } : {},
4120
+ ...n.source ? { source: { ...n.source } } : {},
4121
+ ...n.flipX ? { flipX: true } : {},
4122
+ ...n.flipY ? { flipY: true } : {}
4123
+ }
4124
+ } };
4125
+ }
4126
+ const b = boxOfPath(n.path);
4127
+ const pose = { x: b.x, y: b.y, width: b.width, height: b.height };
4128
+ if (n.rotation) pose.rotation = n.rotation;
4129
+ const fill = fillDataFromSvg(n.fill, b);
4130
+ const strokeFromSvg = strokeDataFromSvg(n.stroke, b);
4131
+ return { pose, data: {
4132
+ path: n.path,
4133
+ ...takes.fill ? { fill: INHERIT } : fill !== void 0 ? { fill: fill && { paint: fadeFill(fill, k) } } : {},
4134
+ ...takes.stroke ? { stroke: INHERIT } : strokeFromSvg !== void 0 ? { stroke: fadeStroke(strokeFromSvg, k) } : {}
4135
+ } };
4136
+ }
4137
+ function clippingGroup(n) {
4138
+ const { clip, ...unclipped } = n;
4139
+ return {
4140
+ kind: "group",
4141
+ clip,
4142
+ children: [unclipped],
4143
+ ...n.fill ? { fill: n.fill } : {},
4144
+ ...n.stroke ? { stroke: n.stroke } : {},
4145
+ ...n.inherits ? { inherits: n.inherits } : {}
4146
+ };
4147
+ }
4117
4148
  function containerPaint(n, takes, box, k) {
4118
4149
  const fill = fillDataFromSvg(n.fill, box);
4119
4150
  const stroke = strokeDataFromSvg(n.stroke, box);
@@ -4129,6 +4160,16 @@ function boxOfPath(path) {
4129
4160
  function isNodeList(input) {
4130
4161
  return Array.isArray(input);
4131
4162
  }
4163
+ var CASCADE_OFFSET_PX = 24;
4164
+ var VIEWPORT_FIT = 0.9;
4165
+ function readFileText(file) {
4166
+ return new Promise((resolve, reject) => {
4167
+ const reader = new FileReader();
4168
+ reader.onload = () => resolve(String(reader.result ?? ""));
4169
+ reader.onerror = () => reject(reader.error);
4170
+ reader.readAsText(file);
4171
+ });
4172
+ }
4132
4173
  function scaleLeafData(data, scale) {
4133
4174
  if (scale === 1) return data;
4134
4175
  const out = { ...data };
@@ -4157,7 +4198,7 @@ async function unpackSvgFiles(files, ctx) {
4157
4198
  const wrapperId = ctx.scene.mintId();
4158
4199
  const union2 = unionAABB(roots.map((d) => d.pose));
4159
4200
  drafts = [
4160
- { kind: "container", id: wrapperId, parentId: null, pose: union2, data: {} },
4201
+ { kind: "group", id: wrapperId, parentId: null, pose: union2, data: {} },
4161
4202
  ...drafts.map((d) => d.parentId === null ? { ...d, parentId: wrapperId } : d)
4162
4203
  ];
4163
4204
  }
@@ -4189,8 +4230,9 @@ async function unpackSvgFiles(files, ctx) {
4189
4230
  kind: d.kind,
4190
4231
  layer,
4191
4232
  pose: place(d.pose),
4192
- data: d.kind === "leaf" ? scaleLeafData(d.data, scale) : d.data,
4233
+ data: d.kind === "mark" ? scaleLeafData(d.data, scale) : d.data,
4193
4234
  parent: d.parentId,
4235
+ ...d.clip ? { clip: d.clip } : {},
4194
4236
  ...d.hidden ? { hidden: true } : {}
4195
4237
  },
4196
4238
  label: "Insert SVG"
@@ -4308,42 +4350,51 @@ function svgTextFromKit(data, pose) {
4308
4350
  return node;
4309
4351
  }
4310
4352
  function svgNodesFromKit(tree, options = {}) {
4311
- const { include, leaf, group } = options;
4353
+ const { include, leaf, group, mark } = options;
4312
4354
  const emit = (id, up) => {
4313
4355
  const node = tree.get(id);
4314
4356
  const out2 = node && (!include || include(id)) ? lower(id, node, up) : null;
4315
4357
  return out2 && node?.hidden === true ? { ...out2, hidden: true } : out2;
4316
4358
  };
4317
4359
  const lower = (id, node, up) => {
4318
- if (node.kind === "container") {
4319
- const own2 = node.data;
4320
- const below = {
4321
- fill: own2?.fill === INHERIT ? up.fill : resolveFill(own2?.fill),
4322
- stroke: own2?.stroke === INHERIT ? up.stroke : resolveStroke(own2?.stroke)
4323
- };
4324
- const emitChildren = () => {
4325
- const children = [];
4326
- for (const c of tree.childrenOf(id)) {
4327
- const child = emit(c, below);
4328
- if (child) children.push(child);
4329
- }
4330
- return children;
4331
- };
4332
- const path = node.data?.path;
4333
- const clip = path ? pathThroughPoseFrame(pathInPoseFrame(path, node.pose), node.pose) : void 0;
4334
- const copies = node.copiesFromPose?.(node.pose, node) ?? [];
4335
- const content = () => ({ kind: "group", children: emitChildren(), ...clip ? { clip } : {} });
4336
- let g = { ...content(), ...groupPaintFromKit(own2, below, node.pose) };
4337
- if (copies.length > 0) {
4338
- g = {
4339
- kind: "group",
4340
- children: [g, ...copies.map((transform) => ({ kind: "group", transform, children: [content()] }))]
4341
- };
4360
+ const own2 = node.data;
4361
+ const isGroup = node.kind === "group";
4362
+ const self = isGroup ? null : markInherits(
4363
+ leaf ? leaf(id, node, up) : svgLeafFromKit(resolvedData(own2 ?? {}, up), node.pose),
4364
+ own2 ?? {},
4365
+ up
4366
+ );
4367
+ const kids = tree.childrenOf(id);
4368
+ if (!isGroup && kids.length === 0) return self;
4369
+ const below = {
4370
+ fill: own2?.fill === INHERIT ? up.fill : resolveFill(own2?.fill),
4371
+ stroke: own2?.stroke === INHERIT ? up.stroke : resolveStroke(own2?.stroke)
4372
+ };
4373
+ const emitChildren = () => {
4374
+ const children = [];
4375
+ for (const c of kids) {
4376
+ const child = emit(c, below);
4377
+ if (child) children.push(child);
4342
4378
  }
4379
+ return children;
4380
+ };
4381
+ const clip = node.clip === "outline" ? clipOutline(node, node.pose) ?? void 0 : void 0;
4382
+ const copies = node.copiesFromPose?.(node.pose, node) ?? [];
4383
+ const content = () => ({ kind: "group", children: emitChildren(), ...clip ? { clip } : {} });
4384
+ const paint = groupPaintFromKit(own2, below, node.pose);
4385
+ const copied = () => copies.map((transform) => ({ kind: "group", transform, children: [content()] }));
4386
+ const withCopies = (g) => copies.length === 0 ? g : { kind: "group", children: [g, ...copied()] };
4387
+ if (isGroup) {
4388
+ const g = withCopies({ ...content(), ...paint });
4343
4389
  return group ? group(id, g) : g;
4344
4390
  }
4345
- const data = node.data ?? {};
4346
- return markInherits(leaf ? leaf(id, node, up) : svgLeafFromKit(resolvedData(data, up), node.pose), data, up);
4391
+ if (mark && self && self.kind !== "group") {
4392
+ const kids2 = content();
4393
+ return mark(id, { kind: "group", children: kids2.children.length === 0 ? [self] : [self, kids2, ...copied()], ...paint });
4394
+ }
4395
+ const inner = withCopies(content());
4396
+ const parts = clip || copies.length > 0 ? [inner] : inner.children;
4397
+ return { kind: "group", children: self ? [self, ...parts] : parts, ...paint };
4347
4398
  };
4348
4399
  const out = [];
4349
4400
  for (const id of options.roots ?? tree.roots) {