particle library
This commit is contained in:
+156
-10
@@ -24,7 +24,7 @@
|
||||
let loopPoseAction;
|
||||
let autoplayStagesAction, autoplayLoopOriginal, autoplayLoopWrapper;
|
||||
let autoplayTransitioning = false;
|
||||
const POSE_BUTTON_EVENTS = ['select_animation', 'select_project', 'unselect_project', 'load_project', 'undo', 'redo', 'add_animation', 'remove_animation'];
|
||||
const POSE_BUTTON_EVENTS = ['select_animation', 'select_project', 'unselect_project', 'load_project', 'undo', 'redo', 'finish_edit', 'add_animation', 'remove_animation'];
|
||||
const PROP_PREVIEWS_PROPERTY = 'animationdirector_prop_previews';
|
||||
const PROP_SLOTS = { left: 'propleft', right: 'propright', floor: 'propfloor' };
|
||||
let propAction, propProjectProperty, propDialog, propListener;
|
||||
@@ -34,7 +34,7 @@
|
||||
const SOUND_SOURCE_KEY = 'animationdirector_sound_source';
|
||||
const SOUND_SELECTION_BLACKLIST = new Set(['needsofnature:a', 'needsofnature:funkytown']);
|
||||
const SOUND_PREVIEW_PROPERTY = 'animationdirector_audio_preview';
|
||||
const SOUND_EVENTS = ['select_animation', 'load_project', 'select_project', 'update_keyframe_selection', 'undo', 'redo'];
|
||||
const SOUND_EVENTS = ['select_animation', 'load_project', 'select_project', 'update_keyframe_selection', 'undo', 'redo', 'finish_edit'];
|
||||
let soundLibraryAction, soundLibraryDialog, soundLibraryListener, soundPreviewProperty, soundAudition;
|
||||
let soundLibrary, soundLibraryGeneration = 0;
|
||||
const soundAudioUrls = new Map(), soundMappedPoints = new WeakMap();
|
||||
@@ -44,6 +44,13 @@
|
||||
'liquid_particle_water', 'smallheart', 'ripped_fabric', 'spray_splash', 'spray_surface_drip',
|
||||
'spray_surface_viscous', 'spray_surface_liquid'].map((name) => `needsofnature:${name}`);
|
||||
let particleLibraryAction, particleLibraryDialog, particleLibrary;
|
||||
const PARTICLE_PREVIEW_PROPERTY = 'animationdirector_particle_preview';
|
||||
const PARTICLE_PREVIEW_PREFIX = 'animationdirector-particle:';
|
||||
const PARTICLE_PREVIEWS_KEY = 'animationdirector_particle_previews';
|
||||
const PARTICLE_PREVIEW_KINDS = { 'needsofnature:liquid_particle': 'liquid', 'needsofnature:liquid_particle_falling': 'falling',
|
||||
'needsofnature:smallheart': 'heart', 'needsofnature:ripped_fabric': 'fabric' };
|
||||
let particlePreviewProperty, particlePreviewToggle, particlePreviewsEnabled = true;
|
||||
const particlePreviewEffects = new Map(), particlePreviewPoints = new Set();
|
||||
const figuraSoundPatches = [], figuraSoundMedia = new Set();
|
||||
let figuraSoundCompatEnabled = true, figuraSoundPlayListener;
|
||||
const SYSTEM_TAG_OPTIONS = {
|
||||
@@ -2958,9 +2965,10 @@
|
||||
const { reason } = previousStagePoseSource();
|
||||
const description = reason || 'Copy the final transforms of all actors from the previous stage into time 0. Existing time-0 transforms are replaced.';
|
||||
previousStagePoseAction.description = description;
|
||||
for (const node of previousStagePoseAction.nodes || []) {
|
||||
for (const node of new Set([previousStagePoseAction.node, ...(previousStagePoseAction.nodes || [])].filter(Boolean))) {
|
||||
node.classList.toggle('disabled', !!reason);
|
||||
node.setAttribute('aria-disabled', String(!!reason));
|
||||
if (node.style) { node.style.opacity = reason ? '0.35' : ''; node.style.filter = reason ? 'grayscale(1)' : ''; }
|
||||
const label = node.querySelector('.tooltip_description');
|
||||
if (label) label.textContent = description;
|
||||
}
|
||||
@@ -2971,6 +2979,7 @@
|
||||
const animation = Animation.selected;
|
||||
if (!animation) return 'Select an animation clip first.';
|
||||
if (!Number.isFinite(animation.length) || animation.length <= 0) return 'Set a positive animation clip length first.';
|
||||
if (animation.loop !== 'loop') return 'Set this animation to Loop before copying its first pose to the end.';
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2979,9 +2988,10 @@
|
||||
const reason = loopPoseUnavailableReason();
|
||||
const description = reason || 'Copy the first pose of all actors to the clip endpoint for looping. Existing endpoint transforms are replaced.';
|
||||
loopPoseAction.description = description;
|
||||
for (const node of loopPoseAction.nodes || []) {
|
||||
for (const node of new Set([loopPoseAction.node, ...(loopPoseAction.nodes || [])].filter(Boolean))) {
|
||||
node.classList.toggle('disabled', !!reason);
|
||||
node.setAttribute('aria-disabled', String(!!reason));
|
||||
if (node.style) { node.style.opacity = reason ? '0.35' : ''; node.style.filter = reason ? 'grayscale(1)' : ''; }
|
||||
const label = node.querySelector('.tooltip_description');
|
||||
if (label) label.textContent = description;
|
||||
}
|
||||
@@ -3038,13 +3048,19 @@
|
||||
Blockbench.showQuickMessage(`Copied actor pose to time ${targetTime}.`, 2500);
|
||||
}
|
||||
|
||||
function cleanSoundPreviewFields(clip) {
|
||||
function cleanEffectPreviewFields(clip) {
|
||||
for (const cues of Object.values(clip?.sound_effects || {})) {
|
||||
for (const cue of Array.isArray(cues) ? cues : [cues]) {
|
||||
if (!cue || typeof cue !== 'object') continue;
|
||||
delete cue.file; delete cue[SOUND_PREVIEW_PROPERTY];
|
||||
}
|
||||
}
|
||||
for (const cues of Object.values(clip?.particle_effects || {})) {
|
||||
for (const cue of Array.isArray(cues) ? cues : [cues]) {
|
||||
if (!cue || typeof cue !== 'object') continue;
|
||||
delete cue.file; delete cue[PARTICLE_PREVIEW_PROPERTY];
|
||||
}
|
||||
}
|
||||
return clip;
|
||||
}
|
||||
|
||||
@@ -3056,7 +3072,7 @@
|
||||
|
||||
const compiled = animation.compileBedrockAnimation() || {};
|
||||
if (!compiled.animations) {
|
||||
return { formatVersion: '1.8.0', clip: cleanSoundPreviewFields(stripBlockRequirementGuideBones(clone(compiled))) };
|
||||
return { formatVersion: '1.8.0', clip: cleanEffectPreviewFields(stripBlockRequirementGuideBones(clone(compiled))) };
|
||||
}
|
||||
|
||||
let clip = compiled.animations[stage.name];
|
||||
@@ -3069,7 +3085,7 @@
|
||||
}
|
||||
return {
|
||||
formatVersion: compiled.format_version || '1.8.0',
|
||||
clip: cleanSoundPreviewFields(stripBlockRequirementGuideBones(clone(clip))),
|
||||
clip: cleanEffectPreviewFields(stripBlockRequirementGuideBones(clone(clip))),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3406,6 +3422,7 @@
|
||||
saveAnimdef(group.id, definition);
|
||||
if (undo) Undo.finishEdit('Save animation definition');
|
||||
mapAnimationSounds(typeof Animation === 'undefined' ? [] : Animation.all);
|
||||
mapAnimationParticles(typeof Animation === 'undefined' ? [] : Animation.all);
|
||||
}
|
||||
|
||||
function exportAnimationDefinitionStub(group, definition, actorMetadata) {
|
||||
@@ -3561,6 +3578,7 @@
|
||||
cancelBlockRequirementUpdate();
|
||||
rebuildBlockRequirementGuides(group.id, entry, definition, true, props.entries);
|
||||
mapAnimationSounds(typeof Animation === 'undefined' ? [] : Animation.all);
|
||||
mapAnimationParticles(typeof Animation === 'undefined' ? [] : Animation.all);
|
||||
if (props.warnings.length) Blockbench.showQuickMessage(`Item props: ${props.warnings.join(' ')}`, 6000);
|
||||
void refreshPropPreviews(true);
|
||||
if (blockRequirementPanel?.vue) blockRequirementPanel.vue.load(group.id);
|
||||
@@ -4569,6 +4587,7 @@
|
||||
};
|
||||
Animator.loadFile(fakeFile, imported.stageNames);
|
||||
mapAnimationSounds(Animation.all);
|
||||
mapAnimationParticles(Animation.all);
|
||||
Blockbench.showQuickMessage(
|
||||
`Imported ${imported.stageNames.length} stage${imported.stageNames.length === 1 ? '' : 's'} for "${outputAnimationId}"`,
|
||||
2500
|
||||
@@ -5797,6 +5816,7 @@
|
||||
setProjectAnimdefAnimators(plan.definition.animators || []);
|
||||
Animator.loadFile({ name: `${plan.parsed.animationId}.animation.json`, path: '', content: JSON.stringify(plan.imported.fileObj) }, plan.imported.stageNames);
|
||||
mapAnimationSounds(Animation.all);
|
||||
mapAnimationParticles(Animation.all);
|
||||
rebuildBlockRequirementGuides(plan.parsed.animationId, plan.blockEntry, plan.definition, false);
|
||||
if (plan.propEntries) Project[PROP_PREVIEWS_PROPERTY] = JSON.stringify(plan.propEntries);
|
||||
Canvas.updateAll();
|
||||
@@ -6162,6 +6182,7 @@
|
||||
}
|
||||
|
||||
function loadParticleLibrary(io = desktopFiles()) {
|
||||
clearParticlePreviews();
|
||||
const ids = new Set(NON_PARTICLE_IDS), warnings = [], definitions = new Map();
|
||||
const selected = localStorage.getItem(PARTICLE_SOURCE_KEY) || localStorage.getItem(SOUND_SOURCE_KEY);
|
||||
const paths = uniqueTrimmedStrings([...savedPackFolders(), ...(selected ? [selected] : []),
|
||||
@@ -6180,11 +6201,16 @@
|
||||
}
|
||||
} catch (error) { warnings.push(`${io.path.basename(file)}: ${error.message}`); }
|
||||
}
|
||||
particleLibrary = { ids: [...ids].sort(), warnings, definitions, paths: paths.slice(0, 64),
|
||||
particleLibrary = { ids: sortParticleLibraryIds(ids), warnings, definitions, paths: paths.slice(0, 64),
|
||||
thumbnails: new Map(), thumbnailBytes: 0 };
|
||||
return particleLibrary;
|
||||
}
|
||||
|
||||
function sortParticleLibraryIds(ids) {
|
||||
return [...ids].sort((a, b) => Number(b.startsWith('needsofnature:')) - Number(a.startsWith('needsofnature:'))
|
||||
|| (a < b ? -1 : a > b ? 1 : 0));
|
||||
}
|
||||
|
||||
function particleThumbnailRows(ids, library = particleLibrary, io = null) {
|
||||
const pending = new Map(), groups = new Map();
|
||||
if (!library?.definitions) return ids.map((id) => ({ id, thumbnail: '' }));
|
||||
@@ -6244,6 +6270,106 @@
|
||||
return uniqueTrimmedStrings((locators || []).map((locator) => locator.name)).sort();
|
||||
}
|
||||
|
||||
function buildParticlePreview(id, texture) {
|
||||
const kind = PARTICLE_PREVIEW_KINDS[id];
|
||||
if (!kind || !texture) return null;
|
||||
const bytes = Buffer.from(texture.split(',')[1], 'base64');
|
||||
const width = bytes.readUInt32BE(16), height = bytes.readUInt32BE(20);
|
||||
const size = kind === 'heart' ? 0.185 : kind === 'fabric' ? 0.19 : 0.12;
|
||||
const lifetime = kind === 'heart' ? 0.8 : kind === 'fabric' ? 6 : 4;
|
||||
const uv = { texture_width: width, texture_height: height, uv: [0, 0], uv_size: [width, height] };
|
||||
if (kind === 'fabric' && height % 5 === 0) {
|
||||
delete uv.uv; delete uv.uv_size;
|
||||
uv.flipbook = { base_UV: [0, 0], size_UV: [width, height / 5], step_UV: [0, height / 5],
|
||||
frames_per_second: 5, max_frame: 5, stretch_to_lifetime: false, loop: true };
|
||||
}
|
||||
const components = {
|
||||
'minecraft:emitter_rate_instant': { num_particles: 1 },
|
||||
'minecraft:emitter_lifetime_once': { active_time: lifetime },
|
||||
'minecraft:emitter_local_space': { position: false, rotation: false, velocity: false },
|
||||
'minecraft:emitter_shape_point': { offset: [0, -1 / 16, 0], direction: [0, 1, 0] },
|
||||
'minecraft:particle_lifetime_expression': { max_lifetime: lifetime },
|
||||
'minecraft:particle_initial_speed': kind === 'heart' ? 2 : 0,
|
||||
'minecraft:particle_motion_dynamic': { linear_acceleration: [0, kind === 'falling' ? -2.4 : kind === 'fabric' ? -0.16 : 0, 0],
|
||||
linear_drag_coefficient: kind === 'heart' ? 3 : kind === 'fabric' ? 4 : 0.4 },
|
||||
'minecraft:particle_appearance_billboard': { size: [size, size], facing_camera_mode: 'rotate_xyz', uv },
|
||||
};
|
||||
if (kind === 'liquid') {
|
||||
delete components['minecraft:particle_motion_dynamic'];
|
||||
components['minecraft:particle_motion_parametric'] = { relative_position: [0,
|
||||
'v.particle_age < 1 ? 0 : -1.2 * (v.particle_age - 1) * (v.particle_age - 1)', 0] };
|
||||
}
|
||||
return { format_version: '1.10.0', particle_effect: {
|
||||
description: { identifier: id, basic_render_parameters: { material: 'particles_alpha', texture: 'editor_preview' } }, components,
|
||||
} };
|
||||
}
|
||||
|
||||
function clearParticlePreviews() {
|
||||
for (const point of particlePreviewPoints) {
|
||||
if (point.file === point[PARTICLE_PREVIEW_PROPERTY] || point.file?.startsWith(PARTICLE_PREVIEW_PREFIX)) point.file = '';
|
||||
delete point[PARTICLE_PREVIEW_PROPERTY];
|
||||
}
|
||||
particlePreviewPoints.clear();
|
||||
for (const [key, effect] of particlePreviewEffects) {
|
||||
for (const emitter of Object.values(effect.emitters)) emitter.delete();
|
||||
effect.config.texture?.dispose();
|
||||
if (typeof Animator !== 'undefined' && Animator.particle_effects?.[key] === effect) delete Animator.particle_effects[key];
|
||||
}
|
||||
particlePreviewEffects.clear();
|
||||
}
|
||||
|
||||
function mapAnimationParticles(animations) {
|
||||
if (typeof Animator === 'undefined' || !Animator.particle_effects || typeof Wintersky === 'undefined'
|
||||
|| !Wintersky.Config || typeof WinterskyScene === 'undefined') return;
|
||||
const points = [];
|
||||
const locators = new Set(availableParticleLocators());
|
||||
for (const animation of animations.filter(Boolean)) {
|
||||
for (const frame of animation.animators?.effects?.particle || []) {
|
||||
for (const point of frame.data_points || []) {
|
||||
const generated = point.file === point[PARTICLE_PREVIEW_PROPERTY] || point.file?.startsWith(PARTICLE_PREVIEW_PREFIX);
|
||||
if (point.file && !generated) continue;
|
||||
if (!particlePreviewsEnabled || (typeof Format !== 'undefined' && Format.id !== 'geckolib_model')
|
||||
|| !locators.has(point.locator) || !Object.hasOwn(PARTICLE_PREVIEW_KINDS, point.effect)) {
|
||||
if (generated) point.file = '';
|
||||
continue;
|
||||
}
|
||||
points.push(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!points.length) return;
|
||||
if (!particleLibrary) {
|
||||
try { loadParticleLibrary(); } catch (_) { return; }
|
||||
}
|
||||
const ids = uniqueTrimmedStrings(points.map((point) => point.effect));
|
||||
for (const { id, thumbnail } of particleThumbnailRows(ids)) {
|
||||
const key = `${PARTICLE_PREVIEW_PREFIX}${id}`;
|
||||
if (particlePreviewEffects.has(key) && Animator.particle_effects[key] === particlePreviewEffects.get(key)) continue;
|
||||
const json = buildParticlePreview(id, thumbnail);
|
||||
if (!json) continue;
|
||||
// Register in memory: loadParticleEmitter would install a filesystem watcher on a virtual file.
|
||||
const config = new Wintersky.Config(WinterskyScene, { preview_texture: thumbnail });
|
||||
config.setFromJSON(json);
|
||||
const effect = { config, emitters: {} };
|
||||
Animator.particle_effects[key] = effect; particlePreviewEffects.set(key, effect);
|
||||
}
|
||||
for (const point of points) {
|
||||
const key = `${PARTICLE_PREVIEW_PREFIX}${point.effect}`;
|
||||
point.file = particlePreviewEffects.has(key) ? key : '';
|
||||
point[PARTICLE_PREVIEW_PROPERTY] = point.file;
|
||||
particlePreviewPoints.add(point);
|
||||
}
|
||||
}
|
||||
|
||||
function setParticlePreviewsEnabled(enabled) {
|
||||
particlePreviewsEnabled = !!enabled;
|
||||
localStorage.setItem(PARTICLE_PREVIEWS_KEY, String(particlePreviewsEnabled));
|
||||
if (particlePreviewToggle) { particlePreviewToggle.value = particlePreviewsEnabled; particlePreviewToggle.updateEnabledState?.(); }
|
||||
clearParticlePreviews();
|
||||
mapAnimationParticles(typeof Animation === 'undefined' ? [] : Animation.all);
|
||||
if (typeof Animator !== 'undefined') Animator.preview?.();
|
||||
}
|
||||
|
||||
function applyLibraryParticle(id, insert = false, locator = null) {
|
||||
if (!importResourceId(id)) throw new Error('Invalid particle ID.');
|
||||
if (locator !== null && !availableParticleLocators().includes(locator)) throw new Error('The selected locator is no longer available in this project.');
|
||||
@@ -6273,13 +6399,14 @@
|
||||
effects.select(); frame.select();
|
||||
Undo.finishEdit('Insert particle from library', { keyframes });
|
||||
}
|
||||
mapAnimationParticles([animation]);
|
||||
if (typeof updateKeyframeSelection === 'function') updateKeyframeSelection();
|
||||
}
|
||||
|
||||
function showParticleLibrary() {
|
||||
if (!particleLibrary) {
|
||||
try { loadParticleLibrary(); }
|
||||
catch (error) { particleLibrary = { ids: [...NON_PARTICLE_IDS].sort(), warnings: [error.message] }; }
|
||||
catch (error) { particleLibrary = { ids: sortParticleLibraryIds(NON_PARTICLE_IDS), warnings: [error.message] }; }
|
||||
}
|
||||
const locators = availableParticleLocators();
|
||||
const selectedFrames = (typeof Timeline === 'undefined' ? [] : Timeline.selected || [])
|
||||
@@ -6293,6 +6420,7 @@
|
||||
buttons: ['Close'], component: {
|
||||
data() { return { query: '', category: 'all', selected: '', page: 0,
|
||||
ids: particleLibrary.ids, locators, locator: defaultLocator, thumbnailRevision: 0,
|
||||
previews: particlePreviewsEnabled,
|
||||
warnings: particleLibrary.warnings.join('\n'), error: '' }; },
|
||||
computed: {
|
||||
categories() { return [...new Set(this.ids.map((id) => id.split(':')[0]))].sort(); },
|
||||
@@ -6308,7 +6436,8 @@
|
||||
methods: {
|
||||
reload() {
|
||||
try { loadParticleLibrary(); this.ids = particleLibrary.ids; this.warnings = particleLibrary.warnings.join('\n');
|
||||
this.page = 0; this.thumbnailRevision++; this.error = ''; }
|
||||
this.page = 0; this.thumbnailRevision++; this.error = '';
|
||||
mapAnimationParticles(typeof Animation === 'undefined' ? [] : Animation.all); }
|
||||
catch (error) { this.error = error.message; }
|
||||
},
|
||||
folder() {
|
||||
@@ -6322,9 +6451,11 @@
|
||||
});
|
||||
},
|
||||
apply(insert) { try { applyLibraryParticle(this.selected, insert, this.locator || null); this.error = ''; } catch (error) { this.error = error.message; } },
|
||||
togglePreviews() { setParticlePreviewsEnabled(this.previews); },
|
||||
},
|
||||
template: `<div style="padding:12px">
|
||||
<div class="bar flex" style="gap:6px"><button @click="jar">Select JAR / ZIP</button><button @click="folder">Select Assets Folder</button><button @click="reload">Refresh</button></div>
|
||||
<p><label><input type="checkbox" v-model="previews" @change="togglePreviews"> Particle Previews</label></p>
|
||||
<p class="subtle">Includes NoN particles, remembered Pack Folders, and locally available Minecraft assets. Select a mod JAR to add its particles.</p>
|
||||
<div class="bar flex" style="gap:8px;margin:10px 0"><input v-model="query" placeholder="Search particle IDs" style="flex:1" aria-label="Search particle IDs"><select v-model="category"><option value="all">All particles</option><option v-for="namespace in categories" :key="namespace" :value="namespace">{{namespace}}</option></select></div>
|
||||
<div style="height:260px;overflow-y:auto;margin:10px 0;border:1px solid var(--color-border)">
|
||||
@@ -6339,6 +6470,7 @@
|
||||
<div class="bar flex" style="gap:8px;margin-top:12px"><label for="animationdirector_particle_locator">Locator</label><select id="animationdirector_particle_locator" v-model="locator" style="flex:1"><option value="">Keep existing / unset</option><option v-for="name in locators" :key="name" :value="name">{{name}}</option></select></div>
|
||||
<div class="bar flex" style="gap:6px;margin-top:12px"><button @click="apply(false)" :disabled="!selected">Apply to Selected / Insert</button><button @click="apply(true)" :disabled="!selected">Insert at Playhead</button></div>
|
||||
<p class="subtle">Thumbnails show the first sprite, not particle motion or in-game tint. Parameterized particles require a NoN addon resolver; their ID alone may not spawn them.</p>
|
||||
<p class="subtle">Timeline previews support NoN liquid, falling liquid, small hearts, and ripped fabric. Select NoN assets to load their textures. Motion is approximate; world collisions, liquid color and gameplay are not simulated.</p>
|
||||
<p style="color:var(--color-error);white-space:pre-wrap">{{error}}</p><p class="subtle" style="white-space:pre-wrap">{{warnings}}</p>
|
||||
</div>`,
|
||||
},
|
||||
@@ -6473,6 +6605,7 @@
|
||||
installFiguraSoundCompat();
|
||||
if (typeof Animation === 'undefined') return;
|
||||
mapAnimationSounds(Animation.all);
|
||||
mapAnimationParticles(Animation.all);
|
||||
for (const animation of Animation.all) {
|
||||
const effects = animation.animators?.effects;
|
||||
if (effects) for (const patch of figuraSoundPatches) {
|
||||
@@ -6517,6 +6650,7 @@
|
||||
}
|
||||
}
|
||||
mapAnimationSounds(typeof Animation === 'undefined' ? [] : [Animation.selected]);
|
||||
mapAnimationParticles(typeof Animation === 'undefined' ? [] : [Animation.selected]);
|
||||
}
|
||||
|
||||
Plugin.register(PLUGIN_ID, {
|
||||
@@ -6602,7 +6736,16 @@ Features without a reference need no additional addon. Whole-animation import an
|
||||
soundPreviewProperty = new Property(KeyframeDataPoint, 'string', SOUND_PREVIEW_PROPERTY, {
|
||||
exposed: false, condition: (point) => point.keyframe.channel === 'sound',
|
||||
});
|
||||
particlePreviewProperty = new Property(KeyframeDataPoint, 'string', PARTICLE_PREVIEW_PROPERTY, {
|
||||
exposed: false, condition: (point) => point.keyframe.channel === 'particle',
|
||||
});
|
||||
}
|
||||
particlePreviewsEnabled = localStorage.getItem(PARTICLE_PREVIEWS_KEY) !== 'false';
|
||||
particlePreviewToggle = new Toggle('animationdirector_particle_previews', {
|
||||
name: 'Particle Previews', icon: 'grain', category: 'animation', default: particlePreviewsEnabled,
|
||||
onChange: setParticlePreviewsEnabled, condition: () => !Blockbench.isWeb,
|
||||
});
|
||||
Blockbench.on('close_project', clearParticlePreviews);
|
||||
soundLibraryAction = new Action('animationdirector_sound_library', {
|
||||
name: 'NoN Sound Library', icon: 'library_music', category: 'animation',
|
||||
description: 'Browse NoN and pack sounds, audition audio and add sound cues to the timeline.',
|
||||
@@ -6822,6 +6965,7 @@ Features without a reference need no additional addon. Whole-animation import an
|
||||
propAction,
|
||||
soundLibraryAction,
|
||||
particleLibraryAction,
|
||||
particlePreviewToggle,
|
||||
'_',
|
||||
createAnimationDefinitionAction,
|
||||
exportConjoinedAction,
|
||||
@@ -6850,6 +6994,8 @@ Features without a reference need no additional addon. Whole-animation import an
|
||||
soundLibraryAction?.delete(); soundLibraryDialog?.delete(); soundPreviewProperty?.delete();
|
||||
if (typeof document !== 'undefined') document.getElementById('animationdirector_particle_library_button')?.remove();
|
||||
particleLibraryAction?.delete(); particleLibraryDialog?.delete(); particleLibrary = undefined;
|
||||
Blockbench.removeListener('close_project', clearParticlePreviews);
|
||||
clearParticlePreviews(); particlePreviewProperty?.delete(); particlePreviewToggle?.delete();
|
||||
clearSoundAudio(); soundLibrary = undefined;
|
||||
clearPropMeshes(); propSources.clear();
|
||||
propAction?.delete(); propDialog?.delete(); propProjectProperty?.delete(); actorUvBasisProjectProperty?.delete();
|
||||
|
||||
Reference in New Issue
Block a user