ggaction 0.0.17 → 0.0.19
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/CHANGELOG.md +96 -0
- package/README.md +6 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +3 -3
- package/src/BasicChartProgram.js +1 -1
- package/src/accessibility.js +3 -2
- package/src/actions/boxPlots/create.js +14 -4
- package/src/actions/boxPlots/edit.js +39 -33
- package/src/actions/boxPlots/materialize.js +7 -3
- package/src/actions/boxPlots/options.js +37 -11
- package/src/actions/canvas/actions.js +70 -95
- package/src/actions/canvas/fitting.js +20 -1
- package/src/actions/canvas/index.js +3 -0
- package/src/actions/canvas/plotBorder.js +28 -0
- package/src/actions/charts/area.js +2 -2
- package/src/actions/charts/bar.js +34 -7
- package/src/actions/charts/ecdf.js +4 -18
- package/src/actions/charts/endpoints.js +4 -18
- package/src/actions/charts/heatmap.js +8 -7
- package/src/actions/charts/index.js +2 -0
- package/src/actions/charts/interval-regression.js +2 -2
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/pie.js +3 -47
- package/src/actions/charts/radial.js +34 -12
- package/src/actions/charts/scatter.js +16 -2
- package/src/actions/charts/shared.js +2 -2
- package/src/actions/charts/text.js +35 -0
- package/src/actions/composition/actions.js +6 -2
- package/src/actions/data/box.js +2 -1
- package/src/actions/data/derived.js +11 -1
- package/src/actions/data/edit.js +3 -1
- package/src/actions/data/index.js +4 -0
- package/src/actions/data/regression.js +25 -16
- package/src/actions/data/regressionStatistics.js +73 -0
- package/src/actions/data/shared.js +8 -4
- package/src/actions/data/stack.js +3 -1
- package/src/actions/distributions/revision.js +24 -18
- package/src/actions/encodings/appearance.js +37 -29
- package/src/actions/encodings/barWidth.js +36 -11
- package/src/actions/encodings/channels.js +0 -1
- package/src/actions/encodings/color/continuous.js +1 -1
- package/src/actions/encodings/color/index.js +1 -1
- package/src/actions/encodings/color/policy.js +10 -3
- package/src/actions/encodings/density/resolve.js +1 -6
- package/src/actions/encodings/position/policies/arc.js +12 -10
- package/src/actions/encodings/position/policies/area.js +3 -3
- package/src/actions/encodings/position/policies/bar.js +15 -6
- package/src/actions/encodings/position/policies/line.js +17 -6
- package/src/actions/encodings/position/resolve.js +3 -2
- package/src/actions/encodings/ranged.js +17 -0
- package/src/actions/encodings/remove.js +14 -8
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/shared.js +26 -24
- package/src/actions/encodings/text.js +6 -3
- package/src/actions/errorBands/create.js +2 -2
- package/src/actions/errorBands/edit.js +2 -1
- package/src/actions/facets/actions.js +176 -247
- package/src/actions/facets/guides.js +2 -1
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axes.js +30 -11
- package/src/actions/guides/axes/axis.js +72 -10
- package/src/actions/guides/axes/components.js +13 -13
- package/src/actions/guides/axes/edit.js +36 -24
- package/src/actions/guides/axes/labels.js +155 -71
- package/src/actions/guides/axes/lines.js +27 -24
- package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
- package/src/actions/guides/axes/parallel/policy.js +8 -7
- package/src/actions/guides/axes/remove.js +13 -8
- package/src/actions/guides/axes/tickGroups.js +11 -7
- package/src/actions/guides/axes/ticks.js +36 -24
- package/src/actions/guides/axes/titles.js +107 -69
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeAxes.js +33 -24
- package/src/actions/guides/facadeLegend.js +12 -4
- package/src/actions/guides/grids/grid.js +8 -2
- package/src/actions/guides/grids/resolve.js +102 -58
- package/src/actions/guides/legends/blocks.js +46 -20
- package/src/actions/guides/legends/categorical/actions.js +95 -19
- package/src/actions/guides/legends/categorical/components.js +15 -18
- package/src/actions/guides/legends/categorical/index.js +8 -0
- package/src/actions/guides/legends/categorical/layout.js +45 -20
- package/src/actions/guides/legends/categorical/options.js +39 -22
- package/src/actions/guides/legends/categorical/recipes.js +7 -3
- package/src/actions/guides/legends/categorical/resolve.js +11 -4
- package/src/actions/guides/legends/categorical/symbols.js +38 -18
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- package/src/actions/guides/legends/colorSize.js +110 -0
- package/src/actions/guides/legends/combine.js +37 -0
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
- package/src/actions/guides/legends/continuous/common.js +1 -447
- package/src/actions/guides/legends/continuous/gradient.js +73 -46
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +33 -21
- package/src/actions/guides/legends/creation.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +8 -7
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/lifecycle.js +2 -1
- package/src/actions/guides/legends/remove.js +9 -3
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +65 -47
- package/src/actions/guides/legends/strokeWidth.js +13 -29
- package/src/actions/guides/legends/target.js +2 -79
- package/src/actions/guides/legends/transition.js +44 -11
- package/src/actions/guides/polar/axes/labels.js +4 -2
- package/src/actions/guides/polar/axes/titles.js +7 -5
- package/src/actions/guides/polar/resolve.js +6 -14
- package/src/actions/guides/tickValues.js +8 -5
- package/src/actions/index.js +16 -16
- package/src/actions/marks/arc/actions.js +3 -10
- package/src/actions/marks/area/actions.js +8 -5
- package/src/actions/marks/area/materialize.js +8 -9
- package/src/actions/marks/bar/create.js +10 -4
- package/src/actions/marks/bar/edit.js +29 -14
- package/src/actions/marks/bar/materialize.js +15 -7
- package/src/actions/marks/line/actions.js +11 -4
- package/src/actions/marks/line/materialize.js +9 -5
- package/src/actions/marks/point/edit.js +1 -1
- package/src/actions/marks/point/materialize.js +5 -1
- package/src/actions/marks/rect/actions.js +3 -3
- package/src/actions/marks/remove.js +12 -10
- package/src/actions/marks/rule/actions.js +7 -4
- package/src/actions/marks/text/actions.js +17 -5
- package/src/actions/marks/text/layout.js +3 -0
- package/src/actions/primitives/createGraphics.js +3 -2
- package/src/actions/primitives/editGraphics.js +15 -11
- package/src/actions/primitives/graphicProperties.js +1 -6
- package/src/actions/primitives/semanticAction.js +27 -1
- package/src/actions/primitives/semanticValidation/guide.js +11 -1
- package/src/actions/primitives/semanticValidation/layer.js +8 -0
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +18 -6
- package/src/actions/regression/resolve.js +2 -6
- package/src/actions/scales/channels.js +9 -4
- package/src/actions/scales/consumers/families.js +16 -5
- package/src/actions/scales/create.js +10 -5
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +3 -3
- package/src/actions/scales/editPolicy.js +12 -3
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +21 -1
- package/src/actions/scales/quantitativeColor.js +8 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +126 -45
- package/src/actions/theme/state.js +12 -0
- package/src/actions/titles/actions.js +5 -18
- package/src/actions/titles/resolve.js +41 -19
- package/src/actions/violinPlots/edit.js +3 -20
- package/src/core/compositionState.js +6 -3
- package/src/core/font.js +9 -2
- package/src/core/graphicProperties.js +1 -0
- package/src/core/optionRequirements.js +1 -0
- package/src/core/programState.js +4 -0
- package/src/core/resourceReferences.js +5 -2
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +35 -0
- package/src/core/vocabulary.js +7 -0
- package/src/grammar/aggregate.js +4 -18
- package/src/grammar/arcs.js +1 -386
- package/src/grammar/areaPaint.js +10 -0
- package/src/grammar/areaSeries.js +18 -10
- package/src/grammar/axisTextPlacement.js +1 -0
- package/src/grammar/bars/geometry.js +30 -3
- package/src/grammar/bars/policy.js +12 -1
- package/src/grammar/bin.js +2 -12
- package/src/grammar/bin2d.js +1 -6
- package/src/grammar/boxPlot.js +56 -9
- package/src/grammar/categoryOrder.js +1 -6
- package/src/grammar/colorSizeLegend.js +39 -0
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +115 -26
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +13 -9
- package/src/grammar/displayLabels.js +36 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +58 -30
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/facets/legendIntent.js +65 -0
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/guideDash.js +1 -0
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +2 -9
- package/src/grammar/legendBlocks.js +1 -0
- package/src/grammar/legendBorder.js +9 -0
- package/src/grammar/legendClip.js +19 -0
- package/src/grammar/legendCombinations.js +45 -0
- package/src/grammar/legendGradient.js +27 -0
- package/src/grammar/legendGroups.js +1 -0
- package/src/grammar/legendOpacity.js +6 -0
- package/src/grammar/legendTitleStyle.js +22 -0
- package/src/grammar/lineSeries.js +32 -17
- package/src/grammar/markLabels.js +5 -2
- package/src/grammar/normalize.js +4 -54
- package/src/grammar/numeric.js +1 -290
- package/src/grammar/parallelCoordinates.js +1 -6
- package/src/grammar/parseTime.js +41 -0
- package/src/grammar/pathCommands.js +11 -4
- package/src/grammar/pointShapes.js +16 -2
- package/src/grammar/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +16 -2
- package/src/grammar/regression/models.js +142 -54
- package/src/grammar/regression/parameters.js +23 -4
- package/src/grammar/regression/statistics.js +64 -0
- package/src/grammar/roundedRect.js +42 -54
- package/src/grammar/scales/appearance.js +1 -206
- package/src/grammar/scales/color.js +1 -382
- package/src/grammar/scales/colorConsumers.js +5 -2
- package/src/grammar/scales/definition.js +1 -197
- package/src/grammar/scales/discretized.js +1 -195
- package/src/grammar/scales/size.js +1 -456
- package/src/grammar/scales/temporal.js +2 -13
- package/src/grammar/scales/transformed.js +1 -383
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/scales/validation.js +1 -66
- package/src/grammar/schemas/concreteGraphic.js +14 -0
- package/src/grammar/schemas/graphic.js +6 -0
- package/src/grammar/schemas/graphicBounds.js +31 -7
- package/src/grammar/schemas/semanticPath.js +19 -3
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +4 -27
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +22 -11
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +15 -2
- package/src/grammar/valueFormat.js +17 -5
- package/src/grammar/weightedStatistics.js +1 -6
- package/src/grammar/window.js +4 -58
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/canvas.js +1 -110
- package/src/layout/facets.js +1 -244
- package/src/layout/guideCollisions.js +9 -1
- package/src/layout/labels.js +1 -0
- package/src/layout/legend.js +13 -0
- package/src/layout/legendGroup.js +1 -0
- package/src/layout/legendItems.js +59 -15
- package/src/layout/legendLane.js +1 -441
- package/src/layout/legendText.js +1 -0
- package/src/layout/title.js +35 -13
- package/src/materialization/bars/aggregate.js +11 -5
- package/src/materialization/bars/histogram.js +4 -1
- package/src/materialization/bars/ranged.js +19 -6
- package/src/materialization/bars/resolve.js +22 -1
- package/src/materialization/compositionSnapshot.js +2 -7
- package/src/materialization/coordinateBounds.js +21 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/edit.js +30 -0
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/owner.js +125 -0
- package/src/materialization/facetGuides/ownerLayout.js +71 -0
- package/src/materialization/facetGuides/placement.js +71 -14
- package/src/materialization/facetGuides/preparation.js +22 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +5 -1
- package/src/materialization/graphicTranslation.js +73 -0
- package/src/materialization/guides/layout.js +1 -57
- package/src/materialization/guides/resources.js +1 -123
- package/src/materialization/layout.js +53 -0
- package/src/materialization/legendBorder.js +28 -0
- package/src/materialization/legendClip.js +76 -0
- package/src/materialization/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +2 -2
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +14 -15
- package/src/materialization/scales/map.js +6 -0
- package/src/materialization/scales/policies/arc.js +8 -7
- package/src/materialization/scales/policies/bar.js +19 -7
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +7 -3
- package/src/materialization/selection/items/arc.js +1 -1
- package/src/materialization/selection/items/bar.js +7 -6
- package/src/materialization/selection/items/path.js +7 -6
- package/src/materialization/selection/items/rect.js +2 -2
- package/src/materialization/text.js +13 -4
- package/src/materialization/textLines.js +15 -0
- package/src/materialization/typography.js +10 -2
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +220 -0
- package/src/persistence.js +19 -6
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +22 -11
- package/src/renderers/canvas/isolated.js +166 -0
- package/src/renderers/canvas/limits.js +3 -0
- package/src/renderers/canvas/line.js +1 -0
- package/src/renderers/canvas/path.js +1 -1
- package/src/renderers/canvas/rect.js +11 -8
- package/src/renderers/canvas/stroke.js +2 -1
- package/src/renderers/canvas/text.js +5 -3
- package/src/renderers/pdf.js +2 -2
- package/src/renderers/png.js +2 -2
- package/src/renderers/svg.js +14 -4
- package/src/selectors/axes.js +1 -0
- package/src/selectors/graphicCapacity.js +38 -0
- package/src/selectors/layers.js +19 -0
- package/src/theme/defaults.js +8 -2
- package/src/version.js +1 -1
- package/types/basic.d.ts +3 -0
- package/types/index.d.ts +12 -0
- package/types/program.d.ts +536 -129
package/src/persistence.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ChartProgram } from "./ChartProgram.js";
|
|
2
2
|
import { BasicChartProgram } from "./BasicChartProgram.js";
|
|
3
|
-
import { cloneAndFreeze } from "./core/immutable.js";
|
|
3
|
+
import { cloneAndFreeze, freezeOwned } from "./core/immutable.js";
|
|
4
4
|
import { hasRegisteredExtension, isBuiltinProgramClass } from "./core/extensionRegistry.js";
|
|
5
5
|
import { packageVersion } from "./version.js";
|
|
6
|
-
import { decodeValue, encodeValue, invalidSnapshot } from "./persistence/codec.js";
|
|
6
|
+
import { decodeValue, encodeValue, encodeSharedValue, invalidSnapshot } from "./persistence/codec.js";
|
|
7
7
|
import { exactKeys, requireObject, STATE_KEYS, validateGraphic, validateProgramState, validateTrace } from "./persistence/validation.js";
|
|
8
8
|
import {
|
|
9
9
|
deriveTransformSchema,
|
|
@@ -25,7 +25,7 @@ function canonicalState(program, extensions, path = "payload") {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function envelope(kind, payload, extensions = []) {
|
|
28
|
-
return JSON.stringify({ schemaVersion: kind === "editable" ?
|
|
28
|
+
return JSON.stringify({ schemaVersion: kind === "editable" ? 3 : 1, kind, packageVersion, extensions, payload: kind === "editable" ? encodeSharedValue(payload) : encodeValue(payload) });
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function readEnvelope(text, kind) {
|
|
@@ -33,7 +33,7 @@ function readEnvelope(text, kind) {
|
|
|
33
33
|
let value;
|
|
34
34
|
try { value = JSON.parse(text); } catch { throw invalidSnapshot("Snapshot must be valid JSON.", "snapshot"); }
|
|
35
35
|
exactKeys(value, ["schemaVersion", "kind", "packageVersion", "extensions", "payload"], "snapshot");
|
|
36
|
-
const versions = kind === "editable" ? [1, 2] : [1];
|
|
36
|
+
const versions = kind === "editable" ? [1, 2, 3] : [1];
|
|
37
37
|
if (!versions.includes(value.schemaVersion) || value.kind !== kind) {
|
|
38
38
|
throw invalidSnapshot(
|
|
39
39
|
`Unsupported snapshot schema or kind; expected schema ${versions.join(" or ")} ${kind}.`,
|
|
@@ -45,6 +45,9 @@ function readEnvelope(text, kind) {
|
|
|
45
45
|
value.extensions.some(name => typeof name !== "string" || !hasRegisteredExtension(name)) || (kind === "graphic" && value.extensions.length)) {
|
|
46
46
|
throw invalidSnapshot("Snapshot requires unknown, duplicate, or inapplicable extensions.", "snapshot.extensions");
|
|
47
47
|
}
|
|
48
|
+
if (value.schemaVersion < 3 && value.payload?.[0] === "shared") {
|
|
49
|
+
throw invalidSnapshot("Shared array payloads require editable snapshot version 3.", "snapshot.payload");
|
|
50
|
+
}
|
|
48
51
|
return { ...value, payload: decodeValue(value.payload) };
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -64,7 +67,7 @@ function restoreState(state, extensions, path = "payload", { requireSchemas = fa
|
|
|
64
67
|
if (requireSchemas) {
|
|
65
68
|
for (const dataset of program.semanticSpec.datasets) {
|
|
66
69
|
if (dataset.schema === undefined) {
|
|
67
|
-
throw invalidSnapshot(`Dataset "${dataset.id}" requires schema in editable snapshot version 2.`, `${path}.semanticSpec.datasets`);
|
|
70
|
+
throw invalidSnapshot(`Dataset "${dataset.id}" requires schema in editable snapshot version 2 or later.`, `${path}.semanticSpec.datasets`);
|
|
68
71
|
}
|
|
69
72
|
validateDatasetSchema(dataset.schema);
|
|
70
73
|
}
|
|
@@ -93,7 +96,7 @@ function migrateStateSchemas(state) {
|
|
|
93
96
|
sourceSchema,
|
|
94
97
|
dataset.transform?.[0] ?? {},
|
|
95
98
|
dataset.values,
|
|
96
|
-
{ sourceId: dataset.source, ownerId: dataset.id }
|
|
99
|
+
{ sourceId: dataset.source, ownerId: dataset.id, sourceTransform: byId.get(dataset.source)?.transform?.[0] }
|
|
97
100
|
);
|
|
98
101
|
}
|
|
99
102
|
active.delete(id);
|
|
@@ -118,6 +121,15 @@ export function serializeProgram(program) {
|
|
|
118
121
|
return envelope("editable", canonicalState(program, extensions), [...extensions].sort());
|
|
119
122
|
}
|
|
120
123
|
|
|
124
|
+
// Decoding creates fresh plain containers. Own them once across the whole graph
|
|
125
|
+
// so child constructors retain shared immutable data instead of copying each pool.
|
|
126
|
+
function ownDecodedState(value, seen = new WeakSet()) {
|
|
127
|
+
if (value === null || typeof value !== "object" || seen.has(value)) return;
|
|
128
|
+
seen.add(value);
|
|
129
|
+
for (const child of Object.values(value)) ownDecodedState(child, seen);
|
|
130
|
+
freezeOwned(value);
|
|
131
|
+
}
|
|
132
|
+
|
|
121
133
|
export function deserializeProgram(text) {
|
|
122
134
|
const stored = readEnvelope(text, "editable");
|
|
123
135
|
const extensions = new Set();
|
|
@@ -128,6 +140,7 @@ export function deserializeProgram(text) {
|
|
|
128
140
|
restoreState(payload, new Set());
|
|
129
141
|
payload = migrateStateSchemas(payload);
|
|
130
142
|
}
|
|
143
|
+
ownDecodedState(payload);
|
|
131
144
|
const program = restoreState(payload, extensions, "payload", { requireSchemas: true });
|
|
132
145
|
if (extensions.size !== stored.extensions.length || stored.extensions.some(name => !extensions.has(name))) {
|
|
133
146
|
throw invalidSnapshot("Snapshot extensions must exactly match its action traces.", "snapshot.extensions");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveCanvasSolidPaint } from "./fill.js";
|
|
1
2
|
import { requireFiniteProperty } from "./validation.js";
|
|
2
3
|
import { validateConcreteGraphicProperties } from
|
|
3
4
|
"../../grammar/schemas/concreteGraphic.js";
|
|
@@ -25,7 +26,7 @@ function drawCircle(context, child, collectionId) {
|
|
|
25
26
|
throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
context.fillStyle = properties.fill;
|
|
29
|
+
context.fillStyle = resolveCanvasSolidPaint(properties.fill);
|
|
29
30
|
context.globalAlpha = opacity;
|
|
30
31
|
context.beginPath();
|
|
31
32
|
context.arc(x, y, radius, 0, Math.PI * 2);
|
|
@@ -44,8 +45,10 @@ function drawCircle(context, child, collectionId) {
|
|
|
44
45
|
`Graphic "${graphicId}" requires a non-negative strokeWidth.`
|
|
45
46
|
);
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
if (strokeWidth > 0) {
|
|
49
|
+
applyCanvasStroke(context, properties);
|
|
50
|
+
context.stroke();
|
|
51
|
+
}
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -4,10 +4,16 @@ import {
|
|
|
4
4
|
} from "../../grammar/paint.js";
|
|
5
5
|
import { resolveCanvasGradientCoordinates } from "./native.js";
|
|
6
6
|
|
|
7
|
+
// SVG's no-paint keyword is not a valid Canvas color assignment. Assign a real
|
|
8
|
+
// transparent color so the backend cannot retain a previous opaque paint.
|
|
9
|
+
export function resolveCanvasSolidPaint(paint) {
|
|
10
|
+
return paint === "none" ? "rgba(0,0,0,0)" : paint;
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
export function applyCanvasFill(context, fill, bounds, graphicId) {
|
|
8
14
|
validateFillPaint(fill, `${graphicId}.fill`);
|
|
9
15
|
if (!isLinearGradientPaint(fill)) {
|
|
10
|
-
context.fillStyle = fill;
|
|
16
|
+
context.fillStyle = resolveCanvasSolidPaint(fill);
|
|
11
17
|
return fill;
|
|
12
18
|
}
|
|
13
19
|
if (typeof context.createLinearGradient !== "function") {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { MAX_RASTER_DIMENSION, MAX_RASTER_PIXELS } from "./limits.js";
|
|
2
|
+
import { prepareIsolatedGlyphs, drawIsolatedGlyph } from "./isolated.js";
|
|
3
|
+
import { resolveCanvasSolidPaint } from "./fill.js";
|
|
1
4
|
import { annotateError } from "../../core/diagnostics.js";
|
|
2
5
|
import { drawCircleGraphic } from "./circle.js";
|
|
3
6
|
import { drawLineGraphic } from "./line.js";
|
|
@@ -39,8 +42,6 @@ const DRAWING_CONTEXT_METHODS = Object.freeze([
|
|
|
39
42
|
"rotate",
|
|
40
43
|
"fillText"
|
|
41
44
|
]);
|
|
42
|
-
const MAX_RASTER_DIMENSION = 32_767;
|
|
43
|
-
const MAX_RASTER_PIXELS = 16_777_216;
|
|
44
45
|
|
|
45
46
|
function requireDrawingContext(context) {
|
|
46
47
|
if (context === null || typeof context !== "object") {
|
|
@@ -81,7 +82,7 @@ function fillCanvasBackground(context, id, canvas, width, height) {
|
|
|
81
82
|
);
|
|
82
83
|
}
|
|
83
84
|
context.globalAlpha = 1;
|
|
84
|
-
context.fillStyle = canvas.properties.background;
|
|
85
|
+
context.fillStyle = resolveCanvasSolidPaint(canvas.properties.background);
|
|
85
86
|
context.fillRect(0, 0, width, height);
|
|
86
87
|
}
|
|
87
88
|
|
|
@@ -143,9 +144,10 @@ export function resolveGraphicRenderTarget(graphicSpec) {
|
|
|
143
144
|
});
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
export function drawResolvedGraphicSpec(target, context) {
|
|
147
|
+
export function drawResolvedGraphicSpec(target, context, options = {}) {
|
|
147
148
|
requireDrawingContext(context);
|
|
148
149
|
const { graphicSpec, canvasId, canvas, width, height } = target;
|
|
150
|
+
const isolated = options.isolated ?? prepareIsolatedGlyphs(target, context, options);
|
|
149
151
|
|
|
150
152
|
fillCanvasBackground(context, canvasId, canvas, width, height);
|
|
151
153
|
|
|
@@ -161,14 +163,14 @@ export function drawResolvedGraphicSpec(target, context) {
|
|
|
161
163
|
return;
|
|
162
164
|
}
|
|
163
165
|
if (!Array.isArray(object.items)) {
|
|
164
|
-
drawConcreteGraphic(context, id, object);
|
|
166
|
+
drawConcreteGraphic(context, id, object, isolated);
|
|
165
167
|
}
|
|
166
168
|
},
|
|
167
169
|
item({ id, object, owner }) {
|
|
168
170
|
drawConcreteGraphic(context, id, {
|
|
169
171
|
...object,
|
|
170
172
|
type: object.type ?? owner.type
|
|
171
|
-
});
|
|
173
|
+
}, isolated);
|
|
172
174
|
},
|
|
173
175
|
exit({ id, object }) {
|
|
174
176
|
if (
|
|
@@ -181,7 +183,7 @@ export function drawResolvedGraphicSpec(target, context) {
|
|
|
181
183
|
});
|
|
182
184
|
}
|
|
183
185
|
|
|
184
|
-
export function
|
|
186
|
+
export function renderCanvasProgram(program, context, { pixelRatio = 1 } = {}, createCanvas) {
|
|
185
187
|
const graphicSpec = requireProgramGraphicSpec(program);
|
|
186
188
|
|
|
187
189
|
requireCanvasContext(context);
|
|
@@ -219,6 +221,7 @@ export function render(program, context, { pixelRatio = 1 } = {}) {
|
|
|
219
221
|
), { code: "resource-limit", operation: "render", optionPath: "physicalPixels", limit: MAX_RASTER_PIXELS, actual: physicalWidth * physicalHeight });
|
|
220
222
|
}
|
|
221
223
|
preflightCanvasGraphicSpec(target, pixelRatio);
|
|
224
|
+
const isolated = prepareIsolatedGlyphs(target, context, {pixelRatio, createCanvas});
|
|
222
225
|
|
|
223
226
|
context.canvas.width = physicalWidth;
|
|
224
227
|
context.canvas.height = physicalHeight;
|
|
@@ -234,16 +237,20 @@ export function render(program, context, { pixelRatio = 1 } = {}) {
|
|
|
234
237
|
try {
|
|
235
238
|
context.scale(pixelRatio, pixelRatio);
|
|
236
239
|
context.clearRect(0, 0, width, height);
|
|
237
|
-
drawResolvedGraphicSpec(target, context);
|
|
240
|
+
drawResolvedGraphicSpec(target, context, {isolated, pixelRatio, createCanvas});
|
|
238
241
|
} finally {
|
|
239
242
|
context.restore();
|
|
240
243
|
}
|
|
241
244
|
}
|
|
242
245
|
|
|
243
|
-
function
|
|
246
|
+
export function render(program, context, options) {
|
|
247
|
+
return renderCanvasProgram(program, context, options);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function drawConcreteGraphic(context, id, graphic, isolated) {
|
|
244
251
|
if (graphic.type === "collection") {
|
|
245
252
|
for (const item of graphic.items ?? []) {
|
|
246
|
-
drawConcreteGraphic(context, item.id ?? id, item);
|
|
253
|
+
drawConcreteGraphic(context, item.id ?? id, item, isolated);
|
|
247
254
|
}
|
|
248
255
|
return;
|
|
249
256
|
}
|
|
@@ -251,5 +258,9 @@ function drawConcreteGraphic(context, id, graphic) {
|
|
|
251
258
|
if (draw === undefined) {
|
|
252
259
|
throw new Error(`Canvas renderer does not support "${graphic.type}" yet.`);
|
|
253
260
|
}
|
|
254
|
-
|
|
261
|
+
if (["normal","difference"].includes(graphic.properties?.blend)) {
|
|
262
|
+
drawIsolatedGlyph(context, id, graphic, draw, isolated);
|
|
263
|
+
} else {
|
|
264
|
+
draw(context, id, graphic);
|
|
265
|
+
}
|
|
255
266
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { resolvePrimitiveGraphicBounds } from "../../grammar/schemas/graphicBounds.js";
|
|
2
|
+
import { walkGraphicTreeEvents } from "../../grammar/schemas/graphicTree.js";
|
|
3
|
+
import { MAX_RASTER_DIMENSION, MAX_RASTER_PIXELS } from "./limits.js";
|
|
4
|
+
|
|
5
|
+
function canvasFactory(context, supplied) {
|
|
6
|
+
if (supplied !== undefined) return supplied;
|
|
7
|
+
const canvas = context.canvas;
|
|
8
|
+
if (typeof canvas?.ownerDocument?.createElement === "function") {
|
|
9
|
+
return (width, height) => {
|
|
10
|
+
const source = canvas.ownerDocument.createElement("canvas");
|
|
11
|
+
source.width = width;
|
|
12
|
+
source.height = height;
|
|
13
|
+
return source;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
if (canvas?.constructor && canvas.constructor !== Object) {
|
|
17
|
+
return (width, height) => new canvas.constructor(width, height);
|
|
18
|
+
}
|
|
19
|
+
throw new TypeError("Isolated glyph painting requires a compatible Canvas surface.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function requireSurface(surface) {
|
|
23
|
+
const context = surface?.getContext?.("2d");
|
|
24
|
+
if (!context || ["save", "restore", "scale", "translate", "beginPath", "fillRect",
|
|
25
|
+
"fill", "stroke", "moveTo", "lineTo", "bezierCurveTo", "closePath", "setLineDash"]
|
|
26
|
+
.some(method => typeof context[method] !== "function")) {
|
|
27
|
+
throw new TypeError("Isolated glyph painting requires a compatible Canvas 2D surface.");
|
|
28
|
+
}
|
|
29
|
+
return context;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// This is renderer-private state: no surface or backend object enters graphicSpec.
|
|
33
|
+
export function prepareIsolatedGlyphs(target, context, { pixelRatio = 1, createCanvas } = {}) {
|
|
34
|
+
const requested = Object.values(target.graphicSpec.objects).some(object =>
|
|
35
|
+
["normal","difference"].includes(object.properties?.blend) ||
|
|
36
|
+
object.items?.some(item => ["normal","difference"].includes(item.properties?.blend)));
|
|
37
|
+
if (!requested) return undefined;
|
|
38
|
+
if (typeof context.drawImage !== "function" || typeof context.globalCompositeOperation !== "string") {
|
|
39
|
+
throw new TypeError("Isolated glyph painting requires Canvas drawImage and compositing support.");
|
|
40
|
+
}
|
|
41
|
+
const factory = canvasFactory(context, createCanvas);
|
|
42
|
+
let surface;
|
|
43
|
+
try {
|
|
44
|
+
surface = factory(1, 1);
|
|
45
|
+
const source = requireSurface(surface);
|
|
46
|
+
const hasGradient = Object.values(target.graphicSpec.objects).some(object =>
|
|
47
|
+
[object, ...(object.items ?? [])].some(item =>
|
|
48
|
+
["normal","difference"].includes(item.properties?.blend) && item.properties.fill?.type === "linear-gradient"));
|
|
49
|
+
const normal=Object.values(target.graphicSpec.objects).some(object=>
|
|
50
|
+
[object,...(object.items??[])].some(item=>item.properties?.blend==="normal"));
|
|
51
|
+
if(normal){
|
|
52
|
+
if(typeof source.setTransform!=="function")throw new TypeError("Normal glyph opacity requires Canvas transform support.");
|
|
53
|
+
source.save();try{
|
|
54
|
+
source.globalCompositeOperation="destination-in";
|
|
55
|
+
if(source.globalCompositeOperation!=="destination-in")throw new TypeError("Normal glyph opacity requires source attenuation.");
|
|
56
|
+
}finally{source.restore();}
|
|
57
|
+
const circle=Object.values(target.graphicSpec.objects).some(object=>
|
|
58
|
+
[object,...(object.items??[])].some(item=>item.properties?.blend==="normal"&&(item.type??object.type)==="circle"));
|
|
59
|
+
if(circle&&typeof source.arc!=="function")throw new TypeError("Normal circle painting requires Canvas arcs.");
|
|
60
|
+
}
|
|
61
|
+
if (hasGradient && (typeof source.createLinearGradient !== "function" ||
|
|
62
|
+
typeof source.createLinearGradient(0, 0, 1, 0)?.addColorStop !== "function")) {
|
|
63
|
+
throw new TypeError("Isolated gradient painting requires Canvas linear gradients.");
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new TypeError("Isolated glyph painting requires a compatible Canvas surface.", {cause:error});
|
|
67
|
+
}
|
|
68
|
+
context.save();
|
|
69
|
+
try {
|
|
70
|
+
const modes=new Set();
|
|
71
|
+
for(const object of Object.values(target.graphicSpec.objects))for(const item of [object,...(object.items??[])]){
|
|
72
|
+
if(item.properties?.blend!==undefined)modes.add(item.properties.blend==="normal"?"source-over":item.properties.blend);
|
|
73
|
+
}
|
|
74
|
+
for(const mode of modes){
|
|
75
|
+
context.globalCompositeOperation=mode;
|
|
76
|
+
if(context.globalCompositeOperation!==mode)throw new TypeError(`Canvas context does not support ${mode} compositing.`);
|
|
77
|
+
}
|
|
78
|
+
} finally { context.restore(); }
|
|
79
|
+
|
|
80
|
+
const plans = new Map();
|
|
81
|
+
// Ordered root graphics may be siblings of the Canvas, not its descendants.
|
|
82
|
+
const frames = [{offset:[0,0],clip:{left:0,top:0,right:target.width,bottom:target.height}}];
|
|
83
|
+
function plan(id, object, type) {
|
|
84
|
+
const properties = object.properties;
|
|
85
|
+
if (!["normal","difference"].includes(properties?.blend)) return;
|
|
86
|
+
const bounds = resolvePrimitiveGraphicBounds(type, properties);
|
|
87
|
+
if (!bounds) throw new Error(`Graphic "${id}" requires finite isolated paint bounds.`);
|
|
88
|
+
const {offset: [x, y], clip} = frames.at(-1);
|
|
89
|
+
const clipped = {
|
|
90
|
+
left: Math.max(bounds.left + x, clip.left),
|
|
91
|
+
right: Math.min(bounds.right + x, clip.right),
|
|
92
|
+
top: Math.max(bounds.top + y, clip.top),
|
|
93
|
+
bottom: Math.min(bounds.bottom + y, clip.bottom)
|
|
94
|
+
};
|
|
95
|
+
if (clipped.right <= clipped.left || clipped.bottom <= clipped.top) {
|
|
96
|
+
plans.set(id, {empty: true});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Align to destination pixels, including fractional nested translations.
|
|
100
|
+
const left = Math.max(Math.floor(clip.left * pixelRatio), Math.floor(clipped.left * pixelRatio) - 1);
|
|
101
|
+
const top = Math.max(Math.floor(clip.top * pixelRatio), Math.floor(clipped.top * pixelRatio) - 1);
|
|
102
|
+
const right = Math.min(Math.ceil(clip.right * pixelRatio), Math.ceil(clipped.right * pixelRatio) + 1);
|
|
103
|
+
const bottom = Math.min(Math.ceil(clip.bottom * pixelRatio), Math.ceil(clipped.bottom * pixelRatio) + 1);
|
|
104
|
+
const width = right - left, height = bottom - top;
|
|
105
|
+
if (![width, height].every(Number.isSafeInteger) || width < 1 || height < 1 ||
|
|
106
|
+
width > MAX_RASTER_DIMENSION || height > MAX_RASTER_DIMENSION ||
|
|
107
|
+
width * height > MAX_RASTER_PIXELS) {
|
|
108
|
+
throw new RangeError(`Graphic "${id}" isolated paint surface exceeds the raster allocation limit.`);
|
|
109
|
+
}
|
|
110
|
+
plans.set(id, {width, height, left: left / pixelRatio - x, top: top / pixelRatio - y});
|
|
111
|
+
}
|
|
112
|
+
walkGraphicTreeEvents(target.graphicSpec, {
|
|
113
|
+
enter({id, object}) {
|
|
114
|
+
if (object.type === "canvas") {
|
|
115
|
+
const parent = frames.at(-1);
|
|
116
|
+
const offset = id === target.canvasId ? [0, 0] :
|
|
117
|
+
[parent.offset[0] + object.properties.x, parent.offset[1] + object.properties.y];
|
|
118
|
+
const properties = object.properties;
|
|
119
|
+
const local = {left: offset[0], top: offset[1],
|
|
120
|
+
right: offset[0] + properties.width, bottom: offset[1] + properties.height};
|
|
121
|
+
const clip = {
|
|
122
|
+
left: Math.max(parent.clip.left, local.left), top: Math.max(parent.clip.top, local.top),
|
|
123
|
+
right: Math.min(parent.clip.right, local.right), bottom: Math.min(parent.clip.bottom, local.bottom)
|
|
124
|
+
};
|
|
125
|
+
frames.push({offset, clip});
|
|
126
|
+
}
|
|
127
|
+
if (!object.items) plan(id, object, object.type);
|
|
128
|
+
},
|
|
129
|
+
item({id, object, owner}) { plan(id, object, object.type ?? owner.type); },
|
|
130
|
+
exit({object}) { if (object.type === "canvas") frames.pop(); }
|
|
131
|
+
});
|
|
132
|
+
return {plans, surface, pixelRatio};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Paint fill and outline together with opacity 1, then composite the source once.
|
|
136
|
+
export function drawIsolatedGlyph(context, id, graphic, draw, prepared) {
|
|
137
|
+
const plan = prepared?.plans.get(id);
|
|
138
|
+
if (!plan) throw new Error(`Graphic "${id}" has no prepared isolated paint source.`);
|
|
139
|
+
if (plan.empty) return;
|
|
140
|
+
const {surface, pixelRatio} = prepared;
|
|
141
|
+
surface.width = plan.width;
|
|
142
|
+
surface.height = plan.height;
|
|
143
|
+
const source = requireSurface(surface);
|
|
144
|
+
source.scale(pixelRatio, pixelRatio);
|
|
145
|
+
source.translate(-plan.left, -plan.top);
|
|
146
|
+
const {blend, opacity = 1, ...properties} = graphic.properties;
|
|
147
|
+
draw(source, id, {...graphic, properties: {...properties, opacity: 1}});
|
|
148
|
+
if(blend==="normal"){
|
|
149
|
+
// Attenuate the complete source once. drawImage(globalAlpha) introduces
|
|
150
|
+
// additional native image-alpha quantization; source masking avoids it.
|
|
151
|
+
source.save();
|
|
152
|
+
try{
|
|
153
|
+
source.setTransform(1,0,0,1,0,0);
|
|
154
|
+
source.globalCompositeOperation="destination-in";
|
|
155
|
+
source.globalAlpha=opacity;
|
|
156
|
+
source.fillStyle="#fff";
|
|
157
|
+
source.fillRect(0,0,plan.width,plan.height);
|
|
158
|
+
}finally{source.restore();}
|
|
159
|
+
}
|
|
160
|
+
context.save();
|
|
161
|
+
try {
|
|
162
|
+
context.globalAlpha = blend==="normal"?1:opacity;
|
|
163
|
+
context.globalCompositeOperation = blend==="normal"?"source-over":blend;
|
|
164
|
+
context.drawImage(surface, plan.left, plan.top, plan.width / pixelRatio, plan.height / pixelRatio);
|
|
165
|
+
} finally { context.restore(); }
|
|
166
|
+
}
|
|
@@ -33,6 +33,7 @@ function drawLine(context, child, collectionId) {
|
|
|
33
33
|
throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
if (strokeWidth === 0) return;
|
|
36
37
|
context.globalAlpha = opacity;
|
|
37
38
|
applyCanvasStroke(context, properties, properties.strokeDash ?? []);
|
|
38
39
|
context.beginPath();
|
|
@@ -82,7 +82,7 @@ function drawPath(context, child, collectionId) {
|
|
|
82
82
|
applyCanvasFill(context, properties.fill, bounds, graphicId);
|
|
83
83
|
context.fill();
|
|
84
84
|
}
|
|
85
|
-
if (hasStroke) {
|
|
85
|
+
if (hasStroke && strokeWidth > 0) {
|
|
86
86
|
applyCanvasStroke(context, properties, strokeDash);
|
|
87
87
|
context.stroke();
|
|
88
88
|
}
|
|
@@ -43,14 +43,17 @@ function drawRect(context, child, collectionId) {
|
|
|
43
43
|
bottom: y + height
|
|
44
44
|
}, graphicId);
|
|
45
45
|
context.fillRect(x, y, width, height);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
// Canvas ignores lineWidth = 0 and would retain a previous nonzero width.
|
|
47
|
+
if (strokeWidth > 0) {
|
|
48
|
+
applyCanvasStroke(context, properties);
|
|
49
|
+
context.beginPath();
|
|
50
|
+
context.moveTo(x, y);
|
|
51
|
+
context.lineTo(x + width, y);
|
|
52
|
+
context.lineTo(x + width, y + height);
|
|
53
|
+
context.lineTo(x, y + height);
|
|
54
|
+
context.closePath();
|
|
55
|
+
context.stroke();
|
|
56
|
+
}
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
export function drawRectGraphic(context, id, graphic) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { resolveCanvasSolidPaint } from "./fill.js";
|
|
1
2
|
import { resolveStrokeDetails } from "../../grammar/strokeStyle.js";
|
|
2
3
|
|
|
3
4
|
export function applyCanvasStroke(context, properties, strokeDash = []) {
|
|
4
5
|
const details = resolveStrokeDetails(properties);
|
|
5
|
-
context.strokeStyle = properties.stroke;
|
|
6
|
+
context.strokeStyle = resolveCanvasSolidPaint(properties.stroke);
|
|
6
7
|
context.lineWidth = properties.strokeWidth;
|
|
7
8
|
context.lineCap = details.lineCap;
|
|
8
9
|
context.lineJoin = details.lineJoin;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveCanvasSolidPaint } from "./fill.js";
|
|
1
2
|
import { requireFiniteProperty, requireStringProperty } from "./validation.js";
|
|
2
3
|
import { validateConcreteGraphicProperties } from
|
|
3
4
|
"../../grammar/schemas/concreteGraphic.js";
|
|
@@ -72,15 +73,16 @@ function drawText(context, child, collectionId) {
|
|
|
72
73
|
context.save();
|
|
73
74
|
|
|
74
75
|
try {
|
|
75
|
-
context.fillStyle = fill;
|
|
76
|
+
context.fillStyle = resolveCanvasSolidPaint(fill);
|
|
76
77
|
context.globalAlpha = opacity;
|
|
77
78
|
const rendererFontWeight = normalizeRendererFontWeight(fontWeight);
|
|
78
|
-
context.font = `${rendererFontWeight} ${fontSize}px ${fontFamily}`;
|
|
79
|
+
context.font = `${properties.fontStyle === "italic" ? "italic " : ""}${rendererFontWeight} ${fontSize}px ${fontFamily}`;
|
|
79
80
|
context.textAlign = textAlign;
|
|
80
81
|
context.textBaseline = textBaseline;
|
|
81
82
|
context.translate(x, y);
|
|
82
83
|
context.rotate(rotation);
|
|
83
|
-
context.fillText(text, 0, 0);
|
|
84
|
+
if (properties.lines === undefined) context.fillText(text, 0, 0);
|
|
85
|
+
else for (const line of properties.lines) context.fillText(line.text, line.x, line.y);
|
|
84
86
|
} finally {
|
|
85
87
|
context.restore();
|
|
86
88
|
}
|
package/src/renderers/pdf.js
CHANGED
|
@@ -73,10 +73,10 @@ export async function renderToPDFBuffer(program, options = {}) {
|
|
|
73
73
|
const target = resolveGraphicRenderTarget(graphicSpec);
|
|
74
74
|
requirePDFPageDimensions(target);
|
|
75
75
|
preflightCanvasGraphicSpec(target);
|
|
76
|
-
const { PDFDocument } = await loadNativeBackend();
|
|
76
|
+
const { PDFDocument, createCanvas } = await loadNativeBackend();
|
|
77
77
|
const document = new PDFDocument(metadata);
|
|
78
78
|
const context = document.beginPage(target.width, target.height);
|
|
79
|
-
drawResolvedGraphicSpec(target, context);
|
|
79
|
+
drawResolvedGraphicSpec(target, context, {createCanvas});
|
|
80
80
|
document.endPage();
|
|
81
81
|
const buffer = document.close();
|
|
82
82
|
return Object.freeze({
|
package/src/renderers/png.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { renderCanvasProgram } from "./canvas/index.js";
|
|
4
4
|
import { validateRendererOptions } from "./options.js";
|
|
5
5
|
import { loadNativeBackend } from "./nativeBackend.js";
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ export async function renderToPNGBuffer(program, options = {}) {
|
|
|
12
12
|
const { pixelRatio = 1 } = options;
|
|
13
13
|
const { createCanvas } = await loadNativeBackend();
|
|
14
14
|
const canvas = createCanvas(1, 1);
|
|
15
|
-
|
|
15
|
+
renderCanvasProgram(program, canvas.getContext("2d"), { pixelRatio }, createCanvas);
|
|
16
16
|
// Canvas drawing is synchronous; encoding uses the native async worker.
|
|
17
17
|
const buffer = await canvas.encode("png");
|
|
18
18
|
return Object.freeze({
|
package/src/renderers/svg.js
CHANGED
|
@@ -265,7 +265,8 @@ function serializeRect(state, graphicId, properties) {
|
|
|
265
265
|
["stroke", properties.stroke],
|
|
266
266
|
["stroke-width", formatNumber(strokeWidth)],
|
|
267
267
|
...strokeDetailAttributes(properties, strokeWidth),
|
|
268
|
-
["opacity", formatNumber(requireOpacity(properties, graphicId))]
|
|
268
|
+
["opacity", formatNumber(requireOpacity(properties, graphicId))],
|
|
269
|
+
["style", properties.blend === "difference" ? "mix-blend-mode:difference" : undefined]
|
|
269
270
|
]);
|
|
270
271
|
}
|
|
271
272
|
|
|
@@ -362,7 +363,8 @@ function serializePath(state, graphicId, properties) {
|
|
|
362
363
|
["stroke-dasharray", strokeDash.length === 0
|
|
363
364
|
? undefined
|
|
364
365
|
: strokeDash.map(formatNumber).join(" ")],
|
|
365
|
-
["opacity", formatNumber(requireOpacity(properties, graphicId))]
|
|
366
|
+
["opacity", formatNumber(requireOpacity(properties, graphicId))],
|
|
367
|
+
["style", properties.blend === "difference" ? "mix-blend-mode:difference" : undefined]
|
|
366
368
|
]);
|
|
367
369
|
}
|
|
368
370
|
|
|
@@ -419,13 +421,16 @@ function serializeText(state, graphicId, properties) {
|
|
|
419
421
|
["font-family", fontFamily],
|
|
420
422
|
["font-size", formatNumber(fontSize)],
|
|
421
423
|
["font-weight", normalizeRendererFontWeight(fontWeight)],
|
|
424
|
+
["font-style", properties.fontStyle],
|
|
422
425
|
["text-anchor", TEXT_ANCHORS[textAlign]],
|
|
423
426
|
["dominant-baseline", DOMINANT_BASELINES[textBaseline]],
|
|
424
427
|
["opacity", formatNumber(opacity)],
|
|
425
428
|
["transform", rotation === 0
|
|
426
429
|
? undefined
|
|
427
430
|
: `rotate(${formatNumber(rotationDegrees)} ${formatNumber(x)} ${formatNumber(y)})`]
|
|
428
|
-
], escapeText(text)
|
|
431
|
+
], properties.lines === undefined ? escapeText(text) : properties.lines.map(line =>
|
|
432
|
+
element("tspan", [["x", formatNumber(x + line.x)], ["y", formatNumber(y + line.y)]], escapeText(line.text))
|
|
433
|
+
).join(""));
|
|
429
434
|
}
|
|
430
435
|
|
|
431
436
|
function serializeConcreteGraphic(state, id, graphic) {
|
|
@@ -579,6 +584,10 @@ export function renderToSVG(program, options = {}) {
|
|
|
579
584
|
resourceNamespace:
|
|
580
585
|
resolvedOptions.resourceNamespace ?? resourceNamespace(graphicSpec)
|
|
581
586
|
};
|
|
587
|
+
walkGraphicTreeEvents(graphicSpec, {
|
|
588
|
+
enter({object}) { if (object.properties?.blend === "difference") state.hasBlend = true; },
|
|
589
|
+
item({object}) { if (object.properties?.blend === "difference") state.hasBlend = true; }
|
|
590
|
+
});
|
|
582
591
|
const body = serializeBody(
|
|
583
592
|
graphicSpec,
|
|
584
593
|
rootId,
|
|
@@ -602,6 +611,7 @@ export function renderToSVG(program, options = {}) {
|
|
|
602
611
|
["xmlns", SVG_NAMESPACE],
|
|
603
612
|
["width", formatNumber(width)],
|
|
604
613
|
["height", formatNumber(height)],
|
|
605
|
-
["viewBox", `0 0 ${formatNumber(width)} ${formatNumber(height)}`]
|
|
614
|
+
["viewBox", `0 0 ${formatNumber(width)} ${formatNumber(height)}`],
|
|
615
|
+
["style", state.hasBlend ? "isolation:isolate" : undefined]
|
|
606
616
|
], `${accessible}${definitions}${body}`);
|
|
607
617
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const p=Object.freeze(["x","y"]),w=Object.freeze(["line","ticks","labels","title"]);function x(n){const e=n.semanticSpec?.guides?.axis??{},a=(n.guideConfigs??n.materializationConfigs?.guides)?.axis??{};return[...new Set([...Object.keys(e),...Object.keys(a)])].flatMap(i=>{const s=Object.hasOwn(e,i)?e[i]:void 0,c=Object.hasOwn(a,i)?a[i]:void 0,d=p.includes(i)?i:s?.channel;if(!p.includes(d))return[];const t=[...new Set((n.semanticSpec.layers??[]).filter(f=>f.encoding?.[d]?.scale===s?.scale).map(f=>f.coordinate).filter(Boolean))];return[{id:i,channel:d,semantic:s,config:c,scale:s?.scale,coordinate:s?.coordinate??(t.length===1?t[0]:void 0),named:i!==d,sides:[...new Set(i!==d?[s.position]:w.map(f=>c?.[f]?.position).filter(Boolean))]}]})}function g(n,e){return n===e?{}:{target:n}}function O(n,e,a={},{create:i=!1,whole:s=!1}={}){const c=x(n),d=c.filter(r=>r.channel===e);let t;if(Object.hasOwn(a,"target")){const r=a.target;if(t=c.find(l=>l.id===r),t===void 0||t.channel!==e)throw new Error(`Unknown ${e}-axis target "${r}".`)}else if(t=d.find(r=>r.id===e),t===void 0){if(d.length>1)throw new Error(`Multiple ${e}-axes require an explicit target.`);t=d[0]}const{target:f,...o}=a,u=t?.id??e;if(!t?.named&&o.position!==void 0&&c.some(r=>r.named)){const r=[...new Set((n.semanticSpec.layers??[]).filter(l=>l.encoding?.[e]?.scale===(o.scale??t?.scale??e)).map(l=>l.coordinate).filter(Boolean))];m(n,{id:u,channel:e,coordinate:t?.coordinate??(r.length===1?r[0]:void 0),position:o.position})}if(t?.named){if(o.scale!==void 0&&o.scale!==t.scale||o.coordinate!==void 0&&o.coordinate!==t.coordinate)throw new Error(`Axis "${u}" resource binding does not match the requested filter.`);if(!s&&o.position!==void 0&&o.position!==t.semantic.position)throw new Error(`Named axis "${u}" components share one side; edit the complete axis position.`);i&&(o.scale??=t.scale),s||(o.position??=t.semantic.position)}return{id:u,owner:t,options:o}}function m(n,{id:e,channel:a,coordinate:i,position:s}){for(const c of x(n))if(c.id!==e&&c.coordinate===i&&c.sides.includes(s))throw new Error(`Axis side "${s}" is already occupied on coordinate "${i}".`)}function b(n,e=n){const a=["Line","Ticks","Labels","Title"];if(e!==n)return a.map(s=>`axis_${e}__${s}`);const i=n==="radius"?"radial":n;return a.map(s=>`${i}Axis${s}`)}export{m as assertCartesianAxisSideAvailable,b as axisGraphicIds,g as axisTargetArgs,x as cartesianAxisOwners,O as selectCartesianAxis};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { annotateError } from "../core/diagnostics.js";
|
|
2
|
+
import { findLayer } from "./layers.js";
|
|
3
|
+
import { findDataset } from "./datasets.js";
|
|
4
|
+
|
|
5
|
+
export function namespaceGraphicId(namespace, id) {
|
|
6
|
+
const encode = value => Array.from(value, character =>
|
|
7
|
+
character.codePointAt(0).toString(16).padStart(6, "0")
|
|
8
|
+
).join("");
|
|
9
|
+
return `g${encode(namespace)}_${id}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function pointOwners(program, id) {
|
|
13
|
+
const layer = findLayer(program, id);
|
|
14
|
+
const dataset = layer?.mark?.type === "point" ? findDataset(program, layer.data) : undefined;
|
|
15
|
+
const owners = Array.isArray(dataset?.values) ? [{ layer, dataset }] : [];
|
|
16
|
+
for (const childId of program.compositionSpec?.children ?? []) {
|
|
17
|
+
const prefix = namespaceGraphicId(`${program.compositionSpec.id}-${childId}`, "");
|
|
18
|
+
const child = program.children[childId];
|
|
19
|
+
if (!id.startsWith(prefix) || child === undefined) continue;
|
|
20
|
+
const localId = id.slice(prefix.length);
|
|
21
|
+
if (child.graphicSpec.objects[localId] === undefined) continue;
|
|
22
|
+
owners.push(...pointOwners(child, localId));
|
|
23
|
+
}
|
|
24
|
+
return owners;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveGraphicItemCapacity(program, { id, label, type }) {
|
|
28
|
+
const owners = pointOwners(program, id);
|
|
29
|
+
if (owners.length > 1) throw annotateError(new Error("Point graphic ownership is ambiguous."), {
|
|
30
|
+
code: "ambiguous-resource", resourceId: id, candidates: owners.map(owner => owner.layer.id)
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
pointOwned: owners.length === 1,
|
|
34
|
+
rawCount: owners[0]?.dataset.values.length,
|
|
35
|
+
graphicType: type ?? program.graphicSpec.objects[id]?.type,
|
|
36
|
+
label
|
|
37
|
+
};
|
|
38
|
+
}
|
package/src/selectors/layers.js
CHANGED
|
@@ -37,3 +37,22 @@ export function resolveEligibleLayer(program, {
|
|
|
37
37
|
if (candidates.length === 0) throw annotateError(new Error(`${label} requires an eligible layer.`), { code: "missing-resource", optionPath: targetOption });
|
|
38
38
|
throw annotateError(new Error(`${label} ${targetOption} is ambiguous; provide ${targetOption}.`), { code: "ambiguous-resource", optionPath: targetOption, candidates: candidates.map(layer => layer.id) });
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
// Configured composite plots retain their established owner errors while sharing
|
|
42
|
+
// the explicit/current/unique resolution order.
|
|
43
|
+
export function resolveConfiguredOwner(program, requested, {
|
|
44
|
+
config, operation, kind, article = "a", materialized = false, missing, ambiguous
|
|
45
|
+
}, validateUserId) {
|
|
46
|
+
const eligible = program.semanticSpec.layers.filter(layer => {
|
|
47
|
+
const value = program.markConfigs[layer.id]?.[config];
|
|
48
|
+
return materialized ? value?.materialized === true : value !== undefined;
|
|
49
|
+
});
|
|
50
|
+
const id = requested === undefined ? program.context.currentMark
|
|
51
|
+
: validateUserId(requested, `${kind[0].toUpperCase() + kind.slice(1)}-plot owner id`);
|
|
52
|
+
const owner = eligible.find(layer => layer.id === id);
|
|
53
|
+
if (owner !== undefined) return owner;
|
|
54
|
+
if (requested !== undefined) throw new Error(`Unknown ${kind}-plot owner "${id}".`);
|
|
55
|
+
if (eligible.length === 1) return eligible[0];
|
|
56
|
+
if (eligible.length === 0) throw new Error(missing ?? `${operation} requires ${article} ${kind} plot.`);
|
|
57
|
+
throw new Error(ambiguous ?? `${operation} target is ambiguous; provide target.`);
|
|
58
|
+
}
|