Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2fa581a
Make animation Save and Export produce identical library files
codename-B Aug 12, 2026
92f4798
Preview VS particle keyframes in the Blockbench viewport
codename-B Aug 12, 2026
6548d58
Export and import sound keyframes on animations
codename-B Aug 12, 2026
81c0bc4
Exclude dist from typecheck and fix step parent frame narrowing
codename-B Aug 12, 2026
cbeef56
Declare bbmodel import types and the requireNativeModule global
codename-B Aug 12, 2026
e232700
Remove stale ts-expect-error directives
codename-B Aug 12, 2026
1ac5e21
Author sound range, volume, pitch, chance and looping in the keyframe…
codename-B Aug 12, 2026
963df74
Fix undo amend flag, codec selection and Blockbench type gaps
codename-B Aug 12, 2026
4cd1091
Re-resolve particle and sound file paths on project load
codename-B Aug 12, 2026
b98166a
Prefer the sound effect code over the picked file when exporting
codename-B Aug 12, 2026
cc912f5
Namespace particle components so Blockbench can preview VS effects
codename-B Aug 12, 2026
6215c2a
Use native path separators so particle textures resolve
codename-B Aug 12, 2026
8703125
Link effect files from the render path instead of on project load
codename-B Aug 12, 2026
cecdec0
Stop one-shot animations retriggering effects on their end rewind
codename-B Aug 12, 2026
43ea7f8
Latch the one-shot rewind so the final preview pass stays muted
codename-B Aug 12, 2026
7448fe6
Qualify bare sound codes with the resolved domain and folder
codename-B Aug 12, 2026
7693fe9
vfx playback pass
codename-B Aug 13, 2026
7673a3b
polish pass/fix tests
codename-B Oct 4, 2026
c2bb391
missing function reload_animations
codename-B Oct 4, 2026
46760f8
direct types
codename-B Oct 4, 2026
bf8a9fe
traversal helper
codename-B Oct 4, 2026
79a176a
more cleanup
codename-B Oct 4, 2026
5a12c85
improve node here too
codename-B Oct 4, 2026
250286c
additional typing checks
codename-B Oct 4, 2026
0446ad7
another typing error cleanup pass
codename-B Oct 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@ import * as PACKAGE from "../package.json";
import { is_vs_project } from "./util";
import { im } from "./import";
import { is_backdrop_project } from "./util/misc";
import { codecVS } from "./codec";
import { ex } from "./export";

// @ts-expect-error: requireNativeModule is missing in blockbench types --- IGNORE ---
const fs = requireNativeModule('fs');
// @ts-expect-error: requireNativeModule is missing in blockbench types --- IGNORE ---
const path = requireNativeModule('path');

const export_action = createAction(`${PACKAGE.name}:export_vs`, {
Expand Down Expand Up @@ -86,4 +83,4 @@ const import_backdrop_action = createAction(`${PACKAGE.name}:import_backdrop_act
});
}
});
MenuBar.addAction(import_backdrop_action, 'file.import');
MenuBar.addAction(import_backdrop_action, 'file.import');
23 changes: 22 additions & 1 deletion src/animation_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createAction } from "./util/moddingTools";
import * as PACKAGE from "../package.json";
import { is_vs_project } from "./util";
import { clear_animations } from "./import_animation";
import { relink_particle_previews } from "./animation_particles";

// Importing and exporting VS animation files is handled natively by the multi-file
// animation workflow (format `animation_files` + the VS AnimationCodec): use the
Expand All @@ -15,7 +16,7 @@ const clear_animations_action = createAction(`${PACKAGE.name}:clear_animations_v
return is_vs_project(Project);
},
click: function () {
const total = (Animation as unknown as typeof _Animation).all.length;
const total = Blockbench.Animation.all.length;
if (total === 0) {
Blockbench.showQuickMessage('No animations to clear');
return;
Expand All @@ -28,3 +29,23 @@ const clear_animations_action = createAction(`${PACKAGE.name}:clear_animations_v
}
});
MenuBar.addAction(clear_animations_action, 'edit');

// Particle keyframes only preview once their effect code has been resolved to a Snowstorm
// emitter file. Import does that automatically; this re-runs it for hand-authored keyframes
// and for effects added to the assets folder since the model was opened.
const reload_vfx_action = createAction(`${PACKAGE.name}:reload_vfx_previews`, {
name: 'Reload VFX Previews',
icon: 'auto_awesome',
condition() {
return is_vs_project(Project);
},
click: function () {
const linked = relink_particle_previews();
Blockbench.showQuickMessage(
linked === 0
? 'No particle effects could be resolved'
: `Linked ${linked} particle keyframe${linked === 1 ? '' : 's'}`
);
}
});
MenuBar.addAction(reload_vfx_action, 'animation');
110 changes: 64 additions & 46 deletions src/animation_codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@ import { compile_animation, compile_animation_library } from "./export_animation
import { create_animation } from "./import_animation";
import { VS_Animation, VS_AnimationLibrary } from "./vs_shape_def";
import { parse_model_location, basename_no_ext } from "./animation_library_paths";
import JSON5 from "json5";

// @ts-expect-error: requireNativeModule is missing in blockbench types --- IGNORE ---
const fs = requireNativeModule('fs');

// Remembers each loaded library file's optional `code`/`name` so re-saving preserves them
// (the engine keys animations by their own `code`, so these are cosmetic, but preserving
// them keeps library files diff-stable across a round trip). Keyed by absolute file path.
const libraryMeta = new Map<string, { code?: string, name?: string }>();

/** All animations, with Blockbench's runtime `Animation` cast to the `_Animation` type. */
/** All animations in the current project. */
function all_animations(): _Animation[] {
return (Animation as unknown as typeof _Animation).all;
return Blockbench.Animation.all;
}

/** Default directory the file dialogs open in: `<assets>/<domain>/animations`. */
Expand All @@ -55,11 +55,18 @@ function build_library(path: string | undefined, animations: VS_Animation[]): VS
return library;
}

function read_library(path: string | undefined): VS_AnimationLibrary | null {
if (!path || !fs.existsSync(path)) return null;
const library = JSON5.parse(fs.readFileSync(path, 'utf-8')) as VS_AnimationLibrary;
if (!Array.isArray(library.animations)) throw new Error(`Invalid animation library: ${path}`);
return library;
}

/** Parses a VS animation library file and adds its animations (grouped under `file.path`). */
function load_file(file: AnimationCodecFile, animation_filter?: string[]): _Animation[] {
const json = (file.json ?? autoParseJSON(file.content as string)) as VS_AnimationLibrary;
const json = (file.json ?? JSON5.parse(file.content as string)) as VS_AnimationLibrary;
const created: _Animation[] = [];
if (!json || !Array.isArray(json.animations)) return created;
if (!Array.isArray(json.animations)) throw new Error(`Invalid animation library: ${file.path}`);

libraryMeta.set(file.path, { code: json.code, name: json.name });

Expand All @@ -70,11 +77,31 @@ function load_file(file: AnimationCodecFile, animation_filter?: string[]): _Anim
return created;
}

/** Compiles a set of animations into a full VS library object. */
function compile_file(animations: _Animation[]): VS_AnimationLibrary {
function compile_file(
animations: _Animation[],
compiled = compile_animation_library(animations).animations,
): VS_AnimationLibrary {
const path = animations[0]?.path;
const compiled = compile_animation_library(animations).animations;
return build_library(path, compiled);
const sharedPath = path && animations.every(a => a.path === path) ? path : undefined;
const library = read_library(sharedPath);
if (!library) return build_library(sharedPath, compiled);

if (sharedPath) libraryMeta.set(sharedPath, { code: library.code, name: library.name });
const renamedFrom = new Map<string, string>();
for (const a of animations) {
if (a.saved_name && a.saved_name !== a.name) renamedFrom.set(a.name, a.saved_name);
}
for (const vsAnim of compiled) {
const oldName = renamedFrom.get(vsAnim.name) ?? vsAnim.name;
const idx = library.animations.findIndex(a => a.name === oldName || a.name === vsAnim.name);
if (idx >= 0) {
library.animations[idx] = vsAnim;
library.animations = library.animations.filter((a, i) => i === idx || a.name !== vsAnim.name);
} else {
library.animations.push(vsAnim);
}
}
return library;
}

/** Opens a dialog to import one or more VS animation library files. */
Expand All @@ -94,7 +121,27 @@ function pick_file(): void {

/** Loads a single picked file into the project (called by pickFile and the file menu). */
function import_file(file: AnimationCodecFile): _Animation[] {
return load_file(file);
Undo.initEdit({ animations: [], outliner: true, elements: Cube.all });
const created = load_file(file);
Undo.finishEdit('Import animations', { animations: created });
return created;
}

function reload_animations(animations: _Animation[]): void {
if (!animations.length) return;
const path = animations[0].path;
const json = JSON5.parse(fs.readFileSync(path, 'utf-8')) as VS_AnimationLibrary;
if (!Array.isArray(json.animations)) throw new Error(`Invalid animation library: ${path}`);
const selected = animations.find(a => a.selected);
Undo.initEdit({ animations, outliner: true, elements: Cube.all });
animations.forEach(a => a.remove(false, false));
const created = load_file({ path, json }, animations.map(a => a.saved_name || a.name));
for (const animation of created) {
const previous = animations.find(a => (a.saved_name || a.name) === animation.name)!;
animation.vs_library_ref = previous.vs_library_ref;
if (previous === selected) animation.select();
}
Undo.finishEdit('Reload animations', { animations: created });
}

/**
Expand All @@ -112,32 +159,7 @@ function write_animation_to_library(animation: _Animation): void {
}
const path = animation.path;

let existing: VS_AnimationLibrary | null = null;
if (fs.existsSync(path)) {
try {
existing = autoParseJSON(fs.readFileSync(path, 'utf-8')) as VS_AnimationLibrary;
} catch (e) {
console.error('[VS Animation Codec] Failed to read existing library, overwriting:', e);
existing = null;
}
}

let library: VS_AnimationLibrary;
if (existing && Array.isArray(existing.animations)) {
library = existing;
libraryMeta.set(path, { code: existing.code, name: existing.name });
const oldName = animation.saved_name ?? vsAnim.name;
const idx = library.animations.findIndex(a => a.name === oldName || a.name === vsAnim.name);
if (idx >= 0) {
library.animations[idx] = vsAnim;
// Drop any later duplicate left over from a rename.
library.animations = library.animations.filter((a, i) => i === idx || a.name !== vsAnim.name);
} else {
library.animations.push(vsAnim);
}
} else {
library = build_library(path, [vsAnim]);
}
const library = compile_file([animation], [vsAnim]);

Blockbench.writeFile(path, { content: autoStringify(library) }, (real_path) => {
animation.saved = true;
Expand All @@ -161,6 +183,7 @@ function save_animation(animation: _Animation, save_as?: boolean): void {
custom_writer: (_content, exportPath) => {
if (!exportPath) return;
animation.path = exportPath;
delete animation.vs_library_ref;
write_animation_to_library(animation);
}
});
Expand Down Expand Up @@ -199,7 +222,7 @@ function export_file(path: string, save_as?: boolean): void {
startpath: filterPath || default_animations_dir(),
custom_writer: (_content, exportPath) => {
if (!exportPath) return;
animations.forEach(a => { a.path = exportPath; });
animations.forEach(a => { a.path = exportPath; delete a.vs_library_ref; });
fs.writeFileSync(exportPath, autoStringify(compile_file(animations)));
animations.forEach(a => { a.saved = true; a.saved_name = a.name; });
},
Expand All @@ -209,15 +232,8 @@ function export_file(path: string, save_as?: boolean): void {
/** Removes an animation's entry from its library file (when deleted from the project). */
function delete_animation_from_file(animation: _Animation): void {
const path = animation.path;
if (!path || !fs.existsSync(path)) return;
let library: VS_AnimationLibrary | null = null;
try {
library = autoParseJSON(fs.readFileSync(path, 'utf-8')) as VS_AnimationLibrary;
} catch (e) {
console.error('[VS Animation Codec] Failed to read library for deletion:', e);
return;
}
if (!library || !Array.isArray(library.animations)) return;
const library = read_library(path);
if (!library) return;
const target = animation.saved_name ?? animation.name;
library.animations = library.animations.filter(a => a.name !== target);
Blockbench.writeFile(path, { content: autoStringify(library) });
Expand All @@ -235,6 +251,8 @@ function create_vs_animation_codec(): AnimationCodec | undefined {
pickFile: pick_file,
importFile: import_file,
loadFile: load_file,
reloadFile: path => reload_animations(all_animations().filter(a => a.path === path && a.saved)),
reloadAnimation: animation => reload_animations([animation]),
compileAnimation: (animation) => compile_animation(animation),
compileFile: compile_file,
saveAnimation: save_animation,
Expand Down
Loading
Loading