@pixel-point/toolcraft 0.0.3 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/prepare-pack.mjs +5 -0
- package/src/generate.mjs +13 -0
- package/src/generate.test.mjs +6 -0
- package/templates/runtime/contracts/component-contracts.test.ts +86 -8
- package/templates/runtime/contracts/component-contracts.ts +36 -8
- package/templates/runtime/contracts/decision-contracts.ts +2 -2
- package/templates/runtime/export/export.test.ts +65 -0
- package/templates/runtime/export/export.ts +54 -1
- package/templates/runtime/react/canvas-shell.test.tsx +7 -7
- package/templates/runtime/react/controls-panel.test.tsx +323 -6
- package/templates/runtime/react/controls-panel.tsx +349 -24
- package/templates/runtime/react/settings-transfer.test.ts +6 -0
- package/templates/runtime/react/settings-transfer.ts +28 -2
- package/templates/runtime/react/timeline-panel.test.tsx +69 -0
- package/templates/runtime/react/timeline-panel.tsx +98 -10
- package/templates/runtime/react/toolbar-panel.test.tsx +6 -6
- package/templates/runtime/react/toolcraft-app.integration.test.tsx +2 -2
- package/templates/runtime/schema/canvas-aspect-ratio-presets.ts +50 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +122 -2
- package/templates/runtime/schema/define-toolcraft.ts +197 -6
- package/templates/runtime/schema/keyframe-capability.test.ts +7 -0
- package/templates/runtime/schema/keyframe-capability.ts +2 -2
- package/templates/runtime/schema/runtime-targets.ts +6 -0
- package/templates/runtime/schema/types.ts +23 -1
- package/templates/runtime/state/canvas-zoom.ts +1 -1
- package/templates/runtime/state/create-template-state.test.ts +9 -3
- package/templates/runtime/state/reducer.test.ts +135 -2
- package/templates/runtime/state/reducer.ts +236 -12
- package/templates/runtime/state/types.ts +1 -0
- package/templates/starter/AGENTS.md +6 -4
- package/templates/starter/docs/toolcraft/README.md +1 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +4 -2
- package/templates/starter/docs/toolcraft/assembly-workflow.md +13 -4
- package/templates/starter/docs/toolcraft/component-rules.md +24 -5
- package/templates/starter/docs/toolcraft/performance.md +5 -0
- package/templates/starter/docs/toolcraft/renderer-technique.md +1 -1
- package/templates/starter/docs/toolcraft/schema-reference.md +53 -9
- package/templates/starter/gitignore +36 -0
- package/templates/starter/src/app/starter-acceptance.test.ts +678 -21
- package/templates/starter/src/app/starter-acceptance.ts +357 -4
- package/templates/ui/components/control-layout/index.tsx +4 -4
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +101 -18
- package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1 -1
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +4 -1
- package/templates/ui/components/controls/slider/slider-value.ts +48 -5
- package/templates/ui/components/primitives/editable-slider-value-label.tsx +6 -1
|
@@ -1,4 +1,8 @@
|
|
|
1
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,
|
|
@@ -22,9 +26,9 @@ import type {
|
|
|
22
26
|
} from "./types";
|
|
23
27
|
|
|
24
28
|
const defaultCanvasSize = {
|
|
25
|
-
height:
|
|
29
|
+
height: 1080,
|
|
26
30
|
unit: "px",
|
|
27
|
-
width:
|
|
31
|
+
width: 1920,
|
|
28
32
|
} satisfies ToolcraftCanvasSize;
|
|
29
33
|
|
|
30
34
|
type ResolvedCanvas = ResolvedToolcraftAppSchema["canvas"];
|
|
@@ -41,6 +45,15 @@ const canvasSizeControlTargets = {
|
|
|
41
45
|
height: "canvas.size.height",
|
|
42
46
|
width: "canvas.size.width",
|
|
43
47
|
} as const;
|
|
48
|
+
const canvasAspectRatioTarget = "canvas.aspectRatio";
|
|
49
|
+
const canvasRenderScaleTarget = "canvas.renderScale";
|
|
50
|
+
const defaultCanvasRenderScale = {
|
|
51
|
+
defaultValue: 2,
|
|
52
|
+
enabled: false,
|
|
53
|
+
max: 2,
|
|
54
|
+
min: 1,
|
|
55
|
+
step: 0.25,
|
|
56
|
+
} satisfies ResolvedToolcraftAppSchema["canvas"]["renderScale"];
|
|
44
57
|
const maxAutoInlineControlLabelLength = 18;
|
|
45
58
|
const settingsTransferTarget = "runtime.settingsTransfer";
|
|
46
59
|
const runtimeSetupSectionTitle = "Setup";
|
|
@@ -185,6 +198,54 @@ function resolveCanvasSizing(
|
|
|
185
198
|
return { mode: "intrinsic-media" };
|
|
186
199
|
}
|
|
187
200
|
|
|
201
|
+
function clampCanvasRenderScale(value: number | undefined, fallback: number): number {
|
|
202
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
203
|
+
return fallback;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return Math.max(1, Math.min(2, value));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function resolveCanvasRenderScale(
|
|
210
|
+
renderScale: ToolcraftAppSchema["canvas"]["renderScale"],
|
|
211
|
+
): ResolvedToolcraftAppSchema["canvas"]["renderScale"] {
|
|
212
|
+
if (renderScale === true) {
|
|
213
|
+
return {
|
|
214
|
+
...defaultCanvasRenderScale,
|
|
215
|
+
enabled: true,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!renderScale) {
|
|
220
|
+
return defaultCanvasRenderScale;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const min = clampCanvasRenderScale(renderScale.min, defaultCanvasRenderScale.min);
|
|
224
|
+
const max = Math.max(
|
|
225
|
+
min,
|
|
226
|
+
clampCanvasRenderScale(renderScale.max, defaultCanvasRenderScale.max),
|
|
227
|
+
);
|
|
228
|
+
const step =
|
|
229
|
+
typeof renderScale.step === "number" && Number.isFinite(renderScale.step)
|
|
230
|
+
? Math.max(0.01, Math.min(1, renderScale.step))
|
|
231
|
+
: defaultCanvasRenderScale.step;
|
|
232
|
+
const defaultValue = Math.max(
|
|
233
|
+
min,
|
|
234
|
+
Math.min(
|
|
235
|
+
max,
|
|
236
|
+
clampCanvasRenderScale(renderScale.defaultValue, defaultCanvasRenderScale.defaultValue),
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
defaultValue,
|
|
242
|
+
enabled: renderScale.enabled ?? true,
|
|
243
|
+
max,
|
|
244
|
+
min,
|
|
245
|
+
step,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
188
249
|
function resolveExport(
|
|
189
250
|
exportSchema: ToolcraftAppSchema["export"],
|
|
190
251
|
): ResolvedExport {
|
|
@@ -259,6 +320,10 @@ function createToolcraftAssembly({
|
|
|
259
320
|
commands.push("canvas.setSize");
|
|
260
321
|
}
|
|
261
322
|
|
|
323
|
+
if (canvas.renderScale.enabled) {
|
|
324
|
+
capabilities.push("canvas.renderScale");
|
|
325
|
+
}
|
|
326
|
+
|
|
262
327
|
if (canvas.draggable) {
|
|
263
328
|
capabilities.push("canvas.draggable");
|
|
264
329
|
commands.push("canvas.panBy", "canvas.setOffset", "canvas.setViewport");
|
|
@@ -553,6 +618,7 @@ function isRuntimeOnlyActionControl(control: ToolcraftControlSchema): boolean {
|
|
|
553
618
|
function isSettingsTransferEligibilityControl(control: ToolcraftControlSchema): boolean {
|
|
554
619
|
return (
|
|
555
620
|
!isRuntimeOnlyActionControl(control) &&
|
|
621
|
+
control.target !== canvasAspectRatioTarget &&
|
|
556
622
|
control.target !== canvasSizeControlTargets.width &&
|
|
557
623
|
control.target !== canvasSizeControlTargets.height
|
|
558
624
|
);
|
|
@@ -699,25 +765,83 @@ function getCanvasSizeLayoutGroups(
|
|
|
699
765
|
: undefined;
|
|
700
766
|
}
|
|
701
767
|
|
|
768
|
+
function getGreatestCommonDivisor(left: number, right: number): number {
|
|
769
|
+
let a = Math.abs(Math.round(left));
|
|
770
|
+
let b = Math.abs(Math.round(right));
|
|
771
|
+
|
|
772
|
+
while (b !== 0) {
|
|
773
|
+
const next = b;
|
|
774
|
+
b = a % b;
|
|
775
|
+
a = next;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return a || 1;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function getCanvasAspectRatioDefaultValue(size: ToolcraftCanvasSize): {
|
|
782
|
+
height: number;
|
|
783
|
+
mode: "custom" | "preset";
|
|
784
|
+
value: string;
|
|
785
|
+
width: number;
|
|
786
|
+
} {
|
|
787
|
+
const divisor = getGreatestCommonDivisor(size.width, size.height);
|
|
788
|
+
const width = Math.max(1, Math.round(size.width / divisor));
|
|
789
|
+
const height = Math.max(1, Math.round(size.height / divisor));
|
|
790
|
+
const value = `${width}:${height}`;
|
|
791
|
+
const preset =
|
|
792
|
+
getToolcraftCanvasAspectRatioPreset(value) ??
|
|
793
|
+
getToolcraftCanvasAspectRatioPresetBySize(size);
|
|
794
|
+
|
|
795
|
+
return {
|
|
796
|
+
height: preset?.ratioHeight ?? height,
|
|
797
|
+
mode: preset ? "preset" : "custom",
|
|
798
|
+
value: preset?.value ?? value,
|
|
799
|
+
width: preset?.ratioWidth ?? width,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
702
803
|
function createCanvasSizeSection({
|
|
804
|
+
aspectRatioControl,
|
|
805
|
+
renderScaleControl,
|
|
703
806
|
sizeControlIds,
|
|
704
807
|
sizeControls,
|
|
705
808
|
}: {
|
|
809
|
+
aspectRatioControl: ToolcraftControlSchema;
|
|
810
|
+
renderScaleControl?: ToolcraftControlSchema;
|
|
706
811
|
sizeControlIds: readonly string[];
|
|
707
812
|
sizeControls: ToolcraftControlSectionSchema["controls"];
|
|
708
813
|
}): ToolcraftControlSectionSchema {
|
|
709
814
|
return {
|
|
710
|
-
controls:
|
|
815
|
+
controls: {
|
|
816
|
+
canvasAspectRatio: aspectRatioControl,
|
|
817
|
+
...sizeControls,
|
|
818
|
+
...(renderScaleControl ? { canvasRenderScale: renderScaleControl } : {}),
|
|
819
|
+
},
|
|
711
820
|
layoutGroups: getCanvasSizeLayoutGroups(sizeControlIds),
|
|
712
821
|
title: runtimeSetupSectionTitle,
|
|
713
822
|
};
|
|
714
823
|
}
|
|
715
824
|
|
|
825
|
+
function createCanvasRenderScaleSection(
|
|
826
|
+
renderScaleControl: ToolcraftControlSchema,
|
|
827
|
+
): ToolcraftControlSectionSchema {
|
|
828
|
+
return {
|
|
829
|
+
controls: {
|
|
830
|
+
canvasRenderScale: renderScaleControl,
|
|
831
|
+
},
|
|
832
|
+
title: runtimeSetupSectionTitle,
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
716
836
|
function mergeCanvasSizeControlsIntoSettingsTransferSection({
|
|
837
|
+
aspectRatioControl,
|
|
838
|
+
renderScaleControl,
|
|
717
839
|
settingsTransferSection,
|
|
718
840
|
sizeControlIds,
|
|
719
841
|
sizeControls,
|
|
720
842
|
}: {
|
|
843
|
+
aspectRatioControl: ToolcraftControlSchema;
|
|
844
|
+
renderScaleControl?: ToolcraftControlSchema;
|
|
721
845
|
settingsTransferSection: ToolcraftControlSectionSchema;
|
|
722
846
|
sizeControlIds: readonly string[];
|
|
723
847
|
sizeControls: ToolcraftControlSectionSchema["controls"];
|
|
@@ -728,7 +852,9 @@ function mergeCanvasSizeControlsIntoSettingsTransferSection({
|
|
|
728
852
|
...settingsTransferSection,
|
|
729
853
|
controls: {
|
|
730
854
|
...settingsTransferSection.controls,
|
|
855
|
+
canvasAspectRatio: aspectRatioControl,
|
|
731
856
|
...sizeControls,
|
|
857
|
+
...(renderScaleControl ? { canvasRenderScale: renderScaleControl } : {}),
|
|
732
858
|
},
|
|
733
859
|
layoutGroups:
|
|
734
860
|
canvasSizeLayoutGroups.length > 0 || settingsTransferSection.layoutGroups?.length
|
|
@@ -740,6 +866,22 @@ function mergeCanvasSizeControlsIntoSettingsTransferSection({
|
|
|
740
866
|
};
|
|
741
867
|
}
|
|
742
868
|
|
|
869
|
+
function mergeCanvasRenderScaleIntoSettingsTransferSection({
|
|
870
|
+
renderScaleControl,
|
|
871
|
+
settingsTransferSection,
|
|
872
|
+
}: {
|
|
873
|
+
renderScaleControl: ToolcraftControlSchema;
|
|
874
|
+
settingsTransferSection: ToolcraftControlSectionSchema;
|
|
875
|
+
}): ToolcraftControlSectionSchema {
|
|
876
|
+
return {
|
|
877
|
+
...settingsTransferSection,
|
|
878
|
+
controls: {
|
|
879
|
+
...settingsTransferSection.controls,
|
|
880
|
+
canvasRenderScale: renderScaleControl,
|
|
881
|
+
},
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
743
885
|
function isPrimaryPanelAction(action: ToolcraftControlActionSchema): boolean {
|
|
744
886
|
return typeof action !== "string" && action.variant !== "outline";
|
|
745
887
|
}
|
|
@@ -1259,14 +1401,46 @@ function normalizePanels({
|
|
|
1259
1401
|
|
|
1260
1402
|
const controls = { ...panels.controls };
|
|
1261
1403
|
const settingsTransferSection = createSettingsTransferSection(settingsTransfer);
|
|
1404
|
+
const renderScaleControl: ToolcraftControlSchema | undefined =
|
|
1405
|
+
canvas.renderScale.enabled && !hasControlTarget(panels, canvasRenderScaleTarget)
|
|
1406
|
+
? {
|
|
1407
|
+
defaultValue: canvas.renderScale.defaultValue,
|
|
1408
|
+
description:
|
|
1409
|
+
"Increases raster canvas backing resolution without changing the visible output size.",
|
|
1410
|
+
label: "Resolution scale",
|
|
1411
|
+
markerCount:
|
|
1412
|
+
Math.floor(
|
|
1413
|
+
(canvas.renderScale.max - canvas.renderScale.min) / canvas.renderScale.step,
|
|
1414
|
+
) + 1,
|
|
1415
|
+
max: canvas.renderScale.max,
|
|
1416
|
+
min: canvas.renderScale.min,
|
|
1417
|
+
performanceReason:
|
|
1418
|
+
"Resolution scale changes raster, Canvas, WebGL, or WebGPU backing pixels.",
|
|
1419
|
+
performanceRole: "workload",
|
|
1420
|
+
step: canvas.renderScale.step,
|
|
1421
|
+
target: canvasRenderScaleTarget,
|
|
1422
|
+
type: "slider",
|
|
1423
|
+
unit: "x",
|
|
1424
|
+
variant: "discrete",
|
|
1425
|
+
}
|
|
1426
|
+
: undefined;
|
|
1262
1427
|
|
|
1263
1428
|
if (!canvas.enabled || canvas.sizing.mode !== "editable-output") {
|
|
1429
|
+
const runtimeSetupSection =
|
|
1430
|
+
settingsTransferSection && renderScaleControl
|
|
1431
|
+
? mergeCanvasRenderScaleIntoSettingsTransferSection({
|
|
1432
|
+
renderScaleControl,
|
|
1433
|
+
settingsTransferSection,
|
|
1434
|
+
})
|
|
1435
|
+
: (settingsTransferSection ??
|
|
1436
|
+
(renderScaleControl ? createCanvasRenderScaleSection(renderScaleControl) : null));
|
|
1437
|
+
|
|
1264
1438
|
return {
|
|
1265
1439
|
...normalizedPanels,
|
|
1266
1440
|
controls: normalizeControlsPanelLayout({
|
|
1267
1441
|
...controls,
|
|
1268
1442
|
sections: [
|
|
1269
|
-
...(
|
|
1443
|
+
...(runtimeSetupSection ? [runtimeSetupSection] : []),
|
|
1270
1444
|
...controls.sections,
|
|
1271
1445
|
],
|
|
1272
1446
|
}),
|
|
@@ -1275,6 +1449,14 @@ function normalizePanels({
|
|
|
1275
1449
|
|
|
1276
1450
|
const sizeControls: ToolcraftControlSectionSchema["controls"] = {};
|
|
1277
1451
|
const sizeControlIds: string[] = [];
|
|
1452
|
+
const aspectRatioControl: ToolcraftControlSchema = {
|
|
1453
|
+
defaultValue: getCanvasAspectRatioDefaultValue(canvas.size),
|
|
1454
|
+
label: "Aspect ratio",
|
|
1455
|
+
performanceReason: "Aspect ratio changes output dimensions and renderer workload.",
|
|
1456
|
+
performanceRole: "workload",
|
|
1457
|
+
target: canvasAspectRatioTarget,
|
|
1458
|
+
type: "aspectRatio",
|
|
1459
|
+
};
|
|
1278
1460
|
|
|
1279
1461
|
if (!hasControlTarget(panels, canvasSizeControlTargets.width)) {
|
|
1280
1462
|
sizeControls.canvasWidth = {
|
|
@@ -1300,7 +1482,7 @@ function normalizePanels({
|
|
|
1300
1482
|
sizeControlIds.push("canvasHeight");
|
|
1301
1483
|
}
|
|
1302
1484
|
|
|
1303
|
-
if (Object.keys(sizeControls).length === 0) {
|
|
1485
|
+
if (Object.keys(sizeControls).length === 0 && !renderScaleControl) {
|
|
1304
1486
|
return {
|
|
1305
1487
|
...normalizedPanels,
|
|
1306
1488
|
controls: normalizeControlsPanelLayout({
|
|
@@ -1315,11 +1497,18 @@ function normalizePanels({
|
|
|
1315
1497
|
|
|
1316
1498
|
const runtimeSettingsSection = settingsTransferSection
|
|
1317
1499
|
? mergeCanvasSizeControlsIntoSettingsTransferSection({
|
|
1500
|
+
aspectRatioControl,
|
|
1501
|
+
renderScaleControl,
|
|
1318
1502
|
settingsTransferSection,
|
|
1319
1503
|
sizeControlIds,
|
|
1320
1504
|
sizeControls,
|
|
1321
1505
|
})
|
|
1322
|
-
: createCanvasSizeSection({
|
|
1506
|
+
: createCanvasSizeSection({
|
|
1507
|
+
aspectRatioControl,
|
|
1508
|
+
renderScaleControl,
|
|
1509
|
+
sizeControlIds,
|
|
1510
|
+
sizeControls,
|
|
1511
|
+
});
|
|
1323
1512
|
|
|
1324
1513
|
return {
|
|
1325
1514
|
...normalizedPanels,
|
|
@@ -1395,6 +1584,7 @@ function assertPanelPersistenceContract({
|
|
|
1395
1584
|
export function defineToolcraft(schema: ToolcraftAppSchema): ResolvedToolcraftAppSchema {
|
|
1396
1585
|
const canvasEnabled = schema.canvas.enabled;
|
|
1397
1586
|
const canvasSize = schema.canvas.size;
|
|
1587
|
+
const canvasRenderScale = resolveCanvasRenderScale(schema.canvas.renderScale);
|
|
1398
1588
|
const canvasSizing = resolveCanvasSizing(schema.canvas);
|
|
1399
1589
|
const persistence = resolvePersistence(schema.persistence);
|
|
1400
1590
|
const settingsTransfer = resolveSettingsTransfer({
|
|
@@ -1406,6 +1596,7 @@ export function defineToolcraft(schema: ToolcraftAppSchema): ResolvedToolcraftAp
|
|
|
1406
1596
|
const canvas = {
|
|
1407
1597
|
...schema.canvas,
|
|
1408
1598
|
draggable: canvasEnabled ? (schema.canvas.draggable ?? true) : false,
|
|
1599
|
+
renderScale: canvasRenderScale,
|
|
1409
1600
|
size: canvasSize ?? defaultCanvasSize,
|
|
1410
1601
|
sizeSource: canvasSize ? ("app" as const) : ("runtime-default" as const),
|
|
1411
1602
|
sizing: canvasSizing,
|
|
@@ -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,6 @@
|
|
|
1
1
|
export const toolcraftRuntimeOwnedTargets = [
|
|
2
|
+
"canvas.aspectRatio",
|
|
3
|
+
"canvas.renderScale",
|
|
2
4
|
"canvas.size.width",
|
|
3
5
|
"canvas.size.height",
|
|
4
6
|
] as const;
|
|
@@ -27,6 +29,10 @@ export function getToolcraftCanvasSizeTargetDimension(
|
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
export function isToolcraftCanvasAspectRatioTarget(target: string): boolean {
|
|
33
|
+
return target === "canvas.aspectRatio";
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
export function isToolcraftReservedTarget(
|
|
31
37
|
target: string,
|
|
32
38
|
): target is ToolcraftReservedTarget {
|
|
@@ -21,6 +21,24 @@ export type ToolcraftCanvasSizingSchema = {
|
|
|
21
21
|
mode: ToolcraftCanvasSizingMode;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
export type ToolcraftCanvasRenderScaleSchema =
|
|
25
|
+
| boolean
|
|
26
|
+
| {
|
|
27
|
+
defaultValue?: number;
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
max?: number;
|
|
30
|
+
min?: number;
|
|
31
|
+
step?: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ResolvedToolcraftCanvasRenderScaleSchema = {
|
|
35
|
+
defaultValue: number;
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
max: number;
|
|
38
|
+
min: number;
|
|
39
|
+
step: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
24
42
|
export type ToolcraftPngExportBackground = "include" | "transparent";
|
|
25
43
|
|
|
26
44
|
export type ToolcraftPngExportSchema = {
|
|
@@ -45,6 +63,7 @@ export type ToolcraftAssemblyComponentId =
|
|
|
45
63
|
export type ToolcraftAssemblyCapability =
|
|
46
64
|
| "canvas.draggable"
|
|
47
65
|
| "canvas.editableSize"
|
|
66
|
+
| "canvas.renderScale"
|
|
48
67
|
| "canvas.upload"
|
|
49
68
|
| "controls.defaults"
|
|
50
69
|
| "controls.panel"
|
|
@@ -141,6 +160,7 @@ export type ToolcraftAssemblyContract = {
|
|
|
141
160
|
export type ToolcraftCanvasSchema = {
|
|
142
161
|
draggable?: boolean;
|
|
143
162
|
enabled: boolean;
|
|
163
|
+
renderScale?: ToolcraftCanvasRenderScaleSchema;
|
|
144
164
|
size?: ToolcraftCanvasSize;
|
|
145
165
|
sizing?: ToolcraftCanvasSizingSchema;
|
|
146
166
|
upload?: boolean;
|
|
@@ -300,6 +320,7 @@ export type ToolcraftControlSchema = {
|
|
|
300
320
|
markerCount?: number;
|
|
301
321
|
max?: number;
|
|
302
322
|
min?: number;
|
|
323
|
+
multiple?: boolean;
|
|
303
324
|
orderRole?: ToolcraftControlOrderRole;
|
|
304
325
|
performanceReason?: string;
|
|
305
326
|
performanceRole?: ToolcraftControlPerformanceRole;
|
|
@@ -358,7 +379,8 @@ export type ToolcraftAppSchema = {
|
|
|
358
379
|
|
|
359
380
|
export type ResolvedToolcraftAppSchema = {
|
|
360
381
|
assembly: ToolcraftAssemblyContract;
|
|
361
|
-
canvas: Required<ToolcraftCanvasSchema> & {
|
|
382
|
+
canvas: Omit<Required<ToolcraftCanvasSchema>, "renderScale"> & {
|
|
383
|
+
renderScale: ResolvedToolcraftCanvasRenderScaleSchema;
|
|
362
384
|
size: ToolcraftCanvasSize;
|
|
363
385
|
sizeSource: ToolcraftCanvasSizeSource;
|
|
364
386
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const toolcraftCanvasZoomMin = 25;
|
|
2
2
|
export const toolcraftCanvasZoomMax = 400;
|
|
3
3
|
export const toolcraftCanvasZoomStep = 10;
|
|
4
|
-
export const toolcraftCanvasZoomDefault =
|
|
4
|
+
export const toolcraftCanvasZoomDefault = 100;
|
|
5
5
|
|
|
6
6
|
export function clampToolcraftCanvasZoom(zoom: number): number {
|
|
7
7
|
return Math.min(toolcraftCanvasZoomMax, Math.max(toolcraftCanvasZoomMin, zoom));
|
|
@@ -54,8 +54,14 @@ describe("createToolcraftState", () => {
|
|
|
54
54
|
const state = createToolcraftState(app);
|
|
55
55
|
|
|
56
56
|
expect(state.values).toMatchObject({
|
|
57
|
-
"canvas.
|
|
58
|
-
|
|
57
|
+
"canvas.aspectRatio": {
|
|
58
|
+
height: 9,
|
|
59
|
+
mode: "preset",
|
|
60
|
+
value: "16:9",
|
|
61
|
+
width: 16,
|
|
62
|
+
},
|
|
63
|
+
"canvas.size.height": 1080,
|
|
64
|
+
"canvas.size.width": 1920,
|
|
59
65
|
});
|
|
60
66
|
});
|
|
61
67
|
|
|
@@ -69,7 +75,7 @@ describe("createToolcraftState", () => {
|
|
|
69
75
|
const state = createToolcraftState(app);
|
|
70
76
|
|
|
71
77
|
expect(state.canvas.size).toEqual(size);
|
|
72
|
-
expect(state.canvas.zoom).toBe(
|
|
78
|
+
expect(state.canvas.zoom).toBe(100);
|
|
73
79
|
});
|
|
74
80
|
|
|
75
81
|
it("preserves seeded canvas width", () => {
|
|
@@ -103,13 +103,112 @@ 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(768);
|
|
107
|
+
expect(changed.values["canvas.aspectRatio"]).toEqual({
|
|
108
|
+
height: 6,
|
|
109
|
+
mode: "custom",
|
|
110
|
+
value: "5:6",
|
|
111
|
+
width: 5,
|
|
112
|
+
});
|
|
106
113
|
expect(changed.values["canvas.size.width"]).toBe(640);
|
|
114
|
+
expect(changed.values["canvas.size.height"]).toBe(768);
|
|
107
115
|
expect(changed.history.undo.at(-1)?.label).toBe("canvas.size.width");
|
|
108
116
|
expect(reset.canvas.size.width).toBe(1200);
|
|
117
|
+
expect(reset.canvas.size.height).toBe(768);
|
|
109
118
|
expect(reset.values["canvas.size.width"]).toBe(1200);
|
|
119
|
+
expect(reset.values["canvas.size.height"]).toBe(768);
|
|
110
120
|
expect(reset.history.undo.at(-1)?.label).toBe("Reset controls");
|
|
111
121
|
expect(undone.canvas.size.width).toBe(1200);
|
|
122
|
+
expect(undone.canvas.size.height).toBe(768);
|
|
112
123
|
expect(redone.canvas.size.width).toBe(640);
|
|
124
|
+
expect(redone.canvas.size.height).toBe(768);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("routes canvas aspect ratio presets through canvas runtime state", () => {
|
|
128
|
+
const state = createState();
|
|
129
|
+
|
|
130
|
+
const changed = toolcraftReducer(state, {
|
|
131
|
+
target: "canvas.aspectRatio",
|
|
132
|
+
type: "controls.setValue",
|
|
133
|
+
value: {
|
|
134
|
+
height: 9,
|
|
135
|
+
mode: "preset",
|
|
136
|
+
value: "16:9",
|
|
137
|
+
width: 16,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(changed.canvas.size).toEqual({ height: 1080, unit: "px", width: 1920 });
|
|
142
|
+
expect(changed.values["canvas.aspectRatio"]).toEqual({
|
|
143
|
+
height: 9,
|
|
144
|
+
mode: "preset",
|
|
145
|
+
value: "16:9",
|
|
146
|
+
width: 16,
|
|
147
|
+
});
|
|
148
|
+
expect(changed.values["canvas.size.width"]).toBe(1920);
|
|
149
|
+
expect(changed.values["canvas.size.height"]).toBe(1080);
|
|
150
|
+
expect(changed.history.undo.at(-1)?.label).toBe("canvas.aspectRatio");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("turns manual canvas size edits into a custom aspect ratio", () => {
|
|
154
|
+
const state = toolcraftReducer(createState(), {
|
|
155
|
+
target: "canvas.aspectRatio",
|
|
156
|
+
type: "controls.setValue",
|
|
157
|
+
value: {
|
|
158
|
+
height: 9,
|
|
159
|
+
mode: "preset",
|
|
160
|
+
value: "16:9",
|
|
161
|
+
width: 16,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const changed = toolcraftReducer(state, {
|
|
166
|
+
target: "canvas.size.height",
|
|
167
|
+
type: "controls.setValue",
|
|
168
|
+
value: "720",
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(changed.canvas.size).toEqual({ height: 720, unit: "px", width: 1920 });
|
|
172
|
+
expect(changed.values["canvas.aspectRatio"]).toEqual({
|
|
173
|
+
height: 3,
|
|
174
|
+
mode: "custom",
|
|
175
|
+
value: "8:3",
|
|
176
|
+
width: 8,
|
|
177
|
+
});
|
|
178
|
+
expect(changed.values["canvas.size.height"]).toBe(720);
|
|
179
|
+
expect(changed.values["canvas.size.width"]).toBe(1920);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("keeps repeated canvas size values as no-op without changing aspect ratio mode", () => {
|
|
183
|
+
const app = defineToolcraft({
|
|
184
|
+
canvas: {
|
|
185
|
+
enabled: true,
|
|
186
|
+
size: { height: 1080, unit: "px", width: 1920 },
|
|
187
|
+
sizing: { mode: "editable-output" },
|
|
188
|
+
},
|
|
189
|
+
panels: {
|
|
190
|
+
controls: {
|
|
191
|
+
sections: [],
|
|
192
|
+
title: "Controls",
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
const state = createToolcraftState(app);
|
|
197
|
+
|
|
198
|
+
const unchanged = toolcraftReducer(state, {
|
|
199
|
+
target: "canvas.size.width",
|
|
200
|
+
type: "controls.setValue",
|
|
201
|
+
value: "1920",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(unchanged).toBe(state);
|
|
205
|
+
expect(unchanged.values["canvas.aspectRatio"]).toEqual({
|
|
206
|
+
height: 9,
|
|
207
|
+
mode: "preset",
|
|
208
|
+
value: "16:9",
|
|
209
|
+
width: 16,
|
|
210
|
+
});
|
|
211
|
+
expect(unchanged.history.undo).toEqual([]);
|
|
113
212
|
});
|
|
114
213
|
|
|
115
214
|
it("updates canvas offset without recording history", () => {
|
|
@@ -170,11 +269,11 @@ describe("toolcraftReducer", () => {
|
|
|
170
269
|
state = toolcraftReducer(state, { type: "canvas.zoomOut" });
|
|
171
270
|
state = toolcraftReducer(state, { type: "canvas.zoomIn" });
|
|
172
271
|
|
|
173
|
-
expect(state.canvas.zoom).toBe(
|
|
272
|
+
expect(state.canvas.zoom).toBe(90);
|
|
174
273
|
|
|
175
274
|
state = toolcraftReducer(state, { type: "canvas.zoomReset" });
|
|
176
275
|
|
|
177
|
-
expect(state.canvas.zoom).toBe(
|
|
276
|
+
expect(state.canvas.zoom).toBe(100);
|
|
178
277
|
});
|
|
179
278
|
|
|
180
279
|
it("sets viewport zoom and offset for gesture zoom", () => {
|
|
@@ -325,6 +424,40 @@ describe("toolcraftReducer", () => {
|
|
|
325
424
|
});
|
|
326
425
|
});
|
|
327
426
|
|
|
427
|
+
it("appends single-layer media when import is explicitly non-replacing", () => {
|
|
428
|
+
const state = createState();
|
|
429
|
+
const first = toolcraftReducer(state, {
|
|
430
|
+
asset: {
|
|
431
|
+
dataUrl: "data:image/png;base64,first",
|
|
432
|
+
fileName: "first.png",
|
|
433
|
+
mimeType: "image/png",
|
|
434
|
+
position: { x: 0, y: 0 },
|
|
435
|
+
size: state.canvas.size,
|
|
436
|
+
},
|
|
437
|
+
replaceExisting: false,
|
|
438
|
+
type: "media.import",
|
|
439
|
+
});
|
|
440
|
+
const second = toolcraftReducer(first, {
|
|
441
|
+
asset: {
|
|
442
|
+
dataUrl: "data:image/png;base64,second",
|
|
443
|
+
fileName: "second.png",
|
|
444
|
+
mimeType: "image/png",
|
|
445
|
+
position: { x: 0, y: 0 },
|
|
446
|
+
size: state.canvas.size,
|
|
447
|
+
},
|
|
448
|
+
replaceExisting: false,
|
|
449
|
+
type: "media.import",
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
expect(second.layers.map((layer) => layer.id)).toEqual(["layer-1", "layer-2"]);
|
|
453
|
+
expect(second.mediaAssets.map((asset) => asset.id)).toEqual(["media-1", "media-2"]);
|
|
454
|
+
expect(second.mediaAssets.map((asset) => asset.fileName)).toEqual([
|
|
455
|
+
"first.png",
|
|
456
|
+
"second.png",
|
|
457
|
+
]);
|
|
458
|
+
expect(second.selectedLayerId).toBe("layer-2");
|
|
459
|
+
});
|
|
460
|
+
|
|
328
461
|
it("adds and selects runtime layers and groups", () => {
|
|
329
462
|
const withGroup = toolcraftReducer(createState(), {
|
|
330
463
|
layer: {
|