@pixel-point/toolcraft 0.0.7 → 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/templates/runtime/contracts/component-contracts.test.ts +84 -12
- package/templates/runtime/contracts/component-contracts.ts +40 -17
- package/templates/runtime/contracts/decision-contracts.ts +1 -1
- 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 +404 -8
- package/templates/runtime/react/controls-panel.tsx +90 -4
- package/templates/runtime/react/media-file.ts +19 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +1 -0
- package/templates/runtime/schema/define-toolcraft.ts +4 -2
- package/templates/runtime/schema/types.ts +4 -0
- package/templates/runtime/state/reducer.test.ts +304 -0
- package/templates/runtime/state/reducer.ts +148 -9
- package/templates/runtime/state/types.ts +10 -2
- package/templates/runtime/testing/performance.test.ts +1282 -48
- package/templates/runtime/testing/performance.ts +676 -39
- package/templates/starter/AGENTS.md +6 -5
- package/templates/starter/docs/toolcraft/acceptance-testing.md +11 -5
- package/templates/starter/docs/toolcraft/assembly-workflow.md +4 -2
- package/templates/starter/docs/toolcraft/component-rules.md +16 -7
- package/templates/starter/docs/toolcraft/custom-controls.md +8 -4
- package/templates/starter/docs/toolcraft/performance.md +47 -5
- package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +6 -6
- package/templates/starter/docs/toolcraft/workflow.md +2 -2
- package/templates/starter/e2e/app-performance.spec.ts +136 -3
- package/templates/starter/e2e/performance-helpers.ts +197 -0
- package/templates/starter/package.json +3 -0
- 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 +66 -5
- package/templates/ui/components/controls/actions/actions-control.tsx +3 -5
- 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/index.ts +1 -0
- package/templates/ui/components/panel/panel-section.tsx +53 -1
|
@@ -5,6 +5,10 @@ export type ToolcraftImportedImageFile = {
|
|
|
5
5
|
size: ToolcraftCanvasSize;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
export type ToolcraftImportedFile = {
|
|
9
|
+
dataUrl: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
8
12
|
function readFileDataUrl(file: File): Promise<string | null> {
|
|
9
13
|
return new Promise((resolve) => {
|
|
10
14
|
const reader = new FileReader();
|
|
@@ -65,6 +69,21 @@ function readImageDataUrlSize(dataUrl: string): Promise<ToolcraftCanvasSize | nu
|
|
|
65
69
|
});
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
export function isToolcraftImageFile(file: File): boolean {
|
|
73
|
+
return (
|
|
74
|
+
file.type.startsWith("image/") ||
|
|
75
|
+
/\.(avif|gif|heic|heif|jpe?g|png|svg|tiff?|webp)$/i.test(file.name)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function readImportedFile(
|
|
80
|
+
file: File,
|
|
81
|
+
): Promise<ToolcraftImportedFile | null> {
|
|
82
|
+
const dataUrl = await readFileDataUrl(file);
|
|
83
|
+
|
|
84
|
+
return dataUrl ? { dataUrl } : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
68
87
|
export async function readImportedImageFile(
|
|
69
88
|
file: File,
|
|
70
89
|
fallbackSize: ToolcraftCanvasSize,
|
|
@@ -332,7 +332,7 @@ function createToolcraftAssembly({
|
|
|
332
332
|
|
|
333
333
|
if (canvas.upload) {
|
|
334
334
|
capabilities.push("canvas.upload");
|
|
335
|
-
commands.push("media.delete", "media.import");
|
|
335
|
+
commands.push("media.delete", "media.import", "media.reorder");
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
338
|
|
|
@@ -478,7 +478,9 @@ function createToolcraftAssembly({
|
|
|
478
478
|
...(canvas.draggable
|
|
479
479
|
? (["canvas.panBy", "canvas.setOffset", "canvas.setViewport"] as const)
|
|
480
480
|
: []),
|
|
481
|
-
...(canvas.upload
|
|
481
|
+
...(canvas.upload
|
|
482
|
+
? (["media.delete", "media.import", "media.reorder"] as const)
|
|
483
|
+
: []),
|
|
482
484
|
])
|
|
483
485
|
: [],
|
|
484
486
|
enabled: canvas.enabled,
|
|
@@ -109,6 +109,7 @@ export type ToolcraftAssemblyCommand =
|
|
|
109
109
|
| "layers.toggleVisibility"
|
|
110
110
|
| "media.delete"
|
|
111
111
|
| "media.import"
|
|
112
|
+
| "media.reorder"
|
|
112
113
|
| "panels.resetOffset"
|
|
113
114
|
| "panels.setOffset"
|
|
114
115
|
| "timeline.changeKeyframeEasing"
|
|
@@ -273,6 +274,8 @@ export type ToolcraftControlPerformanceRole =
|
|
|
273
274
|
| "responsiveness"
|
|
274
275
|
| "workload";
|
|
275
276
|
|
|
277
|
+
export type ToolcraftFileDropAssetKind = "file" | "image";
|
|
278
|
+
|
|
276
279
|
export type ToolcraftControlConditionSchema = {
|
|
277
280
|
equals?: unknown;
|
|
278
281
|
greaterThan?: number;
|
|
@@ -323,6 +326,7 @@ export type ToolcraftCurveInterpolation = "monotone" | "smooth";
|
|
|
323
326
|
export type ToolcraftControlSchema = {
|
|
324
327
|
accept?: string;
|
|
325
328
|
actions?: readonly (ToolcraftActionSchema | string)[];
|
|
329
|
+
assetKind?: ToolcraftFileDropAssetKind;
|
|
326
330
|
addLabel?: string;
|
|
327
331
|
commitMode?: "content" | "setting";
|
|
328
332
|
defaultValue?: unknown;
|
|
@@ -98,6 +98,207 @@ describe("toolcraftReducer", () => {
|
|
|
98
98
|
});
|
|
99
99
|
});
|
|
100
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
|
+
|
|
101
302
|
it("updates canvas size and records history", () => {
|
|
102
303
|
const size = { width: 1200, height: 900, unit: "px" } as const;
|
|
103
304
|
|
|
@@ -511,6 +712,109 @@ describe("toolcraftReducer", () => {
|
|
|
511
712
|
expect(second.selectedLayerId).toBe("layer-2");
|
|
512
713
|
});
|
|
513
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
|
+
|
|
514
818
|
it("adds and selects runtime layers and groups", () => {
|
|
515
819
|
const withGroup = toolcraftReducer(createState(), {
|
|
516
820
|
layer: {
|
|
@@ -212,6 +212,66 @@ 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
|
+
|
|
215
275
|
function canvasSizesEqual(
|
|
216
276
|
first: ToolcraftState["canvas"]["size"],
|
|
217
277
|
second: ToolcraftState["canvas"]["size"],
|
|
@@ -697,16 +757,19 @@ export function toolcraftReducer(
|
|
|
697
757
|
|
|
698
758
|
case "controls.reset": {
|
|
699
759
|
const resetCanvasSize = getResetCanvasSize(state);
|
|
760
|
+
const resetMediaPatch = getResetMediaPatch(state);
|
|
700
761
|
|
|
701
|
-
if (resetCanvasSize) {
|
|
762
|
+
if (resetCanvasSize || resetMediaPatch) {
|
|
702
763
|
return commitStatePatch(state, {
|
|
703
764
|
after: {
|
|
704
765
|
...state.defaults,
|
|
705
|
-
"canvas.size": resetCanvasSize,
|
|
766
|
+
...(resetCanvasSize ? { "canvas.size": resetCanvasSize } : {}),
|
|
767
|
+
...resetMediaPatch?.after,
|
|
706
768
|
},
|
|
707
769
|
before: {
|
|
708
770
|
...state.values,
|
|
709
|
-
"canvas.size": state.canvas.size,
|
|
771
|
+
...(resetCanvasSize ? { "canvas.size": state.canvas.size } : {}),
|
|
772
|
+
...resetMediaPatch?.before,
|
|
710
773
|
},
|
|
711
774
|
label: "Reset controls",
|
|
712
775
|
});
|
|
@@ -727,6 +790,7 @@ export function toolcraftReducer(
|
|
|
727
790
|
const targetSet = new Set(command.targets);
|
|
728
791
|
const before: Record<string, unknown> = {};
|
|
729
792
|
const after: Record<string, unknown> = {};
|
|
793
|
+
const resetMediaPatch = getResetMediaPatch(state, targetSet);
|
|
730
794
|
|
|
731
795
|
for (const target of targetSet) {
|
|
732
796
|
if (!(target in state.defaults) || Object.is(state.values[target], state.defaults[target])) {
|
|
@@ -748,6 +812,11 @@ export function toolcraftReducer(
|
|
|
748
812
|
after["canvas.size"] = resetCanvasSize;
|
|
749
813
|
}
|
|
750
814
|
|
|
815
|
+
if (resetMediaPatch) {
|
|
816
|
+
Object.assign(before, resetMediaPatch.before);
|
|
817
|
+
Object.assign(after, resetMediaPatch.after);
|
|
818
|
+
}
|
|
819
|
+
|
|
751
820
|
if (Object.keys(after).length === 0) {
|
|
752
821
|
return state;
|
|
753
822
|
}
|
|
@@ -1077,15 +1146,29 @@ export function toolcraftReducer(
|
|
|
1077
1146
|
case "media.import": {
|
|
1078
1147
|
const shouldReplaceSingleLayerMedia =
|
|
1079
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;
|
|
1080
1155
|
const shouldResizeCanvas =
|
|
1081
|
-
state.schema.canvas.sizing.mode === "intrinsic-media"
|
|
1156
|
+
state.schema.canvas.sizing.mode === "intrinsic-media" &&
|
|
1157
|
+
assetKind === "image" &&
|
|
1158
|
+
command.asset.size !== undefined;
|
|
1082
1159
|
const layerId =
|
|
1083
1160
|
command.asset.layerId ??
|
|
1084
|
-
|
|
1161
|
+
existingSourceMediaAsset?.layerId ??
|
|
1162
|
+
(shouldReplaceSingleLayerMedia && !sourceTarget
|
|
1163
|
+
? getSingleLayerImportId(state)
|
|
1164
|
+
: undefined) ??
|
|
1085
1165
|
getNextLayerId(state);
|
|
1086
1166
|
const mediaId =
|
|
1087
1167
|
command.asset.id ??
|
|
1088
|
-
|
|
1168
|
+
existingSourceMediaAsset?.id ??
|
|
1169
|
+
(shouldReplaceSingleLayerMedia && !sourceTarget
|
|
1170
|
+
? getSingleMediaImportId(state)
|
|
1171
|
+
: undefined) ??
|
|
1089
1172
|
getNextMediaId(state);
|
|
1090
1173
|
const layer = {
|
|
1091
1174
|
displayName: command.asset.layerName ?? getImportedLayerName(command.asset.fileName),
|
|
@@ -1095,17 +1178,31 @@ export function toolcraftReducer(
|
|
|
1095
1178
|
visible: true,
|
|
1096
1179
|
};
|
|
1097
1180
|
const mediaAsset = {
|
|
1181
|
+
...(command.asset.assetKind ? { assetKind: command.asset.assetKind } : {}),
|
|
1098
1182
|
dataUrl: command.asset.dataUrl,
|
|
1099
1183
|
fileName: command.asset.fileName,
|
|
1100
1184
|
id: mediaId,
|
|
1101
1185
|
layerId,
|
|
1102
1186
|
mimeType: command.asset.mimeType,
|
|
1103
1187
|
position: shouldResizeCanvas ? { x: 0, y: 0 } : command.asset.position,
|
|
1104
|
-
size: command.asset.size,
|
|
1188
|
+
...(command.asset.size ? { size: command.asset.size } : {}),
|
|
1189
|
+
...(sourceTarget ? { sourceTarget } : {}),
|
|
1105
1190
|
};
|
|
1106
|
-
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];
|
|
1107
1198
|
const mediaAssets = shouldReplaceSingleLayerMedia
|
|
1108
|
-
?
|
|
1199
|
+
? sourceTarget
|
|
1200
|
+
? existingSourceMediaAsset
|
|
1201
|
+
? state.mediaAssets.map((asset) =>
|
|
1202
|
+
asset.id === existingSourceMediaAsset.id ? mediaAsset : asset,
|
|
1203
|
+
)
|
|
1204
|
+
: [...state.mediaAssets, mediaAsset]
|
|
1205
|
+
: [mediaAsset]
|
|
1109
1206
|
: [...state.mediaAssets, mediaAsset];
|
|
1110
1207
|
const after = {
|
|
1111
1208
|
...(shouldResizeCanvas ? { "canvas.size": command.asset.size } : {}),
|
|
@@ -1149,6 +1246,48 @@ export function toolcraftReducer(
|
|
|
1149
1246
|
});
|
|
1150
1247
|
}
|
|
1151
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
|
+
|
|
1152
1291
|
case "timeline.setCurrentTime": {
|
|
1153
1292
|
return {
|
|
1154
1293
|
...state,
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ToolcraftCanvasSize,
|
|
3
|
+
ToolcraftFileDropAssetKind,
|
|
4
|
+
ResolvedToolcraftAppSchema,
|
|
5
|
+
} from "../schema/types";
|
|
2
6
|
|
|
3
7
|
export type ToolcraftCommand =
|
|
4
8
|
| {
|
|
@@ -44,6 +48,7 @@ export type ToolcraftCommand =
|
|
|
44
48
|
type: "media.import";
|
|
45
49
|
}
|
|
46
50
|
| { mediaId: string; type: "media.delete" }
|
|
51
|
+
| { mediaIds: string[]; type: "media.reorder" }
|
|
47
52
|
| { currentTimeSeconds: number; type: "timeline.setCurrentTime" }
|
|
48
53
|
| { durationSeconds: number; type: "timeline.setDuration" }
|
|
49
54
|
| { expanded: boolean; type: "timeline.setExpanded" }
|
|
@@ -104,6 +109,7 @@ export const toolcraftRuntimeCommandTypes = [
|
|
|
104
109
|
"panels.resetOffset",
|
|
105
110
|
"media.import",
|
|
106
111
|
"media.delete",
|
|
112
|
+
"media.reorder",
|
|
107
113
|
"timeline.setCurrentTime",
|
|
108
114
|
"timeline.setDuration",
|
|
109
115
|
"timeline.setExpanded",
|
|
@@ -156,13 +162,15 @@ export type ToolcraftLayerDraft = {
|
|
|
156
162
|
};
|
|
157
163
|
|
|
158
164
|
export type ToolcraftMediaAsset = {
|
|
165
|
+
assetKind?: ToolcraftFileDropAssetKind;
|
|
159
166
|
dataUrl: string;
|
|
160
167
|
fileName: string;
|
|
161
168
|
id: string;
|
|
162
169
|
layerId: string;
|
|
163
170
|
mimeType: string;
|
|
164
171
|
position: ToolcraftPoint;
|
|
165
|
-
size
|
|
172
|
+
size?: ToolcraftCanvasSize;
|
|
173
|
+
sourceTarget?: string;
|
|
166
174
|
};
|
|
167
175
|
|
|
168
176
|
export type ToolcraftHistoryPatch = {
|