@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
|
@@ -57,6 +57,10 @@ import type {
|
|
|
57
57
|
ToolcraftControlSchema,
|
|
58
58
|
ResolvedToolcraftAppSchema,
|
|
59
59
|
} from "../schema/types";
|
|
60
|
+
import {
|
|
61
|
+
getToolcraftCanvasAspectRatioPreset,
|
|
62
|
+
toolcraftCanvasAspectRatioPresets,
|
|
63
|
+
} from "../schema/canvas-aspect-ratio-presets";
|
|
60
64
|
import { getToolcraftControlKeyframeCapability } from "../schema/keyframe-capability";
|
|
61
65
|
import { getToolcraftCanvasSizeTargetDimension } from "../schema/runtime-targets";
|
|
62
66
|
import type {
|
|
@@ -112,9 +116,22 @@ type FooterActionProgressEntry = {
|
|
|
112
116
|
id: number;
|
|
113
117
|
progress: number | null;
|
|
114
118
|
};
|
|
119
|
+
type CanvasAspectRatioValue = {
|
|
120
|
+
height: number;
|
|
121
|
+
mode: "custom" | "preset";
|
|
122
|
+
value: string;
|
|
123
|
+
width: number;
|
|
124
|
+
};
|
|
115
125
|
|
|
116
126
|
const hiddenDiscreteMarkerCount = 2;
|
|
117
127
|
const controlsPanelSectionCollapseStorageVersion = 1;
|
|
128
|
+
const canvasAspectRatioOptions = [
|
|
129
|
+
...toolcraftCanvasAspectRatioPresets.map((preset) => ({
|
|
130
|
+
label: preset.value,
|
|
131
|
+
value: preset.value,
|
|
132
|
+
})),
|
|
133
|
+
{ label: "Custom...", value: "custom" },
|
|
134
|
+
] as const;
|
|
118
135
|
|
|
119
136
|
const sectionedCompoundControlTypes = new Set([
|
|
120
137
|
"channelMixer",
|
|
@@ -257,6 +274,86 @@ function asString(value: unknown, fallback = ""): string {
|
|
|
257
274
|
return typeof value === "number" && Number.isFinite(value) ? String(value) : fallback;
|
|
258
275
|
}
|
|
259
276
|
|
|
277
|
+
function parseCanvasAspectRatioOption(value: string): CanvasAspectRatioValue | null {
|
|
278
|
+
const preset = getToolcraftCanvasAspectRatioPreset(value);
|
|
279
|
+
|
|
280
|
+
if (preset) {
|
|
281
|
+
return {
|
|
282
|
+
height: preset.ratioHeight,
|
|
283
|
+
mode: "preset",
|
|
284
|
+
value: preset.value,
|
|
285
|
+
width: preset.ratioWidth,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const match = /^\s*(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)\s*$/u.exec(value);
|
|
290
|
+
|
|
291
|
+
if (!match) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const width = Number.parseFloat(match[1] ?? "");
|
|
296
|
+
const height = Number.parseFloat(match[2] ?? "");
|
|
297
|
+
|
|
298
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
height: Math.round(height),
|
|
304
|
+
mode: "custom",
|
|
305
|
+
value: `${Math.round(width)}:${Math.round(height)}`,
|
|
306
|
+
width: Math.round(width),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function asCanvasAspectRatioValue(
|
|
311
|
+
value: unknown,
|
|
312
|
+
fallback: unknown,
|
|
313
|
+
): CanvasAspectRatioValue {
|
|
314
|
+
if (isRecord(value)) {
|
|
315
|
+
const width = asNumber(value.width, NaN);
|
|
316
|
+
const height = asNumber(value.height, NaN);
|
|
317
|
+
const mode = value.mode === "preset" ? "preset" : "custom";
|
|
318
|
+
|
|
319
|
+
if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
|
|
320
|
+
return {
|
|
321
|
+
height: Math.round(height),
|
|
322
|
+
mode,
|
|
323
|
+
value:
|
|
324
|
+
typeof value.value === "string"
|
|
325
|
+
? value.value
|
|
326
|
+
: `${Math.round(width)}:${Math.round(height)}`,
|
|
327
|
+
width: Math.round(width),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (typeof value === "string") {
|
|
333
|
+
const parsed = parseCanvasAspectRatioOption(value);
|
|
334
|
+
|
|
335
|
+
if (parsed) {
|
|
336
|
+
return parsed;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (fallback !== value) {
|
|
341
|
+
return asCanvasAspectRatioValue(fallback, {
|
|
342
|
+
height: 1,
|
|
343
|
+
mode: "preset",
|
|
344
|
+
value: "1:1",
|
|
345
|
+
width: 1,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
height: 1,
|
|
351
|
+
mode: "preset",
|
|
352
|
+
value: "1:1",
|
|
353
|
+
width: 1,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
260
357
|
function asBoolean(value: unknown, fallback = false): boolean {
|
|
261
358
|
return typeof value === "boolean" ? value : fallback;
|
|
262
359
|
}
|
|
@@ -330,6 +427,8 @@ function formatControlValueLabel(
|
|
|
330
427
|
}
|
|
331
428
|
|
|
332
429
|
switch (control.type) {
|
|
430
|
+
case "aspectRatio":
|
|
431
|
+
return asCanvasAspectRatioValue(value, control.defaultValue).value;
|
|
333
432
|
case "checkbox":
|
|
334
433
|
case "switch":
|
|
335
434
|
return asBoolean(value) ? "On" : "Off";
|
|
@@ -505,6 +604,111 @@ function asFontPickerValue(value: unknown): FontPickerValue {
|
|
|
505
604
|
};
|
|
506
605
|
}
|
|
507
606
|
|
|
607
|
+
function CanvasAspectRatioControl({
|
|
608
|
+
defaultValue,
|
|
609
|
+
name,
|
|
610
|
+
onValueChange,
|
|
611
|
+
value,
|
|
612
|
+
}: {
|
|
613
|
+
defaultValue: unknown;
|
|
614
|
+
name: string;
|
|
615
|
+
onValueChange?: (
|
|
616
|
+
value: CanvasAspectRatioValue,
|
|
617
|
+
meta?: ControlChangeMeta,
|
|
618
|
+
) => void;
|
|
619
|
+
value: unknown;
|
|
620
|
+
}): React.JSX.Element {
|
|
621
|
+
const ratio = asCanvasAspectRatioValue(value, defaultValue);
|
|
622
|
+
const selectedValue = ratio.mode === "custom" ? "custom" : ratio.value;
|
|
623
|
+
|
|
624
|
+
function commitRatio(
|
|
625
|
+
nextRatio: CanvasAspectRatioValue,
|
|
626
|
+
meta?: ControlChangeMeta,
|
|
627
|
+
): void {
|
|
628
|
+
onValueChange?.(nextRatio, meta);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function updatePreset(nextValue: string): void {
|
|
632
|
+
if (nextValue === "custom") {
|
|
633
|
+
commitRatio({
|
|
634
|
+
height: ratio.height,
|
|
635
|
+
mode: "custom",
|
|
636
|
+
value: `${ratio.width}:${ratio.height}`,
|
|
637
|
+
width: ratio.width,
|
|
638
|
+
});
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const nextRatio = parseCanvasAspectRatioOption(nextValue);
|
|
643
|
+
|
|
644
|
+
if (nextRatio) {
|
|
645
|
+
commitRatio(nextRatio);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function updateCustomDimension(
|
|
650
|
+
dimension: "height" | "width",
|
|
651
|
+
nextValue: string,
|
|
652
|
+
meta?: ControlChangeMeta,
|
|
653
|
+
): void {
|
|
654
|
+
const numberValue = Number.parseFloat(nextValue);
|
|
655
|
+
|
|
656
|
+
if (!Number.isFinite(numberValue) || numberValue <= 0) {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const width =
|
|
661
|
+
dimension === "width" ? Math.max(1, Math.round(numberValue)) : ratio.width;
|
|
662
|
+
const height =
|
|
663
|
+
dimension === "height" ? Math.max(1, Math.round(numberValue)) : ratio.height;
|
|
664
|
+
|
|
665
|
+
commitRatio(
|
|
666
|
+
{
|
|
667
|
+
height,
|
|
668
|
+
mode: "custom",
|
|
669
|
+
value: `${width}:${height}`,
|
|
670
|
+
width,
|
|
671
|
+
},
|
|
672
|
+
meta,
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return (
|
|
677
|
+
<div className="min-w-0 space-y-2" data-slot="canvas-aspect-ratio-control">
|
|
678
|
+
<Select
|
|
679
|
+
layout="stacked"
|
|
680
|
+
name={name}
|
|
681
|
+
onValueChange={updatePreset}
|
|
682
|
+
options={canvasAspectRatioOptions}
|
|
683
|
+
value={selectedValue}
|
|
684
|
+
/>
|
|
685
|
+
{ratio.mode === "custom" ? (
|
|
686
|
+
<TextInput
|
|
687
|
+
inputs={[
|
|
688
|
+
{
|
|
689
|
+
commitOnBlur: true,
|
|
690
|
+
defaultValue: String(ratio.width),
|
|
691
|
+
name: "Width",
|
|
692
|
+
onValueChange: (nextValue, meta) =>
|
|
693
|
+
updateCustomDimension("width", nextValue, meta),
|
|
694
|
+
value: String(ratio.width),
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
commitOnBlur: true,
|
|
698
|
+
defaultValue: String(ratio.height),
|
|
699
|
+
name: "Height",
|
|
700
|
+
onValueChange: (nextValue, meta) =>
|
|
701
|
+
updateCustomDimension("height", nextValue, meta),
|
|
702
|
+
value: String(ratio.height),
|
|
703
|
+
},
|
|
704
|
+
]}
|
|
705
|
+
inputsPerRow={2}
|
|
706
|
+
/>
|
|
707
|
+
) : null}
|
|
708
|
+
</div>
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
|
|
508
712
|
function asActionSchemas(
|
|
509
713
|
actions: readonly (ToolcraftActionSchema | string)[] | undefined,
|
|
510
714
|
): readonly ToolcraftActionSchema[] {
|
|
@@ -690,6 +894,7 @@ function isRuntimeSetupSection(section: ToolcraftControlSectionSchema): boolean
|
|
|
690
894
|
return Object.values(section.controls).some(
|
|
691
895
|
(control) =>
|
|
692
896
|
control.type === "settingsTransfer" ||
|
|
897
|
+
control.target === "canvas.aspectRatio" ||
|
|
693
898
|
control.target === "canvas.size.width" ||
|
|
694
899
|
control.target === "canvas.size.height",
|
|
695
900
|
);
|
|
@@ -1765,6 +1970,17 @@ export function ControlsPanel({
|
|
|
1765
1970
|
);
|
|
1766
1971
|
}
|
|
1767
1972
|
|
|
1973
|
+
case "aspectRatio":
|
|
1974
|
+
return (
|
|
1975
|
+
<CanvasAspectRatioControl
|
|
1976
|
+
defaultValue={control.defaultValue}
|
|
1977
|
+
key={id}
|
|
1978
|
+
name={name}
|
|
1979
|
+
onValueChange={commit}
|
|
1980
|
+
value={value}
|
|
1981
|
+
/>
|
|
1982
|
+
);
|
|
1983
|
+
|
|
1768
1984
|
case "anchorGrid":
|
|
1769
1985
|
return withKeyframeLabelAction({
|
|
1770
1986
|
children: (
|
|
@@ -130,6 +130,12 @@ describe("settings transfer", () => {
|
|
|
130
130
|
|
|
131
131
|
expect(state.values["generation.prompt"]).toBe("Imported prompt");
|
|
132
132
|
expect(state.values["unknown.target"]).toBeUndefined();
|
|
133
|
+
expect(state.values["canvas.aspectRatio"]).toEqual({
|
|
134
|
+
height: 900,
|
|
135
|
+
mode: "custom",
|
|
136
|
+
value: "1600:900",
|
|
137
|
+
width: 1600,
|
|
138
|
+
});
|
|
133
139
|
expect(state.canvas.size).toEqual({ height: 900, unit: "px", width: 1600 });
|
|
134
140
|
expect(state.timeline).toMatchObject({
|
|
135
141
|
currentTimeSeconds: 3,
|
|
@@ -123,9 +123,33 @@ export function parseToolcraftSettingsPayload(
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
function applyCanvasSize(
|
|
126
|
-
dispatch:
|
|
126
|
+
{ dispatch, state }: ImportContext,
|
|
127
127
|
size: ToolcraftSettingsTransferPayload["canvas"]["size"],
|
|
128
|
+
options: { deriveAspectRatio: boolean },
|
|
128
129
|
): void {
|
|
130
|
+
const importableTargets = getKnownValueTargets(state.schema);
|
|
131
|
+
|
|
132
|
+
if (
|
|
133
|
+
options.deriveAspectRatio &&
|
|
134
|
+
importableTargets.has("canvas.aspectRatio") &&
|
|
135
|
+
isFinitePositiveNumber(size.width) &&
|
|
136
|
+
isFinitePositiveNumber(size.height)
|
|
137
|
+
) {
|
|
138
|
+
dispatch({
|
|
139
|
+
history: "merge",
|
|
140
|
+
historyGroup: settingsTransferImportHistoryGroup,
|
|
141
|
+
label: "Import settings",
|
|
142
|
+
target: "canvas.aspectRatio",
|
|
143
|
+
type: "controls.setValue",
|
|
144
|
+
value: {
|
|
145
|
+
height: size.height,
|
|
146
|
+
mode: "custom",
|
|
147
|
+
value: `${size.width}:${size.height}`,
|
|
148
|
+
width: size.width,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
129
153
|
if (isFinitePositiveNumber(size.width)) {
|
|
130
154
|
dispatch({
|
|
131
155
|
history: "merge",
|
|
@@ -211,7 +235,9 @@ export function applyToolcraftSettingsPayload(
|
|
|
211
235
|
});
|
|
212
236
|
}
|
|
213
237
|
|
|
214
|
-
applyCanvasSize(context
|
|
238
|
+
applyCanvasSize(context, payload.canvas.size, {
|
|
239
|
+
deriveAspectRatio: !Object.hasOwn(payload.values, "canvas.aspectRatio"),
|
|
240
|
+
});
|
|
215
241
|
applyTimeline(context, payload.timeline);
|
|
216
242
|
}
|
|
217
243
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ToolcraftCanvasSize } from "./types";
|
|
2
|
+
|
|
3
|
+
export type ToolcraftCanvasAspectRatioPresetValue =
|
|
4
|
+
| "1:1"
|
|
5
|
+
| "3:2"
|
|
6
|
+
| "16:9"
|
|
7
|
+
| "3:4"
|
|
8
|
+
| "9:16"
|
|
9
|
+
| "2:3"
|
|
10
|
+
| "4:3";
|
|
11
|
+
|
|
12
|
+
export type ToolcraftCanvasAspectRatioPreset = {
|
|
13
|
+
height: number;
|
|
14
|
+
ratioHeight: number;
|
|
15
|
+
ratioWidth: number;
|
|
16
|
+
value: ToolcraftCanvasAspectRatioPresetValue;
|
|
17
|
+
width: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const toolcraftCanvasAspectRatioPresets = [
|
|
21
|
+
{ height: 1080, ratioHeight: 1, ratioWidth: 1, value: "1:1", width: 1080 },
|
|
22
|
+
{ height: 1080, ratioHeight: 2, ratioWidth: 3, value: "3:2", width: 1620 },
|
|
23
|
+
{ height: 1080, ratioHeight: 9, ratioWidth: 16, value: "16:9", width: 1920 },
|
|
24
|
+
{ height: 1440, ratioHeight: 4, ratioWidth: 3, value: "3:4", width: 1080 },
|
|
25
|
+
{ height: 1920, ratioHeight: 16, ratioWidth: 9, value: "9:16", width: 1080 },
|
|
26
|
+
{ height: 1620, ratioHeight: 3, ratioWidth: 2, value: "2:3", width: 1080 },
|
|
27
|
+
{ height: 1080, ratioHeight: 3, ratioWidth: 4, value: "4:3", width: 1440 },
|
|
28
|
+
] as const satisfies readonly ToolcraftCanvasAspectRatioPreset[];
|
|
29
|
+
|
|
30
|
+
export const toolcraftCanvasAspectRatioPresetValues = new Set<string>(
|
|
31
|
+
toolcraftCanvasAspectRatioPresets.map((preset) => preset.value),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export function getToolcraftCanvasAspectRatioPreset(
|
|
35
|
+
value: string,
|
|
36
|
+
): ToolcraftCanvasAspectRatioPreset | null {
|
|
37
|
+
return (
|
|
38
|
+
toolcraftCanvasAspectRatioPresets.find((preset) => preset.value === value) ?? null
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getToolcraftCanvasAspectRatioPresetBySize(
|
|
43
|
+
size: ToolcraftCanvasSize,
|
|
44
|
+
): ToolcraftCanvasAspectRatioPreset | null {
|
|
45
|
+
return (
|
|
46
|
+
toolcraftCanvasAspectRatioPresets.find(
|
|
47
|
+
(preset) => preset.width === size.width && preset.height === size.height,
|
|
48
|
+
) ?? null
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
toolcraftRuntimeOwnedTargets,
|
|
11
11
|
getToolcraftCanvasSizeTargetDimension,
|
|
12
12
|
isToolcraftReservedTarget,
|
|
13
|
+
isToolcraftRuntimeOwnedTarget,
|
|
13
14
|
} from "./runtime-targets";
|
|
14
15
|
|
|
15
16
|
describe("defineToolcraft", () => {
|
|
@@ -111,6 +112,10 @@ describe("defineToolcraft", () => {
|
|
|
111
112
|
mode: "auto",
|
|
112
113
|
});
|
|
113
114
|
expect(app.panels.controls?.sections[0]?.title).toBe("Setup");
|
|
115
|
+
expect(app.panels.controls?.sections[0]?.controls.canvasAspectRatio).toMatchObject({
|
|
116
|
+
target: "canvas.aspectRatio",
|
|
117
|
+
type: "aspectRatio",
|
|
118
|
+
});
|
|
114
119
|
expect(app.panels.controls?.sections[0]?.controls.canvasWidth).toBeTruthy();
|
|
115
120
|
});
|
|
116
121
|
|
|
@@ -231,7 +236,7 @@ describe("defineToolcraft", () => {
|
|
|
231
236
|
});
|
|
232
237
|
});
|
|
233
238
|
|
|
234
|
-
it("ignores runtime canvas
|
|
239
|
+
it("ignores runtime canvas setup controls in settings transfer eligibility", () => {
|
|
235
240
|
const app = defineToolcraft({
|
|
236
241
|
canvas: { enabled: true, size: { height: 720, unit: "px", width: 1280 } },
|
|
237
242
|
panels: {
|
|
@@ -248,6 +253,7 @@ describe("defineToolcraft", () => {
|
|
|
248
253
|
settingsTransfer: false,
|
|
249
254
|
});
|
|
250
255
|
|
|
256
|
+
expect(app.panels.controls?.sections[0]?.controls.canvasAspectRatio).toBeTruthy();
|
|
251
257
|
expect(app.panels.controls?.sections[0]?.controls.canvasWidth).toBeTruthy();
|
|
252
258
|
expect(app.panels.controls?.sections[0]?.controls.canvasHeight).toBeTruthy();
|
|
253
259
|
expect(getToolcraftSettingsTransferEligibility({ panels: app.panels })).toEqual({
|
|
@@ -326,9 +332,20 @@ describe("defineToolcraft", () => {
|
|
|
326
332
|
expect(runtimeSettingsSection?.title).toBe("Setup");
|
|
327
333
|
expect(Object.keys(runtimeSettingsSection?.controls ?? {})).toEqual([
|
|
328
334
|
"settingsTransfer",
|
|
335
|
+
"canvasAspectRatio",
|
|
329
336
|
"canvasWidth",
|
|
330
337
|
"canvasHeight",
|
|
331
338
|
]);
|
|
339
|
+
expect(runtimeSettingsSection?.controls.canvasAspectRatio).toMatchObject({
|
|
340
|
+
defaultValue: {
|
|
341
|
+
height: 9,
|
|
342
|
+
mode: "preset",
|
|
343
|
+
value: "16:9",
|
|
344
|
+
width: 16,
|
|
345
|
+
},
|
|
346
|
+
target: "canvas.aspectRatio",
|
|
347
|
+
type: "aspectRatio",
|
|
348
|
+
});
|
|
332
349
|
expect(runtimeSettingsSection?.controls.canvasWidth).toBeTruthy();
|
|
333
350
|
expect(runtimeSettingsSection?.controls.canvasHeight).toBeTruthy();
|
|
334
351
|
expect(runtimeSettingsSection?.controls.settingsTransfer?.type).toBe(
|
|
@@ -594,10 +611,12 @@ describe("defineToolcraft", () => {
|
|
|
594
611
|
|
|
595
612
|
it("publishes reserved targets for AI assembly boundaries", () => {
|
|
596
613
|
expect(toolcraftRuntimeOwnedTargets).toEqual([
|
|
614
|
+
"canvas.aspectRatio",
|
|
597
615
|
"canvas.size.width",
|
|
598
616
|
"canvas.size.height",
|
|
599
617
|
]);
|
|
600
618
|
expect(toolcraftReservedTargets).toEqual([
|
|
619
|
+
"canvas.aspectRatio",
|
|
601
620
|
"canvas.size.width",
|
|
602
621
|
"canvas.size.height",
|
|
603
622
|
"selectedLayer.opacity",
|
|
@@ -606,6 +625,7 @@ describe("defineToolcraft", () => {
|
|
|
606
625
|
expect(getToolcraftCanvasSizeTargetDimension("canvas.size.width")).toBe("width");
|
|
607
626
|
expect(getToolcraftCanvasSizeTargetDimension("canvas.size.height")).toBe("height");
|
|
608
627
|
expect(getToolcraftCanvasSizeTargetDimension("generation.prompt")).toBeNull();
|
|
628
|
+
expect(isToolcraftRuntimeOwnedTarget("canvas.aspectRatio")).toBe(true);
|
|
609
629
|
expect(isToolcraftReservedTarget("selectedLayer.opacity")).toBe(true);
|
|
610
630
|
expect(isToolcraftReservedTarget("generation.prompt")).toBe(false);
|
|
611
631
|
});
|
|
@@ -674,6 +694,18 @@ describe("defineToolcraft", () => {
|
|
|
674
694
|
expect(app.canvas.sizeSource).toBe("app");
|
|
675
695
|
expect(app.canvas.sizing).toEqual({ mode: "editable-output" });
|
|
676
696
|
expect(app.panels.controls?.sections[0]?.title).toBe("Setup");
|
|
697
|
+
expect(app.panels.controls?.sections[0]?.controls.canvasAspectRatio).toMatchObject({
|
|
698
|
+
defaultValue: {
|
|
699
|
+
height: 256,
|
|
700
|
+
mode: "custom",
|
|
701
|
+
value: "455:256",
|
|
702
|
+
width: 455,
|
|
703
|
+
},
|
|
704
|
+
label: "Aspect ratio",
|
|
705
|
+
performanceRole: "workload",
|
|
706
|
+
target: "canvas.aspectRatio",
|
|
707
|
+
type: "aspectRatio",
|
|
708
|
+
});
|
|
677
709
|
expect(app.panels.controls?.sections[0]?.controls.canvasWidth).toMatchObject({
|
|
678
710
|
defaultValue: 1365,
|
|
679
711
|
performanceRole: "workload",
|
|
@@ -724,6 +756,18 @@ describe("defineToolcraft", () => {
|
|
|
724
756
|
const [canvasSection, generationSection] = app.panels.controls?.sections ?? [];
|
|
725
757
|
|
|
726
758
|
expect(canvasSection?.title).toBe("Setup");
|
|
759
|
+
expect(canvasSection?.controls.canvasAspectRatio).toMatchObject({
|
|
760
|
+
defaultValue: {
|
|
761
|
+
height: 5,
|
|
762
|
+
mode: "custom",
|
|
763
|
+
value: "8:5",
|
|
764
|
+
width: 8,
|
|
765
|
+
},
|
|
766
|
+
label: "Aspect ratio",
|
|
767
|
+
performanceRole: "workload",
|
|
768
|
+
target: "canvas.aspectRatio",
|
|
769
|
+
type: "aspectRatio",
|
|
770
|
+
});
|
|
727
771
|
expect(canvasSection?.controls.canvasWidth).toMatchObject({
|
|
728
772
|
defaultValue: 1440,
|
|
729
773
|
label: "Canvas width",
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { TOOLCRAFT_COMPONENT_CONTRACTS } from "../contracts/component-contracts";
|
|
2
|
+
import {
|
|
3
|
+
getToolcraftCanvasAspectRatioPreset,
|
|
4
|
+
getToolcraftCanvasAspectRatioPresetBySize,
|
|
5
|
+
} from "./canvas-aspect-ratio-presets";
|
|
2
6
|
import type {
|
|
3
7
|
ToolcraftAssemblyCapability,
|
|
4
8
|
ToolcraftAssemblyCommand,
|
|
@@ -41,6 +45,7 @@ const canvasSizeControlTargets = {
|
|
|
41
45
|
height: "canvas.size.height",
|
|
42
46
|
width: "canvas.size.width",
|
|
43
47
|
} as const;
|
|
48
|
+
const canvasAspectRatioTarget = "canvas.aspectRatio";
|
|
44
49
|
const maxAutoInlineControlLabelLength = 18;
|
|
45
50
|
const settingsTransferTarget = "runtime.settingsTransfer";
|
|
46
51
|
const runtimeSetupSectionTitle = "Setup";
|
|
@@ -553,6 +558,7 @@ function isRuntimeOnlyActionControl(control: ToolcraftControlSchema): boolean {
|
|
|
553
558
|
function isSettingsTransferEligibilityControl(control: ToolcraftControlSchema): boolean {
|
|
554
559
|
return (
|
|
555
560
|
!isRuntimeOnlyActionControl(control) &&
|
|
561
|
+
control.target !== canvasAspectRatioTarget &&
|
|
556
562
|
control.target !== canvasSizeControlTargets.width &&
|
|
557
563
|
control.target !== canvasSizeControlTargets.height
|
|
558
564
|
);
|
|
@@ -699,25 +705,67 @@ function getCanvasSizeLayoutGroups(
|
|
|
699
705
|
: undefined;
|
|
700
706
|
}
|
|
701
707
|
|
|
708
|
+
function getGreatestCommonDivisor(left: number, right: number): number {
|
|
709
|
+
let a = Math.abs(Math.round(left));
|
|
710
|
+
let b = Math.abs(Math.round(right));
|
|
711
|
+
|
|
712
|
+
while (b !== 0) {
|
|
713
|
+
const next = b;
|
|
714
|
+
b = a % b;
|
|
715
|
+
a = next;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return a || 1;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function getCanvasAspectRatioDefaultValue(size: ToolcraftCanvasSize): {
|
|
722
|
+
height: number;
|
|
723
|
+
mode: "custom" | "preset";
|
|
724
|
+
value: string;
|
|
725
|
+
width: number;
|
|
726
|
+
} {
|
|
727
|
+
const divisor = getGreatestCommonDivisor(size.width, size.height);
|
|
728
|
+
const width = Math.max(1, Math.round(size.width / divisor));
|
|
729
|
+
const height = Math.max(1, Math.round(size.height / divisor));
|
|
730
|
+
const value = `${width}:${height}`;
|
|
731
|
+
const preset =
|
|
732
|
+
getToolcraftCanvasAspectRatioPreset(value) ??
|
|
733
|
+
getToolcraftCanvasAspectRatioPresetBySize(size);
|
|
734
|
+
|
|
735
|
+
return {
|
|
736
|
+
height: preset?.ratioHeight ?? height,
|
|
737
|
+
mode: preset ? "preset" : "custom",
|
|
738
|
+
value: preset?.value ?? value,
|
|
739
|
+
width: preset?.ratioWidth ?? width,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
702
743
|
function createCanvasSizeSection({
|
|
744
|
+
aspectRatioControl,
|
|
703
745
|
sizeControlIds,
|
|
704
746
|
sizeControls,
|
|
705
747
|
}: {
|
|
748
|
+
aspectRatioControl: ToolcraftControlSchema;
|
|
706
749
|
sizeControlIds: readonly string[];
|
|
707
750
|
sizeControls: ToolcraftControlSectionSchema["controls"];
|
|
708
751
|
}): ToolcraftControlSectionSchema {
|
|
709
752
|
return {
|
|
710
|
-
controls:
|
|
753
|
+
controls: {
|
|
754
|
+
canvasAspectRatio: aspectRatioControl,
|
|
755
|
+
...sizeControls,
|
|
756
|
+
},
|
|
711
757
|
layoutGroups: getCanvasSizeLayoutGroups(sizeControlIds),
|
|
712
758
|
title: runtimeSetupSectionTitle,
|
|
713
759
|
};
|
|
714
760
|
}
|
|
715
761
|
|
|
716
762
|
function mergeCanvasSizeControlsIntoSettingsTransferSection({
|
|
763
|
+
aspectRatioControl,
|
|
717
764
|
settingsTransferSection,
|
|
718
765
|
sizeControlIds,
|
|
719
766
|
sizeControls,
|
|
720
767
|
}: {
|
|
768
|
+
aspectRatioControl: ToolcraftControlSchema;
|
|
721
769
|
settingsTransferSection: ToolcraftControlSectionSchema;
|
|
722
770
|
sizeControlIds: readonly string[];
|
|
723
771
|
sizeControls: ToolcraftControlSectionSchema["controls"];
|
|
@@ -728,6 +776,7 @@ function mergeCanvasSizeControlsIntoSettingsTransferSection({
|
|
|
728
776
|
...settingsTransferSection,
|
|
729
777
|
controls: {
|
|
730
778
|
...settingsTransferSection.controls,
|
|
779
|
+
canvasAspectRatio: aspectRatioControl,
|
|
731
780
|
...sizeControls,
|
|
732
781
|
},
|
|
733
782
|
layoutGroups:
|
|
@@ -1275,6 +1324,14 @@ function normalizePanels({
|
|
|
1275
1324
|
|
|
1276
1325
|
const sizeControls: ToolcraftControlSectionSchema["controls"] = {};
|
|
1277
1326
|
const sizeControlIds: string[] = [];
|
|
1327
|
+
const aspectRatioControl: ToolcraftControlSchema = {
|
|
1328
|
+
defaultValue: getCanvasAspectRatioDefaultValue(canvas.size),
|
|
1329
|
+
label: "Aspect ratio",
|
|
1330
|
+
performanceReason: "Aspect ratio changes output dimensions and renderer workload.",
|
|
1331
|
+
performanceRole: "workload",
|
|
1332
|
+
target: canvasAspectRatioTarget,
|
|
1333
|
+
type: "aspectRatio",
|
|
1334
|
+
};
|
|
1278
1335
|
|
|
1279
1336
|
if (!hasControlTarget(panels, canvasSizeControlTargets.width)) {
|
|
1280
1337
|
sizeControls.canvasWidth = {
|
|
@@ -1315,11 +1372,12 @@ function normalizePanels({
|
|
|
1315
1372
|
|
|
1316
1373
|
const runtimeSettingsSection = settingsTransferSection
|
|
1317
1374
|
? mergeCanvasSizeControlsIntoSettingsTransferSection({
|
|
1375
|
+
aspectRatioControl,
|
|
1318
1376
|
settingsTransferSection,
|
|
1319
1377
|
sizeControlIds,
|
|
1320
1378
|
sizeControls,
|
|
1321
1379
|
})
|
|
1322
|
-
: createCanvasSizeSection({ sizeControlIds, sizeControls });
|
|
1380
|
+
: createCanvasSizeSection({ aspectRatioControl, sizeControlIds, sizeControls });
|
|
1323
1381
|
|
|
1324
1382
|
return {
|
|
1325
1383
|
...normalizedPanels,
|
|
@@ -51,6 +51,13 @@ describe("getToolcraftControlKeyframeCapability", () => {
|
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
it("blocks runtime-owned canvas size targets even when the visual type could otherwise animate", () => {
|
|
54
|
+
expect(
|
|
55
|
+
getToolcraftControlKeyframeCapability(control("slider", "canvas.aspectRatio")),
|
|
56
|
+
).toEqual({
|
|
57
|
+
capable: false,
|
|
58
|
+
reason: "runtime-owned-target",
|
|
59
|
+
});
|
|
60
|
+
|
|
54
61
|
expect(
|
|
55
62
|
getToolcraftControlKeyframeCapability(control("text", "canvas.size.width")),
|
|
56
63
|
).toEqual({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isToolcraftRuntimeOwnedTarget } from "./runtime-targets";
|
|
2
2
|
import type { ToolcraftControlSchema } from "./types";
|
|
3
3
|
|
|
4
4
|
export type ToolcraftControlKeyframeCapabilityReason =
|
|
@@ -30,7 +30,7 @@ const keyframeCapableControlTypes = new Set([
|
|
|
30
30
|
export function getToolcraftControlKeyframeCapability(
|
|
31
31
|
control: ToolcraftControlSchema,
|
|
32
32
|
): ToolcraftControlKeyframeCapability {
|
|
33
|
-
if (
|
|
33
|
+
if (isToolcraftRuntimeOwnedTarget(control.target)) {
|
|
34
34
|
return {
|
|
35
35
|
capable: false,
|
|
36
36
|
reason: "runtime-owned-target",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const toolcraftRuntimeOwnedTargets = [
|
|
2
|
+
"canvas.aspectRatio",
|
|
2
3
|
"canvas.size.width",
|
|
3
4
|
"canvas.size.height",
|
|
4
5
|
] as const;
|
|
@@ -27,6 +28,10 @@ export function getToolcraftCanvasSizeTargetDimension(
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
export function isToolcraftCanvasAspectRatioTarget(target: string): boolean {
|
|
32
|
+
return target === "canvas.aspectRatio";
|
|
33
|
+
}
|
|
34
|
+
|
|
30
35
|
export function isToolcraftReservedTarget(
|
|
31
36
|
target: string,
|
|
32
37
|
): target is ToolcraftReservedTarget {
|
|
@@ -54,6 +54,12 @@ describe("createToolcraftState", () => {
|
|
|
54
54
|
const state = createToolcraftState(app);
|
|
55
55
|
|
|
56
56
|
expect(state.values).toMatchObject({
|
|
57
|
+
"canvas.aspectRatio": {
|
|
58
|
+
height: 1,
|
|
59
|
+
mode: "preset",
|
|
60
|
+
value: "1:1",
|
|
61
|
+
width: 1,
|
|
62
|
+
},
|
|
57
63
|
"canvas.size.height": 1024,
|
|
58
64
|
"canvas.size.width": 1024,
|
|
59
65
|
});
|