@pixel-point/toolcraft 0.0.2
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/LICENSE.md +98 -0
- package/README.md +41 -0
- package/bin/create-toolcraft-app.mjs +8 -0
- package/bin/toolcraft.mjs +8 -0
- package/package.json +24 -0
- package/scripts/prepare-pack.mjs +29 -0
- package/src/cli.mjs +392 -0
- package/src/cli.test.mjs +284 -0
- package/src/copy-recursive.mjs +86 -0
- package/src/generate.mjs +212 -0
- package/src/generate.test.mjs +322 -0
- package/src/import-map.mjs +14 -0
- package/src/package-json.mjs +80 -0
- package/src/package-json.test.mjs +67 -0
- package/src/rewrite-imports.mjs +85 -0
- package/src/rewrite-imports.test.mjs +58 -0
- package/templates/runtime/contracts/component-contracts.test.ts +1165 -0
- package/templates/runtime/contracts/component-contracts.ts +1340 -0
- package/templates/runtime/contracts/decision-contracts.test.ts +206 -0
- package/templates/runtime/contracts/decision-contracts.ts +283 -0
- package/templates/runtime/contracts/index.test.ts +14 -0
- package/templates/runtime/contracts/index.ts +3 -0
- package/templates/runtime/contracts/types.ts +56 -0
- package/templates/runtime/export/export.test.ts +203 -0
- package/templates/runtime/export/export.ts +132 -0
- package/templates/runtime/export/index.ts +1 -0
- package/templates/runtime/index.ts +14 -0
- package/templates/runtime/react/canvas-shell.test.tsx +424 -0
- package/templates/runtime/react/canvas-shell.tsx +408 -0
- package/templates/runtime/react/control-renderers.ts +31 -0
- package/templates/runtime/react/controls-panel.test.tsx +3736 -0
- package/templates/runtime/react/controls-panel.tsx +2327 -0
- package/templates/runtime/react/curve-geometry.test.ts +70 -0
- package/templates/runtime/react/index.ts +15 -0
- package/templates/runtime/react/layer-tree.ts +96 -0
- package/templates/runtime/react/layers-panel.test.tsx +487 -0
- package/templates/runtime/react/layers-panel.tsx +1348 -0
- package/templates/runtime/react/media-file.ts +82 -0
- package/templates/runtime/react/panel-host-config.ts +80 -0
- package/templates/runtime/react/panel-host-geometry.test.ts +66 -0
- package/templates/runtime/react/panel-host-geometry.ts +109 -0
- package/templates/runtime/react/panel-host-types.ts +74 -0
- package/templates/runtime/react/panel-host.test.tsx +102 -0
- package/templates/runtime/react/panel-host.tsx +353 -0
- package/templates/runtime/react/runtime-public-api.test.tsx +132 -0
- package/templates/runtime/react/settings-transfer.test.ts +150 -0
- package/templates/runtime/react/settings-transfer.ts +279 -0
- package/templates/runtime/react/storage-key-migration.ts +48 -0
- package/templates/runtime/react/theme-runtime.tsx +177 -0
- package/templates/runtime/react/timeline-panel.test.tsx +668 -0
- package/templates/runtime/react/timeline-panel.tsx +2953 -0
- package/templates/runtime/react/toolbar-panel.test.tsx +212 -0
- package/templates/runtime/react/toolbar-panel.tsx +205 -0
- package/templates/runtime/react/toolcraft-app.integration.test.tsx +350 -0
- package/templates/runtime/react/toolcraft-app.test.tsx +339 -0
- package/templates/runtime/react/toolcraft-app.tsx +81 -0
- package/templates/runtime/react/toolcraft-root.test.tsx +347 -0
- package/templates/runtime/react/toolcraft-root.tsx +203 -0
- package/templates/runtime/react/use-toolcraft.ts +41 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +1524 -0
- package/templates/runtime/schema/define-toolcraft.ts +1442 -0
- package/templates/runtime/schema/keyframe-capability.test.ts +90 -0
- package/templates/runtime/schema/keyframe-capability.ts +51 -0
- package/templates/runtime/schema/runtime-targets.ts +40 -0
- package/templates/runtime/schema/types.ts +370 -0
- package/templates/runtime/state/canvas-zoom.ts +8 -0
- package/templates/runtime/state/create-template-state.test.ts +242 -0
- package/templates/runtime/state/create-template-state.ts +95 -0
- package/templates/runtime/state/keyframe-evaluation.test.ts +141 -0
- package/templates/runtime/state/keyframe-evaluation.ts +203 -0
- package/templates/runtime/state/persistence.test.ts +217 -0
- package/templates/runtime/state/persistence.ts +511 -0
- package/templates/runtime/state/reducer.test.ts +937 -0
- package/templates/runtime/state/reducer.ts +1212 -0
- package/templates/runtime/state/timeline-readiness.ts +43 -0
- package/templates/runtime/state/types.ts +242 -0
- package/templates/runtime/styles.css +125 -0
- package/templates/runtime/testing/performance.test.ts +1058 -0
- package/templates/runtime/testing/performance.ts +1078 -0
- package/templates/starter/AGENTS.md +186 -0
- package/templates/starter/LICENSE.md +98 -0
- package/templates/starter/NOTICE.md +8 -0
- package/templates/starter/docs/toolcraft/README.md +41 -0
- package/templates/starter/docs/toolcraft/acceptance-testing.md +205 -0
- package/templates/starter/docs/toolcraft/agent-worklog.md +81 -0
- package/templates/starter/docs/toolcraft/assembly-workflow.md +206 -0
- package/templates/starter/docs/toolcraft/component-rules.md +299 -0
- package/templates/starter/docs/toolcraft/custom-controls.md +71 -0
- package/templates/starter/docs/toolcraft/decision-contract.md +71 -0
- package/templates/starter/docs/toolcraft/performance.md +112 -0
- package/templates/starter/docs/toolcraft/renderer-technique.md +48 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +265 -0
- package/templates/starter/docs/toolcraft/workflow.md +87 -0
- package/templates/starter/e2e/app-browser-acceptance.spec.ts +785 -0
- package/templates/starter/e2e/app-controls.spec.ts +41 -0
- package/templates/starter/e2e/app-performance.spec.ts +326 -0
- package/templates/starter/e2e/canvas-handle-helpers.ts +244 -0
- package/templates/starter/e2e/performance-helpers.ts +612 -0
- package/templates/starter/e2e/product-observable-helpers.ts +170 -0
- package/templates/starter/index.html +12 -0
- package/templates/starter/package.json +52 -0
- package/templates/starter/playwright.config.ts +43 -0
- package/templates/starter/scripts/check-ai-skills.mjs +95 -0
- package/templates/starter/scripts/check-toolcraft-docs.mjs +159 -0
- package/templates/starter/scripts/check-toolcraft-integrity.mjs +232 -0
- package/templates/starter/scripts/run-vite-on-free-port.mjs +48 -0
- package/templates/starter/scripts/toolcraft-port.mjs +54 -0
- package/templates/starter/scripts/toolcraft-port.test.mjs +73 -0
- package/templates/starter/src/app/starter-acceptance.test.ts +5959 -0
- package/templates/starter/src/app/starter-acceptance.ts +2646 -0
- package/templates/starter/src/app/starter-performance.test.ts +1390 -0
- package/templates/starter/src/app/starter-performance.ts +12 -0
- package/templates/starter/src/app/starter-schema.test.ts +70 -0
- package/templates/starter/src/app/starter-schema.ts +15 -0
- package/templates/starter/src/main.tsx +18 -0
- package/templates/starter/src/router.tsx +16 -0
- package/templates/starter/src/routes/index.tsx +7 -0
- package/templates/starter/src/routes/root.tsx +19 -0
- package/templates/starter/src/styles.css +120 -0
- package/templates/starter/tsconfig.json +11 -0
- package/templates/starter/vite.config.ts +13 -0
- package/templates/ui/components/composites/accordion.tsx +73 -0
- package/templates/ui/components/composites/alert-dialog.tsx +190 -0
- package/templates/ui/components/composites/alert.tsx +74 -0
- package/templates/ui/components/composites/aspect-ratio.tsx +22 -0
- package/templates/ui/components/composites/avatar.tsx +98 -0
- package/templates/ui/components/composites/badge.tsx +69 -0
- package/templates/ui/components/composites/breadcrumb.tsx +106 -0
- package/templates/ui/components/composites/card.tsx +91 -0
- package/templates/ui/components/composites/combobox.tsx +486 -0
- package/templates/ui/components/composites/command.tsx +296 -0
- package/templates/ui/components/composites/context-menu.tsx +247 -0
- package/templates/ui/components/composites/dialog.tsx +282 -0
- package/templates/ui/components/composites/dropdown-menu.tsx +299 -0
- package/templates/ui/components/composites/empty.tsx +110 -0
- package/templates/ui/components/composites/hover-card.tsx +44 -0
- package/templates/ui/components/composites/index.ts +30 -0
- package/templates/ui/components/composites/menubar.tsx +214 -0
- package/templates/ui/components/composites/navigation-menu.tsx +167 -0
- package/templates/ui/components/composites/pagination.tsx +131 -0
- package/templates/ui/components/composites/progress.tsx +72 -0
- package/templates/ui/components/composites/radio-group.tsx +84 -0
- package/templates/ui/components/composites/resizable.tsx +42 -0
- package/templates/ui/components/composites/sheet.tsx +153 -0
- package/templates/ui/components/composites/sidebar-structural.tsx +310 -0
- package/templates/ui/components/composites/sidebar.tsx +431 -0
- package/templates/ui/components/composites/sonner.tsx +35 -0
- package/templates/ui/components/composites/spinner.tsx +43 -0
- package/templates/ui/components/composites/table.tsx +108 -0
- package/templates/ui/components/composites/tabs.tsx +83 -0
- package/templates/ui/components/control-layout/index.tsx +437 -0
- package/templates/ui/components/controls/actions/actions-control.tsx +139 -0
- package/templates/ui/components/controls/actions/index.ts +9 -0
- package/templates/ui/components/controls/anchor-grid/anchor-grid-control.tsx +107 -0
- package/templates/ui/components/controls/anchor-grid/index.ts +4 -0
- package/templates/ui/components/controls/boolean/boolean-controls.tsx +79 -0
- package/templates/ui/components/controls/boolean/index.ts +4 -0
- package/templates/ui/components/controls/channel-mixer/channel-mixer-control.tsx +95 -0
- package/templates/ui/components/controls/channel-mixer/index.ts +4 -0
- package/templates/ui/components/controls/channel-tabs/channel-tabs.tsx +42 -0
- package/templates/ui/components/controls/channel-tabs/index.ts +6 -0
- package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +90 -0
- package/templates/ui/components/controls/code-textarea/index.ts +4 -0
- package/templates/ui/components/controls/color/color-control.tsx +571 -0
- package/templates/ui/components/controls/color/color-picker-popover.tsx +104 -0
- package/templates/ui/components/controls/color/index.ts +41 -0
- package/templates/ui/components/controls/color/palette-control-data.ts +436 -0
- package/templates/ui/components/controls/color/palette-control.tsx +535 -0
- package/templates/ui/components/controls/color/style-guide-color-picker-channel-utils.ts +162 -0
- package/templates/ui/components/controls/color/style-guide-color-picker-interactions.ts +190 -0
- package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +485 -0
- package/templates/ui/components/controls/color/style-guide-color-picker-parts.tsx +710 -0
- package/templates/ui/components/controls/color/style-guide-color-picker.tsx +503 -0
- package/templates/ui/components/controls/control-types.ts +43 -0
- package/templates/ui/components/controls/curves/curve-geometry.ts +355 -0
- package/templates/ui/components/controls/curves/curve-graph.tsx +390 -0
- package/templates/ui/components/controls/curves/curves-control.tsx +445 -0
- package/templates/ui/components/controls/curves/index.ts +6 -0
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +191 -0
- package/templates/ui/components/controls/file-drop/index.ts +5 -0
- package/templates/ui/components/controls/font-picker/font-catalog.json +15360 -0
- package/templates/ui/components/controls/font-picker/font-catalog.ts +116 -0
- package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1202 -0
- package/templates/ui/components/controls/font-picker/font-preview-loader.ts +336 -0
- package/templates/ui/components/controls/font-picker/index.ts +24 -0
- package/templates/ui/components/controls/font-picker/use-hover-intent.ts +46 -0
- package/templates/ui/components/controls/gradient/gradient-control-utils.ts +190 -0
- package/templates/ui/components/controls/gradient/gradient-control.tsx +612 -0
- package/templates/ui/components/controls/gradient/gradient-stop-list.tsx +400 -0
- package/templates/ui/components/controls/gradient/gradient-toolbar.tsx +152 -0
- package/templates/ui/components/controls/gradient/index.ts +4 -0
- package/templates/ui/components/controls/image-picker/image-picker-control.tsx +139 -0
- package/templates/ui/components/controls/image-picker/index.ts +7 -0
- package/templates/ui/components/controls/index.ts +192 -0
- package/templates/ui/components/controls/range-input/index.ts +4 -0
- package/templates/ui/components/controls/range-input/range-input-control.tsx +173 -0
- package/templates/ui/components/controls/range-slider/index.ts +4 -0
- package/templates/ui/components/controls/range-slider/range-slider-control.tsx +122 -0
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +61 -0
- package/templates/ui/components/controls/segmented/index.ts +8 -0
- package/templates/ui/components/controls/segmented/segmented-control.tsx +94 -0
- package/templates/ui/components/controls/select/index.ts +4 -0
- package/templates/ui/components/controls/select/select-control.tsx +223 -0
- package/templates/ui/components/controls/slider/index.ts +4 -0
- package/templates/ui/components/controls/slider/slider-control.tsx +150 -0
- package/templates/ui/components/controls/slider/slider-value.ts +56 -0
- package/templates/ui/components/controls/text-input/index.ts +4 -0
- package/templates/ui/components/controls/text-input/text-input-control.tsx +158 -0
- package/templates/ui/components/controls/use-measured-element-width.ts +42 -0
- package/templates/ui/components/controls/vector/index.ts +8 -0
- package/templates/ui/components/controls/vector/vector-control.tsx +401 -0
- package/templates/ui/components/panel/index.ts +19 -0
- package/templates/ui/components/panel/panel-actions.tsx +165 -0
- package/templates/ui/components/panel/panel-header.tsx +61 -0
- package/templates/ui/components/panel/panel-icon-button.tsx +96 -0
- package/templates/ui/components/panel/panel-section.tsx +168 -0
- package/templates/ui/components/panel/panel-surface.tsx +206 -0
- package/templates/ui/components/panel/panel.tsx +210 -0
- package/templates/ui/components/primitives/animated-loader.tsx +61 -0
- package/templates/ui/components/primitives/button-group.tsx +134 -0
- package/templates/ui/components/primitives/button.tsx +429 -0
- package/templates/ui/components/primitives/checkbox.tsx +62 -0
- package/templates/ui/components/primitives/editable-slider-value-label.tsx +337 -0
- package/templates/ui/components/primitives/field.tsx +225 -0
- package/templates/ui/components/primitives/index.ts +82 -0
- package/templates/ui/components/primitives/input-group.tsx +298 -0
- package/templates/ui/components/primitives/input.tsx +61 -0
- package/templates/ui/components/primitives/internal/button-loading.tsx +178 -0
- package/templates/ui/components/primitives/label.tsx +16 -0
- package/templates/ui/components/primitives/popover.tsx +126 -0
- package/templates/ui/components/primitives/portal-layer-context.tsx +33 -0
- package/templates/ui/components/primitives/primitive-arrow-icon.tsx +38 -0
- package/templates/ui/components/primitives/scroll-fade-logic.ts +441 -0
- package/templates/ui/components/primitives/scroll-fade-render.tsx +75 -0
- package/templates/ui/components/primitives/scroll-fade-types.ts +41 -0
- package/templates/ui/components/primitives/scroll-fade.tsx +72 -0
- package/templates/ui/components/primitives/select.tsx +408 -0
- package/templates/ui/components/primitives/selection-state.ts +31 -0
- package/templates/ui/components/primitives/separator.tsx +21 -0
- package/templates/ui/components/primitives/slider/index.ts +4 -0
- package/templates/ui/components/primitives/slider/slider-interaction.tsx +96 -0
- package/templates/ui/components/primitives/slider/slider-parts.tsx +303 -0
- package/templates/ui/components/primitives/slider/slider-reset.ts +152 -0
- package/templates/ui/components/primitives/slider/slider-value.ts +114 -0
- package/templates/ui/components/primitives/slider/slider.tsx +511 -0
- package/templates/ui/components/primitives/switch.tsx +35 -0
- package/templates/ui/components/primitives/textarea.tsx +49 -0
- package/templates/ui/components/primitives/toggle-group.tsx +114 -0
- package/templates/ui/components/primitives/toggle.tsx +46 -0
- package/templates/ui/components/primitives/tooltip.tsx +100 -0
- package/templates/ui/hooks/use-mobile.ts +21 -0
- package/templates/ui/index.ts +31 -0
- package/templates/ui/lib/control-outline.ts +3 -0
- package/templates/ui/lib/input-control-style.ts +131 -0
- package/templates/ui/lib/style-guide-color-utils.ts +111 -0
- package/templates/ui/lib/utils.ts +6 -0
- package/templates/ui/styles.css +291 -0
|
@@ -0,0 +1,1078 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ToolcraftControlSchema,
|
|
3
|
+
ResolvedToolcraftAppSchema,
|
|
4
|
+
} from "../schema/types";
|
|
5
|
+
|
|
6
|
+
export type ToolcraftPerformanceInteraction =
|
|
7
|
+
| "animation-frame"
|
|
8
|
+
| "animation-viewport-drag"
|
|
9
|
+
| "control-change"
|
|
10
|
+
| "control-drag"
|
|
11
|
+
| "export-copy"
|
|
12
|
+
| "media-import"
|
|
13
|
+
| "preview-render"
|
|
14
|
+
| "viewport-zoom-stress"
|
|
15
|
+
| "viewport-stability";
|
|
16
|
+
|
|
17
|
+
export type ToolcraftPerformanceCoverage = {
|
|
18
|
+
automated: boolean;
|
|
19
|
+
automatedTestName: string;
|
|
20
|
+
browser: boolean;
|
|
21
|
+
browserTestName: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type ToolcraftPerformanceBudget = {
|
|
25
|
+
maxExportMs?: number;
|
|
26
|
+
maxFrameGapMs?: number;
|
|
27
|
+
maxInteractionMs?: number;
|
|
28
|
+
maxLongTaskMs?: number;
|
|
29
|
+
maxPreviewMs?: number;
|
|
30
|
+
maxRenderMs?: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type ToolcraftPerformanceValueSet = {
|
|
34
|
+
default: unknown;
|
|
35
|
+
max?: unknown;
|
|
36
|
+
min?: unknown;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type ToolcraftPerformanceStressFixtureKind =
|
|
40
|
+
| "custom"
|
|
41
|
+
| "high-density"
|
|
42
|
+
| "large-canvas"
|
|
43
|
+
| "large-text"
|
|
44
|
+
| "many-items"
|
|
45
|
+
| "max-value"
|
|
46
|
+
| "media";
|
|
47
|
+
|
|
48
|
+
export type ToolcraftPerformanceStressFixture = {
|
|
49
|
+
kind: ToolcraftPerformanceStressFixtureKind;
|
|
50
|
+
minChars?: number;
|
|
51
|
+
minCount?: number;
|
|
52
|
+
minLines?: number;
|
|
53
|
+
reason: string;
|
|
54
|
+
value?: unknown;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type ToolcraftPerformanceScenario = ToolcraftPerformanceCoverage & {
|
|
58
|
+
budget: ToolcraftPerformanceBudget;
|
|
59
|
+
controlLabel?: string;
|
|
60
|
+
expectedObservable: string;
|
|
61
|
+
fixture: string;
|
|
62
|
+
id: string;
|
|
63
|
+
interaction: ToolcraftPerformanceInteraction;
|
|
64
|
+
target?: string;
|
|
65
|
+
stress?: boolean;
|
|
66
|
+
stressFixture?: ToolcraftPerformanceStressFixture;
|
|
67
|
+
uiSelector?: string;
|
|
68
|
+
values?: ToolcraftPerformanceValueSet;
|
|
69
|
+
workload: boolean;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type ToolcraftRendererStrategy =
|
|
73
|
+
| "none"
|
|
74
|
+
| "dom"
|
|
75
|
+
| "svg"
|
|
76
|
+
| "canvas-2d"
|
|
77
|
+
| "webgl"
|
|
78
|
+
| "webgpu";
|
|
79
|
+
|
|
80
|
+
export type ToolcraftRendererWorkload =
|
|
81
|
+
| "none"
|
|
82
|
+
| "simple-composition"
|
|
83
|
+
| "text-output"
|
|
84
|
+
| "vector-output"
|
|
85
|
+
| "pixel-output";
|
|
86
|
+
|
|
87
|
+
export type ToolcraftSourceRepresentation =
|
|
88
|
+
| "reference-runtime"
|
|
89
|
+
| "dom-text"
|
|
90
|
+
| "svg"
|
|
91
|
+
| "canvas-2d"
|
|
92
|
+
| "webgl-texture"
|
|
93
|
+
| "webgpu-texture"
|
|
94
|
+
| "image-media"
|
|
95
|
+
| "video-media"
|
|
96
|
+
| "procedural-data"
|
|
97
|
+
| "mixed";
|
|
98
|
+
|
|
99
|
+
export type ToolcraftProductRepresentation =
|
|
100
|
+
| "text"
|
|
101
|
+
| "vector"
|
|
102
|
+
| "pixel"
|
|
103
|
+
| "video"
|
|
104
|
+
| "mixed";
|
|
105
|
+
|
|
106
|
+
export type ToolcraftPreviewRenderer =
|
|
107
|
+
| "dom"
|
|
108
|
+
| "svg"
|
|
109
|
+
| "canvas-2d"
|
|
110
|
+
| "webgl"
|
|
111
|
+
| "webgpu";
|
|
112
|
+
|
|
113
|
+
export type ToolcraftExportRenderer =
|
|
114
|
+
| "none"
|
|
115
|
+
| "dom"
|
|
116
|
+
| "svg"
|
|
117
|
+
| "canvas-2d"
|
|
118
|
+
| "webgl"
|
|
119
|
+
| "webgpu"
|
|
120
|
+
| "media-recorder"
|
|
121
|
+
| "webcodecs";
|
|
122
|
+
|
|
123
|
+
export type ToolcraftRendererLayerKind =
|
|
124
|
+
| "background"
|
|
125
|
+
| "product-foreground"
|
|
126
|
+
| "editing-handles"
|
|
127
|
+
| "export-composite";
|
|
128
|
+
|
|
129
|
+
export type ToolcraftRendererLayerContent =
|
|
130
|
+
| "bitmap-media"
|
|
131
|
+
| "composite"
|
|
132
|
+
| "dense-pattern"
|
|
133
|
+
| "geometry"
|
|
134
|
+
| "handles"
|
|
135
|
+
| "noise"
|
|
136
|
+
| "shader"
|
|
137
|
+
| "text";
|
|
138
|
+
|
|
139
|
+
export type ToolcraftRendererLayerPrimitiveCount = "low" | "medium" | "high";
|
|
140
|
+
|
|
141
|
+
export type ToolcraftRendererLayerExportMode =
|
|
142
|
+
| "included"
|
|
143
|
+
| "excluded"
|
|
144
|
+
| "composited";
|
|
145
|
+
|
|
146
|
+
export type ToolcraftRendererLayer = {
|
|
147
|
+
content: readonly ToolcraftRendererLayerContent[];
|
|
148
|
+
exportMode: ToolcraftRendererLayerExportMode;
|
|
149
|
+
id: string;
|
|
150
|
+
intentionalRasterizationReason?: string;
|
|
151
|
+
kind: ToolcraftRendererLayerKind;
|
|
152
|
+
primitiveCount: ToolcraftRendererLayerPrimitiveCount;
|
|
153
|
+
renderer: Exclude<ToolcraftRendererStrategy, "none">;
|
|
154
|
+
uiSelector?: string;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export type ToolcraftRendererTechnique = {
|
|
158
|
+
exportRenderer: ToolcraftExportRenderer;
|
|
159
|
+
fidelityRisks: readonly string[];
|
|
160
|
+
intentionalRasterizationReason?: string;
|
|
161
|
+
layers?: readonly ToolcraftRendererLayer[];
|
|
162
|
+
performanceRisks: readonly string[];
|
|
163
|
+
previewExportDifferenceReason?: string;
|
|
164
|
+
previewRenderer: ToolcraftPreviewRenderer;
|
|
165
|
+
productRepresentation: ToolcraftProductRepresentation;
|
|
166
|
+
referenceRendererChangeReason?: string;
|
|
167
|
+
rendererStrategy: ToolcraftRendererStrategy;
|
|
168
|
+
rendererWorkload: ToolcraftRendererWorkload;
|
|
169
|
+
sourceRepresentation: ToolcraftSourceRepresentation;
|
|
170
|
+
whyNotAlternativeStrategies: readonly string[];
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export type ToolcraftPerformanceConfig = {
|
|
174
|
+
rendererStrategy: ToolcraftRendererStrategy;
|
|
175
|
+
rendererTechnique?: ToolcraftRendererTechnique;
|
|
176
|
+
rendererWorkload: ToolcraftRendererWorkload;
|
|
177
|
+
scenarios: readonly ToolcraftPerformanceScenario[];
|
|
178
|
+
usesCustomRenderer: boolean;
|
|
179
|
+
workloadTargets: readonly string[];
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export type ToolcraftPerformanceSensitiveControl = {
|
|
183
|
+
control: ToolcraftControlSchema;
|
|
184
|
+
controlId: string;
|
|
185
|
+
target: string;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export type ToolcraftUnclassifiedPerformanceControl = {
|
|
189
|
+
control: ToolcraftControlSchema;
|
|
190
|
+
controlId: string;
|
|
191
|
+
target: string;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const maxPerformanceBudgetCaps: Required<ToolcraftPerformanceBudget> = {
|
|
195
|
+
maxExportMs: 8000,
|
|
196
|
+
maxFrameGapMs: 120,
|
|
197
|
+
maxInteractionMs: 2000,
|
|
198
|
+
maxLongTaskMs: 250,
|
|
199
|
+
maxPreviewMs: 2000,
|
|
200
|
+
maxRenderMs: 2000,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export function defineToolcraftPerformance(
|
|
204
|
+
config: ToolcraftPerformanceConfig,
|
|
205
|
+
): ToolcraftPerformanceConfig {
|
|
206
|
+
return config;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const workloadControlPattern =
|
|
210
|
+
/char\s*size|cell|density|glyph|grid|iteration|matrix|particle|quality|radius|resolution|sample|scale|size/i;
|
|
211
|
+
|
|
212
|
+
const heavyTextInputPattern = /code|css|instruction|json|prompt|script|shader|template/i;
|
|
213
|
+
|
|
214
|
+
const largeTextStressMinChars = 50_000;
|
|
215
|
+
const largeTextStressMinLines = 1_000;
|
|
216
|
+
|
|
217
|
+
const stressFixtureKinds = new Set<ToolcraftPerformanceStressFixtureKind>([
|
|
218
|
+
"custom",
|
|
219
|
+
"high-density",
|
|
220
|
+
"large-canvas",
|
|
221
|
+
"large-text",
|
|
222
|
+
"many-items",
|
|
223
|
+
"max-value",
|
|
224
|
+
"media",
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
function getControlSemanticText(control: ToolcraftControlSchema): string {
|
|
228
|
+
return [
|
|
229
|
+
control.target,
|
|
230
|
+
typeof control.label === "string" ? control.label : "",
|
|
231
|
+
control.unit ?? "",
|
|
232
|
+
control.valueLabel ?? "",
|
|
233
|
+
control.xLabel ?? "",
|
|
234
|
+
control.yLabel ?? "",
|
|
235
|
+
...(control.options ?? []).flatMap((option) => [option.label, option.value]),
|
|
236
|
+
].join(" ");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isSemanticallyWorkloadControl(control: ToolcraftControlSchema): boolean {
|
|
240
|
+
const semanticText = getControlSemanticText(control);
|
|
241
|
+
|
|
242
|
+
if (control.type === "code" || control.type === "text") {
|
|
243
|
+
return heavyTextInputPattern.test(semanticText);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return workloadControlPattern.test(semanticText);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isPotentialWorkloadControl(control: ToolcraftControlSchema): boolean {
|
|
250
|
+
return control.performanceRole === "workload" || isSemanticallyWorkloadControl(control);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function collectToolcraftPerformanceSensitiveControls(
|
|
254
|
+
schema: ResolvedToolcraftAppSchema,
|
|
255
|
+
): ToolcraftPerformanceSensitiveControl[] {
|
|
256
|
+
return (schema.panels.controls?.sections ?? []).flatMap((section) =>
|
|
257
|
+
Object.entries(section.controls)
|
|
258
|
+
.filter(([, control]) => isPotentialWorkloadControl(control))
|
|
259
|
+
.map(([controlId, control]) => ({
|
|
260
|
+
control,
|
|
261
|
+
controlId,
|
|
262
|
+
target: control.target,
|
|
263
|
+
})),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function collectToolcraftPerformanceRoleConflicts(
|
|
268
|
+
schema: ResolvedToolcraftAppSchema,
|
|
269
|
+
): ToolcraftPerformanceSensitiveControl[] {
|
|
270
|
+
return (schema.panels.controls?.sections ?? []).flatMap((section) =>
|
|
271
|
+
Object.entries(section.controls)
|
|
272
|
+
.filter(
|
|
273
|
+
([, control]) =>
|
|
274
|
+
control.performanceRole === "responsiveness" &&
|
|
275
|
+
isSemanticallyWorkloadControl(control),
|
|
276
|
+
)
|
|
277
|
+
.map(([controlId, control]) => ({
|
|
278
|
+
control,
|
|
279
|
+
controlId,
|
|
280
|
+
target: control.target,
|
|
281
|
+
})),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function collectToolcraftUnclassifiedPerformanceControls(
|
|
286
|
+
schema: ResolvedToolcraftAppSchema,
|
|
287
|
+
): ToolcraftUnclassifiedPerformanceControl[] {
|
|
288
|
+
return (schema.panels.controls?.sections ?? []).flatMap((section) =>
|
|
289
|
+
Object.entries(section.controls)
|
|
290
|
+
.filter(
|
|
291
|
+
([, control]) =>
|
|
292
|
+
control.type !== "panelActions" &&
|
|
293
|
+
control.performanceRole !== "workload" &&
|
|
294
|
+
control.performanceRole !== "responsiveness",
|
|
295
|
+
)
|
|
296
|
+
.map(([controlId, control]) => ({
|
|
297
|
+
control,
|
|
298
|
+
controlId,
|
|
299
|
+
target: control.target,
|
|
300
|
+
})),
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function hasAnyBudget(budget: ToolcraftPerformanceBudget): boolean {
|
|
305
|
+
return Object.values(budget).some((value) => typeof value === "number" && value > 0);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function hasMinDefaultMax(values: ToolcraftPerformanceScenario["values"]): boolean {
|
|
309
|
+
return values !== undefined && "default" in values && "min" in values && "max" in values;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function hasStressFixtureValue(
|
|
313
|
+
fixture: ToolcraftPerformanceStressFixture,
|
|
314
|
+
): fixture is ToolcraftPerformanceStressFixture & { value: unknown } {
|
|
315
|
+
return Object.prototype.hasOwnProperty.call(fixture, "value");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function getTextLineCount(value: string): number {
|
|
319
|
+
if (value.length === 0) {
|
|
320
|
+
return 0;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return value.split(/\r\n|\r|\n/).length;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function isLargeTextWorkloadControl(
|
|
327
|
+
control: ToolcraftControlSchema | undefined,
|
|
328
|
+
): boolean {
|
|
329
|
+
if (
|
|
330
|
+
!control ||
|
|
331
|
+
(control.type !== "code" && control.type !== "text" && control.type !== "textarea")
|
|
332
|
+
) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
control.type === "code" ||
|
|
338
|
+
control.type === "textarea" ||
|
|
339
|
+
heavyTextInputPattern.test(getControlSemanticText(control))
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function getStressFixtureErrors(
|
|
344
|
+
scenario: ToolcraftPerformanceScenario,
|
|
345
|
+
control: ToolcraftControlSchema | undefined,
|
|
346
|
+
): string[] {
|
|
347
|
+
if (!scenario.workload) {
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const errors: string[] = [];
|
|
352
|
+
const fixture = scenario.stressFixture;
|
|
353
|
+
|
|
354
|
+
if (!fixture) {
|
|
355
|
+
return [
|
|
356
|
+
`${scenario.id} workload scenario must declare stressFixture with the real heaviest value used by browser performance tests.`,
|
|
357
|
+
];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!stressFixtureKinds.has(fixture.kind)) {
|
|
361
|
+
errors.push(`${scenario.id} stressFixture.kind "${fixture.kind}" is not supported.`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!fixture.reason.trim()) {
|
|
365
|
+
errors.push(
|
|
366
|
+
`${scenario.id} stressFixture must explain why this is the heaviest useful fixture.`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (fixture.kind !== "custom" && !hasStressFixtureValue(fixture)) {
|
|
371
|
+
errors.push(`${scenario.id} stressFixture must include value so browser tests can use it.`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (!isLargeTextWorkloadControl(control) && fixture.kind !== "large-text") {
|
|
375
|
+
return errors;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (fixture.kind !== "large-text") {
|
|
379
|
+
errors.push(
|
|
380
|
+
`${scenario.id} text workload scenario must use stressFixture.kind "large-text" with a real long text value.`,
|
|
381
|
+
);
|
|
382
|
+
return errors;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (typeof fixture.value !== "string") {
|
|
386
|
+
errors.push(`${scenario.id} large-text stressFixture.value must be a string.`);
|
|
387
|
+
return errors;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const minChars = fixture.minChars ?? largeTextStressMinChars;
|
|
391
|
+
const minLines = fixture.minLines ?? largeTextStressMinLines;
|
|
392
|
+
|
|
393
|
+
if (minChars < largeTextStressMinChars) {
|
|
394
|
+
errors.push(
|
|
395
|
+
`${scenario.id} large-text stressFixture.minChars must be >= ${largeTextStressMinChars}.`,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (minLines < largeTextStressMinLines) {
|
|
400
|
+
errors.push(
|
|
401
|
+
`${scenario.id} large-text stressFixture.minLines must be >= ${largeTextStressMinLines}.`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (fixture.value.length < minChars) {
|
|
406
|
+
errors.push(
|
|
407
|
+
`${scenario.id} large-text stressFixture.value must contain at least ${minChars} characters.`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (getTextLineCount(fixture.value) < minLines) {
|
|
412
|
+
errors.push(
|
|
413
|
+
`${scenario.id} large-text stressFixture.value must contain at least ${minLines} lines.`,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return errors;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function hasPositiveBudgetField(
|
|
421
|
+
budget: ToolcraftPerformanceBudget,
|
|
422
|
+
field: keyof ToolcraftPerformanceBudget,
|
|
423
|
+
): boolean {
|
|
424
|
+
const value = budget[field];
|
|
425
|
+
return typeof value === "number" && value > 0;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function getMissingInteractionBudgetFields(
|
|
429
|
+
scenario: ToolcraftPerformanceScenario,
|
|
430
|
+
): string[] {
|
|
431
|
+
switch (scenario.interaction) {
|
|
432
|
+
case "animation-frame":
|
|
433
|
+
return hasPositiveBudgetField(scenario.budget, "maxFrameGapMs")
|
|
434
|
+
? hasPositiveBudgetField(scenario.budget, "maxLongTaskMs")
|
|
435
|
+
? []
|
|
436
|
+
: ["maxLongTaskMs"]
|
|
437
|
+
: hasPositiveBudgetField(scenario.budget, "maxLongTaskMs")
|
|
438
|
+
? ["maxFrameGapMs"]
|
|
439
|
+
: ["maxFrameGapMs", "maxLongTaskMs"];
|
|
440
|
+
case "animation-viewport-drag":
|
|
441
|
+
case "viewport-zoom-stress":
|
|
442
|
+
return (["maxInteractionMs", "maxFrameGapMs", "maxLongTaskMs"] as const).filter(
|
|
443
|
+
(field) => !hasPositiveBudgetField(scenario.budget, field),
|
|
444
|
+
);
|
|
445
|
+
case "viewport-stability":
|
|
446
|
+
return hasPositiveBudgetField(scenario.budget, "maxFrameGapMs")
|
|
447
|
+
? []
|
|
448
|
+
: ["maxFrameGapMs"];
|
|
449
|
+
case "control-change":
|
|
450
|
+
case "control-drag":
|
|
451
|
+
case "media-import":
|
|
452
|
+
return (["maxInteractionMs", "maxFrameGapMs"] as const).filter(
|
|
453
|
+
(field) => !hasPositiveBudgetField(scenario.budget, field),
|
|
454
|
+
);
|
|
455
|
+
case "export-copy":
|
|
456
|
+
return hasPositiveBudgetField(scenario.budget, "maxExportMs") ? [] : ["maxExportMs"];
|
|
457
|
+
case "preview-render":
|
|
458
|
+
return hasPositiveBudgetField(scenario.budget, "maxPreviewMs") ||
|
|
459
|
+
hasPositiveBudgetField(scenario.budget, "maxRenderMs")
|
|
460
|
+
? []
|
|
461
|
+
: ["maxPreviewMs or maxRenderMs"];
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function getBudgetCapErrors(scenario: ToolcraftPerformanceScenario): string[] {
|
|
466
|
+
return Object.entries(scenario.budget).flatMap(([field, value]) => {
|
|
467
|
+
const budgetField = field as keyof ToolcraftPerformanceBudget;
|
|
468
|
+
const cap = maxPerformanceBudgetCaps[budgetField];
|
|
469
|
+
|
|
470
|
+
if (typeof value !== "number" || value <= cap) {
|
|
471
|
+
return [];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return [`${scenario.id} ${budgetField} budget must be <= ${cap}ms, received ${value}ms.`];
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function requiresConcreteUiTarget(interaction: ToolcraftPerformanceInteraction): boolean {
|
|
479
|
+
return interaction === "control-change" || interaction === "control-drag";
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function getAllSchemaControls(
|
|
483
|
+
schema: ResolvedToolcraftAppSchema,
|
|
484
|
+
): ToolcraftControlSchema[] {
|
|
485
|
+
return (schema.panels.controls?.sections ?? []).flatMap((section) =>
|
|
486
|
+
Object.values(section.controls),
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function getVisiblePerformanceControlTargets(schema: ResolvedToolcraftAppSchema): string[] {
|
|
491
|
+
return getAllSchemaControls(schema)
|
|
492
|
+
.filter((control) => control.type !== "panelActions")
|
|
493
|
+
.map((control) => control.target);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function getActionValue(
|
|
497
|
+
action: NonNullable<ToolcraftControlSchema["actions"]>[number],
|
|
498
|
+
): string {
|
|
499
|
+
return typeof action === "string" ? action : action.value;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function hasOutputDeliveryAction(schema: ResolvedToolcraftAppSchema): boolean {
|
|
503
|
+
return getAllSchemaControls(schema).some(
|
|
504
|
+
(control) =>
|
|
505
|
+
control.type === "panelActions" &&
|
|
506
|
+
(control.actions ?? []).some((action) =>
|
|
507
|
+
/copy|download|export/i.test(getActionValue(action)),
|
|
508
|
+
),
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function hasKeyframeTimeline(schema: ResolvedToolcraftAppSchema): boolean {
|
|
513
|
+
return schema.panels.timeline?.enabled === true && schema.panels.timeline.mode === "keyframes";
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function hasLayersPanel(schema: ResolvedToolcraftAppSchema): boolean {
|
|
517
|
+
return schema.panels.layers === true;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function hasNonEmptyItems(items: readonly string[]): boolean {
|
|
521
|
+
return items.some((item) => item.trim().length > 0);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const rasterRendererStrategies = new Set<ToolcraftRendererStrategy>([
|
|
525
|
+
"canvas-2d",
|
|
526
|
+
"webgl",
|
|
527
|
+
"webgpu",
|
|
528
|
+
]);
|
|
529
|
+
|
|
530
|
+
const semanticForegroundContent = new Set<ToolcraftRendererLayerContent>([
|
|
531
|
+
"geometry",
|
|
532
|
+
"text",
|
|
533
|
+
]);
|
|
534
|
+
|
|
535
|
+
const detailHeavyRendererContent = new Set<ToolcraftRendererLayerContent>([
|
|
536
|
+
"bitmap-media",
|
|
537
|
+
"dense-pattern",
|
|
538
|
+
"noise",
|
|
539
|
+
"shader",
|
|
540
|
+
]);
|
|
541
|
+
|
|
542
|
+
const vectorLayerRendererStrategies = new Set<ToolcraftRendererStrategy>(["dom", "svg"]);
|
|
543
|
+
|
|
544
|
+
function getLayerContentFamily(content: ToolcraftRendererLayerContent): string {
|
|
545
|
+
if (content === "geometry" || content === "handles") {
|
|
546
|
+
return "vector";
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (content === "text") {
|
|
550
|
+
return "text";
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (
|
|
554
|
+
content === "bitmap-media" ||
|
|
555
|
+
content === "dense-pattern" ||
|
|
556
|
+
content === "noise" ||
|
|
557
|
+
content === "shader"
|
|
558
|
+
) {
|
|
559
|
+
return "pixel";
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return "composite";
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function hasSemanticForegroundContent(layer: ToolcraftRendererLayer): boolean {
|
|
566
|
+
return layer.content.some((content) => semanticForegroundContent.has(content));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function hasDetailHeavyRendererLayer(
|
|
570
|
+
technique: ToolcraftRendererTechnique | undefined,
|
|
571
|
+
): boolean {
|
|
572
|
+
return (technique?.layers ?? []).some(
|
|
573
|
+
(layer) =>
|
|
574
|
+
layer.primitiveCount === "high" ||
|
|
575
|
+
layer.content.some((content) => detailHeavyRendererContent.has(content)),
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function hasHighCountCanvas2DRendererLayer(
|
|
580
|
+
technique: ToolcraftRendererTechnique | undefined,
|
|
581
|
+
): boolean {
|
|
582
|
+
return (technique?.layers ?? []).some(
|
|
583
|
+
(layer) =>
|
|
584
|
+
layer.renderer === "canvas-2d" &&
|
|
585
|
+
layer.primitiveCount === "high" &&
|
|
586
|
+
(hasSemanticForegroundContent(layer) ||
|
|
587
|
+
layer.content.some((content) => detailHeavyRendererContent.has(content))),
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function hasStressPreviewOrAnimationScenario(config: ToolcraftPerformanceConfig): boolean {
|
|
592
|
+
return config.scenarios.some(
|
|
593
|
+
(scenario) =>
|
|
594
|
+
scenario.stress === true &&
|
|
595
|
+
(scenario.interaction === "preview-render" ||
|
|
596
|
+
scenario.interaction === "animation-frame"),
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function hasLongTaskBudgetScenario(config: ToolcraftPerformanceConfig): boolean {
|
|
601
|
+
return config.scenarios.some((scenario) =>
|
|
602
|
+
hasPositiveBudgetField(scenario.budget, "maxLongTaskMs"),
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function hasZoomSensitiveRenderer(config: ToolcraftPerformanceConfig): boolean {
|
|
607
|
+
return (
|
|
608
|
+
config.rendererWorkload === "text-output" ||
|
|
609
|
+
config.rendererWorkload === "vector-output" ||
|
|
610
|
+
config.rendererWorkload === "pixel-output" ||
|
|
611
|
+
hasDetailHeavyRendererLayer(config.rendererTechnique)
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function getRendererLayerErrors(technique: ToolcraftRendererTechnique): string[] {
|
|
616
|
+
const errors: string[] = [];
|
|
617
|
+
const layers = technique.layers ?? [];
|
|
618
|
+
|
|
619
|
+
if (technique.productRepresentation === "mixed" && layers.length === 0) {
|
|
620
|
+
errors.push(
|
|
621
|
+
'productRepresentation "mixed" requires rendererTechnique.layers so mixed output is machine-checkable.',
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (technique.productRepresentation === "mixed") {
|
|
626
|
+
const contentFamilies = new Set(
|
|
627
|
+
layers.flatMap((layer) => layer.content.map((content) => getLayerContentFamily(content))),
|
|
628
|
+
);
|
|
629
|
+
contentFamilies.delete("composite");
|
|
630
|
+
|
|
631
|
+
if (contentFamilies.size < 2) {
|
|
632
|
+
errors.push(
|
|
633
|
+
'productRepresentation "mixed" requires rendererTechnique.layers with at least two different content families.',
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
for (const layer of layers) {
|
|
639
|
+
if (!layer.id.trim()) {
|
|
640
|
+
errors.push("rendererTechnique layers must have non-empty ids.");
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (!hasNonEmptyItems(layer.content)) {
|
|
644
|
+
errors.push(`rendererTechnique layer "${layer.id}" must list content.`);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (
|
|
648
|
+
layer.kind === "product-foreground" &&
|
|
649
|
+
hasSemanticForegroundContent(layer) &&
|
|
650
|
+
layer.primitiveCount !== "high" &&
|
|
651
|
+
rasterRendererStrategies.has(layer.renderer) &&
|
|
652
|
+
!layer.intentionalRasterizationReason?.trim()
|
|
653
|
+
) {
|
|
654
|
+
errors.push(
|
|
655
|
+
`rendererTechnique layer "${layer.id}" uses ${layer.renderer} for low-count semantic geometry/text. Use dom/svg for semantic foreground or provide intentionalRasterizationReason.`,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (
|
|
660
|
+
(layer.kind === "product-foreground" || layer.kind === "editing-handles") &&
|
|
661
|
+
!layer.uiSelector?.trim()
|
|
662
|
+
) {
|
|
663
|
+
errors.push(
|
|
664
|
+
`rendererTechnique layer "${layer.id}" is ${layer.kind} and must declare uiSelector so browser tests can verify the visible renderer layer.`,
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (
|
|
669
|
+
layer.kind === "editing-handles" &&
|
|
670
|
+
(!vectorLayerRendererStrategies.has(layer.renderer) || layer.exportMode !== "excluded")
|
|
671
|
+
) {
|
|
672
|
+
errors.push(
|
|
673
|
+
`rendererTechnique layer "${layer.id}" is editing-handles and must use dom/svg with exportMode "excluded".`,
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return errors;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function getRendererTechniqueErrors(config: ToolcraftPerformanceConfig): string[] {
|
|
682
|
+
const errors: string[] = [];
|
|
683
|
+
const technique = config.rendererTechnique;
|
|
684
|
+
|
|
685
|
+
if (config.usesCustomRenderer && !technique) {
|
|
686
|
+
return [
|
|
687
|
+
"Custom renderers must declare rendererTechnique so renderer choice is machine-checkable.",
|
|
688
|
+
];
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (!config.usesCustomRenderer && technique) {
|
|
692
|
+
errors.push("Non-custom renderer configs must omit rendererTechnique.");
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (!technique) {
|
|
696
|
+
return errors;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (technique.rendererWorkload !== config.rendererWorkload) {
|
|
700
|
+
errors.push(
|
|
701
|
+
`rendererTechnique.rendererWorkload "${technique.rendererWorkload}" must match rendererWorkload "${config.rendererWorkload}".`,
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (technique.rendererStrategy !== config.rendererStrategy) {
|
|
706
|
+
errors.push(
|
|
707
|
+
`rendererTechnique.rendererStrategy "${technique.rendererStrategy}" must match rendererStrategy "${config.rendererStrategy}".`,
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (config.usesCustomRenderer && !hasNonEmptyItems(technique.whyNotAlternativeStrategies)) {
|
|
712
|
+
errors.push(
|
|
713
|
+
"Custom renderer technique must explain why alternative renderer strategies were rejected.",
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (config.usesCustomRenderer && !hasNonEmptyItems(technique.fidelityRisks)) {
|
|
718
|
+
errors.push("Custom renderer technique must list fidelity risks.");
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (config.usesCustomRenderer && !hasNonEmptyItems(technique.performanceRisks)) {
|
|
722
|
+
errors.push("Custom renderer technique must list performance risks.");
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (
|
|
726
|
+
technique.productRepresentation === "text" &&
|
|
727
|
+
technique.rendererWorkload !== "text-output" &&
|
|
728
|
+
!technique.intentionalRasterizationReason?.trim()
|
|
729
|
+
) {
|
|
730
|
+
errors.push(
|
|
731
|
+
'productRepresentation "text" requires rendererWorkload "text-output" unless intentionalRasterizationReason is provided.',
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (
|
|
736
|
+
technique.productRepresentation === "vector" &&
|
|
737
|
+
technique.rendererWorkload !== "vector-output" &&
|
|
738
|
+
!technique.intentionalRasterizationReason?.trim()
|
|
739
|
+
) {
|
|
740
|
+
errors.push(
|
|
741
|
+
'productRepresentation "vector" requires rendererWorkload "vector-output" unless intentionalRasterizationReason is provided.',
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (
|
|
746
|
+
technique.productRepresentation === "pixel" &&
|
|
747
|
+
technique.rendererWorkload !== "pixel-output"
|
|
748
|
+
) {
|
|
749
|
+
errors.push('productRepresentation "pixel" requires rendererWorkload "pixel-output".');
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (
|
|
753
|
+
technique.previewRenderer !== technique.exportRenderer &&
|
|
754
|
+
technique.exportRenderer !== "none" &&
|
|
755
|
+
!technique.previewExportDifferenceReason?.trim()
|
|
756
|
+
) {
|
|
757
|
+
errors.push("Different preview/export renderers require previewExportDifferenceReason.");
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (
|
|
761
|
+
technique.sourceRepresentation === "reference-runtime" &&
|
|
762
|
+
technique.previewRenderer !== technique.rendererStrategy &&
|
|
763
|
+
!technique.referenceRendererChangeReason?.trim()
|
|
764
|
+
) {
|
|
765
|
+
errors.push("Reference runtime renderer changes require referenceRendererChangeReason.");
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
errors.push(...getRendererLayerErrors(technique));
|
|
769
|
+
|
|
770
|
+
return errors;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
export function validateToolcraftPerformanceCoverage(
|
|
774
|
+
schema: ResolvedToolcraftAppSchema,
|
|
775
|
+
config: ToolcraftPerformanceConfig,
|
|
776
|
+
): string[] {
|
|
777
|
+
const errors: string[] = [];
|
|
778
|
+
const scenariosByTarget = new Map<string, ToolcraftPerformanceScenario[]>();
|
|
779
|
+
const browserTestNamesByScenario = new Map<string, string>();
|
|
780
|
+
const customRendererStrategies = new Set<ToolcraftRendererStrategy>([
|
|
781
|
+
"dom",
|
|
782
|
+
"svg",
|
|
783
|
+
"canvas-2d",
|
|
784
|
+
"webgl",
|
|
785
|
+
"webgpu",
|
|
786
|
+
]);
|
|
787
|
+
const gpuRendererStrategies = new Set<ToolcraftRendererStrategy>(["webgl", "webgpu"]);
|
|
788
|
+
const controlsByTarget = new Map(
|
|
789
|
+
getAllSchemaControls(schema).map((control) => [control.target, control] as const),
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
errors.push(...getRendererTechniqueErrors(config));
|
|
793
|
+
|
|
794
|
+
if (config.usesCustomRenderer && !customRendererStrategies.has(config.rendererStrategy)) {
|
|
795
|
+
errors.push(
|
|
796
|
+
'Custom renderers must declare rendererStrategy "dom", "svg", "canvas-2d", "webgl", or "webgpu".',
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (!config.usesCustomRenderer && config.rendererStrategy !== "none") {
|
|
801
|
+
errors.push(
|
|
802
|
+
`Non-custom renderer configs must use rendererStrategy "none", received "${config.rendererStrategy}".`,
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (config.usesCustomRenderer && config.rendererWorkload === "none") {
|
|
807
|
+
errors.push(
|
|
808
|
+
'Custom renderers must declare rendererWorkload "simple-composition", "text-output", "vector-output", or "pixel-output".',
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (!config.usesCustomRenderer && config.rendererWorkload !== "none") {
|
|
813
|
+
errors.push(
|
|
814
|
+
`Non-custom renderer configs must use rendererWorkload "none", received "${config.rendererWorkload}".`,
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (
|
|
819
|
+
config.rendererWorkload === "pixel-output" &&
|
|
820
|
+
!gpuRendererStrategies.has(config.rendererStrategy)
|
|
821
|
+
) {
|
|
822
|
+
errors.push(
|
|
823
|
+
`rendererWorkload "pixel-output" must use rendererStrategy "webgl" or "webgpu", received "${config.rendererStrategy}".`,
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (config.rendererWorkload === "pixel-output") {
|
|
828
|
+
if (!hasStressPreviewOrAnimationScenario(config)) {
|
|
829
|
+
errors.push(
|
|
830
|
+
'rendererWorkload "pixel-output" must include a stress preview-render or animation-frame scenario with stress: true for the largest product canvas and heaviest workload values.',
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (!hasLongTaskBudgetScenario(config)) {
|
|
835
|
+
errors.push(
|
|
836
|
+
'rendererWorkload "pixel-output" must include at least one maxLongTaskMs budget so GPU-backed previews cannot pass while freezing the main thread.',
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (config.usesCustomRenderer && hasDetailHeavyRendererLayer(config.rendererTechnique)) {
|
|
842
|
+
if (!hasStressPreviewOrAnimationScenario(config)) {
|
|
843
|
+
errors.push(
|
|
844
|
+
"Detail-heavy custom renderers must include a stress preview-render or animation-frame scenario for the largest product canvas and heaviest workload values.",
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (!hasLongTaskBudgetScenario(config)) {
|
|
849
|
+
errors.push(
|
|
850
|
+
"Detail-heavy custom renderers must include at least one maxLongTaskMs budget so renderer technology can be revised when main-thread work stalls.",
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (
|
|
856
|
+
config.usesCustomRenderer &&
|
|
857
|
+
hasHighCountCanvas2DRendererLayer(config.rendererTechnique) &&
|
|
858
|
+
!hasStressPreviewOrAnimationScenario(config)
|
|
859
|
+
) {
|
|
860
|
+
errors.push(
|
|
861
|
+
"High-count Canvas 2D renderer layers must include stress preview-render or animation-frame evidence before delivery. If that stress evidence fails, revise renderer strategy instead of only lowering product workload.",
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
for (const scenario of config.scenarios) {
|
|
866
|
+
if (!scenario.id.trim()) {
|
|
867
|
+
errors.push("Performance scenario is missing an id.");
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (!scenario.fixture.trim()) {
|
|
871
|
+
errors.push(`${scenario.id} must name a representative fixture.`);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (!scenario.expectedObservable.trim()) {
|
|
875
|
+
errors.push(`${scenario.id} must describe a product-level performance observable.`);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (!hasAnyBudget(scenario.budget)) {
|
|
879
|
+
errors.push(`${scenario.id} must declare at least one numeric performance budget.`);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const missingBudgetFields = getMissingInteractionBudgetFields(scenario);
|
|
883
|
+
if (missingBudgetFields.length > 0) {
|
|
884
|
+
errors.push(
|
|
885
|
+
`${scenario.id} ${scenario.interaction} scenario must declare ${missingBudgetFields.join(
|
|
886
|
+
" and ",
|
|
887
|
+
)}.`,
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
errors.push(...getBudgetCapErrors(scenario));
|
|
892
|
+
|
|
893
|
+
if (!scenario.automated || !scenario.automatedTestName.trim()) {
|
|
894
|
+
errors.push(`${scenario.id} must point to an automated performance test.`);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (!scenario.browser || !scenario.browserTestName.trim()) {
|
|
898
|
+
errors.push(`${scenario.id} must point to a browser performance test.`);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (scenario.browser && scenario.browserTestName.trim()) {
|
|
902
|
+
const previousScenarioId = browserTestNamesByScenario.get(scenario.browserTestName);
|
|
903
|
+
|
|
904
|
+
if (previousScenarioId) {
|
|
905
|
+
errors.push(
|
|
906
|
+
`${scenario.id} browserTestName "${scenario.browserTestName}" is already used by ${previousScenarioId}. Give each performance scenario its own browser test so every control is actually exercised.`,
|
|
907
|
+
);
|
|
908
|
+
} else {
|
|
909
|
+
browserTestNamesByScenario.set(scenario.browserTestName, scenario.id);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (scenario.workload && !hasMinDefaultMax(scenario.values)) {
|
|
914
|
+
errors.push(`${scenario.id} workload scenario must include min/default/max values.`);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
errors.push(...getStressFixtureErrors(scenario, controlsByTarget.get(scenario.target ?? "")));
|
|
918
|
+
|
|
919
|
+
if (
|
|
920
|
+
requiresConcreteUiTarget(scenario.interaction) &&
|
|
921
|
+
!scenario.controlLabel?.trim() &&
|
|
922
|
+
!scenario.uiSelector?.trim()
|
|
923
|
+
) {
|
|
924
|
+
errors.push(
|
|
925
|
+
`${scenario.id} ${scenario.interaction} scenario must declare controlLabel or uiSelector for its real browser interaction.`,
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (scenario.target) {
|
|
930
|
+
const scenarios = scenariosByTarget.get(scenario.target) ?? [];
|
|
931
|
+
scenarios.push(scenario);
|
|
932
|
+
scenariosByTarget.set(scenario.target, scenarios);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const performanceTargets = new Set(config.workloadTargets);
|
|
937
|
+
const schemaTargets = new Set(
|
|
938
|
+
(schema.panels.controls?.sections ?? []).flatMap((section) =>
|
|
939
|
+
Object.values(section.controls).map((control) => control.target),
|
|
940
|
+
),
|
|
941
|
+
);
|
|
942
|
+
const sensitiveTargets = new Set(
|
|
943
|
+
collectToolcraftPerformanceSensitiveControls(schema).map((entry) => entry.target),
|
|
944
|
+
);
|
|
945
|
+
const performanceRoleConflicts = collectToolcraftPerformanceRoleConflicts(schema);
|
|
946
|
+
|
|
947
|
+
for (const { controlId, target } of performanceRoleConflicts) {
|
|
948
|
+
errors.push(
|
|
949
|
+
`${controlId} (${target}) looks performance-sensitive but declares performanceRole "responsiveness". Use performanceRole "workload" with workloadTargets and min/default/max coverage, or rename/restructure the control if it is truly lightweight.`,
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
for (const target of sensitiveTargets) {
|
|
954
|
+
if (!performanceTargets.has(target)) {
|
|
955
|
+
errors.push(
|
|
956
|
+
`${target} is performance-sensitive and must be listed in workloadTargets with min/default/max workload coverage.`,
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
for (const target of performanceTargets) {
|
|
962
|
+
if (!schemaTargets.has(target)) {
|
|
963
|
+
errors.push(`Performance workload target ${target} does not exist in schema controls.`);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const targetScenarios = scenariosByTarget.get(target) ?? [];
|
|
967
|
+
const hasWorkloadCoverage = targetScenarios.some(
|
|
968
|
+
(scenario) =>
|
|
969
|
+
scenario.workload &&
|
|
970
|
+
(scenario.interaction === "control-drag" ||
|
|
971
|
+
scenario.interaction === "control-change") &&
|
|
972
|
+
hasMinDefaultMax(scenario.values),
|
|
973
|
+
);
|
|
974
|
+
|
|
975
|
+
if (!hasWorkloadCoverage) {
|
|
976
|
+
errors.push(`${target} must have min/default/max workload performance coverage.`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
for (const target of getVisiblePerformanceControlTargets(schema)) {
|
|
981
|
+
if (!scenariosByTarget.has(target)) {
|
|
982
|
+
errors.push(
|
|
983
|
+
`${target} must have a performance scenario because every visible control can affect app responsiveness.`,
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (hasOutputDeliveryAction(schema)) {
|
|
989
|
+
const interactions = new Set(config.scenarios.map((scenario) => scenario.interaction));
|
|
990
|
+
|
|
991
|
+
if (!interactions.has("export-copy")) {
|
|
992
|
+
errors.push("Output actions must include an export-copy performance scenario.");
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (config.usesCustomRenderer) {
|
|
997
|
+
const interactions = new Set(config.scenarios.map((scenario) => scenario.interaction));
|
|
998
|
+
const hasAnimatedRendererScenario = interactions.has("animation-frame");
|
|
999
|
+
const needsViewportZoomStress =
|
|
1000
|
+
hasAnimatedRendererScenario || hasZoomSensitiveRenderer(config);
|
|
1001
|
+
|
|
1002
|
+
for (const requiredInteraction of [
|
|
1003
|
+
"preview-render",
|
|
1004
|
+
"control-drag",
|
|
1005
|
+
"viewport-stability",
|
|
1006
|
+
] as const) {
|
|
1007
|
+
if (!interactions.has(requiredInteraction)) {
|
|
1008
|
+
errors.push(
|
|
1009
|
+
`Custom renderers must include a ${requiredInteraction} performance scenario.`,
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
if (hasAnimatedRendererScenario) {
|
|
1015
|
+
const hasAnimatedViewportDrag = config.scenarios.some(
|
|
1016
|
+
(scenario) =>
|
|
1017
|
+
scenario.interaction === "animation-viewport-drag" &&
|
|
1018
|
+
scenario.stress === true,
|
|
1019
|
+
);
|
|
1020
|
+
|
|
1021
|
+
if (!hasAnimatedViewportDrag) {
|
|
1022
|
+
errors.push(
|
|
1023
|
+
"Animated custom renderers must include an animation-viewport-drag performance scenario that samples frames while physically moving the canvas viewport.",
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (needsViewportZoomStress) {
|
|
1029
|
+
const hasViewportZoomStress = config.scenarios.some(
|
|
1030
|
+
(scenario) =>
|
|
1031
|
+
scenario.interaction === "viewport-zoom-stress" &&
|
|
1032
|
+
scenario.stress === true,
|
|
1033
|
+
);
|
|
1034
|
+
|
|
1035
|
+
if (!hasViewportZoomStress) {
|
|
1036
|
+
errors.push(
|
|
1037
|
+
"Detail-heavy or animated custom renderers must include a viewport-zoom-stress performance scenario that uses real zoom controls while sampling frame gaps and long tasks.",
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (schema.canvas.upload && !interactions.has("media-import")) {
|
|
1043
|
+
errors.push(
|
|
1044
|
+
"Custom renderers with canvas upload must include a media-import performance scenario.",
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
if (hasKeyframeTimeline(schema)) {
|
|
1049
|
+
const hasKeyframeViewportStability = config.scenarios.some(
|
|
1050
|
+
(scenario) =>
|
|
1051
|
+
scenario.interaction === "viewport-stability" &&
|
|
1052
|
+
scenario.target === "timeline.keyframes",
|
|
1053
|
+
);
|
|
1054
|
+
|
|
1055
|
+
if (!hasKeyframeViewportStability) {
|
|
1056
|
+
errors.push(
|
|
1057
|
+
'Keyframe custom renderers must include a viewport-stability performance scenario with target "timeline.keyframes" that exercises zoom/radar, expanded keyframes, keyframe creation, and playback or scrubbing.',
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (hasLayersPanel(schema)) {
|
|
1063
|
+
const hasLayerViewportStability = config.scenarios.some(
|
|
1064
|
+
(scenario) =>
|
|
1065
|
+
scenario.interaction === "viewport-stability" &&
|
|
1066
|
+
scenario.target === "layers.interactions",
|
|
1067
|
+
);
|
|
1068
|
+
|
|
1069
|
+
if (!hasLayerViewportStability) {
|
|
1070
|
+
errors.push(
|
|
1071
|
+
'Layer-enabled custom renderers must include a viewport-stability performance scenario with target "layers.interactions" that exercises zoom/radar, layer selection, visibility, reorder or grouping, and selected-layer output stability.',
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
return errors;
|
|
1078
|
+
}
|