improvements
This commit is contained in:
+80
-29
@@ -1304,9 +1304,7 @@
|
||||
return width > 0 && height > 0 && width <= 8192 && height <= 8192 ? { width, height } : null;
|
||||
}
|
||||
|
||||
function tryLoadImportedActorTexture(json, geo, modelPath, actorIndex) {
|
||||
let io;
|
||||
try { io = desktopFiles(); } catch (_) { return; }
|
||||
function actorTextureCandidates(json, geo, modelPath) {
|
||||
const candidateIds = [];
|
||||
const addId = (id) => { if (importResourceId(id) && !candidateIds.includes(id)) candidateIds.push(id); };
|
||||
if (typeof json.afw_texture === 'string') addId(json.afw_texture);
|
||||
@@ -1323,6 +1321,25 @@
|
||||
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;
|
||||
const sourcePaths = [];
|
||||
try { sourcePaths.push(findImportPack(modelPath, io).root); } catch (_) { /* Standalone model. */ }
|
||||
@@ -1342,12 +1359,7 @@
|
||||
let dimensions = pngDimensions(bytes);
|
||||
if (!dimensions) continue;
|
||||
try {
|
||||
const animation = propJson(source, `${relative}.mcmeta`)?.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 };
|
||||
}
|
||||
dimensions = importTextureDimensions(bytes, propJson(source, `${relative}.mcmeta`));
|
||||
} catch (_) { /* Keep the PNG dimensions when metadata is invalid. */ }
|
||||
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 };
|
||||
@@ -5020,22 +5032,53 @@
|
||||
}
|
||||
|
||||
function loadImportTextures(row, packs, io) {
|
||||
const ids = new Set(Object.values(row.json.afw_bone_textures || {}).filter((value) => typeof value === 'string'));
|
||||
if (typeof row.json.afw_texture === 'string') ids.add(row.json.afw_texture);
|
||||
else {
|
||||
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;
|
||||
const candidates = actorTextureCandidates(row.json, row.geo, row.resolved.file);
|
||||
const ids = new Set([...candidates, ...Object.values(row.json.afw_bone_textures || {})
|
||||
.filter((value) => typeof value === 'string')]);
|
||||
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) {
|
||||
const id = resource.includes(':') ? resource : `minecraft:${resource}`;
|
||||
const texture = findImportResource(packs, id, '', '', io);
|
||||
if (texture) row.textures.set(resource, texture);
|
||||
const texture = findImportResource(searchPacks, id, '', '', io);
|
||||
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) {
|
||||
@@ -5059,22 +5102,30 @@
|
||||
metadata[index] = plan.definition.actors[offset];
|
||||
importGeometryIntoCurrentProject(row.geo, index, false, false);
|
||||
for (const [name, resource] of row.textures) {
|
||||
if (!loadedTextures.has(resource.file)) {
|
||||
const texture = new Texture({ name, uv_width: row.geo.description?.texture_width || 64,
|
||||
uv_height: row.geo.description?.texture_height || 64 });
|
||||
const key = resource.file || `${resource.relative}:${name}`;
|
||||
if (!loadedTextures.has(key)) {
|
||||
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.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;
|
||||
if (resource.relative) texture.relative_path = resource.relative;
|
||||
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 || {})) {
|
||||
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,
|
||||
texture: loadedTextures.get(row.textures.get(textureName).file) };
|
||||
const boneTexture = row.textures.get(textureName);
|
||||
if (group && boneTexture) overrides[group.uuid] = { uuid: group.uuid, name: group.name,
|
||||
texture: loadedTextures.get(boneTexture.file || `${boneTexture.relative}:${textureName}`) };
|
||||
}
|
||||
}
|
||||
setProjectActorNames(names);
|
||||
|
||||
@@ -27,6 +27,7 @@ function harness(capture = false) {
|
||||
static all = [];
|
||||
constructor(data) { Object.assign(this, data); }
|
||||
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); }
|
||||
}
|
||||
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, '\\\\')));
|
||||
});
|
||||
|
||||
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) => {
|
||||
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'), {
|
||||
|
||||
Reference in New Issue
Block a user