Files

209 lines
10 KiB
JavaScript

const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const vm = require('node:vm');
function harness() {
const source = fs.readFileSync(path.join(__dirname, '..', 'AnimationDirector.js'), 'utf8').replace(
' Plugin.register(PLUGIN_ID, {',
' globalThis.uvTest = { normalizeActorUvs, expandProjectUvs, scaleCubeUvs, getActorUvBasis, tryLoadImportedActorTexture, applyActorTexturePreview, importGeometryIntoCurrentProject, applyAllActorTexturePreviews, setProjectActorTextures, setProjectBoneTextureOverrides, restoreActorTextureMaterial };\n Plugin.register(PLUGIN_ID, {'
);
class Group {
static all = [];
constructor(data) { Object.assign(this, data); }
init() { Group.all.push(this); return this; }
addTo(parent) { this.parent = parent; return this; }
}
const cubes = [];
class Cube {
static all = cubes;
constructor(name, box = false) {
Object.assign(this, typeof name === 'object' ? name : { name });
this.box_uv = box;
this.faces = { north: { uv: [0, 0, 16, 16] } };
this.preview_controller = { updateUV(cube) {
if (!cube.box_uv || !cube.uv_offset || !cube.from || !cube.to) return;
const sizeX = cube.to[0] - cube.from[0], sizeY = cube.to[1] - cube.from[1];
const sizeZ = cube.to[2] - cube.from[2];
const [u, v] = cube.uv_offset;
cube.faces.north.uv = [u + sizeZ, v + sizeZ, u + sizeZ + sizeX, v + sizeZ + sizeY];
} };
cubes.push(this);
}
init() { return this; }
addTo(parent) { this.parent = parent; return this; }
setUVMode(box) { this.box_uv = box; }
}
class Texture {
static all = [];
constructor(data) { Object.assign(this, data); this.uuid = data.name; }
fromDataURL(url) { this.source = url; return this; }
add() { Texture.all.push(this); return this; }
}
const context = { Buffer, requireNativeModule: require, Group, Cube, Texture, console, Format: { id: 'geckolib_model' },
setTimeout(callback) { callback(); },
Undo: { initEdit() {}, finishEdit() {}, cancelEdit() {} },
Project: { texture_width: 64, texture_height: 64, box_uv: true },
Canvas: { updateAllUVs() {}, updateAll() {} }, Plugin: { register() {} },
localStorage: { getItem() { return null; } },
};
vm.runInNewContext(source, context);
return { ...context.uvTest, context, Cube, Texture };
}
test('growing the project UV canvas preserves other actors and converts box UVs', () => {
const h = harness(), actor1 = new h.Cube('actor1_body', false), actor2 = new h.Cube('actor2_body', true);
h.normalizeActorUvs(1, 64, 64);
h.normalizeActorUvs(2, 64, 64);
h.normalizeActorUvs(1, 128, 64);
assert.equal(h.context.Project.texture_width, 128);
assert.deepEqual(Array.from(actor1.faces.north.uv), [0, 0, 16, 16]);
assert.deepEqual(Array.from(actor2.faces.north.uv), [0, 0, 32, 16]);
assert.equal(actor2.box_uv, false);
h.normalizeActorUvs(1, 32, 64);
assert.deepEqual(Array.from(actor1.faces.north.uv), [0, 0, 64, 16]);
assert.deepEqual(Array.from(actor2.faces.north.uv), [0, 0, 32, 16]);
h.normalizeActorUvs(1, 32, 64);
assert.deepEqual(Array.from(actor1.faces.north.uv), [0, 0, 64, 16], 'Repeated refreshes must not rescale UVs');
});
test('preview material rebuild restores actor overrides without modifying faces or UVs', () => {
const h = harness(), cube = new h.Cube('actor1_body');
const material = {}, replacement = {};
new h.Texture({ name: 'actor.png', material }).add();
h.setProjectActorTextures({ 1: 'actor.png' });
cube.mesh = { material: replacement };
const before = JSON.stringify(cube.faces);
h.restoreActorTextureMaterial({ element: cube });
assert.equal(cube.mesh.material, material);
assert.equal(JSON.stringify(cube.faces), before);
const other = new h.Cube('actor2_body');
other.mesh = { material: replacement };
h.restoreActorTextureMaterial({ element: other });
assert.equal(other.mesh.material, replacement);
});
test('folder overrides take priority when restoring preview materials', () => {
const h = harness(), cube = new h.Cube('actor1_body');
const actorMaterial = {}, folderMaterial = {};
new h.Texture({ name: 'actor.png', material: actorMaterial }).add();
new h.Texture({ name: 'folder.png', material: folderMaterial }).add();
cube.parent = new h.context.Group({ name: 'actor1_details', uuid: 'details' });
cube.mesh = { material: {} };
h.setProjectActorTextures({ 1: 'actor.png' });
h.setProjectBoneTextureOverrides({ details: { uuid: 'details', name: 'actor1_details', texture: 'folder.png' } });
h.restoreActorTextureMaterial({ element: cube });
assert.equal(cube.mesh.material, folderMaterial);
});
test('non-GeckoLib projects keep UVs, canvas dimensions and box UV mode while still assigning textures', () => {
for (const format of ['optifine_entity', 'optifine_part', 'bedrock', 'free', 'java_block']) {
const h = harness(), cube = new h.Cube('actor1_body', true);
h.context.Format.id = format;
const before = JSON.stringify(cube.faces.north.uv);
const texture = new h.Texture({ name: 'custom.png', uv_width: 128, uv_height: 128 });
texture.add();
assert.equal(h.expandProjectUvs(128, 128), false);
assert.equal(h.normalizeActorUvs(1, 128, 128), false);
h.scaleCubeUvs(cube, 2, 2);
h.applyActorTexturePreview(1, 'custom.png');
assert.equal(JSON.stringify(cube.faces.north.uv), before);
assert.equal(cube.faces.north.texture, texture.uuid);
assert.equal(cube.box_uv, true);
assert.equal(h.context.Project.texture_width, 64);
assert.equal(h.context.Project.texture_height, 64);
assert.equal(h.context.Project.animationdirector_actor_uv_basis, undefined);
}
});
test('assigning a manually imported texture to an actor normalizes only that actor once', () => {
const h = harness(), selected = new h.Cube('actor1_body'), other = new h.Cube('actor2_body');
h.normalizeActorUvs(1, 64, 64); h.normalizeActorUvs(2, 64, 64);
const texture = new h.Texture({ name: 'custom.png' });
texture.img = { naturalWidth: 32, naturalHeight: 64 };
texture.add();
h.applyActorTexturePreview(1, 'custom.png');
assert.deepEqual(Array.from(selected.faces.north.uv), [0, 0, 32, 16]);
assert.deepEqual(Array.from(other.faces.north.uv), [0, 0, 16, 16]);
h.applyActorTexturePreview(1, 'custom.png');
assert.deepEqual(Array.from(selected.faces.north.uv), [0, 0, 32, 16]);
});
test('bone texture dimensions override the base without repeated UV scaling', () => {
const h = harness();
const group = new h.context.Group({ name: 'actor1_wind', uuid: 'wind' });
group.init();
const baseCube = new h.Cube('actor1_base'), windCube = new h.Cube('actor1_wind_cube');
windCube.addTo(group);
h.context.Project.texture_width = h.context.Project.texture_height = 128;
h.normalizeActorUvs(1, 128, 128);
for (const [name, size] of [['vanilla', 32], ['wind', 128]]) {
new h.Texture({ name, uv_width: size, uv_height: size }).add();
}
h.setProjectActorTextures({ 1: 'vanilla' });
h.setProjectBoneTextureOverrides({ wind: { uuid: 'wind', name: group.name, texture: 'wind' } });
h.applyAllActorTexturePreviews();
assert.deepEqual(Array.from(baseCube.faces.north.uv), [0, 0, 64, 64]);
assert.deepEqual(Array.from(windCube.faces.north.uv), [0, 0, 16, 16]);
assert.equal(windCube.faces.north.texture, 'wind');
h.applyAllActorTexturePreviews();
assert.deepEqual(Array.from(windCube.faces.north.uv), [0, 0, 16, 16]);
h.setProjectBoneTextureOverrides({});
h.applyAllActorTexturePreviews();
assert.deepEqual(Array.from(windCube.faces.north.uv), [0, 0, 64, 64]);
});
test('importing a larger actor preserves UV mapping of the earlier model', () => {
const h = harness();
const geometry = (width) => ({ description: { texture_width: width, texture_height: width }, bones: [{ name: 'body', cubes: [
{ origin: [0, 0, 0], size: [2, 2, 2], uv: { north: { uv: [0, 0], uv_size: [16, 16] } } },
] }] });
h.importGeometryIntoCurrentProject(geometry(64), 1, false, false);
h.importGeometryIntoCurrentProject(geometry(128), 2, false, false);
assert.equal(h.context.Project.texture_width, 128);
assert.deepEqual(Array.from(h.Cube.all[0].faces.north.uv), [0, 0, 32, 32]);
assert.deepEqual(Array.from(h.Cube.all[1].faces.north.uv), [0, 0, 16, 16]);
assert.equal(h.getActorUvBasis()['1'].width, 64);
assert.equal(h.getActorUvBasis()['2'].width, 128);
});
test('box UV import resolves face coordinates before converting to a smaller texture', () => {
const h = harness();
h.importGeometryIntoCurrentProject({ description: { texture_width: 32, texture_height: 32 }, bones: [
{ name: 'body', cubes: [{ origin: [0, 0, 0], size: [2, 3, 4], uv: [8, 4] }] },
] }, 1, false, false);
const cube = h.Cube.all[0];
assert.equal(cube.box_uv, false);
assert.deepEqual(Array.from(cube.faces.north.uv), [24, 16, 28, 22]);
});
test('box UV import keeps its original coordinates when no scaling is needed', () => {
const h = harness();
h.importGeometryIntoCurrentProject({ description: { texture_width: 64, texture_height: 64 }, bones: [
{ name: 'body', cubes: [{ origin: [0, 0, 0], size: [2, 3, 4], uv: [8, 4] }] },
] }, 1, false, false);
const cube = h.Cube.all[0];
assert.equal(cube.box_uv, true);
assert.deepEqual(Array.from(cube.faces.north.uv), [12, 8, 14, 11]);
});
test('manual model import can load an explicitly referenced texture from its extracted pack', (t) => {
const h = harness(), root = fs.mkdtempSync(path.join(os.tmpdir(), 'ad-uv-pack-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const write = (relative, contents) => { const file = path.join(root, relative);
fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, contents); return file; };
write('pack.mcmeta', '{}');
const model = write('assets/example/geckolib/models/entity/demo.geo.json', '{}');
const png = Buffer.alloc(24); Buffer.from('89504e470d0a1a0a', 'hex').copy(png); png.writeUInt32BE(32, 16); png.writeUInt32BE(64, 20);
write('assets/example/textures/entity/demo.png', png);
new h.Cube('actor1_body');
h.normalizeActorUvs(1, 64, 64);
h.tryLoadImportedActorTexture({ afw_texture: 'example:textures/entity/demo.png' }, {}, model, 1);
assert.equal(h.Texture.all.length, 1);
assert.equal(h.Texture.all[0].relative_path, 'assets/example/textures/entity/demo.png');
assert.equal(JSON.parse(h.context.Project.multiactor_actor_textures)['1'], h.Texture.all[0].name);
assert.equal(h.getActorUvBasis()['1'].width, 32);
});