@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
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
} from 'react';
|
|
12
12
|
import {
|
|
13
13
|
Button,
|
|
14
|
-
EditableSliderValueLabel,
|
|
15
14
|
PanelSurface,
|
|
16
15
|
Popover,
|
|
17
16
|
PopoverContent,
|
|
@@ -74,7 +73,7 @@ const timelineRulerRightInsetPx = timelineTrackColumnBorderWidthPx / 2;
|
|
|
74
73
|
const timelineRowActionColumnWidthPx = 36;
|
|
75
74
|
const timelineExpandedTrackEndOffsetPx =
|
|
76
75
|
timelineTrackEndInsetPx + timelineRowActionColumnWidthPx + timelineTrackColumnBorderWidthPx;
|
|
77
|
-
const timelinePlayheadSafeZonePx =
|
|
76
|
+
const timelinePlayheadSafeZonePx = 7;
|
|
78
77
|
const timelinePlayheadHitAreaWidthPx =
|
|
79
78
|
timelineTrackColumnBorderWidthPx + timelinePlayheadSafeZonePx * 2;
|
|
80
79
|
const maxTimelineDurationSeconds = 60;
|
|
@@ -1667,7 +1666,7 @@ function TimelinePlaybackStrip({
|
|
|
1667
1666
|
<span
|
|
1668
1667
|
aria-hidden="true"
|
|
1669
1668
|
className={cn(
|
|
1670
|
-
'absolute bottom-px size-2 -translate-x-1/2 translate-y-1/2 rounded-full bg-[color:var(--link)] opacity-0 shadow-[0_2px_2px_color-mix(in_oklab,var(--background)_70%,transparent)] transition-[opacity,transform] duration-[120ms] ease-out group-hover/timeline-panel-surface:opacity-100 group-hover/timeline-strip:opacity-100 group-focus-visible/timeline-strip:opacity-100',
|
|
1669
|
+
'absolute bottom-px size-2 -translate-x-1/2 translate-y-1/2 rounded-full bg-[color:var(--link)] opacity-0 shadow-[0_2px_2px_color-mix(in_oklab,var(--background)_70%,transparent)] transition-[opacity,transform] duration-[120ms] ease-out before:absolute before:inset-[-4px] before:content-[""] group-hover/timeline-panel-surface:opacity-100 group-hover/timeline-strip:opacity-100 group-focus-visible/timeline-strip:opacity-100',
|
|
1671
1670
|
isScrubbing && 'scale-110 opacity-100',
|
|
1672
1671
|
)}
|
|
1673
1672
|
data-slot="timeline-playback-handle"
|
|
@@ -2498,6 +2497,99 @@ function useTimelineScrubber({
|
|
|
2498
2497
|
};
|
|
2499
2498
|
}
|
|
2500
2499
|
|
|
2500
|
+
function selectTimelineEditableText(node: HTMLElement): void {
|
|
2501
|
+
const selection = window.getSelection();
|
|
2502
|
+
|
|
2503
|
+
if (!selection) {
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
const range = document.createRange();
|
|
2508
|
+
range.selectNodeContents(node);
|
|
2509
|
+
selection.removeAllRanges();
|
|
2510
|
+
selection.addRange(range);
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
function TimelineDurationValue({
|
|
2514
|
+
durationSeconds,
|
|
2515
|
+
onCommit,
|
|
2516
|
+
}: {
|
|
2517
|
+
durationSeconds: number;
|
|
2518
|
+
onCommit: (value: string) => void;
|
|
2519
|
+
}): React.JSX.Element {
|
|
2520
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
2521
|
+
const editorRef = useRef<HTMLSpanElement>(null);
|
|
2522
|
+
const valueLabel = formatDurationValueLabel(durationSeconds);
|
|
2523
|
+
|
|
2524
|
+
useEffect(() => {
|
|
2525
|
+
if (!isEditing) {
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
const editor = editorRef.current;
|
|
2530
|
+
|
|
2531
|
+
if (!editor) {
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
editor.textContent = valueLabel;
|
|
2536
|
+
editor.focus();
|
|
2537
|
+
selectTimelineEditableText(editor);
|
|
2538
|
+
}, [isEditing, valueLabel]);
|
|
2539
|
+
|
|
2540
|
+
function commitDraft(): void {
|
|
2541
|
+
onCommit(editorRef.current?.textContent ?? valueLabel);
|
|
2542
|
+
setIsEditing(false);
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
if (isEditing) {
|
|
2546
|
+
return (
|
|
2547
|
+
<span
|
|
2548
|
+
aria-label="timeline duration"
|
|
2549
|
+
className="block h-5 min-w-[3ch] !cursor-text overflow-hidden whitespace-nowrap rounded bg-[color:color-mix(in_oklab,var(--foreground)_8%,transparent)] px-1 font-sans text-xs leading-5 text-[color:var(--foreground)] outline-none tabular-nums"
|
|
2550
|
+
contentEditable
|
|
2551
|
+
data-slot="timeline-duration-editor"
|
|
2552
|
+
key="duration-editor"
|
|
2553
|
+
onBlur={commitDraft}
|
|
2554
|
+
onFocus={(event) => selectTimelineEditableText(event.currentTarget)}
|
|
2555
|
+
onKeyDown={(event) => {
|
|
2556
|
+
if (event.key === 'Enter') {
|
|
2557
|
+
event.preventDefault();
|
|
2558
|
+
commitDraft();
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
if (event.key === 'Escape') {
|
|
2562
|
+
event.preventDefault();
|
|
2563
|
+
setIsEditing(false);
|
|
2564
|
+
}
|
|
2565
|
+
}}
|
|
2566
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
2567
|
+
ref={editorRef}
|
|
2568
|
+
role="textbox"
|
|
2569
|
+
suppressContentEditableWarning
|
|
2570
|
+
tabIndex={0}
|
|
2571
|
+
/>
|
|
2572
|
+
);
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
return (
|
|
2576
|
+
<button
|
|
2577
|
+
aria-label="Edit timeline duration"
|
|
2578
|
+
className="block h-5 min-w-[3ch] shrink-0 !cursor-text overflow-hidden rounded px-1 font-sans text-xs leading-5 text-[color:var(--muted-foreground)] tabular-nums transition-colors duration-150 ease-out group-hover/timeline-panel-header:bg-[color:color-mix(in_oklab,var(--foreground)_8%,transparent)] focus-visible:bg-[color:color-mix(in_oklab,var(--foreground)_8%,transparent)] focus-visible:outline-none"
|
|
2579
|
+
data-slot="timeline-duration-display"
|
|
2580
|
+
key="duration-display"
|
|
2581
|
+
onClick={(event) => {
|
|
2582
|
+
event.stopPropagation();
|
|
2583
|
+
setIsEditing(true);
|
|
2584
|
+
}}
|
|
2585
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
2586
|
+
type="button"
|
|
2587
|
+
>
|
|
2588
|
+
{valueLabel}
|
|
2589
|
+
</button>
|
|
2590
|
+
);
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2501
2593
|
function TimelinePanelHeader({
|
|
2502
2594
|
canExpand,
|
|
2503
2595
|
currentTimeSeconds,
|
|
@@ -2538,7 +2630,7 @@ function TimelinePanelHeader({
|
|
|
2538
2630
|
return (
|
|
2539
2631
|
<div
|
|
2540
2632
|
className={cn(
|
|
2541
|
-
'relative flex min-w-0 shrink-0 items-center gap-1',
|
|
2633
|
+
'group/timeline-panel-header relative flex min-w-0 shrink-0 items-center gap-1',
|
|
2542
2634
|
isExpanded
|
|
2543
2635
|
? 'h-9 border-b border-[color:color-mix(in_oklab,var(--border)_8%,transparent)] p-1'
|
|
2544
2636
|
: 'h-full',
|
|
@@ -2566,13 +2658,9 @@ function TimelinePanelHeader({
|
|
|
2566
2658
|
<TimelinePanelDivider />
|
|
2567
2659
|
<div className="ml-2 inline-flex shrink-0 items-center gap-1 text-xs leading-5 text-[color:color-mix(in_oklab,var(--foreground)_90%,transparent)]">
|
|
2568
2660
|
<span>{isExpanded ? 'Duration:' : 'Dur:'}</span>
|
|
2569
|
-
<
|
|
2570
|
-
|
|
2571
|
-
layout="content"
|
|
2572
|
-
maxValueLabel={`${maxTimelineDurationSeconds}s`}
|
|
2661
|
+
<TimelineDurationValue
|
|
2662
|
+
durationSeconds={durationSeconds}
|
|
2573
2663
|
onCommit={onDurationCommit}
|
|
2574
|
-
textAlign="left"
|
|
2575
|
-
valueLabel={formatDurationValueLabel(durationSeconds)}
|
|
2576
2664
|
/>
|
|
2577
2665
|
</div>
|
|
2578
2666
|
<span
|
|
@@ -92,25 +92,25 @@ describe("ToolbarPanel", () => {
|
|
|
92
92
|
it("dispatches zoom commands", () => {
|
|
93
93
|
renderToolbar();
|
|
94
94
|
|
|
95
|
-
expect(screen.getByText("
|
|
95
|
+
expect(screen.getByText("100%")).toBeTruthy();
|
|
96
96
|
|
|
97
97
|
fireEvent.click(screen.getByRole("button", { name: "Zoom out" }));
|
|
98
|
-
expect(screen.getByText("
|
|
98
|
+
expect(screen.getByText("90%")).toBeTruthy();
|
|
99
99
|
|
|
100
100
|
fireEvent.click(screen.getByRole("button", { name: "Zoom in" }));
|
|
101
101
|
fireEvent.click(screen.getByRole("button", { name: "Zoom in" }));
|
|
102
|
-
expect(screen.getByText("
|
|
102
|
+
expect(screen.getByText("110%")).toBeTruthy();
|
|
103
103
|
});
|
|
104
104
|
|
|
105
105
|
it("resets zoom from the zoom label double click", () => {
|
|
106
106
|
renderToolbar();
|
|
107
107
|
|
|
108
108
|
fireEvent.click(screen.getByRole("button", { name: "Zoom in" }));
|
|
109
|
-
expect(screen.getByText("
|
|
109
|
+
expect(screen.getByText("110%")).toBeTruthy();
|
|
110
110
|
|
|
111
|
-
fireEvent.doubleClick(screen.getByText("
|
|
111
|
+
fireEvent.doubleClick(screen.getByText("110%"));
|
|
112
112
|
|
|
113
|
-
expect(screen.getByText("
|
|
113
|
+
expect(screen.getByText("100%")).toBeTruthy();
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
it("centers the canvas through the radar command", () => {
|
|
@@ -268,7 +268,7 @@ describe("assembled Toolcraft template runtime", () => {
|
|
|
268
268
|
expect(screen.getByText("Layers")).toBeTruthy();
|
|
269
269
|
expect(screen.getByText("Dur:")).toBeTruthy();
|
|
270
270
|
expect(screen.queryByText("Duration:")).toBeNull();
|
|
271
|
-
expect(toolbar?.textContent).toContain("
|
|
271
|
+
expect(toolbar?.textContent).toContain("100%");
|
|
272
272
|
expect(screen.getByRole("button", { name: "Undo" })).toBeDisabled();
|
|
273
273
|
expect(screen.getByRole("button", { name: "Redo" })).toBeDisabled();
|
|
274
274
|
expect(themeScope?.dataset.toolcraftTheme).toBe("dark");
|
|
@@ -305,7 +305,7 @@ describe("assembled Toolcraft template runtime", () => {
|
|
|
305
305
|
).toBe("320px");
|
|
306
306
|
|
|
307
307
|
fireEvent.click(screen.getByRole("button", { name: "Zoom in" }));
|
|
308
|
-
expect(screen.getByText("
|
|
308
|
+
expect(screen.getByText("110%")).toBeTruthy();
|
|
309
309
|
|
|
310
310
|
const wheelEvent = new WheelEvent("wheel", {
|
|
311
311
|
bubbles: true,
|
|
@@ -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", () => {
|
|
@@ -46,7 +47,7 @@ describe("defineToolcraft", () => {
|
|
|
46
47
|
});
|
|
47
48
|
|
|
48
49
|
expect(app.canvas.draggable).toBe(true);
|
|
49
|
-
expect(app.canvas.size).toEqual({ height:
|
|
50
|
+
expect(app.canvas.size).toEqual({ height: 1080, unit: "px", width: 1920 });
|
|
50
51
|
expect(app.canvas.sizeSource).toBe("runtime-default");
|
|
51
52
|
expect(app.canvas.sizing).toEqual({ mode: "intrinsic-media" });
|
|
52
53
|
expect(app.toolbar).toEqual({ history: true, radar: true, theme: true, zoom: true });
|
|
@@ -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,14 @@ describe("defineToolcraft", () => {
|
|
|
594
611
|
|
|
595
612
|
it("publishes reserved targets for AI assembly boundaries", () => {
|
|
596
613
|
expect(toolcraftRuntimeOwnedTargets).toEqual([
|
|
614
|
+
"canvas.aspectRatio",
|
|
615
|
+
"canvas.renderScale",
|
|
597
616
|
"canvas.size.width",
|
|
598
617
|
"canvas.size.height",
|
|
599
618
|
]);
|
|
600
619
|
expect(toolcraftReservedTargets).toEqual([
|
|
620
|
+
"canvas.aspectRatio",
|
|
621
|
+
"canvas.renderScale",
|
|
601
622
|
"canvas.size.width",
|
|
602
623
|
"canvas.size.height",
|
|
603
624
|
"selectedLayer.opacity",
|
|
@@ -606,6 +627,7 @@ describe("defineToolcraft", () => {
|
|
|
606
627
|
expect(getToolcraftCanvasSizeTargetDimension("canvas.size.width")).toBe("width");
|
|
607
628
|
expect(getToolcraftCanvasSizeTargetDimension("canvas.size.height")).toBe("height");
|
|
608
629
|
expect(getToolcraftCanvasSizeTargetDimension("generation.prompt")).toBeNull();
|
|
630
|
+
expect(isToolcraftRuntimeOwnedTarget("canvas.aspectRatio")).toBe(true);
|
|
609
631
|
expect(isToolcraftReservedTarget("selectedLayer.opacity")).toBe(true);
|
|
610
632
|
expect(isToolcraftReservedTarget("generation.prompt")).toBe(false);
|
|
611
633
|
});
|
|
@@ -674,6 +696,18 @@ describe("defineToolcraft", () => {
|
|
|
674
696
|
expect(app.canvas.sizeSource).toBe("app");
|
|
675
697
|
expect(app.canvas.sizing).toEqual({ mode: "editable-output" });
|
|
676
698
|
expect(app.panels.controls?.sections[0]?.title).toBe("Setup");
|
|
699
|
+
expect(app.panels.controls?.sections[0]?.controls.canvasAspectRatio).toMatchObject({
|
|
700
|
+
defaultValue: {
|
|
701
|
+
height: 256,
|
|
702
|
+
mode: "custom",
|
|
703
|
+
value: "455:256",
|
|
704
|
+
width: 455,
|
|
705
|
+
},
|
|
706
|
+
label: "Aspect ratio",
|
|
707
|
+
performanceRole: "workload",
|
|
708
|
+
target: "canvas.aspectRatio",
|
|
709
|
+
type: "aspectRatio",
|
|
710
|
+
});
|
|
677
711
|
expect(app.panels.controls?.sections[0]?.controls.canvasWidth).toMatchObject({
|
|
678
712
|
defaultValue: 1365,
|
|
679
713
|
performanceRole: "workload",
|
|
@@ -724,6 +758,18 @@ describe("defineToolcraft", () => {
|
|
|
724
758
|
const [canvasSection, generationSection] = app.panels.controls?.sections ?? [];
|
|
725
759
|
|
|
726
760
|
expect(canvasSection?.title).toBe("Setup");
|
|
761
|
+
expect(canvasSection?.controls.canvasAspectRatio).toMatchObject({
|
|
762
|
+
defaultValue: {
|
|
763
|
+
height: 5,
|
|
764
|
+
mode: "custom",
|
|
765
|
+
value: "8:5",
|
|
766
|
+
width: 8,
|
|
767
|
+
},
|
|
768
|
+
label: "Aspect ratio",
|
|
769
|
+
performanceRole: "workload",
|
|
770
|
+
target: "canvas.aspectRatio",
|
|
771
|
+
type: "aspectRatio",
|
|
772
|
+
});
|
|
727
773
|
expect(canvasSection?.controls.canvasWidth).toMatchObject({
|
|
728
774
|
defaultValue: 1440,
|
|
729
775
|
label: "Canvas width",
|
|
@@ -748,6 +794,80 @@ describe("defineToolcraft", () => {
|
|
|
748
794
|
expect(generationSection?.title).toBe("Generation");
|
|
749
795
|
});
|
|
750
796
|
|
|
797
|
+
it("appends raster render scale to the technical setup controls when enabled", () => {
|
|
798
|
+
const app = defineToolcraft({
|
|
799
|
+
canvas: {
|
|
800
|
+
enabled: true,
|
|
801
|
+
renderScale: true,
|
|
802
|
+
size: { height: 900, unit: "px", width: 1440 },
|
|
803
|
+
sizing: { mode: "editable-output" },
|
|
804
|
+
},
|
|
805
|
+
panels: {
|
|
806
|
+
controls: {
|
|
807
|
+
sections: [],
|
|
808
|
+
title: "Controls",
|
|
809
|
+
},
|
|
810
|
+
},
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
const setupSection = app.panels.controls?.sections[0];
|
|
814
|
+
|
|
815
|
+
expect(app.canvas.renderScale).toEqual({
|
|
816
|
+
defaultValue: 2,
|
|
817
|
+
enabled: true,
|
|
818
|
+
max: 2,
|
|
819
|
+
min: 1,
|
|
820
|
+
step: 0.25,
|
|
821
|
+
});
|
|
822
|
+
expect(app.assembly.capabilities).toContain("canvas.renderScale");
|
|
823
|
+
expect(Object.keys(setupSection?.controls ?? {})).toEqual([
|
|
824
|
+
"canvasAspectRatio",
|
|
825
|
+
"canvasWidth",
|
|
826
|
+
"canvasHeight",
|
|
827
|
+
"canvasRenderScale",
|
|
828
|
+
]);
|
|
829
|
+
expect(setupSection?.controls.canvasRenderScale).toMatchObject({
|
|
830
|
+
defaultValue: 2,
|
|
831
|
+
label: "Resolution scale",
|
|
832
|
+
markerCount: 5,
|
|
833
|
+
max: 2,
|
|
834
|
+
min: 1,
|
|
835
|
+
step: 0.25,
|
|
836
|
+
target: "canvas.renderScale",
|
|
837
|
+
type: "slider",
|
|
838
|
+
unit: "x",
|
|
839
|
+
variant: "discrete",
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it("can prepend render scale without editable output sizing", () => {
|
|
844
|
+
const app = defineToolcraft({
|
|
845
|
+
canvas: {
|
|
846
|
+
enabled: true,
|
|
847
|
+
renderScale: { defaultValue: 1.5 },
|
|
848
|
+
size: { height: 900, unit: "px", width: 1440 },
|
|
849
|
+
sizing: { mode: "fixed-output" },
|
|
850
|
+
},
|
|
851
|
+
panels: {
|
|
852
|
+
controls: {
|
|
853
|
+
sections: [],
|
|
854
|
+
title: "Controls",
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
expect(app.panels.controls?.sections[0]).toMatchObject({
|
|
860
|
+
controls: {
|
|
861
|
+
canvasRenderScale: {
|
|
862
|
+
defaultValue: 1.5,
|
|
863
|
+
target: "canvas.renderScale",
|
|
864
|
+
type: "slider",
|
|
865
|
+
},
|
|
866
|
+
},
|
|
867
|
+
title: "Setup",
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
|
|
751
871
|
it("does not prepend canvas size controls for intrinsic media or explicitly fixed output", () => {
|
|
752
872
|
const intrinsicApp = defineToolcraft({
|
|
753
873
|
canvas: {
|