UV fixes, supporting differently sized textures
This commit is contained in:
+173
-6
@@ -2,6 +2,7 @@
|
|||||||
const PLUGIN_ID = 'AnimationDirector';
|
const PLUGIN_ID = 'AnimationDirector';
|
||||||
const ACTOR_NAMES_PROPERTY = 'multiactor_actor_names';
|
const ACTOR_NAMES_PROPERTY = 'multiactor_actor_names';
|
||||||
const ACTOR_TEXTURES_PROPERTY = 'multiactor_actor_textures';
|
const ACTOR_TEXTURES_PROPERTY = 'multiactor_actor_textures';
|
||||||
|
const ACTOR_UV_BASIS_PROPERTY = 'animationdirector_actor_uv_basis';
|
||||||
const BONE_TEXTURE_OVERRIDES_PROPERTY = 'multiactor_bone_texture_overrides';
|
const BONE_TEXTURE_OVERRIDES_PROPERTY = 'multiactor_bone_texture_overrides';
|
||||||
const ACTOR_ANIMDEF_METADATA_PROPERTY = 'multiactor_actor_animdef_metadata';
|
const ACTOR_ANIMDEF_METADATA_PROPERTY = 'multiactor_actor_animdef_metadata';
|
||||||
const ANIMDEF_ANIMATORS_PROPERTY = 'multiactor_animdef_animators';
|
const ANIMDEF_ANIMATORS_PROPERTY = 'multiactor_animdef_animators';
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
let importAction, replaceActorAction, importConjoinedAnimationAction, exportConjoinedAction, createAnimationDefinitionAction, actorPreferencesAction, boneTextureOverridesAction, exportGeckoModelAction;
|
let importAction, replaceActorAction, importConjoinedAnimationAction, exportConjoinedAction, createAnimationDefinitionAction, actorPreferencesAction, boneTextureOverridesAction, exportGeckoModelAction;
|
||||||
let actorNamesProjectProperty, actorTexturesProjectProperty, boneTextureOverridesProjectProperty, actorAnimdefMetadataProjectProperty, animdefAnimatorsProjectProperty, animdefDefinitionsProjectProperty;
|
let actorNamesProjectProperty, actorTexturesProjectProperty, actorUvBasisProjectProperty, boneTextureOverridesProjectProperty, actorAnimdefMetadataProjectProperty, animdefAnimatorsProjectProperty, animdefDefinitionsProjectProperty;
|
||||||
let actorTexturePreviewProjectListener;
|
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'];
|
||||||
let actorTexturePreviewTimeouts = [];
|
let actorTexturePreviewTimeouts = [];
|
||||||
@@ -128,6 +129,18 @@
|
|||||||
setProjectStringMap(ACTOR_TEXTURES_PROPERTY, textures);
|
setProjectStringMap(ACTOR_TEXTURES_PROPERTY, textures);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getActorUvBasis() {
|
||||||
|
try { return JSON.parse(Project?.[ACTOR_UV_BASIS_PROPERTY] || '{}'); } catch (_) { return {}; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActorUvBasis(actorIndex, width, height) {
|
||||||
|
const all = getActorUvBasis();
|
||||||
|
if (all[actorIndex]?.width === width && all[actorIndex]?.height === height) return;
|
||||||
|
all[actorIndex] = { width, height };
|
||||||
|
Project[ACTOR_UV_BASIS_PROPERTY] = JSON.stringify(all);
|
||||||
|
Project.saved = false;
|
||||||
|
}
|
||||||
|
|
||||||
function uniqueTrimmedStrings(values) {
|
function uniqueTrimmedStrings(values) {
|
||||||
const out = [];
|
const out = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
@@ -560,6 +573,53 @@
|
|||||||
cube.updateUV?.();
|
cube.updateUV?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validUvDimension(value, fallback = 64) {
|
||||||
|
return Number.isFinite(value) && value > 0 && value <= 8192 ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scaleCubeUvs(cube, x, y) {
|
||||||
|
if (x === 1 && y === 1) return;
|
||||||
|
if (cube.box_uv) {
|
||||||
|
if (typeof cube.setUVMode !== 'function') throw new Error('Box UV conversion is unavailable in this Blockbench version.');
|
||||||
|
// Blockbench derives face UVs from uv_offset only while box UV mode is active.
|
||||||
|
cube.preview_controller?.updateUV?.(cube);
|
||||||
|
cube.setUVMode(false);
|
||||||
|
Project.box_uv = false;
|
||||||
|
}
|
||||||
|
for (const face of Object.values(cube.faces || {})) {
|
||||||
|
if (!Array.isArray(face.uv) || face.uv.length !== 4) continue;
|
||||||
|
face.uv = [face.uv[0] * x, face.uv[1] * y, face.uv[2] * x, face.uv[3] * y];
|
||||||
|
}
|
||||||
|
cube.autouv = 0;
|
||||||
|
cube.preview_controller?.updateUV?.(cube);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandProjectUvs(width, height) {
|
||||||
|
const oldWidth = validUvDimension(Project.texture_width), oldHeight = validUvDimension(Project.texture_height);
|
||||||
|
const nextWidth = Math.max(oldWidth, width), nextHeight = Math.max(oldHeight, height);
|
||||||
|
if (nextWidth === oldWidth && nextHeight === oldHeight) return false;
|
||||||
|
for (const cube of Cube.all) scaleCubeUvs(cube, nextWidth / oldWidth, nextHeight / oldHeight);
|
||||||
|
Project.texture_width = nextWidth;
|
||||||
|
Project.texture_height = nextHeight;
|
||||||
|
Canvas.updateAllUVs?.();
|
||||||
|
Project.saved = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeActorUvs(actorIndex, width, height, original = null) {
|
||||||
|
width = validUvDimension(width); height = validUvDimension(height);
|
||||||
|
const basis = original || getActorUvBasis()[actorIndex] || {
|
||||||
|
width: validUvDimension(Project.texture_width), height: validUvDimension(Project.texture_height) };
|
||||||
|
const oldWidth = validUvDimension(basis.width), oldHeight = validUvDimension(basis.height);
|
||||||
|
expandProjectUvs(width, height);
|
||||||
|
const x = oldWidth / width, y = oldHeight / height;
|
||||||
|
if (x !== 1 || y !== 1) {
|
||||||
|
for (const cube of Cube.all) if (cubeBelongsToActor(cube, actorIndex)) scaleCubeUvs(cube, x, y);
|
||||||
|
}
|
||||||
|
setActorUvBasis(actorIndex, width, height);
|
||||||
|
return x !== 1 || y !== 1;
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------
|
// ----------------------------
|
||||||
// Block requirement authoring
|
// Block requirement authoring
|
||||||
// ----------------------------
|
// ----------------------------
|
||||||
@@ -1122,9 +1182,13 @@
|
|||||||
const prefix = `actor${actorIndex}_`;
|
const prefix = `actor${actorIndex}_`;
|
||||||
const previousBindings = replaceExisting ? captureActorAnimationBindings(actorIndex) : null;
|
const previousBindings = replaceExisting ? captureActorAnimationBindings(actorIndex) : null;
|
||||||
|
|
||||||
if (manageUndo) Undo.initEdit({ outliner: true, selection: true, elements: [], textures: [],
|
if (manageUndo) Undo.initEdit({ outliner: true, selection: true, elements: Cube.all.slice(), textures: [],
|
||||||
|
uv_mode: true, actor_uv_edit: true,
|
||||||
animations: previousBindings ? previousBindings.map((entry) => entry.animation) : [] });
|
animations: previousBindings ? previousBindings.map((entry) => entry.animation) : [] });
|
||||||
if (replaceExisting) removeActorGeometry(actorIndex);
|
if (replaceExisting) removeActorGeometry(actorIndex);
|
||||||
|
const uvWidth = validUvDimension(geo?.description?.texture_width);
|
||||||
|
const uvHeight = validUvDimension(geo?.description?.texture_height);
|
||||||
|
expandProjectUvs(uvWidth, uvHeight);
|
||||||
|
|
||||||
const boneByName = new Map();
|
const boneByName = new Map();
|
||||||
for (const b of bones) if (b?.name) boneByName.set(b.name, b);
|
for (const b of bones) if (b?.name) boneByName.set(b.name, b);
|
||||||
@@ -1198,6 +1262,7 @@
|
|||||||
if (Array.isArray(c?.uv)) {
|
if (Array.isArray(c?.uv)) {
|
||||||
cube.box_uv = true;
|
cube.box_uv = true;
|
||||||
cube.uv_offset = [num(c.uv[0], 0), num(c.uv[1], 0)];
|
cube.uv_offset = [num(c.uv[0], 0), num(c.uv[1], 0)];
|
||||||
|
cube.preview_controller?.updateUV?.(cube);
|
||||||
} else if (c?.uv && typeof c.uv === 'object') {
|
} else if (c?.uv && typeof c.uv === 'object') {
|
||||||
cube.box_uv = false;
|
cube.box_uv = false;
|
||||||
applyPerFaceUVWithXFlip(cube, c.uv);
|
applyPerFaceUVWithXFlip(cube, c.uv);
|
||||||
@@ -1205,6 +1270,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
normalizeActorUvs(actorIndex, uvWidth, uvHeight, {
|
||||||
|
width: validUvDimension(Project.texture_width), height: validUvDimension(Project.texture_height) });
|
||||||
|
|
||||||
const animationResult = replaceExisting
|
const animationResult = replaceExisting
|
||||||
? rebindActorAnimations(actorIndex, previousBindings)
|
? rebindActorAnimations(actorIndex, previousBindings)
|
||||||
: { rebound: 0, missing: 0 };
|
: { rebound: 0, missing: 0 };
|
||||||
@@ -1230,6 +1298,82 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pngDimensions(bytes) {
|
||||||
|
if (!bytes || bytes.length < 24 || bytes.toString('hex', 0, 8) !== '89504e470d0a1a0a') return null;
|
||||||
|
const width = bytes.readUInt32BE(16), height = bytes.readUInt32BE(20);
|
||||||
|
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; }
|
||||||
|
const candidateIds = [];
|
||||||
|
const addId = (id) => { if (importResourceId(id) && !candidateIds.includes(id)) candidateIds.push(id); };
|
||||||
|
if (typeof json.afw_texture === 'string') addId(json.afw_texture);
|
||||||
|
const path = String(modelPath || '').replace(/\\/g, '/');
|
||||||
|
const namespace = /\/assets\/([^/]+)\//.exec(path)?.[1] || 'minecraft';
|
||||||
|
const identifier = String(geo?.description?.identifier || '').split('.').pop();
|
||||||
|
const filename = path.split('/').pop()?.replace(/(?:\.(?:m|f|mf|m\+f))?\.geo\.json$/i, '') || '';
|
||||||
|
for (const entity of [...new Set([identifier, filename])]) {
|
||||||
|
if (!/^[a-z0-9_]+$/.test(entity)) continue;
|
||||||
|
addId(`${namespace}:textures/entity/${entity}/${entity}.png`);
|
||||||
|
addId(`${namespace}:textures/entity/${entity}.png`);
|
||||||
|
if (namespace !== 'minecraft') {
|
||||||
|
addId(`minecraft:textures/entity/${entity}/${entity}.png`);
|
||||||
|
addId(`minecraft:textures/entity/${entity}.png`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!candidateIds.length) return;
|
||||||
|
const sourcePaths = [];
|
||||||
|
try { sourcePaths.push(findImportPack(modelPath, io).root); } catch (_) { /* Standalone model. */ }
|
||||||
|
try { sourcePaths.push(...savedPackFolders()); } catch (_) { /* No registered packs. */ }
|
||||||
|
try { sourcePaths.push(...discoverPropSources(io).map((source) => source.file)); } catch (_) { /* No cached assets. */ }
|
||||||
|
const sources = [];
|
||||||
|
for (const sourcePath of [...new Set(sourcePaths)]) {
|
||||||
|
try { sources.push(openPropSource(sourcePath, io)); } catch (_) { /* Try other asset sources. */ }
|
||||||
|
}
|
||||||
|
for (const id of candidateIds) {
|
||||||
|
for (const source of sources) {
|
||||||
|
const parsed = importResourceId(id);
|
||||||
|
const relative = `assets/${parsed.namespace}/${parsed.resource}`;
|
||||||
|
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 {
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
} 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 };
|
||||||
|
Undo.initEdit(edit);
|
||||||
|
try {
|
||||||
|
const name = `actor${actorIndex}_${parsed.namespace}_${parsed.resource.split('/').pop()}`;
|
||||||
|
const texture = new Texture({ name, uv_width: dimensions.width, uv_height: dimensions.height });
|
||||||
|
texture.keep_size = true;
|
||||||
|
const projectId = Project.uuid;
|
||||||
|
texture.load_callback = () => { if (Project?.uuid === projectId) scheduleActorTexturePreviewApply(); };
|
||||||
|
texture.fromDataURL(`data:image/png;base64,${bytes.toString('base64')}`);
|
||||||
|
texture.name = name;
|
||||||
|
texture.relative_path = relative;
|
||||||
|
texture.add(false);
|
||||||
|
const selected = getProjectActorTextures();
|
||||||
|
selected[actorIndex] = name;
|
||||||
|
setProjectActorTextures(selected);
|
||||||
|
normalizeActorUvs(actorIndex, dimensions.width, dimensions.height);
|
||||||
|
applyActorTexturePreview(actorIndex, name);
|
||||||
|
Undo.finishEdit(`Load Actor ${actorIndex} Texture`, { ...edit, elements: Cube.all.slice(), textures: Texture.all.slice() });
|
||||||
|
return;
|
||||||
|
} catch (error) { Undo.cancelEdit(); throw error; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function importActorGeckoJson() {
|
function importActorGeckoJson() {
|
||||||
if (!Project) {
|
if (!Project) {
|
||||||
Blockbench.showQuickMessage('No project open. Create/open a project first.', 2500);
|
Blockbench.showQuickMessage('No project open. Create/open a project first.', 2500);
|
||||||
@@ -1257,7 +1401,9 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
importGeometryIntoCurrentProject(geo, getNextActorIndex());
|
const actorIndex = getNextActorIndex();
|
||||||
|
importGeometryIntoCurrentProject(geo, actorIndex);
|
||||||
|
tryLoadImportedActorTexture(json, geo, files[0]?.path, actorIndex);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
Blockbench.showQuickMessage(`Import failed: ${e?.message || e}`, 4000);
|
Blockbench.showQuickMessage(`Import failed: ${e?.message || e}`, 4000);
|
||||||
@@ -1323,6 +1469,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
importGeometryIntoCurrentProject(geo, actorIndex, true);
|
importGeometryIntoCurrentProject(geo, actorIndex, true);
|
||||||
|
tryLoadImportedActorTexture(json, geo, files[0]?.path, actorIndex);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
Blockbench.showQuickMessage(`Replacement failed: ${e?.message || e}`, 4000);
|
Blockbench.showQuickMessage(`Replacement failed: ${e?.message || e}`, 4000);
|
||||||
@@ -1452,6 +1599,7 @@
|
|||||||
showAnimdefMetadata = !!result?.show_animdef_metadata;
|
showAnimdefMetadata = !!result?.show_animdef_metadata;
|
||||||
},
|
},
|
||||||
onConfirm: (result) => {
|
onConfirm: (result) => {
|
||||||
|
let editing = false;
|
||||||
try {
|
try {
|
||||||
const outNames = {};
|
const outNames = {};
|
||||||
const outTextures = {};
|
const outTextures = {};
|
||||||
@@ -1483,13 +1631,18 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const aspects = { elements: Cube.all.slice(), uv_mode: true, actor_preferences_edit: true };
|
||||||
|
Undo.initEdit(aspects);
|
||||||
|
editing = true;
|
||||||
setProjectActorNames(outNames);
|
setProjectActorNames(outNames);
|
||||||
setProjectActorTextures(outTextures);
|
setProjectActorTextures(outTextures);
|
||||||
setProjectActorAnimdefMetadata(outAnimdefMetadata);
|
setProjectActorAnimdefMetadata(outAnimdefMetadata);
|
||||||
setProjectBoneTextureOverrides(outBoneTextureOverrides);
|
setProjectBoneTextureOverrides(outBoneTextureOverrides);
|
||||||
applyAllActorTexturePreviews(true);
|
applyAllActorTexturePreviews(true);
|
||||||
|
Undo.finishEdit('Edit Actor Preferences', { ...aspects, elements: Cube.all.slice() });
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (editing) Undo.cancelEdit();
|
||||||
Blockbench.showMessageBox({
|
Blockbench.showMessageBox({
|
||||||
title: 'Actor Preferences',
|
title: 'Actor Preferences',
|
||||||
message: e?.message || String(e),
|
message: e?.message || String(e),
|
||||||
@@ -1701,6 +1854,10 @@
|
|||||||
const texture = findTextureByActorTextureName(textureName);
|
const texture = findTextureByActorTextureName(textureName);
|
||||||
if (!texture) return { applied: 0, missing: true };
|
if (!texture) return { applied: 0, missing: true };
|
||||||
|
|
||||||
|
const imageWidth = texture.img?.naturalWidth || texture.canvas?.width || 0;
|
||||||
|
const imageHeight = texture.display_height || texture.img?.naturalHeight || texture.canvas?.height || 0;
|
||||||
|
if (imageWidth > 0 && imageHeight > 0) normalizeActorUvs(actorIndex, imageWidth, imageHeight);
|
||||||
|
|
||||||
let applied = 0;
|
let applied = 0;
|
||||||
const cubes = Array.isArray(Cube.all) ? Cube.all : [];
|
const cubes = Array.isArray(Cube.all) ? Cube.all : [];
|
||||||
for (const cube of cubes) {
|
for (const cube of cubes) {
|
||||||
@@ -3833,7 +3990,8 @@
|
|||||||
// Automatic import reads only explicitly registered, extracted packs. No network access.
|
// Automatic import reads only explicitly registered, extracted packs. No network access.
|
||||||
const PACK_FOLDERS_KEY = 'animationdirector_pack_folders';
|
const PACK_FOLDERS_KEY = 'animationdirector_pack_folders';
|
||||||
const IMPORT_METADATA = [ACTOR_NAMES_PROPERTY, ACTOR_TEXTURES_PROPERTY, BONE_TEXTURE_OVERRIDES_PROPERTY,
|
const IMPORT_METADATA = [ACTOR_NAMES_PROPERTY, ACTOR_TEXTURES_PROPERTY, BONE_TEXTURE_OVERRIDES_PROPERTY,
|
||||||
ACTOR_ANIMDEF_METADATA_PROPERTY, ANIMDEF_ANIMATORS_PROPERTY, ANIMDEF_DEFINITIONS_PROPERTY, BLOCK_REQUIREMENTS_PROPERTY, PROP_PREVIEWS_PROPERTY];
|
ACTOR_ANIMDEF_METADATA_PROPERTY, ANIMDEF_ANIMATORS_PROPERTY, ANIMDEF_DEFINITIONS_PROPERTY, BLOCK_REQUIREMENTS_PROPERTY,
|
||||||
|
PROP_PREVIEWS_PROPERTY, ACTOR_UV_BASIS_PROPERTY];
|
||||||
|
|
||||||
// Item previews are private Three.js attachments, never actor geometry or animation tracks.
|
// Item previews are private Three.js attachments, never actor geometry or animation tracks.
|
||||||
function propResourcePath(id, kind, extension) {
|
function propResourcePath(id, kind, extension) {
|
||||||
@@ -4885,7 +5043,7 @@
|
|||||||
cancelBlockRequirementUpdate();
|
cancelBlockRequirementUpdate();
|
||||||
if (!newProject(Formats.geckolib_model)) throw new Error('Could not create the GeckoLib project.');
|
if (!newProject(Formats.geckolib_model)) throw new Error('Could not create the GeckoLib project.');
|
||||||
const projectId = Project.uuid;
|
const projectId = Project.uuid;
|
||||||
const aspects = { outliner: true, selection: true, elements: [], groups: [], textures: [], animations: [],
|
const aspects = { outliner: true, selection: true, elements: [], groups: [], textures: [], animations: [], uv_mode: true,
|
||||||
automatic_animation_import: true };
|
automatic_animation_import: true };
|
||||||
Undo.initEdit(aspects);
|
Undo.initEdit(aspects);
|
||||||
try {
|
try {
|
||||||
@@ -5096,6 +5254,9 @@ You can import or export complete AFW animation IDs as one file containing all s
|
|||||||
default: '{}',
|
default: '{}',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (!ModelProject.properties[ACTOR_UV_BASIS_PROPERTY]) {
|
||||||
|
actorUvBasisProjectProperty = new Property(ModelProject, 'string', ACTOR_UV_BASIS_PROPERTY, { default: '{}' });
|
||||||
|
}
|
||||||
if (!ModelProject.properties[BONE_TEXTURE_OVERRIDES_PROPERTY]) {
|
if (!ModelProject.properties[BONE_TEXTURE_OVERRIDES_PROPERTY]) {
|
||||||
boneTextureOverridesProjectProperty = new Property(ModelProject, 'string', BONE_TEXTURE_OVERRIDES_PROPERTY, {
|
boneTextureOverridesProjectProperty = new Property(ModelProject, 'string', BONE_TEXTURE_OVERRIDES_PROPERTY, {
|
||||||
default: '{}',
|
default: '{}',
|
||||||
@@ -5136,6 +5297,12 @@ You can import or export complete AFW animation IDs as one file containing all s
|
|||||||
if (aspects.block_requirement_edit) save[BLOCK_REQUIREMENTS_PROPERTY] = Project[BLOCK_REQUIREMENTS_PROPERTY] || '{}';
|
if (aspects.block_requirement_edit) save[BLOCK_REQUIREMENTS_PROPERTY] = Project[BLOCK_REQUIREMENTS_PROPERTY] || '{}';
|
||||||
if (aspects.animdef_edit) save[ANIMDEF_DEFINITIONS_PROPERTY] = Project[ANIMDEF_DEFINITIONS_PROPERTY] || '{}';
|
if (aspects.animdef_edit) save[ANIMDEF_DEFINITIONS_PROPERTY] = Project[ANIMDEF_DEFINITIONS_PROPERTY] || '{}';
|
||||||
if (aspects.prop_preview_edit) save[PROP_PREVIEWS_PROPERTY] = Project[PROP_PREVIEWS_PROPERTY] || '{}';
|
if (aspects.prop_preview_edit) save[PROP_PREVIEWS_PROPERTY] = Project[PROP_PREVIEWS_PROPERTY] || '{}';
|
||||||
|
if (aspects.actor_uv_edit) save[ACTOR_UV_BASIS_PROPERTY] = Project[ACTOR_UV_BASIS_PROPERTY] || '{}';
|
||||||
|
if (aspects.actor_texture_edit) save[ACTOR_TEXTURES_PROPERTY] = Project[ACTOR_TEXTURES_PROPERTY] || '{}';
|
||||||
|
if (aspects.actor_preferences_edit) {
|
||||||
|
for (const property of [ACTOR_NAMES_PROPERTY, ACTOR_TEXTURES_PROPERTY, ACTOR_ANIMDEF_METADATA_PROPERTY,
|
||||||
|
BONE_TEXTURE_OVERRIDES_PROPERTY, ACTOR_UV_BASIS_PROPERTY]) save[property] = Project[property] || '{}';
|
||||||
|
}
|
||||||
if (aspects.automatic_animation_import) {
|
if (aspects.automatic_animation_import) {
|
||||||
for (const property of IMPORT_METADATA) save[property] = Project[property] ?? (property === ANIMDEF_ANIMATORS_PROPERTY ? '' : '{}');
|
for (const property of IMPORT_METADATA) save[property] = Project[property] ?? (property === ANIMDEF_ANIMATORS_PROPERTY ? '' : '{}');
|
||||||
}
|
}
|
||||||
@@ -5279,7 +5446,7 @@ You can import or export complete AFW animation IDs as one file containing all s
|
|||||||
},
|
},
|
||||||
onunload() {
|
onunload() {
|
||||||
clearPropMeshes(); propSources.clear();
|
clearPropMeshes(); propSources.clear();
|
||||||
propAction?.delete(); propDialog?.delete(); propProjectProperty?.delete();
|
propAction?.delete(); propDialog?.delete(); propProjectProperty?.delete(); actorUvBasisProjectProperty?.delete();
|
||||||
if (propListener) for (const event of ['select_animation','select_project','load_project','unselect_project','undo','redo','render_frame']) Blockbench.removeListener(event, propListener);
|
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;
|
propDialog = undefined; propListener = undefined;
|
||||||
clearScheduledActorTexturePreviewApplies();
|
clearScheduledActorTexturePreviewApplies();
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user