From 8764af4c1a6b9795ff7aaaa66a45cad5890ffd26 Mon Sep 17 00:00:00 2001 From: L1Z0 <2grif484d@mozmail.com> Date: Mon, 28 Sep 2026 18:56:59 +0200 Subject: [PATCH] block requirements --- AnimationDirector.js | 385 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 381 insertions(+), 4 deletions(-) diff --git a/AnimationDirector.js b/AnimationDirector.js index 4a7d0b5..e02cddc 100644 --- a/AnimationDirector.js +++ b/AnimationDirector.js @@ -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 .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,8 +4023,32 @@ 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', description: 'Import a GeckoLib/Bedrock geometry JSON into the current project and prefix bones with actorN_.', @@ -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(); }, }); })();