@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/README.md CHANGED
@@ -83,6 +83,53 @@ gradient — it just blends the stops in sRGB, which moves the midpoint rather
83
83
  than losing the paint. A space this package does not know is dropped on import
84
84
  rather than carried through.
85
85
 
86
+ ## Scene nodes, both ways
87
+
88
+ `svgNodesToKitDrafts(parseSvg(text), nextId)` lowers a document to scene-node
89
+ drafts the kit's built-in path, text and image painters draw, and registers
90
+ the document's markers. Containers carry no opacity, so an element or group
91
+ `opacity` is multiplied into the paints of the leaves under it. `nextId` is
92
+ handed the node each id is for, and `options.leaf` maps each leaf's kit data
93
+ into data of your own shape, from the source node's metadata too.
94
+
95
+ `svgNodesFromKit(scene)` goes the other way: containers become groups, and
96
+ each leaf is written the way its painter draws it, with the pose baked into
97
+ the geometry and box-relative paints resolved into that box. Hand
98
+ `serializeSvg` the result. Options pick the roots, skip nodes (a hidden
99
+ layer), replace a leaf's lowering or decorate a group. The per-leaf pieces are
100
+ public too: `svgLeafFromKit`, `svgPaintFromKit`, `svgStrokeFromKit`,
101
+ `svgImageFromKit`.
102
+
103
+ ## Stroke markers
104
+
105
+ A marker reference goes out as `marker-start` / `-mid` / `-end="url(#id)"` with
106
+ one `<marker>` def per distinct reference. The path keeps its full-length `d`:
107
+ baking the kit's inset in would have a re-import trim the line a second time.
108
+ The cost is that other renderers draw the line under a hollow head.
109
+
110
+ | Reference | Def id | What the def carries |
111
+ |---|---|---|
112
+ | `'arrow'` | `arrow` | `markerUnits="strokeWidth"` |
113
+ | `{ key: 'arrow', size: 3 }` | `arrow-s3` | `markerUnits="userSpaceOnUse"`, `wzl:key="arrow"`, `wzl:size="3"` |
114
+ | `{ key: 'arrow', size: { px: 6 } }` | `arrow-s6px` | the same, drawn at 6 user units, `wzl:size="6px"` |
115
+
116
+ Any entry with a nonzero inset adds `wzl:inset` in marker units, and the root
117
+ declares the `wzl` namespace whenever one of these attributes appears. `orient`
118
+ is `auto-start-reverse`, because the kit turns every start head around, or an
119
+ entry's fixed angle in degrees. An entry with its own `toSvg` writes its own
120
+ def and gets none of the `wzl` attributes (a sized reference to one warns). A
121
+ reference to a key nothing registered writes no attribute.
122
+
123
+ On import, `url(#id)` naming a registered key reads back as that key, so an
124
+ unsized built-in needs no def. A def with `wzl:key` and `wzl:size` reads back
125
+ as that sized reference, registering the def's geometry under the key if
126
+ nothing else has. Any other `<marker>` becomes an entry in
127
+ `ParseResult.markers`, keyed by its id plus a hash of what it draws, and
128
+ `svgNodesToKitDrafts` registers it. It is minted per reference rather than per
129
+ def, because a `userSpaceOnUse` size and an `orient="auto"` start both depend on
130
+ the referencing stroke. `wzl:inset` restores the inset; without it the inset is
131
+ 0. A reference to a marker neither registered nor defined warns and is dropped.
132
+
86
133
  ## Raster images
87
134
 
88
135
  `<image>` parses to an `SvgImageNode` holding the `href` verbatim — an
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Mat3 } from '@weasel-js/geom';
2
- import { Path, FillStyle, ScreenLength, StrokeAlign, StyledRun, TextStyle, TextVerticalAlign, Stroke, MarkerEntry, TilePatternSpec, ImageNodeData, IngestCtx } from '@weasel-js/core';
2
+ import { Path, FillStyle, ScreenLength, StrokeAlign, MarkerRef, StyledRun, TextStyle, TextVerticalAlign, Stroke, MarkerEntry, TilePatternSpec, ImageNodeData, IngestCtx } from '@weasel-js/core';
3
3
 
4
4
  /**
5
5
  * Public types for `@weasel-js/svg`. The package exposes a flat,
@@ -99,10 +99,12 @@ interface SvgStroke {
99
99
  * writes the stroke centered and says so through `onWarn`; the parser
100
100
  * never sets it. */
101
101
  align?: StrokeAlign;
102
- /** `marker-start` / `marker-mid` / `marker-end`, as the bare `url(#id)` key. */
103
- markerStart?: string;
104
- markerMid?: string;
105
- markerEnd?: string;
102
+ /** `marker-start` / `marker-mid` / `marker-end` as the kit's `MarkerRef`: the
103
+ * bare `url(#id)` key, or a key with the `size` the serializer writes into a
104
+ * def of its own. */
105
+ markerStart?: MarkerRef;
106
+ markerMid?: MarkerRef;
107
+ markerEnd?: MarkerRef;
106
108
  }
107
109
  /**
108
110
  * Leaf node: a path geometry plus fill/stroke. All other v1 shapes
@@ -390,6 +392,104 @@ declare function tilePreviewSvg(spec: TilePatternSpec, background?: string): str
390
392
  /** `tilePreviewSvg` packed as a `url(...)` value for CSS `background-image`. */
391
393
  declare function tilePreviewCssUrl(spec: TilePatternSpec, background?: string): string;
392
394
 
395
+ /**
396
+ * Map between SVG `<linearGradient>` / `<radialGradient>` elements and
397
+ * weasel's `FillStyle` gradient variants. Definitions are indexed by id
398
+ * wherever in the document they appear, and looked up when a `fill` /
399
+ * `stroke` attribute references one as `url(#id)`. On serialize, we
400
+ * emit a fresh `<defs>` block with stable generated ids.
401
+ */
402
+
403
+ /** `true` when SVG has a paint server for this kind, so the reference needs no
404
+ * fallback color and the root needs no foreign namespace. */
405
+ /** Whether SVG has its own paint server for paint kind `kind`: solids, the
406
+ * linear and radial gradients, and patterns. Any other kind is written in
407
+ * weasel's namespace with a fallback color, which other renderers paint flat. */
408
+ declare function nativeSvgKind(kind: string): boolean;
409
+ /** Whether a gradient interpolating through `space` blends the same in every
410
+ * SVG renderer — only sRGB, the default. A perceptual space still exports,
411
+ * but renderers other than weasel blend it in sRGB. */
412
+ declare function nativeSvgSpace(space: string | null | undefined): boolean;
413
+
414
+ /**
415
+ * The kit-to-SVG direction: scene leaves back to `SvgNode`s, the inverse of
416
+ * `svgNodesToKitDrafts`. Each leaf is written as the built-in painter that
417
+ * draws it would draw it — the pose baked into the geometry, box-relative
418
+ * paints resolved into that box — so `serializeSvg` over the result shows what
419
+ * the canvas shows.
420
+ */
421
+
422
+ /** A leaf's box: where it is drawn, and the frame its box-relative paints
423
+ * resolve against. */
424
+ interface SvgKitPose {
425
+ x: number;
426
+ y: number;
427
+ width: number;
428
+ height: number;
429
+ rotation?: number;
430
+ }
431
+ /** The leaf data the kit's path, text and image painters read — what
432
+ * `svgNodesToKitDrafts` writes. */
433
+ interface SvgKitLeafData {
434
+ path?: Path;
435
+ fill?: FillStyle | null;
436
+ stroke?: Stroke | null;
437
+ text?: string;
438
+ style?: TextStyle;
439
+ runs?: readonly StyledRun[];
440
+ verticalAlign?: TextVerticalAlign;
441
+ image?: ImageNodeData['image'];
442
+ }
443
+ /** A kit paint as an `SvgPaint`. `null` is `fill="none"`; a box-relative
444
+ * gradient or pattern is resolved into `box`, since the geometry it paints
445
+ * is written already placed there. */
446
+ declare function svgPaintFromKit(fill: FillStyle | null, box: SvgKitPose): SvgPaint;
447
+ /** A kit `Stroke` as an `SvgStroke` — the inverse of `strokeDataFromSvg`.
448
+ * One with no paint or no width draws nothing and is written as no stroke. */
449
+ declare function svgStrokeFromKit(stroke: Stroke | null | undefined, box: SvgKitPose): SvgStroke | undefined;
450
+ /** Write a `kit:image` leaf as an `SvgImageNode` — the inverse of the image
451
+ * arm of `svgNodesToKitDrafts`. The pose is the image's box. */
452
+ declare function svgImageFromKit(image: ImageNodeData['image'], pose: SvgKitPose): SvgImageNode;
453
+ /**
454
+ * Write one leaf as the `SvgNode` its built-in painter draws — image, text or
455
+ * path, in the order the painters claim a node. `null` for data none of them
456
+ * draws.
457
+ *
458
+ * A path with no `fill` takes `kit:path`'s fallback: the default shape fill
459
+ * when it has no stroke, and no fill when it has one.
460
+ */
461
+ declare function svgLeafFromKit(data: SvgKitLeafData, pose: SvgKitPose): SvgPathNode | SvgTextNode | SvgImageNode | null;
462
+ /** One node as {@link svgNodesFromKit} reads it. */
463
+ interface SvgKitTreeNode {
464
+ kind: string;
465
+ data: unknown;
466
+ pose: SvgKitPose;
467
+ }
468
+ /** The read side of a scene that {@link svgNodesFromKit} walks. A kit
469
+ * `Scene` is one as it stands. */
470
+ interface SvgKitTree<TId extends string = string> {
471
+ readonly roots: readonly TId[];
472
+ childrenOf(id: TId): readonly TId[];
473
+ get(id: TId): SvgKitTreeNode | undefined;
474
+ }
475
+ interface SvgNodesFromKitOptions<TId extends string = string> {
476
+ /** Walk these ids, in this order, instead of the tree's roots. */
477
+ roots?: readonly TId[];
478
+ /** `false` leaves a node out, with everything under it — a hidden layer. */
479
+ include?(id: TId): boolean;
480
+ /** Lower a leaf yourself; `null` leaves it out. Defaults to {@link svgLeafFromKit}. */
481
+ leaf?(id: TId, node: SvgKitTreeNode): SvgNode | null;
482
+ /** Decorate the group written for a container, e.g. with `meta`. */
483
+ group?(id: TId, group: SvgGroupNode): SvgGroupNode;
484
+ }
485
+ /**
486
+ * Walk a scene's container tree into `SvgNode`s ready for `serializeSvg`:
487
+ * every `container` becomes a group of its children, in z-order, and every
488
+ * leaf goes through {@link svgLeafFromKit} unless `options.leaf` says
489
+ * otherwise.
490
+ */
491
+ declare function svgNodesFromKit<TId extends string>(tree: SvgKitTree<TId>, options?: SvgNodesFromKitOptions<TId>): SvgNode[];
492
+
393
493
  /** Axis-aligned box, in the coordinate space of the SVG being unpacked. Used
394
494
  * for the bounds a draft occupies before it becomes a scene node. */
395
495
  interface SvgDraftBounds {
@@ -406,7 +506,7 @@ type DraftPose = SvgDraftBounds & {
406
506
  * order (a draft's `parentId` always names an earlier draft, or `null` for
407
507
  * roots). Leaf `data` is kit-painter-native (see module doc).
408
508
  */
409
- type SvgSceneDraft = {
509
+ type SvgSceneDraft<TData = Record<string, unknown>> = {
410
510
  kind: 'container';
411
511
  id: string;
412
512
  parentId: string | null;
@@ -416,8 +516,29 @@ type SvgSceneDraft = {
416
516
  id: string;
417
517
  parentId: string | null;
418
518
  pose: DraftPose;
419
- data: Record<string, unknown>;
519
+ data: TData;
420
520
  };
521
+ /** A leaf `SvgNode`: everything but a group. */
522
+ type SvgLeafNode = Exclude<SvgNode, SvgGroupNode>;
523
+ interface SvgNodesToKitDraftsOptions<TData> {
524
+ /** Lower a leaf yourself, from the kit data the default writes for it and
525
+ * the node it came from — for data shaped other than the kit painters', or
526
+ * metadata the document carries. `null` leaves it out. */
527
+ leaf?(id: string, draft: {
528
+ pose: DraftPose;
529
+ data: SvgKitLeafData;
530
+ }, source: SvgLeafNode): TData | null;
531
+ }
532
+ /** Lower an `SvgPaint` onto the `kit:path` painter's `data.fill` — a
533
+ * `FillStyle`, or `null` for SVG's `fill="none"` (the painter skips the
534
+ * fill rather than falling back to its default). A solid paint keeps its
535
+ * `fill-opacity`.
536
+ *
537
+ * A gradient rides through as the `FillStyle` it already is, normalized to
538
+ * the leaf's own box: `objectBoundingBox` gradients already are, and a
539
+ * `userSpaceOnUse` one is rebased so it survives the fit-clamp and the
540
+ * drop-point placement that move the geometry out from under it. */
541
+ declare function fillDataFromSvg(paint: SvgPaint | undefined, box: SvgDraftBounds): FillStyle | null | undefined;
421
542
  /** Lower an `SvgStroke` onto the leaf's `data.stroke`.
422
543
  *
423
544
  * Everything the SVG carried — paint, width, cap, join, dash, miter limit,
@@ -426,16 +547,21 @@ type SvgSceneDraft = {
426
547
  * `userSpaceOnUse` gradient survives the fit-clamp and the drop-point
427
548
  * placement. */
428
549
  declare function strokeDataFromSvg(stroke: SvgStroke | undefined, box: SvgDraftBounds): Stroke | undefined;
429
- /** Write a `kit:image` leaf as an `SvgImageNode` — the inverse of the image
430
- * arm of {@link svgNodesToKitDrafts}. The pose is the image's box. */
431
- declare function svgImageFromKit(image: ImageNodeData['image'], pose: DraftPose): SvgImageNode;
432
550
  /**
433
551
  * Walk an `SvgNode[]` tree and emit a flat, parent-before-child list of
434
552
  * {@link SvgSceneDraft}s. Each `<g>` becomes a container whose pose is the
435
553
  * union AABB of its descendants (the kit `group` action's convention);
436
554
  * path/text leaves carry kit-painter-native data. Empty groups are dropped.
555
+ *
556
+ * Handed a whole `ParseResult`, it also registers the document's markers
557
+ * (`parsed.markers`), without which the strokes naming them draw bare.
558
+ * Containers carry no opacity, so an element or group `opacity` is multiplied
559
+ * into the paints of every leaf under it.
560
+ *
561
+ * `nextId` is handed the node each id is for. `options.leaf` replaces a leaf's
562
+ * data; a group whose leaves it all leaves out is dropped like an empty one.
437
563
  */
438
- declare function svgNodesToKitDrafts(nodes: readonly SvgNode[], nextId: () => string): SvgSceneDraft[];
564
+ declare function svgNodesToKitDrafts<TData = Record<string, unknown>>(input: ParseResult | readonly SvgNode[], nextId: (source: SvgNode) => string, options?: SvgNodesToKitDraftsOptions<TData>): SvgSceneDraft<TData>[];
439
565
  /**
440
566
  * Parse each file and insert its node tree — one undoable `applyOps` batch
441
567
  * per file. See the module doc for placement and wrapping policy. A file
@@ -444,4 +570,4 @@ declare function svgNodesToKitDrafts(nodes: readonly SvgNode[], nextId: () => st
444
570
  */
445
571
  declare function unpackSvgFiles(files: File[], ctx: IngestCtx): Promise<void>;
446
572
 
447
- export { IDENTITY_MATRIX, type Matrix, type NamespaceMeta, type NamespacedElement, type ParseOptions, type ParseResult, type SerializeOptions, type SvgDraftBounds, type SvgGroupNode, type SvgImageNode, type SvgNode, type SvgPaint, type SvgPathNode, type SvgSceneDraft, type SvgStroke, type SvgTextNode, parseSvg, serializeSvg, strokeDataFromSvg, svgImageFromKit, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
573
+ export { IDENTITY_MATRIX, type Matrix, type NamespaceMeta, type NamespacedElement, type ParseOptions, type ParseResult, type SerializeOptions, type SvgDraftBounds, type SvgGroupNode, type SvgImageNode, type SvgKitLeafData, type SvgKitPose, type SvgKitTree, type SvgKitTreeNode, type SvgLeafNode, type SvgNode, type SvgNodesFromKitOptions, type SvgNodesToKitDraftsOptions, type SvgPaint, type SvgPathNode, type SvgSceneDraft, type SvgStroke, type SvgTextNode, fillDataFromSvg, nativeSvgKind, nativeSvgSpace, parseSvg, serializeSvg, strokeDataFromSvg, svgImageFromKit, svgLeafFromKit, svgNodesFromKit, svgNodesToKitDrafts, svgPaintFromKit, svgStrokeFromKit, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };