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 };\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, setTimeout(callback) { callback(); }, Undo: { initEdit() {}, finishEdit() {}, cancelEdit() {} }, Project: { texture_width: 64, texture_height: 64, box_uv: true }, Canvas: { updateAllUVs() {} }, 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('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('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); });