ggaction 0.0.24 → 0.0.26

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.
Files changed (217) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +2 -2
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/knowledge/task-resolver.js +1 -1
  7. package/package.json +1 -1
  8. package/src/accessibility.js +13 -0
  9. package/src/actions/canvas/actions.js +2 -1
  10. package/src/actions/canvas/fitting.js +68 -13
  11. package/src/actions/charts/beeswarm.js +2 -1
  12. package/src/actions/charts/ecdf.js +2 -1
  13. package/src/actions/charts/endpoints.js +25 -16
  14. package/src/actions/charts/heatmap.js +5 -3
  15. package/src/actions/charts/missing.js +17 -0
  16. package/src/actions/charts/radar.js +70 -33
  17. package/src/actions/charts/raincloud.js +18 -4
  18. package/src/actions/charts/rug-strip.js +2 -2
  19. package/src/actions/charts/shared.js +17 -0
  20. package/src/actions/composition/actions.js +24 -7
  21. package/src/actions/composition/adjacent.js +90 -0
  22. package/src/actions/composition/index.js +11 -1
  23. package/src/actions/composition/layer.js +42 -0
  24. package/src/actions/composition/layerPlan.js +176 -0
  25. package/src/actions/composition/targets.js +105 -0
  26. package/src/actions/data/derived.js +3 -0
  27. package/src/actions/data/filter.js +1 -1
  28. package/src/actions/data/raincloudFilter.js +3 -1
  29. package/src/actions/encodings/appearance.js +26 -7
  30. package/src/actions/encodings/channels.js +1 -0
  31. package/src/actions/encodings/color/continuous.js +37 -34
  32. package/src/actions/encodings/color/index.js +57 -43
  33. package/src/actions/encodings/color/policy.js +15 -0
  34. package/src/actions/encodings/density/resolve.js +3 -2
  35. package/src/actions/encodings/density.js +2 -2
  36. package/src/actions/encodings/offset.js +2 -0
  37. package/src/actions/encodings/pathOrder.js +2 -0
  38. package/src/actions/encodings/position/areaAlignment.js +42 -0
  39. package/src/actions/encodings/position/index.js +37 -3
  40. package/src/actions/encodings/position/policies/line.js +1 -1
  41. package/src/actions/encodings/ranged.js +2 -0
  42. package/src/actions/encodings/remove.js +13 -10
  43. package/src/actions/encodings/ruleAppearance.js +14 -2
  44. package/src/actions/facets/actions.js +1 -1
  45. package/src/actions/guides/axes/axes.js +2 -1
  46. package/src/actions/guides/axes/edit.js +1 -1
  47. package/src/actions/guides/axes/labels.js +2 -1
  48. package/src/actions/guides/axes/parallel/lifecycle.js +11 -4
  49. package/src/actions/guides/axes/parallel/policy.js +5 -2
  50. package/src/actions/guides/axes/parallel/resolve.js +3 -1
  51. package/src/actions/guides/axes/parallel.js +9 -4
  52. package/src/actions/guides/axes/titles.js +1 -1
  53. package/src/actions/guides/facadeLegend.js +3 -2
  54. package/src/actions/guides/grids/grid.js +3 -2
  55. package/src/actions/guides/grids/resolve.js +2 -1
  56. package/src/actions/guides/guides.js +3 -2
  57. package/src/actions/guides/legends/automatic.js +28 -0
  58. package/src/actions/guides/legends/blocks.js +15 -21
  59. package/src/actions/guides/legends/categorical/actions.js +1 -1
  60. package/src/actions/guides/legends/categorical/components.js +4 -3
  61. package/src/actions/guides/legends/categorical/layout.js +5 -2
  62. package/src/actions/guides/legends/categorical/options.js +9 -14
  63. package/src/actions/guides/legends/categorical/recipes.js +3 -0
  64. package/src/actions/guides/legends/categorical/resolve.js +7 -5
  65. package/src/actions/guides/legends/colorSize.js +51 -14
  66. package/src/actions/guides/legends/continuous/common.js +1 -1
  67. package/src/actions/guides/legends/continuous/gradient.js +1 -1
  68. package/src/actions/guides/legends/continuous/opacity.js +2 -0
  69. package/src/actions/guides/legends/edit.js +1 -1
  70. package/src/actions/guides/legends/focused.js +2 -2
  71. package/src/actions/guides/legends/group.js +1 -1
  72. package/src/actions/guides/legends/lane.js +1 -1
  73. package/src/actions/guides/legends/lifecycle.js +2 -2
  74. package/src/actions/guides/legends/remove.js +18 -4
  75. package/src/actions/guides/legends/size.js +1 -1
  76. package/src/actions/guides/legends/transition.js +15 -6
  77. package/src/actions/guides/polar/axes/facade.js +4 -2
  78. package/src/actions/guides/polar/axes/labels.js +6 -1
  79. package/src/actions/guides/polar/axes/shared.js +4 -4
  80. package/src/actions/guides/polar/axes/titles.js +13 -2
  81. package/src/actions/guides/polar/grids.js +6 -3
  82. package/src/actions/guides/tickValues.js +7 -1
  83. package/src/actions/index.js +2 -0
  84. package/src/actions/marks/area/actions.js +9 -2
  85. package/src/actions/marks/area/materialize.js +1 -1
  86. package/src/actions/marks/bar/materialize.js +4 -4
  87. package/src/actions/marks/lifecycle.js +14 -0
  88. package/src/actions/marks/line/actions.js +3 -0
  89. package/src/actions/marks/line/materialize.js +4 -2
  90. package/src/actions/marks/point/edit.js +2 -0
  91. package/src/actions/marks/point/jitter.js +2 -0
  92. package/src/actions/marks/point/materialize.js +5 -4
  93. package/src/actions/marks/point/packing.js +2 -0
  94. package/src/actions/marks/references.js +5 -3
  95. package/src/actions/marks/remove.js +16 -0
  96. package/src/actions/marks/shared.js +2 -1
  97. package/src/actions/marks/text/actions.js +5 -2
  98. package/src/actions/overlays/index.js +168 -0
  99. package/src/actions/overlays/snapshot.js +45 -0
  100. package/src/actions/primitives/editGraphics.js +21 -27
  101. package/src/actions/primitives/semanticAction.js +4 -5
  102. package/src/actions/primitives/semanticValidation/guide.js +1 -0
  103. package/src/actions/primitives/semanticValidation/layer.js +7 -0
  104. package/src/actions/scales/consumers/common.js +10 -0
  105. package/src/actions/scales/consumers/families.js +1 -1
  106. package/src/actions/scales/preview.js +1 -1
  107. package/src/actions/selection/actions.js +63 -11
  108. package/src/actions/titles/actions.js +3 -14
  109. package/src/actions/titles/resolve.js +12 -9
  110. package/src/core/action.js +41 -25
  111. package/src/core/compositionScales.js +12 -0
  112. package/src/core/compositionState.js +16 -2
  113. package/src/core/immutable.js +11 -6
  114. package/src/core/programState.js +16 -0
  115. package/src/core/resourceReferences.js +19 -3
  116. package/src/core/validation.js +7 -0
  117. package/src/core/vocabulary.js +8 -2
  118. package/src/grammar/actionTargets.js +24 -0
  119. package/src/grammar/bin2d.js +7 -0
  120. package/src/grammar/colorSizeLegend.js +56 -5
  121. package/src/grammar/displayLabels.js +10 -0
  122. package/src/grammar/facetHeaders.js +1 -1
  123. package/src/grammar/facets/dependencies.js +10 -3
  124. package/src/grammar/facets/guides.js +2 -1
  125. package/src/grammar/facets/legendIntent.js +7 -5
  126. package/src/grammar/legendBlocks.js +1 -1
  127. package/src/grammar/legendCombinations.js +1 -1
  128. package/src/grammar/legendGroups.js +1 -1
  129. package/src/grammar/legendItemsOffset.js +12 -0
  130. package/src/grammar/legendOverflow.js +25 -0
  131. package/src/grammar/lineSeries.js +1 -1
  132. package/src/grammar/markSelection.js +23 -0
  133. package/src/grammar/overlay.js +51 -0
  134. package/src/grammar/parallelCoordinates.js +1 -208
  135. package/src/grammar/pathCommands.js +8 -2
  136. package/src/grammar/pathSeries.js +1 -1
  137. package/src/grammar/plotComposition.js +152 -0
  138. package/src/grammar/pointShapes.js +18 -0
  139. package/src/grammar/scales/transformed.js +1 -1
  140. package/src/grammar/schemas/concreteGraphic.js +8 -1
  141. package/src/grammar/schemas/graphicBounds.js +8 -1
  142. package/src/grammar/schemas/graphicTree.js +18 -2
  143. package/src/grammar/schemas/semanticPath.js +3 -0
  144. package/src/grammar/strokeStyle.js +14 -8
  145. package/src/grammar/text.js +72 -0
  146. package/src/inspection.js +1 -1
  147. package/src/inspectionDescriptors.js +1 -1
  148. package/src/layout/composition.js +30 -4
  149. package/src/layout/compositionLegend.js +91 -0
  150. package/src/layout/facetHeaderTitles.js +1 -1
  151. package/src/layout/labels.js +55 -27
  152. package/src/layout/legend.js +3 -2
  153. package/src/layout/legendGroup.js +1 -1
  154. package/src/layout/legendItems.js +7 -6
  155. package/src/layout/legendLane.js +1 -1
  156. package/src/layout/legendText.js +1 -1
  157. package/src/materialization/adjacentSource.js +27 -0
  158. package/src/materialization/composition.js +165 -52
  159. package/src/materialization/compositionLegends.js +120 -0
  160. package/src/materialization/compositionScales.js +125 -0
  161. package/src/materialization/compositionTitle.js +96 -0
  162. package/src/materialization/facetGuides/edit.js +22 -2
  163. package/src/materialization/facetGuides/layout.js +1 -1
  164. package/src/materialization/facetGuides/owner.js +4 -3
  165. package/src/materialization/facetGuides/ownerLayout.js +12 -6
  166. package/src/materialization/facetGuides/placement.js +19 -22
  167. package/src/materialization/facetGuides/preparation.js +17 -5
  168. package/src/materialization/facetHeaders.js +5 -1
  169. package/src/materialization/facets.js +1 -1
  170. package/src/materialization/guides/layout.js +1 -1
  171. package/src/materialization/guides/resources.js +1 -1
  172. package/src/materialization/guides/visibility.js +105 -0
  173. package/src/materialization/legendClip.js +1 -1
  174. package/src/materialization/legendGroups.js +1 -1
  175. package/src/materialization/legendItemsOffset.js +68 -0
  176. package/src/materialization/legendSuppression.js +22 -0
  177. package/src/materialization/legends.js +3 -2
  178. package/src/materialization/marks/capabilities.js +4 -0
  179. package/src/materialization/marks/index.js +5 -1
  180. package/src/materialization/marks/pathOrder.js +1 -1
  181. package/src/materialization/marks/policies.js +2 -1
  182. package/src/materialization/overlayItems.js +154 -0
  183. package/src/materialization/scales/policies/binnedPosition.js +43 -5
  184. package/src/materialization/scales/policies/series.js +12 -4
  185. package/src/materialization/scales/resolve.js +2 -0
  186. package/src/materialization/selection/items/bar.js +21 -3
  187. package/src/materialization/selection/items/common.js +18 -5
  188. package/src/materialization/selection/items/overlay.js +15 -0
  189. package/src/materialization/selection/items/path.js +1 -0
  190. package/src/materialization/selection/items/point.js +2 -3
  191. package/src/materialization/selection/items/tick.js +3 -12
  192. package/src/materialization/selection/path.js +2 -2
  193. package/src/materialization/selection/policies/bar.js +2 -0
  194. package/src/materialization/selection/policies/index.js +35 -1
  195. package/src/materialization/selection/policies/point.js +2 -0
  196. package/src/materialization/selection/policies/text.js +17 -0
  197. package/src/materialization/selection/rows.js +103 -0
  198. package/src/materialization/selection/state.js +19 -3
  199. package/src/materialization/selection/styles.js +8 -0
  200. package/src/materialization/text.js +6 -489
  201. package/src/materialization/textGeometry.js +503 -0
  202. package/src/materialization/textLines.js +20 -0
  203. package/src/persistence/validation.js +97 -24
  204. package/src/renderers/canvas/index.js +12 -7
  205. package/src/renderers/canvas/text.js +5 -5
  206. package/src/renderers/nativeBackend.js +25 -0
  207. package/src/renderers/pdf.js +3 -3
  208. package/src/renderers/png.js +4 -3
  209. package/src/renderers/svgSerialization.js +1 -643
  210. package/src/scene.js +5 -3
  211. package/src/selectors/plotComposition.js +115 -0
  212. package/src/selectors/selectionTargets.js +19 -0
  213. package/src/version.js +1 -1
  214. package/types/basic.d.ts +2 -2
  215. package/types/index.d.ts +11 -0
  216. package/types/inspection.d.ts +34 -0
  217. package/types/program.d.ts +553 -365
@@ -1,643 +1 @@
1
- import { collectTransitionGraphics } from "./transitionScene.js";
2
- import { isOwned } from "../core/immutable.js";
3
- import { validateGraphicClip } from "../grammar/schemas/graphicClip.js";
4
- import {
5
- requireSingleOrderedGraphicByType,
6
- walkGraphicTreeEvents
7
- } from "../grammar/schemas/graphicTree.js";
8
- import {
9
- validateConcreteGraphicProperties,
10
- validateConcreteGraphicValue
11
- } from "../grammar/schemas/concreteGraphic.js";
12
- import { resolvePathCommandBounds } from
13
- "../grammar/schemas/graphicBounds.js";
14
- import {
15
- isLinearGradientPaint,
16
- resolveLinearGradientCoordinates,
17
- validateFillPaint
18
- } from "../grammar/paint.js";
19
- import {
20
- requireFiniteProperty,
21
- requireStringProperty
22
- } from "./canvas/validation.js";
23
- import { normalizeRendererFontWeight } from "./text.js";
24
- import { validateRendererOptions } from "./options.js";
25
-
26
- const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
27
- const SVG_OPTIONS = new Set(["title", "description", "resourceNamespace"]);
28
- const RESOURCE_NAMESPACE = /^[A-Za-z][A-Za-z0-9_-]*$/;
29
- const TEXT_ANCHORS = Object.freeze({
30
- left: "start",
31
- start: "start",
32
- center: "middle",
33
- right: "end",
34
- end: "end"
35
- });
36
- const DOMINANT_BASELINES = Object.freeze({
37
- top: "text-before-edge",
38
- hanging: "hanging",
39
- middle: "middle",
40
- alphabetic: "alphabetic",
41
- ideographic: "ideographic",
42
- bottom: "text-after-edge"
43
- });
44
-
45
- const namespaceByGraphic = new WeakMap();
46
-
47
- function resourceNamespace(graphicSpec) {
48
- const cached = namespaceByGraphic.get(graphicSpec);
49
- if (cached !== undefined) return cached;
50
- const serialized = JSON.stringify(graphicSpec);
51
- let hash = 0xcbf29ce484222325n;
52
- for (let index = 0; index < serialized.length; index += 1) {
53
- hash ^= BigInt(serialized.charCodeAt(index));
54
- hash = BigInt.asUintN(64, hash * 0x100000001b3n);
55
- }
56
- const namespace = hash.toString(36);
57
- if (isOwned(graphicSpec)) namespaceByGraphic.set(graphicSpec, namespace);
58
- return namespace;
59
- }
60
-
61
- function escapeText(value) {
62
- for (const character of value) {
63
- const point = character.codePointAt(0);
64
- if (!(
65
- point === 9 || point === 10 || point === 13 ||
66
- (point >= 0x20 && point <= 0xd7ff) ||
67
- (point >= 0xe000 && point <= 0xfffd) ||
68
- (point >= 0x10000 && point <= 0x10ffff)
69
- )) {
70
- throw new TypeError("SVG output requires XML 1.0-compatible strings.");
71
- }
72
- }
73
- return value
74
- .replaceAll("&", "&amp;")
75
- .replaceAll("<", "&lt;")
76
- .replaceAll(">", "&gt;");
77
- }
78
-
79
- function escapeAttribute(value) {
80
- return escapeText(String(value))
81
- .replaceAll("\"", "&quot;")
82
- .replaceAll("'", "&apos;");
83
- }
84
-
85
- function formatNumber(value) {
86
- if (!Number.isFinite(value)) {
87
- throw new TypeError("SVG renderer requires finite numeric output.");
88
- }
89
- return Object.is(value, -0) ? "0" : String(value);
90
- }
91
-
92
- function attributes(entries) {
93
- return entries
94
- .filter(([, value]) => value !== undefined)
95
- .map(([name, value]) => `${name}="${escapeAttribute(value)}"`)
96
- .join(" ");
97
- }
98
-
99
- function element(name, entries, content) {
100
- const serialized = attributes(entries);
101
- const opening = serialized.length === 0 ? `<${name}` : `<${name} ${serialized}`;
102
- if (content === undefined) return `${opening}/>`;
103
- return `${opening}>${content}</${name}>`;
104
- }
105
-
106
- function requireOpacity(properties, graphicId) {
107
- const opacity = properties.opacity ?? 1;
108
- if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1) {
109
- throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
110
- }
111
- return opacity;
112
- }
113
-
114
- function requireStrokeDash(properties, graphicId) {
115
- const strokeDash = properties.strokeDash ?? [];
116
- if (
117
- !Array.isArray(strokeDash) ||
118
- !strokeDash.every(value => Number.isFinite(value) && value >= 0)
119
- ) {
120
- throw new Error(
121
- `Graphic "${graphicId}" requires a non-negative finite strokeDash array.`
122
- );
123
- }
124
- return strokeDash;
125
- }
126
-
127
- function strokeDetailAttributes(properties, strokeWidth) {
128
- if (typeof properties.stroke !== "string" || !(strokeWidth > 0)) return [];
129
- return [
130
- ["stroke-linecap", Object.hasOwn(properties, "lineCap")
131
- ? properties.lineCap
132
- : undefined],
133
- ["stroke-linejoin", Object.hasOwn(properties, "lineJoin")
134
- ? properties.lineJoin
135
- : undefined],
136
- ["stroke-miterlimit", Object.hasOwn(properties, "miterLimit")
137
- ? formatNumber(properties.miterLimit)
138
- : undefined]
139
- ];
140
- }
141
-
142
- function requireCanvasGeometry(id, canvas, { nested }) {
143
- const properties = canvas.properties ?? {};
144
- const width = requireFiniteProperty(properties, "width", id);
145
- const height = requireFiniteProperty(properties, "height", id);
146
- if (width < 0 || height < 0) {
147
- const label = nested ? `Nested Canvas "${id}"` : "Canvas";
148
- throw new Error(`${label} width and height must not be negative.`);
149
- }
150
- return {
151
- ...(nested ? {
152
- x: requireFiniteProperty(properties, "x", id),
153
- y: requireFiniteProperty(properties, "y", id)
154
- } : {}),
155
- width,
156
- height
157
- };
158
- }
159
-
160
- function requireCanvasBackground(id, canvas) {
161
- const background = canvas.properties?.background;
162
- if (background === undefined) return undefined;
163
- if (typeof background !== "string") {
164
- throw new Error(
165
- `Graphic "${id}" requires a string background property.`
166
- );
167
- }
168
- return background;
169
- }
170
-
171
- function addLinearGradient(state, fill, bounds, graphicId) {
172
- const coordinates = resolveLinearGradientCoordinates(fill, bounds);
173
- const id =
174
- `ggaction-gradient-${state.resourceNamespace}-${state.gradientCount}`;
175
- state.gradientCount += 1;
176
- const stops = fill.stops.map(stop => element("stop", [
177
- ["offset", formatNumber(stop.offset)],
178
- ["stop-color", stop.color]
179
- ])).join("");
180
- state.definitions.push(element("linearGradient", [
181
- ["id", id],
182
- ["gradientUnits", "userSpaceOnUse"],
183
- ["x1", formatNumber(coordinates.from.x)],
184
- ["y1", formatNumber(coordinates.from.y)],
185
- ["x2", formatNumber(coordinates.to.x)],
186
- ["y2", formatNumber(coordinates.to.y)]
187
- ], stops));
188
- return `url(#${id})`;
189
- }
190
-
191
- function serializeFill(state, fill, bounds, graphicId) {
192
- validateFillPaint(fill, `${graphicId}.fill`);
193
- if (!isLinearGradientPaint(fill)) return fill;
194
- return addLinearGradient(state, fill, bounds, graphicId);
195
- }
196
-
197
- function serializeCircle(state, graphicId, properties) {
198
- validateConcreteGraphicProperties("circle", properties);
199
- const x = requireFiniteProperty(properties, "x", graphicId);
200
- const y = requireFiniteProperty(properties, "y", graphicId);
201
- const radius = requireFiniteProperty(properties, "radius", graphicId);
202
- if (radius < 0) {
203
- throw new Error(`Graphic "${graphicId}" requires a non-negative radius.`);
204
- }
205
- if (typeof properties.fill !== "string") {
206
- throw new Error(`Graphic "${graphicId}" requires a string fill property.`);
207
- }
208
- let strokeWidth;
209
- if (properties.stroke !== undefined) {
210
- if (typeof properties.stroke !== "string") {
211
- throw new Error(`Graphic "${graphicId}" requires a string stroke property.`);
212
- }
213
- strokeWidth = requireFiniteProperty(properties, "strokeWidth", graphicId);
214
- if (strokeWidth < 0) {
215
- throw new Error(
216
- `Graphic "${graphicId}" requires a non-negative strokeWidth.`
217
- );
218
- }
219
- }
220
- return element("circle", [
221
- ["cx", formatNumber(x)],
222
- ["cy", formatNumber(y)],
223
- ["r", formatNumber(radius)],
224
- ["fill", properties.fill],
225
- ["stroke", properties.stroke],
226
- ["stroke-width", strokeWidth === undefined
227
- ? undefined
228
- : formatNumber(strokeWidth)],
229
- ...strokeDetailAttributes(properties, strokeWidth),
230
- ["opacity", formatNumber(requireOpacity(properties, graphicId))]
231
- ]);
232
- }
233
-
234
- function serializeRect(state, graphicId, properties) {
235
- validateConcreteGraphicProperties("rect", properties);
236
- const x = requireFiniteProperty(properties, "x", graphicId);
237
- const y = requireFiniteProperty(properties, "y", graphicId);
238
- const width = requireFiniteProperty(properties, "width", graphicId);
239
- const height = requireFiniteProperty(properties, "height", graphicId);
240
- const strokeWidth = requireFiniteProperty(
241
- properties,
242
- "strokeWidth",
243
- graphicId
244
- );
245
- if (width < 0 || height < 0 || strokeWidth < 0) {
246
- throw new Error(
247
- `Graphic "${graphicId}" requires non-negative rect dimensions and strokeWidth.`
248
- );
249
- }
250
- if (properties.fill === undefined) {
251
- throw new Error(`Graphic "${graphicId}" requires a fill property.`);
252
- }
253
- if (typeof properties.stroke !== "string") {
254
- throw new Error(`Graphic "${graphicId}" requires a string stroke property.`);
255
- }
256
- const fill = serializeFill(state, properties.fill, {
257
- left: x,
258
- right: x + width,
259
- top: y,
260
- bottom: y + height
261
- }, graphicId);
262
- return element("rect", [
263
- ["x", formatNumber(x)],
264
- ["y", formatNumber(y)],
265
- ["width", formatNumber(width)],
266
- ["height", formatNumber(height)],
267
- ["fill", fill],
268
- ["stroke", properties.stroke],
269
- ["stroke-width", formatNumber(strokeWidth)],
270
- ...strokeDetailAttributes(properties, strokeWidth),
271
- ["opacity", formatNumber(requireOpacity(properties, graphicId))],
272
- ["style", properties.blend === "difference" ? "mix-blend-mode:difference" : undefined]
273
- ]);
274
- }
275
-
276
- function serializeLine(state, graphicId, properties) {
277
- validateConcreteGraphicProperties("line", properties);
278
- const strokeWidth = requireFiniteProperty(
279
- properties,
280
- "strokeWidth",
281
- graphicId
282
- );
283
- if (strokeWidth < 0) {
284
- throw new Error(
285
- `Graphic "${graphicId}" requires a non-negative strokeWidth.`
286
- );
287
- }
288
- if (typeof properties.stroke !== "string") {
289
- throw new Error(`Graphic "${graphicId}" requires a string stroke property.`);
290
- }
291
- const strokeDash = requireStrokeDash(properties, graphicId);
292
- return element("line", [
293
- ["x1", formatNumber(requireFiniteProperty(properties, "x1", graphicId))],
294
- ["y1", formatNumber(requireFiniteProperty(properties, "y1", graphicId))],
295
- ["x2", formatNumber(requireFiniteProperty(properties, "x2", graphicId))],
296
- ["y2", formatNumber(requireFiniteProperty(properties, "y2", graphicId))],
297
- ["fill", "none"],
298
- ["stroke", properties.stroke],
299
- ["stroke-width", formatNumber(strokeWidth)],
300
- ...strokeDetailAttributes(properties, strokeWidth),
301
- ["stroke-dasharray", strokeDash.length === 0
302
- ? undefined
303
- : strokeDash.map(formatNumber).join(" ")],
304
- ["opacity", formatNumber(requireOpacity(properties, graphicId))]
305
- ]);
306
- }
307
-
308
- function serializePathData(commands) {
309
- return commands.map(command => {
310
- if (command.op === "M" || command.op === "L") {
311
- return `${command.op}${formatNumber(command.x)} ${formatNumber(command.y)}`;
312
- }
313
- if (command.op === "C") {
314
- return `C${formatNumber(command.x1)} ${formatNumber(command.y1)} ` +
315
- `${formatNumber(command.x2)} ${formatNumber(command.y2)} ` +
316
- `${formatNumber(command.x)} ${formatNumber(command.y)}`;
317
- }
318
- return "Z";
319
- }).join(" ");
320
- }
321
-
322
- function serializePath(state, graphicId, properties) {
323
- validateConcreteGraphicProperties("path", properties);
324
- const commands = properties.commands;
325
- if (!Array.isArray(commands)) {
326
- throw new Error(
327
- `Graphic "${graphicId}" requires concrete path commands.`
328
- );
329
- }
330
- const hasFill = properties.fill !== undefined;
331
- const hasStroke = properties.stroke !== undefined;
332
- if (!hasFill && !hasStroke) {
333
- throw new Error(`Graphic "${graphicId}" requires a fill or stroke property.`);
334
- }
335
- if (hasFill && commands.at(-1).op !== "Z") {
336
- throw new Error(`Graphic "${graphicId}" requires a final Z command when filled.`);
337
- }
338
- if (hasStroke && typeof properties.stroke !== "string") {
339
- throw new Error(`Graphic "${graphicId}" requires a string stroke property.`);
340
- }
341
- const strokeWidth = hasStroke
342
- ? requireFiniteProperty(properties, "strokeWidth", graphicId)
343
- : undefined;
344
- if (hasStroke && strokeWidth < 0) {
345
- throw new Error(
346
- `Graphic "${graphicId}" requires a non-negative strokeWidth.`
347
- );
348
- }
349
- const strokeDash = requireStrokeDash(properties, graphicId);
350
- let fill = "none";
351
- if (hasFill) {
352
- const bounds = resolvePathCommandBounds(commands);
353
- if (bounds === undefined) {
354
- throw new Error(`Graphic "${graphicId}" requires finite path fill bounds.`);
355
- }
356
- fill = serializeFill(state, properties.fill, bounds, graphicId);
357
- }
358
- return element("path", [
359
- ["d", serializePathData(commands)],
360
- ["fill", fill],
361
- ["stroke", properties.stroke],
362
- ["stroke-width", strokeWidth === undefined
363
- ? undefined
364
- : formatNumber(strokeWidth)],
365
- ...strokeDetailAttributes(properties, strokeWidth),
366
- ["stroke-dasharray", strokeDash.length === 0
367
- ? undefined
368
- : strokeDash.map(formatNumber).join(" ")],
369
- ["opacity", formatNumber(requireOpacity(properties, graphicId))],
370
- ["style", properties.blend === "difference" ? "mix-blend-mode:difference" : undefined]
371
- ]);
372
- }
373
-
374
- function serializeText(state, graphicId, properties) {
375
- validateConcreteGraphicProperties("text", properties);
376
- const x = requireFiniteProperty(properties, "x", graphicId);
377
- const y = requireFiniteProperty(properties, "y", graphicId);
378
- const fontSize = requireFiniteProperty(properties, "fontSize", graphicId);
379
- const rotation = properties.rotation ?? 0;
380
- const opacity = requireOpacity(properties, graphicId);
381
- const text = properties.text;
382
- validateConcreteGraphicValue("text", "text", text);
383
- const fill = requireStringProperty(properties, "fill", graphicId);
384
- const fontFamily = requireStringProperty(
385
- properties,
386
- "fontFamily",
387
- graphicId
388
- );
389
- const textAlign = requireStringProperty(
390
- properties,
391
- "textAlign",
392
- graphicId
393
- );
394
- const textBaseline = requireStringProperty(
395
- properties,
396
- "textBaseline",
397
- graphicId
398
- );
399
- const fontWeight = properties.fontWeight ?? "normal";
400
- if (fontSize <= 0) {
401
- throw new Error(`Graphic "${graphicId}" requires a positive fontSize.`);
402
- }
403
- if (!Number.isFinite(rotation)) {
404
- throw new Error(`Graphic "${graphicId}" requires a finite rotation.`);
405
- }
406
- if (
407
- !(
408
- (typeof fontWeight === "string" && fontWeight.length > 0) ||
409
- Number.isFinite(fontWeight)
410
- )
411
- ) {
412
- throw new Error(`Graphic "${graphicId}" requires a valid fontWeight.`);
413
- }
414
- if (TEXT_ANCHORS[textAlign] === undefined) {
415
- throw new Error(`Graphic "${graphicId}" has invalid textAlign.`);
416
- }
417
- if (DOMINANT_BASELINES[textBaseline] === undefined) {
418
- throw new Error(`Graphic "${graphicId}" has invalid textBaseline.`);
419
- }
420
- const rotationDegrees = rotation * 180 / Math.PI;
421
- return element("text", [
422
- ["x", formatNumber(x)],
423
- ["y", formatNumber(y)],
424
- ["fill", fill],
425
- ["font-family", fontFamily],
426
- ["font-size", formatNumber(fontSize)],
427
- ["font-weight", normalizeRendererFontWeight(fontWeight)],
428
- ["font-style", properties.fontStyle],
429
- ["text-anchor", TEXT_ANCHORS[textAlign]],
430
- ["dominant-baseline", DOMINANT_BASELINES[textBaseline]],
431
- ["opacity", formatNumber(opacity)],
432
- ["transform", rotation === 0
433
- ? undefined
434
- : `rotate(${formatNumber(rotationDegrees)} ${formatNumber(x)} ${formatNumber(y)})`]
435
- ], properties.lines === undefined ? escapeText(text) : properties.lines.map(line =>
436
- element("tspan", [["x", formatNumber(x + line.x)], ["y", formatNumber(y + line.y)]], escapeText(line.text))
437
- ).join(""));
438
- }
439
-
440
- function serializeConcreteGraphic(state, id, graphic) {
441
- if (graphic.type === "collection") {
442
- const items = (graphic.items ?? []).map(item =>
443
- serializeConcreteGraphic(state, item.id ?? id, item)
444
- ).join("");
445
- return element("g", [], items);
446
- }
447
- const properties = graphic.properties ?? {};
448
- if (graphic.type === "circle") {
449
- return serializeCircle(state, id, properties);
450
- }
451
- if (graphic.type === "rect") {
452
- return serializeRect(state, id, properties);
453
- }
454
- if (graphic.type === "line") {
455
- return serializeLine(state, id, properties);
456
- }
457
- if (graphic.type === "path") {
458
- return serializePath(state, id, properties);
459
- }
460
- if (graphic.type === "text") {
461
- return serializeText(state, id, properties);
462
- }
463
- throw new Error(`SVG renderer does not support "${graphic.type}" yet.`);
464
- }
465
-
466
- function openNestedCanvas(state, id, canvas) {
467
- const { x, y, width, height } = requireCanvasGeometry(
468
- id,
469
- canvas,
470
- { nested: true }
471
- );
472
- const clipId = `ggaction-clip-${state.resourceNamespace}-${state.clipCount}`;
473
- state.clipCount += 1;
474
- state.definitions.push(element("clipPath", [["id", clipId]], element(
475
- "rect",
476
- [
477
- ["x", "0"],
478
- ["y", "0"],
479
- ["width", formatNumber(width)],
480
- ["height", formatNumber(height)]
481
- ]
482
- )));
483
- const background = requireCanvasBackground(id, canvas);
484
- const backgroundRect = background === undefined
485
- ? ""
486
- : element("rect", [
487
- ["x", "0"],
488
- ["y", "0"],
489
- ["width", formatNumber(width)],
490
- ["height", formatNumber(height)],
491
- ["fill", background]
492
- ]);
493
- return `<g ${attributes([
494
- ["transform", `translate(${formatNumber(x)} ${formatNumber(y)})`],
495
- ["clip-path", `url(#${clipId})`]
496
- ])}>${backgroundRect}`;
497
- }
498
-
499
- function serializeBody(graphicSpec, rootId, rootCanvas, width, height, state) {
500
- const output = [];
501
- const background = requireCanvasBackground(rootId, rootCanvas);
502
- if (background !== undefined) {
503
- output.push(element("rect", [
504
- ["x", "0"],
505
- ["y", "0"],
506
- ["width", formatNumber(width)],
507
- ["height", formatNumber(height)],
508
- ["fill", background]
509
- ]));
510
- }
511
- walkGraphicTreeEvents(graphicSpec, {
512
- enter({ id, object }) {
513
- if (id === rootId) return;
514
- if (object.clip !== undefined) {
515
- validateGraphicClip(object.clip, object.type);
516
- const clipId = `ggaction-clip-${state.resourceNamespace}-${state.clipCount++}`;
517
- state.definitions.push(element("clipPath", [["id", clipId]], element("rect",
518
- Object.entries(object.clip).map(([key, value]) => [key, formatNumber(value)]))));
519
- output.push(`<g clip-path="url(#${clipId})">`);
520
- }
521
- if (object.type === "canvas") {
522
- output.push(openNestedCanvas(state, id, object));
523
- return;
524
- }
525
- if (object.type === "collection") {
526
- output.push("<g>");
527
- return;
528
- }
529
- if (!Array.isArray(object.items)) {
530
- output.push(serializeConcreteGraphic(state, id, object));
531
- }
532
- },
533
- item({ id, object, owner }) {
534
- output.push(serializeConcreteGraphic(state, id, {
535
- ...object,
536
- type: object.type ?? owner.type
537
- }));
538
- },
539
- exit({ id, object }) {
540
- if (
541
- id !== rootId &&
542
- (object.type === "collection" || object.type === "canvas")
543
- ) {
544
- output.push("</g>");
545
- }
546
- if (object.clip !== undefined) output.push("</g>");
547
- }
548
- });
549
- return output.join("");
550
- }
551
-
552
- function requireSVGOptions(options) {
553
- validateRendererOptions(options, SVG_OPTIONS, "renderToSVG options");
554
- for (const key of SVG_OPTIONS) {
555
- if (
556
- options[key] !== undefined &&
557
- (typeof options[key] !== "string" || options[key].length === 0)
558
- ) {
559
- throw new TypeError(`renderToSVG ${key} must be a non-empty string.`);
560
- }
561
- }
562
- if (
563
- options.resourceNamespace !== undefined &&
564
- !RESOURCE_NAMESPACE.test(options.resourceNamespace)
565
- ) {
566
- throw new TypeError(
567
- "renderToSVG resourceNamespace must start with a letter and contain only letters, numbers, _ or -."
568
- );
569
- }
570
- return options;
571
- }
572
-
573
- export function renderToSVG(program, options = {}) {
574
- const resolvedOptions = requireSVGOptions(options);
575
- const graphicSpec = program?.graphicSpec;
576
- if (
577
- graphicSpec === null ||
578
- typeof graphicSpec !== "object" ||
579
- graphicSpec.objects === null ||
580
- typeof graphicSpec.objects !== "object" ||
581
- !Array.isArray(graphicSpec.order)
582
- ) {
583
- throw new TypeError("renderToSVG requires a program with a graphicSpec.");
584
- }
585
- const { id: rootId, object: rootCanvas } =
586
- requireSingleOrderedGraphicByType(graphicSpec, "canvas");
587
- const { width, height } = requireCanvasGeometry(
588
- rootId,
589
- rootCanvas,
590
- { nested: false }
591
- );
592
- const state = {
593
- clipCount: 1,
594
- definitions: [],
595
- gradientCount: 1,
596
- resourceNamespace:
597
- resolvedOptions.resourceNamespace ?? resourceNamespace(graphicSpec)
598
- };
599
- walkGraphicTreeEvents(graphicSpec, {
600
- enter({object}) {
601
- if (object.clip !== undefined) validateGraphicClip(object.clip, object.type);
602
- if (object.properties?.blend === "difference") state.hasBlend = true;
603
- },
604
- item({object}) { if (object.properties?.blend === "difference") state.hasBlend = true; }
605
- });
606
- const body = serializeBody(
607
- graphicSpec,
608
- rootId,
609
- rootCanvas,
610
- width,
611
- height,
612
- state
613
- );
614
- const accessible = [
615
- resolvedOptions.title === undefined
616
- ? ""
617
- : element("title", [], escapeText(resolvedOptions.title)),
618
- resolvedOptions.description === undefined
619
- ? ""
620
- : element("desc", [], escapeText(resolvedOptions.description))
621
- ].join("");
622
- const definitions = state.definitions.length === 0
623
- ? ""
624
- : element("defs", [], state.definitions.join(""));
625
- return element("svg", [
626
- ["xmlns", SVG_NAMESPACE],
627
- ["width", formatNumber(width)],
628
- ["height", formatNumber(height)],
629
- ["viewBox", `0 0 ${formatNumber(width)} ${formatNumber(height)}`],
630
- ["style", state.hasBlend ? "isolation:isolate" : undefined]
631
- ], `${accessible}${definitions}${body}`);
632
- }
633
-
634
- // Internal adapter: only concrete graphics and opaque correspondence keys enter here.
635
- export function renderTransitionGraphics(graphicSpec, identities) {
636
- const scene = collectTransitionGraphics(graphicSpec, identities);
637
- const state = { definitions: [], gradientCount: 1, clipCount: 1,
638
- resourceNamespace: `scene-${resourceNamespace(graphicSpec)}` };
639
- const nodes = scene.nodes.map(({ properties, clips, ...node }) => ({ ...node,
640
- svg: serializeConcreteGraphic(state, node.key, { type: node.type, properties }) }));
641
- return { version: 1, width: scene.width, height: scene.height,
642
- definitions: state.definitions.join(""), nodes };
643
- }
1
+ import{collectTransitionGraphics as F}from"./transitionScene.js";import{isOwned as V}from"../core/immutable.js";import{validateGraphicClip as b}from"../grammar/schemas/graphicClip.js";import{requireSingleOrderedGraphicByType as j,walkGraphicTreeEvents as q}from"../grammar/schemas/graphicTree.js";import{validateConcreteGraphicProperties as $,validateConcreteGraphicValue as D}from"../grammar/schemas/concreteGraphic.js";import{resolvePathCommandBounds as M}from"../grammar/schemas/graphicBounds.js";import{isLinearGradientPaint as L,resolveLinearGradientCoordinates as R,validateFillPaint as _}from"../grammar/paint.js";import{requireFiniteProperty as l,requireStringProperty as g}from"./canvas/validation.js";import{normalizeRendererFontWeight as Z}from"./text.js";import{validateRendererOptions as J}from"./options.js";const U="http://www.w3.org/2000/svg",C=new Set(["title","description","resourceNamespace"]),X=/^[A-Za-z][A-Za-z0-9_-]*$/,S=Object.freeze({left:"start",start:"start",center:"middle",right:"end",end:"end"}),v=Object.freeze({top:"text-before-edge",hanging:"hanging",middle:"middle",alphabetic:"alphabetic",ideographic:"ideographic",bottom:"text-after-edge"}),A=new WeakMap;function N(n){const t=A.get(n);if(t!==void 0)return t;const e=JSON.stringify(n);let r=3421674724,o=2216829733;for(let s=0;s<e.length;s+=1){o=(o^e.charCodeAt(s))>>>0;const u=o*435;r=Math.imul(r,435)+Math.imul(o,256)+Math.floor(u/4294967296)>>>0,o=u>>>0}const f=(BigInt(r)<<32n|BigInt(o)).toString(36);return V(n)&&A.set(n,f),f}function x(n){for(const t of n){const e=t.codePointAt(0);if(!(e===9||e===10||e===13||e>=32&&e<=55295||e>=57344&&e<=65533||e>=65536&&e<=1114111))throw new TypeError("SVG output requires XML 1.0-compatible strings.")}return n.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;")}function H(n){return x(String(n)).replaceAll('"',"&quot;").replaceAll("'","&apos;")}function i(n){if(!Number.isFinite(n))throw new TypeError("SVG renderer requires finite numeric output.");return Object.is(n,-0)?"0":String(n)}function O(n){return n.filter(([,t])=>t!==void 0).map(([t,e])=>`${t}="${H(e)}"`).join(" ")}function d(n,t,e){const r=O(t),o=r.length===0?`<${n}`:`<${n} ${r}`;return e===void 0?`${o}/>`:`${o}>${e}</${n}>`}function k(n,t){const e=n.opacity??1;if(!Number.isFinite(e)||e<0||e>1)throw new Error(`Graphic "${t}" requires opacity from 0 to 1.`);return e}function z(n,t){const e=n.strokeDash??[];if(!Array.isArray(e)||!e.every(r=>Number.isFinite(r)&&r>=0))throw new Error(`Graphic "${t}" requires a non-negative finite strokeDash array.`);return e}function G(n,t){return typeof n.stroke!="string"||!(t>0)?[]:[["stroke-linecap",Object.hasOwn(n,"lineCap")?n.lineCap:void 0],["stroke-linejoin",Object.hasOwn(n,"lineJoin")?n.lineJoin:void 0],["stroke-miterlimit",Object.hasOwn(n,"miterLimit")?i(n.miterLimit):void 0]]}function T(n,t,{nested:e}){const r=t.properties??{},o=l(r,"width",n),f=l(r,"height",n);if(o<0||f<0){const s=e?`Nested Canvas "${n}"`:"Canvas";throw new Error(`${s} width and height must not be negative.`)}return{...e?{x:l(r,"x",n),y:l(r,"y",n)}:{},width:o,height:f}}function W(n,t){const e=t.properties?.background;if(e!==void 0){if(typeof e!="string")throw new Error(`Graphic "${n}" requires a string background property.`);return e}}function K(n,t,e,r){const o=R(t,e),f=`ggaction-gradient-${n.resourceNamespace}-${n.gradientCount}`;n.gradientCount+=1;const s=t.stops.map(u=>d("stop",[["offset",i(u.offset)],["stop-color",u.color]])).join("");return n.definitions.push(d("linearGradient",[["id",f],["gradientUnits","userSpaceOnUse"],["x1",i(o.from.x)],["y1",i(o.from.y)],["x2",i(o.to.x)],["y2",i(o.to.y)]],s)),`url(#${f})`}function B(n,t,e,r){return _(t,`${r}.fill`),L(t)?K(n,t,e,r):t}function Q(n,t,e){$("circle",e);const r=l(e,"x",t),o=l(e,"y",t),f=l(e,"radius",t);if(f<0)throw new Error(`Graphic "${t}" requires a non-negative radius.`);if(typeof e.fill!="string")throw new Error(`Graphic "${t}" requires a string fill property.`);let s;if(e.stroke!==void 0){if(typeof e.stroke!="string")throw new Error(`Graphic "${t}" requires a string stroke property.`);if(s=l(e,"strokeWidth",t),s<0)throw new Error(`Graphic "${t}" requires a non-negative strokeWidth.`)}return d("circle",[["cx",i(r)],["cy",i(o)],["r",i(f)],["fill",e.fill],["stroke",e.stroke],["stroke-width",s===void 0?void 0:i(s)],...G(e,s),["opacity",i(k(e,t))]])}function Y(n,t,e){$("rect",e);const r=l(e,"x",t),o=l(e,"y",t),f=l(e,"width",t),s=l(e,"height",t),u=l(e,"strokeWidth",t);if(f<0||s<0||u<0)throw new Error(`Graphic "${t}" requires non-negative rect dimensions and strokeWidth.`);if(e.fill===void 0)throw new Error(`Graphic "${t}" requires a fill property.`);if(typeof e.stroke!="string")throw new Error(`Graphic "${t}" requires a string stroke property.`);const c=B(n,e.fill,{left:r,right:r+f,top:o,bottom:o+s},t);return d("rect",[["x",i(r)],["y",i(o)],["width",i(f)],["height",i(s)],["fill",c],["stroke",e.stroke],["stroke-width",i(u)],...G(e,u),["opacity",i(k(e,t))],["style",e.blend==="difference"?"mix-blend-mode:difference":void 0]])}function I(n,t,e){$("line",e);const r=l(e,"strokeWidth",t);if(r<0)throw new Error(`Graphic "${t}" requires a non-negative strokeWidth.`);if(typeof e.stroke!="string")throw new Error(`Graphic "${t}" requires a string stroke property.`);const o=z(e,t);return d("line",[["x1",i(l(e,"x1",t))],["y1",i(l(e,"y1",t))],["x2",i(l(e,"x2",t))],["y2",i(l(e,"y2",t))],["fill","none"],["stroke",e.stroke],["stroke-width",i(r)],...G(e,r),["stroke-dasharray",o.length===0?void 0:o.map(i).join(" ")],["opacity",i(k(e,t))]])}function ee(n){return n.map(t=>t.op==="M"||t.op==="L"?`${t.op}${i(t.x)} ${i(t.y)}`:t.op==="C"?`C${i(t.x1)} ${i(t.y1)} ${i(t.x2)} ${i(t.y2)} ${i(t.x)} ${i(t.y)}`:"Z").join(" ")}function te(n,t,e){$("path",e);const r=e.commands;if(!Array.isArray(r))throw new Error(`Graphic "${t}" requires concrete path commands.`);const o=e.fill!==void 0,f=e.stroke!==void 0;if(!o&&!f)throw new Error(`Graphic "${t}" requires a fill or stroke property.`);if(o&&r.at(-1).op!=="Z")throw new Error(`Graphic "${t}" requires a final Z command when filled.`);if(f&&typeof e.stroke!="string")throw new Error(`Graphic "${t}" requires a string stroke property.`);const s=f?l(e,"strokeWidth",t):void 0;if(f&&s<0)throw new Error(`Graphic "${t}" requires a non-negative strokeWidth.`);const u=z(e,t);let c="none";if(o){const a=M(r);if(a===void 0)throw new Error(`Graphic "${t}" requires finite path fill bounds.`);c=B(n,e.fill,a,t)}return d("path",[["d",ee(r)],["fill",c],["stroke",e.stroke],["stroke-width",s===void 0?void 0:i(s)],...G(e,s),["stroke-dasharray",u.length===0?void 0:u.map(i).join(" ")],["opacity",i(k(e,t))],["style",e.blend==="difference"?"mix-blend-mode:difference":void 0]])}function ne(n,t,e){$("text",e);const r=l(e,"x",t),o=l(e,"y",t),f=l(e,"fontSize",t),s=e.rotation??0,u=k(e,t),c=e.text;D("text","text",c);const a=g(e,"fill",t),y=g(e,"fontFamily",t),w=g(e,"textAlign",t),m=g(e,"textBaseline",t),h=e.fontWeight??"normal";if(f<=0)throw new Error(`Graphic "${t}" requires a positive fontSize.`);if(!Number.isFinite(s))throw new Error(`Graphic "${t}" requires a finite rotation.`);if(!(typeof h=="string"&&h.length>0||Number.isFinite(h)))throw new Error(`Graphic "${t}" requires a valid fontWeight.`);if(S[w]===void 0)throw new Error(`Graphic "${t}" has invalid textAlign.`);if(v[m]===void 0)throw new Error(`Graphic "${t}" has invalid textBaseline.`);const P=s*180/Math.PI;return d("text",[["x",i(r)],["y",i(o)],["fill",a],["font-family",y],["font-size",i(f)],["font-weight",Z(h)],["font-style",e.fontStyle],["text-anchor",S[w]],["dominant-baseline",v[m]],["opacity",i(u)],["transform",s===0?void 0:`rotate(${i(P)} ${i(r)} ${i(o)})`]],e.lines===void 0?x(c):e.lines.map(E=>d("tspan",[["x",i(r+E.x)],["y",i(o+E.y)]],x(E.text))).join(""))}function p(n,t,e){if(e.type==="collection"){const o=(e.items??[]).map(f=>p(n,f.id??t,f)).join("");return d("g",[],o)}const r=e.properties??{};if(e.type==="circle")return Q(n,t,r);if(e.type==="rect")return Y(n,t,r);if(e.type==="line")return I(n,t,r);if(e.type==="path")return te(n,t,r);if(e.type==="text")return ne(n,t,r);throw new Error(`SVG renderer does not support "${e.type}" yet.`)}function re(n,t,e){const{x:r,y:o,width:f,height:s}=T(t,e,{nested:!0}),u=`ggaction-clip-${n.resourceNamespace}-${n.clipCount}`;n.clipCount+=1,n.definitions.push(d("clipPath",[["id",u]],d("rect",[["x","0"],["y","0"],["width",i(f)],["height",i(s)]])));const c=W(t,e),a=c===void 0?"":d("rect",[["x","0"],["y","0"],["width",i(f)],["height",i(s)],["fill",c]]);return`<g ${O([["transform",`translate(${i(r)} ${i(o)})`],["clip-path",`url(#${u})`]])}>${a}`}function ie(n,t,e,r,o,f){const s=[],u=W(t,e);return u!==void 0&&s.push(d("rect",[["x","0"],["y","0"],["width",i(r)],["height",i(o)],["fill",u]])),q(n,{enter({id:c,object:a}){if(c!==t){if(a.clip!==void 0){b(a.clip,a.type);const y=`ggaction-clip-${f.resourceNamespace}-${f.clipCount++}`;f.definitions.push(d("clipPath",[["id",y]],d("rect",Object.entries(a.clip).map(([w,m])=>[w,i(m)])))),s.push(`<g clip-path="url(#${y})">`)}if(a.type==="canvas"){s.push(re(f,c,a));return}if(a.type==="collection"){s.push("<g>");return}Array.isArray(a.items)||s.push(p(f,c,a))}},item({id:c,object:a,owner:y}){s.push(p(f,c,{...a,type:a.type??y.type}))},exit({id:c,object:a}){c!==t&&(a.type==="collection"||a.type==="canvas")&&s.push("</g>"),a.clip!==void 0&&s.push("</g>")}}),s.join("")}function oe(n){J(n,C,"renderToSVG options");for(const t of C)if(n[t]!==void 0&&(typeof n[t]!="string"||n[t].length===0))throw new TypeError(`renderToSVG ${t} must be a non-empty string.`);if(n.resourceNamespace!==void 0&&!X.test(n.resourceNamespace))throw new TypeError("renderToSVG resourceNamespace must start with a letter and contain only letters, numbers, _ or -.");return n}function me(n,t={}){const e=oe(t),r=n?.graphicSpec;if(r===null||typeof r!="object"||r.objects===null||typeof r.objects!="object"||!Array.isArray(r.order))throw new TypeError("renderToSVG requires a program with a graphicSpec.");const{id:o,object:f}=j(r,"canvas"),{width:s,height:u}=T(o,f,{nested:!1});let c=e.resourceNamespace;const a={clipCount:1,definitions:[],gradientCount:1,get resourceNamespace(){return c??=N(r)}};q(r,{enter({object:h}){h.clip!==void 0&&b(h.clip,h.type),h.properties?.blend==="difference"&&(a.hasBlend=!0)},item({object:h}){h.properties?.blend==="difference"&&(a.hasBlend=!0)}});const y=ie(r,o,f,s,u,a),w=[e.title===void 0?"":d("title",[],x(e.title)),e.description===void 0?"":d("desc",[],x(e.description))].join(""),m=a.definitions.length===0?"":d("defs",[],a.definitions.join(""));return d("svg",[["xmlns",U],["width",i(s)],["height",i(u)],["viewBox",`0 0 ${i(s)} ${i(u)}`],["style",a.hasBlend?"isolation:isolate":void 0]],`${w}${m}${y}`)}function $e(n,t){const e=F(n,t);let r;const o={definitions:[],gradientCount:1,clipCount:1,get resourceNamespace(){return r??=`scene-${N(n)}`}},f=e.nodes.map(({properties:s,clips:u,...c})=>({...c,svg:p(o,c.key,{type:c.type,properties:s})}));return{version:1,width:e.width,height:e.height,definitions:o.definitions.join(""),nodes:f}}export{me as renderToSVG,$e as renderTransitionGraphics};
package/src/scene.js CHANGED
@@ -5,9 +5,10 @@ import { resolveMarkItems, findSelectionPolicy } from "./materialization/selecti
5
5
  import { namespaceGraphicId } from "./selectors/graphicCapacity.js";
6
6
  import { findLayer } from "./selectors/layers.js";
7
7
  import { findDataset } from "./selectors/datasets.js";
8
- import { cloneAndFreeze } from "./core/immutable.js";
8
+ import { cloneAndFreeze, isOwned } from "./core/immutable.js";
9
9
 
10
10
  const scenes = new WeakMap(), canvasScenes = new WeakMap();
11
+ const ownedRowTokens = new WeakMap();
11
12
  function canonical(value) {
12
13
  if (value instanceof Date) return `date:${value.toISOString()}`;
13
14
  if (typeof value === "number" && !Number.isFinite(value)) return `number:${value}`;
@@ -37,8 +38,9 @@ function identities(program, project = id => id, scope = "", result = new Map())
37
38
  // derived array indexes. Unresolvable derived provenance deliberately fades.
38
39
  const rowTokens = new WeakMap();
39
40
  const rowToken = row => {
40
- if (!rowTokens.has(row)) rowTokens.set(row, token(row));
41
- return rowTokens.get(row);
41
+ const cache = isOwned(row) ? ownedRowTokens : rowTokens;
42
+ if (!cache.has(row)) cache.set(row, token(row));
43
+ return cache.get(row);
42
44
  };
43
45
  let items;
44
46
  try { items = resolveMarkItems(program, layer.id); }