block requirements
This commit is contained in:
+381
-4
@@ -6,6 +6,12 @@
|
|||||||
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';
|
||||||
const ANIMDEF_DEFINITIONS_PROPERTY = 'animationdirector_animdef_definitions';
|
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 = {
|
const SYSTEM_TAG_OPTIONS = {
|
||||||
'': 'None (normal animation)',
|
'': 'None (normal animation)',
|
||||||
manualpeak: 'Manual peak',
|
manualpeak: 'Manual peak',
|
||||||
@@ -551,6 +557,316 @@
|
|||||||
cube.updateUV?.();
|
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
|
// Import Actor Geometry
|
||||||
// ----------------------------
|
// ----------------------------
|
||||||
@@ -1126,7 +1442,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getGroupsForTextureOverrides() {
|
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 || '')));
|
return groups.sort((a, b) => String(a?.name || '').localeCompare(String(b?.name || '')));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1410,6 +1726,7 @@
|
|||||||
|
|
||||||
for (const [key, override] of Object.entries(overrides)) {
|
for (const [key, override] of Object.entries(overrides)) {
|
||||||
const group = findGroupForTextureOverride(key, override);
|
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 boneName = String(group?.name || override?.name || '').trim();
|
||||||
const texturePath = getTextureExportPath(override?.texture);
|
const texturePath = getTextureExportPath(override?.texture);
|
||||||
if (boneName && texturePath) {
|
if (boneName && texturePath) {
|
||||||
@@ -1423,6 +1740,7 @@
|
|||||||
const boneTextureCounts = {};
|
const boneTextureCounts = {};
|
||||||
|
|
||||||
for (const cube of cubes) {
|
for (const cube of cubes) {
|
||||||
|
if (isBlockRequirementGuide(cube)) continue;
|
||||||
const group = cube?.parent instanceof Group ? cube.parent : null;
|
const group = cube?.parent instanceof Group ? cube.parent : null;
|
||||||
const boneName = String(group?.name || '').trim();
|
const boneName = String(group?.name || '').trim();
|
||||||
const texturePath = getReadableTexturePathFromCube(cube);
|
const texturePath = getReadableTexturePathFromCube(cube);
|
||||||
@@ -1582,7 +1900,21 @@
|
|||||||
try {
|
try {
|
||||||
const options = typeof codec.getExportOptions === 'function' ? codec.getExportOptions() : undefined;
|
const options = typeof codec.getExportOptions === 'function' ? codec.getExportOptions() : undefined;
|
||||||
const json = parseCompiledModelContent(codec.compile(options));
|
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) {
|
} catch (e) {
|
||||||
lastError = e;
|
lastError = e;
|
||||||
}
|
}
|
||||||
@@ -1840,7 +2172,7 @@
|
|||||||
|
|
||||||
const compiled = animation.compileBedrockAnimation() || {};
|
const compiled = animation.compileBedrockAnimation() || {};
|
||||||
if (!compiled.animations) {
|
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];
|
let clip = compiled.animations[stage.name];
|
||||||
@@ -1853,7 +2185,7 @@
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
formatVersion: compiled.format_version || '1.8.0',
|
formatVersion: compiled.format_version || '1.8.0',
|
||||||
clip: clone(clip),
|
clip: stripBlockRequirementGuideBones(clone(clip)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2403,6 +2735,14 @@
|
|||||||
|
|
||||||
form._placement_separator = '_';
|
form._placement_separator = '_';
|
||||||
form._placement = { type: 'info', text: '### Playback and placement' };
|
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 = {
|
form.base_speed = {
|
||||||
label: 'Base playback speed', type: 'text', value: saved.speed ?? '', placeholder: 'Default (1.0)',
|
label: 'Base playback speed', type: 'text', value: saved.speed ?? '', placeholder: 'Default (1.0)',
|
||||||
description: 'Positive speed used unless an individual stage overrides it.',
|
description: 'Positive speed used unless an individual stage overrides it.',
|
||||||
@@ -2595,6 +2935,9 @@
|
|||||||
const displayName = String(result?.display_name || '').trim();
|
const displayName = String(result?.display_name || '').trim();
|
||||||
const animators = normalizeAnimatorNames(String(result?.animators || '').split(','));
|
const animators = normalizeAnimatorNames(String(result?.animators || '').split(','));
|
||||||
const definition = {};
|
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 (displayName) definition.display_name = displayName;
|
||||||
if (animators.length) definition.animators = animators;
|
if (animators.length) definition.animators = animators;
|
||||||
const selectedTag = String(result?.system_tags || '');
|
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: '{}',
|
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', {
|
importAction = new Action('import_actor_gecko_json', {
|
||||||
name: 'Import Actor',
|
name: 'Import Actor',
|
||||||
@@ -3781,6 +4148,7 @@ You can import or export complete AFW animation IDs as one file containing all s
|
|||||||
replaceActorAction,
|
replaceActorAction,
|
||||||
exportConjoinedAction,
|
exportConjoinedAction,
|
||||||
createAnimationDefinitionAction,
|
createAnimationDefinitionAction,
|
||||||
|
blockRequirementAction,
|
||||||
{
|
{
|
||||||
id: 'animationdirector_deprecated',
|
id: 'animationdirector_deprecated',
|
||||||
name: 'Deprecated',
|
name: 'Deprecated',
|
||||||
@@ -3819,12 +4187,21 @@ You can import or export complete AFW animation IDs as one file containing all s
|
|||||||
exportAllAction.delete();
|
exportAllAction.delete();
|
||||||
exportConjoinedAction.delete();
|
exportConjoinedAction.delete();
|
||||||
createAnimationDefinitionAction.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 (actorNamesProjectProperty?.delete) actorNamesProjectProperty.delete();
|
||||||
if (actorTexturesProjectProperty?.delete) actorTexturesProjectProperty.delete();
|
if (actorTexturesProjectProperty?.delete) actorTexturesProjectProperty.delete();
|
||||||
if (boneTextureOverridesProjectProperty?.delete) boneTextureOverridesProjectProperty.delete();
|
if (boneTextureOverridesProjectProperty?.delete) boneTextureOverridesProjectProperty.delete();
|
||||||
if (actorAnimdefMetadataProjectProperty?.delete) actorAnimdefMetadataProjectProperty.delete();
|
if (actorAnimdefMetadataProjectProperty?.delete) actorAnimdefMetadataProjectProperty.delete();
|
||||||
if (animdefAnimatorsProjectProperty?.delete) animdefAnimatorsProjectProperty.delete();
|
if (animdefAnimatorsProjectProperty?.delete) animdefAnimatorsProjectProperty.delete();
|
||||||
if (animdefDefinitionsProjectProperty?.delete) animdefDefinitionsProjectProperty.delete();
|
if (animdefDefinitionsProjectProperty?.delete) animdefDefinitionsProjectProperty.delete();
|
||||||
|
if (blockRequirementsProjectProperty?.delete) blockRequirementsProjectProperty.delete();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user