diff --git a/AnimationDirector.js b/AnimationDirector.js index 9b602d4..4d8635d 100644 --- a/AnimationDirector.js +++ b/AnimationDirector.js @@ -72,7 +72,7 @@ let importAction, replaceActorAction, importConjoinedAnimationAction, exportConjoinedAction, createAnimationDefinitionAction, actorPreferencesAction, boneTextureOverridesAction, exportGeckoModelAction; let actorNamesProjectProperty, actorTexturesProjectProperty, actorUvBasisProjectProperty, boneTextureOverridesProjectProperty, actorAnimdefMetadataProjectProperty, animdefAnimatorsProjectProperty, animdefDefinitionsProjectProperty; let actorTexturePreviewProjectListener; - const ACTOR_TEXTURE_PREVIEW_EVENTS = ['load_project', 'select_project', 'setup_project', 'load_from_recent_project_data', 'add_texture', 'change_texture_path']; + const ACTOR_TEXTURE_PREVIEW_EVENTS = ['load_project', 'select_project', 'setup_project', 'load_from_recent_project_data', 'add_texture', 'change_texture_path', 'select_texture']; let actorTexturePreviewTimeouts = []; // ---------------------------- @@ -1964,6 +1964,23 @@ return true; } + function restoreActorTextureMaterial({ element } = {}) { + if (!Project || !element?.mesh || isBlockRequirementGuide(element)) return; + const override = getNearestBoneTextureOverride(element, getProjectBoneTextureOverrides()); + let texture = override?.texture ? findTextureByActorTextureName(override.texture) : null; + if (!texture) { + let node = element; + let actorIndex; + while (node && !actorIndex) { + actorIndex = readActorIndexFromName(node.name); + node = node.parent; + } + if (actorIndex) texture = findTextureByActorTextureName(getProjectActorTextures()[actorIndex]); + } + // Only restore the preview material; do not rebuild faces or trigger another update. + if (texture) forceActorTextureMaterial(element, texture); + } + function applyTextureToCube(cube, texture) { if (!cube || !texture) return; @@ -7221,6 +7238,7 @@ Features without a reference need no additional addon. Whole-animation import an }); actorTexturePreviewProjectListener = scheduleActorTexturePreviewApply; + Cube.preview_controller.on('setup update_faces', restoreActorTextureMaterial); for (const eventName of ACTOR_TEXTURE_PREVIEW_EVENTS) { Blockbench.on(eventName, actorTexturePreviewProjectListener); } @@ -7250,6 +7268,8 @@ Features without a reference need no additional addon. Whole-animation import an if (propListener) for (const event of ['select_animation','select_project','load_project','unselect_project','undo','redo','render_frame']) Blockbench.removeListener(event, propListener); propDialog = undefined; propListener = undefined; clearScheduledActorTexturePreviewApplies(); + Cube.preview_controller.removeListener('setup', restoreActorTextureMaterial); + Cube.preview_controller.removeListener('update_faces', restoreActorTextureMaterial); if (actorTexturePreviewProjectListener) { for (const eventName of ACTOR_TEXTURE_PREVIEW_EVENTS) { Blockbench.removeListener(eventName, actorTexturePreviewProjectListener); diff --git a/test/block-requirements.test.js b/test/block-requirements.test.js index 58ef6cd..3c6d94a 100644 --- a/test/block-requirements.test.js +++ b/test/block-requirements.test.js @@ -34,7 +34,12 @@ function harness() { class Cube { static all = []; static preview_controller = { - on(names, listener) { for (const name of names.split(' ')) previewEvents.set(name, listener); }, + on(names, listener) { + for (const name of names.split(' ')) { + const previous = previewEvents.get(name); + previewEvents.set(name, (event) => { previous?.(event); listener(event); }); + } + }, }; constructor(data) { Object.assign(this, data, { uuid: `cube-${++sequence}`, mesh: {}, diff --git a/test/uv-normalization.test.js b/test/uv-normalization.test.js index b2f916e..c05990c 100644 --- a/test/uv-normalization.test.js +++ b/test/uv-normalization.test.js @@ -8,7 +8,7 @@ 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 };\n 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 = []; @@ -69,6 +69,35 @@ test('growing the project UV canvas preserves other actors and converts box UVs' 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);