@pixel-point/toolcraft 0.0.3 → 0.0.4
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/templates/runtime/contracts/component-contracts.test.ts +28 -7
- package/templates/runtime/contracts/component-contracts.ts +14 -7
- package/templates/runtime/contracts/decision-contracts.ts +1 -1
- package/templates/runtime/export/export.test.ts +65 -0
- package/templates/runtime/export/export.ts +54 -1
- package/templates/runtime/react/controls-panel.test.tsx +54 -6
- package/templates/runtime/react/controls-panel.tsx +216 -0
- package/templates/runtime/react/settings-transfer.test.ts +6 -0
- package/templates/runtime/react/settings-transfer.ts +28 -2
- package/templates/runtime/schema/canvas-aspect-ratio-presets.ts +50 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +45 -1
- package/templates/runtime/schema/define-toolcraft.ts +60 -2
- 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 +5 -0
- package/templates/runtime/state/create-template-state.test.ts +6 -0
- package/templates/runtime/state/reducer.test.ts +55 -0
- package/templates/runtime/state/reducer.ts +214 -9
- package/templates/starter/AGENTS.md +5 -3
- package/templates/starter/docs/toolcraft/acceptance-testing.md +3 -1
- package/templates/starter/docs/toolcraft/assembly-workflow.md +10 -3
- package/templates/starter/docs/toolcraft/component-rules.md +12 -5
- package/templates/starter/docs/toolcraft/schema-reference.md +45 -7
- package/templates/starter/src/app/starter-acceptance.test.ts +623 -21
- package/templates/starter/src/app/starter-acceptance.ts +290 -3
- package/templates/ui/components/control-layout/index.tsx +4 -4
- package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1 -1
|
@@ -103,13 +103,68 @@ describe("toolcraftReducer", () => {
|
|
|
103
103
|
const redone = toolcraftReducer(undone, { type: "history.redo" });
|
|
104
104
|
|
|
105
105
|
expect(changed.canvas.size.width).toBe(640);
|
|
106
|
+
expect(changed.canvas.size.height).toBe(410);
|
|
106
107
|
expect(changed.values["canvas.size.width"]).toBe(640);
|
|
108
|
+
expect(changed.values["canvas.size.height"]).toBe(410);
|
|
107
109
|
expect(changed.history.undo.at(-1)?.label).toBe("canvas.size.width");
|
|
108
110
|
expect(reset.canvas.size.width).toBe(1200);
|
|
111
|
+
expect(reset.canvas.size.height).toBe(768);
|
|
109
112
|
expect(reset.values["canvas.size.width"]).toBe(1200);
|
|
113
|
+
expect(reset.values["canvas.size.height"]).toBe(768);
|
|
110
114
|
expect(reset.history.undo.at(-1)?.label).toBe("Reset controls");
|
|
111
115
|
expect(undone.canvas.size.width).toBe(1200);
|
|
116
|
+
expect(undone.canvas.size.height).toBe(768);
|
|
112
117
|
expect(redone.canvas.size.width).toBe(640);
|
|
118
|
+
expect(redone.canvas.size.height).toBe(410);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("routes canvas aspect ratio presets through canvas runtime state", () => {
|
|
122
|
+
const state = createState();
|
|
123
|
+
|
|
124
|
+
const changed = toolcraftReducer(state, {
|
|
125
|
+
target: "canvas.aspectRatio",
|
|
126
|
+
type: "controls.setValue",
|
|
127
|
+
value: {
|
|
128
|
+
height: 9,
|
|
129
|
+
mode: "preset",
|
|
130
|
+
value: "16:9",
|
|
131
|
+
width: 16,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(changed.canvas.size).toEqual({ height: 1080, unit: "px", width: 1920 });
|
|
136
|
+
expect(changed.values["canvas.aspectRatio"]).toEqual({
|
|
137
|
+
height: 9,
|
|
138
|
+
mode: "preset",
|
|
139
|
+
value: "16:9",
|
|
140
|
+
width: 16,
|
|
141
|
+
});
|
|
142
|
+
expect(changed.values["canvas.size.width"]).toBe(1920);
|
|
143
|
+
expect(changed.values["canvas.size.height"]).toBe(1080);
|
|
144
|
+
expect(changed.history.undo.at(-1)?.label).toBe("canvas.aspectRatio");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("keeps canvas size edits locked to the current aspect ratio", () => {
|
|
148
|
+
const state = toolcraftReducer(createState(), {
|
|
149
|
+
target: "canvas.aspectRatio",
|
|
150
|
+
type: "controls.setValue",
|
|
151
|
+
value: {
|
|
152
|
+
height: 9,
|
|
153
|
+
mode: "preset",
|
|
154
|
+
value: "16:9",
|
|
155
|
+
width: 16,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const changed = toolcraftReducer(state, {
|
|
160
|
+
target: "canvas.size.height",
|
|
161
|
+
type: "controls.setValue",
|
|
162
|
+
value: "720",
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(changed.canvas.size).toEqual({ height: 720, unit: "px", width: 1280 });
|
|
166
|
+
expect(changed.values["canvas.size.height"]).toBe(720);
|
|
167
|
+
expect(changed.values["canvas.size.width"]).toBe(1280);
|
|
113
168
|
});
|
|
114
169
|
|
|
115
170
|
it("updates canvas offset without recording history", () => {
|
|
@@ -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,137 @@ 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 applyCanvasAspectRatioToSize({
|
|
137
|
+
anchor,
|
|
138
|
+
ratio,
|
|
139
|
+
size,
|
|
140
|
+
value,
|
|
141
|
+
}: {
|
|
142
|
+
anchor: "height" | "width";
|
|
143
|
+
ratio: CanvasAspectRatioValue;
|
|
144
|
+
size: ToolcraftState["canvas"]["size"];
|
|
145
|
+
value: number;
|
|
146
|
+
}): ToolcraftState["canvas"]["size"] {
|
|
147
|
+
if (anchor === "width") {
|
|
148
|
+
return {
|
|
149
|
+
...size,
|
|
150
|
+
height: Math.max(1, Math.round((value * ratio.height) / ratio.width)),
|
|
151
|
+
width: value,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
...size,
|
|
157
|
+
height: value,
|
|
158
|
+
width: Math.max(1, Math.round((value * ratio.width) / ratio.height)),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getCanvasAspectRatioPresetSize(
|
|
163
|
+
ratio: CanvasAspectRatioValue,
|
|
164
|
+
): ToolcraftState["canvas"]["size"] | null {
|
|
165
|
+
if (ratio.mode !== "preset") {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const preset = getToolcraftCanvasAspectRatioPreset(ratio.value);
|
|
170
|
+
|
|
171
|
+
if (!preset) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
height: preset.height,
|
|
177
|
+
unit: "px",
|
|
178
|
+
width: preset.width,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
37
182
|
function getResetCanvasSize(
|
|
38
183
|
state: ToolcraftState,
|
|
39
184
|
): ToolcraftState["canvas"]["size"] | null {
|
|
@@ -408,6 +553,45 @@ export function toolcraftReducer(
|
|
|
408
553
|
case "controls.setValue": {
|
|
409
554
|
const canvasSizeDimension = getToolcraftCanvasSizeTargetDimension(command.target);
|
|
410
555
|
|
|
556
|
+
if (isToolcraftCanvasAspectRatioTarget(command.target)) {
|
|
557
|
+
const ratio = normalizeCanvasAspectRatioValue(command.value, state.canvas.size);
|
|
558
|
+
const size =
|
|
559
|
+
getCanvasAspectRatioPresetSize(ratio) ??
|
|
560
|
+
applyCanvasAspectRatioToSize({
|
|
561
|
+
anchor: "width",
|
|
562
|
+
ratio,
|
|
563
|
+
size: state.canvas.size,
|
|
564
|
+
value: state.canvas.size.width,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
if (
|
|
568
|
+
state.canvas.size.width === size.width &&
|
|
569
|
+
state.canvas.size.height === size.height &&
|
|
570
|
+
Object.is(state.values[command.target], ratio)
|
|
571
|
+
) {
|
|
572
|
+
return state;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return commitStatePatch(state, {
|
|
576
|
+
after: {
|
|
577
|
+
[canvasAspectRatioTarget]: ratio,
|
|
578
|
+
"canvas.size": size,
|
|
579
|
+
[canvasSizeWidthTarget]: size.width,
|
|
580
|
+
[canvasSizeHeightTarget]: size.height,
|
|
581
|
+
},
|
|
582
|
+
before: {
|
|
583
|
+
[canvasAspectRatioTarget]: state.values[command.target],
|
|
584
|
+
"canvas.size": state.canvas.size,
|
|
585
|
+
[canvasSizeWidthTarget]: state.values[canvasSizeWidthTarget],
|
|
586
|
+
[canvasSizeHeightTarget]: state.values[canvasSizeHeightTarget],
|
|
587
|
+
},
|
|
588
|
+
label: command.label ?? command.target,
|
|
589
|
+
}, {
|
|
590
|
+
group: command.historyGroup,
|
|
591
|
+
mode: command.history,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
411
595
|
if (canvasSizeDimension) {
|
|
412
596
|
const dimensionValue = asCanvasSizeDimension(command.value);
|
|
413
597
|
|
|
@@ -415,26 +599,47 @@ export function toolcraftReducer(
|
|
|
415
599
|
return state;
|
|
416
600
|
}
|
|
417
601
|
|
|
602
|
+
const hasAspectRatioLock =
|
|
603
|
+
canvasAspectRatioTarget in state.values ||
|
|
604
|
+
canvasAspectRatioTarget in state.defaults;
|
|
605
|
+
const size = hasAspectRatioLock
|
|
606
|
+
? applyCanvasAspectRatioToSize({
|
|
607
|
+
anchor: canvasSizeDimension,
|
|
608
|
+
ratio: normalizeCanvasAspectRatioValue(
|
|
609
|
+
state.values[canvasAspectRatioTarget],
|
|
610
|
+
state.canvas.size,
|
|
611
|
+
),
|
|
612
|
+
size: state.canvas.size,
|
|
613
|
+
value: dimensionValue,
|
|
614
|
+
})
|
|
615
|
+
: {
|
|
616
|
+
...state.canvas.size,
|
|
617
|
+
[canvasSizeDimension]: dimensionValue,
|
|
618
|
+
};
|
|
619
|
+
const targetValue = size[canvasSizeDimension];
|
|
620
|
+
const otherTarget =
|
|
621
|
+
canvasSizeDimension === "width" ? canvasSizeHeightTarget : canvasSizeWidthTarget;
|
|
622
|
+
const otherValue = canvasSizeDimension === "width" ? size.height : size.width;
|
|
623
|
+
|
|
418
624
|
if (
|
|
419
|
-
state.canvas.size
|
|
420
|
-
state.
|
|
625
|
+
state.canvas.size.width === size.width &&
|
|
626
|
+
state.canvas.size.height === size.height &&
|
|
627
|
+
state.values[command.target] === targetValue &&
|
|
628
|
+
state.values[otherTarget] === otherValue
|
|
421
629
|
) {
|
|
422
630
|
return state;
|
|
423
631
|
}
|
|
424
632
|
|
|
425
|
-
const size = {
|
|
426
|
-
...state.canvas.size,
|
|
427
|
-
[canvasSizeDimension]: dimensionValue,
|
|
428
|
-
};
|
|
429
|
-
|
|
430
633
|
return commitStatePatch(state, {
|
|
431
634
|
after: {
|
|
432
635
|
"canvas.size": size,
|
|
433
|
-
[command.target]:
|
|
636
|
+
[command.target]: targetValue,
|
|
637
|
+
[otherTarget]: otherValue,
|
|
434
638
|
},
|
|
435
639
|
before: {
|
|
436
640
|
"canvas.size": state.canvas.size,
|
|
437
641
|
[command.target]: state.values[command.target],
|
|
642
|
+
[otherTarget]: state.values[otherTarget],
|
|
438
643
|
},
|
|
439
644
|
label: command.label ?? command.target,
|
|
440
645
|
}, {
|
|
@@ -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`. 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`, and `Canvas height` 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.
|
|
@@ -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;
|
|
@@ -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,19 @@ 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`, and `Canvas height` in that order. Do not split the canvas size fields and settings-transfer actions into app-authored sections.
|
|
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`, and `Canvas height` in that order. Do not split the canvas size fields and settings-transfer actions into app-authored sections.
|
|
150
150
|
|
|
151
151
|
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
152
|
|
|
153
153
|
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
154
|
|
|
155
|
+
Every app with `Export PNG` must include a separate `Image Export` controls section with:
|
|
156
|
+
|
|
157
|
+
- `export.image.format` as `select`, defaulting to `png`, with `png` and `jpg` baseline options;
|
|
158
|
+
- `export.image.resolution` as `select`, defaulting to `4k`, with `2k`, `4k`, and `8k` baseline options.
|
|
159
|
+
|
|
160
|
+
`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`.
|
|
161
|
+
|
|
155
162
|
Animated apps with `Export Video` must include a separate `Video Export` controls section with at least:
|
|
156
163
|
|
|
157
164
|
- `export.video.format` as `select`, defaulting to `mp4`, with `mp4` and `webm` baseline options;
|
|
@@ -159,7 +166,7 @@ Animated apps with `Export Video` must include a separate `Video Export` control
|
|
|
159
166
|
|
|
160
167
|
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
168
|
|
|
162
|
-
Use standard export helpers. `createToolcraftPngExportCanvas`
|
|
169
|
+
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
170
|
|
|
164
171
|
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
172
|
|
|
@@ -124,9 +124,9 @@ Use `colorOpacity` when one product entity owns both color and opacity, such as
|
|
|
124
124
|
|
|
125
125
|
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
126
|
|
|
127
|
-
Mixed inline rows require label parity: every field in that row has a visible label. The
|
|
127
|
+
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
128
|
|
|
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.
|
|
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 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
130
|
|
|
131
131
|
## File Upload
|
|
132
132
|
|
|
@@ -215,11 +215,11 @@ For compound controls such as `fontPicker`, `description` must not enumerate own
|
|
|
215
215
|
|
|
216
216
|
If a source label is unavoidably long, keep the visible label concise and rely on native `title` for the full text.
|
|
217
217
|
|
|
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`
|
|
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 a short contextual label such as `Include` or, only for icon-only visual toggles, `label: false` with the meaning in `target` and `description`.
|
|
219
219
|
|
|
220
220
|
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
221
|
|
|
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
|
|
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. 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
223
|
|
|
224
224
|
## Layers
|
|
225
225
|
|
|
@@ -267,7 +267,7 @@ Use schema `settingsTransfer` for settings import/export. Do not add Import Sett
|
|
|
267
267
|
|
|
268
268
|
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
269
|
|
|
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`, and `Canvas height` in that order. Do not split these into separate app-authored sections, rename the controls, or rebuild the block by hand.
|
|
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`, `Aspect ratio`, `Canvas width`, and `Canvas height` in that order. Do not split these into separate app-authored sections, rename the controls, or rebuild the block by hand.
|
|
271
271
|
|
|
272
272
|
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
273
|
|
|
@@ -277,6 +277,13 @@ Still-output product apps include one primary `Export PNG` action.
|
|
|
277
277
|
|
|
278
278
|
Animated product apps include `Export Video` as the primary action and `Export PNG` as the secondary action.
|
|
279
279
|
|
|
280
|
+
Every product app with `Export PNG` includes a separate `Image Export` section. That section must contain:
|
|
281
|
+
|
|
282
|
+
- `export.image.format` as a `select`, with default value `png` and baseline options `png` and `jpg`;
|
|
283
|
+
- `export.image.resolution` as a `select`, with default value `4k` and baseline options `2k`, `4k`, and `8k`.
|
|
284
|
+
|
|
285
|
+
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.
|
|
286
|
+
|
|
280
287
|
Animated product apps with `Export Video` include a separate `Video Export` section. That section must contain:
|
|
281
288
|
|
|
282
289
|
- `export.video.format` as a `select`, with default value `mp4` and baseline options `mp4` and `webm`;
|
|
@@ -45,9 +45,47 @@ export: {
|
|
|
45
45
|
- `appearance.background` or `scene.background` as a `color` control;
|
|
46
46
|
- `export.includeBackground` as a boolean/options control.
|
|
47
47
|
|
|
48
|
-
PNG exporters should call `createToolcraftPngExportCanvas({ background, includeBackground, state, render })`, where `background` and `
|
|
48
|
+
PNG exporters should call `createToolcraftPngExportCanvas({ background, includeBackground, resolution, state, render })`, where `background`, `includeBackground`, and `resolution` come from runtime state. `includeBackground` controls only PNG alpha; live preview, workspace canvas backing, and video export keep the product background. For every app with `Export PNG`, `resolution` comes from `export.image.resolution`: `2k`, `4k`, and `8k` render actual 2048/4096/8192px long-edge PNGs. `current` or omitted resolution falls back to retina sizing. Video export always includes the product background, uses `getToolcraftRetinaExportSize`, and must prove exported metadata duration matches the runtime timeline duration.
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
Every app with `Export PNG` exposes a separate `Image Export` controls section. For still-output apps it sits directly above sticky footer actions. For animated apps with both `Export PNG` and `Export Video`, it sits immediately before `Video Export`:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
{
|
|
54
|
+
title: "Image Export",
|
|
55
|
+
controls: {
|
|
56
|
+
imageFormat: {
|
|
57
|
+
defaultValue: "png",
|
|
58
|
+
label: "Format",
|
|
59
|
+
options: [
|
|
60
|
+
{ label: "PNG", value: "png" },
|
|
61
|
+
{ label: "JPG", value: "jpg" },
|
|
62
|
+
],
|
|
63
|
+
target: "export.image.format",
|
|
64
|
+
type: "select",
|
|
65
|
+
},
|
|
66
|
+
imageResolution: {
|
|
67
|
+
defaultValue: "4k",
|
|
68
|
+
label: "Resolution",
|
|
69
|
+
options: [
|
|
70
|
+
{ label: "2K", value: "2k" },
|
|
71
|
+
{ label: "4K", value: "4k" },
|
|
72
|
+
{ label: "8K", value: "8k" },
|
|
73
|
+
],
|
|
74
|
+
target: "export.image.resolution",
|
|
75
|
+
type: "select",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
layoutGroups: [
|
|
79
|
+
{
|
|
80
|
+
layout: "inline",
|
|
81
|
+
columns: 2,
|
|
82
|
+
controls: ["imageFormat", "imageResolution"],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Animated apps with `Export Video` also expose a separate `Video Export` controls section. Do not mix video export settings into renderer/effect sections. Place this section after `Image Export` as the final authored controls section directly above sticky footer export buttons. `Format` and `Resolution` are a compact semantic pair, so use an inline two-column layout by default; stack them only when labels or selected values would clip.
|
|
51
89
|
|
|
52
90
|
```ts
|
|
53
91
|
{
|
|
@@ -96,9 +134,9 @@ Choose sizing from product context:
|
|
|
96
134
|
|
|
97
135
|
For product output, export, copy, download, shader rendering, procedural rendering, or no single intrinsic source image, use `editable-output` unless the product explicitly needs `fixed-output`.
|
|
98
136
|
|
|
99
|
-
A prompt-provided base/default size is only the initial `canvas.size`. It must not remove the runtime Canvas width and Canvas height controls. Use `fixed-output` only when the reference or product explicitly locks dimensions, and add runtime acceptance with `canvasSizingCoverage: "fixed-output-size"`.
|
|
137
|
+
A prompt-provided base/default size is only the initial `canvas.size`. It must not remove the runtime Aspect ratio, Canvas width, and Canvas height controls. Aspect presets use canonical output sizes (`16:9` is `1920x1080`; the other presets are derived around a 1080px short edge or matching portrait long edge). Use `fixed-output` only when the reference or product explicitly locks dimensions, and add runtime acceptance with `canvasSizingCoverage: "fixed-output-size"`.
|
|
100
138
|
|
|
101
|
-
Resolved `canvas.size` exists for every canvas app, but visible `Canvas width
|
|
139
|
+
Resolved `canvas.size` exists for every canvas app, but visible `Aspect ratio`, `Canvas width`, and `Canvas height` controls are mandatory only for `editable-output` sizing. They do not depend on `settingsTransfer`: when settings transfer is off, the runtime prepends a technical `Setup` canvas size section without a visible heading; when settings transfer is on, the controls merge into the first technical `Setup` runtime settings section without a visible heading. Do not hand-build a duplicate size selector.
|
|
102
140
|
|
|
103
141
|
## Panels
|
|
104
142
|
|
|
@@ -203,9 +241,9 @@ Control labels are judged with their nearest visible context. Short property lab
|
|
|
203
241
|
|
|
204
242
|
If a target prefix has to be split across sections, the spec must name the workflow reason. Otherwise the acceptance validator treats the split as a sectioning error.
|
|
205
243
|
|
|
206
|
-
Switch and checkbox labels name the setting context only. Do not prefix them with `Enable` or `Disable`; use `CRT`, `Glow`, `Loop`, or `Guides` instead. If the nearest section title already names the context, do not duplicate it as the visible toggle label. Use
|
|
244
|
+
Switch and checkbox labels name the setting context only. Do not prefix them with `Enable` or `Disable`; use `CRT`, `Glow`, `Loop`, or `Guides` instead. If the nearest section title already names the context, do not duplicate it as the visible toggle label. Use a short contextual label such as `Include` or, only for icon-only visual toggles, `label: false` with the product meaning in `target` and `description`.
|
|
207
245
|
|
|
208
|
-
Inline two-column groups are preferred when controls tune one close product meaning and labels/values fit. Short numeric text pairs can be inline. Related short `select` pairs can be inline, especially workflow pairs such as `Format` + `Resolution`, `Codec` + `Profile`, or `Width unit` + `Height unit`. Use stacked one-control rows only as a fit fallback when a label, selected value, or option text would clip, truncate, or lose padding; record that fallback reason in the spec or worklog. A short numeric/text field may also pair with one related plain `color` field when both configure the same entity, such as `Mask size` and `Color` inside `Mask`. `colorOpacity` never renders in inline two-column groups; if either color control has opacity, keep the controls stacked. Color controls show visible field labels in mixed sections that contain any non-color control. Omit visible color labels only in color-only sections
|
|
246
|
+
Inline two-column groups are preferred when controls tune one close product meaning and labels/values fit. Short numeric text pairs can be inline. Related short `select` pairs can be inline, especially workflow pairs such as `Format` + `Resolution`, `Codec` + `Profile`, or `Width unit` + `Height unit`. Use stacked one-control rows only as a fit fallback when a label, selected value, or option text would clip, truncate, or lose padding; record that fallback reason in the spec or worklog. A short numeric/text field may also pair with one related plain `color` field when both configure the same entity, such as `Mask size` and `Color` inside `Mask`. `colorOpacity` never renders in inline two-column groups; if either color control has opacity, keep the controls stacked. Color controls show visible field labels in mixed sections that contain any non-color control. Omit visible color labels only in color-only sections. Mixed inline rows require visible labels on every field, except the required Background row where the color control uses `label: false` because the section title owns the context. Color fields in other mixed rows must not be unlabeled. Two adjacent `switch` or `checkbox` controls for the same product entity must share one inline row when both visible labels fit without truncation; the runtime auto-pairs safe adjacent toggles by target entity, and schemas should stack them only when either label is too long. A single `switch` or `checkbox` may share an inline row with one related parameter control when the toggle label fits and both controls edit the same entity; shorten the toggle label when the section title already supplies context. Toggle plus parameter rows are equal-width two-column rows: each control occupies one half, never intrinsic toggle width plus remaining space. The required Background row uses `Include` plus unlabeled background color. Schema `slider` and `rangeSlider` controls always stay stacked at full width; the only built-in exception is the paired letter-spacing and line-height footer sliders inside `fontPicker`. Do not place sibling controls for case, color, opacity, size, weight, letter spacing, or line height when the same text entity already uses `fontPicker`.
|
|
209
247
|
|
|
210
248
|
`rangeSlider` is always a full-width two-thumb control. Do not include it in `layoutGroups`. Its `defaultValue` must start with different lower and upper values, such as `[20, 80]`, so the two handles do not collapse into one apparent slider. Manual range labels accept built-in separators such as slash, hyphen, spaces, and dashes.
|
|
211
249
|
|
|
@@ -258,7 +296,7 @@ When enabled, the runtime inserts a technical `Setup` settings-transfer section
|
|
|
258
296
|
|
|
259
297
|
After adding, removing, or reorganizing controls, sections, timeline, or layers, recalculate settings-transfer eligibility. If the threshold is reached, use `"auto"` / `true` or add an explicit `runtime.settingsTransfer` opt-out acceptance row with product evidence.
|
|
260
298
|
|
|
261
|
-
When settings transfer is enabled and the canvas uses `editable-output` sizing, the first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Canvas width`, and `Canvas height` in that order. Do not split these into separate sections or recreate them manually.
|
|
299
|
+
When settings transfer is enabled and the canvas uses `editable-output` sizing, the first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`, and `Canvas height` in that order. Do not split these into separate sections or recreate them manually.
|
|
262
300
|
|
|
263
301
|
A settings-transfer section with only `Export Settings` and `Import Settings` means the canvas is not `editable-output` or the app already declares its own `canvas.size.width` / `canvas.size.height` controls. For product-output apps, treat that as a schema decision to review.
|
|
264
302
|
|