@pixel-point/toolcraft 0.0.3 → 0.0.6
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/package.json +1 -1
- package/scripts/prepare-pack.mjs +5 -0
- package/src/generate.mjs +13 -0
- package/src/generate.test.mjs +6 -0
- package/templates/runtime/contracts/component-contracts.test.ts +86 -8
- package/templates/runtime/contracts/component-contracts.ts +36 -8
- package/templates/runtime/contracts/decision-contracts.ts +2 -2
- package/templates/runtime/export/export.test.ts +65 -0
- package/templates/runtime/export/export.ts +54 -1
- package/templates/runtime/react/canvas-shell.test.tsx +7 -7
- package/templates/runtime/react/controls-panel.test.tsx +323 -6
- package/templates/runtime/react/controls-panel.tsx +349 -24
- package/templates/runtime/react/settings-transfer.test.ts +6 -0
- package/templates/runtime/react/settings-transfer.ts +28 -2
- package/templates/runtime/react/timeline-panel.test.tsx +69 -0
- package/templates/runtime/react/timeline-panel.tsx +98 -10
- package/templates/runtime/react/toolbar-panel.test.tsx +6 -6
- package/templates/runtime/react/toolcraft-app.integration.test.tsx +2 -2
- package/templates/runtime/schema/canvas-aspect-ratio-presets.ts +50 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +122 -2
- package/templates/runtime/schema/define-toolcraft.ts +197 -6
- package/templates/runtime/schema/keyframe-capability.test.ts +7 -0
- package/templates/runtime/schema/keyframe-capability.ts +2 -2
- package/templates/runtime/schema/runtime-targets.ts +6 -0
- package/templates/runtime/schema/types.ts +23 -1
- package/templates/runtime/state/canvas-zoom.ts +1 -1
- package/templates/runtime/state/create-template-state.test.ts +9 -3
- package/templates/runtime/state/reducer.test.ts +135 -2
- package/templates/runtime/state/reducer.ts +236 -12
- package/templates/runtime/state/types.ts +1 -0
- package/templates/starter/AGENTS.md +6 -4
- package/templates/starter/docs/toolcraft/README.md +1 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +4 -2
- package/templates/starter/docs/toolcraft/assembly-workflow.md +13 -4
- package/templates/starter/docs/toolcraft/component-rules.md +24 -5
- package/templates/starter/docs/toolcraft/performance.md +5 -0
- package/templates/starter/docs/toolcraft/renderer-technique.md +1 -1
- package/templates/starter/docs/toolcraft/schema-reference.md +53 -9
- package/templates/starter/gitignore +36 -0
- package/templates/starter/src/app/starter-acceptance.test.ts +678 -21
- package/templates/starter/src/app/starter-acceptance.ts +357 -4
- package/templates/ui/components/control-layout/index.tsx +4 -4
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +101 -18
- package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1 -1
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +4 -1
- package/templates/ui/components/controls/slider/slider-value.ts +48 -5
- package/templates/ui/components/primitives/editable-slider-value-label.tsx +6 -1
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getToolcraftCanvasSizeTargetDimension,
|
|
3
|
+
isToolcraftCanvasAspectRatioTarget,
|
|
4
|
+
} from "../schema/runtime-targets";
|
|
5
|
+
import { getToolcraftCanvasAspectRatioPreset } from "../schema/canvas-aspect-ratio-presets";
|
|
2
6
|
import {
|
|
3
7
|
clampToolcraftCanvasZoom,
|
|
4
8
|
toolcraftCanvasZoomDefault,
|
|
@@ -18,6 +22,16 @@ import type {
|
|
|
18
22
|
|
|
19
23
|
const minTimelineDurationSeconds = 1;
|
|
20
24
|
const maxTimelineDurationSeconds = 60;
|
|
25
|
+
const canvasAspectRatioTarget = "canvas.aspectRatio";
|
|
26
|
+
const canvasSizeWidthTarget = "canvas.size.width";
|
|
27
|
+
const canvasSizeHeightTarget = "canvas.size.height";
|
|
28
|
+
|
|
29
|
+
type CanvasAspectRatioValue = {
|
|
30
|
+
height: number;
|
|
31
|
+
mode: "custom" | "preset";
|
|
32
|
+
value: string;
|
|
33
|
+
width: number;
|
|
34
|
+
};
|
|
21
35
|
|
|
22
36
|
function asCanvasSizeDimension(value: unknown): number | null {
|
|
23
37
|
const numberValue =
|
|
@@ -34,6 +48,153 @@ function asCanvasSizeDimension(value: unknown): number | null {
|
|
|
34
48
|
return Math.max(1, Math.round(numberValue));
|
|
35
49
|
}
|
|
36
50
|
|
|
51
|
+
function getGreatestCommonDivisor(left: number, right: number): number {
|
|
52
|
+
let a = Math.abs(Math.round(left));
|
|
53
|
+
let b = Math.abs(Math.round(right));
|
|
54
|
+
|
|
55
|
+
while (b !== 0) {
|
|
56
|
+
const next = b;
|
|
57
|
+
b = a % b;
|
|
58
|
+
a = next;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return a || 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getCanvasAspectRatioFromSize(
|
|
65
|
+
size: ToolcraftState["canvas"]["size"],
|
|
66
|
+
): CanvasAspectRatioValue {
|
|
67
|
+
const divisor = getGreatestCommonDivisor(size.width, size.height);
|
|
68
|
+
const width = Math.max(1, Math.round(size.width / divisor));
|
|
69
|
+
const height = Math.max(1, Math.round(size.height / divisor));
|
|
70
|
+
const value = `${width}:${height}`;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
height,
|
|
74
|
+
mode: "custom",
|
|
75
|
+
value,
|
|
76
|
+
width,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
81
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseCanvasAspectRatioString(value: string): CanvasAspectRatioValue | null {
|
|
85
|
+
const match = /^\s*(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)\s*$/u.exec(value);
|
|
86
|
+
|
|
87
|
+
if (!match) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const width = asCanvasSizeDimension(match[1]);
|
|
92
|
+
const height = asCanvasSizeDimension(match[2]);
|
|
93
|
+
|
|
94
|
+
if (width === null || height === null) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
height,
|
|
100
|
+
mode: getToolcraftCanvasAspectRatioPreset(`${width}:${height}`)
|
|
101
|
+
? "preset"
|
|
102
|
+
: "custom",
|
|
103
|
+
value: `${width}:${height}`,
|
|
104
|
+
width,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeCanvasAspectRatioValue(
|
|
109
|
+
value: unknown,
|
|
110
|
+
fallbackSize: ToolcraftState["canvas"]["size"],
|
|
111
|
+
): CanvasAspectRatioValue {
|
|
112
|
+
if (typeof value === "string") {
|
|
113
|
+
return parseCanvasAspectRatioString(value) ?? getCanvasAspectRatioFromSize(fallbackSize);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (isRecord(value)) {
|
|
117
|
+
const width = asCanvasSizeDimension(value.width);
|
|
118
|
+
const height = asCanvasSizeDimension(value.height);
|
|
119
|
+
|
|
120
|
+
if (width !== null && height !== null) {
|
|
121
|
+
const rawValue = typeof value.value === "string" ? value.value : `${width}:${height}`;
|
|
122
|
+
const mode = value.mode === "preset" ? "preset" : "custom";
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
height,
|
|
126
|
+
mode,
|
|
127
|
+
value: mode === "preset" ? rawValue : `${width}:${height}`,
|
|
128
|
+
width,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return getCanvasAspectRatioFromSize(fallbackSize);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function canvasAspectRatioValuesEqual(
|
|
137
|
+
first: unknown,
|
|
138
|
+
second: CanvasAspectRatioValue,
|
|
139
|
+
): boolean {
|
|
140
|
+
if (!isRecord(first)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
first.height === second.height &&
|
|
146
|
+
first.mode === second.mode &&
|
|
147
|
+
first.value === second.value &&
|
|
148
|
+
first.width === second.width
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function applyCanvasAspectRatioToSize({
|
|
153
|
+
anchor,
|
|
154
|
+
ratio,
|
|
155
|
+
size,
|
|
156
|
+
value,
|
|
157
|
+
}: {
|
|
158
|
+
anchor: "height" | "width";
|
|
159
|
+
ratio: CanvasAspectRatioValue;
|
|
160
|
+
size: ToolcraftState["canvas"]["size"];
|
|
161
|
+
value: number;
|
|
162
|
+
}): ToolcraftState["canvas"]["size"] {
|
|
163
|
+
if (anchor === "width") {
|
|
164
|
+
return {
|
|
165
|
+
...size,
|
|
166
|
+
height: Math.max(1, Math.round((value * ratio.height) / ratio.width)),
|
|
167
|
+
width: value,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
...size,
|
|
173
|
+
height: value,
|
|
174
|
+
width: Math.max(1, Math.round((value * ratio.width) / ratio.height)),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function getCanvasAspectRatioPresetSize(
|
|
179
|
+
ratio: CanvasAspectRatioValue,
|
|
180
|
+
): ToolcraftState["canvas"]["size"] | null {
|
|
181
|
+
if (ratio.mode !== "preset") {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const preset = getToolcraftCanvasAspectRatioPreset(ratio.value);
|
|
186
|
+
|
|
187
|
+
if (!preset) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
height: preset.height,
|
|
193
|
+
unit: "px",
|
|
194
|
+
width: preset.width,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
37
198
|
function getResetCanvasSize(
|
|
38
199
|
state: ToolcraftState,
|
|
39
200
|
): ToolcraftState["canvas"]["size"] | null {
|
|
@@ -408,33 +569,91 @@ export function toolcraftReducer(
|
|
|
408
569
|
case "controls.setValue": {
|
|
409
570
|
const canvasSizeDimension = getToolcraftCanvasSizeTargetDimension(command.target);
|
|
410
571
|
|
|
411
|
-
if (
|
|
412
|
-
const
|
|
572
|
+
if (isToolcraftCanvasAspectRatioTarget(command.target)) {
|
|
573
|
+
const ratio = normalizeCanvasAspectRatioValue(command.value, state.canvas.size);
|
|
574
|
+
const size =
|
|
575
|
+
getCanvasAspectRatioPresetSize(ratio) ??
|
|
576
|
+
applyCanvasAspectRatioToSize({
|
|
577
|
+
anchor: "width",
|
|
578
|
+
ratio,
|
|
579
|
+
size: state.canvas.size,
|
|
580
|
+
value: state.canvas.size.width,
|
|
581
|
+
});
|
|
413
582
|
|
|
414
|
-
if (
|
|
583
|
+
if (
|
|
584
|
+
state.canvas.size.width === size.width &&
|
|
585
|
+
state.canvas.size.height === size.height &&
|
|
586
|
+
canvasAspectRatioValuesEqual(state.values[command.target], ratio)
|
|
587
|
+
) {
|
|
415
588
|
return state;
|
|
416
589
|
}
|
|
417
590
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
591
|
+
return commitStatePatch(state, {
|
|
592
|
+
after: {
|
|
593
|
+
[canvasAspectRatioTarget]: ratio,
|
|
594
|
+
"canvas.size": size,
|
|
595
|
+
[canvasSizeWidthTarget]: size.width,
|
|
596
|
+
[canvasSizeHeightTarget]: size.height,
|
|
597
|
+
},
|
|
598
|
+
before: {
|
|
599
|
+
[canvasAspectRatioTarget]: state.values[command.target],
|
|
600
|
+
"canvas.size": state.canvas.size,
|
|
601
|
+
[canvasSizeWidthTarget]: state.values[canvasSizeWidthTarget],
|
|
602
|
+
[canvasSizeHeightTarget]: state.values[canvasSizeHeightTarget],
|
|
603
|
+
},
|
|
604
|
+
label: command.label ?? command.target,
|
|
605
|
+
}, {
|
|
606
|
+
group: command.historyGroup,
|
|
607
|
+
mode: command.history,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (canvasSizeDimension) {
|
|
612
|
+
const dimensionValue = asCanvasSizeDimension(command.value);
|
|
613
|
+
|
|
614
|
+
if (dimensionValue === null) {
|
|
422
615
|
return state;
|
|
423
616
|
}
|
|
424
617
|
|
|
618
|
+
const hasAspectRatioControl =
|
|
619
|
+
canvasAspectRatioTarget in state.values ||
|
|
620
|
+
canvasAspectRatioTarget in state.defaults;
|
|
425
621
|
const size = {
|
|
426
622
|
...state.canvas.size,
|
|
427
623
|
[canvasSizeDimension]: dimensionValue,
|
|
428
624
|
};
|
|
625
|
+
const aspectRatio = getCanvasAspectRatioFromSize(size);
|
|
626
|
+
const targetValue = size[canvasSizeDimension];
|
|
627
|
+
const otherTarget =
|
|
628
|
+
canvasSizeDimension === "width" ? canvasSizeHeightTarget : canvasSizeWidthTarget;
|
|
629
|
+
const otherValue = canvasSizeDimension === "width" ? size.height : size.width;
|
|
630
|
+
|
|
631
|
+
const sizeUnchanged =
|
|
632
|
+
state.canvas.size.width === size.width &&
|
|
633
|
+
state.canvas.size.height === size.height &&
|
|
634
|
+
state.values[command.target] === targetValue &&
|
|
635
|
+
state.values[otherTarget] === otherValue;
|
|
636
|
+
|
|
637
|
+
if (sizeUnchanged) {
|
|
638
|
+
return state;
|
|
639
|
+
}
|
|
429
640
|
|
|
430
641
|
return commitStatePatch(state, {
|
|
431
642
|
after: {
|
|
643
|
+
...(hasAspectRatioControl
|
|
644
|
+
? { [canvasAspectRatioTarget]: aspectRatio }
|
|
645
|
+
: {}),
|
|
432
646
|
"canvas.size": size,
|
|
433
|
-
[command.target]:
|
|
647
|
+
[command.target]: targetValue,
|
|
648
|
+
[otherTarget]: otherValue,
|
|
434
649
|
},
|
|
435
650
|
before: {
|
|
651
|
+
...(hasAspectRatioControl
|
|
652
|
+
? { [canvasAspectRatioTarget]: state.values[canvasAspectRatioTarget] }
|
|
653
|
+
: {}),
|
|
436
654
|
"canvas.size": state.canvas.size,
|
|
437
655
|
[command.target]: state.values[command.target],
|
|
656
|
+
[otherTarget]: state.values[otherTarget],
|
|
438
657
|
},
|
|
439
658
|
label: command.label ?? command.target,
|
|
440
659
|
}, {
|
|
@@ -809,13 +1028,18 @@ export function toolcraftReducer(
|
|
|
809
1028
|
};
|
|
810
1029
|
|
|
811
1030
|
case "media.import": {
|
|
812
|
-
const shouldReplaceSingleLayerMedia =
|
|
1031
|
+
const shouldReplaceSingleLayerMedia =
|
|
1032
|
+
!state.schema.panels.layers && command.replaceExisting !== false;
|
|
813
1033
|
const shouldResizeCanvas =
|
|
814
1034
|
state.schema.canvas.sizing.mode === "intrinsic-media";
|
|
815
1035
|
const layerId =
|
|
816
|
-
command.asset.layerId ??
|
|
1036
|
+
command.asset.layerId ??
|
|
1037
|
+
(shouldReplaceSingleLayerMedia ? getSingleLayerImportId(state) : undefined) ??
|
|
1038
|
+
getNextLayerId(state);
|
|
817
1039
|
const mediaId =
|
|
818
|
-
command.asset.id ??
|
|
1040
|
+
command.asset.id ??
|
|
1041
|
+
(shouldReplaceSingleLayerMedia ? getSingleMediaImportId(state) : undefined) ??
|
|
1042
|
+
getNextMediaId(state);
|
|
819
1043
|
const layer = {
|
|
820
1044
|
displayName: command.asset.layerName ?? getImportedLayerName(command.asset.fileName),
|
|
821
1045
|
id: layerId,
|
|
@@ -23,8 +23,8 @@ Then follow `workflow.md` to choose the required contract docs and verification
|
|
|
23
23
|
9. Animated preview renderers suspend or coalesce non-essential animation work during canvas drag, pan, pinch, zoom, and radar/center interactions, then resume without changing user playback state.
|
|
24
24
|
10. If a Figma URL is provided, inspect the Figma file through MCP and rebuild from its structure; never implement from a screenshot or by eye.
|
|
25
25
|
11. Choose an explicit persistence policy; use schema `persistence` for user-edited app settings that should survive reload, and test real reload restoration when localStorage is enabled.
|
|
26
|
-
12. Use schema `settingsTransfer: "auto"` for complex apps that need import/export of control settings; never implement settings import/export through `panelActions` or route-local file inputs. After adding, removing, or reorganizing controls, sections, timeline, or layers, recalculate settings-transfer eligibility. The runtime threshold is 12 product controls, 5 product sections, or weighted score 18. Visible `Canvas width
|
|
27
|
-
13. Product apps expose `Background`
|
|
26
|
+
12. Use schema `settingsTransfer: "auto"` for complex apps that need import/export of control settings; never implement settings import/export through `panelActions` or route-local file inputs. After adding, removing, or reorganizing controls, sections, timeline, or layers, recalculate settings-transfer eligibility. The runtime threshold is 12 product controls, 5 product sections, or weighted score 18. Visible `Aspect ratio`, `Canvas width`, and `Canvas height` controls are owned by `editable-output` canvas sizing, not by settings transfer. Runtime aspect presets apply canonical canvas sizes, with `16:9` equal to `1920x1080`; manual Canvas width/height edits keep the typed dimension, keep the other dimension unchanged, switch Aspect ratio to Custom, and show the reduced current ratio in custom ratio inputs; when no explicit product size is provided, the runtime default canvas size is also `1920x1080`. Non-vector raster, Canvas 2D, WebGL, and WebGPU previews set `canvas.renderScale: true`; the first technical runtime section then appends `Resolution scale` after canvas sizing so backing pixels can increase up to 2x without changing CSS/output size. Performance fixes must preserve the selected render scale and keep canvas preview responsive to sliders/high-frequency controls at that scale; diagnose the bottleneck before reducing quality. Do not pass budgets by silently downsampling, stretching a lower-resolution backing canvas, blurring output, or clamping `canvas.renderScale` below the user's chosen value. When settings transfer and editable-output canvas sizing are both enabled, the first technical `Setup` runtime section contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`, `Canvas height`, and, for raster outputs, `Resolution scale` in that order and renders without a visible section heading.
|
|
27
|
+
13. Product apps expose a required `Background` section directly before export settings. It contains a Switch labeled `Include` and a background color control with `label: false` in one equal-width inline row; PNG export wires those runtime values into the standard export helper while live preview, workspace canvas backing, and video export keep the background. Every app with `Export PNG` exposes `Image Export` with `export.image.format` and `export.image.resolution` as two `select` controls in one compact two-column inline row, and passes the selected resolution to `createToolcraftPngExportCanvas({ resolution })` so 2K/4K/8K change actual PNG dimensions. Animated apps with both PNG and video export place `Image Export` immediately before `Video Export`.
|
|
28
28
|
14. Keep `docs/toolcraft/agent-worklog.md` current with a decision trail, product decisions, evidence, verification, and risks.
|
|
29
29
|
15. Prove every visible entity through acceptance, browser, and performance coverage.
|
|
30
30
|
16. Workload performance scenarios must declare `stressFixture`; browser perf tests must use `getToolcraftPerformanceStressValue(appPerformance, scenarioId)` so heavy-case tests cannot use toy values.
|
|
@@ -146,7 +146,7 @@ Do not rerun `pnpm install` after every edit. Run it after fresh export, depende
|
|
|
146
146
|
|
|
147
147
|
Do not run the full browser performance suite for Tier 0-2 edits unless a performance checkpoint trigger applies.
|
|
148
148
|
|
|
149
|
-
Run a full performance checkpoint with `pnpm verify:perf` when the first working version of an app exists, when renderer/canvas/animation/export/timeline/layers change, after fixing a bug that previously broke functionality, after any performance optimization, or when the user asks to optimize performance, fix lag, remove jank, speed up animation, or stabilize drag/zoom.
|
|
149
|
+
Run a full performance checkpoint with `pnpm verify:perf` when the first working version of an app exists, when renderer/canvas/animation/export/timeline/layers change, when `canvas.renderScale` or the `Resolution scale` retina slider is added/enabled, after fixing a bug that previously broke functionality, after any performance optimization, or when the user asks to optimize performance, fix lag, remove jank, speed up animation, or stabilize drag/zoom.
|
|
150
150
|
|
|
151
151
|
Fast feature loops may defer full performance only when none of the checkpoint triggers above apply. Record the deferred check and reason in the verification note or worklog.
|
|
152
152
|
|
|
@@ -176,7 +176,9 @@ The app is complete only when:
|
|
|
176
176
|
- reset returns schema controls to `defaultValue`;
|
|
177
177
|
- sticky footer export actions operate on final product output at `state.canvas.size`;
|
|
178
178
|
- still products expose Export PNG; animated products expose Export Video plus Export PNG;
|
|
179
|
-
- PNG export uses `Background`
|
|
179
|
+
- PNG export uses the required `Background` section with `Include` plus unlabeled background color runtime controls, while live preview, workspace canvas backing, and video keep background;
|
|
180
|
+
- every PNG export includes `Image Export` format/resolution `select` controls, and passes `export.image.resolution` into `createToolcraftPngExportCanvas`;
|
|
181
|
+
- animated products with both PNG and video export place `Image Export` immediately before `Video Export`;
|
|
180
182
|
- all export paths use retina output dimensions from the standard export helper;
|
|
181
183
|
- layers are absent for single-layer apps and fully working when enabled;
|
|
182
184
|
- timeline is absent, playback, keyframes, or custom reference timeline according to product behavior;
|
|
@@ -29,7 +29,7 @@ Every implementation pass must choose a verification tier before editing. Use th
|
|
|
29
29
|
| Tier 3 | Renderer, canvas, timeline, layers, upload, export, zoom, heavy controls, or a performance checkpoint trigger | `pnpm verify:quick` + targeted browser/perf scenarios |
|
|
30
30
|
| Tier 4 | Final delivery, fresh export, runtime/template/contract changes, broad renderer/product rewrites | `pnpm verify:final` |
|
|
31
31
|
|
|
32
|
-
Run `pnpm verify:perf` when a performance checkpoint is triggered: first working app version, renderer/canvas/animation/export/timeline/layers changes, a fix for previously broken functionality, any performance optimization, or a user request to optimize performance, fix lag, remove jank, speed up animation, or stabilize drag/zoom.
|
|
32
|
+
Run `pnpm verify:perf` when a performance checkpoint is triggered: first working app version, renderer/canvas/animation/export/timeline/layers changes, adding/enabling `canvas.renderScale` or the `Resolution scale` retina slider, a fix for previously broken functionality, any performance optimization, or a user request to optimize performance, fix lag, remove jank, speed up animation, or stabilize drag/zoom.
|
|
33
33
|
|
|
34
34
|
Fresh folders or dependency changes need `pnpm install` before verification. Final delivery still starts the local app after the gate:
|
|
35
35
|
|
|
@@ -16,7 +16,7 @@ Every visible product entity must prove it works. A control is not accepted beca
|
|
|
16
16
|
|
|
17
17
|
`pnpm verify:final` must pass before final delivery. Incremental edits use the verification tier classifier from `assembly-workflow.md`: run targeted browser acceptance for the changed entity, and add `pnpm verify:perf` whenever a performance checkpoint is triggered.
|
|
18
18
|
|
|
19
|
-
A performance checkpoint is triggered by the first working app version, renderer/canvas/animation/export/timeline/layers changes, a fix for previously broken functionality, any performance optimization, or a user request to optimize performance, fix lag, remove jank, speed up animation, or stabilize drag/zoom.
|
|
19
|
+
A performance checkpoint is triggered by the first working app version, renderer/canvas/animation/export/timeline/layers changes, adding/enabling `canvas.renderScale` or the `Resolution scale` retina slider, a fix for previously broken functionality, any performance optimization, or a user request to optimize performance, fix lag, remove jank, speed up animation, or stabilize drag/zoom.
|
|
20
20
|
|
|
21
21
|
## Product Readiness
|
|
22
22
|
|
|
@@ -58,7 +58,7 @@ Each row should name:
|
|
|
58
58
|
|
|
59
59
|
The test gate rejects rows without matching automated and browser test names.
|
|
60
60
|
|
|
61
|
-
`fixed-output` canvas sizing must be deliberate. Its runtime acceptance row must explain why width and
|
|
61
|
+
`fixed-output` canvas sizing must be deliberate. Its runtime acceptance row must explain why width, height, and aspect ratio are non-editable. A default size from the prompt should use `editable-output`, which keeps the runtime Aspect ratio, Canvas width, and Canvas height controls.
|
|
62
62
|
|
|
63
63
|
When localStorage persistence is enabled, add a runtime acceptance row that proves reload behavior. The browser test must change a real user-facing setting, wait for persistence to write, call a real page reload, and verify the restored control value or product output. Importing a settings JSON file is not persistence coverage.
|
|
64
64
|
|
|
@@ -136,6 +136,8 @@ Valid acceptance evidence includes:
|
|
|
136
136
|
|
|
137
137
|
Product apps must include output delivery acceptance. Still-output apps need `Export PNG` evidence. Animated apps need both `Export Video` evidence and `Export PNG` evidence. Clipboard copy can be tested as an additional behavior, but it cannot replace export coverage.
|
|
138
138
|
|
|
139
|
+
Every app with `Export PNG` must exercise the separate `Image Export` section: choose at least two `export.image.format` values, choose at least two `export.image.resolution` values, export the image, and decode the result to prove file type and actual pixel dimensions changed. Animated apps with both `Export PNG` and `Export Video` still need this image-export coverage; `Video Export` does not replace it.
|
|
140
|
+
|
|
139
141
|
Async Export, Download, Copy, Generate, or Apply acceptance must prove the sticky footer top accent indicator is visible while the returned `onPanelAction` Promise is pending, advances when `reportProgress(0..1)` is called, and hides after it settles. Video export acceptance must prove frame-based progress updates during render/encode instead of only toggling a pending state.
|
|
140
142
|
|
|
141
143
|
Animated app acceptance must also exercise the separate `Video Export` section: choose at least two `export.video.format` values, choose at least two `export.video.resolution` values, verify unsupported MIME/container choices fall back safely, and assert exported video bytes, dimensions, MIME/container, and duration match runtime timeline state. The duration assertion must load the exported blob as a video, wait for metadata, and compare `video.duration` with the edited timeline duration; `blobSize > 0`, `blobType`, WebM parser fallback, or assigning the expected duration when metadata is missing are not enough.
|
|
@@ -138,7 +138,7 @@ Every product app exposes output background controls:
|
|
|
138
138
|
|
|
139
139
|
Preview, PNG export, and video export read the background color runtime value. PNG export passes the include-background runtime value to the export helper. Turning `export.includeBackground` off makes only PNG output transparent; live preview, workspace canvas backing, and video output keep the background.
|
|
140
140
|
|
|
141
|
-
Keep those controls together in one `Background` section
|
|
141
|
+
Keep those controls together in one required `Background` section directly before the first export settings section. With PNG export that first settings section is `Image Export`; with video-only export it is `Video Export`. Use an equal-width inline row with `export.includeBackground` on the left and the background color parameter on the right; each control occupies half the row. The switch label is `Include`; the color control uses `label: false` because the section title already supplies the background context.
|
|
142
142
|
|
|
143
143
|
Every product app needs output delivery in sticky footer `panelActions`. Still-output apps expose `Export PNG`. Animated apps expose `Export Video` and `Export PNG`. Clipboard copy is optional and never replaces export. If an odd number of footer actions leaves one action alone in the final row, that final action spans the full row.
|
|
144
144
|
|
|
@@ -146,12 +146,21 @@ Async product actions such as Export, Download, Copy, Generate, or Apply must re
|
|
|
146
146
|
|
|
147
147
|
For complex apps, use schema `settingsTransfer: "auto"` or `true` for settings import/export. Recalculate settings-transfer eligibility after adding, removing, or reorganizing controls, sections, timeline, or layers. The runtime threshold is 12 product controls, 5 product sections, or weighted score 18. Do not put Import Settings or Export Settings in sticky footer `panelActions`; runtime inserts the technical `Setup` settings-transfer section first without a visible section heading.
|
|
148
148
|
|
|
149
|
-
If the app also uses `editable-output` canvas sizing, that first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Canvas width`,
|
|
149
|
+
If the app also uses `editable-output` canvas sizing, that first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`, `Canvas height`, and optional `Resolution scale` in that order. Do not split the canvas size fields and settings-transfer actions into app-authored sections.
|
|
150
|
+
|
|
151
|
+
For non-vector raster, Canvas 2D, WebGL, or WebGPU previews, set `canvas.renderScale: true`. Runtime appends `Resolution scale` to the same first technical section. The slider changes backing resolution from `1x` to `2x` without changing visible canvas size; DOM/SVG/vector-native previews should not use it.
|
|
150
152
|
|
|
151
153
|
If a controls panel shows only `Export Settings` and `Import Settings` in the first runtime section, check the canvas sizing decision. Product-output apps usually need `editable-output`; intrinsic media and explicitly fixed output are the cases where visible canvas size inputs are absent.
|
|
152
154
|
|
|
153
155
|
For user-edited settings that should survive reload, use schema `persistence` with a stable app-specific key. When localStorage persistence is enabled, acceptance must prove a user setting restores after a real browser reload. Do not use settings import/export as a workaround for broken persistence.
|
|
154
156
|
|
|
157
|
+
Every app with `Export PNG` must include a separate `Image Export` controls section with:
|
|
158
|
+
|
|
159
|
+
- `export.image.format` as `select`, defaulting to `png`, with `png` and `jpg` baseline options;
|
|
160
|
+
- `export.image.resolution` as `select`, defaulting to `4k`, with `2k`, `4k`, and `8k` baseline options.
|
|
161
|
+
|
|
162
|
+
`Image Export` `Format` and `Resolution` are one compact workflow pair: render them in a two-column inline row by default. For still-output apps, place `Image Export` directly above sticky footer export buttons. For animated apps with both image and video export, place `Image Export` immediately before `Video Export`.
|
|
163
|
+
|
|
155
164
|
Animated apps with `Export Video` must include a separate `Video Export` controls section with at least:
|
|
156
165
|
|
|
157
166
|
- `export.video.format` as `select`, defaulting to `mp4`, with `mp4` and `webm` baseline options;
|
|
@@ -159,7 +168,7 @@ Animated apps with `Export Video` must include a separate `Video Export` control
|
|
|
159
168
|
|
|
160
169
|
Place `Video Export` as the final authored controls section directly above sticky footer export buttons. Treat `Format` and `Resolution` as a compact semantic pair and put them in one two-column inline row by default. Use vertical rows only when the compact row would clip labels or selected values, and record that fallback reason in the worklog.
|
|
161
170
|
|
|
162
|
-
Use standard export helpers. `createToolcraftPngExportCanvas`
|
|
171
|
+
Use standard export helpers. `createToolcraftPngExportCanvas` accepts `includeBackground` for runtime PNG transparency and `resolution` for image-export output size. Pass the selected `export.image.resolution` into the PNG helper so 2K/4K/8K produce actual 2048/4096/8192px long-edge PNGs. Do not rely on static `export.png.background` alone when the UI exposes background controls. Video export keeps background and still uses `getToolcraftRetinaExportSize`.
|
|
163
172
|
|
|
164
173
|
Video export must choose the actual MIME/container with `MediaRecorder.isTypeSupported(...)` or an explicit encoder/transcoder capability check. `MOV` and `ProRes` are allowed only when the app provides a custom encoder/transcoder and proves it with acceptance plus performance coverage. Treat `4K` as an export resolution target, not a hardcoded canvas lock. Offline rendered-frame export must encode or mux frame timestamps from runtime timeline time; real-time `canvas.captureStream()` plus `MediaRecorder` records wall-clock export time and is not enough when renderer work can be slower than playback. Browser acceptance must load the exported blob as a video, wait for metadata, and compare `video.duration` with the edited timeline duration; `blobSize > 0`, `blobType`, parser fallback, or assigning the expected duration in `catch` is not enough.
|
|
165
174
|
|
|
@@ -190,7 +199,7 @@ Do not rerun `pnpm install` after every edit. Run it after fresh export, depende
|
|
|
190
199
|
|
|
191
200
|
Use `pnpm verify:ui` when a tier calls for the browser acceptance suite without the performance suite. Use a focused named Playwright test instead when only one entity changed and the relevant test is already known.
|
|
192
201
|
|
|
193
|
-
Run a full performance checkpoint with `pnpm verify:perf` when the first working version of the app exists, when renderer/canvas/animation/export/timeline/layers change, after fixing a bug that previously broke functionality, after any performance optimization, or when the user asks to optimize performance, fix lag, remove jank, speed up animation, or stabilize drag/zoom.
|
|
202
|
+
Run a full performance checkpoint with `pnpm verify:perf` when the first working version of the app exists, when renderer/canvas/animation/export/timeline/layers change, when `canvas.renderScale` or the `Resolution scale` retina slider is added/enabled, after fixing a bug that previously broke functionality, after any performance optimization, or when the user asks to optimize performance, fix lag, remove jank, speed up animation, or stabilize drag/zoom.
|
|
194
203
|
|
|
195
204
|
Fast feature loops may defer full performance only when none of the checkpoint triggers apply. Record the deferred check and reason in the worklog.
|
|
196
205
|
|
|
@@ -50,6 +50,10 @@ Visual discrete sliders must declare `step`; the runtime derives one marker per
|
|
|
50
50
|
|
|
51
51
|
Schema sliders always render stacked at full width. Do not put `slider` or `rangeSlider` controls in two-column inline rows. The only built-in exception is `fontPicker`, whose letter-spacing and line-height footer sliders stay paired inside that component.
|
|
52
52
|
|
|
53
|
+
Use slider `unit` only for measurement or scale suffixes: `%`, `px`, `°`, `x`, `s`, `ms`, `fps`, `rows`, `cols`, or a similarly useful domain unit. Do not use `unit` to repeat the entity already named by the section or label. Avoid `Letters` + `letters`, `Shape Density / Count` + `shapes`, `Words` + `words`, `Symbols` + `symbols`, `Items` + `items`, `Particles` + `particles`, and `Layers` + `layers`. If the numeric value needs an entity noun to make sense, rename the label or section instead of appending the noun to the value. Compact units render tight (`70%`, `24px`, `1.2x`, `8s`); word or acronym units render with a space (`5 cols`, `17 fps`) only when they are truly needed.
|
|
54
|
+
|
|
55
|
+
Slider value labels are editable only when they contain a numeric value. Textual state labels such as `Normal` are display-only and must not expose hover or click editing affordances.
|
|
56
|
+
|
|
53
57
|
Range sliders are always full-width two-thumb controls. Do not put a `rangeSlider` in an inline row. Its `defaultValue` must start with different lower and upper values, such as `[20, 80]`, so the control does not collapse into a single-value slider.
|
|
54
58
|
|
|
55
59
|
Range slider value editing accepts common range separators such as `20/80`, `20-80`, `20 - 80`, `20 80`, and en-dash ranges. Use the built-in parser instead of adding custom label parsing.
|
|
@@ -124,9 +128,9 @@ Use `colorOpacity` when one product entity owns both color and opacity, such as
|
|
|
124
128
|
|
|
125
129
|
When one short numeric/text field and one plain `color` field configure the same entity, they can share a two-column inline row. Example: `Mask size` and `Color` belong in the same `Mask` row instead of two stacked rows. Do not put `colorOpacity` in inline rows.
|
|
126
130
|
|
|
127
|
-
Mixed inline rows require label parity: every field in that row has a visible label. The
|
|
131
|
+
Mixed inline rows require label parity: every field in that row has a visible label. The required `Background` section row is the only section-title-owned exception: use the switch label `Include` beside the background color parameter with `label: false`. Color fields in other mixed rows must not be unlabeled.
|
|
128
132
|
|
|
129
|
-
Renderer-owned output background is a base product control. Use a schema `color` target such as `appearance.background` or `scene.background`, add an `export.includeBackground` control for PNG transparency, and make preview/export read those runtime values. Keep them in one `Background` section.
|
|
133
|
+
Renderer-owned output background is a base product control. Use a schema `color` target such as `appearance.background` or `scene.background`, add an `export.includeBackground` control for PNG transparency, and make preview/export read those runtime values. Keep them in one required `Background` section directly before the first export settings section. With PNG export, that first section is `Image Export`; with video-only export, it is `Video Export`. Use one equal-width inline row with `export.includeBackground` on the left and `appearance.background` on the right when no other fit rule is violated. The switch label is `Include`, not `Include background`; the background color control uses `label: false`. Each control occupies one half of the row; do not shrink the toggle column to intrinsic width. `export.includeBackground` controls only PNG alpha; it must not make live preview, workspace canvas backing, or video output transparent. Do not hardcode a configurable background in CSS, Canvas `fillStyle`, or WebGL clear color.
|
|
130
134
|
|
|
131
135
|
## File Upload
|
|
132
136
|
|
|
@@ -134,6 +138,8 @@ Use `fileDrop` for source material uploads in the controls panel. Do not place u
|
|
|
134
138
|
|
|
135
139
|
In single-layer apps, the runtime shows uploaded image preview and clear button in the file control. Clearing removes source material from the renderer and canvas.
|
|
136
140
|
|
|
141
|
+
Use `multiple: true` when the app needs several uploaded images as one source set. The runtime appends media, switches to a four-column thumbnail grid when more than one image is present, puts the add-more tile last, and keeps per-image removal inside the file control.
|
|
142
|
+
|
|
137
143
|
In multi-layer apps, deletion and visibility belong to the Layers panel; `fileDrop` stays an upload target.
|
|
138
144
|
|
|
139
145
|
## Image Picker
|
|
@@ -211,15 +217,17 @@ Short labels must still be semantically sufficient with nearby context. `Animati
|
|
|
211
217
|
|
|
212
218
|
Visible control labels can get a runtime-owned filled Phosphor question tooltip icon. Put a concise product-specific explanation in `description` only when it adds meaning beyond the label. Do not write recaps like `Adjusts Opacity`, and do not build custom help icons beside built-in labels.
|
|
213
219
|
|
|
220
|
+
Do not add `description` to obvious color clusters. If a section title already names the palette/color context, sequential labels such as `Color 1`, `Color 2`, or simple palette controls such as `Spread` do not need help icons. Keep the whole obvious group clean unless the tooltip explains a non-obvious product behavior.
|
|
221
|
+
|
|
214
222
|
For compound controls such as `fontPicker`, `description` must not enumerate owned fields like font, weight, size, case, color, opacity, letter spacing, or line height. The component already labels those fields.
|
|
215
223
|
|
|
216
224
|
If a source label is unavoidably long, keep the visible label concise and rely on native `title` for the full text.
|
|
217
225
|
|
|
218
|
-
Switch and checkbox labels name the setting context, not the action. Do not prefix them with `Enable` or `Disable`; use `CRT`, `Glow`, `Loop`, or `Guides` instead of `Enable CRT` or `Disable guides`. If the section title already names the setting context, do not repeat that title as the visible toggle label; use `label: false`
|
|
226
|
+
Switch and checkbox labels name the setting context, not the action. Do not prefix them with `Enable` or `Disable`; use `CRT`, `Glow`, `Loop`, or `Guides` instead of `Enable CRT` or `Disable guides`. If the section title already names the setting context, do not repeat that title as the visible toggle label; use a short contextual label such as `Include` or, only for icon-only visual toggles, `label: false` with the meaning in `target` and `description`.
|
|
219
227
|
|
|
220
228
|
Two adjacent `switch` or `checkbox` controls for the same product entity must share one inline row when every visible label fits without truncation. Use short one- or two-word labels such as `Snap X` and `Snap Y`, or `Glow` and `Loop`. The runtime auto-pairs safe adjacent toggles by target entity; use explicit layout groups only when pairing a toggle with a non-toggle parameter. If either label would truncate in half-width, remove the inline group and let the toggles stack.
|
|
221
229
|
|
|
222
|
-
A single `switch` or `checkbox` may share an inline row with one related parameter control when the toggle label fits and the controls edit the same entity. Example: `Loop` plus `Duration`, or
|
|
230
|
+
A single `switch` or `checkbox` may share an inline row with one related parameter control when the toggle label fits and the controls edit the same entity. This row is always equal-width: each control occupies one half. Example: `Loop` plus `Duration`, or `Include` plus unlabeled background color inside the required `Background` section. If the section title already names the toggle context, shorten the label instead of repeating the title.
|
|
223
231
|
|
|
224
232
|
## Layers
|
|
225
233
|
|
|
@@ -267,16 +275,27 @@ Use schema `settingsTransfer` for settings import/export. Do not add Import Sett
|
|
|
267
275
|
|
|
268
276
|
Recalculate settings-transfer eligibility after adding, removing, or reorganizing controls, sections, timeline, or layers. The runtime threshold is 12 product controls, 5 product sections, or weighted score 18. If the threshold is reached, use `settingsTransfer: "auto"` / `true` or document a product-specific opt-out through `runtime.settingsTransfer` acceptance evidence.
|
|
269
277
|
|
|
270
|
-
When settings transfer and editable-output canvas sizing are both enabled, the first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Canvas width`,
|
|
278
|
+
When settings transfer and editable-output canvas sizing are both enabled, the first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`, `Canvas height`, and optional `Resolution scale` in that order. Do not split these into separate app-authored sections, rename the controls, or rebuild the block by hand.
|
|
271
279
|
|
|
272
280
|
If only `Export Settings` and `Import Settings` appear in that section, the schema is not using `editable-output` canvas sizing or already owns `canvas.size.width` / `canvas.size.height` controls. For product-output apps, prefer fixing the canvas sizing decision over adding hand-built size fields.
|
|
273
281
|
|
|
282
|
+
Manual `Canvas width` or `Canvas height` edits are exact output-size edits. They keep the other dimension unchanged, switch `Aspect ratio` to `Custom`, and update the custom ratio inputs to the reduced current ratio. Do not recreate the old behavior where typing one size field stays locked to the previous aspect preset.
|
|
283
|
+
|
|
284
|
+
Enable `canvas.renderScale: true` for non-vector raster previews such as Canvas 2D, WebGL, or WebGPU output. Runtime adds a `Resolution scale` slider after canvas sizing; it defaults to `2x` and lets users trade preview quality/performance without changing output size. Adding or enabling this slider requires a full `pnpm verify:perf` checkpoint. Performance fixes must preserve the selected scale and keep canvas preview responsive while dragging sliders or other high-frequency controls. Diagnose the actual bottleneck before lowering quality; do not silently downsample, stretch a lower-resolution backing canvas, blur output, or clamp `canvas.renderScale` below the user's chosen value. Do not enable it for DOM/SVG/vector-native previews.
|
|
285
|
+
|
|
274
286
|
Reset belongs to the controls panel header reset button. Do not add a footer action with `label`, `value`, or `command` containing reset; acceptance treats that as a duplicate Reset.
|
|
275
287
|
|
|
276
288
|
Still-output product apps include one primary `Export PNG` action.
|
|
277
289
|
|
|
278
290
|
Animated product apps include `Export Video` as the primary action and `Export PNG` as the secondary action.
|
|
279
291
|
|
|
292
|
+
Every product app with `Export PNG` includes a separate `Image Export` section. That section must contain:
|
|
293
|
+
|
|
294
|
+
- `export.image.format` as a `select`, with default value `png` and baseline options `png` and `jpg`;
|
|
295
|
+
- `export.image.resolution` as a `select`, with default value `4k` and baseline options `2k`, `4k`, and `8k`.
|
|
296
|
+
|
|
297
|
+
Place `Image Export` directly above sticky footer export buttons for still-output apps. For animated apps with both PNG and video export, place `Image Export` immediately before `Video Export`. `Format` and `Resolution` are one compact workflow pair: render them in a two-column inline row by default. Do not use `segmented` for this pair; it must visually match the Video Export dropdown structure.
|
|
298
|
+
|
|
280
299
|
Animated product apps with `Export Video` include a separate `Video Export` section. That section must contain:
|
|
281
300
|
|
|
282
301
|
- `export.video.format` as a `select`, with default value `mp4` and baseline options `mp4` and `webm`;
|
|
@@ -55,6 +55,8 @@ Ordinary controls still need lightweight responsiveness checks. They should not
|
|
|
55
55
|
- panel scroll affecting canvas zoom;
|
|
56
56
|
- timeline or layer interactions destabilizing the viewport.
|
|
57
57
|
|
|
58
|
+
When `canvas.renderScale` / `Resolution scale` is enabled, responsiveness coverage must include slider or other high-frequency control drags at the selected scale. If the canvas lags, diagnose the source before changing quality: renderer technique, React update frequency, decoded media, shader/program setup, buffer uploads, layout work, stale async renders, or animation scheduling.
|
|
59
|
+
|
|
58
60
|
## Renderer Performance
|
|
59
61
|
|
|
60
62
|
Custom renderers should:
|
|
@@ -105,8 +107,11 @@ Run a full performance checkpoint with `pnpm verify:perf` when:
|
|
|
105
107
|
|
|
106
108
|
- the first working version of the app exists;
|
|
107
109
|
- renderer, canvas, animation, export, timeline, or layers change;
|
|
110
|
+
- `canvas.renderScale` or the `Resolution scale` retina slider is added/enabled;
|
|
108
111
|
- a bug that previously broke functionality is fixed;
|
|
109
112
|
- a performance optimization lands;
|
|
110
113
|
- the user asks to optimize performance, fix lag, remove jank, speed up animation, or stabilize drag/zoom.
|
|
111
114
|
|
|
115
|
+
Performance fixes must preserve selected output and preview quality. Do not pass budgets by lowering image quality, selected `canvas.renderScale`, export resolution, source media fidelity, or canvas backing pixels unless the user explicitly chooses that lower-quality value through a visible control. Prefer coalescing slider updates, caching expensive inputs, moving work off the React render path, reusing GPU resources, or changing renderer strategy over reducing visual fidelity.
|
|
116
|
+
|
|
112
117
|
Do not use the full performance suite as the default loop for Tier 0-2 edits. Those edits still need the targeted checks named by the verification tier, but they should not pay for renderer and viewport stress tests unless a checkpoint trigger applies. If a fast feature loop defers full performance, record the deferred check and reason in the worklog.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Choose render technology per product layer. Do not choose a renderer because it is convenient; choose it from product output semantics, reference behavior, fidelity, and workload.
|
|
4
4
|
|
|
5
|
-
The initial renderer choice is provisional. It becomes accepted only after the app passes performance checks with the largest useful product canvas and the heaviest useful values for its own controls. If those checks show frame gaps, long tasks, viewport shaking, slow export, or interaction jank, revise the renderer strategy from that evidence before delivery.
|
|
5
|
+
The initial renderer choice is provisional. It becomes accepted only after the app passes performance checks with the largest useful product canvas and the heaviest useful values for its own controls. If those checks show frame gaps, long tasks, viewport shaking, slow export, or interaction jank, revise the renderer strategy from that evidence before delivery. Do not make a renderer look fast by silently reducing the selected preview scale, backing pixels, source media quality, or export fidelity.
|
|
6
6
|
|
|
7
7
|
## Strategy Guide
|
|
8
8
|
|