improvements
This commit is contained in:
+109
-29
@@ -794,7 +794,7 @@
|
||||
if (elements.length) Canvas.updateView({ elements, element_aspects: { faces: true, uv: true } });
|
||||
}
|
||||
|
||||
function rebuildBlockRequirementGuides(animationId, entry, importedDefinition, manageUndo = true) {
|
||||
function rebuildBlockRequirementGuides(animationId, entry, importedDefinition, manageUndo = true, importedProps) {
|
||||
const boxes = entry ? buildBlockRequirementGuideBoxes(entry) : [];
|
||||
const groupName = `${BLOCK_GUIDE_PREFIX}${animationId}`;
|
||||
const oldGroups = Group.all.filter((group) => group.name === groupName);
|
||||
@@ -806,7 +806,8 @@
|
||||
const createdElements = [];
|
||||
const createdGroups = [];
|
||||
if (manageUndo) Undo.initEdit({ outliner: true, elements: oldElements, groups: oldGroups,
|
||||
block_requirement_edit: true, animdef_edit: importedDefinition !== undefined });
|
||||
block_requirement_edit: true, animdef_edit: importedDefinition !== undefined,
|
||||
prop_preview_edit: importedProps !== undefined });
|
||||
if (entry) {
|
||||
const root = oldGroups[0] || new Group({ name: groupName, export: false });
|
||||
if (!oldGroups.length) { root.init(); root.addTo('root'); }
|
||||
@@ -829,6 +830,7 @@
|
||||
}
|
||||
Project[BLOCK_REQUIREMENTS_PROPERTY] = JSON.stringify(updated);
|
||||
if (importedDefinition !== undefined) saveAnimdef(animationId, importedDefinition);
|
||||
if (importedProps !== undefined) Project[PROP_PREVIEWS_PROPERTY] = JSON.stringify(importedProps);
|
||||
hideInternalBlockGuideFaces(createdElements);
|
||||
// Rebuilding every cube would reset forced actor materials in single-texture formats.
|
||||
Canvas.updateView({ elements: createdElements, groups: createdGroups,
|
||||
@@ -836,7 +838,7 @@
|
||||
for (const cube of createdElements) applyBlockRequirementGuideMaterial(cube);
|
||||
if (manageUndo) Undo.finishEdit(importedDefinition !== undefined ? 'Import animation definition' : entry ? 'Set block requirement' : 'Remove block requirement',
|
||||
{ outliner: true, elements: createdElements, groups: createdGroups, block_requirement_edit: true,
|
||||
animdef_edit: importedDefinition !== undefined });
|
||||
animdef_edit: importedDefinition !== undefined, prop_preview_edit: importedProps !== undefined });
|
||||
Project.saved = false;
|
||||
}
|
||||
|
||||
@@ -2738,6 +2740,11 @@
|
||||
}
|
||||
});
|
||||
definition.actors.sort((a, b) => roles.indexOf(a.label) - roles.indexOf(b.label));
|
||||
for (const stage of definition.stages) {
|
||||
if (stage.props && typeof stage.props === 'object' && !Array.isArray(stage.props)) {
|
||||
stage.props = Object.fromEntries(Object.entries(stage.props).map(([label, value]) => [labelMap.get(label) || label, value]));
|
||||
}
|
||||
}
|
||||
if (definition.position_anchor_actor !== undefined) {
|
||||
if (!labelMap.has(definition.position_anchor_actor)) throw new Error('The position anchor references an unknown actor.');
|
||||
definition.position_anchor_actor = labelMap.get(definition.position_anchor_actor);
|
||||
@@ -2745,7 +2752,7 @@
|
||||
return definition;
|
||||
}
|
||||
|
||||
function applyAnimationDefinitionImport(group, analysis, source) {
|
||||
function applyAnimationDefinitionImport(group, analysis, source, sourcePaths = []) {
|
||||
const definition = prepareAnimationDefinitionImport(source, analysis);
|
||||
let entry = null;
|
||||
if (definition.block_requirements) {
|
||||
@@ -2761,8 +2768,11 @@
|
||||
entry = readBlockRequirementForm(values);
|
||||
buildBlockRequirementGuideBoxes(entry);
|
||||
}
|
||||
const props = importPropPreviews(group.id, definition, sourcePaths, readPropPreviews());
|
||||
cancelBlockRequirementUpdate();
|
||||
rebuildBlockRequirementGuides(group.id, entry, definition);
|
||||
rebuildBlockRequirementGuides(group.id, entry, definition, true, props.entries);
|
||||
if (props.warnings.length) Blockbench.showQuickMessage(`Item props: ${props.warnings.join(' ')}`, 6000);
|
||||
void refreshPropPreviews(true);
|
||||
if (blockRequirementPanel?.vue) blockRequirementPanel.vue.load(group.id);
|
||||
showAnimationDefinitionReviewDialog(group, analysis);
|
||||
}
|
||||
@@ -2793,7 +2803,7 @@
|
||||
if (!Project || Project.uuid !== projectId) throw new Error('The active project changed. Import the definition again.');
|
||||
const group = collectConjoinedAnimationGroups().groups.find((candidate) => candidate.id === result.animation_id);
|
||||
if (!group) throw new Error('The selected animation no longer exists.');
|
||||
applyAnimationDefinitionImport(group, analyzeAnimationDefinitionGroup(group), source);
|
||||
applyAnimationDefinitionImport(group, analyzeAnimationDefinitionGroup(group), source, [files[0].path]);
|
||||
return true;
|
||||
} catch (e) { report(e); return false; }
|
||||
},
|
||||
@@ -3951,12 +3961,6 @@
|
||||
return results;
|
||||
}
|
||||
|
||||
function propItemOptions(items, search) {
|
||||
const query = String(search || '').trim().toLowerCase();
|
||||
return { '': 'Select a matching item...', ...Object.fromEntries(items
|
||||
.filter((id) => id.toLowerCase().includes(query)).map((id) => [id, id])) };
|
||||
}
|
||||
|
||||
function propJson(source, path) {
|
||||
const content = source.read(path);
|
||||
return content ? JSON.parse(content.toString('utf8').replace(/^\uFEFF/, '')) : null;
|
||||
@@ -4086,6 +4090,70 @@
|
||||
return definition.actors?.find((entry) => entry.label === actor)?.[field];
|
||||
}
|
||||
|
||||
function importPropPreviews(animation, definition, preferredSources = [], existing = {}) {
|
||||
const entries = Object.fromEntries(Object.entries(existing).filter(([, entry]) => entry?.animation !== animation));
|
||||
const warnings = [], requested = [];
|
||||
for (const actor of definition.actors || []) for (const slot of Object.keys(PROP_SLOTS)) {
|
||||
const base = effectivePropItem(definition, actor.label, slot, 0);
|
||||
if (typeof base === 'string') requested.push({ actor: actor.label, slot, stage: 0, item: base });
|
||||
for (const stage of definition.stages || []) {
|
||||
const item = effectivePropItem(definition, actor.label, slot, stage.stage);
|
||||
if (item === base) continue;
|
||||
if (typeof item === 'string' || item === null && typeof base === 'string') {
|
||||
requested.push({ actor: actor.label, slot, stage: stage.stage, item });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!requested.length) return { entries, warnings };
|
||||
let io;
|
||||
try { io = desktopFiles(); }
|
||||
catch (_) { warnings.push('Preview assets require desktop Blockbench. Prop IDs remain in the imported definition.'); return { entries, warnings }; }
|
||||
const paths = new Set();
|
||||
const addPath = (path) => {
|
||||
if (!path) return;
|
||||
try {
|
||||
const resolved = importFilePath(path, io);
|
||||
if (io.fs.statSync(resolved).isDirectory() || /\.(?:jar|zip)$/i.test(resolved)) paths.add(resolved);
|
||||
else paths.add(findImportPack(resolved, io).root);
|
||||
} catch (_) { /* A standalone definition may have no parent pack. */ }
|
||||
};
|
||||
preferredSources.forEach(addPath);
|
||||
try { savedPackFolders().forEach(addPath); } catch (_) { /* No registered packs. */ }
|
||||
try { addPath(localStorage.getItem('animationdirector_prop_source')); } catch (_) { /* No saved source. */ }
|
||||
try { discoverPropSources(io).forEach((source) => addPath(source.file)); } catch (_) { /* No client assets found. */ }
|
||||
const sources = [];
|
||||
for (const path of paths) {
|
||||
try { sources.push({ path, data: openPropSource(path, io) }); }
|
||||
catch (_) { /* Try the next available source. */ }
|
||||
}
|
||||
const resolved = new Map(), missing = new Set();
|
||||
for (const prop of requested) {
|
||||
const key = propEntryKey(animation, prop.actor, prop.slot, prop.stage);
|
||||
if (prop.item === null) {
|
||||
entries[key] = { animation, actor: prop.actor, slot: prop.slot, stage: prop.stage, mode: 'hidden' };
|
||||
continue;
|
||||
}
|
||||
const assetKey = `${prop.slot}:${prop.item}`;
|
||||
if (!resolved.has(assetKey)) {
|
||||
let found = null;
|
||||
for (const source of sources) {
|
||||
try { found = { source: source.path, asset: loadPropAsset(source.data, prop.item, prop.slot) }; break; }
|
||||
catch (_) { /* Item resources may live in another source. */ }
|
||||
}
|
||||
resolved.set(assetKey, found);
|
||||
}
|
||||
const found = resolved.get(assetKey);
|
||||
if (!found) { missing.add(prop.item); continue; }
|
||||
entries[key] = { animation, actor: prop.actor, slot: prop.slot, stage: prop.stage, mode: 'item',
|
||||
item: prop.item, source: found.source, texture: '', preset: 'generated', asset: found.asset };
|
||||
if (JSON.stringify(entries).length > 24 * 1024 * 1024) {
|
||||
delete entries[key]; missing.add(prop.item);
|
||||
}
|
||||
}
|
||||
if (missing.size) warnings.push(`No preview for ${[...missing].join(', ')}; choose an asset source in Item Props. The animdef items were preserved.`);
|
||||
return { entries, warnings };
|
||||
}
|
||||
|
||||
function writePropDefinition(definition, actor, slot, stage, mode, item) {
|
||||
const result = clone(definition), field = `prop_${slot}`;
|
||||
if (!['item','hidden','inherit'].includes(mode) || !Number.isInteger(Number(stage)) || Number(stage) < 0) throw new Error('Choose a valid prop behavior and stage.');
|
||||
@@ -4324,7 +4392,7 @@
|
||||
const selected = parseAfwStageAnimationName(getSelectedAnimation());
|
||||
const state = {
|
||||
projectId: Project.uuid, animation: selected && animationIds.includes(selected.id) ? selected.id : animationIds[0] || '',
|
||||
actor: '', slot: 'right', stage: '0', source: '', jar: '', folder: '', search: '', choice: '',
|
||||
actor: '', slot: 'right', stage: '0', source: '', jar: '', folder: '', choice: '',
|
||||
actors: [], stages: [], sources: discoverPropSources(), items: [], busy: false,
|
||||
};
|
||||
const addSource = (file) => {
|
||||
@@ -4390,11 +4458,10 @@
|
||||
folder: { label: 'Add extracted assets folder', type: 'folder', value: '',
|
||||
condition: (result) => result.mode === 'item' },
|
||||
item: { label: 'Item ID', type: 'text', value: initial.item, placeholder: 'minecraft:carrot',
|
||||
description: 'The in-game item saved to the animdef.', condition: (result) => result.mode === 'item' },
|
||||
item_search: { label: 'Search available items', type: 'text', value: '', placeholder: 'Search by item ID',
|
||||
condition: (result) => result.mode === 'item' && !!state.items.length },
|
||||
description: 'The in-game item saved to the animdef. You can also type an ID not listed below.',
|
||||
condition: (result) => result.mode === 'item' },
|
||||
item_choice: { label: 'Choose item', type: 'select', value: '',
|
||||
options: () => propItemOptions(state.items, state.search),
|
||||
options: () => ({ '': 'Select an item...', ...Object.fromEntries(state.items.map((id) => [id, id])) }),
|
||||
condition: (result) => result.mode === 'item' && !!state.items.length },
|
||||
_manual: { type: 'info', text: '### Manual texture fallback\nFor items with a special renderer, choose a PNG to preview an extruded sprite. This does not change the in-game model.',
|
||||
condition: (result) => result.mode === 'item' },
|
||||
@@ -4409,29 +4476,38 @@
|
||||
onFormChange(result) {
|
||||
const nextSource = result.jar && result.jar !== state.jar ? result.jar
|
||||
: result.folder && result.folder !== state.folder ? result.folder : result.source;
|
||||
const sourceChanged = nextSource !== state.source;
|
||||
const targetChanged = result.animation !== state.animation || result.actor !== state.actor
|
||||
|| result.slot !== state.slot || result.stage !== state.stage;
|
||||
state.jar = result.jar; state.folder = result.folder;
|
||||
if (nextSource !== state.source) {
|
||||
if (sourceChanged) {
|
||||
state.source = nextSource; addSource(nextSource);
|
||||
this.form.setValues({ source: nextSource, item_choice: '' }, false);
|
||||
state.choice = '';
|
||||
this.form.setValues({ source: nextSource, item_choice: '' }, false);
|
||||
try { indexItems(); } catch (error) { state.items = []; Blockbench.showQuickMessage(error.message, 3500); }
|
||||
}
|
||||
if (result.animation !== state.animation) {
|
||||
state.animation = result.animation; state.actor = ''; state.stage = '0'; selectAnimation();
|
||||
const previousSource = state.source, entry = savedEntry();
|
||||
this.form.setValues({ actor: state.actor, stage: state.stage, source: state.source, ...entry }, false);
|
||||
if (state.source !== previousSource) try { indexItems(); } catch (error) { state.items = []; Blockbench.showQuickMessage(error.message, 3500); }
|
||||
state.choice = '';
|
||||
this.form.setValues({ actor: state.actor, stage: state.stage, source: state.source, item_choice: '', ...entry }, false);
|
||||
if (state.source !== previousSource) {
|
||||
try { indexItems(); } catch (error) { state.items = []; Blockbench.showQuickMessage(error.message, 3500); }
|
||||
state.choice = '';
|
||||
this.form.setValues({ item_choice: '' }, false);
|
||||
}
|
||||
} else if (result.actor !== state.actor || result.slot !== state.slot || result.stage !== state.stage) {
|
||||
state.actor = result.actor; state.slot = result.slot; state.stage = result.stage;
|
||||
const previousSource = state.source, entry = savedEntry();
|
||||
this.form.setValues({ source: state.source, ...entry }, false);
|
||||
if (state.source !== previousSource) try { indexItems(); } catch (error) { state.items = []; Blockbench.showQuickMessage(error.message, 3500); }
|
||||
}
|
||||
if (result.item_search !== state.search) {
|
||||
state.search = result.item_search || '';
|
||||
state.choice = '';
|
||||
this.form.setValues({ item_choice: '' }, false);
|
||||
} else if (result.item_choice && result.item_choice !== state.choice) {
|
||||
this.form.setValues({ source: state.source, item_choice: '', ...entry }, false);
|
||||
if (state.source !== previousSource) {
|
||||
try { indexItems(); } catch (error) { state.items = []; Blockbench.showQuickMessage(error.message, 3500); }
|
||||
state.choice = '';
|
||||
this.form.setValues({ item_choice: '' }, false);
|
||||
}
|
||||
}
|
||||
if (!sourceChanged && !targetChanged && result.item_choice && result.item_choice !== state.choice) {
|
||||
state.choice = result.item_choice;
|
||||
this.form.setValues({ item: result.item_choice }, false);
|
||||
}
|
||||
@@ -4850,6 +4926,7 @@
|
||||
setProjectAnimdefAnimators(plan.definition.animators || []);
|
||||
Animator.loadFile({ name: `${plan.parsed.animationId}.animation.json`, path: '', content: JSON.stringify(plan.imported.fileObj) }, plan.imported.stageNames);
|
||||
rebuildBlockRequirementGuides(plan.parsed.animationId, plan.blockEntry, plan.definition, false);
|
||||
if (plan.propEntries) Project[PROP_PREVIEWS_PROPERTY] = JSON.stringify(plan.propEntries);
|
||||
Canvas.updateAll();
|
||||
applyAllActorTexturePreviews(false);
|
||||
refreshBlockGuideFaces();
|
||||
@@ -4858,6 +4935,7 @@
|
||||
Undo.finishEdit('Automatically import animation', finished);
|
||||
Project.saved = false;
|
||||
if (Animation.all[0]) Animation.all[0].select();
|
||||
if (plan.propEntries) void refreshPropPreviews(true);
|
||||
if (blockRequirementPanel?.vue) blockRequirementPanel.vue.load(plan.parsed.animationId);
|
||||
Blockbench.showQuickMessage(`Imported ${plan.parsed.animationId}: ${plan.rows.length} actors, ${plan.imported.stageNames.length} stages.`, 4000);
|
||||
} catch (error) {
|
||||
@@ -4982,10 +5060,12 @@
|
||||
blockEntry = readBlockRequirementForm(Object.fromEntries(Object.entries(fields).filter(([, value]) => value.type !== 'info').map(([key, value]) => [key, value.value])));
|
||||
buildBlockRequirementGuideBoxes(blockEntry);
|
||||
}
|
||||
const props = importPropPreviews(parsed.animationId, prepared, registry.packs.map((pack) => pack.root));
|
||||
warnings.push(...props.warnings);
|
||||
const review = { info: { type: 'info', text: `### Import ${parsed.animationId}\n${rows.map((row) => `${row.label}: ${row.resolved.kind}\n${row.resolved.file}`).join('\n\n')}\n\n${warnings.join('\n')}` } };
|
||||
const accepted = await importDialog('Automatic Import: Review', review, 'Create Project and Import');
|
||||
if (!accepted) return;
|
||||
commitAutomaticImport({ parsed, rows, definition: prepared, imported, blockEntry });
|
||||
commitAutomaticImport({ parsed, rows, definition: prepared, imported, blockEntry, propEntries: props.entries });
|
||||
localStorage.setItem(PACK_FOLDERS_KEY, JSON.stringify(registry.packs.map((pack) => pack.root)));
|
||||
} catch (error) {
|
||||
Blockbench.showMessageBox({ title: 'Automatically Import Animation', message: error.message, buttons: ['OK'] });
|
||||
|
||||
Reference in New Issue
Block a user