improvements

This commit is contained in:
2026-09-29 00:10:46 +02:00
parent d04f26465f
commit 8cfdf0ecec
2 changed files with 114 additions and 29 deletions
+80 -29
View File
@@ -1304,9 +1304,7 @@
return width > 0 && height > 0 && width <= 8192 && height <= 8192 ? { width, height } : null; return width > 0 && height > 0 && width <= 8192 && height <= 8192 ? { width, height } : null;
} }
function tryLoadImportedActorTexture(json, geo, modelPath, actorIndex) { function actorTextureCandidates(json, geo, modelPath) {
let io;
try { io = desktopFiles(); } catch (_) { return; }
const candidateIds = []; const candidateIds = [];
const addId = (id) => { if (importResourceId(id) && !candidateIds.includes(id)) candidateIds.push(id); }; const addId = (id) => { if (importResourceId(id) && !candidateIds.includes(id)) candidateIds.push(id); };
if (typeof json.afw_texture === 'string') addId(json.afw_texture); if (typeof json.afw_texture === 'string') addId(json.afw_texture);
@@ -1323,6 +1321,25 @@
addId(`minecraft:textures/entity/${entity}.png`); addId(`minecraft:textures/entity/${entity}.png`);
} }
} }
return candidateIds;
}
function importTextureDimensions(bytes, metadata = null) {
let dimensions = pngDimensions(bytes);
if (!dimensions) return null;
const animation = metadata?.animation;
if (animation) {
const width = validUvDimension(animation.width, dimensions.width);
const height = validUvDimension(animation.height, width);
if (width <= dimensions.width && height <= dimensions.height) dimensions = { width, height };
}
return dimensions;
}
function tryLoadImportedActorTexture(json, geo, modelPath, actorIndex) {
let io;
try { io = desktopFiles(); } catch (_) { return; }
const candidateIds = actorTextureCandidates(json, geo, modelPath);
if (!candidateIds.length) return; if (!candidateIds.length) return;
const sourcePaths = []; const sourcePaths = [];
try { sourcePaths.push(findImportPack(modelPath, io).root); } catch (_) { /* Standalone model. */ } try { sourcePaths.push(findImportPack(modelPath, io).root); } catch (_) { /* Standalone model. */ }
@@ -1342,12 +1359,7 @@
let dimensions = pngDimensions(bytes); let dimensions = pngDimensions(bytes);
if (!dimensions) continue; if (!dimensions) continue;
try { try {
const animation = propJson(source, `${relative}.mcmeta`)?.animation; dimensions = importTextureDimensions(bytes, propJson(source, `${relative}.mcmeta`));
if (animation) {
const width = validUvDimension(animation.width, dimensions.width);
const height = validUvDimension(animation.height, width);
if (width <= dimensions.width && height <= dimensions.height) dimensions = { width, height };
}
} catch (_) { /* Keep the PNG dimensions when metadata is invalid. */ } } catch (_) { /* Keep the PNG dimensions when metadata is invalid. */ }
const edit = { elements: Cube.all.filter((cube) => cubeBelongsToActor(cube, actorIndex)), const edit = { elements: Cube.all.filter((cube) => cubeBelongsToActor(cube, actorIndex)),
textures: Texture.all.slice(), uv_mode: true, actor_uv_edit: true, actor_texture_edit: true }; textures: Texture.all.slice(), uv_mode: true, actor_uv_edit: true, actor_texture_edit: true };
@@ -5020,22 +5032,53 @@
} }
function loadImportTextures(row, packs, io) { function loadImportTextures(row, packs, io) {
const ids = new Set(Object.values(row.json.afw_bone_textures || {}).filter((value) => typeof value === 'string')); const candidates = actorTextureCandidates(row.json, row.geo, row.resolved.file);
if (typeof row.json.afw_texture === 'string') ids.add(row.json.afw_texture); const ids = new Set([...candidates, ...Object.values(row.json.afw_bone_textures || {})
else { .filter((value) => typeof value === 'string')]);
const entity = importResourceId(row.entity);
for (const resource of [`${entity.namespace}:textures/entity/${entity.resource}/${entity.resource}.png`,
`${entity.namespace}:textures/entity/${entity.resource}.png`]) {
if (findImportResource(packs, resource, '', '', io)) { row.defaultTexture = resource; ids.add(resource); break; }
}
}
if (typeof row.json.afw_texture === 'string') row.defaultTexture = row.json.afw_texture;
row.textures = new Map(); row.textures = new Map();
let cachedSources;
let modelPack;
try { modelPack = findImportPack(row.resolved.file, io); } catch (_) { /* Standalone model. */ }
const searchPacks = modelPack && !packs.some((pack) => pack.root === modelPack.root)
? [modelPack, ...packs] : packs;
for (const resource of ids) { for (const resource of ids) {
const id = resource.includes(':') ? resource : `minecraft:${resource}`; const id = resource.includes(':') ? resource : `minecraft:${resource}`;
const texture = findImportResource(packs, id, '', '', io); const texture = findImportResource(searchPacks, id, '', '', io);
if (texture) row.textures.set(resource, texture); if (texture) {
let dimensions = null;
try {
if (io.fs.statSync(texture.file).size <= 4 * 1024 * 1024) {
const bytes = io.fs.readFileSync(texture.file);
let metadata = null;
try { metadata = readImportJson(`${texture.file}.mcmeta`, io); } catch (_) { /* Static texture. */ }
dimensions = importTextureDimensions(bytes, metadata);
}
} catch (_) { /* Let Blockbench report unreadable texture files. */ }
row.textures.set(resource, { ...texture, dimensions });
continue;
}
if (!cachedSources) {
cachedSources = [];
for (const entry of discoverPropSources(io)) {
try { cachedSources.push(openPropSource(entry.file, io)); } catch (_) { /* Try another asset source. */ }
}
}
const parsed = importResourceId(id);
if (!parsed) continue;
const relative = `assets/${parsed.namespace}/${parsed.resource}`;
for (const source of cachedSources) {
let bytes;
try { bytes = source.read(relative); } catch (_) { continue; }
if (!bytes || bytes.length > 4 * 1024 * 1024) continue;
let dimensions = pngDimensions(bytes);
if (!dimensions) continue;
try { dimensions = importTextureDimensions(bytes, propJson(source, `${relative}.mcmeta`)); }
catch (_) { /* Keep the PNG dimensions when metadata is invalid. */ }
row.textures.set(resource, { bytes, relative, dimensions });
break;
}
} }
row.defaultTexture = candidates.find((id) => row.textures.has(id));
} }
function commitAutomaticImport(plan) { function commitAutomaticImport(plan) {
@@ -5059,22 +5102,30 @@
metadata[index] = plan.definition.actors[offset]; metadata[index] = plan.definition.actors[offset];
importGeometryIntoCurrentProject(row.geo, index, false, false); importGeometryIntoCurrentProject(row.geo, index, false, false);
for (const [name, resource] of row.textures) { for (const [name, resource] of row.textures) {
if (!loadedTextures.has(resource.file)) { const key = resource.file || `${resource.relative}:${name}`;
const texture = new Texture({ name, uv_width: row.geo.description?.texture_width || 64, if (!loadedTextures.has(key)) {
uv_height: row.geo.description?.texture_height || 64 }); const texture = new Texture({ name, uv_width: resource.dimensions?.width || row.geo.description?.texture_width || 64,
uv_height: resource.dimensions?.height || row.geo.description?.texture_height || 64 });
texture.keep_size = true; texture.keep_size = true;
texture.load_callback = () => { if (Project?.uuid === projectId) scheduleActorTexturePreviewApply(); }; texture.load_callback = () => { if (Project?.uuid === projectId) scheduleActorTexturePreviewApply(); };
texture.fromPath(resource.file); if (resource.file) texture.fromPath(resource.file);
else texture.fromDataURL(`data:image/png;base64,${resource.bytes.toString('base64')}`);
texture.name = name; texture.name = name;
if (resource.relative) texture.relative_path = resource.relative;
texture.add(false); texture.add(false);
loadedTextures.set(resource.file, name); loadedTextures.set(key, name);
} }
} }
if (row.textures.has(row.defaultTexture)) textures[index] = loadedTextures.get(row.textures.get(row.defaultTexture).file); const base = row.textures.get(row.defaultTexture);
if (base) {
textures[index] = loadedTextures.get(base.file || `${base.relative}:${row.defaultTexture}`);
if (base.dimensions) normalizeActorUvs(index, base.dimensions.width, base.dimensions.height);
}
for (const [bone, textureName] of Object.entries(row.json.afw_bone_textures || {})) { for (const [bone, textureName] of Object.entries(row.json.afw_bone_textures || {})) {
const group = Group.all.find((candidate) => candidate.name === `actor${index}_${bone}`); const group = Group.all.find((candidate) => candidate.name === `actor${index}_${bone}`);
if (group && row.textures.has(textureName)) overrides[group.uuid] = { uuid: group.uuid, name: group.name, const boneTexture = row.textures.get(textureName);
texture: loadedTextures.get(row.textures.get(textureName).file) }; if (group && boneTexture) overrides[group.uuid] = { uuid: group.uuid, name: group.name,
texture: loadedTextures.get(boneTexture.file || `${boneTexture.relative}:${textureName}`) };
} }
} }
setProjectActorNames(names); setProjectActorNames(names);
+34
View File
@@ -27,6 +27,7 @@ function harness(capture = false) {
static all = []; static all = [];
constructor(data) { Object.assign(this, data); } constructor(data) { Object.assign(this, data); }
fromPath(file) { this.path = file; this.name = path.basename(file); return this; } fromPath(file) { this.path = file; this.name = path.basename(file); return this; }
fromDataURL(url) { this.source = url; return this; }
add(undo) { assert.equal(undo, false); Texture.all.push(this); } add(undo) { assert.equal(undo, false); Texture.all.push(this); }
} }
const context = { const context = {
@@ -272,6 +273,39 @@ test('complete import prepares all stages, actor models, local textures and defi
assert.ok(h.storage.get('animationdirector_pack_folders').includes(source.root.replace(/\\/g, '\\\\'))); assert.ok(h.storage.get('animationdirector_pack_folders').includes(source.root.replace(/\\/g, '\\\\')));
}); });
test('complete import finds model texture in cached assets and uses its PNG dimensions', async (t) => {
const f = fixtures(t), h = harness(true), source = f.makePack('source');
const defFile = f.write(path.join(source.root, 'data/test/afw_animdefs/example.json'), {
actors: [{ label: 'wolf', entity_types: ['minecraft:wolf'] }], stages: [{ stage: 1 }],
});
f.write(path.join(source.root, 'assets/test/animations/afw/example.animation.json'), {
animations: { p1_wolf: { animation_length: 1, bones: { body: { rotation: [0, 0, 0] } } } },
});
f.model(source, 'test:source/entity/wolf.m');
const cache = path.join(f.root, 'cached-assets');
const image = path.join(cache, 'assets/test/textures/entity/wolf.png');
fs.mkdirSync(path.dirname(image), { recursive: true });
const png = Buffer.alloc(24);
Buffer.from('89504e470d0a1a0a', 'hex').copy(png);
png.writeUInt32BE(32, 16); png.writeUInt32BE(64, 20);
fs.writeFileSync(image, png);
h.storage.set('animationdirector_prop_source', cache);
h.context.selectedFile = { path: defFile };
await h.automaticallyImportAnimation();
assert.deepEqual(h.errors, []);
const row = h.context.importedPlan.rows[0];
assert.equal(row.defaultTexture, 'test:textures/entity/wolf.png');
assert.equal(row.textures.get(row.defaultTexture).dimensions.width, 32);
assert.equal(row.textures.get(row.defaultTexture).dimensions.height, 64);
const importing = harness();
importing.commitAutomaticImport(h.context.importedPlan);
assert.equal(importing.context.Texture.all[0].uv_width, 32);
assert.equal(importing.context.Texture.all[0].uv_height, 64);
assert.equal(JSON.parse(importing.context.Project.animationdirector_actor_uv_basis)['1'].width, 32);
assert.equal(JSON.parse(importing.context.Project.multiactor_actor_textures)['1'], row.defaultTexture);
});
test('definition actors exclude extra clips, their effects and stages containing only unused actors', async (t) => { test('definition actors exclude extra clips, their effects and stages containing only unused actors', async (t) => {
const f = fixtures(t), h = harness(true), source = f.makePack('source'); const f = fixtures(t), h = harness(true), source = f.makePack('source');
const file = f.write(path.join(source.root, 'data/test/afw_animdefs/example.json'), { const file = f.write(path.join(source.root, 'data/test/afw_animdefs/example.json'), {