@ai-game-assets/phaser 0.6.0 → 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 +28 -3
- package/dist/designer-support.d.ts.map +1 -1
- package/dist/designer-support.js +552 -29
- 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 +1546 -134
- 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, registerInGameDesignerPanel, 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,6 +100,59 @@ 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);
|
|
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
|
+
};
|
|
40
156
|
const applyOpenState = (isOpen) => {
|
|
41
157
|
elements.root.dataset.open = String(isOpen);
|
|
42
158
|
elements.toggle.setAttribute("aria-expanded", String(isOpen));
|
|
@@ -60,10 +176,31 @@ export function installAiAssetDesigner(options) {
|
|
|
60
176
|
}
|
|
61
177
|
};
|
|
62
178
|
const syncAnimationChoices = (assetId) => {
|
|
179
|
+
const resolvedAssetId = resolveTargetAssetId(manifest, assetId, selectedTargetId);
|
|
180
|
+
const resolvedAsset = manifest.assets[resolvedAssetId] ?? manifest.assets[assetId];
|
|
63
181
|
const asset = manifest.assets[assetId];
|
|
64
182
|
const linkedAnimations = Object.entries(asset.linkedAnimationAssets ?? {});
|
|
65
183
|
const isVoice = asset.kind === "voice";
|
|
66
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;
|
|
67
204
|
if (asset.kind !== "collection") {
|
|
68
205
|
const baseOption = document.createElement("option");
|
|
69
206
|
baseOption.value = resolveTargetAssetId(manifest, assetId, selectedTargetId);
|
|
@@ -83,17 +220,143 @@ export function installAiAssetDesigner(options) {
|
|
|
83
220
|
: resolveTargetAssetId(manifest, assetId, selectedTargetId);
|
|
84
221
|
elements.animationSelect.value = selectedTargetAssetId;
|
|
85
222
|
};
|
|
86
|
-
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 = {}) => {
|
|
87
346
|
const asset = manifest.assets[assetId];
|
|
88
347
|
const activeVersion = asset.versions[asset.activeVersion];
|
|
89
348
|
const isAudio = isAudioAsset(asset);
|
|
90
349
|
const isVoice = isVoiceAsset(asset);
|
|
91
350
|
const isVoiceLine = asset.kind === "voice-line";
|
|
351
|
+
const tileset = tilesetMetadataForAsset(asset);
|
|
352
|
+
const tilesetAnimation = selectedTilesetAnimationKey
|
|
353
|
+
? tilesetAnimationForKey(asset, selectedTilesetAnimationKey)
|
|
354
|
+
: undefined;
|
|
92
355
|
stopCurrentAnimationPreview?.();
|
|
93
356
|
stopCurrentAnimationPreview = undefined;
|
|
94
|
-
elements.promptInput.value = activeVersion?.prompt ?? asset.prompt;
|
|
95
|
-
elements.widthInput.value = String(asset.frameGrid?.frameWidth ?? asset.dimensions?.width ?? 1);
|
|
96
|
-
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);
|
|
97
360
|
elements.audioFormatSelect.value = asset.audioSettings?.format ?? "mp3";
|
|
98
361
|
elements.audioDurationInput.value = String(asset.audioSettings?.durationSeconds ?? activeVersion?.durationSeconds ?? "");
|
|
99
362
|
elements.audioLoopInput.checked = Boolean(asset.audioSettings?.loop);
|
|
@@ -110,14 +373,43 @@ export function installAiAssetDesigner(options) {
|
|
|
110
373
|
elements.audioLoopField.hidden = !isAudio;
|
|
111
374
|
elements.voiceTextField.hidden = !isVoice;
|
|
112
375
|
elements.voiceTextField.firstChild.textContent = isVoiceLine ? "Text to say" : "Demo sentence";
|
|
113
|
-
elements.formatField.hidden = isAudio ||
|
|
114
|
-
|
|
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;
|
|
115
388
|
syncTargetVariantLabel(assetId);
|
|
116
|
-
elements.frameCountInput.value = String(
|
|
117
|
-
|
|
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
|
+
}
|
|
118
407
|
const activeVersionSource = activeVersion?.file ? resolveAssetUrl(activeVersion.file) : "";
|
|
119
|
-
elements.frameCountField.hidden =
|
|
120
|
-
|
|
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}`;
|
|
121
413
|
elements.currentImage.src = activeVersionSource;
|
|
122
414
|
if (isAudio) {
|
|
123
415
|
renderAudioPlayer({
|
|
@@ -136,29 +428,53 @@ export function installAiAssetDesigner(options) {
|
|
|
136
428
|
elements.currentAnimation.hidden = true;
|
|
137
429
|
elements.currentAudio.hidden = !isAudio;
|
|
138
430
|
elements.currentImage.hidden = isAudio;
|
|
139
|
-
elements.currentAnimationButton.textContent =
|
|
431
|
+
elements.currentAnimationButton.textContent = isTilesetAnimation
|
|
432
|
+
? "Edit animation..."
|
|
433
|
+
: "Edit...";
|
|
434
|
+
elements.currentTouchUpButton.textContent = isBaseTileset ? "Edit tileset..." : "Touch up...";
|
|
140
435
|
elements.currentRevertButton.hidden = true;
|
|
141
436
|
elements.currentPreview.classList.add("is-selected");
|
|
142
|
-
elements.currentAnimationButton.hidden =
|
|
437
|
+
elements.currentAnimationButton.hidden = isTilesetAnimation && selectedTilesetAnimationKey
|
|
438
|
+
? !hasEditableTilesetAnimationFrames(assetId, selectedTilesetAnimationKey)
|
|
439
|
+
: !activeVersion?.file || (!asset.frameGrid && !isAudio);
|
|
143
440
|
elements.currentTouchUpButton.hidden = !activeVersion?.file ||
|
|
441
|
+
Boolean(selectedTilesetAnimationKey) ||
|
|
144
442
|
isAudio ||
|
|
145
443
|
Boolean(asset.frameGrid) ||
|
|
146
444
|
normalizeAssetFormat(asset.settings?.format) === "svg" ||
|
|
147
445
|
isSvgSource(activeVersion?.file ?? "");
|
|
148
|
-
|
|
149
|
-
|
|
446
|
+
if (syncOptions.preserveOptions && displayedOptions?.assetId === assetId) {
|
|
447
|
+
renderGeneratedOptions(assetId, displayedOptions.generated);
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
clearGeneratedOptions();
|
|
451
|
+
}
|
|
150
452
|
setStatus(elements, "", "idle");
|
|
151
453
|
elements.promoteButton.disabled = true;
|
|
454
|
+
elements.uploadButton.disabled = Boolean(selectedTilesetAnimationKey);
|
|
152
455
|
elements.versionsButton.disabled = Object.keys(asset.versions).length <= 1;
|
|
153
|
-
elements.deriveButton.hidden =
|
|
456
|
+
elements.deriveButton.hidden = Boolean(selectedTilesetAnimationKey) ||
|
|
457
|
+
isAudio || deriveCandidatesForAsset(assetId).length === 0;
|
|
154
458
|
selectedOption = undefined;
|
|
155
459
|
editedCurrentOption = undefined;
|
|
156
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();
|
|
157
472
|
};
|
|
158
473
|
const showOptionInCurrentPreview = (option, label = "selected option") => {
|
|
159
474
|
const asset = manifest.assets[selectedTargetAssetId];
|
|
160
475
|
const optionAsset = assetWithGeneratedGeometry(asset, option);
|
|
161
476
|
const isAudio = isAudioAsset(asset);
|
|
477
|
+
const isTilesetAnimation = Boolean(selectedTilesetAnimationKey && isDesignerTilesetAsset(optionAsset));
|
|
162
478
|
stopCurrentAnimationPreview?.();
|
|
163
479
|
stopCurrentAnimationPreview = undefined;
|
|
164
480
|
editedCurrentOption = option;
|
|
@@ -166,10 +482,18 @@ export function installAiAssetDesigner(options) {
|
|
|
166
482
|
elements.currentPreview.hidden = false;
|
|
167
483
|
elements.currentPreview.setAttribute("aria-label", `Preview ${label} for ${readableAssetName(selectedTargetAssetId)}`);
|
|
168
484
|
elements.currentAnimation.hidden = true;
|
|
169
|
-
elements.currentAnimationButton.textContent =
|
|
170
|
-
|
|
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...";
|
|
171
494
|
elements.currentTouchUpButton.hidden =
|
|
172
495
|
isAudio ||
|
|
496
|
+
isTilesetAnimation ||
|
|
173
497
|
Boolean(optionAsset.frameGrid) ||
|
|
174
498
|
option.mimeType === "image/svg+xml";
|
|
175
499
|
elements.currentRevertButton.hidden = false;
|
|
@@ -189,6 +513,262 @@ export function installAiAssetDesigner(options) {
|
|
|
189
513
|
elements.currentImage.alt = `${readableAssetName(selectedTargetAssetId)} ${label}`;
|
|
190
514
|
}
|
|
191
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
|
+
};
|
|
192
772
|
const restoreActivePreview = () => {
|
|
193
773
|
const asset = manifest.assets[selectedTargetAssetId];
|
|
194
774
|
const activeVersion = asset.versions[asset.activeVersion];
|
|
@@ -199,18 +779,28 @@ export function installAiAssetDesigner(options) {
|
|
|
199
779
|
editedCurrentOption = undefined;
|
|
200
780
|
previewedVersionName = undefined;
|
|
201
781
|
selectedOption = undefined;
|
|
782
|
+
forgetPendingOption(selectedTargetAssetId);
|
|
202
783
|
for (const item of elements.options.querySelectorAll(".ai-game-assets-designer__option")) {
|
|
203
784
|
item.classList.remove("is-selected");
|
|
204
785
|
}
|
|
205
786
|
const activeVersionSource = resolveAssetUrl(activeVersion.file);
|
|
206
787
|
const isAudio = isAudioAsset(asset);
|
|
788
|
+
const isTilesetAnimation = Boolean(selectedTilesetAnimationKey && isDesignerTilesetAsset(asset));
|
|
207
789
|
elements.currentPreview.classList.add("is-selected");
|
|
208
790
|
elements.currentPreview.setAttribute("aria-label", `Preview active ${readableAssetName(selectedTargetAssetId)} version`);
|
|
209
791
|
elements.currentAnimation.hidden = true;
|
|
210
|
-
elements.currentAnimationButton.textContent =
|
|
211
|
-
|
|
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...";
|
|
212
801
|
elements.currentTouchUpButton.hidden =
|
|
213
802
|
isAudio ||
|
|
803
|
+
isTilesetAnimation ||
|
|
214
804
|
Boolean(asset.frameGrid) ||
|
|
215
805
|
normalizeAssetFormat(asset.settings?.format) === "svg" ||
|
|
216
806
|
isSvgSource(activeVersion.file);
|
|
@@ -225,21 +815,28 @@ export function installAiAssetDesigner(options) {
|
|
|
225
815
|
label: readableAssetName(selectedTargetAssetId),
|
|
226
816
|
playback: activeVersion.audioPlayback
|
|
227
817
|
});
|
|
228
|
-
options.onPreview(selectedTargetAssetId, activeVersionSource, asset);
|
|
818
|
+
(options.onAssetReady ?? options.onPreview)(selectedTargetAssetId, activeVersionSource, asset);
|
|
819
|
+
setStatus(elements, "Reverted preview to the active version.", "info");
|
|
229
820
|
}
|
|
230
821
|
else {
|
|
231
822
|
elements.currentAudio.innerHTML = "";
|
|
232
823
|
elements.currentImage.src = activeVersionSource;
|
|
233
824
|
elements.currentImage.alt = `${readableAssetName(selectedTargetAssetId)} active version`;
|
|
825
|
+
setStatus(elements, "Reverting preview in the game...", "busy");
|
|
234
826
|
previewCurrentAsset({
|
|
235
827
|
scene: options.scene,
|
|
236
828
|
manifest,
|
|
237
829
|
assetId: selectedTargetAssetId,
|
|
238
830
|
src: activeVersionSource,
|
|
239
|
-
onPreview
|
|
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
|
+
}
|
|
240
838
|
});
|
|
241
839
|
}
|
|
242
|
-
setStatus(elements, "Reverted preview to the active version.", "info");
|
|
243
840
|
};
|
|
244
841
|
const syncAsset = (assetId) => {
|
|
245
842
|
syncAnimationChoices(assetId);
|
|
@@ -335,6 +932,7 @@ export function installAiAssetDesigner(options) {
|
|
|
335
932
|
selectedAssetId,
|
|
336
933
|
assetIds: visibleAssetIds(),
|
|
337
934
|
onSelect(assetId) {
|
|
935
|
+
invalidatePanelWork();
|
|
338
936
|
selectedAssetId = assetId;
|
|
339
937
|
syncAsset(selectedAssetId);
|
|
340
938
|
}
|
|
@@ -362,13 +960,117 @@ export function installAiAssetDesigner(options) {
|
|
|
362
960
|
activeGeneration = undefined;
|
|
363
961
|
elements.regenerateButton.textContent = "Regenerate";
|
|
364
962
|
unlockGenerationStatus();
|
|
963
|
+
elements.promoteButton.disabled = !selectedOption || activePromotionId !== undefined;
|
|
964
|
+
syncPromoteAllButton();
|
|
965
|
+
syncMixTilesetButton();
|
|
365
966
|
return true;
|
|
366
967
|
};
|
|
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
|
+
};
|
|
367
1067
|
elements.assetSelect.addEventListener("change", () => {
|
|
1068
|
+
invalidatePanelWork();
|
|
368
1069
|
selectedAssetId = elements.assetSelect.value;
|
|
369
1070
|
syncAsset(selectedAssetId);
|
|
370
1071
|
});
|
|
371
1072
|
elements.targetSelect.addEventListener("change", () => {
|
|
1073
|
+
invalidatePanelWork();
|
|
372
1074
|
selectedTargetId = elements.targetSelect.value || undefined;
|
|
373
1075
|
syncAsset(selectedAssetId);
|
|
374
1076
|
setStatus(elements, selectedTargetId
|
|
@@ -376,9 +1078,27 @@ export function installAiAssetDesigner(options) {
|
|
|
376
1078
|
: "Editing default target.", "info");
|
|
377
1079
|
});
|
|
378
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;
|
|
379
1091
|
selectedTargetAssetId = elements.animationSelect.value;
|
|
380
1092
|
syncTargetAsset(selectedTargetAssetId);
|
|
381
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);
|
|
382
1102
|
elements.formatSelect.addEventListener("change", () => {
|
|
383
1103
|
if (canEditGenerationFormat(manifest, selectedAssetId, selectedTargetAssetId)) {
|
|
384
1104
|
formatDrafts.set(selectedTargetAssetId, normalizeAssetFormat(elements.formatSelect.value));
|
|
@@ -464,57 +1184,212 @@ export function installAiAssetDesigner(options) {
|
|
|
464
1184
|
event.preventDefault();
|
|
465
1185
|
elements.currentPreview.click();
|
|
466
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
|
+
});
|
|
467
1331
|
elements.regenerateButton.addEventListener("click", async () => {
|
|
468
1332
|
if (activeGeneration) {
|
|
469
1333
|
activeGeneration.controller.abort();
|
|
470
1334
|
activeGeneration = undefined;
|
|
471
1335
|
elements.regenerateButton.textContent = "Regenerate";
|
|
472
1336
|
unlockGenerationStatus();
|
|
1337
|
+
elements.promoteButton.disabled = !selectedOption || activePromotionId !== undefined;
|
|
1338
|
+
syncPromoteAllButton();
|
|
473
1339
|
setStatus(elements, "Generation cancelled.", "info");
|
|
474
1340
|
return;
|
|
475
1341
|
}
|
|
1342
|
+
if (selectedTilesetAnimationKey) {
|
|
1343
|
+
await regenerateTilesetAnimation(selectedTilesetAnimationKey);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
476
1346
|
const controller = new AbortController();
|
|
477
1347
|
const currentGenerationId = generationId + 1;
|
|
478
1348
|
const generationAssetId = selectedTargetAssetId;
|
|
1349
|
+
const generationAsset = manifest.assets[generationAssetId];
|
|
479
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
|
+
}
|
|
480
1367
|
generationId = currentGenerationId;
|
|
481
1368
|
activeGeneration = { controller, id: currentGenerationId };
|
|
1369
|
+
syncPromoteAllButton();
|
|
482
1370
|
setStatus(elements, "Generating options...", "busy");
|
|
483
1371
|
lockGenerationStatus();
|
|
484
1372
|
elements.promoteButton.disabled = true;
|
|
485
1373
|
elements.regenerateButton.textContent = "Cancel";
|
|
486
|
-
|
|
487
|
-
selectedOption =
|
|
1374
|
+
clearGeneratedOptions();
|
|
1375
|
+
selectedOption = editedCurrentOption;
|
|
488
1376
|
const streamedOptions = [];
|
|
489
1377
|
try {
|
|
490
1378
|
const generationRequest = {
|
|
491
1379
|
assetId: generationAssetId,
|
|
492
|
-
prompt: elements.promptInput.value,
|
|
1380
|
+
prompt: isBaseTilesetGeneration ? undefined : elements.promptInput.value,
|
|
493
1381
|
count: options.optionCount ?? 3,
|
|
494
1382
|
format: generationFormat,
|
|
495
1383
|
audioSettings: audioGenerationOverridesFromInputs(elements, manifest.assets[generationAssetId]),
|
|
496
1384
|
voiceSettings: voiceGenerationOverridesFromInputs(elements, manifest.assets[generationAssetId]),
|
|
497
1385
|
styleGuide: await styleGuideRequest(styleGuideDraft),
|
|
498
|
-
...
|
|
1386
|
+
...generationOverrides
|
|
499
1387
|
};
|
|
500
1388
|
await client.generateStream(generationRequest, (option) => {
|
|
501
1389
|
if (activeGeneration?.id !== currentGenerationId)
|
|
502
1390
|
return;
|
|
503
1391
|
streamedOptions.push(option);
|
|
504
|
-
|
|
505
|
-
elements,
|
|
506
|
-
generated: [...streamedOptions].sort((left, right) => left.index - right.index),
|
|
507
|
-
scene: options.scene,
|
|
508
|
-
manifest,
|
|
509
|
-
assetId: generationAssetId,
|
|
510
|
-
designerOptions: options,
|
|
511
|
-
onPreview: options.onPreview,
|
|
512
|
-
onSelected(selected) {
|
|
513
|
-
selectedOption = selected;
|
|
514
|
-
showOptionInCurrentPreview(selected);
|
|
515
|
-
elements.promoteButton.disabled = false;
|
|
516
|
-
}
|
|
517
|
-
});
|
|
1392
|
+
renderGeneratedOptions(generationAssetId, [...streamedOptions].sort((left, right) => left.index - right.index));
|
|
518
1393
|
}, {
|
|
519
1394
|
signal: controller.signal
|
|
520
1395
|
});
|
|
@@ -522,7 +1397,11 @@ export function installAiAssetDesigner(options) {
|
|
|
522
1397
|
return;
|
|
523
1398
|
}
|
|
524
1399
|
finishGeneration(currentGenerationId);
|
|
525
|
-
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");
|
|
526
1405
|
}
|
|
527
1406
|
catch (error) {
|
|
528
1407
|
if (isAbortError(error)) {
|
|
@@ -530,7 +1409,7 @@ export function installAiAssetDesigner(options) {
|
|
|
530
1409
|
}
|
|
531
1410
|
finishGeneration(currentGenerationId);
|
|
532
1411
|
if (streamedOptions.length === 0) {
|
|
533
|
-
|
|
1412
|
+
clearGeneratedOptions();
|
|
534
1413
|
}
|
|
535
1414
|
setStatus(elements, `Generation failed. ${errorMessage(error)}`, "error");
|
|
536
1415
|
}
|
|
@@ -550,23 +1429,23 @@ export function installAiAssetDesigner(options) {
|
|
|
550
1429
|
elements,
|
|
551
1430
|
prompt: elements.promptInput.value
|
|
552
1431
|
});
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
designerOptions: options,
|
|
560
|
-
onPreview: options.onPreview,
|
|
561
|
-
onSelected(option) {
|
|
562
|
-
selectedOption = option;
|
|
563
|
-
showOptionInCurrentPreview(option);
|
|
564
|
-
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.");
|
|
565
1438
|
}
|
|
566
|
-
|
|
1439
|
+
uploadedOption.tileset = {
|
|
1440
|
+
...uploadedOption.tileset,
|
|
1441
|
+
tiles
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
renderGeneratedOptions(selectedTargetAssetId, [uploadedOption]);
|
|
567
1445
|
selectedOption = uploadedOption;
|
|
1446
|
+
rememberPendingOption(selectedTargetAssetId, uploadedOption);
|
|
568
1447
|
showOptionInCurrentPreview(uploadedOption);
|
|
569
|
-
elements.promoteButton.disabled =
|
|
1448
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
570
1449
|
if (isAudioAsset(asset)) {
|
|
571
1450
|
options.onPreview(selectedTargetAssetId, uploadedOption.dataUrl, assetWithGeneratedGeometry(asset, uploadedOption));
|
|
572
1451
|
}
|
|
@@ -599,7 +1478,7 @@ export function installAiAssetDesigner(options) {
|
|
|
599
1478
|
prompt: elements.promptInput.value,
|
|
600
1479
|
format: generationFormat,
|
|
601
1480
|
onConfirm: async (deriveRequest) => {
|
|
602
|
-
|
|
1481
|
+
clearGeneratedOptions();
|
|
603
1482
|
elements.promoteButton.disabled = true;
|
|
604
1483
|
selectedOption = undefined;
|
|
605
1484
|
if (deriveRequest.strategy === "scale" ||
|
|
@@ -619,23 +1498,11 @@ export function installAiAssetDesigner(options) {
|
|
|
619
1498
|
mirrorX: deriveRequest.mirrorX,
|
|
620
1499
|
mirrorY: deriveRequest.mirrorY
|
|
621
1500
|
});
|
|
622
|
-
|
|
623
|
-
elements,
|
|
624
|
-
generated: [option],
|
|
625
|
-
scene: options.scene,
|
|
626
|
-
manifest,
|
|
627
|
-
assetId: derivationAssetId,
|
|
628
|
-
designerOptions: options,
|
|
629
|
-
onPreview: options.onPreview,
|
|
630
|
-
onSelected(selected) {
|
|
631
|
-
selectedOption = selected;
|
|
632
|
-
showOptionInCurrentPreview(selected);
|
|
633
|
-
elements.promoteButton.disabled = false;
|
|
634
|
-
}
|
|
635
|
-
});
|
|
1501
|
+
renderGeneratedOptions(derivationAssetId, [option]);
|
|
636
1502
|
selectedOption = option;
|
|
1503
|
+
rememberPendingOption(derivationAssetId, option);
|
|
637
1504
|
showOptionInCurrentPreview(option);
|
|
638
|
-
elements.promoteButton.disabled =
|
|
1505
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
639
1506
|
previewOption({
|
|
640
1507
|
scene: options.scene,
|
|
641
1508
|
manifest,
|
|
@@ -654,6 +1521,7 @@ export function installAiAssetDesigner(options) {
|
|
|
654
1521
|
: elements.promptInput.value;
|
|
655
1522
|
generationId = currentGenerationId;
|
|
656
1523
|
activeGeneration = { controller, id: currentGenerationId };
|
|
1524
|
+
syncPromoteAllButton();
|
|
657
1525
|
elements.regenerateButton.textContent = "Cancel";
|
|
658
1526
|
setStatus(elements, "Generating options...", "busy");
|
|
659
1527
|
lockGenerationStatus();
|
|
@@ -672,20 +1540,7 @@ export function installAiAssetDesigner(options) {
|
|
|
672
1540
|
if (activeGeneration?.id !== currentGenerationId)
|
|
673
1541
|
return;
|
|
674
1542
|
streamedOptions.push(option);
|
|
675
|
-
|
|
676
|
-
elements,
|
|
677
|
-
generated: [...streamedOptions].sort((left, right) => left.index - right.index),
|
|
678
|
-
scene: options.scene,
|
|
679
|
-
manifest,
|
|
680
|
-
assetId: derivationAssetId,
|
|
681
|
-
designerOptions: options,
|
|
682
|
-
onPreview: options.onPreview,
|
|
683
|
-
onSelected(selected) {
|
|
684
|
-
selectedOption = selected;
|
|
685
|
-
showOptionInCurrentPreview(selected);
|
|
686
|
-
elements.promoteButton.disabled = false;
|
|
687
|
-
}
|
|
688
|
-
});
|
|
1543
|
+
renderGeneratedOptions(derivationAssetId, [...streamedOptions].sort((left, right) => left.index - right.index));
|
|
689
1544
|
}, {
|
|
690
1545
|
signal: controller.signal
|
|
691
1546
|
});
|
|
@@ -705,49 +1560,348 @@ export function installAiAssetDesigner(options) {
|
|
|
705
1560
|
});
|
|
706
1561
|
});
|
|
707
1562
|
elements.promoteButton.addEventListener("click", async () => {
|
|
708
|
-
if (!selectedOption)
|
|
1563
|
+
if (!selectedOption || activePromotionId !== undefined)
|
|
709
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;
|
|
710
1583
|
const versionName = `promoted-${Date.now()}`;
|
|
711
1584
|
setStatus(elements, `Promoting ${versionName}...`, "busy");
|
|
712
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
|
+
}
|
|
713
1705
|
try {
|
|
714
|
-
await client.save({
|
|
715
|
-
assetId: selectedTargetAssetId,
|
|
716
|
-
versionName,
|
|
717
|
-
dataUrl: selectedOption.dataUrl,
|
|
718
|
-
prompt: selectedOption.prompt,
|
|
719
|
-
model: selectedOption.model,
|
|
720
|
-
revisedPrompt: selectedOption.revisedPrompt,
|
|
721
|
-
dimensions: selectedOption.dimensions,
|
|
722
|
-
frameGrid: selectedOption.frameGrid,
|
|
723
|
-
animations: assetWithGeneratedGeometry(manifest.assets[selectedTargetAssetId], selectedOption, { inheritAnimations: Boolean(previewedVersionName) }).animations,
|
|
724
|
-
settings: selectedOption.settings,
|
|
725
|
-
audioSettings: selectedOption.audioSettings,
|
|
726
|
-
audioPlayback: selectedOption.audioPlayback,
|
|
727
|
-
voiceSettings: selectedOption.voiceSettings,
|
|
728
|
-
durationSeconds: selectedOption.durationSeconds,
|
|
729
|
-
activate: true,
|
|
730
|
-
notes: "Promoted from the AI asset designer."
|
|
731
|
-
});
|
|
732
|
-
manifest = await client.getManifest();
|
|
733
1706
|
options.onManifestUpdated?.(manifest);
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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) ?? []);
|
|
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
|
+
}
|
|
741
1863
|
}
|
|
1864
|
+
catch (error) {
|
|
1865
|
+
liveRefreshErrors.push({ assetId, error });
|
|
1866
|
+
}
|
|
1867
|
+
pendingOptions.delete(assetId);
|
|
1868
|
+
formatDrafts.delete(assetId);
|
|
1869
|
+
promotedCount += 1;
|
|
742
1870
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1871
|
+
catch (error) {
|
|
1872
|
+
promotionError = { assetId, error };
|
|
1873
|
+
break;
|
|
746
1874
|
}
|
|
747
1875
|
}
|
|
1876
|
+
try {
|
|
1877
|
+
if (promotedCount > 0) {
|
|
1878
|
+
options.onManifestUpdated?.(manifest);
|
|
1879
|
+
}
|
|
1880
|
+
syncTargetAsset(selectedTargetAssetId);
|
|
1881
|
+
renderAssetBrowser();
|
|
1882
|
+
}
|
|
748
1883
|
catch (error) {
|
|
749
|
-
|
|
750
|
-
|
|
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();
|
|
751
1905
|
}
|
|
752
1906
|
});
|
|
753
1907
|
elements.restartButton.addEventListener("click", () => {
|
|
@@ -762,30 +1916,45 @@ export function installAiAssetDesigner(options) {
|
|
|
762
1916
|
resolveAssetUrl,
|
|
763
1917
|
async onSelect(versionName, option) {
|
|
764
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
|
+
};
|
|
765
1930
|
for (const item of elements.options.querySelectorAll(".ai-game-assets-designer__option")) {
|
|
766
1931
|
item.classList.remove("is-selected");
|
|
767
1932
|
}
|
|
768
|
-
selectedOption =
|
|
769
|
-
|
|
1933
|
+
selectedOption = versionOption;
|
|
1934
|
+
rememberPendingOption(selectedTargetAssetId, versionOption, {
|
|
1935
|
+
inheritAnimations: true,
|
|
1936
|
+
previewedVersionName: versionName
|
|
1937
|
+
});
|
|
1938
|
+
showOptionInCurrentPreview(versionOption, versionName);
|
|
770
1939
|
previewedVersionName = versionName;
|
|
771
1940
|
elements.currentPreview.classList.add("is-selected");
|
|
772
|
-
elements.promoteButton.disabled =
|
|
1941
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
773
1942
|
if (isAudioAsset(asset)) {
|
|
774
1943
|
renderAudioPlayer({
|
|
775
1944
|
container: elements.currentAudio,
|
|
776
|
-
src:
|
|
1945
|
+
src: versionOption.dataUrl,
|
|
777
1946
|
label: readableAssetName(selectedTargetAssetId),
|
|
778
|
-
playback:
|
|
1947
|
+
playback: versionOption.audioPlayback
|
|
779
1948
|
});
|
|
780
|
-
options.onPreview(selectedTargetAssetId,
|
|
1949
|
+
options.onPreview(selectedTargetAssetId, versionOption.dataUrl, optionAsset);
|
|
781
1950
|
}
|
|
782
1951
|
else {
|
|
783
|
-
elements.currentImage.src =
|
|
1952
|
+
elements.currentImage.src = versionOption.dataUrl;
|
|
784
1953
|
previewImageSource({
|
|
785
1954
|
scene: options.scene,
|
|
786
1955
|
manifest,
|
|
787
1956
|
assetId: selectedTargetAssetId,
|
|
788
|
-
src:
|
|
1957
|
+
src: versionOption.dataUrl,
|
|
789
1958
|
textureKey: `ai-version-preview:${selectedTargetAssetId}:${versionName}:${Date.now()}`,
|
|
790
1959
|
assetOverride: optionAsset,
|
|
791
1960
|
onPreview: options.onPreview
|
|
@@ -801,6 +1970,7 @@ export function installAiAssetDesigner(options) {
|
|
|
801
1970
|
});
|
|
802
1971
|
options.onManifestUpdated?.(manifest);
|
|
803
1972
|
if (previewedVersionName === versionName) {
|
|
1973
|
+
forgetPendingOption(selectedTargetAssetId);
|
|
804
1974
|
syncTargetAsset(selectedTargetAssetId);
|
|
805
1975
|
}
|
|
806
1976
|
else {
|
|
@@ -816,6 +1986,113 @@ export function installAiAssetDesigner(options) {
|
|
|
816
1986
|
}
|
|
817
1987
|
});
|
|
818
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
|
+
};
|
|
819
2096
|
elements.styleButton.addEventListener("click", () => {
|
|
820
2097
|
void openStyleGuideEditor({
|
|
821
2098
|
root: elements.root,
|
|
@@ -838,16 +2115,62 @@ export function installAiAssetDesigner(options) {
|
|
|
838
2115
|
setStatus(elements, `Style promotion failed. ${errorMessage(error)}`, "error");
|
|
839
2116
|
throw error;
|
|
840
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
|
+
}
|
|
841
2126
|
}
|
|
842
2127
|
});
|
|
843
2128
|
});
|
|
844
|
-
elements.currentAnimationButton.addEventListener("click", (event) => {
|
|
2129
|
+
elements.currentAnimationButton.addEventListener("click", async (event) => {
|
|
845
2130
|
event.stopPropagation();
|
|
846
2131
|
const asset = manifest.assets[selectedTargetAssetId];
|
|
847
2132
|
const activeVersion = asset.versions[asset.activeVersion];
|
|
848
2133
|
const sourceOption = editedCurrentOption;
|
|
849
2134
|
if (!sourceOption && !activeVersion?.file)
|
|
850
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
|
+
}
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
851
2174
|
if (isAudioAsset(asset)) {
|
|
852
2175
|
void openAudioEditor({
|
|
853
2176
|
root: elements.root,
|
|
@@ -877,7 +2200,11 @@ export function installAiAssetDesigner(options) {
|
|
|
877
2200
|
durationSeconds: previousOption?.durationSeconds ?? activeVersion?.durationSeconds
|
|
878
2201
|
};
|
|
879
2202
|
selectedOption = editedCurrentOption;
|
|
880
|
-
|
|
2203
|
+
rememberPendingOption(selectedTargetAssetId, editedCurrentOption, {
|
|
2204
|
+
inheritAnimations: Boolean(previewedVersionName),
|
|
2205
|
+
previewedVersionName
|
|
2206
|
+
});
|
|
2207
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
881
2208
|
elements.currentRevertButton.hidden = false;
|
|
882
2209
|
options.onPreview(selectedTargetAssetId, dataUrl, optionAsset);
|
|
883
2210
|
renderAudioPlayer({
|
|
@@ -928,7 +2255,11 @@ export function installAiAssetDesigner(options) {
|
|
|
928
2255
|
animations
|
|
929
2256
|
};
|
|
930
2257
|
selectedOption = editedCurrentOption;
|
|
931
|
-
|
|
2258
|
+
rememberPendingOption(selectedTargetAssetId, editedCurrentOption, {
|
|
2259
|
+
inheritAnimations: Boolean(previewedVersionName),
|
|
2260
|
+
previewedVersionName
|
|
2261
|
+
});
|
|
2262
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
932
2263
|
elements.currentRevertButton.hidden = false;
|
|
933
2264
|
previewImageSource({
|
|
934
2265
|
scene: options.scene,
|
|
@@ -948,17 +2279,94 @@ export function installAiAssetDesigner(options) {
|
|
|
948
2279
|
const asset = manifest.assets[selectedTargetAssetId];
|
|
949
2280
|
const activeVersion = asset.versions[asset.activeVersion];
|
|
950
2281
|
const sourceOption = editedCurrentOption;
|
|
2282
|
+
const editorAsset = sourceOption
|
|
2283
|
+
? assetWithGeneratedGeometry(asset, sourceOption)
|
|
2284
|
+
: asset;
|
|
951
2285
|
if ((!sourceOption && !activeVersion?.file) ||
|
|
952
2286
|
isAudioAsset(asset) ||
|
|
953
|
-
|
|
2287
|
+
editorAsset.frameGrid ||
|
|
954
2288
|
sourceOption?.mimeType === "image/svg+xml" ||
|
|
955
2289
|
(!sourceOption && (normalizeAssetFormat(asset.settings?.format) === "svg" ||
|
|
956
2290
|
isSvgSource(activeVersion.file)))) {
|
|
957
2291
|
return;
|
|
958
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
|
+
});
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
959
2367
|
void openFrameTouchUpEditor({
|
|
960
2368
|
root: elements.root,
|
|
961
|
-
asset,
|
|
2369
|
+
asset: editorAsset,
|
|
962
2370
|
title: readableAssetName(selectedTargetAssetId),
|
|
963
2371
|
frameSrc: sourceOption?.dataUrl ?? resolveAssetUrl(activeVersion.file),
|
|
964
2372
|
displaySize: resolvePreviewDisplaySize(options, selectedTargetAssetId, asset),
|
|
@@ -980,10 +2388,14 @@ export function installAiAssetDesigner(options) {
|
|
|
980
2388
|
settings: previousOption?.settings ?? activeVersion?.settings ?? asset.settings
|
|
981
2389
|
};
|
|
982
2390
|
selectedOption = editedCurrentOption;
|
|
2391
|
+
rememberPendingOption(selectedTargetAssetId, editedCurrentOption, {
|
|
2392
|
+
inheritAnimations: Boolean(previewedVersionName),
|
|
2393
|
+
previewedVersionName
|
|
2394
|
+
});
|
|
983
2395
|
elements.currentImage.src = dataUrl;
|
|
984
2396
|
elements.currentPreview.classList.add("is-selected");
|
|
985
2397
|
elements.currentRevertButton.hidden = false;
|
|
986
|
-
elements.promoteButton.disabled =
|
|
2398
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
987
2399
|
previewImageSource({
|
|
988
2400
|
scene: options.scene,
|
|
989
2401
|
manifest,
|