@pixel-point/toolcraft 0.0.6 → 0.0.8
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/src/generate.test.mjs +1 -1
- package/templates/runtime/contracts/component-contracts.test.ts +123 -15
- package/templates/runtime/contracts/component-contracts.ts +93 -19
- package/templates/runtime/contracts/decision-contracts.test.ts +3 -2
- package/templates/runtime/contracts/decision-contracts.ts +2 -2
- package/templates/runtime/export/export.test.ts +31 -0
- package/templates/runtime/export/export.ts +41 -0
- package/templates/runtime/react/canvas-shell.test.tsx +77 -1
- package/templates/runtime/react/canvas-shell.tsx +178 -23
- package/templates/runtime/react/control-conditions.ts +166 -0
- package/templates/runtime/react/controls-panel-filedrop-reorder.test.tsx +176 -0
- package/templates/runtime/react/controls-panel.test.tsx +590 -9
- package/templates/runtime/react/controls-panel.tsx +472 -10
- package/templates/runtime/react/media-file.ts +19 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +2 -0
- package/templates/runtime/schema/define-toolcraft.ts +11 -3
- package/templates/runtime/schema/types.ts +27 -0
- package/templates/runtime/state/reducer.test.ts +357 -0
- package/templates/runtime/state/reducer.ts +195 -9
- package/templates/runtime/state/types.ts +12 -2
- package/templates/runtime/testing/performance.test.ts +1282 -48
- package/templates/runtime/testing/performance.ts +676 -39
- package/templates/starter/AGENTS.md +13 -12
- package/templates/starter/docs/toolcraft/README.md +4 -3
- package/templates/starter/docs/toolcraft/acceptance-testing.md +17 -7
- package/templates/starter/docs/toolcraft/assembly-workflow.md +9 -7
- package/templates/starter/docs/toolcraft/component-rules.md +20 -7
- package/templates/starter/docs/toolcraft/custom-controls.md +9 -5
- package/templates/starter/docs/toolcraft/performance.md +51 -13
- package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +11 -6
- package/templates/starter/docs/toolcraft/workflow.md +7 -10
- package/templates/starter/e2e/app-performance.spec.ts +136 -3
- package/templates/starter/e2e/performance-helpers.ts +197 -0
- package/templates/starter/package.json +4 -1
- package/templates/starter/src/app/starter-acceptance.test.ts +529 -19
- package/templates/starter/src/app/starter-acceptance.ts +269 -12
- package/templates/starter/src/app/starter-performance.test.ts +67 -6
- package/templates/ui/components/controls/actions/actions-control.tsx +3 -5
- package/templates/ui/components/controls/collection-actions/collection-actions-control.tsx +60 -0
- package/templates/ui/components/controls/collection-actions/index.ts +4 -0
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +340 -44
- package/templates/ui/components/controls/file-drop/index.ts +1 -1
- package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1 -6
- package/templates/ui/components/controls/index.ts +9 -0
- package/templates/ui/components/panel/panel-section.tsx +53 -1
- package/templates/ui/index.ts +1 -0
|
@@ -95,6 +95,7 @@ export type ToolcraftAssemblyCommand =
|
|
|
95
95
|
| "canvas.zoomReset"
|
|
96
96
|
| "controls.apply"
|
|
97
97
|
| "controls.reset"
|
|
98
|
+
| "controls.resetTargets"
|
|
98
99
|
| "controls.setValue"
|
|
99
100
|
| "history.redo"
|
|
100
101
|
| "history.undo"
|
|
@@ -108,6 +109,7 @@ export type ToolcraftAssemblyCommand =
|
|
|
108
109
|
| "layers.toggleVisibility"
|
|
109
110
|
| "media.delete"
|
|
110
111
|
| "media.import"
|
|
112
|
+
| "media.reorder"
|
|
111
113
|
| "panels.resetOffset"
|
|
112
114
|
| "panels.setOffset"
|
|
113
115
|
| "timeline.changeKeyframeEasing"
|
|
@@ -272,6 +274,8 @@ export type ToolcraftControlPerformanceRole =
|
|
|
272
274
|
| "responsiveness"
|
|
273
275
|
| "workload";
|
|
274
276
|
|
|
277
|
+
export type ToolcraftFileDropAssetKind = "file" | "image";
|
|
278
|
+
|
|
275
279
|
export type ToolcraftControlConditionSchema = {
|
|
276
280
|
equals?: unknown;
|
|
277
281
|
greaterThan?: number;
|
|
@@ -292,6 +296,20 @@ export type ToolcraftColorOpacityValueSchema = {
|
|
|
292
296
|
opacity?: number;
|
|
293
297
|
};
|
|
294
298
|
|
|
299
|
+
export type ToolcraftCollectionItemControlSchema = {
|
|
300
|
+
commitMode?: "content" | "setting";
|
|
301
|
+
defaultValue?: unknown;
|
|
302
|
+
label?: boolean | string;
|
|
303
|
+
markerCount?: number;
|
|
304
|
+
max?: number;
|
|
305
|
+
min?: number;
|
|
306
|
+
options?: readonly { label: string; value: string }[];
|
|
307
|
+
step?: number;
|
|
308
|
+
type: string;
|
|
309
|
+
unit?: string;
|
|
310
|
+
variant?: string;
|
|
311
|
+
};
|
|
312
|
+
|
|
295
313
|
export type ToolcraftFontPickerValueSchema = {
|
|
296
314
|
color?: string;
|
|
297
315
|
fontId: string;
|
|
@@ -308,23 +326,32 @@ export type ToolcraftCurveInterpolation = "monotone" | "smooth";
|
|
|
308
326
|
export type ToolcraftControlSchema = {
|
|
309
327
|
accept?: string;
|
|
310
328
|
actions?: readonly (ToolcraftActionSchema | string)[];
|
|
329
|
+
assetKind?: ToolcraftFileDropAssetKind;
|
|
330
|
+
addLabel?: string;
|
|
311
331
|
commitMode?: "content" | "setting";
|
|
312
332
|
defaultValue?: unknown;
|
|
313
333
|
description?: string;
|
|
314
334
|
disabled?: boolean;
|
|
315
335
|
disabledWhen?: ToolcraftControlDisabledConditionSchema;
|
|
336
|
+
hardMaxItems?: number;
|
|
316
337
|
interpolation?: ToolcraftCurveInterpolation;
|
|
317
338
|
items?: readonly ToolcraftImagePickerItemSchema[];
|
|
339
|
+
itemControl?: ToolcraftCollectionItemControlSchema;
|
|
340
|
+
itemDefaultValue?: unknown;
|
|
341
|
+
itemLabel?: string;
|
|
318
342
|
keyframeable?: boolean;
|
|
319
343
|
label?: boolean | string;
|
|
320
344
|
markerCount?: number;
|
|
321
345
|
max?: number;
|
|
322
346
|
min?: number;
|
|
347
|
+
minItems?: number;
|
|
323
348
|
multiple?: boolean;
|
|
324
349
|
orderRole?: ToolcraftControlOrderRole;
|
|
325
350
|
performanceReason?: string;
|
|
326
351
|
performanceRole?: ToolcraftControlPerformanceRole;
|
|
327
352
|
options?: readonly { label: string; value: string }[];
|
|
353
|
+
recommendedMaxItems?: number;
|
|
354
|
+
removeLabel?: string;
|
|
328
355
|
step?: number;
|
|
329
356
|
target: string;
|
|
330
357
|
type: string;
|
|
@@ -45,6 +45,260 @@ describe("toolcraftReducer", () => {
|
|
|
45
45
|
expect(state.history.undo.at(-1)?.label).toBe("Reset controls");
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
it("resets selected control targets to defaults and records one history patch", () => {
|
|
49
|
+
const app = defineToolcraft({
|
|
50
|
+
canvas: { enabled: false },
|
|
51
|
+
panels: {
|
|
52
|
+
controls: {
|
|
53
|
+
sections: [
|
|
54
|
+
{
|
|
55
|
+
controls: {
|
|
56
|
+
contrast: {
|
|
57
|
+
defaultValue: 22,
|
|
58
|
+
target: "style.contrast",
|
|
59
|
+
type: "slider",
|
|
60
|
+
},
|
|
61
|
+
opacity: {
|
|
62
|
+
defaultValue: 75,
|
|
63
|
+
target: "selectedLayer.opacity",
|
|
64
|
+
type: "slider",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
title: "Tone",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
title: "Controls",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
const changedOpacity = toolcraftReducer(createToolcraftState(app), {
|
|
75
|
+
target: "selectedLayer.opacity",
|
|
76
|
+
type: "controls.setValue",
|
|
77
|
+
value: 12,
|
|
78
|
+
});
|
|
79
|
+
const changedBoth = toolcraftReducer(changedOpacity, {
|
|
80
|
+
target: "style.contrast",
|
|
81
|
+
type: "controls.setValue",
|
|
82
|
+
value: 9,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const state = toolcraftReducer(changedBoth, {
|
|
86
|
+
label: "Reset Tone section",
|
|
87
|
+
targets: ["selectedLayer.opacity"],
|
|
88
|
+
type: "controls.resetTargets",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(state.values["selectedLayer.opacity"]).toBe(75);
|
|
92
|
+
expect(state.values["style.contrast"]).toBe(9);
|
|
93
|
+
expect(state.history.undo).toHaveLength(3);
|
|
94
|
+
expect(state.history.undo.at(-1)).toMatchObject({
|
|
95
|
+
after: { "selectedLayer.opacity": 75 },
|
|
96
|
+
before: { "selectedLayer.opacity": 12 },
|
|
97
|
+
label: "Reset Tone section",
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("clears single-layer fileDrop media on global reset", () => {
|
|
102
|
+
const app = defineToolcraft({
|
|
103
|
+
canvas: { enabled: true, upload: true },
|
|
104
|
+
panels: {
|
|
105
|
+
controls: {
|
|
106
|
+
sections: [
|
|
107
|
+
{
|
|
108
|
+
controls: {
|
|
109
|
+
source: {
|
|
110
|
+
defaultValue: null,
|
|
111
|
+
target: "media.source",
|
|
112
|
+
type: "fileDrop",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
title: "Source",
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
title: "Controls",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
const imported = toolcraftReducer(createToolcraftState(app), {
|
|
123
|
+
asset: {
|
|
124
|
+
dataUrl: "data:image/png;base64,test",
|
|
125
|
+
fileName: "source.png",
|
|
126
|
+
mimeType: "image/png",
|
|
127
|
+
position: { x: 0, y: 0 },
|
|
128
|
+
size: { height: 768, unit: "px", width: 1024 },
|
|
129
|
+
sourceTarget: "media.source",
|
|
130
|
+
},
|
|
131
|
+
type: "media.import",
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const state = toolcraftReducer(imported, { type: "controls.reset" });
|
|
135
|
+
const undone = toolcraftReducer(state, { type: "history.undo" });
|
|
136
|
+
const redone = toolcraftReducer(undone, { type: "history.redo" });
|
|
137
|
+
|
|
138
|
+
expect(imported.mediaAssets).toHaveLength(1);
|
|
139
|
+
expect(state.mediaAssets).toEqual([]);
|
|
140
|
+
expect(state.values["media.source"]).toBeNull();
|
|
141
|
+
expect(undone.mediaAssets).toEqual(imported.mediaAssets);
|
|
142
|
+
expect(redone.mediaAssets).toEqual([]);
|
|
143
|
+
expect(state.history.undo.at(-1)).toMatchObject({
|
|
144
|
+
after: { mediaAssets: [] },
|
|
145
|
+
before: { mediaAssets: imported.mediaAssets },
|
|
146
|
+
label: "Reset controls",
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("stores arbitrary fileDrop media without image dimensions and clears it on reset", () => {
|
|
151
|
+
const app = defineToolcraft({
|
|
152
|
+
canvas: { enabled: true, upload: true },
|
|
153
|
+
panels: {
|
|
154
|
+
controls: {
|
|
155
|
+
sections: [
|
|
156
|
+
{
|
|
157
|
+
controls: {
|
|
158
|
+
source: {
|
|
159
|
+
assetKind: "file",
|
|
160
|
+
defaultValue: null,
|
|
161
|
+
target: "files.source",
|
|
162
|
+
type: "fileDrop",
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
title: "Files",
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
title: "Controls",
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
const imported = toolcraftReducer(createToolcraftState(app), {
|
|
173
|
+
asset: {
|
|
174
|
+
assetKind: "file",
|
|
175
|
+
dataUrl: "data:text/plain;base64,aGVsbG8=",
|
|
176
|
+
fileName: "notes.txt",
|
|
177
|
+
mimeType: "text/plain",
|
|
178
|
+
position: { x: 0, y: 0 },
|
|
179
|
+
sourceTarget: "files.source",
|
|
180
|
+
},
|
|
181
|
+
type: "media.import",
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const state = toolcraftReducer(imported, { type: "controls.reset" });
|
|
185
|
+
|
|
186
|
+
expect(imported.mediaAssets).toHaveLength(1);
|
|
187
|
+
expect(imported.mediaAssets[0]).toMatchObject({
|
|
188
|
+
assetKind: "file",
|
|
189
|
+
fileName: "notes.txt",
|
|
190
|
+
sourceTarget: "files.source",
|
|
191
|
+
});
|
|
192
|
+
expect(imported.mediaAssets[0]?.size).toBeUndefined();
|
|
193
|
+
expect(imported.canvas.size).toEqual(createToolcraftState(app).canvas.size);
|
|
194
|
+
expect(state.mediaAssets).toEqual([]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("clears only selected fileDrop media on section reset", () => {
|
|
198
|
+
const app = defineToolcraft({
|
|
199
|
+
canvas: { enabled: true, upload: true },
|
|
200
|
+
panels: {
|
|
201
|
+
controls: {
|
|
202
|
+
sections: [
|
|
203
|
+
{
|
|
204
|
+
controls: {
|
|
205
|
+
primary: {
|
|
206
|
+
defaultValue: null,
|
|
207
|
+
target: "media.primary",
|
|
208
|
+
type: "fileDrop",
|
|
209
|
+
},
|
|
210
|
+
secondary: {
|
|
211
|
+
defaultValue: null,
|
|
212
|
+
target: "media.secondary",
|
|
213
|
+
type: "fileDrop",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
title: "Sources",
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
title: "Controls",
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
const first = toolcraftReducer(createToolcraftState(app), {
|
|
224
|
+
asset: {
|
|
225
|
+
dataUrl: "data:image/png;base64,primary",
|
|
226
|
+
fileName: "primary.png",
|
|
227
|
+
mimeType: "image/png",
|
|
228
|
+
position: { x: 0, y: 0 },
|
|
229
|
+
size: { height: 768, unit: "px", width: 1024 },
|
|
230
|
+
sourceTarget: "media.primary",
|
|
231
|
+
},
|
|
232
|
+
replaceExisting: false,
|
|
233
|
+
type: "media.import",
|
|
234
|
+
});
|
|
235
|
+
const second = toolcraftReducer(first, {
|
|
236
|
+
asset: {
|
|
237
|
+
dataUrl: "data:image/png;base64,secondary",
|
|
238
|
+
fileName: "secondary.png",
|
|
239
|
+
mimeType: "image/png",
|
|
240
|
+
position: { x: 0, y: 0 },
|
|
241
|
+
size: { height: 768, unit: "px", width: 1024 },
|
|
242
|
+
sourceTarget: "media.secondary",
|
|
243
|
+
},
|
|
244
|
+
replaceExisting: false,
|
|
245
|
+
type: "media.import",
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const state = toolcraftReducer(second, {
|
|
249
|
+
label: "Reset Primary section",
|
|
250
|
+
targets: ["media.primary"],
|
|
251
|
+
type: "controls.resetTargets",
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect(state.mediaAssets).toHaveLength(1);
|
|
255
|
+
expect(state.mediaAssets[0]?.fileName).toBe("secondary.png");
|
|
256
|
+
expect(state.history.undo.at(-1)).toMatchObject({
|
|
257
|
+
after: { mediaAssets: state.mediaAssets },
|
|
258
|
+
before: { mediaAssets: second.mediaAssets },
|
|
259
|
+
label: "Reset Primary section",
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("keeps layer-owned media on global reset", () => {
|
|
264
|
+
const app = defineToolcraft({
|
|
265
|
+
canvas: { enabled: true, upload: true },
|
|
266
|
+
panels: {
|
|
267
|
+
controls: {
|
|
268
|
+
sections: [
|
|
269
|
+
{
|
|
270
|
+
controls: {
|
|
271
|
+
source: {
|
|
272
|
+
defaultValue: null,
|
|
273
|
+
target: "media.source",
|
|
274
|
+
type: "fileDrop",
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
title: "Source",
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
title: "Controls",
|
|
281
|
+
},
|
|
282
|
+
layers: true,
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
const imported = toolcraftReducer(createToolcraftState(app), {
|
|
286
|
+
asset: {
|
|
287
|
+
dataUrl: "data:image/png;base64,test",
|
|
288
|
+
fileName: "source.png",
|
|
289
|
+
mimeType: "image/png",
|
|
290
|
+
position: { x: 0, y: 0 },
|
|
291
|
+
size: { height: 768, unit: "px", width: 1024 },
|
|
292
|
+
sourceTarget: "media.source",
|
|
293
|
+
},
|
|
294
|
+
type: "media.import",
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const state = toolcraftReducer(imported, { type: "controls.reset" });
|
|
298
|
+
|
|
299
|
+
expect(state.mediaAssets).toEqual(imported.mediaAssets);
|
|
300
|
+
});
|
|
301
|
+
|
|
48
302
|
it("updates canvas size and records history", () => {
|
|
49
303
|
const size = { width: 1200, height: 900, unit: "px" } as const;
|
|
50
304
|
|
|
@@ -458,6 +712,109 @@ describe("toolcraftReducer", () => {
|
|
|
458
712
|
expect(second.selectedLayerId).toBe("layer-2");
|
|
459
713
|
});
|
|
460
714
|
|
|
715
|
+
it("reorders imported media assets and records undo history", () => {
|
|
716
|
+
const state = createState();
|
|
717
|
+
const first = toolcraftReducer(state, {
|
|
718
|
+
asset: {
|
|
719
|
+
dataUrl: "data:image/png;base64,first",
|
|
720
|
+
fileName: "first.png",
|
|
721
|
+
mimeType: "image/png",
|
|
722
|
+
position: { x: 0, y: 0 },
|
|
723
|
+
size: state.canvas.size,
|
|
724
|
+
},
|
|
725
|
+
replaceExisting: false,
|
|
726
|
+
type: "media.import",
|
|
727
|
+
});
|
|
728
|
+
const second = toolcraftReducer(first, {
|
|
729
|
+
asset: {
|
|
730
|
+
dataUrl: "data:image/png;base64,second",
|
|
731
|
+
fileName: "second.png",
|
|
732
|
+
mimeType: "image/png",
|
|
733
|
+
position: { x: 0, y: 0 },
|
|
734
|
+
size: state.canvas.size,
|
|
735
|
+
},
|
|
736
|
+
replaceExisting: false,
|
|
737
|
+
type: "media.import",
|
|
738
|
+
});
|
|
739
|
+
const third = toolcraftReducer(second, {
|
|
740
|
+
asset: {
|
|
741
|
+
dataUrl: "data:image/png;base64,third",
|
|
742
|
+
fileName: "third.png",
|
|
743
|
+
mimeType: "image/png",
|
|
744
|
+
position: { x: 0, y: 0 },
|
|
745
|
+
size: state.canvas.size,
|
|
746
|
+
},
|
|
747
|
+
replaceExisting: false,
|
|
748
|
+
type: "media.import",
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
const reordered = toolcraftReducer(third, {
|
|
752
|
+
mediaIds: ["media-3", "media-1", "media-2"],
|
|
753
|
+
type: "media.reorder",
|
|
754
|
+
});
|
|
755
|
+
const undone = toolcraftReducer(reordered, { type: "history.undo" });
|
|
756
|
+
|
|
757
|
+
expect(reordered.mediaAssets.map((asset) => asset.id)).toEqual([
|
|
758
|
+
"media-3",
|
|
759
|
+
"media-1",
|
|
760
|
+
"media-2",
|
|
761
|
+
]);
|
|
762
|
+
expect(reordered.history.undo.at(-1)?.label).toBe("Reorder media");
|
|
763
|
+
expect(undone.mediaAssets.map((asset) => asset.id)).toEqual([
|
|
764
|
+
"media-1",
|
|
765
|
+
"media-2",
|
|
766
|
+
"media-3",
|
|
767
|
+
]);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it("keeps omitted media assets after explicitly reordered media", () => {
|
|
771
|
+
const state = createState();
|
|
772
|
+
const first = toolcraftReducer(state, {
|
|
773
|
+
asset: {
|
|
774
|
+
dataUrl: "data:image/png;base64,first",
|
|
775
|
+
fileName: "first.png",
|
|
776
|
+
mimeType: "image/png",
|
|
777
|
+
position: { x: 0, y: 0 },
|
|
778
|
+
size: state.canvas.size,
|
|
779
|
+
},
|
|
780
|
+
replaceExisting: false,
|
|
781
|
+
type: "media.import",
|
|
782
|
+
});
|
|
783
|
+
const second = toolcraftReducer(first, {
|
|
784
|
+
asset: {
|
|
785
|
+
dataUrl: "data:image/png;base64,second",
|
|
786
|
+
fileName: "second.png",
|
|
787
|
+
mimeType: "image/png",
|
|
788
|
+
position: { x: 0, y: 0 },
|
|
789
|
+
size: state.canvas.size,
|
|
790
|
+
},
|
|
791
|
+
replaceExisting: false,
|
|
792
|
+
type: "media.import",
|
|
793
|
+
});
|
|
794
|
+
const third = toolcraftReducer(second, {
|
|
795
|
+
asset: {
|
|
796
|
+
dataUrl: "data:image/png;base64,third",
|
|
797
|
+
fileName: "third.png",
|
|
798
|
+
mimeType: "image/png",
|
|
799
|
+
position: { x: 0, y: 0 },
|
|
800
|
+
size: state.canvas.size,
|
|
801
|
+
},
|
|
802
|
+
replaceExisting: false,
|
|
803
|
+
type: "media.import",
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
const reordered = toolcraftReducer(third, {
|
|
807
|
+
mediaIds: ["media-2"],
|
|
808
|
+
type: "media.reorder",
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
expect(reordered.mediaAssets.map((asset) => asset.id)).toEqual([
|
|
812
|
+
"media-2",
|
|
813
|
+
"media-1",
|
|
814
|
+
"media-3",
|
|
815
|
+
]);
|
|
816
|
+
});
|
|
817
|
+
|
|
461
818
|
it("adds and selects runtime layers and groups", () => {
|
|
462
819
|
const withGroup = toolcraftReducer(createState(), {
|
|
463
820
|
layer: {
|
|
@@ -212,6 +212,77 @@ function getResetCanvasSize(
|
|
|
212
212
|
};
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
function getFileDropResetTargets(
|
|
216
|
+
state: ToolcraftState,
|
|
217
|
+
targets?: ReadonlySet<string>,
|
|
218
|
+
): Set<string> {
|
|
219
|
+
const fileDropTargets = new Set<string>();
|
|
220
|
+
|
|
221
|
+
for (const section of state.schema.panels.controls?.sections ?? []) {
|
|
222
|
+
for (const control of Object.values(section.controls)) {
|
|
223
|
+
if (control.type !== "fileDrop" || (targets && !targets.has(control.target))) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fileDropTargets.add(control.target);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return fileDropTargets;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function getResetMediaPatch(
|
|
235
|
+
state: ToolcraftState,
|
|
236
|
+
targets?: ReadonlySet<string>,
|
|
237
|
+
): Pick<ToolcraftHistoryPatch, "after" | "before"> | null {
|
|
238
|
+
if (state.schema.panels.layers || state.mediaAssets.length === 0) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const fileDropTargets = getFileDropResetTargets(state, targets);
|
|
243
|
+
|
|
244
|
+
if (fileDropTargets.size === 0) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const mediaAssets = state.mediaAssets.filter((asset) => {
|
|
249
|
+
if (asset.sourceTarget) {
|
|
250
|
+
return !fileDropTargets.has(asset.sourceTarget);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return false;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (mediaAssets.length === state.mediaAssets.length) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const timeline = getMediaReadyTimelineState(state.schema, state.timeline, mediaAssets);
|
|
261
|
+
const shouldCommitTimeline = timeline !== state.timeline;
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
after: {
|
|
265
|
+
mediaAssets,
|
|
266
|
+
...(shouldCommitTimeline ? { timeline } : {}),
|
|
267
|
+
},
|
|
268
|
+
before: {
|
|
269
|
+
mediaAssets: state.mediaAssets,
|
|
270
|
+
...(shouldCommitTimeline ? { timeline: state.timeline } : {}),
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function canvasSizesEqual(
|
|
276
|
+
first: ToolcraftState["canvas"]["size"],
|
|
277
|
+
second: ToolcraftState["canvas"]["size"],
|
|
278
|
+
): boolean {
|
|
279
|
+
return (
|
|
280
|
+
first.height === second.height &&
|
|
281
|
+
first.unit === second.unit &&
|
|
282
|
+
first.width === second.width
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
215
286
|
function clampTimelineDuration(value: number): number {
|
|
216
287
|
if (!Number.isFinite(value)) {
|
|
217
288
|
return minTimelineDurationSeconds;
|
|
@@ -686,16 +757,19 @@ export function toolcraftReducer(
|
|
|
686
757
|
|
|
687
758
|
case "controls.reset": {
|
|
688
759
|
const resetCanvasSize = getResetCanvasSize(state);
|
|
760
|
+
const resetMediaPatch = getResetMediaPatch(state);
|
|
689
761
|
|
|
690
|
-
if (resetCanvasSize) {
|
|
762
|
+
if (resetCanvasSize || resetMediaPatch) {
|
|
691
763
|
return commitStatePatch(state, {
|
|
692
764
|
after: {
|
|
693
765
|
...state.defaults,
|
|
694
|
-
"canvas.size": resetCanvasSize,
|
|
766
|
+
...(resetCanvasSize ? { "canvas.size": resetCanvasSize } : {}),
|
|
767
|
+
...resetMediaPatch?.after,
|
|
695
768
|
},
|
|
696
769
|
before: {
|
|
697
770
|
...state.values,
|
|
698
|
-
"canvas.size": state.canvas.size,
|
|
771
|
+
...(resetCanvasSize ? { "canvas.size": state.canvas.size } : {}),
|
|
772
|
+
...resetMediaPatch?.before,
|
|
699
773
|
},
|
|
700
774
|
label: "Reset controls",
|
|
701
775
|
});
|
|
@@ -712,6 +786,48 @@ export function toolcraftReducer(
|
|
|
712
786
|
);
|
|
713
787
|
}
|
|
714
788
|
|
|
789
|
+
case "controls.resetTargets": {
|
|
790
|
+
const targetSet = new Set(command.targets);
|
|
791
|
+
const before: Record<string, unknown> = {};
|
|
792
|
+
const after: Record<string, unknown> = {};
|
|
793
|
+
const resetMediaPatch = getResetMediaPatch(state, targetSet);
|
|
794
|
+
|
|
795
|
+
for (const target of targetSet) {
|
|
796
|
+
if (!(target in state.defaults) || Object.is(state.values[target], state.defaults[target])) {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
before[target] = state.values[target];
|
|
801
|
+
after[target] = state.defaults[target];
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const resetCanvasSize = getResetCanvasSize(state);
|
|
805
|
+
const shouldResetCanvasSize =
|
|
806
|
+
resetCanvasSize !== null &&
|
|
807
|
+
(targetSet.has(canvasSizeWidthTarget) || targetSet.has(canvasSizeHeightTarget)) &&
|
|
808
|
+
!canvasSizesEqual(state.canvas.size, resetCanvasSize);
|
|
809
|
+
|
|
810
|
+
if (shouldResetCanvasSize) {
|
|
811
|
+
before["canvas.size"] = state.canvas.size;
|
|
812
|
+
after["canvas.size"] = resetCanvasSize;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (resetMediaPatch) {
|
|
816
|
+
Object.assign(before, resetMediaPatch.before);
|
|
817
|
+
Object.assign(after, resetMediaPatch.after);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (Object.keys(after).length === 0) {
|
|
821
|
+
return state;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return commitStatePatch(state, {
|
|
825
|
+
after,
|
|
826
|
+
before,
|
|
827
|
+
label: command.label ?? "Reset section",
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
715
831
|
case "layers.add": {
|
|
716
832
|
const layer = createLayer(state, command.layer);
|
|
717
833
|
const insertIndex = clampInsertIndex(state.layers.length, command.insertIndex);
|
|
@@ -1030,15 +1146,29 @@ export function toolcraftReducer(
|
|
|
1030
1146
|
case "media.import": {
|
|
1031
1147
|
const shouldReplaceSingleLayerMedia =
|
|
1032
1148
|
!state.schema.panels.layers && command.replaceExisting !== false;
|
|
1149
|
+
const assetKind = command.asset.assetKind ?? "image";
|
|
1150
|
+
const sourceTarget = command.asset.sourceTarget;
|
|
1151
|
+
const existingSourceMediaAsset =
|
|
1152
|
+
shouldReplaceSingleLayerMedia && sourceTarget
|
|
1153
|
+
? state.mediaAssets.find((asset) => asset.sourceTarget === sourceTarget)
|
|
1154
|
+
: undefined;
|
|
1033
1155
|
const shouldResizeCanvas =
|
|
1034
|
-
state.schema.canvas.sizing.mode === "intrinsic-media"
|
|
1156
|
+
state.schema.canvas.sizing.mode === "intrinsic-media" &&
|
|
1157
|
+
assetKind === "image" &&
|
|
1158
|
+
command.asset.size !== undefined;
|
|
1035
1159
|
const layerId =
|
|
1036
1160
|
command.asset.layerId ??
|
|
1037
|
-
|
|
1161
|
+
existingSourceMediaAsset?.layerId ??
|
|
1162
|
+
(shouldReplaceSingleLayerMedia && !sourceTarget
|
|
1163
|
+
? getSingleLayerImportId(state)
|
|
1164
|
+
: undefined) ??
|
|
1038
1165
|
getNextLayerId(state);
|
|
1039
1166
|
const mediaId =
|
|
1040
1167
|
command.asset.id ??
|
|
1041
|
-
|
|
1168
|
+
existingSourceMediaAsset?.id ??
|
|
1169
|
+
(shouldReplaceSingleLayerMedia && !sourceTarget
|
|
1170
|
+
? getSingleMediaImportId(state)
|
|
1171
|
+
: undefined) ??
|
|
1042
1172
|
getNextMediaId(state);
|
|
1043
1173
|
const layer = {
|
|
1044
1174
|
displayName: command.asset.layerName ?? getImportedLayerName(command.asset.fileName),
|
|
@@ -1048,17 +1178,31 @@ export function toolcraftReducer(
|
|
|
1048
1178
|
visible: true,
|
|
1049
1179
|
};
|
|
1050
1180
|
const mediaAsset = {
|
|
1181
|
+
...(command.asset.assetKind ? { assetKind: command.asset.assetKind } : {}),
|
|
1051
1182
|
dataUrl: command.asset.dataUrl,
|
|
1052
1183
|
fileName: command.asset.fileName,
|
|
1053
1184
|
id: mediaId,
|
|
1054
1185
|
layerId,
|
|
1055
1186
|
mimeType: command.asset.mimeType,
|
|
1056
1187
|
position: shouldResizeCanvas ? { x: 0, y: 0 } : command.asset.position,
|
|
1057
|
-
size: command.asset.size,
|
|
1188
|
+
...(command.asset.size ? { size: command.asset.size } : {}),
|
|
1189
|
+
...(sourceTarget ? { sourceTarget } : {}),
|
|
1058
1190
|
};
|
|
1059
|
-
const layers = shouldReplaceSingleLayerMedia
|
|
1191
|
+
const layers = shouldReplaceSingleLayerMedia
|
|
1192
|
+
? sourceTarget
|
|
1193
|
+
? state.layers.some((entry) => entry.id === layerId)
|
|
1194
|
+
? state.layers.map((entry) => (entry.id === layerId ? layer : entry))
|
|
1195
|
+
: [...state.layers, layer]
|
|
1196
|
+
: [layer]
|
|
1197
|
+
: [...state.layers, layer];
|
|
1060
1198
|
const mediaAssets = shouldReplaceSingleLayerMedia
|
|
1061
|
-
?
|
|
1199
|
+
? sourceTarget
|
|
1200
|
+
? existingSourceMediaAsset
|
|
1201
|
+
? state.mediaAssets.map((asset) =>
|
|
1202
|
+
asset.id === existingSourceMediaAsset.id ? mediaAsset : asset,
|
|
1203
|
+
)
|
|
1204
|
+
: [...state.mediaAssets, mediaAsset]
|
|
1205
|
+
: [mediaAsset]
|
|
1062
1206
|
: [...state.mediaAssets, mediaAsset];
|
|
1063
1207
|
const after = {
|
|
1064
1208
|
...(shouldResizeCanvas ? { "canvas.size": command.asset.size } : {}),
|
|
@@ -1102,6 +1246,48 @@ export function toolcraftReducer(
|
|
|
1102
1246
|
});
|
|
1103
1247
|
}
|
|
1104
1248
|
|
|
1249
|
+
case "media.reorder": {
|
|
1250
|
+
if (state.mediaAssets.length < 2 || command.mediaIds.length === 0) {
|
|
1251
|
+
return state;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
const mediaById = new Map(state.mediaAssets.map((asset) => [asset.id, asset]));
|
|
1255
|
+
const seenIds = new Set<string>();
|
|
1256
|
+
const reorderedMediaAssets = command.mediaIds.flatMap((mediaId) => {
|
|
1257
|
+
const mediaAsset = mediaById.get(mediaId);
|
|
1258
|
+
|
|
1259
|
+
if (!mediaAsset || seenIds.has(mediaId)) {
|
|
1260
|
+
return [];
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
seenIds.add(mediaId);
|
|
1264
|
+
return [mediaAsset];
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
if (reorderedMediaAssets.length === 0) {
|
|
1268
|
+
return state;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
for (const mediaAsset of state.mediaAssets) {
|
|
1272
|
+
if (!seenIds.has(mediaAsset.id)) {
|
|
1273
|
+
reorderedMediaAssets.push(mediaAsset);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
if (
|
|
1278
|
+
reorderedMediaAssets.length === state.mediaAssets.length &&
|
|
1279
|
+
reorderedMediaAssets.every((asset, index) => asset.id === state.mediaAssets[index]?.id)
|
|
1280
|
+
) {
|
|
1281
|
+
return state;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
return commitStatePatch(state, {
|
|
1285
|
+
after: { mediaAssets: reorderedMediaAssets },
|
|
1286
|
+
before: { mediaAssets: state.mediaAssets },
|
|
1287
|
+
label: "Reorder media",
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1105
1291
|
case "timeline.setCurrentTime": {
|
|
1106
1292
|
return {
|
|
1107
1293
|
...state,
|