@pixel-point/toolcraft 0.0.8 → 0.0.11
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/README.md +42 -9
- package/package.json +1 -1
- package/src/generate.mjs +42 -5
- package/src/generate.test.mjs +40 -0
- package/src/package-json.mjs +15 -0
- package/src/package-json.test.mjs +14 -1
- package/templates/runtime/contracts/component-contracts.test.ts +251 -47
- package/templates/runtime/contracts/component-contracts.ts +130 -57
- package/templates/runtime/contracts/decision-contracts.test.ts +38 -1
- package/templates/runtime/contracts/decision-contracts.ts +22 -9
- package/templates/runtime/export/export.test.ts +63 -0
- package/templates/runtime/export/export.ts +55 -0
- package/templates/runtime/index.ts +1 -0
- package/templates/runtime/react/canvas-shell.test.tsx +58 -4
- package/templates/runtime/react/canvas-shell.tsx +31 -9
- package/templates/runtime/react/controls-panel.test.tsx +916 -108
- package/templates/runtime/react/controls-panel.tsx +136 -30
- package/templates/runtime/react/runtime-public-api.test.tsx +1 -1
- package/templates/runtime/react/settings-transfer.test.ts +4 -0
- package/templates/runtime/react/settings-transfer.ts +6 -1
- package/templates/runtime/react/timeline-panel.test.tsx +14 -0
- package/templates/runtime/react/timeline-panel.tsx +44 -7
- package/templates/runtime/react/toolcraft-app.integration.test.tsx +9 -1
- package/templates/runtime/react/toolcraft-app.test.tsx +112 -3
- package/templates/runtime/react/toolcraft-app.tsx +56 -37
- package/templates/runtime/schema/define-toolcraft.test.ts +266 -170
- package/templates/runtime/schema/define-toolcraft.ts +140 -246
- package/templates/runtime/schema/runtime-targets.ts +21 -0
- package/templates/runtime/schema/types.ts +44 -0
- package/templates/runtime/state/create-template-state.test.ts +156 -0
- package/templates/runtime/state/create-template-state.ts +38 -8
- package/templates/runtime/state/media-defaults.ts +105 -0
- package/templates/runtime/state/persistence.test.ts +58 -0
- package/templates/runtime/state/persistence.ts +105 -1
- package/templates/runtime/state/reducer.test.ts +280 -4
- package/templates/runtime/state/reducer.ts +195 -9
- package/templates/runtime/state/timeline-loop.test.ts +71 -0
- package/templates/runtime/state/timeline-loop.ts +35 -0
- package/templates/runtime/state/types.ts +27 -0
- package/templates/runtime/testing/performance.test.ts +810 -21
- package/templates/runtime/testing/performance.ts +823 -53
- package/templates/starter/AGENTS.md +24 -18
- package/templates/starter/docs/toolcraft/README.md +8 -4
- package/templates/starter/docs/toolcraft/acceptance-testing.md +43 -10
- package/templates/starter/docs/toolcraft/agent-worklog.md +1 -0
- package/templates/starter/docs/toolcraft/assembly-workflow.md +55 -20
- package/templates/starter/docs/toolcraft/component-rules.md +75 -42
- package/templates/starter/docs/toolcraft/decision-contract.md +2 -0
- package/templates/starter/docs/toolcraft/performance.md +37 -11
- package/templates/starter/docs/toolcraft/schema-reference.md +171 -41
- package/templates/starter/docs/toolcraft/workflow.md +5 -2
- package/templates/starter/e2e/app-browser-acceptance.spec.ts +3 -3
- package/templates/starter/e2e/app-performance.spec.ts +55 -2
- package/templates/starter/e2e/performance-helpers.ts +45 -0
- package/templates/starter/gitignore +1 -0
- package/templates/starter/index.html +1 -0
- package/templates/starter/package.json +3 -0
- package/templates/starter/playwright.config.ts +1 -1
- package/templates/starter/scripts/check-toolcraft-docs.mjs +1 -0
- package/templates/starter/scripts/run-vite-on-free-port.mjs +114 -13
- package/templates/starter/scripts/toolcraft-port.mjs +280 -0
- package/templates/starter/scripts/toolcraft-port.test.mjs +207 -1
- package/templates/starter/src/app/starter-acceptance.test.ts +3412 -479
- package/templates/starter/src/app/starter-acceptance.ts +1453 -97
- package/templates/starter/src/app/starter-performance.test.ts +111 -7
- package/templates/starter/src/app/starter-performance.ts +5 -0
- package/templates/starter/src/app/starter-schema.test.ts +32 -7
- package/templates/starter/src/app/starter-schema.ts +6 -2
- package/templates/starter/vite.config.ts +58 -2
- package/templates/ui/components/control-layout/index.tsx +8 -3
- package/templates/ui/components/controls/actions/actions-control.tsx +56 -5
- package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
- package/templates/ui/components/controls/color/index.ts +4 -1
- package/templates/ui/components/controls/color/palette-control.tsx +34 -4
- package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
- package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +186 -14
- package/templates/ui/components/controls/file-drop/index.ts +6 -1
- package/templates/ui/components/controls/index.ts +4 -0
- package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -1
- package/templates/ui/components/controls/select/select-control.tsx +8 -25
- package/templates/ui/components/controls/slider/slider-value.ts +0 -1
- package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
- package/templates/ui/components/controls/vector/index.ts +1 -0
- package/templates/ui/components/controls/vector/vector-control.tsx +109 -12
- package/templates/ui/components/panel/panel-actions.tsx +1 -1
- package/templates/ui/components/panel/panel-section.tsx +29 -5
|
@@ -110,7 +110,9 @@ export type ToolcraftAssemblyCommand =
|
|
|
110
110
|
| "media.delete"
|
|
111
111
|
| "media.import"
|
|
112
112
|
| "media.reorder"
|
|
113
|
+
| "media.transform"
|
|
113
114
|
| "panels.resetOffset"
|
|
115
|
+
| "panels.setHidden"
|
|
114
116
|
| "panels.setOffset"
|
|
115
117
|
| "timeline.changeKeyframeEasing"
|
|
116
118
|
| "timeline.deleteControlKeyframes"
|
|
@@ -180,11 +182,13 @@ export type ToolcraftTimelineMode = "keyframes" | "playback";
|
|
|
180
182
|
export type ToolcraftTimelinePanelSchema =
|
|
181
183
|
| boolean
|
|
182
184
|
| {
|
|
185
|
+
defaultDurationSeconds?: number;
|
|
183
186
|
enabled?: boolean;
|
|
184
187
|
mode?: ToolcraftTimelineMode;
|
|
185
188
|
};
|
|
186
189
|
|
|
187
190
|
export type ResolvedToolcraftTimelinePanelSchema = {
|
|
191
|
+
defaultDurationSeconds: number;
|
|
188
192
|
enabled: boolean;
|
|
189
193
|
mode: ToolcraftTimelineMode;
|
|
190
194
|
};
|
|
@@ -192,6 +196,7 @@ export type ResolvedToolcraftTimelinePanelSchema = {
|
|
|
192
196
|
export type ToolcraftPersistableStateSlice =
|
|
193
197
|
| "canvas"
|
|
194
198
|
| "layers"
|
|
199
|
+
| "media"
|
|
195
200
|
| "panels"
|
|
196
201
|
| "timeline"
|
|
197
202
|
| "values";
|
|
@@ -247,6 +252,7 @@ export type ToolcraftActionSchema = {
|
|
|
247
252
|
| "export"
|
|
248
253
|
| "rotate-ccw"
|
|
249
254
|
| "shuffle"
|
|
255
|
+
| "upload-simple"
|
|
250
256
|
| "wand-sparkles";
|
|
251
257
|
label?: string;
|
|
252
258
|
value: string;
|
|
@@ -276,6 +282,39 @@ export type ToolcraftControlPerformanceRole =
|
|
|
276
282
|
|
|
277
283
|
export type ToolcraftFileDropAssetKind = "file" | "image";
|
|
278
284
|
|
|
285
|
+
export type ToolcraftMediaPositionSchema = {
|
|
286
|
+
x: number;
|
|
287
|
+
y: number;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export type ToolcraftMediaTransformSchema = {
|
|
291
|
+
flipHorizontal?: boolean;
|
|
292
|
+
flipVertical?: boolean;
|
|
293
|
+
rotationDeg?: 0 | 90 | 180 | 270;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
export type ToolcraftDefaultMediaAssetSchema = {
|
|
297
|
+
assetKind?: ToolcraftFileDropAssetKind;
|
|
298
|
+
dataUrl: string;
|
|
299
|
+
fileName: string;
|
|
300
|
+
id?: string;
|
|
301
|
+
layerId?: string;
|
|
302
|
+
layerName?: string;
|
|
303
|
+
mimeType?: string;
|
|
304
|
+
position?: ToolcraftMediaPositionSchema;
|
|
305
|
+
size?: ToolcraftCanvasSize;
|
|
306
|
+
sourceTarget?: string;
|
|
307
|
+
transform?: ToolcraftMediaTransformSchema;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
export type ToolcraftMediaSchema = {
|
|
311
|
+
defaultAssets?: readonly ToolcraftDefaultMediaAssetSchema[];
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
export type ResolvedToolcraftMediaSchema = {
|
|
315
|
+
defaultAssets: readonly ToolcraftDefaultMediaAssetSchema[];
|
|
316
|
+
};
|
|
317
|
+
|
|
279
318
|
export type ToolcraftControlConditionSchema = {
|
|
280
319
|
equals?: unknown;
|
|
281
320
|
greaterThan?: number;
|
|
@@ -323,12 +362,15 @@ export type ToolcraftFontPickerValueSchema = {
|
|
|
323
362
|
|
|
324
363
|
export type ToolcraftCurveInterpolation = "monotone" | "smooth";
|
|
325
364
|
|
|
365
|
+
export type ToolcraftVectorCoordinateMode = "cartesian" | "screen";
|
|
366
|
+
|
|
326
367
|
export type ToolcraftControlSchema = {
|
|
327
368
|
accept?: string;
|
|
328
369
|
actions?: readonly (ToolcraftActionSchema | string)[];
|
|
329
370
|
assetKind?: ToolcraftFileDropAssetKind;
|
|
330
371
|
addLabel?: string;
|
|
331
372
|
commitMode?: "content" | "setting";
|
|
373
|
+
coordinateMode?: ToolcraftVectorCoordinateMode;
|
|
332
374
|
defaultValue?: unknown;
|
|
333
375
|
description?: string;
|
|
334
376
|
disabled?: boolean;
|
|
@@ -398,6 +440,7 @@ export type ResolvedToolcraftPanelsSchema = {
|
|
|
398
440
|
export type ToolcraftAppSchema = {
|
|
399
441
|
canvas: ToolcraftCanvasSchema;
|
|
400
442
|
export?: ToolcraftExportSchema;
|
|
443
|
+
media?: ToolcraftMediaSchema;
|
|
401
444
|
panels: ToolcraftPanelsSchema;
|
|
402
445
|
persistence?: ToolcraftPersistenceSchema;
|
|
403
446
|
settingsTransfer?: ToolcraftSettingsTransferSchema;
|
|
@@ -412,6 +455,7 @@ export type ResolvedToolcraftAppSchema = {
|
|
|
412
455
|
sizeSource: ToolcraftCanvasSizeSource;
|
|
413
456
|
};
|
|
414
457
|
export: ResolvedToolcraftExportSchema;
|
|
458
|
+
media: ResolvedToolcraftMediaSchema;
|
|
415
459
|
panels: ResolvedToolcraftPanelsSchema;
|
|
416
460
|
persistence: ResolvedToolcraftPersistenceSchema;
|
|
417
461
|
settingsTransfer: ResolvedToolcraftSettingsTransferSchema;
|
|
@@ -65,6 +65,42 @@ describe("createToolcraftState", () => {
|
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
it("keeps runtime timeline presentation out of product values and defaults", () => {
|
|
69
|
+
const app = defineToolcraft({
|
|
70
|
+
canvas: { enabled: true, renderScale: true, sizing: { mode: "editable-output" } },
|
|
71
|
+
panels: {
|
|
72
|
+
controls: {
|
|
73
|
+
sections: [],
|
|
74
|
+
title: "Controls",
|
|
75
|
+
},
|
|
76
|
+
timeline: { mode: "playback" },
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const state = createToolcraftState(app);
|
|
81
|
+
|
|
82
|
+
expect(state.values).not.toHaveProperty("panels.timeline.extended");
|
|
83
|
+
expect(state.values).not.toHaveProperty("panels.timeline.visible");
|
|
84
|
+
expect(state.defaults).not.toHaveProperty("panels.timeline.extended");
|
|
85
|
+
expect(state.defaults).not.toHaveProperty("panels.timeline.visible");
|
|
86
|
+
expect(state.panels.timeline.extended).toBeUndefined();
|
|
87
|
+
expect(state.panels.timeline.hidden).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("initializes timeline duration from the schema loop duration", () => {
|
|
91
|
+
const app = defineToolcraft({
|
|
92
|
+
canvas: { enabled: true },
|
|
93
|
+
panels: {
|
|
94
|
+
timeline: { defaultDurationSeconds: 14, mode: "playback" },
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const state = createToolcraftState(app);
|
|
99
|
+
|
|
100
|
+
expect(state.timeline.durationSeconds).toBe(14);
|
|
101
|
+
expect(state.timeline.isLooping).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
68
104
|
it("initializes canvas state from resolved canvas size", () => {
|
|
69
105
|
const size = { width: 1200, height: 900, unit: "px" } as const;
|
|
70
106
|
const app = defineToolcraft({
|
|
@@ -122,6 +158,126 @@ describe("createToolcraftState", () => {
|
|
|
122
158
|
expect(state.selectedLayerId).toBeNull();
|
|
123
159
|
});
|
|
124
160
|
|
|
161
|
+
it("initializes predefined media files as runtime attachments", () => {
|
|
162
|
+
const app = defineToolcraft({
|
|
163
|
+
canvas: { enabled: true, upload: true },
|
|
164
|
+
media: {
|
|
165
|
+
defaultAssets: [
|
|
166
|
+
{
|
|
167
|
+
dataUrl: "data:image/png;base64,AAAA",
|
|
168
|
+
fileName: "background.png",
|
|
169
|
+
id: "default-background",
|
|
170
|
+
layerId: "default-background-layer",
|
|
171
|
+
mimeType: "image/png",
|
|
172
|
+
size: { height: 1080, unit: "px", width: 1920 },
|
|
173
|
+
sourceTarget: "media.background",
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
panels: {
|
|
178
|
+
controls: {
|
|
179
|
+
sections: [
|
|
180
|
+
{
|
|
181
|
+
controls: {
|
|
182
|
+
background: {
|
|
183
|
+
defaultValue: null,
|
|
184
|
+
target: "media.background",
|
|
185
|
+
type: "fileDrop",
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
title: "Controls",
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const state = createToolcraftState(app);
|
|
196
|
+
|
|
197
|
+
expect(state.mediaAssets).toEqual([
|
|
198
|
+
expect.objectContaining({
|
|
199
|
+
dataUrl: "data:image/png;base64,AAAA",
|
|
200
|
+
fileName: "background.png",
|
|
201
|
+
id: "default-background",
|
|
202
|
+
layerId: "default-background-layer",
|
|
203
|
+
mimeType: "image/png",
|
|
204
|
+
sourceTarget: "media.background",
|
|
205
|
+
}),
|
|
206
|
+
]);
|
|
207
|
+
expect(state.layers).toEqual([
|
|
208
|
+
expect.objectContaining({
|
|
209
|
+
id: "default-background-layer",
|
|
210
|
+
name: "background",
|
|
211
|
+
visible: true,
|
|
212
|
+
}),
|
|
213
|
+
]);
|
|
214
|
+
expect(state.selectedLayerId).toBe("default-background-layer");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("lets persisted media state override schema default media", () => {
|
|
218
|
+
const app = defineToolcraft({
|
|
219
|
+
canvas: { enabled: true, upload: true },
|
|
220
|
+
media: {
|
|
221
|
+
defaultAssets: [
|
|
222
|
+
{
|
|
223
|
+
dataUrl: "data:image/png;base64,AAAA",
|
|
224
|
+
fileName: "background.png",
|
|
225
|
+
sourceTarget: "media.background",
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
panels: {},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const state = createToolcraftState(app, { mediaAssets: [] });
|
|
233
|
+
|
|
234
|
+
expect(state.mediaAssets).toEqual([]);
|
|
235
|
+
expect(state.layers).toEqual([]);
|
|
236
|
+
expect(state.selectedLayerId).toBeNull();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("restores default media layer names when persisted media omits layer state", () => {
|
|
240
|
+
const app = defineToolcraft({
|
|
241
|
+
canvas: { enabled: true, upload: true },
|
|
242
|
+
media: {
|
|
243
|
+
defaultAssets: [
|
|
244
|
+
{
|
|
245
|
+
dataUrl: "data:image/png;base64,AAAA",
|
|
246
|
+
fileName: "background.png",
|
|
247
|
+
id: "default-background",
|
|
248
|
+
layerId: "default-background-layer",
|
|
249
|
+
layerName: "Hero source",
|
|
250
|
+
sourceTarget: "media.background",
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
},
|
|
254
|
+
panels: {},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const state = createToolcraftState(app, {
|
|
258
|
+
mediaAssets: [
|
|
259
|
+
{
|
|
260
|
+
dataUrl: "data:image/png;base64,AAAA",
|
|
261
|
+
fileName: "background.png",
|
|
262
|
+
id: "default-background",
|
|
263
|
+
layerId: "default-background-layer",
|
|
264
|
+
mimeType: "image/png",
|
|
265
|
+
position: { x: 0, y: 0 },
|
|
266
|
+
sourceTarget: "media.background",
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
expect(state.layers).toEqual([
|
|
272
|
+
expect.objectContaining({
|
|
273
|
+
displayName: "Hero source",
|
|
274
|
+
id: "default-background-layer",
|
|
275
|
+
name: "Hero source",
|
|
276
|
+
}),
|
|
277
|
+
]);
|
|
278
|
+
expect(state.selectedLayerId).toBe("default-background-layer");
|
|
279
|
+
});
|
|
280
|
+
|
|
125
281
|
it("accepts seeded runtime layer state", () => {
|
|
126
282
|
const app = defineToolcraft({
|
|
127
283
|
canvas: { enabled: true },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResolvedToolcraftAppSchema } from "../schema/types";
|
|
2
|
+
import { isToolcraftTimelinePanelRuntimeTarget } from "../schema/runtime-targets";
|
|
2
3
|
import type {
|
|
3
4
|
ToolcraftInitialState,
|
|
4
5
|
ToolcraftState,
|
|
@@ -6,6 +7,12 @@ import type {
|
|
|
6
7
|
ToolcraftTimelineState,
|
|
7
8
|
} from "./types";
|
|
8
9
|
import { toolcraftCanvasZoomDefault } from "./canvas-zoom";
|
|
10
|
+
import {
|
|
11
|
+
cloneToolcraftLayers,
|
|
12
|
+
cloneToolcraftMediaAssets,
|
|
13
|
+
createToolcraftDefaultMediaState,
|
|
14
|
+
createToolcraftLayersFromMediaAssets,
|
|
15
|
+
} from "./media-defaults";
|
|
9
16
|
import { getMediaReadyTimelineState } from "./timeline-readiness";
|
|
10
17
|
|
|
11
18
|
function cloneTimelineKeyframeGroups(
|
|
@@ -26,12 +33,16 @@ function cloneTimelineKeyframeGroups(
|
|
|
26
33
|
}));
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
function createDefaultTimelineState(
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
function createDefaultTimelineState({
|
|
37
|
+
defaultDurationSeconds,
|
|
38
|
+
timeline,
|
|
39
|
+
}: {
|
|
40
|
+
defaultDurationSeconds: number;
|
|
41
|
+
timeline?: Partial<ToolcraftTimelineState>;
|
|
42
|
+
}): ToolcraftTimelineState {
|
|
32
43
|
return {
|
|
33
44
|
currentTimeSeconds: 0,
|
|
34
|
-
durationSeconds:
|
|
45
|
+
durationSeconds: defaultDurationSeconds,
|
|
35
46
|
expanded: false,
|
|
36
47
|
isLooping: true,
|
|
37
48
|
isPlaying: true,
|
|
@@ -46,15 +57,34 @@ export function createToolcraftState(
|
|
|
46
57
|
initialState: ToolcraftInitialState = {},
|
|
47
58
|
): ToolcraftState {
|
|
48
59
|
const defaults: Record<string, unknown> = {};
|
|
49
|
-
const
|
|
60
|
+
const defaultMediaState = createToolcraftDefaultMediaState(schema);
|
|
61
|
+
const hasInitialMediaAssets = Object.hasOwn(initialState, "mediaAssets");
|
|
62
|
+
const mediaAssets = hasInitialMediaAssets
|
|
63
|
+
? cloneToolcraftMediaAssets(initialState.mediaAssets ?? [])
|
|
64
|
+
: cloneToolcraftMediaAssets(defaultMediaState.mediaAssets);
|
|
65
|
+
const layers =
|
|
66
|
+
initialState.layers ??
|
|
67
|
+
(hasInitialMediaAssets
|
|
68
|
+
? createToolcraftLayersFromMediaAssets(mediaAssets, defaultMediaState.layers)
|
|
69
|
+
: cloneToolcraftLayers(defaultMediaState.layers));
|
|
70
|
+
const selectedLayerId =
|
|
71
|
+
initialState.selectedLayerId ??
|
|
72
|
+
(hasInitialMediaAssets ? (layers[0]?.id ?? null) : defaultMediaState.selectedLayerId);
|
|
50
73
|
const timeline = getMediaReadyTimelineState(
|
|
51
74
|
schema,
|
|
52
|
-
createDefaultTimelineState(
|
|
75
|
+
createDefaultTimelineState({
|
|
76
|
+
defaultDurationSeconds: schema.panels.timeline?.defaultDurationSeconds ?? 8,
|
|
77
|
+
timeline: initialState.timeline,
|
|
78
|
+
}),
|
|
53
79
|
mediaAssets,
|
|
54
80
|
);
|
|
55
81
|
|
|
56
82
|
for (const section of schema.panels.controls?.sections ?? []) {
|
|
57
83
|
for (const control of Object.values(section.controls)) {
|
|
84
|
+
if (isToolcraftTimelinePanelRuntimeTarget(control.target)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
58
88
|
defaults[control.target] = control.defaultValue;
|
|
59
89
|
}
|
|
60
90
|
}
|
|
@@ -79,7 +109,7 @@ export function createToolcraftState(
|
|
|
79
109
|
redo: [],
|
|
80
110
|
undo: [],
|
|
81
111
|
},
|
|
82
|
-
layers
|
|
112
|
+
layers,
|
|
83
113
|
mediaAssets,
|
|
84
114
|
panels: {
|
|
85
115
|
controls: { ...panels.controls, ...initialState.panels?.controls },
|
|
@@ -88,7 +118,7 @@ export function createToolcraftState(
|
|
|
88
118
|
toolbar: { ...panels.toolbar, ...initialState.panels?.toolbar },
|
|
89
119
|
},
|
|
90
120
|
schema,
|
|
91
|
-
selectedLayerId
|
|
121
|
+
selectedLayerId,
|
|
92
122
|
timeline,
|
|
93
123
|
values: { ...defaults, ...initialState.values },
|
|
94
124
|
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ResolvedToolcraftAppSchema } from "../schema/types";
|
|
2
|
+
import type { ToolcraftLayer, ToolcraftMediaAsset } from "./types";
|
|
3
|
+
|
|
4
|
+
export type ToolcraftDefaultMediaState = {
|
|
5
|
+
layers: ToolcraftLayer[];
|
|
6
|
+
mediaAssets: ToolcraftMediaAsset[];
|
|
7
|
+
selectedLayerId: string | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function getDefaultLayerName(fileName: string): string {
|
|
11
|
+
const baseName = fileName.replace(/\.[^.]+$/u, "").trim();
|
|
12
|
+
|
|
13
|
+
return baseName || "Media";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function cloneMediaAsset(asset: ToolcraftMediaAsset): ToolcraftMediaAsset {
|
|
17
|
+
return {
|
|
18
|
+
...asset,
|
|
19
|
+
position: { ...asset.position },
|
|
20
|
+
...(asset.size ? { size: { ...asset.size } } : {}),
|
|
21
|
+
...(asset.transform ? { transform: { ...asset.transform } } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function cloneLayer(layer: ToolcraftLayer): ToolcraftLayer {
|
|
26
|
+
return { ...layer };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function cloneToolcraftMediaAssets(
|
|
30
|
+
mediaAssets: readonly ToolcraftMediaAsset[],
|
|
31
|
+
): ToolcraftMediaAsset[] {
|
|
32
|
+
return mediaAssets.map(cloneMediaAsset);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function cloneToolcraftLayers(layers: readonly ToolcraftLayer[]): ToolcraftLayer[] {
|
|
36
|
+
return layers.map(cloneLayer);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createToolcraftLayersFromMediaAssets(
|
|
40
|
+
mediaAssets: readonly ToolcraftMediaAsset[],
|
|
41
|
+
defaultLayers: readonly ToolcraftLayer[] = [],
|
|
42
|
+
): ToolcraftLayer[] {
|
|
43
|
+
const layers: ToolcraftLayer[] = [];
|
|
44
|
+
const defaultLayerById = new Map(defaultLayers.map((layer) => [layer.id, layer]));
|
|
45
|
+
const seenLayerIds = new Set<string>();
|
|
46
|
+
|
|
47
|
+
for (const asset of mediaAssets) {
|
|
48
|
+
if (seenLayerIds.has(asset.layerId)) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
seenLayerIds.add(asset.layerId);
|
|
53
|
+
layers.push(
|
|
54
|
+
cloneLayer(
|
|
55
|
+
defaultLayerById.get(asset.layerId) ?? {
|
|
56
|
+
displayName: getDefaultLayerName(asset.fileName),
|
|
57
|
+
id: asset.layerId,
|
|
58
|
+
kind: "layer",
|
|
59
|
+
name: getDefaultLayerName(asset.fileName),
|
|
60
|
+
visible: true,
|
|
61
|
+
},
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return layers;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createToolcraftDefaultMediaState(
|
|
70
|
+
schema: ResolvedToolcraftAppSchema,
|
|
71
|
+
): ToolcraftDefaultMediaState {
|
|
72
|
+
const layers: ToolcraftLayer[] = [];
|
|
73
|
+
const mediaAssets: ToolcraftMediaAsset[] = [];
|
|
74
|
+
|
|
75
|
+
schema.media.defaultAssets.forEach((asset, index) => {
|
|
76
|
+
const layerId = asset.layerId ?? `default-media-layer-${index + 1}`;
|
|
77
|
+
const layerName = asset.layerName ?? getDefaultLayerName(asset.fileName);
|
|
78
|
+
|
|
79
|
+
layers.push({
|
|
80
|
+
displayName: layerName,
|
|
81
|
+
id: layerId,
|
|
82
|
+
kind: "layer",
|
|
83
|
+
name: layerName,
|
|
84
|
+
visible: true,
|
|
85
|
+
});
|
|
86
|
+
mediaAssets.push({
|
|
87
|
+
...(asset.assetKind ? { assetKind: asset.assetKind } : {}),
|
|
88
|
+
dataUrl: asset.dataUrl,
|
|
89
|
+
fileName: asset.fileName,
|
|
90
|
+
id: asset.id ?? `default-media-${index + 1}`,
|
|
91
|
+
layerId,
|
|
92
|
+
mimeType: asset.mimeType ?? (asset.assetKind === "file" ? "application/octet-stream" : "image/*"),
|
|
93
|
+
position: asset.position ?? { x: 0, y: 0 },
|
|
94
|
+
...(asset.size ? { size: asset.size } : {}),
|
|
95
|
+
...(asset.sourceTarget ? { sourceTarget: asset.sourceTarget } : {}),
|
|
96
|
+
...(asset.transform ? { transform: asset.transform } : {}),
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
layers,
|
|
102
|
+
mediaAssets,
|
|
103
|
+
selectedLayerId: layers[0]?.id ?? null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -77,6 +77,7 @@ describe("Toolcraft template state persistence", () => {
|
|
|
77
77
|
],
|
|
78
78
|
panels: {
|
|
79
79
|
controls: { offset: { x: 20, y: 10 } },
|
|
80
|
+
timeline: { hidden: true },
|
|
80
81
|
},
|
|
81
82
|
selectedLayerId: "layer-1",
|
|
82
83
|
timeline: {
|
|
@@ -125,6 +126,7 @@ describe("Toolcraft template state persistence", () => {
|
|
|
125
126
|
],
|
|
126
127
|
panels: {
|
|
127
128
|
controls: { offset: { x: 20, y: 10 } },
|
|
129
|
+
timeline: { hidden: true },
|
|
128
130
|
},
|
|
129
131
|
selectedLayerId: "layer-1",
|
|
130
132
|
timeline: {
|
|
@@ -146,10 +148,63 @@ describe("Toolcraft template state persistence", () => {
|
|
|
146
148
|
expect(snapshot?.state.values).not.toHaveProperty("legacy.target");
|
|
147
149
|
});
|
|
148
150
|
|
|
151
|
+
it("persists media state when the app opts into media persistence", () => {
|
|
152
|
+
const schema = defineToolcraft({
|
|
153
|
+
canvas: { enabled: true, upload: true },
|
|
154
|
+
media: {
|
|
155
|
+
defaultAssets: [
|
|
156
|
+
{
|
|
157
|
+
dataUrl: "data:image/png;base64,AAAA",
|
|
158
|
+
fileName: "background.png",
|
|
159
|
+
id: "default-background",
|
|
160
|
+
layerId: "default-background-layer",
|
|
161
|
+
mimeType: "image/png",
|
|
162
|
+
sourceTarget: "media.background",
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
panels: {
|
|
167
|
+
controls: {
|
|
168
|
+
sections: [
|
|
169
|
+
{
|
|
170
|
+
controls: {
|
|
171
|
+
background: {
|
|
172
|
+
defaultValue: null,
|
|
173
|
+
target: "media.background",
|
|
174
|
+
type: "fileDrop",
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
title: "Controls",
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
persistence: {
|
|
183
|
+
include: ["values", "panels", "media"],
|
|
184
|
+
key: "toolcraft:media-persistence-test:state:v1",
|
|
185
|
+
storage: "localStorage",
|
|
186
|
+
version: 1,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
const state = createToolcraftState(schema, { mediaAssets: [] });
|
|
190
|
+
const snapshot = createToolcraftPersistenceSnapshot(state, schema.persistence);
|
|
191
|
+
const parsed = parseToolcraftPersistenceSnapshot(
|
|
192
|
+
schema,
|
|
193
|
+
JSON.stringify(snapshot),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
expect(snapshot?.state.mediaAssets).toEqual([]);
|
|
197
|
+
expect(parsed?.mediaAssets).toEqual([]);
|
|
198
|
+
expect(createToolcraftState(schema, parsed).mediaAssets).toEqual([]);
|
|
199
|
+
});
|
|
200
|
+
|
|
149
201
|
it("parses matching versions and ignores stale targets or invalid versions", () => {
|
|
150
202
|
const schema = createPersistentSchema();
|
|
151
203
|
const rawValue = JSON.stringify({
|
|
152
204
|
state: {
|
|
205
|
+
panels: {
|
|
206
|
+
timeline: { hidden: true },
|
|
207
|
+
},
|
|
153
208
|
values: {
|
|
154
209
|
"generation.prompt": "Restored prompt",
|
|
155
210
|
"legacy.target": "remove me",
|
|
@@ -160,6 +215,9 @@ describe("Toolcraft template state persistence", () => {
|
|
|
160
215
|
});
|
|
161
216
|
|
|
162
217
|
expect(parseToolcraftPersistenceSnapshot(schema, rawValue)).toEqual({
|
|
218
|
+
panels: {
|
|
219
|
+
timeline: { hidden: true },
|
|
220
|
+
},
|
|
163
221
|
values: {
|
|
164
222
|
"generation.prompt": "Restored prompt",
|
|
165
223
|
"selectedLayer.opacity": 55,
|