sound library
This commit is contained in:
+309
-13
@@ -25,6 +25,13 @@
|
||||
const propMeshes = new Map();
|
||||
const propSources = new Map();
|
||||
let propPreviewSignature = null, propPreviewGeneration = 0;
|
||||
const SOUND_SOURCE_KEY = 'animationdirector_sound_source';
|
||||
const SOUND_PREVIEW_PROPERTY = 'animationdirector_audio_preview';
|
||||
const SOUND_EVENTS = ['select_animation', 'load_project', 'select_project', 'update_keyframe_selection'];
|
||||
let soundLibraryAction, soundLibraryDialog, soundLibraryListener, soundPreviewProperty, soundAudition;
|
||||
let soundLibrary, soundLibraryGeneration = 0, soundPreviewMode = 'dry';
|
||||
const soundAudioUrls = new Map(), soundMappedPoints = new WeakMap();
|
||||
let soundAudioBytes = 0;
|
||||
const SYSTEM_TAG_OPTIONS = {
|
||||
'': 'None (normal animation)',
|
||||
manualpeak: 'Manual peak',
|
||||
@@ -136,7 +143,7 @@
|
||||
function setActorUvBasis(actorIndex, width, height) {
|
||||
const all = getActorUvBasis();
|
||||
if (all[actorIndex]?.width === width && all[actorIndex]?.height === height) return;
|
||||
all[actorIndex] = { width, height };
|
||||
all[actorIndex] = { ...all[actorIndex], width, height };
|
||||
Project[ACTOR_UV_BASIS_PROPERTY] = JSON.stringify(all);
|
||||
Project.saved = false;
|
||||
}
|
||||
@@ -606,20 +613,34 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function normalizeActorUvs(actorIndex, width, height, original = null) {
|
||||
function normalizeCubeUvs(cube, actorIndex, width, height, original = null) {
|
||||
width = validUvDimension(width); height = validUvDimension(height);
|
||||
const basis = original || getActorUvBasis()[actorIndex] || {
|
||||
const all = getActorUvBasis(), actor = all[actorIndex];
|
||||
const key = cube.uuid || cube.name;
|
||||
const basis = original || actor?.cubes?.[key] || actor || {
|
||||
width: validUvDimension(Project.texture_width), height: validUvDimension(Project.texture_height) };
|
||||
const oldWidth = validUvDimension(basis.width), oldHeight = validUvDimension(basis.height);
|
||||
expandProjectUvs(width, height);
|
||||
const x = oldWidth / width, y = oldHeight / height;
|
||||
if (x !== 1 || y !== 1) {
|
||||
for (const cube of Cube.all) if (cubeBelongsToActor(cube, actorIndex)) scaleCubeUvs(cube, x, y);
|
||||
scaleCubeUvs(cube, x, y);
|
||||
if (actor?.cubes?.[key]?.width !== width || actor?.cubes?.[key]?.height !== height) {
|
||||
all[actorIndex] = { ...(actor || basis), cubes: { ...actor?.cubes, [key]: { width, height } } };
|
||||
Project[ACTOR_UV_BASIS_PROPERTY] = JSON.stringify(all);
|
||||
Project.saved = false;
|
||||
}
|
||||
setActorUvBasis(actorIndex, width, height);
|
||||
return x !== 1 || y !== 1;
|
||||
}
|
||||
|
||||
function normalizeActorUvs(actorIndex, width, height, original = null) {
|
||||
let changed = false;
|
||||
for (const cube of Cube.all) {
|
||||
if (cubeBelongsToActor(cube, actorIndex)) changed = normalizeCubeUvs(cube, actorIndex, width, height, original) || changed;
|
||||
}
|
||||
expandProjectUvs(validUvDimension(width), validUvDimension(height));
|
||||
setActorUvBasis(actorIndex, width, height);
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Block requirement authoring
|
||||
// ----------------------------
|
||||
@@ -1843,6 +1864,11 @@
|
||||
function applyTextureToCube(cube, texture) {
|
||||
if (!cube || !texture) return;
|
||||
|
||||
const actorIndex = readActorIndexFromName(cube.name) || readActorIndexFromName(cube.parent?.name);
|
||||
const imageWidth = texture.img?.naturalWidth || texture.canvas?.width || texture.uv_width || 0;
|
||||
const imageHeight = texture.display_height || texture.img?.naturalHeight || texture.canvas?.height || texture.uv_height || 0;
|
||||
if (actorIndex && imageWidth > 0 && imageHeight > 0) normalizeCubeUvs(cube, actorIndex, imageWidth, imageHeight);
|
||||
|
||||
const faces = ['north', 'south', 'east', 'west', 'up', 'down'];
|
||||
const textureId = texture.uuid || texture.id || texture.name;
|
||||
|
||||
@@ -1869,14 +1895,13 @@
|
||||
const texture = findTextureByActorTextureName(textureName);
|
||||
if (!texture) return { applied: 0, missing: true };
|
||||
|
||||
const imageWidth = texture.img?.naturalWidth || texture.canvas?.width || 0;
|
||||
const imageHeight = texture.display_height || texture.img?.naturalHeight || texture.canvas?.height || 0;
|
||||
if (imageWidth > 0 && imageHeight > 0) normalizeActorUvs(actorIndex, imageWidth, imageHeight);
|
||||
|
||||
let applied = 0;
|
||||
const overrides = getProjectBoneTextureOverrides();
|
||||
const cubes = Array.isArray(Cube.all) ? Cube.all : [];
|
||||
for (const cube of cubes) {
|
||||
if (!cubeBelongsToActor(cube, actorIndex)) continue;
|
||||
const override = getNearestBoneTextureOverride(cube, overrides);
|
||||
if (override?.texture && findTextureByActorTextureName(override.texture)) continue;
|
||||
applyTextureToCube(cube, texture);
|
||||
applied++;
|
||||
}
|
||||
@@ -2439,6 +2464,16 @@
|
||||
return { groups, ignored };
|
||||
}
|
||||
|
||||
function cleanSoundPreviewFields(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];
|
||||
}
|
||||
}
|
||||
return clip;
|
||||
}
|
||||
|
||||
function compileConjoinedStage(stage) {
|
||||
const animation = stage?.animation;
|
||||
if (typeof animation?.compileBedrockAnimation !== 'function') {
|
||||
@@ -2447,7 +2482,7 @@
|
||||
|
||||
const compiled = animation.compileBedrockAnimation() || {};
|
||||
if (!compiled.animations) {
|
||||
return { formatVersion: '1.8.0', clip: stripBlockRequirementGuideBones(clone(compiled)) };
|
||||
return { formatVersion: '1.8.0', clip: cleanSoundPreviewFields(stripBlockRequirementGuideBones(clone(compiled))) };
|
||||
}
|
||||
|
||||
let clip = compiled.animations[stage.name];
|
||||
@@ -2460,7 +2495,7 @@
|
||||
}
|
||||
return {
|
||||
formatVersion: compiled.format_version || '1.8.0',
|
||||
clip: stripBlockRequirementGuideBones(clone(clip)),
|
||||
clip: cleanSoundPreviewFields(stripBlockRequirementGuideBones(clone(clip))),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3936,6 +3971,7 @@
|
||||
content: JSON.stringify(imported.fileObj, null, 2),
|
||||
};
|
||||
Animator.loadFile(fakeFile, imported.stageNames);
|
||||
mapAnimationSounds(Animation.all);
|
||||
Blockbench.showQuickMessage(
|
||||
`Imported ${imported.stageNames.length} stage${imported.stageNames.length === 1 ? '' : 's'} for "${outputAnimationId}"`,
|
||||
2500
|
||||
@@ -5132,7 +5168,6 @@
|
||||
const base = row.textures.get(row.defaultTexture);
|
||||
if (base) {
|
||||
textures[index] = loadedTextures.get(base.file || `${base.relative}:${row.defaultTexture}`);
|
||||
if (base.dimensions) normalizeActorUvs(index, base.dimensions.width, base.dimensions.height);
|
||||
}
|
||||
for (const [bone, textureName] of Object.entries(row.json.afw_bone_textures || {})) {
|
||||
const group = Group.all.find((candidate) => candidate.name === `actor${index}_${bone}`);
|
||||
@@ -5147,6 +5182,7 @@
|
||||
setProjectBoneTextureOverrides(overrides);
|
||||
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);
|
||||
rebuildBlockRequirementGuides(plan.parsed.animationId, plan.blockEntry, plan.definition, false);
|
||||
if (plan.propEntries) Project[PROP_PREVIEWS_PROPERTY] = JSON.stringify(plan.propEntries);
|
||||
Canvas.updateAll();
|
||||
@@ -5313,6 +5349,248 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Sound IDs remain gameplay cues. Blob URLs are editor-only, lazily loaded audio previews.
|
||||
function stopSoundAudition() {
|
||||
if (soundAudition) { soundAudition.pause(); soundAudition = null; }
|
||||
}
|
||||
|
||||
function clearSoundAudio() {
|
||||
stopSoundAudition();
|
||||
for (const url of soundAudioUrls.values()) URL.revokeObjectURL(url);
|
||||
soundAudioUrls.clear(); soundAudioBytes = 0;
|
||||
}
|
||||
|
||||
function soundJsonPaths(source, io) {
|
||||
if (source.files) return [...source.files.keys()].filter((name) => /^assets\/[^/]+\/sounds\.json$/.test(name));
|
||||
const root = io.path.basename(source.path) === 'assets' ? io.path.dirname(source.path) : source.path;
|
||||
const assets = io.path.join(root, 'assets');
|
||||
if (!io.fs.existsSync(assets)) return [];
|
||||
return io.fs.readdirSync(assets, { withFileTypes: true }).slice(0, 256)
|
||||
.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink() && /^[a-z0-9_.-]+$/.test(entry.name))
|
||||
.map((entry) => `assets/${entry.name}/sounds.json`);
|
||||
}
|
||||
|
||||
function loadSoundLibrary(io = desktopFiles()) {
|
||||
clearSoundAudio();
|
||||
const events = new Map(), sources = [], warnings = [];
|
||||
const selected = localStorage.getItem(SOUND_SOURCE_KEY);
|
||||
const paths = uniqueTrimmedStrings([...savedPackFolders(), ...(selected ? [selected] : [])]);
|
||||
for (const file of paths.slice(0, 64)) {
|
||||
try {
|
||||
const source = openPropSource(file, io);
|
||||
sources.push(source);
|
||||
for (const name of soundJsonPaths(source, io)) {
|
||||
const raw = source.read(name);
|
||||
if (!raw) continue;
|
||||
const json = JSON.parse(raw.toString('utf8').replace(/^\uFEFF/, ''));
|
||||
const namespace = name.split('/')[1];
|
||||
for (const [key, value] of Object.entries(json)) {
|
||||
const id = `${namespace}:${key}`;
|
||||
if (!importResourceId(id) || !Array.isArray(value?.sounds) || events.size >= 10000) continue;
|
||||
const entries = value.sounds.slice(0, 256);
|
||||
if (!events.has(id)) events.set(id, { entries, replace: value.replace === true });
|
||||
else if (!events.get(id).replace) {
|
||||
events.get(id).entries.push(...entries);
|
||||
events.get(id).replace = value.replace === true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) { warnings.push(`${io.path.basename(file)}: ${error.message}`); }
|
||||
}
|
||||
soundLibrary = { events, sources, warnings };
|
||||
soundLibraryGeneration++;
|
||||
return soundLibrary;
|
||||
}
|
||||
|
||||
function ensureSoundLibrary() {
|
||||
if (!soundLibrary) {
|
||||
try { loadSoundLibrary(); }
|
||||
catch (_) { soundLibrary = { events: new Map(), sources: [], warnings: ['Sound assets require Blockbench Desktop.'] }; }
|
||||
}
|
||||
return soundLibrary;
|
||||
}
|
||||
|
||||
function soundCueId(cue, mode = soundPreviewMode) {
|
||||
const text = String(cue || '').trim();
|
||||
const bare = text.replace(/^(?:needsofnature|minecraft):/, '');
|
||||
if (bare.toLowerCase() === 'reactiveimpact_silent') return null;
|
||||
const semantic = { reactiveimpact: { dry: 'impactdry01', wet: 'impactwet01', full: 'wet04', peak: 'shot_in01' }[mode] || 'impactdry01',
|
||||
impactdry: 'impactdry01', impactwet: 'impactwet01', birth: 'pop' };
|
||||
if (semantic[bare.toLowerCase()]) return `needsofnature:${semantic[bare.toLowerCase()]}`;
|
||||
const id = text.includes(':') ? text : `needsofnature:${text}`;
|
||||
return importResourceId(id) ? id : null;
|
||||
}
|
||||
|
||||
function resolveSoundAudio(cue, library = ensureSoundLibrary(), visiting = new Set()) {
|
||||
if (/^random\(/i.test(String(cue).trim())) {
|
||||
const match = String(cue).trim().match(/^random\((.*)\)$/i);
|
||||
if (!match) return null;
|
||||
for (const option of match[1].split(',').slice(0, 256)) {
|
||||
const audio = resolveSoundAudio(option, library, visiting);
|
||||
if (audio) return audio;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const id = soundCueId(cue);
|
||||
if (!id || visiting.has(id) || visiting.size >= 16) return null;
|
||||
const event = library.events.get(id);
|
||||
if (!event) return null;
|
||||
const next = new Set(visiting); next.add(id);
|
||||
for (const entry of event.entries) {
|
||||
const object = typeof entry === 'string' ? { name: entry } : entry;
|
||||
if (!object || typeof object.name !== 'string') continue;
|
||||
const name = object.name.includes(':') ? object.name : `minecraft:${object.name}`;
|
||||
const resource = importResourceId(name);
|
||||
if (!resource) continue;
|
||||
if (object.type === 'event') {
|
||||
const audio = resolveSoundAudio(name, library, next);
|
||||
if (audio) return audio;
|
||||
continue;
|
||||
}
|
||||
const asset = `assets/${resource.namespace}/sounds/${resource.resource}.ogg`;
|
||||
if (soundAudioUrls.has(asset)) return soundAudioUrls.get(asset);
|
||||
for (const source of library.sources) {
|
||||
try {
|
||||
const bytes = source.read(asset);
|
||||
if (!bytes) continue;
|
||||
if (soundAudioBytes + bytes.length > 64 * 1024 * 1024) return null;
|
||||
const url = URL.createObjectURL(new Blob([bytes], { type: 'audio/ogg' }));
|
||||
soundAudioUrls.set(asset, url); soundAudioBytes += bytes.length;
|
||||
return url;
|
||||
} catch (_) { /* An unavailable asset must never prevent animation import. */ }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function mapAnimationSounds(animations) {
|
||||
for (const animation of animations || []) {
|
||||
for (const keyframe of animation?.animators?.effects?.sound || []) {
|
||||
for (const point of keyframe.data_points || []) {
|
||||
if (point.file && point.file !== point[SOUND_PREVIEW_PROPERTY]) continue;
|
||||
const signature = `${soundLibraryGeneration}:${soundPreviewMode}:${point.effect}:${point.sound}:${point.file}`;
|
||||
if (soundMappedPoints.get(point) === signature) continue;
|
||||
const audio = resolveSoundAudio(point.effect) || (point.sound ? resolveSoundAudio(point.sound) : null);
|
||||
point.file = audio || '';
|
||||
point[SOUND_PREVIEW_PROPERTY] = point.file;
|
||||
soundMappedPoints.set(point, `${soundLibraryGeneration}:${soundPreviewMode}:${point.effect}:${point.sound}:${point.file}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setSoundPreviewMode(mode) {
|
||||
soundPreviewMode = ['dry', 'wet', 'full', 'peak'].includes(mode) ? mode : 'dry';
|
||||
mapAnimationSounds(typeof Animation === 'undefined' ? [] : Animation.all);
|
||||
}
|
||||
|
||||
function applyLibrarySound(id, insert = false) {
|
||||
const animation = typeof Animation === 'undefined' ? null : Animation.selected;
|
||||
if (!animation) throw new Error('Select an animation first.');
|
||||
const selected = (Timeline.selected || []).filter((keyframe) => keyframe.channel === 'sound' && keyframe.animator?.animation === animation);
|
||||
if (!insert && selected.length) {
|
||||
Undo.initEdit({ keyframes: selected });
|
||||
for (const keyframe of selected) {
|
||||
const point = keyframe.data_points[0];
|
||||
point.effect = id; point.file = ''; point[SOUND_PREVIEW_PROPERTY] = '';
|
||||
}
|
||||
mapAnimationSounds([animation]);
|
||||
Undo.finishEdit('Apply sound from library');
|
||||
} else {
|
||||
const effects = animation.animators.effects || (animation.animators.effects = new EffectAnimator(animation));
|
||||
const keyframes = effects.sound.filter((frame) => Math.abs(frame.time - Timeline.time) < 0.00001);
|
||||
Undo.initEdit({ keyframes });
|
||||
let keyframe = keyframes[0];
|
||||
if (keyframe) {
|
||||
keyframe.extend({ data_points: [...keyframe.data_points.map((point) => point.getUndoCopy()), { effect: id }] });
|
||||
} else {
|
||||
keyframe = effects.createKeyframe({ data_points: [{ effect: id }] }, Timeline.time, 'sound', false, false);
|
||||
keyframes.push(keyframe);
|
||||
}
|
||||
mapAnimationSounds([animation]);
|
||||
effects.select(); keyframe.select();
|
||||
Undo.finishEdit('Insert sound from library', { keyframes });
|
||||
}
|
||||
if (typeof updateKeyframeSelection === 'function') updateKeyframeSelection();
|
||||
}
|
||||
|
||||
function showSoundLibrary() {
|
||||
stopSoundAudition();
|
||||
ensureSoundLibrary();
|
||||
soundLibraryDialog?.delete();
|
||||
soundLibraryDialog = new Dialog({ id: 'animationdirector_sound_library', title: 'Sound Library', width: 700,
|
||||
buttons: ['Close'], onConfirm: stopSoundAudition, onCancel: stopSoundAudition,
|
||||
component: {
|
||||
data() { return { query: '', category: 'all', selected: '', mode: soundPreviewMode, page: 0,
|
||||
ids: [...soundLibrary.events.keys()].sort(), warnings: soundLibrary.warnings.join('\n'), error: '' }; },
|
||||
computed: {
|
||||
rows() {
|
||||
const cues = ['reactiveimpact', 'reactiveimpact_silent', 'impactdry', 'impactwet', 'birth'];
|
||||
return [...cues.map((id) => ({ id, category: 'Gameplay cues' })), ...this.ids.map((id) => ({ id, category: 'Sound events' }))]
|
||||
.filter((entry) => (this.category === 'all' || entry.category === this.category) && entry.id.toLowerCase().includes(this.query.toLowerCase()));
|
||||
},
|
||||
visibleRows() { return this.rows.slice(this.page * 100, (this.page + 1) * 100); },
|
||||
},
|
||||
watch: { query() { this.page = 0; }, category() { this.page = 0; } },
|
||||
methods: {
|
||||
reload() {
|
||||
try { loadSoundLibrary(); this.ids = [...soundLibrary.events.keys()].sort(); this.warnings = soundLibrary.warnings.join('\n');
|
||||
this.error = ''; mapAnimationSounds(typeof Animation === 'undefined' ? [] : Animation.all); }
|
||||
catch (error) { this.error = error.message; }
|
||||
},
|
||||
folder() {
|
||||
try { const folder = Blockbench.pickDirectory({ title: 'Select folder containing assets', resource_id: SOUND_SOURCE_KEY });
|
||||
if (folder) { localStorage.setItem(SOUND_SOURCE_KEY, folder); this.reload(); } }
|
||||
catch (error) { this.error = error.message; }
|
||||
},
|
||||
jar() {
|
||||
Filesystem.importFile({ type: 'Sound assets', extensions: ['jar', 'zip'], readtype: 'none', multiple: false }, (files) => {
|
||||
if (files?.[0]?.path) { localStorage.setItem(SOUND_SOURCE_KEY, files[0].path); this.reload(); }
|
||||
});
|
||||
},
|
||||
preview() {
|
||||
stopSoundAudition(); this.error = '';
|
||||
const file = resolveSoundAudio(this.selected);
|
||||
if (!file) { this.error = this.selected === 'reactiveimpact_silent' ? 'Silent cue: gameplay effects only.' : 'No audio found. Select the NoN JAR or an extracted assets folder.'; return; }
|
||||
soundAudition = new Audio(file);
|
||||
soundAudition.volume = Math.max(0, Math.min(1, typeof settings === 'undefined' ? 1 : (settings.volume?.value ?? 100) / 100));
|
||||
soundAudition.play().catch((error) => { this.error = error.message; });
|
||||
},
|
||||
stop: stopSoundAudition,
|
||||
apply(insert) { try { applyLibrarySound(this.selected, insert); this.error = ''; } catch (error) { this.error = error.message; } },
|
||||
modeChanged() { stopSoundAudition(); setSoundPreviewMode(this.mode); },
|
||||
},
|
||||
template: `<div style="padding:12px">
|
||||
<div class="bar flex" style="gap:6px"><button @click="jar">Select NoN JAR / ZIP</button><button @click="folder">Select Assets Folder</button><button @click="reload">Refresh</button></div>
|
||||
<p class="subtle">Uses the selected sound source and remembered Pack Folders. Previews never change exported cue IDs.</p>
|
||||
<div class="bar flex" style="gap:8px;margin:10px 0"><input v-model="query" placeholder="Search sound IDs" style="flex:1" aria-label="Search sound IDs"><select v-model="category"><option value="all">All sounds</option><option>Sound events</option><option>Gameplay cues</option></select></div>
|
||||
<label>Reactive preview <select v-model="mode" @change="modeChanged"><option value="dry">Dry</option><option value="wet">Wet</option><option value="full">Full</option><option value="peak">Peak</option></select></label>
|
||||
<p class="subtle">Gameplay cues and random events use representative audio, not a gameplay simulation.</p>
|
||||
<div style="height:260px;overflow-y:auto;margin:10px 0;border:1px solid var(--color-border)">
|
||||
<div v-for="row in visibleRows" :key="row.category+row.id" @click="selected=row.id;stop()" @dblclick="selected=row.id;preview()" :style="{background:selected===row.id?'var(--color-accent)':'',padding:'6px 10px',cursor:'pointer'}"><span>{{row.id}}</span><small style="float:right;opacity:.6">{{row.category}}</small></div>
|
||||
</div>
|
||||
<div class="bar flex" style="gap:6px"><button @click="page--" :disabled="page===0">Previous</button><span>{{rows.length}} results</span><button @click="page++" :disabled="(page+1)*100>=rows.length">Next</button></div>
|
||||
<div class="bar flex" style="gap:6px;margin-top:12px"><button @click="preview" :disabled="!selected">Play</button><button @click="stop">Stop</button><button @click="apply(false)" :disabled="!selected">Apply to Selected / Insert</button><button @click="apply(true)" :disabled="!selected">Insert at Playhead</button></div>
|
||||
<p style="color:var(--color-error);white-space:pre-wrap">{{error}}</p><p class="subtle" style="white-space:pre-wrap">{{warnings}}</p>
|
||||
</div>`,
|
||||
},
|
||||
});
|
||||
soundLibraryDialog.show();
|
||||
}
|
||||
|
||||
function refreshSoundLibraryPanel() {
|
||||
if (typeof document !== 'undefined') {
|
||||
const panel = document.getElementById('panel_keyframe');
|
||||
if (panel && !document.getElementById('animationdirector_sound_library_button')) {
|
||||
const button = document.createElement('button');
|
||||
button.id = 'animationdirector_sound_library_button'; button.textContent = 'Sound Library...';
|
||||
button.style.cssText = 'margin:8px;width:calc(100% - 16px)';
|
||||
button.addEventListener('click', showSoundLibrary); panel.appendChild(button);
|
||||
}
|
||||
}
|
||||
mapAnimationSounds(typeof Animation === 'undefined' ? [] : [Animation.selected]);
|
||||
}
|
||||
|
||||
Plugin.register(PLUGIN_ID, {
|
||||
title: 'AnimationDirector',
|
||||
author: 'L1Z0',
|
||||
@@ -5326,6 +5604,19 @@ You can import or export complete AFW animation IDs as one file containing all s
|
||||
version: '1.1.0',
|
||||
variant: 'both',
|
||||
onload() {
|
||||
if (typeof Property === 'function' && typeof KeyframeDataPoint !== 'undefined') {
|
||||
soundPreviewProperty = new Property(KeyframeDataPoint, 'string', SOUND_PREVIEW_PROPERTY, {
|
||||
exposed: false, condition: (point) => point.keyframe.channel === 'sound',
|
||||
});
|
||||
}
|
||||
soundLibraryAction = new Action('animationdirector_sound_library', {
|
||||
name: 'Sound Library...', icon: 'library_music', category: 'animation',
|
||||
description: 'Browse NoN and pack sounds, audition audio and add sound cues to the timeline.',
|
||||
click: showSoundLibrary,
|
||||
});
|
||||
soundLibraryListener = refreshSoundLibraryPanel;
|
||||
Blockbench.on(SOUND_EVENTS.join(' '), soundLibraryListener);
|
||||
refreshSoundLibraryPanel();
|
||||
if (typeof Property === 'function' && typeof ModelProject !== 'undefined' && ModelProject?.properties) {
|
||||
if (!ModelProject.properties[ACTOR_NAMES_PROPERTY]) {
|
||||
actorNamesProjectProperty = new Property(ModelProject, 'string', ACTOR_NAMES_PROPERTY, {
|
||||
@@ -5514,6 +5805,7 @@ You can import or export complete AFW animation IDs as one file containing all s
|
||||
replaceActorAction,
|
||||
blockRequirementAction,
|
||||
propAction,
|
||||
soundLibraryAction,
|
||||
createAnimationDefinitionAction,
|
||||
exportConjoinedAction,
|
||||
],
|
||||
@@ -5528,6 +5820,10 @@ You can import or export complete AFW animation IDs as one file containing all s
|
||||
MenuBar.update();
|
||||
},
|
||||
onunload() {
|
||||
for (const event of SOUND_EVENTS) Blockbench.removeListener(event, soundLibraryListener);
|
||||
if (typeof document !== 'undefined') document.getElementById('animationdirector_sound_library_button')?.remove();
|
||||
soundLibraryAction?.delete(); soundLibraryDialog?.delete(); soundPreviewProperty?.delete();
|
||||
clearSoundAudio(); soundLibrary = undefined;
|
||||
clearPropMeshes(); propSources.clear();
|
||||
propAction?.delete(); propDialog?.delete(); propProjectProperty?.delete(); actorUvBasisProjectProperty?.delete();
|
||||
if (propListener) for (const event of ['select_animation','select_project','load_project','unselect_project','undo','redo','render_frame']) Blockbench.removeListener(event, propListener);
|
||||
|
||||
Reference in New Issue
Block a user