block requirements

This commit is contained in:
2026-09-28 18:56:59 +02:00
parent f1f597dcca
commit 8764af4c1a
+381 -4
View File
@@ -6,6 +6,12 @@
const ACTOR_ANIMDEF_METADATA_PROPERTY = 'multiactor_actor_animdef_metadata';
const ANIMDEF_ANIMATORS_PROPERTY = 'multiactor_animdef_animators';
const ANIMDEF_DEFINITIONS_PROPERTY = 'animationdirector_animdef_definitions';
const BLOCK_REQUIREMENTS_PROPERTY = 'animationdirector_block_requirements';
const BLOCK_GUIDE_PREFIX = '__ad_block_requirement_';
const BLOCK_GUIDE_COLORS = { required: 0x489dff, clearance: 0xff5555 };
const blockGuideMaterials = new Map();
let blockRequirementAction, blockRequirementsProjectProperty, blockGuidePreviewListener;
let blockRequirementUndoSaveListener, blockRequirementUndoLoadListener;
const SYSTEM_TAG_OPTIONS = {
'': 'None (normal animation)',
manualpeak: 'Manual peak',
@@ -551,6 +557,316 @@
cube.updateUV?.();
}
// ----------------------------
// Block requirement authoring
// ----------------------------
function getBlockRequirementEntries() {
try {
const entries = JSON.parse(Project?.[BLOCK_REQUIREMENTS_PROPERTY] || '{}');
return entries && typeof entries === 'object' && !Array.isArray(entries) ? entries : {};
} catch (e) {
return {};
}
}
function getAuthoredBlockRequirement(animationId) {
return getBlockRequirementEntries()[animationId] || null;
}
function blockRequirementInteger(value, label, minimum = 1) {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < minimum || parsed > 32) {
throw new Error(`${label} must be a whole number between ${minimum} and 32.`);
}
return parsed;
}
function readBlockRequirementForm(result) {
const blocks = uniqueTrimmedStrings(parseCommaSeparatedStrings(result.blocks));
for (const block of blocks) {
if (!/^#?[a-z0-9_.-]+:[a-z0-9/._-]+$/.test(block)) {
throw new Error(`Invalid block ID/tag "${block}". Use minecraft:stone or #minecraft:fences.`);
}
}
const requirement = { type: result.type };
if (blocks.length) requirement.blocks = blocks.length === 1 ? blocks[0] : blocks;
if (result.type === 'wall') {
const minHeight = blockRequirementInteger(result.height_min, 'Minimum height');
const minWidth = blockRequirementInteger(result.width_min, 'Minimum width');
const maxHeight = String(result.height_max ?? '').trim()
? blockRequirementInteger(result.height_max, 'Maximum height') : null;
const maxWidth = String(result.width_max ?? '').trim()
? blockRequirementInteger(result.width_max, 'Maximum width') : null;
if (maxHeight !== null && maxHeight < minHeight) throw new Error('Maximum height must be at least the minimum height.');
if (maxWidth !== null && maxWidth < minWidth) throw new Error('Maximum width must be at least the minimum width.');
requirement.height = { min: minHeight };
if (maxHeight !== null) requirement.height.max = maxHeight;
requirement.width = { min: minWidth };
if (maxWidth !== null) {
requirement.width.max = maxWidth;
if (result.both_axes) requirement.width.both_axes = true;
if (result.include_obstructions) requirement.width.include_obstructions = true;
}
if (result.facing) {
if (!blocks.length) throw new Error('A facing requirement needs a block ID or tag.');
requirement.facing = result.facing;
}
} else if (result.type === 'center_support') {
requirement.support = result.support;
if (result.support === 'surface' && !blocks.length) throw new Error('Custom surface support requires a block ID or tag.');
requirement.placement = result.placement;
if (result.placement === 'surface_footprint') {
requirement.surface_footprint = {
width: blockRequirementInteger(result.footprint_width, 'Footprint width'),
depth: blockRequirementInteger(result.footprint_depth, 'Footprint depth'),
height: blockRequirementInteger(result.footprint_height, 'Footprint clearance height'),
margin: blockRequirementInteger(result.footprint_margin, 'Footprint margin', 0),
};
} else {
requirement.surface_radius = blockRequirementInteger(result.surface_radius, 'Surface radius', 0);
}
} else {
throw new Error('Choose a wall or center support requirement.');
}
if (requirement.placement !== 'surface_footprint') {
requirement.clearance = {
width: blockRequirementInteger(result.clearance_width, 'Clearance width'),
height: blockRequirementInteger(result.clearance_height, 'Clearance height'),
depth: blockRequirementInteger(result.clearance_depth, 'Clearance depth'),
};
}
const wallThickness = Number(result.wall_thickness ?? 1);
const surfaceHeight = Number(result.surface_height ?? 1);
if (!Number.isFinite(wallThickness) || wallThickness <= 0 || wallThickness > 1) throw new Error('Preview wall thickness must be above 0 and at most 1 block.');
if (!Number.isFinite(surfaceHeight) || surfaceHeight <= 0 || surfaceHeight > 2) throw new Error('Preview surface height must be above 0 and at most 2 blocks.');
return { requirement, preview: { wallThickness, surfaceHeight } };
}
function buildBlockRequirementGuideBoxes(entry) {
const req = entry.requirement;
const boxes = [];
const add = (kind, from, to, label) => {
if (boxes.length >= 4096) throw new Error('This preview would exceed 4096 guide cubes. Reduce its dimensions.');
boxes.push({ kind, from: from.map((value) => value * 16), to: to.map((value) => value * 16), label });
};
const cell = (kind, x, y, z, height = 1, label = kind) =>
add(kind, [x, y, z], [x + 1, y + height, z + 1], label);
const clearance = req.clearance;
if (req.type === 'wall') {
const width = req.width.min;
const thickness = entry.preview?.wallThickness ?? 1;
// NoN places the anchor half a block away from the wall's collision surface.
const gridZ = -1 - thickness / 2;
const left = -width / 2;
for (let x = 0; x < width; x++) {
for (let y = 0; y < req.height.min; y++) {
add('required', [left + x, y, -0.5 - thickness], [left + x + 1, y + 1, -0.5], 'wall');
}
cell('required', left + x, -1, gridZ, 1, 'floor');
if (req.height.max === req.height.min) cell('clearance', left + x, req.height.max, gridZ, 1, 'height_cap');
}
const padding = clearance.width - 1;
for (let x = -padding; x < width + padding; x++) {
for (let z = 1; z <= clearance.depth; z++) {
for (let y = 0; y < clearance.height; y++) cell('clearance', left + x, y, gridZ + z);
cell('required', left + x, -1, gridZ + z, 1, 'floor');
}
}
} else {
const top = req.support === 'slab' || req.support === 'half_height' ? 0.5
: req.support === 'full_block' ? 1 : entry.preview?.surfaceHeight ?? 1;
if (req.placement === 'surface_footprint') {
const footprint = req.surface_footprint;
for (let x = -footprint.margin; x < footprint.width + footprint.margin; x++) {
for (let z = -footprint.margin; z < footprint.depth + footprint.margin; z++) {
if (x >= 0 && x < footprint.width && z >= 0 && z < footprint.depth) {
cell('required', x - 0.5, 0, z - 0.5, top, 'support');
}
for (let y = 0; y < footprint.height; y++) cell('clearance', x - 0.5, top + y, z - 0.5);
}
}
} else {
cell('required', -0.5, 0, -0.5, top, 'support');
const radius = req.surface_radius || 0;
for (let x = -radius; x <= radius; x++) {
for (let z = -radius; z <= radius; z++) {
for (let y = 0; y < clearance.height; y++) cell('clearance', x - 0.5, top + y, z - 0.5, 1, 'above_support');
}
}
const padding = clearance.width - 1;
for (let x = -padding; x <= padding; x++) {
for (let z = 1; z <= clearance.depth; z++) {
for (let y = 0; y < clearance.height; y++) cell('clearance', x - 0.5, y, z - 0.5);
cell('required', x - 0.5, -1, z - 0.5, 1, 'floor');
}
}
}
}
// Merge overlapping clearance checks (surface-radius and directional checks can overlap).
const columns = new Map();
for (const box of boxes) {
const key = [box.kind, box.from[0], box.from[2], box.to[0], box.to[2]].join(':');
if (!columns.has(key)) columns.set(key, []);
columns.get(key).push(box);
}
const merged = [];
for (const column of columns.values()) {
column.sort((a, b) => a.from[1] - b.from[1]);
let current;
for (const box of column) {
if (current && box.from[1] < current.to[1]) current.to[1] = Math.max(current.to[1], box.to[1]);
else { current = { ...box, from: box.from.slice(), to: box.to.slice() }; merged.push(current); }
}
}
return merged;
}
function isBlockRequirementGuide(node) {
if (String(node?.name || '').startsWith(BLOCK_GUIDE_PREFIX)) return true;
return node?.getAllAncestors?.().some((parent) => String(parent.name || '').startsWith(BLOCK_GUIDE_PREFIX)) || false;
}
function applyBlockRequirementGuideMaterial(cube) {
if (!isBlockRequirementGuide(cube) || !cube.mesh || typeof THREE === 'undefined') return;
const kind = cube.name.includes('_clearance_') ? 'clearance' : 'required';
let material = blockGuideMaterials.get(kind);
if (!material) {
material = new THREE.MeshBasicMaterial({ color: BLOCK_GUIDE_COLORS[kind], transparent: true,
opacity: 0.22, depthWrite: false, side: THREE.DoubleSide });
blockGuideMaterials.set(kind, material);
}
cube.mesh.material = material;
}
function rebuildBlockRequirementGuides(animationId, entry) {
const boxes = entry ? buildBlockRequirementGuideBoxes(entry) : [];
const groupName = `${BLOCK_GUIDE_PREFIX}${animationId}`;
const oldGroups = Group.all.filter((group) => group.name === groupName);
const oldElements = Cube.all.filter((cube) => cube.getAllAncestors().some((group) => oldGroups.includes(group)));
const entries = getBlockRequirementEntries();
const updated = { ...entries };
if (entry) updated[animationId] = entry;
else updated[animationId] = { requirement: null };
const createdElements = [];
const createdGroups = [];
Undo.initEdit({ outliner: true, elements: oldElements, groups: oldGroups,
block_requirement_edit: true });
for (const group of oldGroups) group.remove();
if (entry) {
const root = new Group({ name: groupName, export: false });
root.init();
root.addTo('root');
createdGroups.push(root);
boxes.forEach((box, index) => {
const cube = new Cube({ name: `${BLOCK_GUIDE_PREFIX}${box.kind}_${box.label}_${index + 1}`,
from: box.from, to: box.to, export: false, box_uv: false });
cube.init();
cube.addTo(root);
createdElements.push(cube);
});
}
Project[BLOCK_REQUIREMENTS_PROPERTY] = JSON.stringify(updated);
Canvas.updateAll();
for (const cube of createdElements) applyBlockRequirementGuideMaterial(cube);
Undo.finishEdit(entry ? 'Set block requirement' : 'Remove block requirement',
{ outliner: true, elements: createdElements, groups: createdGroups, block_requirement_edit: true });
Project.saved = false;
}
function showBlockRequirementEditor(animationId) {
const entry = getAuthoredBlockRequirement(animationId);
const req = (entry ? entry.requirement : getSavedAnimdef(animationId).block_requirements) || {};
const wall = (result) => result?.type === 'wall';
const center = (result) => result?.type === 'center_support';
const directional = (result) => wall(result) || result?.placement !== 'surface_footprint';
const footprint = (result) => center(result) && result?.placement === 'surface_footprint';
const rangeMin = (range, fallback) => typeof range === 'number' ? range : range?.min ?? fallback;
const rangeMax = (range) => typeof range === 'number' ? range : range?.max ?? '';
const dimension = (label, value, condition, min = 1) => ({ label, type: 'number', value, min, max: 32, step: 1, condition });
const form = {
_about: { type: 'info', text: `### Block requirement: ${animationId}\nBlue = required structure/floor. Red = clearance. `
+ 'Guides show the minimum valid layout at the animation origin (16 pixels = 1 block). Clearance extends toward +Z; actors face the wall toward -Z. '
+ 'Guides are excluded from model and animation exports; the requirement is included in Create Animation Definition.' },
type: { label: 'Requirement type', type: 'select', value: req.type || 'wall', options: { wall: 'Wall', center_support: 'Center support' } },
blocks: { label: 'Block IDs / tags', type: 'text', value: (Array.isArray(req.blocks) ? req.blocks : req.blocks ? [req.blocks] : []).join(', '),
placeholder: '#minecraft:fences, minecraft:stone', description: 'Comma-separated alternatives. Leave empty for any supported wall/support. Required for custom surfaces and facing.' },
_wall: { type: 'info', text: '### Wall structure', condition: wall },
height_min: dimension('Minimum height', rangeMin(req.height, req.height?.max != null ? 1 : 2), wall),
height_max: { label: 'Maximum height', type: 'text', value: rangeMax(req.height), placeholder: 'No limit', condition: wall },
width_min: dimension('Minimum width', rangeMin(req.width, 1), wall),
width_max: { label: 'Maximum width', type: 'text', value: req.width?.max ?? '', placeholder: 'No limit', condition: wall },
both_axes: { label: 'Maximum width: both axes', type: 'checkbox', value: req.width?.both_axes || false,
condition: (result) => wall(result) && !!String(result?.width_max || '').trim(), description: 'Also check the front/back axis at the required height levels.' },
include_obstructions: { label: 'Maximum width: count obstructions', type: 'checkbox', value: req.width?.include_obstructions || false,
condition: (result) => wall(result) && !!String(result?.width_max || '').trim(), description: 'Count any collidable neighbor, including blocks outside the IDs/tags above.' },
facing: { label: 'Required block face', type: 'select', value: req.facing || '', condition: wall,
options: { '': 'Any facing', front: 'Front', back: 'Back', left: 'Left', right: 'Right' }, description: 'The block face pointing toward the animation area. Stairs: the step face is Back.' },
_center: { type: 'info', text: '### Center support', condition: center },
support: { label: 'Support', type: 'select', value: req.support || 'full_block', condition: center,
options: { full_block: 'Full block', slab: 'Bottom slab', half_height: 'Bottom slab or bed', surface: 'Custom surface (IDs/tags)' } },
placement: { label: 'Placement', type: 'select', value: req.placement || 'directional_clearance', condition: center,
options: { directional_clearance: 'Directional clearance', surface_footprint: 'Surface footprint' } },
surface_radius: dimension('Surrounding surface radius', req.surface_radius ?? 0, (result) => center(result) && directional(result), 0),
footprint_width: dimension('Footprint width', req.surface_footprint?.width ?? 1, footprint),
footprint_depth: dimension('Footprint depth', req.surface_footprint?.depth ?? 1, footprint),
footprint_height: dimension('Clearance above footprint', req.surface_footprint?.height ?? 2, footprint),
footprint_margin: dimension('Free-space border', req.surface_footprint?.margin ?? 0, footprint, 0),
_clearance: { type: 'info', text: '### Directional clearance', condition: directional },
clearance_width: dimension('Clearance width', req.clearance?.width ?? 1, directional),
_width_hint: { type: 'info', text: 'Width 1 checks directly beside the support / across the matched wall. Width 2 adds one block on each side.', condition: directional },
clearance_height: dimension('Clearance height', req.clearance?.height ?? 2, directional),
clearance_depth: dimension('Clearance depth', req.clearance?.depth ?? 1, directional),
_preview: { type: 'info', text: '### Preview shape (editor only)\nActual collision shapes depend on the block state. These values adjust only the guide, not the exported requirement. Maximum limits/facing remain matching rules; the guide shows the minimum structure.' },
wall_thickness: { label: 'Wall thickness (blocks)', type: 'number', value: entry?.preview?.wallThickness ?? 1, min: 0.0625, max: 1, step: 0.0625, condition: wall,
description: 'Full block: 1. Fence post: 0.25. Adjust to the intended collision surface.' },
surface_height: { label: 'Custom surface height', type: 'number', value: entry?.preview?.surfaceHeight ?? 1, min: 0.0625, max: 2, step: 0.0625,
condition: (result) => center(result) && result?.support === 'surface', description: 'For example a bed is 0.5625 blocks high; slab/half-height mode uses 0.5.' },
};
new Dialog({ id: 'animationdirector_block_requirement', title: 'Block Requirement', width: 680, form,
buttons: ['Cancel', 'Remove Requirement', 'Save & Build Guides'], cancelIndex: 0, confirmIndex: 2,
onButton(index) {
if (index === 1) {
rebuildBlockRequirementGuides(animationId, null);
this.hide();
}
},
onConfirm(result) {
try { rebuildBlockRequirementGuides(animationId, readBlockRequirementForm(result)); return true; }
catch (e) { Blockbench.showMessageBox({ title: 'Block Requirement', message: e.message, buttons: ['OK'] }); return false; }
},
}).show();
}
function addBlockRequirement() {
if (!Project) return Blockbench.showQuickMessage('Open a project first.', 2500);
let groups;
try { groups = collectConjoinedAnimationGroups().groups; }
catch (e) { return Blockbench.showMessageBox({ title: 'Block Requirement', message: e.message, buttons: ['OK'] }); }
const options = Object.fromEntries(groups.map((group) => [group.id, group.id]));
const selected = parseAfwStageAnimationName(getSelectedAnimation());
const defaultId = selected?.id || groups[0]?.id || '';
new Dialog({ id: 'animationdirector_block_requirement_target', title: 'Add / Edit Block Requirement', width: 480,
form: { animation: groups.length
? { label: 'Animation', type: 'select', options, value: options[defaultId] ? defaultId : groups[0].id }
: { label: 'Animation ID', type: 'text', value: '', placeholder: 'wolfmplayer', description: 'Use the ID of the future <animationId>.p1 stage, without .p1.' } },
onConfirm(result) {
const id = String(result.animation || '').trim();
if (!/^[a-z0-9._-]+$/.test(id)) { Blockbench.showQuickMessage('Enter a valid lowercase animation ID.', 2500); return false; }
showBlockRequirementEditor(id);
return true;
},
}).show();
}
function stripBlockRequirementGuideBones(clip) {
if (clip?.bones) {
for (const name of Object.keys(clip.bones)) {
if (name.startsWith(BLOCK_GUIDE_PREFIX)) delete clip.bones[name];
}
}
return clip;
}
// ----------------------------
// Import Actor Geometry
// ----------------------------
@@ -1126,7 +1442,7 @@
}
function getGroupsForTextureOverrides() {
const groups = Array.isArray(Group.all) ? Group.all.filter((group) => group) : [];
const groups = Array.isArray(Group.all) ? Group.all.filter((group) => group && !isBlockRequirementGuide(group)) : [];
return groups.sort((a, b) => String(a?.name || '').localeCompare(String(b?.name || '')));
}
@@ -1410,6 +1726,7 @@
for (const [key, override] of Object.entries(overrides)) {
const group = findGroupForTextureOverride(key, override);
if (isBlockRequirementGuide(group) || String(override?.name || '').startsWith(BLOCK_GUIDE_PREFIX)) continue;
const boneName = String(group?.name || override?.name || '').trim();
const texturePath = getTextureExportPath(override?.texture);
if (boneName && texturePath) {
@@ -1423,6 +1740,7 @@
const boneTextureCounts = {};
for (const cube of cubes) {
if (isBlockRequirementGuide(cube)) continue;
const group = cube?.parent instanceof Group ? cube.parent : null;
const boneName = String(group?.name || '').trim();
const texturePath = getReadableTexturePathFromCube(cube);
@@ -1582,7 +1900,21 @@
try {
const options = typeof codec.getExportOptions === 'function' ? codec.getExportOptions() : undefined;
const json = parseCompiledModelContent(codec.compile(options));
if (Array.isArray(json?.['minecraft:geometry'])) return json;
if (Array.isArray(json?.['minecraft:geometry'])) {
for (const geometry of json['minecraft:geometry']) {
const excluded = new Set();
const bones = geometry.bones || [];
for (const bone of bones) if (bone.name?.startsWith(BLOCK_GUIDE_PREFIX)) excluded.add(bone.name);
// Also exclude descendants if a creator has renamed a guide bone.
let previousSize;
do {
previousSize = excluded.size;
for (const bone of bones) if (excluded.has(bone.parent)) excluded.add(bone.name);
} while (excluded.size !== previousSize);
geometry.bones = bones.filter((bone) => !excluded.has(bone.name));
}
return json;
}
} catch (e) {
lastError = e;
}
@@ -1840,7 +2172,7 @@
const compiled = animation.compileBedrockAnimation() || {};
if (!compiled.animations) {
return { formatVersion: '1.8.0', clip: clone(compiled) };
return { formatVersion: '1.8.0', clip: stripBlockRequirementGuideBones(clone(compiled)) };
}
let clip = compiled.animations[stage.name];
@@ -1853,7 +2185,7 @@
}
return {
formatVersion: compiled.format_version || '1.8.0',
clip: clone(clip),
clip: stripBlockRequirementGuideBones(clone(clip)),
};
}
@@ -2403,6 +2735,14 @@
form._placement_separator = '_';
form._placement = { type: 'info', text: '### Playback and placement' };
const authoredRequirement = getAuthoredBlockRequirement(group.id);
const blockRequirement = authoredRequirement ? authoredRequirement.requirement : saved.block_requirements;
form._block_requirement = {
type: 'info',
text: blockRequirement
? `#### Block requirement\nIncluded automatically:\n\n\`\`\`json\n${JSON.stringify(blockRequirement, null, 2)}\n\`\`\`\nEdit using Tools > AnimationDirector Anims > Add Block Requirement.`
: '#### Block requirement\nNone. Use Tools > AnimationDirector Anims > Add Block Requirement to create one.',
};
form.base_speed = {
label: 'Base playback speed', type: 'text', value: saved.speed ?? '', placeholder: 'Default (1.0)',
description: 'Positive speed used unless an individual stage overrides it.',
@@ -2595,6 +2935,9 @@
const displayName = String(result?.display_name || '').trim();
const animators = normalizeAnimatorNames(String(result?.animators || '').split(','));
const definition = {};
const authored = getAuthoredBlockRequirement(group.id);
const requirement = authored ? authored.requirement : saved.block_requirements;
if (requirement) definition.block_requirements = clone(requirement);
if (displayName) definition.display_name = displayName;
if (animators.length) definition.animators = animators;
const selectedTag = String(result?.system_tags || '');
@@ -3680,7 +4023,31 @@ You can import or export complete AFW animation IDs as one file containing all s
default: '{}',
});
}
if (!ModelProject.properties[BLOCK_REQUIREMENTS_PROPERTY]) {
blockRequirementsProjectProperty = new Property(ModelProject, 'string', BLOCK_REQUIREMENTS_PROPERTY, {
default: '{}',
});
}
}
blockGuidePreviewListener = ({ element }) => applyBlockRequirementGuideMaterial(element);
Cube.preview_controller.on('setup update_faces', blockGuidePreviewListener);
for (const cube of Cube.all) applyBlockRequirementGuideMaterial(cube);
blockRequirementUndoSaveListener = ({ save, aspects }) => {
if (aspects.block_requirement_edit) save[BLOCK_REQUIREMENTS_PROPERTY] = Project[BLOCK_REQUIREMENTS_PROPERTY] || '{}';
};
blockRequirementUndoLoadListener = ({ save }) => {
if (save[BLOCK_REQUIREMENTS_PROPERTY] !== undefined) Project[BLOCK_REQUIREMENTS_PROPERTY] = save[BLOCK_REQUIREMENTS_PROPERTY];
};
Blockbench.on('create_undo_save', blockRequirementUndoSaveListener);
Blockbench.on('load_undo_save', blockRequirementUndoLoadListener);
blockRequirementAction = new Action('animationdirector_add_block_requirement', {
name: 'Add Block Requirement',
description: 'Configure a NoN block requirement and create blue structure / red clearance guides.',
icon: 'view_in_ar',
click: addBlockRequirement,
});
importAction = new Action('import_actor_gecko_json', {
name: 'Import Actor',
@@ -3781,6 +4148,7 @@ You can import or export complete AFW animation IDs as one file containing all s
replaceActorAction,
exportConjoinedAction,
createAnimationDefinitionAction,
blockRequirementAction,
{
id: 'animationdirector_deprecated',
name: 'Deprecated',
@@ -3819,12 +4187,21 @@ You can import or export complete AFW animation IDs as one file containing all s
exportAllAction.delete();
exportConjoinedAction.delete();
createAnimationDefinitionAction.delete();
blockRequirementAction.delete();
Cube.preview_controller.removeListener('setup', blockGuidePreviewListener);
Cube.preview_controller.removeListener('update_faces', blockGuidePreviewListener);
Blockbench.removeListener('create_undo_save', blockRequirementUndoSaveListener);
Blockbench.removeListener('load_undo_save', blockRequirementUndoLoadListener);
Canvas.updateAllFaces();
for (const material of blockGuideMaterials.values()) material.dispose();
blockGuideMaterials.clear();
if (actorNamesProjectProperty?.delete) actorNamesProjectProperty.delete();
if (actorTexturesProjectProperty?.delete) actorTexturesProjectProperty.delete();
if (boneTextureOverridesProjectProperty?.delete) boneTextureOverridesProjectProperty.delete();
if (actorAnimdefMetadataProjectProperty?.delete) actorAnimdefMetadataProjectProperty.delete();
if (animdefAnimatorsProjectProperty?.delete) animdefAnimatorsProjectProperty.delete();
if (animdefDefinitionsProjectProperty?.delete) animdefDefinitionsProjectProperty.delete();
if (blockRequirementsProjectProperty?.delete) blockRequirementsProjectProperty.delete();
},
});
})();