@ai-game-assets/phaser 0.5.6 → 0.7.0
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/dist/animations.d.ts +2 -0
- package/dist/animations.d.ts.map +1 -1
- package/dist/animations.js +4 -3
- package/dist/animations.js.map +1 -1
- package/dist/debug-client.d.ts +52 -2
- package/dist/debug-client.d.ts.map +1 -1
- package/dist/debug-client.js +66 -0
- package/dist/debug-client.js.map +1 -1
- package/dist/designer-support.d.ts +31 -4
- package/dist/designer-support.d.ts.map +1 -1
- package/dist/designer-support.js +732 -80
- package/dist/designer-support.js.map +1 -1
- package/dist/designer.d.ts +19 -2
- package/dist/designer.d.ts.map +1 -1
- package/dist/designer.js +1735 -185
- package/dist/designer.js.map +1 -1
- package/dist/first-drafts.js +21 -9
- package/dist/first-drafts.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/keys.d.ts +2 -0
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +6 -0
- package/dist/keys.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +50 -3
- package/dist/loader.js.map +1 -1
- package/dist/phaser-types.d.ts +1 -0
- package/dist/phaser-types.d.ts.map +1 -1
- package/dist/runtime.d.ts +30 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +235 -8
- package/dist/runtime.js.map +1 -1
- package/dist/tileset-dialog.d.ts +121 -0
- package/dist/tileset-dialog.d.ts.map +1 -0
- package/dist/tileset-dialog.js +1594 -0
- package/dist/tileset-dialog.js.map +1 -0
- package/dist/tilesets.d.ts +13 -0
- package/dist/tilesets.d.ts.map +1 -0
- package/dist/tilesets.js +60 -0
- package/dist/tilesets.js.map +1 -0
- package/package.json +3 -2
package/dist/designer.js
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
|
-
import { linkedAnimationAssetIds, resolveTargetAssetId, topLevelAiAssetIds } from "@ai-game-assets/core";
|
|
1
|
+
import { expandAiAssetIds, linkedAnimationAssetIds, registerInGameDesignerPanel, resolveTargetAssetId, topLevelAiAssetIds } from "@ai-game-assets/core";
|
|
2
2
|
import { AiAssetDebugClient } from "./debug-client.js";
|
|
3
3
|
import { ensureMissingAiAssetFirstDrafts } from "./first-drafts.js";
|
|
4
4
|
import { createLocalDerivedOption, openDeriveDialog, referenceImageForCandidate } from "./derive-dialog.js";
|
|
5
|
-
import {
|
|
5
|
+
import { createMixedTilesetOption, isDesignerTilesetAsset, openTilesetBaseMixerDialog, openTilesetAnimationEditor, openTilesetAnimationMixerDialog, openTilesetEditor, tilesetAnimationForKey, resolveTilesetBaseMixCurrent, tilesetMetadataForAsset, tilesetTileGenerationOverride } from "./tileset-dialog.js";
|
|
6
|
+
import { aiTilesetAnimationTextureKey } from "./keys.js";
|
|
7
|
+
/**
|
|
8
|
+
* Resolves the canonical, untransformed source used by Edit Tileset. A pending
|
|
9
|
+
* option is its own editing lineage: when it has no saved source metadata, its
|
|
10
|
+
* rendered image becomes the new source instead of borrowing the active
|
|
11
|
+
* version's unrelated source sheet.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveTilesetEditorInitialState(options) {
|
|
14
|
+
if (options.currentOption) {
|
|
15
|
+
const sourceSrc = options.currentOption.tilesetSourceDataUrl;
|
|
16
|
+
return {
|
|
17
|
+
sourceSrc: sourceSrc ?? options.renderedSrc,
|
|
18
|
+
initialTransforms: sourceSrc
|
|
19
|
+
? cloneTilesetTransforms(options.currentOption.tilesetTransforms)
|
|
20
|
+
: undefined
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const sourceFile = options.activeVersion?.tilesetSourceFile;
|
|
24
|
+
return {
|
|
25
|
+
sourceSrc: sourceFile
|
|
26
|
+
? options.resolveAssetUrl(sourceFile)
|
|
27
|
+
: options.renderedSrc,
|
|
28
|
+
initialTransforms: sourceFile
|
|
29
|
+
? cloneTilesetTransforms(options.activeVersion?.tilesetTransforms)
|
|
30
|
+
: undefined
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function cloneTilesetTransforms(transforms) {
|
|
34
|
+
return transforms?.map((transform) => ({ ...transform }));
|
|
35
|
+
}
|
|
36
|
+
import { assetWithGeneratedGeometry, audioGenerationOverridesFromInputs, bindKeyboardCapture, canEditGenerationFormat, createDesignerElements, effectiveGenerationFormat, ensureDesignerStyles, errorMessage, generationOverridesFromInputs, hasStyleGuide, imageSourceToDataUrl, installPromotedImageTexture, isAbortError, isAudioAsset, isSvgSource, isVoiceAsset, loadImageElement, mimeTypeFromDataUrl, normalizeAssetFormat, normalizeAudioFormat, openAnimationEditor, openAssetVersionsDialog, openAudioEditor, openFrameTouchUpEditor, openStyleGuideEditor, pickUploadFile, positiveIntegerInput, previewCurrentAsset, previewImageSource, previewOption, readableAssetName, renderAssetFolderBrowser, renderAudioPlayer, renderOptions, resolvePreviewDisplaySize, setStatus, stopStatusAnimation, styleGuideDraftFromManifest, styleGuideRequest, uploadedOptionFromFile, voiceGenerationOverridesFromInputs } from "./designer-support.js";
|
|
6
37
|
export function installAiAssetDesigner(options) {
|
|
7
38
|
ensureDesignerStyles();
|
|
8
39
|
const client = options.client ?? new AiAssetDebugClient();
|
|
@@ -12,14 +43,46 @@ export function installAiAssetDesigner(options) {
|
|
|
12
43
|
let selectedAssetId = visibleAssetIds()[0] ?? Object.keys(manifest.assets)[0];
|
|
13
44
|
let selectedTargetId = options.targetId;
|
|
14
45
|
let selectedTargetAssetId = selectedAssetId;
|
|
46
|
+
let selectedTilesetAnimationKey;
|
|
15
47
|
let selectedOption;
|
|
48
|
+
let displayedOptions;
|
|
16
49
|
let stopCurrentAnimationPreview;
|
|
17
50
|
let editedCurrentOption;
|
|
18
51
|
let previewedVersionName;
|
|
52
|
+
const pendingOptions = new Map();
|
|
19
53
|
let styleGuideDraft = styleGuideDraftFromManifest(manifest, resolveAssetUrl);
|
|
20
54
|
const formatDrafts = new Map();
|
|
55
|
+
const tilesetPromptDrafts = new Map();
|
|
56
|
+
const tilesetAnimationPromptDrafts = new Map();
|
|
57
|
+
const tilesetAnimationFrameDrafts = new Map();
|
|
58
|
+
let tilesetAnimationPromptRevision = 0;
|
|
59
|
+
let displayedTilesetAnimationOptions;
|
|
60
|
+
let stopTilesetAnimationOptionPreviews;
|
|
21
61
|
let activeGeneration;
|
|
22
62
|
let generationId = 0;
|
|
63
|
+
let panelRevision = 0;
|
|
64
|
+
let promotionId = 0;
|
|
65
|
+
let activePromotionId;
|
|
66
|
+
let mixingTileset = false;
|
|
67
|
+
const regenerateTilesetTile = async (assetId, tile, tileset, currentTileSrc) => {
|
|
68
|
+
const guide = await styleGuideRequest(styleGuideDraft);
|
|
69
|
+
return client.generate({
|
|
70
|
+
assetId,
|
|
71
|
+
count: 3,
|
|
72
|
+
format: effectiveGenerationFormat(manifest, formatDrafts, selectedAssetId, assetId),
|
|
73
|
+
tileset,
|
|
74
|
+
styleGuide: {
|
|
75
|
+
...guide,
|
|
76
|
+
images: [
|
|
77
|
+
...guide.images,
|
|
78
|
+
{
|
|
79
|
+
name: `${assetId}-current-tile-${tile + 1}-style.png`,
|
|
80
|
+
dataUrl: currentTileSrc
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
};
|
|
23
86
|
if (!selectedAssetId) {
|
|
24
87
|
throw new Error("AI asset designer requires at least one asset.");
|
|
25
88
|
}
|
|
@@ -37,15 +100,107 @@ export function installAiAssetDesigner(options) {
|
|
|
37
100
|
const mount = options.mount ?? document.body;
|
|
38
101
|
mount.append(elements.root);
|
|
39
102
|
bindKeyboardCapture(elements.root, options.scene);
|
|
40
|
-
const
|
|
103
|
+
const syncPromoteAllButton = () => {
|
|
104
|
+
elements.promoteAllButton.disabled =
|
|
105
|
+
pendingOptions.size === 0 ||
|
|
106
|
+
Boolean(activeGeneration) ||
|
|
107
|
+
activePromotionId !== undefined;
|
|
108
|
+
};
|
|
109
|
+
const rememberPendingOption = (assetId, option, pending = {}) => {
|
|
110
|
+
pendingOptions.set(assetId, {
|
|
111
|
+
option,
|
|
112
|
+
inheritAnimations: Boolean(pending.inheritAnimations),
|
|
113
|
+
previewedVersionName: pending.previewedVersionName,
|
|
114
|
+
tilesetAnimations: pending.tilesetAnimations,
|
|
115
|
+
animationOnlyKey: pending.animationOnlyKey
|
|
116
|
+
});
|
|
117
|
+
syncPromoteAllButton();
|
|
118
|
+
};
|
|
119
|
+
const forgetPendingOption = (assetId) => {
|
|
120
|
+
pendingOptions.delete(assetId);
|
|
121
|
+
syncPromoteAllButton();
|
|
122
|
+
};
|
|
123
|
+
const hasEditableTilesetAnimationFrames = (assetId, animationKey) => {
|
|
124
|
+
const pendingFrames = pendingOptions.get(assetId)?.tilesetAnimations?.[animationKey];
|
|
125
|
+
if (pendingFrames?.length)
|
|
126
|
+
return true;
|
|
127
|
+
const asset = manifest.assets[assetId];
|
|
128
|
+
const activeVersion = asset?.versions[asset.activeVersion];
|
|
129
|
+
return Boolean(activeVersion?.tilesetAnimations?.[animationKey]?.files.length);
|
|
130
|
+
};
|
|
131
|
+
const installTilesetAnimationFrameTextures = async (frames, asset, textureKeyForFrame) => {
|
|
132
|
+
const tileset = tilesetMetadataForAsset(asset);
|
|
133
|
+
if (!tileset || !options.scene.textures.addSpriteSheet)
|
|
134
|
+
return [];
|
|
135
|
+
const images = await Promise.all(frames.map((frame) => loadImageElement(frame)));
|
|
136
|
+
const textureKeys = images.map((image, index) => {
|
|
137
|
+
const textureKey = textureKeyForFrame(index);
|
|
138
|
+
if (options.scene.textures.exists(textureKey)) {
|
|
139
|
+
options.scene.textures.remove(textureKey);
|
|
140
|
+
}
|
|
141
|
+
options.scene.textures.addSpriteSheet?.(textureKey, image, {
|
|
142
|
+
frameWidth: tileset.tileWidth,
|
|
143
|
+
frameHeight: tileset.tileHeight,
|
|
144
|
+
margin: tileset.margin,
|
|
145
|
+
spacing: tileset.spacing
|
|
146
|
+
});
|
|
147
|
+
return textureKey;
|
|
148
|
+
});
|
|
149
|
+
return textureKeys;
|
|
150
|
+
};
|
|
151
|
+
const previewTilesetAnimationFrames = async (assetId, animationKey, frames, asset) => {
|
|
152
|
+
const previewId = Date.now();
|
|
153
|
+
const textureKeys = await installTilesetAnimationFrameTextures(frames, asset, (index) => `ai-preview:${assetId}:tileset:${encodeURIComponent(animationKey)}:${index}:${previewId}`);
|
|
154
|
+
options.onTilesetAnimationPreview?.(assetId, animationKey, textureKeys, asset);
|
|
155
|
+
};
|
|
156
|
+
const applyOpenState = (isOpen) => {
|
|
41
157
|
elements.root.dataset.open = String(isOpen);
|
|
42
158
|
elements.toggle.setAttribute("aria-expanded", String(isOpen));
|
|
43
159
|
};
|
|
160
|
+
const dockPanel = registerInGameDesignerPanel({
|
|
161
|
+
id: "ai-game-assets.assets",
|
|
162
|
+
label: "Assets",
|
|
163
|
+
panel: elements.panel,
|
|
164
|
+
dragHandle: elements.title,
|
|
165
|
+
button: elements.toggle,
|
|
166
|
+
order: 10,
|
|
167
|
+
ariaLabel: "Toggle AI asset designer",
|
|
168
|
+
onOpenChange: applyOpenState
|
|
169
|
+
});
|
|
170
|
+
const setOpen = (isOpen) => {
|
|
171
|
+
if (isOpen) {
|
|
172
|
+
dockPanel.open();
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
dockPanel.close();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
44
178
|
const syncAnimationChoices = (assetId) => {
|
|
179
|
+
const resolvedAssetId = resolveTargetAssetId(manifest, assetId, selectedTargetId);
|
|
180
|
+
const resolvedAsset = manifest.assets[resolvedAssetId] ?? manifest.assets[assetId];
|
|
45
181
|
const asset = manifest.assets[assetId];
|
|
46
182
|
const linkedAnimations = Object.entries(asset.linkedAnimationAssets ?? {});
|
|
47
183
|
const isVoice = asset.kind === "voice";
|
|
48
184
|
elements.animationSelect.innerHTML = "";
|
|
185
|
+
if (isDesignerTilesetAsset(resolvedAsset)) {
|
|
186
|
+
const baseOption = document.createElement("option");
|
|
187
|
+
baseOption.value = "tileset:base";
|
|
188
|
+
baseOption.textContent = "Base tileset";
|
|
189
|
+
elements.animationSelect.append(baseOption);
|
|
190
|
+
for (const animation of tilesetMetadataForAsset(resolvedAsset)?.animations ?? []) {
|
|
191
|
+
const option = document.createElement("option");
|
|
192
|
+
option.value = `tileset:animation:${encodeURIComponent(animation.key)}`;
|
|
193
|
+
option.textContent = readableAssetName(animation.key);
|
|
194
|
+
elements.animationSelect.append(option);
|
|
195
|
+
}
|
|
196
|
+
selectedTargetAssetId = resolvedAssetId;
|
|
197
|
+
selectedTilesetAnimationKey = undefined;
|
|
198
|
+
elements.animationField.hidden = false;
|
|
199
|
+
elements.animationField.firstElementChild.textContent = "Tileset view";
|
|
200
|
+
elements.animationSelect.value = "tileset:base";
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
selectedTilesetAnimationKey = undefined;
|
|
49
204
|
if (asset.kind !== "collection") {
|
|
50
205
|
const baseOption = document.createElement("option");
|
|
51
206
|
baseOption.value = resolveTargetAssetId(manifest, assetId, selectedTargetId);
|
|
@@ -65,17 +220,143 @@ export function installAiAssetDesigner(options) {
|
|
|
65
220
|
: resolveTargetAssetId(manifest, assetId, selectedTargetId);
|
|
66
221
|
elements.animationSelect.value = selectedTargetAssetId;
|
|
67
222
|
};
|
|
68
|
-
const
|
|
223
|
+
const effectiveTilesetTileCount = (asset) => {
|
|
224
|
+
const tileset = tilesetMetadataForAsset(asset);
|
|
225
|
+
if (!tileset)
|
|
226
|
+
return 0;
|
|
227
|
+
const capacity = tileset.columns * tileset.rows;
|
|
228
|
+
return positiveIntegerInput(elements.frameCountInput, Math.min(tileset.tileCount ?? capacity, capacity));
|
|
229
|
+
};
|
|
230
|
+
const tilePromptDraftsForAsset = (assetId) => {
|
|
231
|
+
const existing = tilesetPromptDrafts.get(assetId);
|
|
232
|
+
if (existing)
|
|
233
|
+
return existing;
|
|
234
|
+
const configured = tilesetMetadataForAsset(manifest.assets[assetId])?.tiles
|
|
235
|
+
?.map((tile) => tile.prompt) ?? [];
|
|
236
|
+
tilesetPromptDrafts.set(assetId, configured);
|
|
237
|
+
return configured;
|
|
238
|
+
};
|
|
239
|
+
const tilesetAnimationPromptDraftKey = (assetId, animationKey) => (`${assetId}\u0000${animationKey}`);
|
|
240
|
+
const animationTilePromptDraftsForAsset = (assetId, animationKey) => {
|
|
241
|
+
const draftKey = tilesetAnimationPromptDraftKey(assetId, animationKey);
|
|
242
|
+
const existing = tilesetAnimationPromptDrafts.get(draftKey);
|
|
243
|
+
if (existing)
|
|
244
|
+
return existing;
|
|
245
|
+
const asset = manifest.assets[assetId];
|
|
246
|
+
const tileset = tilesetMetadataForAsset(asset);
|
|
247
|
+
const animation = tilesetAnimationForKey(asset, animationKey);
|
|
248
|
+
const tileCount = tileset
|
|
249
|
+
? Math.min(tileset.tileCount ?? tileset.columns * tileset.rows, tileset.columns * tileset.rows)
|
|
250
|
+
: 0;
|
|
251
|
+
const configured = animation?.tiles?.map((tile) => tile.prompt) ??
|
|
252
|
+
Array.from({ length: tileCount }, (_, tile) => {
|
|
253
|
+
const basePrompt = tileset?.tiles?.[tile]?.prompt?.trim();
|
|
254
|
+
return basePrompt
|
|
255
|
+
? `Keep this ${basePrompt} tile unchanged unless this animation needs it to move.`
|
|
256
|
+
: "Keep this tile unchanged unless this animation needs it to move.";
|
|
257
|
+
});
|
|
258
|
+
tilesetAnimationPromptDrafts.set(draftKey, configured);
|
|
259
|
+
return configured;
|
|
260
|
+
};
|
|
261
|
+
const renderTilesetPromptInputs = (assetId) => {
|
|
262
|
+
const asset = manifest.assets[assetId];
|
|
263
|
+
const tileset = tilesetMetadataForAsset(asset);
|
|
264
|
+
const isAnimation = Boolean(selectedTilesetAnimationKey);
|
|
265
|
+
const tileCount = isAnimation && tileset
|
|
266
|
+
? Math.min(tileset.tileCount ?? tileset.columns * tileset.rows, tileset.columns * tileset.rows)
|
|
267
|
+
: effectiveTilesetTileCount(asset);
|
|
268
|
+
const drafts = selectedTilesetAnimationKey
|
|
269
|
+
? animationTilePromptDraftsForAsset(assetId, selectedTilesetAnimationKey)
|
|
270
|
+
: tilePromptDraftsForAsset(assetId);
|
|
271
|
+
if (!isAnimation)
|
|
272
|
+
elements.frameCountInput.value = String(tileCount);
|
|
273
|
+
elements.tilesetPromptsList.replaceChildren();
|
|
274
|
+
const title = elements.tilesetPromptsField.querySelector(".ai-game-assets-designer__tile-prompts-title");
|
|
275
|
+
const hint = elements.tilesetPromptsField.querySelector(".ai-game-assets-designer__tile-prompts-hint");
|
|
276
|
+
if (title)
|
|
277
|
+
title.textContent = isAnimation ? "Tile animation prompts" : "Tile prompts";
|
|
278
|
+
if (hint) {
|
|
279
|
+
hint.textContent = isAnimation
|
|
280
|
+
? "Describe how every tile behaves in this animation, in exact sheet order."
|
|
281
|
+
: "Describe each tile in exact sheet order.";
|
|
282
|
+
}
|
|
283
|
+
for (let tileIndex = 0; tileIndex < tileCount; tileIndex += 1) {
|
|
284
|
+
const input = document.createElement("textarea");
|
|
285
|
+
input.rows = 3;
|
|
286
|
+
input.value = drafts[tileIndex] ?? "";
|
|
287
|
+
input.setAttribute("aria-label", `Tile ${tileIndex + 1} prompt`);
|
|
288
|
+
input.addEventListener("input", () => {
|
|
289
|
+
drafts[tileIndex] = input.value;
|
|
290
|
+
if (isAnimation && selectedTilesetAnimationKey) {
|
|
291
|
+
elements.promoteButton.disabled = true;
|
|
292
|
+
void stageTilesetAnimationPromptDraft(assetId, selectedTilesetAnimationKey);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
const label = document.createElement("label");
|
|
296
|
+
label.className = "ai-game-assets-designer__field";
|
|
297
|
+
const labelText = document.createElement("span");
|
|
298
|
+
labelText.textContent = `Tile ${tileIndex + 1}`;
|
|
299
|
+
label.append(labelText, input);
|
|
300
|
+
elements.tilesetPromptsList.append(label);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
const tilesetAnimationPromptDefinitions = (assetId, animationKey) => {
|
|
304
|
+
const tileset = tilesetMetadataForAsset(manifest.assets[assetId]);
|
|
305
|
+
if (!tileset)
|
|
306
|
+
return undefined;
|
|
307
|
+
const tileCount = Math.min(tileset.tileCount ?? tileset.columns * tileset.rows, tileset.columns * tileset.rows);
|
|
308
|
+
const prompts = animationTilePromptDraftsForAsset(assetId, animationKey)
|
|
309
|
+
.slice(0, tileCount)
|
|
310
|
+
.map((prompt) => prompt.trim());
|
|
311
|
+
if (prompts.length !== tileCount || prompts.some((prompt) => !prompt))
|
|
312
|
+
return undefined;
|
|
313
|
+
return prompts.map((prompt) => ({ prompt }));
|
|
314
|
+
};
|
|
315
|
+
const tilesetAnimationDefinitionFromInputs = (assetId, animationKey) => {
|
|
316
|
+
const animation = tilesetAnimationForKey(manifest.assets[assetId], animationKey);
|
|
317
|
+
const tiles = tilesetAnimationPromptDefinitions(assetId, animationKey);
|
|
318
|
+
if (!animation || !tiles)
|
|
319
|
+
return undefined;
|
|
320
|
+
const frameCount = positiveIntegerInput(elements.frameCountInput, animation.frameCount);
|
|
321
|
+
const defaultDelayMs = Math.round(1000 / Math.max(1, animation.frameRate));
|
|
322
|
+
return {
|
|
323
|
+
...animation,
|
|
324
|
+
frameCount,
|
|
325
|
+
tiles,
|
|
326
|
+
frameTimings: Array.from({ length: frameCount }, (_, index) => (animation.frameTimings?.[index] ?? { delayMs: defaultDelayMs }))
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
const assetWithTilesetAnimationDefinition = (asset, definition) => ({
|
|
330
|
+
...asset,
|
|
331
|
+
tileset: asset.tileset ? {
|
|
332
|
+
...asset.tileset,
|
|
333
|
+
animations: (asset.tileset.animations ?? []).map((animation) => (animation.key === definition.key ? definition : animation))
|
|
334
|
+
} : asset.tileset
|
|
335
|
+
});
|
|
336
|
+
const tilesetPromptDefinitions = (assetId, tileCount) => {
|
|
337
|
+
const prompts = tilePromptDraftsForAsset(assetId)
|
|
338
|
+
.slice(0, tileCount)
|
|
339
|
+
.map((prompt) => prompt.trim());
|
|
340
|
+
if (prompts.length !== tileCount || prompts.some((prompt) => !prompt)) {
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
return prompts.map((prompt) => ({ prompt }));
|
|
344
|
+
};
|
|
345
|
+
const syncTargetAsset = (assetId, syncOptions = {}) => {
|
|
69
346
|
const asset = manifest.assets[assetId];
|
|
70
347
|
const activeVersion = asset.versions[asset.activeVersion];
|
|
71
348
|
const isAudio = isAudioAsset(asset);
|
|
72
349
|
const isVoice = isVoiceAsset(asset);
|
|
73
350
|
const isVoiceLine = asset.kind === "voice-line";
|
|
351
|
+
const tileset = tilesetMetadataForAsset(asset);
|
|
352
|
+
const tilesetAnimation = selectedTilesetAnimationKey
|
|
353
|
+
? tilesetAnimationForKey(asset, selectedTilesetAnimationKey)
|
|
354
|
+
: undefined;
|
|
74
355
|
stopCurrentAnimationPreview?.();
|
|
75
356
|
stopCurrentAnimationPreview = undefined;
|
|
76
|
-
elements.promptInput.value = activeVersion?.prompt ?? asset.prompt;
|
|
77
|
-
elements.widthInput.value = String(asset.frameGrid?.frameWidth ?? asset.dimensions?.width ?? 1);
|
|
78
|
-
elements.heightInput.value = String(asset.frameGrid?.frameHeight ?? asset.dimensions?.height ?? 1);
|
|
357
|
+
elements.promptInput.value = tilesetAnimation?.prompt ?? activeVersion?.prompt ?? asset.prompt;
|
|
358
|
+
elements.widthInput.value = String(asset.frameGrid?.frameWidth ?? tileset?.tileWidth ?? asset.dimensions?.width ?? 1);
|
|
359
|
+
elements.heightInput.value = String(asset.frameGrid?.frameHeight ?? tileset?.tileHeight ?? asset.dimensions?.height ?? 1);
|
|
79
360
|
elements.audioFormatSelect.value = asset.audioSettings?.format ?? "mp3";
|
|
80
361
|
elements.audioDurationInput.value = String(asset.audioSettings?.durationSeconds ?? activeVersion?.durationSeconds ?? "");
|
|
81
362
|
elements.audioLoopInput.checked = Boolean(asset.audioSettings?.loop);
|
|
@@ -92,14 +373,43 @@ export function installAiAssetDesigner(options) {
|
|
|
92
373
|
elements.audioLoopField.hidden = !isAudio;
|
|
93
374
|
elements.voiceTextField.hidden = !isVoice;
|
|
94
375
|
elements.voiceTextField.firstChild.textContent = isVoiceLine ? "Text to say" : "Demo sentence";
|
|
95
|
-
elements.formatField.hidden = isAudio ||
|
|
96
|
-
|
|
376
|
+
elements.formatField.hidden = isAudio || Boolean(selectedTilesetAnimationKey) ||
|
|
377
|
+
elements.formatField.hidden;
|
|
378
|
+
const isTilesetAnimation = Boolean(tileset && selectedTilesetAnimationKey);
|
|
379
|
+
const isBaseTileset = Boolean(tileset && !selectedTilesetAnimationKey);
|
|
380
|
+
elements.promptField.hidden = Boolean(tileset);
|
|
381
|
+
elements.tilesetPromptsField.hidden = !tileset;
|
|
382
|
+
elements.widthField.firstElementChild.textContent = tileset ? "Tile width" : "Width";
|
|
383
|
+
elements.heightField.firstElementChild.textContent = tileset ? "Tile height" : "Height";
|
|
384
|
+
elements.frameCountField.firstElementChild.textContent = isTilesetAnimation
|
|
385
|
+
? "Animation frames"
|
|
386
|
+
: tileset ? "Tiles" : "Frames";
|
|
387
|
+
elements.dimensionGrid.hidden = isAudio || isTilesetAnimation;
|
|
97
388
|
syncTargetVariantLabel(assetId);
|
|
98
|
-
elements.frameCountInput.value = String(
|
|
99
|
-
|
|
389
|
+
elements.frameCountInput.value = String(isTilesetAnimation
|
|
390
|
+
? tilesetAnimation?.frameCount ?? 1
|
|
391
|
+
: asset.frameGrid?.frameCount ??
|
|
392
|
+
(asset.frameGrid
|
|
393
|
+
? asset.frameGrid.columns * asset.frameGrid.rows
|
|
394
|
+
: tileset?.tileCount ?? (tileset ? tileset.columns * tileset.rows : 1)));
|
|
395
|
+
if (tileset && !isTilesetAnimation) {
|
|
396
|
+
elements.frameCountInput.max = String(tileset.columns * tileset.rows);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
elements.frameCountInput.removeAttribute("max");
|
|
400
|
+
}
|
|
401
|
+
if (tileset) {
|
|
402
|
+
renderTilesetPromptInputs(assetId);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
elements.tilesetPromptsList.replaceChildren();
|
|
406
|
+
}
|
|
100
407
|
const activeVersionSource = activeVersion?.file ? resolveAssetUrl(activeVersion.file) : "";
|
|
101
|
-
elements.frameCountField.hidden =
|
|
102
|
-
|
|
408
|
+
elements.frameCountField.hidden = !isTilesetAnimation &&
|
|
409
|
+
(!tileset && (asset.kind === "image" || !asset.frameGrid));
|
|
410
|
+
elements.versionLabel.textContent = selectedTilesetAnimationKey
|
|
411
|
+
? `Active ${readableAssetName(assetId)} animation: ${readableAssetName(selectedTilesetAnimationKey)}`
|
|
412
|
+
: `Active ${readableAssetName(assetId)}: ${asset.activeVersion}`;
|
|
103
413
|
elements.currentImage.src = activeVersionSource;
|
|
104
414
|
if (isAudio) {
|
|
105
415
|
renderAudioPlayer({
|
|
@@ -118,23 +428,415 @@ export function installAiAssetDesigner(options) {
|
|
|
118
428
|
elements.currentAnimation.hidden = true;
|
|
119
429
|
elements.currentAudio.hidden = !isAudio;
|
|
120
430
|
elements.currentImage.hidden = isAudio;
|
|
121
|
-
elements.currentAnimationButton.textContent =
|
|
431
|
+
elements.currentAnimationButton.textContent = isTilesetAnimation
|
|
432
|
+
? "Edit animation..."
|
|
433
|
+
: "Edit...";
|
|
434
|
+
elements.currentTouchUpButton.textContent = isBaseTileset ? "Edit tileset..." : "Touch up...";
|
|
435
|
+
elements.currentRevertButton.hidden = true;
|
|
122
436
|
elements.currentPreview.classList.add("is-selected");
|
|
123
|
-
elements.currentAnimationButton.hidden =
|
|
437
|
+
elements.currentAnimationButton.hidden = isTilesetAnimation && selectedTilesetAnimationKey
|
|
438
|
+
? !hasEditableTilesetAnimationFrames(assetId, selectedTilesetAnimationKey)
|
|
439
|
+
: !activeVersion?.file || (!asset.frameGrid && !isAudio);
|
|
124
440
|
elements.currentTouchUpButton.hidden = !activeVersion?.file ||
|
|
441
|
+
Boolean(selectedTilesetAnimationKey) ||
|
|
125
442
|
isAudio ||
|
|
126
443
|
Boolean(asset.frameGrid) ||
|
|
127
444
|
normalizeAssetFormat(asset.settings?.format) === "svg" ||
|
|
128
445
|
isSvgSource(activeVersion?.file ?? "");
|
|
129
|
-
|
|
130
|
-
|
|
446
|
+
if (syncOptions.preserveOptions && displayedOptions?.assetId === assetId) {
|
|
447
|
+
renderGeneratedOptions(assetId, displayedOptions.generated);
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
clearGeneratedOptions();
|
|
451
|
+
}
|
|
131
452
|
setStatus(elements, "", "idle");
|
|
132
453
|
elements.promoteButton.disabled = true;
|
|
454
|
+
elements.uploadButton.disabled = Boolean(selectedTilesetAnimationKey);
|
|
133
455
|
elements.versionsButton.disabled = Object.keys(asset.versions).length <= 1;
|
|
134
|
-
elements.deriveButton.hidden =
|
|
456
|
+
elements.deriveButton.hidden = Boolean(selectedTilesetAnimationKey) ||
|
|
457
|
+
isAudio || deriveCandidatesForAsset(assetId).length === 0;
|
|
135
458
|
selectedOption = undefined;
|
|
136
459
|
editedCurrentOption = undefined;
|
|
137
460
|
previewedVersionName = undefined;
|
|
461
|
+
const pending = pendingOptions.get(assetId);
|
|
462
|
+
if (pending) {
|
|
463
|
+
selectedOption = pending.option;
|
|
464
|
+
editedCurrentOption = pending.option;
|
|
465
|
+
previewedVersionName = pending.previewedVersionName;
|
|
466
|
+
showOptionInCurrentPreview(pending.option, "pending preview");
|
|
467
|
+
previewedVersionName = pending.previewedVersionName;
|
|
468
|
+
elements.currentPreview.classList.add("is-selected");
|
|
469
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
470
|
+
}
|
|
471
|
+
syncPromoteAllButton();
|
|
472
|
+
};
|
|
473
|
+
const showOptionInCurrentPreview = (option, label = "selected option") => {
|
|
474
|
+
const asset = manifest.assets[selectedTargetAssetId];
|
|
475
|
+
const optionAsset = assetWithGeneratedGeometry(asset, option);
|
|
476
|
+
const isAudio = isAudioAsset(asset);
|
|
477
|
+
const isTilesetAnimation = Boolean(selectedTilesetAnimationKey && isDesignerTilesetAsset(optionAsset));
|
|
478
|
+
stopCurrentAnimationPreview?.();
|
|
479
|
+
stopCurrentAnimationPreview = undefined;
|
|
480
|
+
editedCurrentOption = option;
|
|
481
|
+
previewedVersionName = undefined;
|
|
482
|
+
elements.currentPreview.hidden = false;
|
|
483
|
+
elements.currentPreview.setAttribute("aria-label", `Preview ${label} for ${readableAssetName(selectedTargetAssetId)}`);
|
|
484
|
+
elements.currentAnimation.hidden = true;
|
|
485
|
+
elements.currentAnimationButton.textContent = isTilesetAnimation
|
|
486
|
+
? "Edit animation..."
|
|
487
|
+
: "Edit...";
|
|
488
|
+
elements.currentAnimationButton.hidden = isTilesetAnimation && selectedTilesetAnimationKey
|
|
489
|
+
? !hasEditableTilesetAnimationFrames(selectedTargetAssetId, selectedTilesetAnimationKey)
|
|
490
|
+
: !optionAsset.frameGrid && !isAudio;
|
|
491
|
+
elements.currentTouchUpButton.textContent = isDesignerTilesetAsset(optionAsset)
|
|
492
|
+
? "Edit tileset..."
|
|
493
|
+
: "Touch up...";
|
|
494
|
+
elements.currentTouchUpButton.hidden =
|
|
495
|
+
isAudio ||
|
|
496
|
+
isTilesetAnimation ||
|
|
497
|
+
Boolean(optionAsset.frameGrid) ||
|
|
498
|
+
option.mimeType === "image/svg+xml";
|
|
499
|
+
elements.currentRevertButton.hidden = false;
|
|
500
|
+
elements.currentAudio.hidden = !isAudio;
|
|
501
|
+
elements.currentImage.hidden = isAudio;
|
|
502
|
+
if (isAudio) {
|
|
503
|
+
renderAudioPlayer({
|
|
504
|
+
container: elements.currentAudio,
|
|
505
|
+
src: option.dataUrl,
|
|
506
|
+
label: readableAssetName(selectedTargetAssetId),
|
|
507
|
+
playback: option.audioPlayback
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
elements.currentAudio.innerHTML = "";
|
|
512
|
+
elements.currentImage.src = option.dataUrl;
|
|
513
|
+
elements.currentImage.alt = `${readableAssetName(selectedTargetAssetId)} ${label}`;
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
const syncMixTilesetButton = () => {
|
|
517
|
+
const session = displayedOptions;
|
|
518
|
+
const asset = session ? manifest.assets[session.assetId] : undefined;
|
|
519
|
+
const baseVisible = Boolean(session &&
|
|
520
|
+
session.assetId === selectedTargetAssetId &&
|
|
521
|
+
session.generated.length === 3 &&
|
|
522
|
+
isDesignerTilesetAsset(asset) &&
|
|
523
|
+
!selectedTilesetAnimationKey);
|
|
524
|
+
const animationSession = displayedTilesetAnimationOptions;
|
|
525
|
+
const animationVisible = Boolean(animationSession &&
|
|
526
|
+
animationSession.assetId === selectedTargetAssetId &&
|
|
527
|
+
animationSession.animationKey === selectedTilesetAnimationKey &&
|
|
528
|
+
animationSession.generated.length === 3);
|
|
529
|
+
const visible = baseVisible || animationVisible;
|
|
530
|
+
elements.mixTilesetButton.textContent = animationVisible ? "Mix tilesets" : "Mix tileset";
|
|
531
|
+
elements.mixTilesetButton.hidden = !visible;
|
|
532
|
+
elements.mixTilesetButton.disabled = !visible || Boolean(activeGeneration || activePromotionId !== undefined || mixingTileset);
|
|
533
|
+
};
|
|
534
|
+
const renderGeneratedOptions = (assetId, generated) => {
|
|
535
|
+
stopTilesetAnimationOptionPreviews?.();
|
|
536
|
+
stopTilesetAnimationOptionPreviews = undefined;
|
|
537
|
+
displayedTilesetAnimationOptions = undefined;
|
|
538
|
+
displayedOptions = {
|
|
539
|
+
assetId,
|
|
540
|
+
generated: [...generated]
|
|
541
|
+
};
|
|
542
|
+
renderOptions({
|
|
543
|
+
elements,
|
|
544
|
+
generated,
|
|
545
|
+
scene: options.scene,
|
|
546
|
+
manifest,
|
|
547
|
+
assetId,
|
|
548
|
+
designerOptions: options,
|
|
549
|
+
onPreview: options.onPreview,
|
|
550
|
+
onSelected(option) {
|
|
551
|
+
selectedOption = option;
|
|
552
|
+
rememberPendingOption(assetId, option);
|
|
553
|
+
showOptionInCurrentPreview(option);
|
|
554
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
syncMixTilesetButton();
|
|
558
|
+
};
|
|
559
|
+
const currentTilesetOption = async (assetId, definition) => {
|
|
560
|
+
const asset = manifest.assets[assetId];
|
|
561
|
+
const pending = pendingOptions.get(assetId)?.option;
|
|
562
|
+
const activeVersion = asset.versions[asset.activeVersion];
|
|
563
|
+
const dataUrl = pending?.dataUrl ??
|
|
564
|
+
await imageSourceToDataUrl(resolveAssetUrl(activeVersion.file));
|
|
565
|
+
const tilesetSourceDataUrl = pending
|
|
566
|
+
? pending.tilesetSourceDataUrl
|
|
567
|
+
: activeVersion.tilesetSourceFile
|
|
568
|
+
? await imageSourceToDataUrl(resolveAssetUrl(activeVersion.tilesetSourceFile))
|
|
569
|
+
: undefined;
|
|
570
|
+
return {
|
|
571
|
+
...(pending ?? {
|
|
572
|
+
index: -1,
|
|
573
|
+
dataUrl,
|
|
574
|
+
mimeType: mimeTypeFromDataUrl(dataUrl),
|
|
575
|
+
prompt: activeVersion.prompt ?? asset.prompt,
|
|
576
|
+
model: activeVersion.model,
|
|
577
|
+
revisedPrompt: activeVersion.revisedPrompt,
|
|
578
|
+
dimensions: asset.dimensions,
|
|
579
|
+
settings: activeVersion.settings ?? asset.settings
|
|
580
|
+
}),
|
|
581
|
+
dataUrl,
|
|
582
|
+
tilesetSourceDataUrl,
|
|
583
|
+
tilesetTransforms: tilesetSourceDataUrl
|
|
584
|
+
? cloneTilesetTransforms(pending ? pending.tilesetTransforms : activeVersion.tilesetTransforms)
|
|
585
|
+
: undefined,
|
|
586
|
+
tileset: assetWithTilesetAnimationDefinition(asset, definition).tileset
|
|
587
|
+
};
|
|
588
|
+
};
|
|
589
|
+
const activeTilesetAnimationFrames = (assetId, animationKey) => {
|
|
590
|
+
const asset = manifest.assets[assetId];
|
|
591
|
+
const activeVersion = asset?.versions[asset.activeVersion];
|
|
592
|
+
const files = activeVersion?.tilesetAnimations?.[animationKey]?.files;
|
|
593
|
+
if (!asset || !activeVersion || !files?.length) {
|
|
594
|
+
return Promise.reject(new Error(`Generate ${readableAssetName(animationKey)} animation frames before promoting its prompts.`));
|
|
595
|
+
}
|
|
596
|
+
const cacheKey = `${assetId}\u0000${asset.activeVersion}\u0000${animationKey}`;
|
|
597
|
+
const existing = tilesetAnimationFrameDrafts.get(cacheKey);
|
|
598
|
+
if (existing)
|
|
599
|
+
return existing;
|
|
600
|
+
const loading = Promise.all(files.map((file) => imageSourceToDataUrl(resolveAssetUrl(file))));
|
|
601
|
+
tilesetAnimationFrameDrafts.set(cacheKey, loading);
|
|
602
|
+
void loading.catch(() => {
|
|
603
|
+
if (tilesetAnimationFrameDrafts.get(cacheKey) === loading) {
|
|
604
|
+
tilesetAnimationFrameDrafts.delete(cacheKey);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
return loading;
|
|
608
|
+
};
|
|
609
|
+
const promptOnlyTilesetAnimationDefinition = (assetId, animationKey, pending = pendingOptions.get(assetId)) => {
|
|
610
|
+
const tiles = tilesetAnimationPromptDefinitions(assetId, animationKey);
|
|
611
|
+
const declared = tilesetAnimationForKey(manifest.assets[assetId], animationKey);
|
|
612
|
+
const pendingDefinition = pending?.option.tileset?.animations?.find((animation) => animation.key === animationKey);
|
|
613
|
+
const source = pendingDefinition ?? declared;
|
|
614
|
+
return source && tiles ? { ...source, tiles } : undefined;
|
|
615
|
+
};
|
|
616
|
+
const stageTilesetAnimationPromptDraft = async (assetId, animationKey) => {
|
|
617
|
+
const revision = tilesetAnimationPromptRevision + 1;
|
|
618
|
+
tilesetAnimationPromptRevision = revision;
|
|
619
|
+
if (!promptOnlyTilesetAnimationDefinition(assetId, animationKey)) {
|
|
620
|
+
elements.promoteButton.disabled = true;
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
const pendingBeforeLoad = pendingOptions.get(assetId);
|
|
625
|
+
const loadedFrames = pendingBeforeLoad?.tilesetAnimations?.[animationKey] ??
|
|
626
|
+
await activeTilesetAnimationFrames(assetId, animationKey);
|
|
627
|
+
if (revision !== tilesetAnimationPromptRevision ||
|
|
628
|
+
selectedTargetAssetId !== assetId ||
|
|
629
|
+
selectedTilesetAnimationKey !== animationKey)
|
|
630
|
+
return;
|
|
631
|
+
const pending = pendingOptions.get(assetId);
|
|
632
|
+
const frames = pending?.tilesetAnimations?.[animationKey] ?? loadedFrames;
|
|
633
|
+
const definition = promptOnlyTilesetAnimationDefinition(assetId, animationKey, pending);
|
|
634
|
+
if (!definition) {
|
|
635
|
+
elements.promoteButton.disabled = true;
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (frames.length !== definition.frameCount) {
|
|
639
|
+
throw new Error(`${readableAssetName(animationKey)} has ${frames.length} current frames, but ` +
|
|
640
|
+
`${definition.frameCount} are configured. Generate new frames before promoting.`);
|
|
641
|
+
}
|
|
642
|
+
const option = await currentTilesetOption(assetId, definition);
|
|
643
|
+
if (revision !== tilesetAnimationPromptRevision ||
|
|
644
|
+
selectedTargetAssetId !== assetId ||
|
|
645
|
+
selectedTilesetAnimationKey !== animationKey)
|
|
646
|
+
return;
|
|
647
|
+
const latestPending = pendingOptions.get(assetId);
|
|
648
|
+
selectedOption = option;
|
|
649
|
+
editedCurrentOption = option;
|
|
650
|
+
rememberPendingOption(assetId, option, {
|
|
651
|
+
inheritAnimations: latestPending?.inheritAnimations,
|
|
652
|
+
previewedVersionName: latestPending?.previewedVersionName,
|
|
653
|
+
tilesetAnimations: {
|
|
654
|
+
...(latestPending?.tilesetAnimations ?? {}),
|
|
655
|
+
[animationKey]: latestPending?.tilesetAnimations?.[animationKey] ?? frames
|
|
656
|
+
},
|
|
657
|
+
animationOnlyKey: latestPending?.animationOnlyKey ??
|
|
658
|
+
(latestPending ? undefined : animationKey)
|
|
659
|
+
});
|
|
660
|
+
if (displayedTilesetAnimationOptions?.assetId === assetId &&
|
|
661
|
+
displayedTilesetAnimationOptions.animationKey === animationKey) {
|
|
662
|
+
displayedTilesetAnimationOptions.animation = {
|
|
663
|
+
...displayedTilesetAnimationOptions.animation,
|
|
664
|
+
tiles: definition.tiles
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
668
|
+
setStatus(elements, "Animation prompt changes ready. Promote to save them.", "info");
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
if (revision !== tilesetAnimationPromptRevision ||
|
|
672
|
+
selectedTargetAssetId !== assetId ||
|
|
673
|
+
selectedTilesetAnimationKey !== animationKey)
|
|
674
|
+
return;
|
|
675
|
+
elements.promoteButton.disabled = !selectedOption || activePromotionId !== undefined;
|
|
676
|
+
setStatus(elements, `Could not stage animation prompts. ${errorMessage(error)}`, "error");
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
const selectTilesetAnimationFrames = async (assetId, definition, frames, card) => {
|
|
680
|
+
const currentPrompts = tilesetAnimationPromptDefinitions(assetId, definition.key);
|
|
681
|
+
const selectedDefinition = currentPrompts
|
|
682
|
+
? { ...definition, tiles: currentPrompts }
|
|
683
|
+
: definition;
|
|
684
|
+
const option = await currentTilesetOption(assetId, selectedDefinition);
|
|
685
|
+
const pending = pendingOptions.get(assetId);
|
|
686
|
+
selectedOption = option;
|
|
687
|
+
rememberPendingOption(assetId, option, {
|
|
688
|
+
inheritAnimations: pending?.inheritAnimations,
|
|
689
|
+
previewedVersionName: pending?.previewedVersionName,
|
|
690
|
+
tilesetAnimations: {
|
|
691
|
+
...(pending?.tilesetAnimations ?? {}),
|
|
692
|
+
[selectedDefinition.key]: frames
|
|
693
|
+
},
|
|
694
|
+
animationOnlyKey: pending?.animationOnlyKey ??
|
|
695
|
+
(pending ? undefined : selectedDefinition.key)
|
|
696
|
+
});
|
|
697
|
+
for (const item of elements.options.querySelectorAll(".ai-game-assets-designer__option")) {
|
|
698
|
+
item.classList.toggle("is-selected", item === card);
|
|
699
|
+
}
|
|
700
|
+
elements.currentPreview.classList.remove("is-selected");
|
|
701
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
702
|
+
await previewTilesetAnimationFrames(assetId, selectedDefinition.key, frames, assetWithTilesetAnimationDefinition(manifest.assets[assetId], selectedDefinition));
|
|
703
|
+
};
|
|
704
|
+
const renderTilesetAnimationOptions = (assetId, definition, generated) => {
|
|
705
|
+
stopTilesetAnimationOptionPreviews?.();
|
|
706
|
+
displayedOptions = undefined;
|
|
707
|
+
displayedTilesetAnimationOptions = {
|
|
708
|
+
assetId,
|
|
709
|
+
animationKey: definition.key,
|
|
710
|
+
animation: definition,
|
|
711
|
+
generated: [...generated]
|
|
712
|
+
};
|
|
713
|
+
elements.options.replaceChildren();
|
|
714
|
+
elements.options.classList.remove("is-audio");
|
|
715
|
+
const stops = [];
|
|
716
|
+
const delayForFrame = (index) => (definition.frameTimings?.[index]?.delayMs ??
|
|
717
|
+
Math.round(1000 / Math.max(1, definition.frameRate)));
|
|
718
|
+
for (const candidate of generated) {
|
|
719
|
+
const card = document.createElement("div");
|
|
720
|
+
card.className = "ai-game-assets-designer__option";
|
|
721
|
+
const button = document.createElement("button");
|
|
722
|
+
button.type = "button";
|
|
723
|
+
button.className = "ai-game-assets-designer__option-select";
|
|
724
|
+
button.setAttribute("aria-label", `Select animation option ${candidate.index + 1}`);
|
|
725
|
+
const image = document.createElement("img");
|
|
726
|
+
image.alt = `${assetId} animation option ${candidate.index + 1}`;
|
|
727
|
+
const label = document.createElement("span");
|
|
728
|
+
label.textContent = `Option ${candidate.index + 1}`;
|
|
729
|
+
button.append(image, label);
|
|
730
|
+
card.append(button);
|
|
731
|
+
elements.options.append(card);
|
|
732
|
+
let frame = 0;
|
|
733
|
+
let timeout;
|
|
734
|
+
let stopped = false;
|
|
735
|
+
const animate = () => {
|
|
736
|
+
if (stopped || candidate.frames.length === 0)
|
|
737
|
+
return;
|
|
738
|
+
image.src = candidate.frames[frame % candidate.frames.length].dataUrl;
|
|
739
|
+
const currentFrame = frame;
|
|
740
|
+
frame += 1;
|
|
741
|
+
timeout = window.setTimeout(animate, delayForFrame(currentFrame));
|
|
742
|
+
};
|
|
743
|
+
animate();
|
|
744
|
+
stops.push(() => {
|
|
745
|
+
stopped = true;
|
|
746
|
+
if (timeout !== undefined)
|
|
747
|
+
window.clearTimeout(timeout);
|
|
748
|
+
});
|
|
749
|
+
button.addEventListener("click", async () => {
|
|
750
|
+
setStatus(elements, `Applying animation option ${candidate.index + 1}...`, "busy");
|
|
751
|
+
try {
|
|
752
|
+
await selectTilesetAnimationFrames(assetId, definition, candidate.frames.map((item) => item.dataUrl), card);
|
|
753
|
+
setStatus(elements, `Previewing animation option ${candidate.index + 1}. Promote to save it.`, "info");
|
|
754
|
+
}
|
|
755
|
+
catch (error) {
|
|
756
|
+
setStatus(elements, `Could not preview animation. ${errorMessage(error)}`, "error");
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
stopTilesetAnimationOptionPreviews = () => stops.forEach((stop) => stop());
|
|
761
|
+
syncMixTilesetButton();
|
|
762
|
+
};
|
|
763
|
+
const clearGeneratedOptions = () => {
|
|
764
|
+
stopTilesetAnimationOptionPreviews?.();
|
|
765
|
+
stopTilesetAnimationOptionPreviews = undefined;
|
|
766
|
+
displayedTilesetAnimationOptions = undefined;
|
|
767
|
+
displayedOptions = undefined;
|
|
768
|
+
elements.options.innerHTML = "";
|
|
769
|
+
elements.options.classList.remove("is-audio");
|
|
770
|
+
syncMixTilesetButton();
|
|
771
|
+
};
|
|
772
|
+
const restoreActivePreview = () => {
|
|
773
|
+
const asset = manifest.assets[selectedTargetAssetId];
|
|
774
|
+
const activeVersion = asset.versions[asset.activeVersion];
|
|
775
|
+
if (!activeVersion?.file)
|
|
776
|
+
return;
|
|
777
|
+
stopCurrentAnimationPreview?.();
|
|
778
|
+
stopCurrentAnimationPreview = undefined;
|
|
779
|
+
editedCurrentOption = undefined;
|
|
780
|
+
previewedVersionName = undefined;
|
|
781
|
+
selectedOption = undefined;
|
|
782
|
+
forgetPendingOption(selectedTargetAssetId);
|
|
783
|
+
for (const item of elements.options.querySelectorAll(".ai-game-assets-designer__option")) {
|
|
784
|
+
item.classList.remove("is-selected");
|
|
785
|
+
}
|
|
786
|
+
const activeVersionSource = resolveAssetUrl(activeVersion.file);
|
|
787
|
+
const isAudio = isAudioAsset(asset);
|
|
788
|
+
const isTilesetAnimation = Boolean(selectedTilesetAnimationKey && isDesignerTilesetAsset(asset));
|
|
789
|
+
elements.currentPreview.classList.add("is-selected");
|
|
790
|
+
elements.currentPreview.setAttribute("aria-label", `Preview active ${readableAssetName(selectedTargetAssetId)} version`);
|
|
791
|
+
elements.currentAnimation.hidden = true;
|
|
792
|
+
elements.currentAnimationButton.textContent = isTilesetAnimation
|
|
793
|
+
? "Edit animation..."
|
|
794
|
+
: "Edit...";
|
|
795
|
+
elements.currentAnimationButton.hidden = isTilesetAnimation && selectedTilesetAnimationKey
|
|
796
|
+
? !hasEditableTilesetAnimationFrames(selectedTargetAssetId, selectedTilesetAnimationKey)
|
|
797
|
+
: !asset.frameGrid && !isAudio;
|
|
798
|
+
elements.currentTouchUpButton.textContent = isDesignerTilesetAsset(asset)
|
|
799
|
+
? "Edit tileset..."
|
|
800
|
+
: "Touch up...";
|
|
801
|
+
elements.currentTouchUpButton.hidden =
|
|
802
|
+
isAudio ||
|
|
803
|
+
isTilesetAnimation ||
|
|
804
|
+
Boolean(asset.frameGrid) ||
|
|
805
|
+
normalizeAssetFormat(asset.settings?.format) === "svg" ||
|
|
806
|
+
isSvgSource(activeVersion.file);
|
|
807
|
+
elements.currentRevertButton.hidden = true;
|
|
808
|
+
elements.currentAudio.hidden = !isAudio;
|
|
809
|
+
elements.currentImage.hidden = isAudio;
|
|
810
|
+
elements.promoteButton.disabled = true;
|
|
811
|
+
if (isAudio) {
|
|
812
|
+
renderAudioPlayer({
|
|
813
|
+
container: elements.currentAudio,
|
|
814
|
+
src: activeVersionSource,
|
|
815
|
+
label: readableAssetName(selectedTargetAssetId),
|
|
816
|
+
playback: activeVersion.audioPlayback
|
|
817
|
+
});
|
|
818
|
+
(options.onAssetReady ?? options.onPreview)(selectedTargetAssetId, activeVersionSource, asset);
|
|
819
|
+
setStatus(elements, "Reverted preview to the active version.", "info");
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
elements.currentAudio.innerHTML = "";
|
|
823
|
+
elements.currentImage.src = activeVersionSource;
|
|
824
|
+
elements.currentImage.alt = `${readableAssetName(selectedTargetAssetId)} active version`;
|
|
825
|
+
setStatus(elements, "Reverting preview in the game...", "busy");
|
|
826
|
+
previewCurrentAsset({
|
|
827
|
+
scene: options.scene,
|
|
828
|
+
manifest,
|
|
829
|
+
assetId: selectedTargetAssetId,
|
|
830
|
+
src: activeVersionSource,
|
|
831
|
+
onPreview(assetId, textureKey, previewAsset) {
|
|
832
|
+
(options.onAssetReady ?? options.onPreview)(assetId, textureKey, previewAsset);
|
|
833
|
+
setStatus(elements, "Reverted preview to the active version.", "info");
|
|
834
|
+
},
|
|
835
|
+
onError(error) {
|
|
836
|
+
setStatus(elements, `Could not revert the game preview. ${error.message}`, "error");
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
}
|
|
138
840
|
};
|
|
139
841
|
const syncAsset = (assetId) => {
|
|
140
842
|
syncAnimationChoices(assetId);
|
|
@@ -230,6 +932,7 @@ export function installAiAssetDesigner(options) {
|
|
|
230
932
|
selectedAssetId,
|
|
231
933
|
assetIds: visibleAssetIds(),
|
|
232
934
|
onSelect(assetId) {
|
|
935
|
+
invalidatePanelWork();
|
|
233
936
|
selectedAssetId = assetId;
|
|
234
937
|
syncAsset(selectedAssetId);
|
|
235
938
|
}
|
|
@@ -257,16 +960,117 @@ export function installAiAssetDesigner(options) {
|
|
|
257
960
|
activeGeneration = undefined;
|
|
258
961
|
elements.regenerateButton.textContent = "Regenerate";
|
|
259
962
|
unlockGenerationStatus();
|
|
963
|
+
elements.promoteButton.disabled = !selectedOption || activePromotionId !== undefined;
|
|
964
|
+
syncPromoteAllButton();
|
|
965
|
+
syncMixTilesetButton();
|
|
260
966
|
return true;
|
|
261
967
|
};
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
968
|
+
const invalidatePanelWork = () => {
|
|
969
|
+
panelRevision += 1;
|
|
970
|
+
if (!activeGeneration)
|
|
971
|
+
return;
|
|
972
|
+
const { controller, id } = activeGeneration;
|
|
973
|
+
controller.abort();
|
|
974
|
+
finishGeneration(id);
|
|
975
|
+
};
|
|
976
|
+
const regenerateTilesetAnimation = async (animationKey) => {
|
|
977
|
+
const assetId = selectedTargetAssetId;
|
|
978
|
+
const workflowPanelRevision = panelRevision;
|
|
979
|
+
const isWorkflowPanelCurrent = () => panelRevision === workflowPanelRevision &&
|
|
980
|
+
selectedTargetAssetId === assetId &&
|
|
981
|
+
selectedTilesetAnimationKey === animationKey;
|
|
982
|
+
const asset = manifest.assets[assetId];
|
|
983
|
+
const animation = tilesetAnimationForKey(asset, animationKey);
|
|
984
|
+
const activeVersion = asset?.versions[asset.activeVersion];
|
|
985
|
+
const definition = tilesetAnimationDefinitionFromInputs(assetId, animationKey);
|
|
986
|
+
if (!asset || !animation || !activeVersion?.file) {
|
|
987
|
+
setStatus(elements, `Tileset animation "${animationKey}" is not ready to generate.`, "error");
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
if (!definition) {
|
|
991
|
+
setStatus(elements, "Add an animation prompt for every tile before generating.", "error");
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const controller = new AbortController();
|
|
995
|
+
const currentGenerationId = generationId + 1;
|
|
996
|
+
generationId = currentGenerationId;
|
|
997
|
+
activeGeneration = { controller, id: currentGenerationId };
|
|
998
|
+
syncPromoteAllButton();
|
|
999
|
+
elements.regenerateButton.textContent = "Cancel generation";
|
|
1000
|
+
clearGeneratedOptions();
|
|
1001
|
+
elements.promoteButton.disabled = true;
|
|
1002
|
+
setStatus(elements, "Generating options...", "busy");
|
|
1003
|
+
lockGenerationStatus();
|
|
1004
|
+
try {
|
|
1005
|
+
const candidates = await client.generateTilesetAnimationStream({
|
|
1006
|
+
assetId,
|
|
1007
|
+
animationKey,
|
|
1008
|
+
frameCount: definition.frameCount,
|
|
1009
|
+
tiles: definition.tiles,
|
|
1010
|
+
count: 3
|
|
1011
|
+
}, (candidate) => {
|
|
1012
|
+
if (activeGeneration?.id !== currentGenerationId)
|
|
1013
|
+
return;
|
|
1014
|
+
if (candidate.frames.length !== definition.frameCount) {
|
|
1015
|
+
throw new Error(`Requested ${definition.frameCount} frames, but the generation server returned ` +
|
|
1016
|
+
`${candidate.frames.length} for option ${candidate.index + 1}. ` +
|
|
1017
|
+
"Restart the AI asset dev server and regenerate the options.");
|
|
1018
|
+
}
|
|
1019
|
+
const session = displayedTilesetAnimationOptions;
|
|
1020
|
+
const generated = [
|
|
1021
|
+
...(session?.assetId === assetId && session.animationKey === animationKey
|
|
1022
|
+
? session.generated
|
|
1023
|
+
: []),
|
|
1024
|
+
candidate
|
|
1025
|
+
].sort((left, right) => left.index - right.index);
|
|
1026
|
+
renderTilesetAnimationOptions(assetId, definition, generated);
|
|
1027
|
+
setStatus(elements, `Generated option ${candidate.index + 1}. Waiting for all 3 animation sequences...`, "busy");
|
|
1028
|
+
}, {
|
|
1029
|
+
signal: controller.signal
|
|
1030
|
+
});
|
|
1031
|
+
if (!finishGeneration(currentGenerationId))
|
|
1032
|
+
return;
|
|
1033
|
+
if (candidates.length !== 3) {
|
|
1034
|
+
throw new Error(`Expected 3 tileset animation candidates, received ${candidates.length}.`);
|
|
1035
|
+
}
|
|
1036
|
+
const mismatchedCandidate = candidates.find((candidate) => (candidate.frames.length !== definition.frameCount));
|
|
1037
|
+
if (mismatchedCandidate) {
|
|
1038
|
+
throw new Error(`Requested ${definition.frameCount} frames, but option ` +
|
|
1039
|
+
`${mismatchedCandidate.index + 1} contains ${mismatchedCandidate.frames.length}. ` +
|
|
1040
|
+
"Regenerate the options for the current frame count.");
|
|
1041
|
+
}
|
|
1042
|
+
if (!isWorkflowPanelCurrent())
|
|
1043
|
+
return;
|
|
1044
|
+
renderTilesetAnimationOptions(assetId, definition, candidates);
|
|
1045
|
+
setStatus(elements, "Three animation options ready. Preview one or mix tilesets.", "info");
|
|
1046
|
+
}
|
|
1047
|
+
catch (error) {
|
|
1048
|
+
finishGeneration(currentGenerationId);
|
|
1049
|
+
if (isAbortError(error)) {
|
|
1050
|
+
if (isWorkflowPanelCurrent()) {
|
|
1051
|
+
setStatus(elements, "Generation cancelled.", "info");
|
|
1052
|
+
}
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (isWorkflowPanelCurrent()) {
|
|
1056
|
+
if (displayedTilesetAnimationOptions?.assetId === assetId &&
|
|
1057
|
+
displayedTilesetAnimationOptions.animationKey === animationKey) {
|
|
1058
|
+
clearGeneratedOptions();
|
|
1059
|
+
}
|
|
1060
|
+
setStatus(elements, `Tileset animation generation failed. ${errorMessage(error)}`, "error");
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
finally {
|
|
1064
|
+
finishGeneration(currentGenerationId);
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
265
1067
|
elements.assetSelect.addEventListener("change", () => {
|
|
1068
|
+
invalidatePanelWork();
|
|
266
1069
|
selectedAssetId = elements.assetSelect.value;
|
|
267
1070
|
syncAsset(selectedAssetId);
|
|
268
1071
|
});
|
|
269
1072
|
elements.targetSelect.addEventListener("change", () => {
|
|
1073
|
+
invalidatePanelWork();
|
|
270
1074
|
selectedTargetId = elements.targetSelect.value || undefined;
|
|
271
1075
|
syncAsset(selectedAssetId);
|
|
272
1076
|
setStatus(elements, selectedTargetId
|
|
@@ -274,9 +1078,27 @@ export function installAiAssetDesigner(options) {
|
|
|
274
1078
|
: "Editing default target.", "info");
|
|
275
1079
|
});
|
|
276
1080
|
elements.animationSelect.addEventListener("change", () => {
|
|
1081
|
+
invalidatePanelWork();
|
|
1082
|
+
if (isDesignerTilesetAsset(manifest.assets[selectedTargetAssetId])) {
|
|
1083
|
+
const prefix = "tileset:animation:";
|
|
1084
|
+
selectedTilesetAnimationKey = elements.animationSelect.value.startsWith(prefix)
|
|
1085
|
+
? decodeURIComponent(elements.animationSelect.value.slice(prefix.length))
|
|
1086
|
+
: undefined;
|
|
1087
|
+
syncTargetAsset(selectedTargetAssetId);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
selectedTilesetAnimationKey = undefined;
|
|
277
1091
|
selectedTargetAssetId = elements.animationSelect.value;
|
|
278
1092
|
syncTargetAsset(selectedTargetAssetId);
|
|
279
1093
|
});
|
|
1094
|
+
const syncTilesetPromptCount = () => {
|
|
1095
|
+
const asset = manifest.assets[selectedTargetAssetId];
|
|
1096
|
+
if (isDesignerTilesetAsset(asset) && !selectedTilesetAnimationKey) {
|
|
1097
|
+
renderTilesetPromptInputs(selectedTargetAssetId);
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
elements.frameCountInput.addEventListener("change", syncTilesetPromptCount);
|
|
1101
|
+
elements.frameCountInput.addEventListener("blur", syncTilesetPromptCount);
|
|
280
1102
|
elements.formatSelect.addEventListener("change", () => {
|
|
281
1103
|
if (canEditGenerationFormat(manifest, selectedAssetId, selectedTargetAssetId)) {
|
|
282
1104
|
formatDrafts.set(selectedTargetAssetId, normalizeAssetFormat(elements.formatSelect.value));
|
|
@@ -292,18 +1114,41 @@ export function installAiAssetDesigner(options) {
|
|
|
292
1114
|
elements.currentPreview.addEventListener("click", () => {
|
|
293
1115
|
const asset = manifest.assets[selectedTargetAssetId];
|
|
294
1116
|
const activeVersion = asset.versions[asset.activeVersion];
|
|
295
|
-
if (!activeVersion?.file)
|
|
1117
|
+
if (!editedCurrentOption && !activeVersion?.file)
|
|
296
1118
|
return;
|
|
297
1119
|
for (const item of elements.options.querySelectorAll(".ai-game-assets-designer__option")) {
|
|
298
1120
|
item.classList.remove("is-selected");
|
|
299
1121
|
}
|
|
300
1122
|
elements.currentPreview.classList.add("is-selected");
|
|
301
|
-
if (previewedVersionName) {
|
|
302
|
-
editedCurrentOption = undefined;
|
|
303
|
-
previewedVersionName = undefined;
|
|
304
|
-
}
|
|
305
1123
|
selectedOption = editedCurrentOption;
|
|
306
1124
|
elements.promoteButton.disabled = !selectedOption;
|
|
1125
|
+
if (editedCurrentOption) {
|
|
1126
|
+
const optionAsset = assetWithGeneratedGeometry(asset, editedCurrentOption, {
|
|
1127
|
+
inheritAnimations: Boolean(previewedVersionName)
|
|
1128
|
+
});
|
|
1129
|
+
if (isAudioAsset(asset)) {
|
|
1130
|
+
renderAudioPlayer({
|
|
1131
|
+
container: elements.currentAudio,
|
|
1132
|
+
src: editedCurrentOption.dataUrl,
|
|
1133
|
+
label: readableAssetName(selectedTargetAssetId),
|
|
1134
|
+
playback: editedCurrentOption.audioPlayback
|
|
1135
|
+
});
|
|
1136
|
+
options.onPreview(selectedTargetAssetId, editedCurrentOption.dataUrl, optionAsset);
|
|
1137
|
+
}
|
|
1138
|
+
else {
|
|
1139
|
+
previewImageSource({
|
|
1140
|
+
scene: options.scene,
|
|
1141
|
+
manifest,
|
|
1142
|
+
assetId: selectedTargetAssetId,
|
|
1143
|
+
src: editedCurrentOption.dataUrl,
|
|
1144
|
+
textureKey: `ai-current-option:${selectedTargetAssetId}:${Date.now()}`,
|
|
1145
|
+
assetOverride: optionAsset,
|
|
1146
|
+
onPreview: options.onPreview
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
setStatus(elements, "Previewing the selected option. Promote to save it to code.", "info");
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
307
1152
|
if (isAudioAsset(asset)) {
|
|
308
1153
|
const activeVersionSource = resolveAssetUrl(activeVersion.file);
|
|
309
1154
|
renderAudioPlayer({
|
|
@@ -327,65 +1172,224 @@ export function installAiAssetDesigner(options) {
|
|
|
327
1172
|
}
|
|
328
1173
|
setStatus(elements, "Previewing active version.", "info");
|
|
329
1174
|
});
|
|
1175
|
+
elements.currentRevertButton.addEventListener("click", (event) => {
|
|
1176
|
+
event.stopPropagation();
|
|
1177
|
+
restoreActivePreview();
|
|
1178
|
+
});
|
|
330
1179
|
elements.currentPreview.addEventListener("keydown", (event) => {
|
|
1180
|
+
if (event.target !== elements.currentPreview)
|
|
1181
|
+
return;
|
|
331
1182
|
if (event.key !== "Enter" && event.key !== " ")
|
|
332
1183
|
return;
|
|
333
1184
|
event.preventDefault();
|
|
334
1185
|
elements.currentPreview.click();
|
|
335
1186
|
});
|
|
1187
|
+
elements.mixTilesetButton.addEventListener("click", async () => {
|
|
1188
|
+
const session = displayedOptions;
|
|
1189
|
+
const animationSession = displayedTilesetAnimationOptions;
|
|
1190
|
+
const assetId = selectedTargetAssetId;
|
|
1191
|
+
const asset = manifest.assets[assetId];
|
|
1192
|
+
const activeVersion = asset?.versions[asset.activeVersion];
|
|
1193
|
+
if (animationSession &&
|
|
1194
|
+
animationSession.assetId === assetId &&
|
|
1195
|
+
animationSession.animationKey === selectedTilesetAnimationKey &&
|
|
1196
|
+
animationSession.generated.length === 3 &&
|
|
1197
|
+
asset?.tileset &&
|
|
1198
|
+
activeVersion?.file &&
|
|
1199
|
+
!mixingTileset &&
|
|
1200
|
+
!activeGeneration &&
|
|
1201
|
+
activePromotionId === undefined) {
|
|
1202
|
+
const mixPanelRevision = panelRevision;
|
|
1203
|
+
const definition = animationSession.animation;
|
|
1204
|
+
const draftAsset = assetWithTilesetAnimationDefinition(asset, definition);
|
|
1205
|
+
const pending = pendingOptions.get(assetId);
|
|
1206
|
+
mixingTileset = true;
|
|
1207
|
+
syncMixTilesetButton();
|
|
1208
|
+
setStatus(elements, "Opening tileset animation mixer...", "busy");
|
|
1209
|
+
try {
|
|
1210
|
+
const baseSheetSrc = pending?.option.dataUrl ??
|
|
1211
|
+
await imageSourceToDataUrl(resolveAssetUrl(activeVersion.file));
|
|
1212
|
+
const currentFrames = pending?.tilesetAnimations?.[definition.key] ??
|
|
1213
|
+
await Promise.all((activeVersion.tilesetAnimations?.[definition.key]?.files ?? [])
|
|
1214
|
+
.map((file) => imageSourceToDataUrl(resolveAssetUrl(file))));
|
|
1215
|
+
if (panelRevision !== mixPanelRevision ||
|
|
1216
|
+
selectedTargetAssetId !== assetId ||
|
|
1217
|
+
selectedTilesetAnimationKey !== definition.key)
|
|
1218
|
+
return;
|
|
1219
|
+
const mixed = await openTilesetAnimationMixerDialog({
|
|
1220
|
+
root: elements.root,
|
|
1221
|
+
asset: draftAsset,
|
|
1222
|
+
assetId,
|
|
1223
|
+
animationKey: definition.key,
|
|
1224
|
+
baseSheetSrc,
|
|
1225
|
+
baseAnimationFrameSrcs: currentFrames,
|
|
1226
|
+
candidates: animationSession.generated,
|
|
1227
|
+
tilePrompts: definition.tiles,
|
|
1228
|
+
regenerateTile: async (tile, currentTileSrc) => {
|
|
1229
|
+
const prompt = definition.tiles?.[tile];
|
|
1230
|
+
if (!prompt)
|
|
1231
|
+
throw new Error(`Tile ${tile + 1} requires an animation prompt.`);
|
|
1232
|
+
return client.generateTilesetAnimationStream({
|
|
1233
|
+
assetId,
|
|
1234
|
+
animationKey: definition.key,
|
|
1235
|
+
count: 3,
|
|
1236
|
+
frameCount: definition.frameCount,
|
|
1237
|
+
tiles: [prompt],
|
|
1238
|
+
tileset: tilesetTileGenerationOverride(asset.tileset, tile),
|
|
1239
|
+
baseDataUrl: currentTileSrc,
|
|
1240
|
+
styleGuide: await styleGuideRequest(styleGuideDraft)
|
|
1241
|
+
}, () => undefined);
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
if (panelRevision !== mixPanelRevision ||
|
|
1245
|
+
selectedTargetAssetId !== assetId ||
|
|
1246
|
+
selectedTilesetAnimationKey !== definition.key)
|
|
1247
|
+
return;
|
|
1248
|
+
if (!mixed) {
|
|
1249
|
+
setStatus(elements, "Tileset animation mixing cancelled. The three options are still available.", "info");
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
await selectTilesetAnimationFrames(assetId, definition, mixed.frames);
|
|
1253
|
+
setStatus(elements, "Mixed tileset animation ready. Promote to save the selected tile combination.", "success");
|
|
1254
|
+
}
|
|
1255
|
+
catch (error) {
|
|
1256
|
+
setStatus(elements, `Could not mix tileset animation. ${errorMessage(error)}`, "error");
|
|
1257
|
+
}
|
|
1258
|
+
finally {
|
|
1259
|
+
mixingTileset = false;
|
|
1260
|
+
syncMixTilesetButton();
|
|
1261
|
+
}
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
if (mixingTileset ||
|
|
1265
|
+
activeGeneration ||
|
|
1266
|
+
activePromotionId !== undefined ||
|
|
1267
|
+
!session ||
|
|
1268
|
+
session.assetId !== assetId ||
|
|
1269
|
+
session.generated.length !== 3 ||
|
|
1270
|
+
!isDesignerTilesetAsset(asset) ||
|
|
1271
|
+
selectedTilesetAnimationKey ||
|
|
1272
|
+
!activeVersion?.file) {
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
const mixPanelRevision = panelRevision;
|
|
1276
|
+
const candidates = [...session.generated].sort((left, right) => left.index - right.index);
|
|
1277
|
+
mixingTileset = true;
|
|
1278
|
+
syncMixTilesetButton();
|
|
1279
|
+
setStatus(elements, "Opening tileset mixer...", "busy");
|
|
1280
|
+
try {
|
|
1281
|
+
const currentOption = editedCurrentOption;
|
|
1282
|
+
const activeSheetSrc = currentOption?.dataUrl ??
|
|
1283
|
+
await imageSourceToDataUrl(resolveAssetUrl(activeVersion.file));
|
|
1284
|
+
const current = resolveTilesetBaseMixCurrent(asset, activeSheetSrc, currentOption);
|
|
1285
|
+
if (panelRevision !== mixPanelRevision ||
|
|
1286
|
+
selectedTargetAssetId !== assetId ||
|
|
1287
|
+
selectedTilesetAnimationKey) {
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
const mixed = await openTilesetBaseMixerDialog({
|
|
1291
|
+
root: elements.root,
|
|
1292
|
+
asset: current.asset,
|
|
1293
|
+
assetId,
|
|
1294
|
+
baseSheetSrc: current.sheetSrc,
|
|
1295
|
+
candidates,
|
|
1296
|
+
regenerateTile: (tile, tileset, currentTileSrc) => regenerateTilesetTile(assetId, tile, tileset, currentTileSrc)
|
|
1297
|
+
});
|
|
1298
|
+
if (panelRevision !== mixPanelRevision ||
|
|
1299
|
+
selectedTargetAssetId !== assetId ||
|
|
1300
|
+
selectedTilesetAnimationKey) {
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
if (!mixed) {
|
|
1304
|
+
setStatus(elements, "Tileset mixing cancelled. The three options are still available.", "info");
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
const template = candidates[0];
|
|
1308
|
+
const mixedOption = createMixedTilesetOption(template, mixed, template.prompt || asset.prompt, Math.max(...candidates.map((option) => option.index)) + 1);
|
|
1309
|
+
selectedOption = mixedOption;
|
|
1310
|
+
rememberPendingOption(assetId, mixedOption);
|
|
1311
|
+
showOptionInCurrentPreview(mixedOption, "mixed tileset");
|
|
1312
|
+
elements.currentPreview.classList.add("is-selected");
|
|
1313
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
1314
|
+
previewOption({
|
|
1315
|
+
scene: options.scene,
|
|
1316
|
+
manifest,
|
|
1317
|
+
assetId,
|
|
1318
|
+
option: mixedOption,
|
|
1319
|
+
onPreview: options.onPreview
|
|
1320
|
+
});
|
|
1321
|
+
setStatus(elements, "Mixed tileset ready. Promote to save the selected tile combination.", "success");
|
|
1322
|
+
}
|
|
1323
|
+
catch (error) {
|
|
1324
|
+
setStatus(elements, `Could not mix tileset. ${errorMessage(error)}`, "error");
|
|
1325
|
+
}
|
|
1326
|
+
finally {
|
|
1327
|
+
mixingTileset = false;
|
|
1328
|
+
syncMixTilesetButton();
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
336
1331
|
elements.regenerateButton.addEventListener("click", async () => {
|
|
337
1332
|
if (activeGeneration) {
|
|
338
1333
|
activeGeneration.controller.abort();
|
|
339
1334
|
activeGeneration = undefined;
|
|
340
1335
|
elements.regenerateButton.textContent = "Regenerate";
|
|
341
1336
|
unlockGenerationStatus();
|
|
1337
|
+
elements.promoteButton.disabled = !selectedOption || activePromotionId !== undefined;
|
|
1338
|
+
syncPromoteAllButton();
|
|
342
1339
|
setStatus(elements, "Generation cancelled.", "info");
|
|
343
1340
|
return;
|
|
344
1341
|
}
|
|
1342
|
+
if (selectedTilesetAnimationKey) {
|
|
1343
|
+
await regenerateTilesetAnimation(selectedTilesetAnimationKey);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
345
1346
|
const controller = new AbortController();
|
|
346
1347
|
const currentGenerationId = generationId + 1;
|
|
347
1348
|
const generationAssetId = selectedTargetAssetId;
|
|
1349
|
+
const generationAsset = manifest.assets[generationAssetId];
|
|
348
1350
|
const generationFormat = effectiveGenerationFormat(manifest, formatDrafts, selectedAssetId, generationAssetId);
|
|
1351
|
+
const generationOverrides = generationOverridesFromInputs(elements, generationAsset, generationFormat);
|
|
1352
|
+
const isBaseTilesetGeneration = isDesignerTilesetAsset(generationAsset);
|
|
1353
|
+
if (isBaseTilesetGeneration && generationOverrides.tileset) {
|
|
1354
|
+
renderTilesetPromptInputs(generationAssetId);
|
|
1355
|
+
const tileCount = generationOverrides.tileset.tileCount ??
|
|
1356
|
+
effectiveTilesetTileCount(generationAsset);
|
|
1357
|
+
const tiles = tilesetPromptDefinitions(generationAssetId, tileCount);
|
|
1358
|
+
if (!tiles) {
|
|
1359
|
+
setStatus(elements, "Add a prompt for every tile before generating.", "error");
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
generationOverrides.tileset = {
|
|
1363
|
+
...generationOverrides.tileset,
|
|
1364
|
+
tiles
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
349
1367
|
generationId = currentGenerationId;
|
|
350
1368
|
activeGeneration = { controller, id: currentGenerationId };
|
|
1369
|
+
syncPromoteAllButton();
|
|
351
1370
|
setStatus(elements, "Generating options...", "busy");
|
|
352
1371
|
lockGenerationStatus();
|
|
353
1372
|
elements.promoteButton.disabled = true;
|
|
354
1373
|
elements.regenerateButton.textContent = "Cancel";
|
|
355
|
-
|
|
356
|
-
selectedOption =
|
|
357
|
-
editedCurrentOption = undefined;
|
|
358
|
-
previewedVersionName = undefined;
|
|
1374
|
+
clearGeneratedOptions();
|
|
1375
|
+
selectedOption = editedCurrentOption;
|
|
359
1376
|
const streamedOptions = [];
|
|
360
1377
|
try {
|
|
361
1378
|
const generationRequest = {
|
|
362
1379
|
assetId: generationAssetId,
|
|
363
|
-
prompt: elements.promptInput.value,
|
|
1380
|
+
prompt: isBaseTilesetGeneration ? undefined : elements.promptInput.value,
|
|
364
1381
|
count: options.optionCount ?? 3,
|
|
365
1382
|
format: generationFormat,
|
|
366
1383
|
audioSettings: audioGenerationOverridesFromInputs(elements, manifest.assets[generationAssetId]),
|
|
367
1384
|
voiceSettings: voiceGenerationOverridesFromInputs(elements, manifest.assets[generationAssetId]),
|
|
368
1385
|
styleGuide: await styleGuideRequest(styleGuideDraft),
|
|
369
|
-
...
|
|
1386
|
+
...generationOverrides
|
|
370
1387
|
};
|
|
371
1388
|
await client.generateStream(generationRequest, (option) => {
|
|
372
1389
|
if (activeGeneration?.id !== currentGenerationId)
|
|
373
1390
|
return;
|
|
374
1391
|
streamedOptions.push(option);
|
|
375
|
-
|
|
376
|
-
elements,
|
|
377
|
-
generated: [...streamedOptions].sort((left, right) => left.index - right.index),
|
|
378
|
-
scene: options.scene,
|
|
379
|
-
manifest,
|
|
380
|
-
assetId: generationAssetId,
|
|
381
|
-
designerOptions: options,
|
|
382
|
-
onPreview: options.onPreview,
|
|
383
|
-
onSelected(selected) {
|
|
384
|
-
selectedOption = selected;
|
|
385
|
-
previewedVersionName = undefined;
|
|
386
|
-
elements.promoteButton.disabled = false;
|
|
387
|
-
}
|
|
388
|
-
});
|
|
1392
|
+
renderGeneratedOptions(generationAssetId, [...streamedOptions].sort((left, right) => left.index - right.index));
|
|
389
1393
|
}, {
|
|
390
1394
|
signal: controller.signal
|
|
391
1395
|
});
|
|
@@ -393,7 +1397,11 @@ export function installAiAssetDesigner(options) {
|
|
|
393
1397
|
return;
|
|
394
1398
|
}
|
|
395
1399
|
finishGeneration(currentGenerationId);
|
|
396
|
-
setStatus(elements, streamedOptions.length
|
|
1400
|
+
setStatus(elements, streamedOptions.length === 3 && isBaseTilesetGeneration
|
|
1401
|
+
? "Three tileset options ready. Preview one or mix tiles."
|
|
1402
|
+
: streamedOptions.length > 0
|
|
1403
|
+
? "Pick an option to preview it."
|
|
1404
|
+
: "Generation finished with no options.", streamedOptions.length > 0 ? "info" : "error");
|
|
397
1405
|
}
|
|
398
1406
|
catch (error) {
|
|
399
1407
|
if (isAbortError(error)) {
|
|
@@ -401,7 +1409,7 @@ export function installAiAssetDesigner(options) {
|
|
|
401
1409
|
}
|
|
402
1410
|
finishGeneration(currentGenerationId);
|
|
403
1411
|
if (streamedOptions.length === 0) {
|
|
404
|
-
|
|
1412
|
+
clearGeneratedOptions();
|
|
405
1413
|
}
|
|
406
1414
|
setStatus(elements, `Generation failed. ${errorMessage(error)}`, "error");
|
|
407
1415
|
}
|
|
@@ -421,23 +1429,23 @@ export function installAiAssetDesigner(options) {
|
|
|
421
1429
|
elements,
|
|
422
1430
|
prompt: elements.promptInput.value
|
|
423
1431
|
});
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
designerOptions: options,
|
|
431
|
-
onPreview: options.onPreview,
|
|
432
|
-
onSelected(option) {
|
|
433
|
-
selectedOption = option;
|
|
434
|
-
previewedVersionName = undefined;
|
|
435
|
-
elements.promoteButton.disabled = false;
|
|
1432
|
+
if (isDesignerTilesetAsset(asset) && uploadedOption.tileset) {
|
|
1433
|
+
const tileCount = uploadedOption.tileset.tileCount ??
|
|
1434
|
+
uploadedOption.tileset.columns * uploadedOption.tileset.rows;
|
|
1435
|
+
const tiles = tilesetPromptDefinitions(selectedTargetAssetId, tileCount);
|
|
1436
|
+
if (!tiles) {
|
|
1437
|
+
throw new Error("Add a prompt for every tile before uploading a tileset.");
|
|
436
1438
|
}
|
|
437
|
-
|
|
1439
|
+
uploadedOption.tileset = {
|
|
1440
|
+
...uploadedOption.tileset,
|
|
1441
|
+
tiles
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
renderGeneratedOptions(selectedTargetAssetId, [uploadedOption]);
|
|
438
1445
|
selectedOption = uploadedOption;
|
|
439
|
-
|
|
440
|
-
|
|
1446
|
+
rememberPendingOption(selectedTargetAssetId, uploadedOption);
|
|
1447
|
+
showOptionInCurrentPreview(uploadedOption);
|
|
1448
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
441
1449
|
if (isAudioAsset(asset)) {
|
|
442
1450
|
options.onPreview(selectedTargetAssetId, uploadedOption.dataUrl, assetWithGeneratedGeometry(asset, uploadedOption));
|
|
443
1451
|
}
|
|
@@ -470,11 +1478,9 @@ export function installAiAssetDesigner(options) {
|
|
|
470
1478
|
prompt: elements.promptInput.value,
|
|
471
1479
|
format: generationFormat,
|
|
472
1480
|
onConfirm: async (deriveRequest) => {
|
|
473
|
-
|
|
1481
|
+
clearGeneratedOptions();
|
|
474
1482
|
elements.promoteButton.disabled = true;
|
|
475
1483
|
selectedOption = undefined;
|
|
476
|
-
editedCurrentOption = undefined;
|
|
477
|
-
previewedVersionName = undefined;
|
|
478
1484
|
if (deriveRequest.strategy === "scale" ||
|
|
479
1485
|
deriveRequest.strategy === "tile" ||
|
|
480
1486
|
deriveRequest.strategy === "crop") {
|
|
@@ -492,22 +1498,11 @@ export function installAiAssetDesigner(options) {
|
|
|
492
1498
|
mirrorX: deriveRequest.mirrorX,
|
|
493
1499
|
mirrorY: deriveRequest.mirrorY
|
|
494
1500
|
});
|
|
495
|
-
|
|
496
|
-
elements,
|
|
497
|
-
generated: [option],
|
|
498
|
-
scene: options.scene,
|
|
499
|
-
manifest,
|
|
500
|
-
assetId: derivationAssetId,
|
|
501
|
-
designerOptions: options,
|
|
502
|
-
onPreview: options.onPreview,
|
|
503
|
-
onSelected(selected) {
|
|
504
|
-
selectedOption = selected;
|
|
505
|
-
previewedVersionName = undefined;
|
|
506
|
-
elements.promoteButton.disabled = false;
|
|
507
|
-
}
|
|
508
|
-
});
|
|
1501
|
+
renderGeneratedOptions(derivationAssetId, [option]);
|
|
509
1502
|
selectedOption = option;
|
|
510
|
-
|
|
1503
|
+
rememberPendingOption(derivationAssetId, option);
|
|
1504
|
+
showOptionInCurrentPreview(option);
|
|
1505
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
511
1506
|
previewOption({
|
|
512
1507
|
scene: options.scene,
|
|
513
1508
|
manifest,
|
|
@@ -526,6 +1521,7 @@ export function installAiAssetDesigner(options) {
|
|
|
526
1521
|
: elements.promptInput.value;
|
|
527
1522
|
generationId = currentGenerationId;
|
|
528
1523
|
activeGeneration = { controller, id: currentGenerationId };
|
|
1524
|
+
syncPromoteAllButton();
|
|
529
1525
|
elements.regenerateButton.textContent = "Cancel";
|
|
530
1526
|
setStatus(elements, "Generating options...", "busy");
|
|
531
1527
|
lockGenerationStatus();
|
|
@@ -544,20 +1540,7 @@ export function installAiAssetDesigner(options) {
|
|
|
544
1540
|
if (activeGeneration?.id !== currentGenerationId)
|
|
545
1541
|
return;
|
|
546
1542
|
streamedOptions.push(option);
|
|
547
|
-
|
|
548
|
-
elements,
|
|
549
|
-
generated: [...streamedOptions].sort((left, right) => left.index - right.index),
|
|
550
|
-
scene: options.scene,
|
|
551
|
-
manifest,
|
|
552
|
-
assetId: derivationAssetId,
|
|
553
|
-
designerOptions: options,
|
|
554
|
-
onPreview: options.onPreview,
|
|
555
|
-
onSelected(selected) {
|
|
556
|
-
selectedOption = selected;
|
|
557
|
-
previewedVersionName = undefined;
|
|
558
|
-
elements.promoteButton.disabled = false;
|
|
559
|
-
}
|
|
560
|
-
});
|
|
1543
|
+
renderGeneratedOptions(derivationAssetId, [...streamedOptions].sort((left, right) => left.index - right.index));
|
|
561
1544
|
}, {
|
|
562
1545
|
signal: controller.signal
|
|
563
1546
|
});
|
|
@@ -577,49 +1560,348 @@ export function installAiAssetDesigner(options) {
|
|
|
577
1560
|
});
|
|
578
1561
|
});
|
|
579
1562
|
elements.promoteButton.addEventListener("click", async () => {
|
|
580
|
-
if (!selectedOption)
|
|
1563
|
+
if (!selectedOption || activePromotionId !== undefined)
|
|
581
1564
|
return;
|
|
1565
|
+
const currentPromotionId = promotionId + 1;
|
|
1566
|
+
promotionId = currentPromotionId;
|
|
1567
|
+
activePromotionId = currentPromotionId;
|
|
1568
|
+
syncPromoteAllButton();
|
|
1569
|
+
syncMixTilesetButton();
|
|
1570
|
+
const promotedAssetId = selectedTargetAssetId;
|
|
1571
|
+
const promotedOption = selectedOption;
|
|
1572
|
+
const promotedPending = pendingOptions.get(promotedAssetId);
|
|
1573
|
+
const promotionPanelRevision = panelRevision;
|
|
1574
|
+
const promotionSelectedAssetId = selectedAssetId;
|
|
1575
|
+
const promotionTargetId = selectedTargetId;
|
|
1576
|
+
const promotionAnimationKey = selectedTilesetAnimationKey;
|
|
1577
|
+
const inheritAnimations = Boolean(previewedVersionName);
|
|
1578
|
+
const isPromotionPanelCurrent = () => panelRevision === promotionPanelRevision &&
|
|
1579
|
+
selectedAssetId === promotionSelectedAssetId &&
|
|
1580
|
+
selectedTargetId === promotionTargetId &&
|
|
1581
|
+
selectedTargetAssetId === promotedAssetId &&
|
|
1582
|
+
selectedTilesetAnimationKey === promotionAnimationKey;
|
|
582
1583
|
const versionName = `promoted-${Date.now()}`;
|
|
583
1584
|
setStatus(elements, `Promoting ${versionName}...`, "busy");
|
|
584
1585
|
elements.promoteButton.disabled = true;
|
|
1586
|
+
elements.panel.setAttribute("aria-busy", "true");
|
|
1587
|
+
elements.panel.setAttribute("inert", "");
|
|
1588
|
+
let saved;
|
|
1589
|
+
let promotedAsset;
|
|
1590
|
+
let promotedVersionName;
|
|
1591
|
+
try {
|
|
1592
|
+
const animationFrames = promotionAnimationKey
|
|
1593
|
+
? promotedPending?.tilesetAnimations?.[promotionAnimationKey]
|
|
1594
|
+
: undefined;
|
|
1595
|
+
if (promotionAnimationKey &&
|
|
1596
|
+
animationFrames &&
|
|
1597
|
+
promotedPending?.animationOnlyKey === promotionAnimationKey) {
|
|
1598
|
+
const definition = promotedOption.tileset?.animations?.find((animation) => animation.key === promotionAnimationKey);
|
|
1599
|
+
const animationSaved = await client.saveTilesetAnimation({
|
|
1600
|
+
assetId: promotedAssetId,
|
|
1601
|
+
animationKey: promotionAnimationKey,
|
|
1602
|
+
frames: animationFrames,
|
|
1603
|
+
definition,
|
|
1604
|
+
versionName,
|
|
1605
|
+
notes: "Promoted from a selected or mixed tileset animation preview."
|
|
1606
|
+
});
|
|
1607
|
+
saved = animationSaved;
|
|
1608
|
+
manifest = animationSaved.manifest;
|
|
1609
|
+
promotedAsset = animationSaved.asset;
|
|
1610
|
+
promotedVersionName = animationSaved.versionName;
|
|
1611
|
+
}
|
|
1612
|
+
else {
|
|
1613
|
+
saved = await client.save({
|
|
1614
|
+
assetId: promotedAssetId,
|
|
1615
|
+
versionName,
|
|
1616
|
+
dataUrl: promotedOption.dataUrl,
|
|
1617
|
+
prompt: promotedOption.prompt,
|
|
1618
|
+
model: promotedOption.model,
|
|
1619
|
+
revisedPrompt: promotedOption.revisedPrompt,
|
|
1620
|
+
dimensions: promotedOption.dimensions,
|
|
1621
|
+
frameGrid: promotedOption.frameGrid,
|
|
1622
|
+
tileset: promotedOption.tileset,
|
|
1623
|
+
tilesetSourceDataUrl: promotedOption.tilesetSourceDataUrl,
|
|
1624
|
+
tilesetTransforms: promotedOption.tilesetTransforms,
|
|
1625
|
+
animations: assetWithGeneratedGeometry(manifest.assets[promotedAssetId], promotedOption, { inheritAnimations }).animations,
|
|
1626
|
+
settings: promotedOption.settings,
|
|
1627
|
+
audioSettings: promotedOption.audioSettings,
|
|
1628
|
+
audioPlayback: promotedOption.audioPlayback,
|
|
1629
|
+
voiceSettings: promotedOption.voiceSettings,
|
|
1630
|
+
durationSeconds: promotedOption.durationSeconds,
|
|
1631
|
+
activate: true,
|
|
1632
|
+
notes: "Promoted from the AI asset designer."
|
|
1633
|
+
});
|
|
1634
|
+
manifest = saved.manifest;
|
|
1635
|
+
promotedAsset = saved.asset;
|
|
1636
|
+
promotedVersionName = saved.versionName;
|
|
1637
|
+
for (const [animationKey, frames] of Object.entries(promotedPending?.tilesetAnimations ?? {})) {
|
|
1638
|
+
const animationSaved = await client.saveTilesetAnimation({
|
|
1639
|
+
assetId: promotedAssetId,
|
|
1640
|
+
animationKey,
|
|
1641
|
+
frames,
|
|
1642
|
+
definition: promotedOption.tileset?.animations?.find((animation) => animation.key === animationKey),
|
|
1643
|
+
notes: "Regenerated with the asset style during promotion."
|
|
1644
|
+
});
|
|
1645
|
+
manifest = animationSaved.manifest;
|
|
1646
|
+
promotedAsset = animationSaved.asset;
|
|
1647
|
+
promotedVersionName = animationSaved.versionName;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
for (const [animationKey, frames] of Object.entries(promotedPending?.tilesetAnimations ?? {})) {
|
|
1651
|
+
await installTilesetAnimationFrameTextures(frames, promotedAsset, (index) => aiTilesetAnimationTextureKey(promotedAssetId, animationKey, index));
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
catch (error) {
|
|
1655
|
+
if (activePromotionId === currentPromotionId) {
|
|
1656
|
+
activePromotionId = undefined;
|
|
1657
|
+
}
|
|
1658
|
+
syncPromoteAllButton();
|
|
1659
|
+
syncMixTilesetButton();
|
|
1660
|
+
elements.panel.removeAttribute("aria-busy");
|
|
1661
|
+
elements.panel.removeAttribute("inert");
|
|
1662
|
+
if (isPromotionPanelCurrent()) {
|
|
1663
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
1664
|
+
setStatus(elements, `Promotion failed. ${errorMessage(error)}`, "error");
|
|
1665
|
+
}
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
forgetPendingOption(promotedAssetId);
|
|
1669
|
+
if (isDesignerTilesetAsset(promotedAsset)) {
|
|
1670
|
+
tilesetPromptDrafts.set(promotedAssetId, promotedAsset.tileset?.tiles?.map((tile) => tile.prompt) ?? []);
|
|
1671
|
+
}
|
|
1672
|
+
let promotedTextureKey;
|
|
1673
|
+
let liveRefreshError;
|
|
1674
|
+
const captureLiveRefreshError = (error) => {
|
|
1675
|
+
liveRefreshError ??= error;
|
|
1676
|
+
};
|
|
1677
|
+
if (promotedAsset.activeVersion !== promotedVersionName) {
|
|
1678
|
+
captureLiveRefreshError(new Error(`Saved version "${promotedVersionName}" was not returned as the active version.`));
|
|
1679
|
+
}
|
|
1680
|
+
else if (!isAudioAsset(promotedAsset)) {
|
|
1681
|
+
try {
|
|
1682
|
+
promotedTextureKey = (await installPromotedImageTexture({
|
|
1683
|
+
scene: options.scene,
|
|
1684
|
+
manifest,
|
|
1685
|
+
assetId: promotedAssetId,
|
|
1686
|
+
src: promotedOption.dataUrl,
|
|
1687
|
+
assetOverride: promotedAsset
|
|
1688
|
+
})).textureKey;
|
|
1689
|
+
}
|
|
1690
|
+
catch (generatedSourceError) {
|
|
1691
|
+
try {
|
|
1692
|
+
promotedTextureKey = (await installPromotedImageTexture({
|
|
1693
|
+
scene: options.scene,
|
|
1694
|
+
manifest,
|
|
1695
|
+
assetId: promotedAssetId,
|
|
1696
|
+
src: resolveAssetUrl(saved.file),
|
|
1697
|
+
assetOverride: promotedAsset
|
|
1698
|
+
})).textureKey;
|
|
1699
|
+
}
|
|
1700
|
+
catch (savedSourceError) {
|
|
1701
|
+
captureLiveRefreshError(new Error(`Could not install the promoted texture. ${errorMessage(savedSourceError)}`, { cause: generatedSourceError }));
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
585
1705
|
try {
|
|
586
|
-
await client.save({
|
|
587
|
-
assetId: selectedTargetAssetId,
|
|
588
|
-
versionName,
|
|
589
|
-
dataUrl: selectedOption.dataUrl,
|
|
590
|
-
prompt: selectedOption.prompt,
|
|
591
|
-
model: selectedOption.model,
|
|
592
|
-
revisedPrompt: selectedOption.revisedPrompt,
|
|
593
|
-
dimensions: selectedOption.dimensions,
|
|
594
|
-
frameGrid: selectedOption.frameGrid,
|
|
595
|
-
animations: assetWithGeneratedGeometry(manifest.assets[selectedTargetAssetId], selectedOption).animations,
|
|
596
|
-
settings: selectedOption.settings,
|
|
597
|
-
audioSettings: selectedOption.audioSettings,
|
|
598
|
-
audioPlayback: selectedOption.audioPlayback,
|
|
599
|
-
voiceSettings: selectedOption.voiceSettings,
|
|
600
|
-
durationSeconds: selectedOption.durationSeconds,
|
|
601
|
-
activate: true,
|
|
602
|
-
notes: "Promoted from the AI asset designer."
|
|
603
|
-
});
|
|
604
|
-
manifest = await client.getManifest();
|
|
605
1706
|
options.onManifestUpdated?.(manifest);
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
1707
|
+
}
|
|
1708
|
+
catch (error) {
|
|
1709
|
+
captureLiveRefreshError(error);
|
|
1710
|
+
}
|
|
1711
|
+
try {
|
|
1712
|
+
if (promotedTextureKey) {
|
|
1713
|
+
(options.onAssetReady ?? options.onPreview)(promotedAssetId, promotedTextureKey, promotedAsset);
|
|
1714
|
+
}
|
|
1715
|
+
else if (isAudioAsset(promotedAsset)) {
|
|
1716
|
+
(options.onAssetReady ?? options.onPreview)(promotedAssetId, resolveAssetUrl(saved.file), promotedAsset);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
catch (error) {
|
|
1720
|
+
captureLiveRefreshError(error);
|
|
1721
|
+
}
|
|
1722
|
+
formatDrafts.delete(promotedAssetId);
|
|
1723
|
+
if (isPromotionPanelCurrent()) {
|
|
1724
|
+
try {
|
|
1725
|
+
syncTargetAsset(promotedAssetId, { preserveOptions: true });
|
|
1726
|
+
}
|
|
1727
|
+
catch (error) {
|
|
1728
|
+
captureLiveRefreshError(error);
|
|
1729
|
+
}
|
|
1730
|
+
if (liveRefreshError) {
|
|
1731
|
+
setStatus(elements, `Promoted ${promotedAssetId}, but the live game could not refresh. ` +
|
|
1732
|
+
`Restart to load it. ${errorMessage(liveRefreshError)}`, "error");
|
|
1733
|
+
}
|
|
1734
|
+
else {
|
|
1735
|
+
setStatus(elements, `Promoted ${promotedAssetId} to ${promotedVersionName}.`, "success");
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
if (activePromotionId === currentPromotionId) {
|
|
1739
|
+
activePromotionId = undefined;
|
|
1740
|
+
}
|
|
1741
|
+
syncPromoteAllButton();
|
|
1742
|
+
syncMixTilesetButton();
|
|
1743
|
+
elements.panel.removeAttribute("aria-busy");
|
|
1744
|
+
elements.panel.removeAttribute("inert");
|
|
1745
|
+
if (!isPromotionPanelCurrent() && selectedOption) {
|
|
1746
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
1747
|
+
}
|
|
1748
|
+
if (options.restartOnPromote) {
|
|
1749
|
+
window.location.reload();
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
elements.promoteAllButton.addEventListener("click", async () => {
|
|
1753
|
+
if (pendingOptions.size === 0 || activePromotionId !== undefined)
|
|
1754
|
+
return;
|
|
1755
|
+
const entries = [...pendingOptions.entries()];
|
|
1756
|
+
const currentPromotionId = promotionId + 1;
|
|
1757
|
+
promotionId = currentPromotionId;
|
|
1758
|
+
activePromotionId = currentPromotionId;
|
|
1759
|
+
elements.panel.setAttribute("aria-busy", "true");
|
|
1760
|
+
elements.panel.setAttribute("inert", "");
|
|
1761
|
+
elements.promoteButton.disabled = true;
|
|
1762
|
+
syncPromoteAllButton();
|
|
1763
|
+
syncMixTilesetButton();
|
|
1764
|
+
let promotedCount = 0;
|
|
1765
|
+
let promotionError;
|
|
1766
|
+
const liveRefreshErrors = [];
|
|
1767
|
+
for (const [index, [assetId, pending]] of entries.entries()) {
|
|
1768
|
+
const asset = manifest.assets[assetId];
|
|
1769
|
+
if (!asset) {
|
|
1770
|
+
promotionError = { assetId, error: new Error("Asset no longer exists in the manifest.") };
|
|
1771
|
+
break;
|
|
1772
|
+
}
|
|
1773
|
+
const versionName = `promoted-${Date.now()}-${index + 1}`;
|
|
1774
|
+
setStatus(elements, `Promoting ${readableAssetName(assetId)} (${index + 1}/${entries.length})...`, "busy");
|
|
1775
|
+
try {
|
|
1776
|
+
let saved;
|
|
1777
|
+
let promotedAsset;
|
|
1778
|
+
const animationOnlyFrames = pending.animationOnlyKey
|
|
1779
|
+
? pending.tilesetAnimations?.[pending.animationOnlyKey]
|
|
1780
|
+
: undefined;
|
|
1781
|
+
if (pending.animationOnlyKey && animationOnlyFrames) {
|
|
1782
|
+
saved = await client.saveTilesetAnimation({
|
|
1783
|
+
assetId,
|
|
1784
|
+
animationKey: pending.animationOnlyKey,
|
|
1785
|
+
frames: animationOnlyFrames,
|
|
1786
|
+
definition: pending.option.tileset?.animations?.find((animation) => animation.key === pending.animationOnlyKey),
|
|
1787
|
+
versionName,
|
|
1788
|
+
notes: "Promoted from a tileset animation preview with Promote all."
|
|
1789
|
+
});
|
|
1790
|
+
manifest = saved.manifest;
|
|
1791
|
+
promotedAsset = saved.asset;
|
|
1792
|
+
}
|
|
1793
|
+
else {
|
|
1794
|
+
saved = await client.save({
|
|
1795
|
+
assetId,
|
|
1796
|
+
versionName,
|
|
1797
|
+
dataUrl: pending.option.dataUrl,
|
|
1798
|
+
prompt: pending.option.prompt,
|
|
1799
|
+
model: pending.option.model,
|
|
1800
|
+
revisedPrompt: pending.option.revisedPrompt,
|
|
1801
|
+
dimensions: pending.option.dimensions,
|
|
1802
|
+
frameGrid: pending.option.frameGrid,
|
|
1803
|
+
tileset: pending.option.tileset,
|
|
1804
|
+
tilesetSourceDataUrl: pending.option.tilesetSourceDataUrl,
|
|
1805
|
+
tilesetTransforms: pending.option.tilesetTransforms,
|
|
1806
|
+
animations: assetWithGeneratedGeometry(asset, pending.option, {
|
|
1807
|
+
inheritAnimations: pending.inheritAnimations
|
|
1808
|
+
}).animations,
|
|
1809
|
+
settings: pending.option.settings,
|
|
1810
|
+
audioSettings: pending.option.audioSettings,
|
|
1811
|
+
audioPlayback: pending.option.audioPlayback,
|
|
1812
|
+
voiceSettings: pending.option.voiceSettings,
|
|
1813
|
+
durationSeconds: pending.option.durationSeconds,
|
|
1814
|
+
activate: true,
|
|
1815
|
+
notes: "Promoted from the AI asset designer with Promote all."
|
|
1816
|
+
});
|
|
1817
|
+
manifest = saved.manifest;
|
|
1818
|
+
promotedAsset = saved.asset;
|
|
1819
|
+
for (const [animationKey, frames] of Object.entries(pending.tilesetAnimations ?? {})) {
|
|
1820
|
+
const animationSaved = await client.saveTilesetAnimation({
|
|
1821
|
+
assetId,
|
|
1822
|
+
animationKey,
|
|
1823
|
+
frames,
|
|
1824
|
+
definition: pending.option.tileset?.animations?.find((animation) => animation.key === animationKey),
|
|
1825
|
+
notes: "Regenerated with the asset style during Promote all."
|
|
1826
|
+
});
|
|
1827
|
+
manifest = animationSaved.manifest;
|
|
1828
|
+
promotedAsset = animationSaved.asset;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
if (isDesignerTilesetAsset(promotedAsset)) {
|
|
1832
|
+
tilesetPromptDrafts.set(assetId, promotedAsset.tileset?.tiles?.map((tile) => tile.prompt) ?? []);
|
|
613
1833
|
}
|
|
1834
|
+
for (const [animationKey, frames] of Object.entries(pending.tilesetAnimations ?? {})) {
|
|
1835
|
+
await installTilesetAnimationFrameTextures(frames, promotedAsset, (frameIndex) => aiTilesetAnimationTextureKey(assetId, animationKey, frameIndex));
|
|
1836
|
+
}
|
|
1837
|
+
try {
|
|
1838
|
+
if (isAudioAsset(promotedAsset)) {
|
|
1839
|
+
(options.onAssetReady ?? options.onPreview)(assetId, resolveAssetUrl(saved.file), promotedAsset);
|
|
1840
|
+
}
|
|
1841
|
+
else {
|
|
1842
|
+
let installed;
|
|
1843
|
+
try {
|
|
1844
|
+
installed = await installPromotedImageTexture({
|
|
1845
|
+
scene: options.scene,
|
|
1846
|
+
manifest,
|
|
1847
|
+
assetId,
|
|
1848
|
+
src: pending.option.dataUrl,
|
|
1849
|
+
assetOverride: promotedAsset
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
catch {
|
|
1853
|
+
installed = await installPromotedImageTexture({
|
|
1854
|
+
scene: options.scene,
|
|
1855
|
+
manifest,
|
|
1856
|
+
assetId,
|
|
1857
|
+
src: resolveAssetUrl(saved.file),
|
|
1858
|
+
assetOverride: promotedAsset
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
(options.onAssetReady ?? options.onPreview)(assetId, installed.textureKey, promotedAsset);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
catch (error) {
|
|
1865
|
+
liveRefreshErrors.push({ assetId, error });
|
|
1866
|
+
}
|
|
1867
|
+
pendingOptions.delete(assetId);
|
|
1868
|
+
formatDrafts.delete(assetId);
|
|
1869
|
+
promotedCount += 1;
|
|
614
1870
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
1871
|
+
catch (error) {
|
|
1872
|
+
promotionError = { assetId, error };
|
|
1873
|
+
break;
|
|
618
1874
|
}
|
|
619
1875
|
}
|
|
1876
|
+
try {
|
|
1877
|
+
if (promotedCount > 0) {
|
|
1878
|
+
options.onManifestUpdated?.(manifest);
|
|
1879
|
+
}
|
|
1880
|
+
syncTargetAsset(selectedTargetAssetId);
|
|
1881
|
+
renderAssetBrowser();
|
|
1882
|
+
}
|
|
620
1883
|
catch (error) {
|
|
621
|
-
|
|
622
|
-
|
|
1884
|
+
liveRefreshErrors.push({ assetId: selectedTargetAssetId, error });
|
|
1885
|
+
}
|
|
1886
|
+
activePromotionId = undefined;
|
|
1887
|
+
elements.panel.removeAttribute("aria-busy");
|
|
1888
|
+
elements.panel.removeAttribute("inert");
|
|
1889
|
+
syncPromoteAllButton();
|
|
1890
|
+
syncMixTilesetButton();
|
|
1891
|
+
if (promotionError) {
|
|
1892
|
+
setStatus(elements, `Promoted ${promotedCount}/${entries.length} assets. ` +
|
|
1893
|
+
`${readableAssetName(promotionError.assetId)} failed: ${errorMessage(promotionError.error)}`, "error");
|
|
1894
|
+
}
|
|
1895
|
+
else if (liveRefreshErrors.length > 0) {
|
|
1896
|
+
const firstError = liveRefreshErrors[0];
|
|
1897
|
+
setStatus(elements, `Promoted all ${promotedCount} assets, but ${readableAssetName(firstError.assetId)} ` +
|
|
1898
|
+
`could not refresh live. Restart to load it. ${errorMessage(firstError.error)}`, "error");
|
|
1899
|
+
}
|
|
1900
|
+
else {
|
|
1901
|
+
setStatus(elements, `Promoted all ${promotedCount} pending assets.`, "success");
|
|
1902
|
+
}
|
|
1903
|
+
if (options.restartOnPromote && promotedCount > 0) {
|
|
1904
|
+
window.location.reload();
|
|
623
1905
|
}
|
|
624
1906
|
});
|
|
625
1907
|
elements.restartButton.addEventListener("click", () => {
|
|
@@ -634,30 +1916,45 @@ export function installAiAssetDesigner(options) {
|
|
|
634
1916
|
resolveAssetUrl,
|
|
635
1917
|
async onSelect(versionName, option) {
|
|
636
1918
|
const optionAsset = assetWithGeneratedGeometry(asset, option, { inheritAnimations: true });
|
|
1919
|
+
const version = asset.versions[versionName];
|
|
1920
|
+
const tilesetSourceDataUrl = version?.tilesetSourceFile
|
|
1921
|
+
? await imageSourceToDataUrl(resolveAssetUrl(version.tilesetSourceFile))
|
|
1922
|
+
: undefined;
|
|
1923
|
+
const versionOption = {
|
|
1924
|
+
...option,
|
|
1925
|
+
tilesetSourceDataUrl,
|
|
1926
|
+
tilesetTransforms: tilesetSourceDataUrl
|
|
1927
|
+
? cloneTilesetTransforms(version?.tilesetTransforms)
|
|
1928
|
+
: undefined
|
|
1929
|
+
};
|
|
637
1930
|
for (const item of elements.options.querySelectorAll(".ai-game-assets-designer__option")) {
|
|
638
1931
|
item.classList.remove("is-selected");
|
|
639
1932
|
}
|
|
640
|
-
selectedOption =
|
|
641
|
-
|
|
1933
|
+
selectedOption = versionOption;
|
|
1934
|
+
rememberPendingOption(selectedTargetAssetId, versionOption, {
|
|
1935
|
+
inheritAnimations: true,
|
|
1936
|
+
previewedVersionName: versionName
|
|
1937
|
+
});
|
|
1938
|
+
showOptionInCurrentPreview(versionOption, versionName);
|
|
642
1939
|
previewedVersionName = versionName;
|
|
643
1940
|
elements.currentPreview.classList.add("is-selected");
|
|
644
|
-
elements.promoteButton.disabled =
|
|
1941
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
645
1942
|
if (isAudioAsset(asset)) {
|
|
646
1943
|
renderAudioPlayer({
|
|
647
1944
|
container: elements.currentAudio,
|
|
648
|
-
src:
|
|
1945
|
+
src: versionOption.dataUrl,
|
|
649
1946
|
label: readableAssetName(selectedTargetAssetId),
|
|
650
|
-
playback:
|
|
1947
|
+
playback: versionOption.audioPlayback
|
|
651
1948
|
});
|
|
652
|
-
options.onPreview(selectedTargetAssetId,
|
|
1949
|
+
options.onPreview(selectedTargetAssetId, versionOption.dataUrl, optionAsset);
|
|
653
1950
|
}
|
|
654
1951
|
else {
|
|
655
|
-
elements.currentImage.src =
|
|
1952
|
+
elements.currentImage.src = versionOption.dataUrl;
|
|
656
1953
|
previewImageSource({
|
|
657
1954
|
scene: options.scene,
|
|
658
1955
|
manifest,
|
|
659
1956
|
assetId: selectedTargetAssetId,
|
|
660
|
-
src:
|
|
1957
|
+
src: versionOption.dataUrl,
|
|
661
1958
|
textureKey: `ai-version-preview:${selectedTargetAssetId}:${versionName}:${Date.now()}`,
|
|
662
1959
|
assetOverride: optionAsset,
|
|
663
1960
|
onPreview: options.onPreview
|
|
@@ -673,6 +1970,7 @@ export function installAiAssetDesigner(options) {
|
|
|
673
1970
|
});
|
|
674
1971
|
options.onManifestUpdated?.(manifest);
|
|
675
1972
|
if (previewedVersionName === versionName) {
|
|
1973
|
+
forgetPendingOption(selectedTargetAssetId);
|
|
676
1974
|
syncTargetAsset(selectedTargetAssetId);
|
|
677
1975
|
}
|
|
678
1976
|
else {
|
|
@@ -688,6 +1986,113 @@ export function installAiAssetDesigner(options) {
|
|
|
688
1986
|
}
|
|
689
1987
|
});
|
|
690
1988
|
});
|
|
1989
|
+
const previewBulkOption = (assetId, option) => new Promise((resolve, reject) => {
|
|
1990
|
+
const previewAsset = assetWithGeneratedGeometry(manifest.assets[assetId], option);
|
|
1991
|
+
previewImageSource({
|
|
1992
|
+
scene: options.scene,
|
|
1993
|
+
manifest,
|
|
1994
|
+
assetId,
|
|
1995
|
+
src: option.dataUrl,
|
|
1996
|
+
textureKey: `ai-bulk-preview:${assetId}:${Date.now()}`,
|
|
1997
|
+
assetOverride: previewAsset,
|
|
1998
|
+
onPreview(previewAssetId, textureKey, asset) {
|
|
1999
|
+
options.onPreview(previewAssetId, textureKey, asset);
|
|
2000
|
+
resolve(asset);
|
|
2001
|
+
},
|
|
2002
|
+
onError: reject
|
|
2003
|
+
});
|
|
2004
|
+
});
|
|
2005
|
+
const regenerateAllGraphicalAssets = async (styleGuide) => {
|
|
2006
|
+
if (activeGeneration || activePromotionId !== undefined)
|
|
2007
|
+
return;
|
|
2008
|
+
const assetIds = expandAiAssetIds(manifest, visibleAssetIds(), {
|
|
2009
|
+
includeLinkedAnimations: true,
|
|
2010
|
+
targetId: selectedTargetId
|
|
2011
|
+
}).filter((assetId) => {
|
|
2012
|
+
const asset = manifest.assets[assetId];
|
|
2013
|
+
return Boolean(asset && asset.kind !== "collection" && !isAudioAsset(asset));
|
|
2014
|
+
});
|
|
2015
|
+
if (assetIds.length === 0) {
|
|
2016
|
+
setStatus(elements, "There are no graphical assets to regenerate.", "info");
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
const controller = new AbortController();
|
|
2020
|
+
const currentGenerationId = generationId + 1;
|
|
2021
|
+
generationId = currentGenerationId;
|
|
2022
|
+
activeGeneration = { controller, id: currentGenerationId };
|
|
2023
|
+
clearGeneratedOptions();
|
|
2024
|
+
syncPromoteAllButton();
|
|
2025
|
+
const failures = [];
|
|
2026
|
+
let generatedCount = 0;
|
|
2027
|
+
for (const [index, assetId] of assetIds.entries()) {
|
|
2028
|
+
if (activeGeneration?.id !== currentGenerationId)
|
|
2029
|
+
return;
|
|
2030
|
+
setStatus(elements, `Regenerating ${readableAssetName(assetId)} (${index + 1}/${assetIds.length})...`, "busy");
|
|
2031
|
+
try {
|
|
2032
|
+
const generated = await client.generate({
|
|
2033
|
+
assetId,
|
|
2034
|
+
count: 1,
|
|
2035
|
+
styleGuide
|
|
2036
|
+
}, {
|
|
2037
|
+
signal: controller.signal
|
|
2038
|
+
});
|
|
2039
|
+
const option = generated[0];
|
|
2040
|
+
if (!option) {
|
|
2041
|
+
throw new Error("Generation finished without an option.");
|
|
2042
|
+
}
|
|
2043
|
+
rememberPendingOption(assetId, option);
|
|
2044
|
+
const previewAsset = await previewBulkOption(assetId, option);
|
|
2045
|
+
const generatedTilesetAnimations = {};
|
|
2046
|
+
for (const animation of previewAsset.tileset?.animations ?? []) {
|
|
2047
|
+
setStatus(elements, `Regenerating ${readableAssetName(assetId)} · ` +
|
|
2048
|
+
`${readableAssetName(animation.key)} (${index + 1}/${assetIds.length})...`, "busy");
|
|
2049
|
+
const candidates = await client.generateTilesetAnimationStream({
|
|
2050
|
+
assetId,
|
|
2051
|
+
animationKey: animation.key,
|
|
2052
|
+
prompt: animation.prompt,
|
|
2053
|
+
count: 1,
|
|
2054
|
+
baseDataUrl: option.dataUrl,
|
|
2055
|
+
styleGuide
|
|
2056
|
+
}, () => { }, {
|
|
2057
|
+
signal: controller.signal
|
|
2058
|
+
});
|
|
2059
|
+
const candidate = candidates[0];
|
|
2060
|
+
if (!candidate || candidate.frames.length !== animation.frameCount) {
|
|
2061
|
+
throw new Error(`${readableAssetName(animation.key)} did not return its ` +
|
|
2062
|
+
`${animation.frameCount} required frames.`);
|
|
2063
|
+
}
|
|
2064
|
+
const frameDataUrls = candidate.frames.map((frame) => frame.dataUrl);
|
|
2065
|
+
generatedTilesetAnimations[animation.key] = frameDataUrls;
|
|
2066
|
+
rememberPendingOption(assetId, option, {
|
|
2067
|
+
tilesetAnimations: { ...generatedTilesetAnimations }
|
|
2068
|
+
});
|
|
2069
|
+
await previewTilesetAnimationFrames(assetId, animation.key, frameDataUrls, previewAsset);
|
|
2070
|
+
}
|
|
2071
|
+
generatedCount += 1;
|
|
2072
|
+
if (selectedTargetAssetId === assetId) {
|
|
2073
|
+
selectedOption = option;
|
|
2074
|
+
showOptionInCurrentPreview(option, "regenerated preview");
|
|
2075
|
+
elements.currentPreview.classList.add("is-selected");
|
|
2076
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
catch (error) {
|
|
2080
|
+
if (isAbortError(error))
|
|
2081
|
+
return;
|
|
2082
|
+
failures.push({ assetId, error });
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
if (!finishGeneration(currentGenerationId))
|
|
2086
|
+
return;
|
|
2087
|
+
if (failures.length > 0) {
|
|
2088
|
+
const firstFailure = failures[0];
|
|
2089
|
+
setStatus(elements, `Regenerated ${generatedCount}/${assetIds.length} graphical assets. ` +
|
|
2090
|
+
`${readableAssetName(firstFailure.assetId)} failed: ${errorMessage(firstFailure.error)}`, "error");
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
setStatus(elements, `Regenerated and previewed ${generatedCount} graphical assets. ` +
|
|
2094
|
+
"Use Promote all to save them.", "success");
|
|
2095
|
+
};
|
|
691
2096
|
elements.styleButton.addEventListener("click", () => {
|
|
692
2097
|
void openStyleGuideEditor({
|
|
693
2098
|
root: elements.root,
|
|
@@ -710,53 +2115,97 @@ export function installAiAssetDesigner(options) {
|
|
|
710
2115
|
setStatus(elements, `Style promotion failed. ${errorMessage(error)}`, "error");
|
|
711
2116
|
throw error;
|
|
712
2117
|
}
|
|
2118
|
+
},
|
|
2119
|
+
async onRegenerateAll(draft) {
|
|
2120
|
+
try {
|
|
2121
|
+
await regenerateAllGraphicalAssets(await styleGuideRequest(draft));
|
|
2122
|
+
}
|
|
2123
|
+
catch (error) {
|
|
2124
|
+
setStatus(elements, `Bulk regeneration failed. ${errorMessage(error)}`, "error");
|
|
2125
|
+
}
|
|
713
2126
|
}
|
|
714
2127
|
});
|
|
715
2128
|
});
|
|
716
|
-
elements.currentAnimationButton.addEventListener("click", (event) => {
|
|
2129
|
+
elements.currentAnimationButton.addEventListener("click", async (event) => {
|
|
717
2130
|
event.stopPropagation();
|
|
718
2131
|
const asset = manifest.assets[selectedTargetAssetId];
|
|
719
2132
|
const activeVersion = asset.versions[asset.activeVersion];
|
|
720
|
-
|
|
2133
|
+
const sourceOption = editedCurrentOption;
|
|
2134
|
+
if (!sourceOption && !activeVersion?.file)
|
|
2135
|
+
return;
|
|
2136
|
+
if (selectedTilesetAnimationKey && isDesignerTilesetAsset(asset)) {
|
|
2137
|
+
const assetId = selectedTargetAssetId;
|
|
2138
|
+
const animationKey = selectedTilesetAnimationKey;
|
|
2139
|
+
const definition = promptOnlyTilesetAnimationDefinition(assetId, animationKey);
|
|
2140
|
+
if (!definition) {
|
|
2141
|
+
setStatus(elements, "Add an animation prompt for every tile before editing the animation.", "error");
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
try {
|
|
2145
|
+
const pendingFrames = pendingOptions.get(assetId)?.tilesetAnimations?.[animationKey];
|
|
2146
|
+
const frames = pendingFrames ?? await activeTilesetAnimationFrames(assetId, animationKey);
|
|
2147
|
+
if (frames.length !== definition.frameCount) {
|
|
2148
|
+
throw new Error(`${readableAssetName(animationKey)} has ${frames.length} current frames, but ` +
|
|
2149
|
+
`${definition.frameCount} are configured. Generate new frames before editing.`);
|
|
2150
|
+
}
|
|
2151
|
+
const editorAsset = assetWithTilesetAnimationDefinition(asset, definition);
|
|
2152
|
+
await openTilesetAnimationEditor({
|
|
2153
|
+
root: elements.root,
|
|
2154
|
+
asset: editorAsset,
|
|
2155
|
+
assetId,
|
|
2156
|
+
animationKey,
|
|
2157
|
+
frames,
|
|
2158
|
+
onConfirm: async ({ frames: editedFrames, animation }) => {
|
|
2159
|
+
const currentPrompts = tilesetAnimationPromptDefinitions(assetId, animationKey);
|
|
2160
|
+
const editedDefinition = currentPrompts
|
|
2161
|
+
? { ...animation, tiles: currentPrompts }
|
|
2162
|
+
: animation;
|
|
2163
|
+
await selectTilesetAnimationFrames(assetId, editedDefinition, editedFrames);
|
|
2164
|
+
elements.currentRevertButton.hidden = false;
|
|
2165
|
+
setStatus(elements, "Tileset animation edits applied. Promote to save them to code.", "success");
|
|
2166
|
+
}
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
catch (error) {
|
|
2170
|
+
setStatus(elements, `Could not edit tileset animation. ${errorMessage(error)}`, "error");
|
|
2171
|
+
}
|
|
721
2172
|
return;
|
|
2173
|
+
}
|
|
722
2174
|
if (isAudioAsset(asset)) {
|
|
723
2175
|
void openAudioEditor({
|
|
724
2176
|
root: elements.root,
|
|
725
2177
|
asset,
|
|
726
2178
|
assetId: selectedTargetAssetId,
|
|
727
|
-
src:
|
|
728
|
-
initialPlayback:
|
|
2179
|
+
src: sourceOption?.dataUrl ?? resolveAssetUrl(activeVersion.file),
|
|
2180
|
+
initialPlayback: sourceOption?.audioPlayback ?? activeVersion?.audioPlayback,
|
|
729
2181
|
onConfirm: async (audioPlayback) => {
|
|
730
|
-
const
|
|
2182
|
+
const previousOption = editedCurrentOption;
|
|
2183
|
+
const dataUrl = previousOption?.dataUrl ??
|
|
731
2184
|
await imageSourceToDataUrl(resolveAssetUrl(activeVersion.file));
|
|
732
2185
|
const optionAsset = {
|
|
733
2186
|
...asset,
|
|
734
2187
|
audioPlayback
|
|
735
2188
|
};
|
|
736
2189
|
editedCurrentOption = {
|
|
737
|
-
|
|
2190
|
+
...previousOption,
|
|
2191
|
+
index: previousOption?.index ?? -1,
|
|
738
2192
|
dataUrl,
|
|
739
2193
|
mimeType: mimeTypeFromDataUrl(dataUrl),
|
|
740
|
-
prompt: activeVersion
|
|
741
|
-
model: activeVersion
|
|
742
|
-
revisedPrompt: activeVersion
|
|
743
|
-
audioSettings: activeVersion
|
|
2194
|
+
prompt: previousOption?.prompt ?? activeVersion?.prompt ?? asset.prompt,
|
|
2195
|
+
model: previousOption?.model ?? activeVersion?.model,
|
|
2196
|
+
revisedPrompt: previousOption?.revisedPrompt ?? activeVersion?.revisedPrompt,
|
|
2197
|
+
audioSettings: previousOption?.audioSettings ?? activeVersion?.audioSettings ?? asset.audioSettings,
|
|
744
2198
|
audioPlayback,
|
|
745
|
-
voiceSettings: activeVersion
|
|
746
|
-
durationSeconds: activeVersion
|
|
2199
|
+
voiceSettings: previousOption?.voiceSettings ?? activeVersion?.voiceSettings ?? asset.voiceSettings,
|
|
2200
|
+
durationSeconds: previousOption?.durationSeconds ?? activeVersion?.durationSeconds
|
|
747
2201
|
};
|
|
748
2202
|
selectedOption = editedCurrentOption;
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
...activeVersion,
|
|
756
|
-
audioPlayback
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
};
|
|
2203
|
+
rememberPendingOption(selectedTargetAssetId, editedCurrentOption, {
|
|
2204
|
+
inheritAnimations: Boolean(previewedVersionName),
|
|
2205
|
+
previewedVersionName
|
|
2206
|
+
});
|
|
2207
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
2208
|
+
elements.currentRevertButton.hidden = false;
|
|
760
2209
|
options.onPreview(selectedTargetAssetId, dataUrl, optionAsset);
|
|
761
2210
|
renderAudioPlayer({
|
|
762
2211
|
container: elements.currentAudio,
|
|
@@ -769,36 +2218,49 @@ export function installAiAssetDesigner(options) {
|
|
|
769
2218
|
});
|
|
770
2219
|
return;
|
|
771
2220
|
}
|
|
772
|
-
|
|
2221
|
+
const editorAsset = sourceOption
|
|
2222
|
+
? assetWithGeneratedGeometry(asset, sourceOption, {
|
|
2223
|
+
inheritAnimations: Boolean(previewedVersionName)
|
|
2224
|
+
})
|
|
2225
|
+
: asset;
|
|
2226
|
+
if (!editorAsset.frameGrid)
|
|
773
2227
|
return;
|
|
774
2228
|
void openAnimationEditor({
|
|
775
2229
|
root: elements.root,
|
|
776
|
-
asset,
|
|
2230
|
+
asset: editorAsset,
|
|
777
2231
|
assetId: selectedTargetAssetId,
|
|
778
|
-
src:
|
|
779
|
-
displaySize: resolvePreviewDisplaySize(options, selectedTargetAssetId,
|
|
780
|
-
initialAnimations:
|
|
2232
|
+
src: sourceOption?.dataUrl ?? resolveAssetUrl(activeVersion.file),
|
|
2233
|
+
displaySize: resolvePreviewDisplaySize(options, selectedTargetAssetId, editorAsset),
|
|
2234
|
+
initialAnimations: sourceOption?.animations ?? editorAsset.animations,
|
|
781
2235
|
onConfirm: async ({ animations, dataUrl: editedDataUrl }) => {
|
|
2236
|
+
const previousOption = editedCurrentOption;
|
|
782
2237
|
const dataUrl = editedDataUrl ??
|
|
783
|
-
|
|
2238
|
+
previousOption?.dataUrl ??
|
|
784
2239
|
await imageSourceToDataUrl(resolveAssetUrl(activeVersion.file));
|
|
785
2240
|
const optionAsset = {
|
|
786
|
-
...
|
|
2241
|
+
...editorAsset,
|
|
787
2242
|
animations
|
|
788
2243
|
};
|
|
789
2244
|
editedCurrentOption = {
|
|
790
|
-
|
|
2245
|
+
...previousOption,
|
|
2246
|
+
index: previousOption?.index ?? -1,
|
|
791
2247
|
dataUrl,
|
|
792
2248
|
mimeType: mimeTypeFromDataUrl(dataUrl),
|
|
793
|
-
prompt: activeVersion
|
|
794
|
-
model: activeVersion
|
|
795
|
-
revisedPrompt: activeVersion
|
|
796
|
-
dimensions:
|
|
797
|
-
frameGrid:
|
|
2249
|
+
prompt: previousOption?.prompt ?? activeVersion?.prompt ?? asset.prompt,
|
|
2250
|
+
model: previousOption?.model ?? activeVersion?.model,
|
|
2251
|
+
revisedPrompt: previousOption?.revisedPrompt ?? activeVersion?.revisedPrompt,
|
|
2252
|
+
dimensions: previousOption?.dimensions ?? editorAsset.dimensions,
|
|
2253
|
+
frameGrid: previousOption?.frameGrid ?? editorAsset.frameGrid,
|
|
2254
|
+
settings: previousOption?.settings ?? activeVersion?.settings ?? asset.settings,
|
|
798
2255
|
animations
|
|
799
2256
|
};
|
|
800
2257
|
selectedOption = editedCurrentOption;
|
|
801
|
-
|
|
2258
|
+
rememberPendingOption(selectedTargetAssetId, editedCurrentOption, {
|
|
2259
|
+
inheritAnimations: Boolean(previewedVersionName),
|
|
2260
|
+
previewedVersionName
|
|
2261
|
+
});
|
|
2262
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
2263
|
+
elements.currentRevertButton.hidden = false;
|
|
802
2264
|
previewImageSource({
|
|
803
2265
|
scene: options.scene,
|
|
804
2266
|
manifest,
|
|
@@ -816,37 +2278,124 @@ export function installAiAssetDesigner(options) {
|
|
|
816
2278
|
event.stopPropagation();
|
|
817
2279
|
const asset = manifest.assets[selectedTargetAssetId];
|
|
818
2280
|
const activeVersion = asset.versions[asset.activeVersion];
|
|
819
|
-
|
|
2281
|
+
const sourceOption = editedCurrentOption;
|
|
2282
|
+
const editorAsset = sourceOption
|
|
2283
|
+
? assetWithGeneratedGeometry(asset, sourceOption)
|
|
2284
|
+
: asset;
|
|
2285
|
+
if ((!sourceOption && !activeVersion?.file) ||
|
|
820
2286
|
isAudioAsset(asset) ||
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
2287
|
+
editorAsset.frameGrid ||
|
|
2288
|
+
sourceOption?.mimeType === "image/svg+xml" ||
|
|
2289
|
+
(!sourceOption && (normalizeAssetFormat(asset.settings?.format) === "svg" ||
|
|
2290
|
+
isSvgSource(activeVersion.file)))) {
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
if (isDesignerTilesetAsset(editorAsset)) {
|
|
2294
|
+
const assetId = selectedTargetAssetId;
|
|
2295
|
+
const editorTileset = tilesetMetadataForAsset(editorAsset);
|
|
2296
|
+
const draftTiles = tilesetPromptDefinitions(assetId, editorTileset.tileCount ?? editorTileset.columns * editorTileset.rows);
|
|
2297
|
+
const tilesetEditorAsset = draftTiles ? {
|
|
2298
|
+
...editorAsset,
|
|
2299
|
+
tileset: {
|
|
2300
|
+
...editorTileset,
|
|
2301
|
+
tiles: draftTiles
|
|
2302
|
+
}
|
|
2303
|
+
} : editorAsset;
|
|
2304
|
+
const renderedSrc = sourceOption?.dataUrl ?? resolveAssetUrl(activeVersion.file);
|
|
2305
|
+
const editorInitialState = resolveTilesetEditorInitialState({
|
|
2306
|
+
renderedSrc,
|
|
2307
|
+
currentOption: sourceOption,
|
|
2308
|
+
activeVersion,
|
|
2309
|
+
resolveAssetUrl
|
|
2310
|
+
});
|
|
2311
|
+
void openTilesetEditor({
|
|
2312
|
+
root: elements.root,
|
|
2313
|
+
asset: tilesetEditorAsset,
|
|
2314
|
+
assetId,
|
|
2315
|
+
src: renderedSrc,
|
|
2316
|
+
sourceSrc: editorInitialState.sourceSrc,
|
|
2317
|
+
initialTransforms: editorInitialState.initialTransforms,
|
|
2318
|
+
regenerateTile: (tile, tileset, currentTileSrc) => regenerateTilesetTile(assetId, tile, tileset, currentTileSrc),
|
|
2319
|
+
onConfirm: async ({ dataUrl, sourceDataUrl, transforms }) => {
|
|
2320
|
+
const previousOption = editedCurrentOption;
|
|
2321
|
+
const optionAsset = {
|
|
2322
|
+
...tilesetEditorAsset,
|
|
2323
|
+
dimensions: previousOption?.dimensions ?? tilesetEditorAsset.dimensions,
|
|
2324
|
+
tileset: tilesetEditorAsset.tileset,
|
|
2325
|
+
settings: {
|
|
2326
|
+
...(previousOption?.settings ?? activeVersion?.settings ?? tilesetEditorAsset.settings),
|
|
2327
|
+
format: "png"
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2330
|
+
editedCurrentOption = {
|
|
2331
|
+
...previousOption,
|
|
2332
|
+
index: previousOption?.index ?? -1,
|
|
2333
|
+
dataUrl,
|
|
2334
|
+
mimeType: "image/png",
|
|
2335
|
+
prompt: previousOption?.prompt ?? activeVersion?.prompt ?? asset.prompt,
|
|
2336
|
+
model: previousOption?.model ?? activeVersion?.model,
|
|
2337
|
+
revisedPrompt: previousOption?.revisedPrompt ?? activeVersion?.revisedPrompt,
|
|
2338
|
+
dimensions: optionAsset.dimensions,
|
|
2339
|
+
tileset: optionAsset.tileset,
|
|
2340
|
+
tilesetSourceDataUrl: sourceDataUrl,
|
|
2341
|
+
tilesetTransforms: cloneTilesetTransforms(transforms),
|
|
2342
|
+
settings: optionAsset.settings
|
|
2343
|
+
};
|
|
2344
|
+
selectedOption = editedCurrentOption;
|
|
2345
|
+
rememberPendingOption(assetId, editedCurrentOption, {
|
|
2346
|
+
inheritAnimations: Boolean(previewedVersionName),
|
|
2347
|
+
previewedVersionName
|
|
2348
|
+
});
|
|
2349
|
+
elements.currentImage.src = dataUrl;
|
|
2350
|
+
elements.currentPreview.classList.add("is-selected");
|
|
2351
|
+
elements.currentRevertButton.hidden = false;
|
|
2352
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
2353
|
+
previewImageSource({
|
|
2354
|
+
scene: options.scene,
|
|
2355
|
+
manifest,
|
|
2356
|
+
assetId,
|
|
2357
|
+
src: dataUrl,
|
|
2358
|
+
textureKey: `ai-current-tileset-edit:${assetId}:${Date.now()}`,
|
|
2359
|
+
assetOverride: optionAsset,
|
|
2360
|
+
onPreview: options.onPreview
|
|
2361
|
+
});
|
|
2362
|
+
setStatus(elements, "Tileset edits applied. Promote to save them to code.", "success");
|
|
2363
|
+
}
|
|
2364
|
+
});
|
|
824
2365
|
return;
|
|
825
2366
|
}
|
|
826
2367
|
void openFrameTouchUpEditor({
|
|
827
2368
|
root: elements.root,
|
|
828
|
-
asset,
|
|
2369
|
+
asset: editorAsset,
|
|
829
2370
|
title: readableAssetName(selectedTargetAssetId),
|
|
830
|
-
frameSrc:
|
|
2371
|
+
frameSrc: sourceOption?.dataUrl ?? resolveAssetUrl(activeVersion.file),
|
|
831
2372
|
displaySize: resolvePreviewDisplaySize(options, selectedTargetAssetId, asset),
|
|
832
2373
|
onSave: async (dataUrl) => {
|
|
2374
|
+
const previousOption = editedCurrentOption;
|
|
833
2375
|
const optionAsset = {
|
|
834
2376
|
...asset,
|
|
835
|
-
dimensions: asset.dimensions
|
|
2377
|
+
dimensions: previousOption?.dimensions ?? asset.dimensions
|
|
836
2378
|
};
|
|
837
2379
|
editedCurrentOption = {
|
|
838
|
-
|
|
2380
|
+
...previousOption,
|
|
2381
|
+
index: previousOption?.index ?? -1,
|
|
839
2382
|
dataUrl,
|
|
840
2383
|
mimeType: mimeTypeFromDataUrl(dataUrl),
|
|
841
|
-
prompt: activeVersion
|
|
842
|
-
model: activeVersion
|
|
843
|
-
revisedPrompt: activeVersion
|
|
844
|
-
dimensions: asset.dimensions
|
|
2384
|
+
prompt: previousOption?.prompt ?? activeVersion?.prompt ?? asset.prompt,
|
|
2385
|
+
model: previousOption?.model ?? activeVersion?.model,
|
|
2386
|
+
revisedPrompt: previousOption?.revisedPrompt ?? activeVersion?.revisedPrompt,
|
|
2387
|
+
dimensions: previousOption?.dimensions ?? asset.dimensions,
|
|
2388
|
+
settings: previousOption?.settings ?? activeVersion?.settings ?? asset.settings
|
|
845
2389
|
};
|
|
846
2390
|
selectedOption = editedCurrentOption;
|
|
2391
|
+
rememberPendingOption(selectedTargetAssetId, editedCurrentOption, {
|
|
2392
|
+
inheritAnimations: Boolean(previewedVersionName),
|
|
2393
|
+
previewedVersionName
|
|
2394
|
+
});
|
|
847
2395
|
elements.currentImage.src = dataUrl;
|
|
848
2396
|
elements.currentPreview.classList.add("is-selected");
|
|
849
|
-
elements.
|
|
2397
|
+
elements.currentRevertButton.hidden = false;
|
|
2398
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
850
2399
|
previewImageSource({
|
|
851
2400
|
scene: options.scene,
|
|
852
2401
|
manifest,
|
|
@@ -895,6 +2444,7 @@ export function installAiAssetDesigner(options) {
|
|
|
895
2444
|
destroy: () => {
|
|
896
2445
|
activeGeneration?.controller.abort();
|
|
897
2446
|
stopStatusAnimation(elements.status);
|
|
2447
|
+
dockPanel.destroy();
|
|
898
2448
|
elements.root.remove();
|
|
899
2449
|
}
|
|
900
2450
|
};
|