diff --git a/plugins.json b/plugins.json
index 05e64263..72efc131 100644
--- a/plugins.json
+++ b/plugins.json
@@ -394,17 +394,21 @@
},
"animation_utils": {
"title": "GeckoLib Animation Utils",
- "author": "Eliot Lash, Gecko, McHorse, AzureDoom, Tslat",
+ "author": "Eliot Lash, Tslat, Gecko, McHorse",
"icon": "icon.png",
"description": "Create animated blocks, items, entities, and armor using the GeckoLib library and plugin.",
"tags": [
"Minecraft: Java Edition"
],
- "version": "3.2.1",
+ "version": "4.0",
"min_version": "4.11.0",
"await_loading": true,
"variant": "both",
- "max_version": "5.0.0"
+ "max_version": "5.0.0",
+ "has_changelog": true,
+ "website": "https://github.com/bernie-g/geckolib/wiki",
+ "repository": "https://github.com/JannisX11/blockbench-plugins/tree/master/plugins/animation_utils",
+ "bug_tracker": "https://github.com/bernie-g/geckolib/issues"
},
"modded_entity_fabric": {
"title": "Fabric Modded Entity",
diff --git a/plugins/animation_utils/CHANGELOG.md b/plugins/animation_utils/CHANGELOG.md
deleted file mode 100644
index 9a9f392e..00000000
--- a/plugins/animation_utils/CHANGELOG.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# GeckoLib Animation Utils Changelog
-
-## Legend
-💥 = Breaking Change
-🚀 = New Feature
-🐞 = Bug Fix
-🦎 = Non-user-facing Change
-
-## 3.2.1
-- 🐞 Fix some animation exporting issues with specific animation setup cases
-
-## 3.2
-- 🚀 Auto-export the particle texture entry in the textures list for block/item display jsons if not defined
-- 🚀 Auto-convert bedrock animation jsons to GeckoLib-supported animation jsons when exporting
-- 🐞 Fix the particle texture entry not exporting if the name doesn't end in .png
-- 🐞 Fixed item_display_transforms being shipped with .geo jsons for non-bedrock models
-- 🐞 Forced known forward-compatible versions to export as 1.12.0 to maintain compatibility while we work out a better system
-
-## 3.1.1
-- 🐞 Fix the item display settings being cleared if saving as an entity type model
-- 🐞 Fix the armour template having swapped pivot points on the legs
-- 🐞 Fix incorrect importing of loop type. Closes [#591](https://github.com/bernie-g/geckolib/issues/591)
-
-## 3.1.0
-- 💥 Update to new plugin format, bump minimum Blockbench version to 4.8.0
-- 🚀 Added support for "Reverse Keyframes" action
-- 🦎 Ported plugin to TypeScript, added developer [README](./README.md) and a few unit tests
-
-## 3.0.7
-- 🐞 Don't save `geckolib_format_version` in animation json for bedrock models
-- 🐞 Remove hold menu hiding code that was causing issues for other plugins (regression of an old bug occurred in version 3.0.6)
-- 🦎 Disable minification of JS bundle, fix some build errors on case sensitive filesystems, and upgrade to NodeJS v16.16
\ No newline at end of file
diff --git a/plugins/animation_utils/README.md b/plugins/animation_utils/README.md
index ae7a4729..83015fb6 100644
--- a/plugins/animation_utils/README.md
+++ b/plugins/animation_utils/README.md
@@ -67,7 +67,8 @@ npm run build
```
This will first run prebuild/pretest/test scripts, then build the plugin and automatically update the [plugins.json](../../plugins.json) manifest with your settings.
-Then, update the [CHANGELOG](./CHANGELOG.md) to add patch notes for the new version.
+Then, update the [changelog](./changelog.json) to add patch notes for the new version.
+New version entries go at the bottom.
Double-check everything looks right, then commit and make a PR to [JannisX11/blockbench-plugins](https://github.com/JannisX11/blockbench-plugins) to release the plugin.
diff --git a/plugins/animation_utils/about.md b/plugins/animation_utils/about.md
index 859beeb4..186bbe8f 100644
--- a/plugins/animation_utils/about.md
+++ b/plugins/animation_utils/about.md
@@ -1,5 +1,32 @@
-GeckoLib is a powerful animation library for Minecraft Java Edition available for Forge and Fabric. Out of the box support for easings, math-based animations, resource pack overloading, and animation stacking. This plugin allows you to create animated assets for use in your GeckoLib mods.
+
-See the [GeckoLib wiki](https://github.com/bernie-g/geckolib/wiki) for help using the plugin and library.
+Blockbench plugin for the GeckoLib animation and rendering library mod for Minecraft: Java edition.
+This plugin allows you to create and edit model and animation assets for GeckoLib-based mods, utilising Blockbench's live animation editor and other editing tools.
-[Changelog (patch notes)](https://github.com/JannisX11/blockbench-plugins/blob/master/plugins/animation_utils/CHANGELOG.md)
+Utilise native support for resourcepack-able controller-based animations, mathematical expressions, easings, and more to create armour, items, entities, blocks, and anything else you can think of!
+
+Must be paired with a Mod for Minecraft: Java utilising the GeckoLib library.
+
+
model_identifier
form element in dialog windows
+ */
+function getObjectIdPlaceholder(formResult) {
+ const name = formResult === null || formResult === void 0 ? void 0 : formResult['name'];
+ const modelType = formResult === null || formResult === void 0 ? void 0 : formResult[_constants__WEBPACK_IMPORTED_MODULE_0__.PROPERTY_MODEL_TYPE];
+ if (!name && !modelType)
+ return 'my_entity';
+ const invalidPathChar = new RegExp('[^_\\-/.a-z0-9]+', 'g');
+ const pseudoWhitepaceChar = new RegExp('[\\s&-]+', 'g');
+ if (name)
+ return name.toLowerCase().replace(pseudoWhitepaceChar, "_").replace(invalidPathChar, "");
+ switch (_constants__WEBPACK_IMPORTED_MODULE_0__.GeckoModelType[modelType]) {
+ case _constants__WEBPACK_IMPORTED_MODULE_0__.GeckoModelType.ENTITY: return 'my_entity';
+ case _constants__WEBPACK_IMPORTED_MODULE_0__.GeckoModelType.BLOCK: return 'my_block';
+ case _constants__WEBPACK_IMPORTED_MODULE_0__.GeckoModelType.ITEM: return 'my_item';
+ case _constants__WEBPACK_IMPORTED_MODULE_0__.GeckoModelType.ARMOR: return 'my_armor';
+ case _constants__WEBPACK_IMPORTED_MODULE_0__.GeckoModelType.OBJECT: return 'my_object';
+ default: return 'my_entity';
+ }
+}
+/**
+ * Create the Project Settings dialog form for use in both new projects and editing existing ones
+ */
+function createProjectSettingsForm(Project) {
+ const form = { format: { type: 'info', label: 'data.format', text: Format.name || 'unknown', description: Format.description } };
+ const properties = ModelProject['properties'];
+ const modelType = properties[_constants__WEBPACK_IMPORTED_MODULE_0__.PROPERTY_MODEL_TYPE];
+ if (modelType) {
+ form[_constants__WEBPACK_IMPORTED_MODULE_0__.PROPERTY_MODEL_TYPE] = {
+ label: modelType.label,
+ description: modelType.description,
+ default: _constants__WEBPACK_IMPORTED_MODULE_0__.GeckoModelType.ENTITY.toUpperCase(),
+ value: _constants__WEBPACK_IMPORTED_MODULE_0__.GeckoModelType[Project[_constants__WEBPACK_IMPORTED_MODULE_0__.PROPERTY_MODEL_TYPE].toUpperCase()].toUpperCase(),
+ placeholder: modelType.placeholder,
+ type: 'select',
+ options: typeof modelType.options == 'function' ? modelType.options() : modelType.options,
+ };
}
- else if (typeof source == 'object') {
- if (source.vector) {
- return getKeyframeDataPoints(source.vector);
- }
- const points = [];
- if (source.pre) {
- points.push(getKeyframeDataPoints(source.pre)[0]);
- }
- if (source.post) {
- points.push(getKeyframeDataPoints(source.post)[0]);
+ for (const key in properties) {
+ const property = properties[key];
+ if (property.exposed === false || !Condition(property.condition))
+ continue;
+ const entry = form[property.name] = {
+ label: property.label,
+ description: property.description,
+ value: Project[property.name],
+ placeholder: property.placeholder,
+ type: property.type
+ };
+ if (property.name === 'name') {
+ entry.label = 'Project Name';
+ entry.placeholder = 'My Project';
+ entry.description = 'The name of the Blockbench project';
}
- return points;
- }
-}
-function geoLoopToBbLoop(jsonLoop) {
- if (jsonLoop) {
- if (typeof jsonLoop === 'boolean') {
- return jsonLoop ? 'loop' : 'once';
+ else if (property.name === 'model_identifier') {
+ entry.label = 'Object ID';
+ entry.description = 'The registered id of the object this model represents, for exporting purposes';
+ entry.placeholder = getObjectIdPlaceholder();
}
- if (typeof jsonLoop === 'string') {
- if (jsonLoop === "hold_on_last_frame")
- return 'hold';
- if (jsonLoop === "loop" || jsonLoop === "true")
- return 'loop';
+ switch (property.type) {
+ case 'boolean':
+ entry.type = 'checkbox';
+ break;
+ case 'string':
+ entry.type = 'text';
+ break;
+ default:
+ if (property.options) {
+ entry['options'] = typeof property.options == 'function' ? property.options() : property.options;
+ entry.type = 'select';
+ }
+ break;
}
}
- return 'once';
-}
-function animatorLoadFile(file, animation_filter) {
- // Currently no modifications are needed
- // eslint-disable-next-line no-undef
- const json = file.json || autoParseJSON(file.content);
- const path = file.path;
- const new_animations = [];
- if (json && typeof json.animations === 'object') {
- for (const ani_name in json.animations) {
- if (animation_filter && !animation_filter.includes(ani_name))
- continue;
- //Animation
- const a = json.animations[ani_name];
- const animation = new Blockbench.Animation({
- name: ani_name,
- path,
- loop: geoLoopToBbLoop(a.loop),
- override: a.override_previous_animation,
- anim_time_update: (typeof a.anim_time_update == 'string'
- ? a.anim_time_update.replace(/;(?!$)/, ';\n')
- : a.anim_time_update),
- blend_weight: (typeof a.blend_weight == 'string'
- ? a.blend_weight.replace(/;(?!$)/, ';\n')
- : a.blend_weight),
- length: a.animation_length
- }).add();
- //Bones
- if (a.bones) {
- for (const bone_name in a.bones) {
- const b = a.bones[bone_name];
- const lowercase_bone_name = bone_name.toLowerCase();
- const group = Group.all.find(group => group.name.toLowerCase() == lowercase_bone_name);
- const uuid = group ? group.uuid : guid();
- let ga; // eslint-disable-line @typescript-eslint/no-unused-vars
- const ba = new GeckolibBoneAnimator(uuid, animation, bone_name);
- animation.animators[uuid] = ba;
- //Channels
- for (const channel in b) {
- if (Animator.possible_channels[channel]) {
- if (typeof b[channel] === 'string' || typeof b[channel] === 'number' || b[channel] instanceof Array) {
- ba.addKeyframe({
- time: 0,
- channel,
- easing: b[channel].easing,
- easingArgs: b[channel].easingArgs,
- data_points: getKeyframeDataPoints(b[channel]),
- });
- }
- else if (typeof b[channel] === 'object' && b[channel].post) {
- ba.addKeyframe({
- time: 0,
- channel,
- easing: b[channel].easing,
- easingArgs: b[channel].easingArgs,
- interpolation: b[channel].lerp_mode,
- data_points: getKeyframeDataPoints(b[channel]),
- });
- }
- else if (typeof b[channel] === 'object') {
- for (const timestamp in b[channel]) {
- ba.addKeyframe({
- time: parseFloat(timestamp),
- channel,
- easing: b[channel][timestamp].easing,
- easingArgs: b[channel][timestamp].easingArgs,
- interpolation: b[channel][timestamp].lerp_mode,
- data_points: getKeyframeDataPoints(b[channel][timestamp]),
- });
- }
+ if (form['name'] && (Project.save_path || Project.export_path || Format.image_editor) && !Format['legacy_editable_file_name'])
+ delete form['name'];
+ form['uv_mode'] = {
+ label: 'dialog.project.default_uv_mode',
+ description: 'dialog.project.default_uv_mode.description',
+ type: 'select',
+ condition: Format.optional_box_uv,
+ options: {
+ face_uv: 'dialog.project.uv_mode.face_uv',
+ box_uv: 'dialog.project.uv_mode.box_uv',
+ },
+ value: Project.box_uv ? 'box_uv' : 'face_uv',
+ };
+ form['texture_size'] = {
+ label: 'dialog.project.texture_size',
+ type: 'vector',
+ dimensions: 2,
+ value: [Project.texture_width, Project.texture_height],
+ min: 1
+ };
+ return form;
+}
+/**
+ * Create the 'new project' popup dialogue for GeckoLib projects.
+ *
+ * The contents of this is mostly a copy of project.js
"project_window" action declaration (Copyright Blockbench)
+ * Periodically check this is up-to-date with Blockbench to ensure ongoing compatibility
+ * @return false if the user clicks cancel
, otherwise true
+ */
+function createProjectSettingsDialog(Project, form) {
+ const dialog = new Dialog({
+ id: 'project',
+ title: 'dialog.project.title',
+ width: 500,
+ form,
+ onConfirm: function (formResult) {
+ let save;
+ const box_uv = formResult['uv_mode'] == 'box_uv';
+ const texture_width = Math.clamp(formResult['texture_size'][0], 1, Infinity);
+ const texture_height = Math.clamp(formResult['texture_size'][1], 1, Infinity);
+ if (Project.box_uv != box_uv || Project.texture_width != texture_width || Project.texture_height != texture_height) {
+ // Adjust UV Mapping if resolution changed
+ if (!Project.box_uv && !box_uv && !Format['per_texture_uv_size'] && (Project.texture_width != texture_width || Project.texture_height != texture_height)) {
+ save = Undo.initEdit({ elements: [...Cube.all, ...Mesh.all], uv_only: true, uv_mode: true });
+ Cube.all.forEach(cube => {
+ for (const key in cube.faces) {
+ const uv = cube.faces[key].uv;
+ uv[0] *= texture_width / Project.texture_width;
+ uv[2] *= texture_width / Project.texture_width;
+ uv[1] *= texture_height / Project.texture_height;
+ uv[3] *= texture_height / Project.texture_height;
+ }
+ });
+ Mesh.all.forEach(mesh => {
+ for (const key in mesh.faces) {
+ const uv = mesh.faces[key].uv;
+ for (const vkey in uv) {
+ uv[vkey][0] *= texture_width / Project.texture_width;
+ uv[vkey][1] *= texture_height / Project.texture_height;
}
}
- }
- }
- }
- if (a.sound_effects) {
- if (!animation.animators.effects) {
- animation.animators.effects = new EffectAnimator(animation);
- }
- for (const timestamp0 in a.sound_effects) {
- let sounds = a.sound_effects[timestamp0];
- if (sounds instanceof Array === false)
- sounds = [sounds];
- animation.animators.effects.addKeyframe({
- channel: 'sound',
- time: parseFloat(timestamp0),
- data_points: sounds
});
}
- }
- if (a.particle_effects) {
- if (!animation.animators.effects) {
- animation.animators.effects = new EffectAnimator(animation);
- }
- for (const timestamp1 in a.particle_effects) {
- let particles = a.particle_effects[timestamp1];
- if (particles instanceof Array === false)
- particles = [particles];
- particles.forEach(particle => {
- if (particle)
- particle.script = particle.pre_effect_script;
- });
- animation.animators.effects.addKeyframe({
- channel: 'particle',
- time: parseFloat(timestamp1),
- data_points: particles
- });
+ // Convert UV mode per element
+ if (Project.box_uv != box_uv && ((box_uv && !Cube.all.find(cube => cube['box_uv'])) || (!box_uv && !Cube.all.find(cube => !cube['box_uv'])))) {
+ if (!save)
+ save = Undo.initEdit({ elements: Cube.all, uv_only: true, uv_mode: true });
+ Cube.all.forEach(cube => cube.setUVMode(box_uv));
}
+ if (!save)
+ save = Undo.initEdit({ uv_mode: true });
+ Project.texture_width = texture_width;
+ Project.texture_height = texture_height;
+ if (Format.optional_box_uv)
+ Project.box_uv = box_uv;
+ Canvas.updateAllUVs();
+ updateSelection();
}
- if (a.timeline) {
- if (!animation.animators.effects) {
- animation.animators.effects = new EffectAnimator(animation);
- }
- for (const timestamp2 in a.timeline) {
- const entry = a.timeline[timestamp2];
- const script = entry instanceof Array ? entry.join('\n') : entry;
- animation.animators.effects.addKeyframe({
- channel: 'timeline',
- time: parseFloat(timestamp2),
- data_points: [{ script }]
- });
+ const properties = ModelProject['properties'];
+ for (const key in properties) {
+ properties[key].merge(Project, formResult);
+ }
+ Project.name = Project.name.trim();
+ Project.model_identifier = Project.model_identifier.trim();
+ if (save)
+ Undo.finishEdit('Change project UV settings');
+ Blockbench.dispatchEvent('update_project_settings', formResult);
+ BARS.updateConditions();
+ if (Project.EditSession) {
+ const metadata = {
+ texture_width: Project.texture_width,
+ texture_height: Project.texture_height,
+ box_uv: Project.box_uv
+ };
+ for (const key in properties) {
+ properties[key].copy(Project, metadata);
}
+ Project.EditSession.sendAll('change_project_meta', JSON.stringify(metadata));
}
- animation.calculateSnappingFromKeyframes();
- if (!Blockbench.Animation.selected && Animator.open) {
- animation.select();
+ const modelType = _constants__WEBPACK_IMPORTED_MODULE_0__.GeckoModelType[formResult[_constants__WEBPACK_IMPORTED_MODULE_0__.PROPERTY_MODEL_TYPE]];
+ Project[_constants__WEBPACK_IMPORTED_MODULE_0__.PROPERTY_MODEL_TYPE] = modelType;
+ if (modelType == _constants__WEBPACK_IMPORTED_MODULE_0__.GeckoModelType.ITEM)
+ Project.parent = 'builtin/entity';
+ if (Project.name === Format.name || Project.name === '')
+ Project.name = "GeckoLib " + Project[_constants__WEBPACK_IMPORTED_MODULE_0__.PROPERTY_MODEL_TYPE];
+ switch (modelType) {
+ case _constants__WEBPACK_IMPORTED_MODULE_0__.GeckoModelType.ARMOR:
+ if (Outliner.root.length === 0) {
+ Codecs.project.parse(_resources_armorTemplate_json__WEBPACK_IMPORTED_MODULE_1__, null);
+ }
+ else {
+ alert('Unable to generate Armor Template over an existing model. Please select Armor on a new or empty project to use this model type.');
+ return false;
+ }
+ break;
+ default:
+ break;
}
- new_animations.push(animation);
- }
- }
- return new_animations;
+ Format.display_mode = modelType === _constants__WEBPACK_IMPORTED_MODULE_0__.GeckoModelType.ITEM || settings[_constants__WEBPACK_IMPORTED_MODULE_0__.SETTING_ALWAYS_SHOW_DISPLAY].value;
+ dialog.hide();
+ },
+ onFormChange(formResult) {
+ try {
+ document.getElementById('model_identifier')['placeholder'] = getObjectIdPlaceholder(formResult);
+ } // eslint-disable-next-line no-empty
+ catch (ex) { }
+ },
+ });
+ dialog.show();
+ return true;
}
-//#endregion Codec Helpers / Export Settings
-//#region Codec / ModelFormat
-function maybeExportItemJson(options = {}) {
- function checkExport(key, condition) {
- key = options[key];
- if (key === undefined) {
- return condition;
- }
- else {
- return key;
- }
- }
+/**
+ * Export the item display json
+ *
+ * Only called for GeckoLib projects + */ +function buildDisplaySettingsJson(options = {}) { if (!Project) return; - const blockmodel = {}; - if (checkExport('comment', settings.credit.value)) { - blockmodel.credit = settings.credit.value; - } - if (checkExport('parent', Project.parent != '')) { - blockmodel.parent = Project.parent; - } - if (checkExport('ambientocclusion', Project.ambientocclusion === false)) { - blockmodel.ambientocclusion = false; - } - if (Project.texture_width !== 16 || Project.texture_height !== 16) { - blockmodel.texture_size = [Project.texture_width, Project.texture_height]; - } - if (checkExport('front_gui_light', Project.front_gui_light)) { - blockmodel.gui_light = 'front'; - } - if (checkExport('overrides', Project.overrides)) { - blockmodel.overrides = Project.overrides; - } - if (checkExport('display', Object.keys(Project.display_settings).length >= 1)) { - const new_display = {}; - let entries = 0; - for (const i in DisplayMode.slots) { - const key = DisplayMode.slots[i]; + const modelProperties = {}; + if (options['comment'] || settings.credit.value) + modelProperties.credit = settings.credit.value; + if (options['parent'] || Project.parent != '') + modelProperties.parent = Project.parent; + if (options['ambientocclusion'] || Project.ambientocclusion === false) + modelProperties.ambientocclusion = false; + if (Project.texture_width !== 16 || Project.texture_height !== 16) + modelProperties.texture_size = [Project.texture_width, Project.texture_height]; + if (options['front_gui_light'] || Project.front_gui_light) + modelProperties.gui_light = 'front'; + if (options['overrides'] || Project.overrides) + modelProperties.overrides = Project.overrides; + if (options['display'] || !(0,_utils__WEBPACK_IMPORTED_MODULE_2__.isEmpty)(Project.display_settings)) { + const nonDefaultDisplays = {}; + for (const slot in DisplayMode.slots) { + const perspective = DisplayMode.slots[slot]; // eslint-disable-next-line no-prototype-builtins - if (DisplayMode.slots.hasOwnProperty(i) && Project.display_settings[key] && Project.display_settings[key].export) { - new_display[key] = Project.display_settings[key].export(); - entries++; + if (DisplayMode.slots.hasOwnProperty(slot) && Project.display_settings[perspective]) { + const display = Project.display_settings[perspective].export(); + if (display) + nonDefaultDisplays[perspective] = display; } } - if (entries) { - blockmodel.display = new_display; - } + if (!(0,_utils__WEBPACK_IMPORTED_MODULE_2__.isEmpty)(nonDefaultDisplays)) + modelProperties.display = nonDefaultDisplays; } - if (checkExport('textures', Object.keys(Project.textures).length >= 1)) { - for (const tex of Project.textures) { - if (tex.particle || Object.keys(Project.textures).length === 1) { - let name = tex.name; + if (options['textures'] || !(0,_utils__WEBPACK_IMPORTED_MODULE_2__.isEmpty)(Project.textures)) { + for (const texture of Project.textures) { + if (texture.particle || (settings[_constants__WEBPACK_IMPORTED_MODULE_0__.SETTING_AUTO_PARTICLE_TEXTURE].value && Object.keys(Project.textures).length === 1)) { + let name = texture.name; if (name.indexOf(".png") > 0) name = name.substring(0, name.indexOf(".png")); - const texturesMap = {}; - texturesMap['particle'] = name; - blockmodel.textures = texturesMap; + if (!texture.particle) { + if (!(0,_utils__WEBPACK_IMPORTED_MODULE_2__.isValidPath)(name)) { + name = name.toLowerCase().replace(" ", "_"); + if (!(0,_utils__WEBPACK_IMPORTED_MODULE_2__.isValidPath)(name)) + continue; + } + } + name = (Project[_constants__WEBPACK_IMPORTED_MODULE_0__.PROPERTY_MODEL_TYPE] == _constants__WEBPACK_IMPORTED_MODULE_0__.GeckoModelType.BLOCK ? "block/" : "item/") + name; + if (Project[_constants__WEBPACK_IMPORTED_MODULE_0__.PROPERTY_MODID]) + name = Project[_constants__WEBPACK_IMPORTED_MODULE_0__.PROPERTY_MODID] + ":" + name; + modelProperties.textures = { 'particle': name }; break; } } } - const blockmodelString = JSON.stringify(blockmodel, null, 2); - const scope = codec; - const path = _settings__WEBPACK_IMPORTED_MODULE_2__["default"].itemModelPath; Blockbench.export({ - resource_id: 'model', - type: Codecs.java_block.name, - extensions: ['json'], - name: scope.fileName().replace(".geo", ".item"), - startpath: path, - content: blockmodelString, - }, (real_path) => { - _settings__WEBPACK_IMPORTED_MODULE_2__["default"].itemModelPath = real_path; - }); - return this; -} -const codec = Codecs.bedrock; -const format = new ModelFormat({ - id: "animated_entity_model", - name: "GeckoLib Animated Model", - category: "minecraft", - description: "Animated Model for Java mods using GeckoLib", - icon: "view_in_ar", - rotate_cubes: true, - box_uv: true, - optional_box_uv: true, - single_texture: true, - bone_rig: true, - centered_grid: true, - animated_textures: true, - select_texture_for_particles: true, - animation_mode: true, - animation_files: true, - locators: true, - codec: Codecs.project, - display_mode: false, - onActivation: function () { - } -}); -//Object.defineProperty(format, 'integer_size', {get: _ => Templates.get('integer_size')}) -// codec.format = format; // This sets the default format for the codec -/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (codec); // This is used for plugin "Export Animated Model" menu item -//#endregion Codec / ModelFormat + resource_id: 'model', + type: Codecs.java_block.name, + extensions: ['json'], + name: Project.model_identifier ? (Project.model_identifier + ".json") : codec.fileName().replace(".geo", ""), + startpath: Project[_constants__WEBPACK_IMPORTED_MODULE_0__.PROPERTY_FILEPATH_CACHE].display, + content: JSON.stringify(modelProperties, null, 2), + }, file_path => { + const oldPath = Project[_constants__WEBPACK_IMPORTED_MODULE_0__.PROPERTY_FILEPATH_CACHE].display; + Project[_constants__WEBPACK_IMPORTED_MODULE_0__.PROPERTY_FILEPATH_CACHE].display = settings[_constants__WEBPACK_IMPORTED_MODULE_0__.SETTING_REMEMBER_EXPORT_LOCATIONS].value ? file_path : undefined; + if (oldPath !== Project[_constants__WEBPACK_IMPORTED_MODULE_0__.PROPERTY_FILEPATH_CACHE].display) + Project.saved = false; + }); + return this; +} +/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (codec); + + +/***/ }), + +/***/ "./ts/constants.ts": +/*!*************************!*\ + !*** ./ts/constants.ts ***! + \*************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ GECKOLIB_MODEL_ID: () => (/* binding */ GECKOLIB_MODEL_ID), +/* harmony export */ GeckoModelType: () => (/* binding */ GeckoModelType), +/* harmony export */ PROPERTY_FILEPATH_CACHE: () => (/* binding */ PROPERTY_FILEPATH_CACHE), +/* harmony export */ PROPERTY_MODEL_TYPE: () => (/* binding */ PROPERTY_MODEL_TYPE), +/* harmony export */ PROPERTY_MODID: () => (/* binding */ PROPERTY_MODID), +/* harmony export */ SETTING_ALWAYS_SHOW_DISPLAY: () => (/* binding */ SETTING_ALWAYS_SHOW_DISPLAY), +/* harmony export */ SETTING_AUTO_PARTICLE_TEXTURE: () => (/* binding */ SETTING_AUTO_PARTICLE_TEXTURE), +/* harmony export */ SETTING_CONVERT_BEDROCK_ANIMATIONS: () => (/* binding */ SETTING_CONVERT_BEDROCK_ANIMATIONS), +/* harmony export */ SETTING_DEFAULT_MODID: () => (/* binding */ SETTING_DEFAULT_MODID), +/* harmony export */ SETTING_REMEMBER_EXPORT_LOCATIONS: () => (/* binding */ SETTING_REMEMBER_EXPORT_LOCATIONS) +/* harmony export */ }); +/** + * GeckoLib plugin model format ID. Used to identify model types generated from this plugin + */ +const GECKOLIB_MODEL_ID = "animated_entity_model"; +// Setting name constants +const SETTING_AUTO_PARTICLE_TEXTURE = 'geckolib_auto_particle_texture'; +const SETTING_CONVERT_BEDROCK_ANIMATIONS = 'geckolib_convert_bedrock_animations'; +const SETTING_ALWAYS_SHOW_DISPLAY = 'geckolib_always_show_display'; +const SETTING_REMEMBER_EXPORT_LOCATIONS = 'geckolib_remember_export_locations'; +const SETTING_DEFAULT_MODID = 'geckolib_default_modid'; +// Property name constants +const PROPERTY_MODID = 'geckolib_modid'; +const PROPERTY_MODEL_TYPE = 'geckolib_model_type'; +const PROPERTY_FILEPATH_CACHE = 'geckolib_filepath_cache'; +/** + * Available GeckoLib model types + */ +var GeckoModelType; +(function (GeckoModelType) { + GeckoModelType["ENTITY"] = "Entity"; + GeckoModelType["BLOCK"] = "Block"; + GeckoModelType["ITEM"] = "Item"; + GeckoModelType["ARMOR"] = "Armor"; + GeckoModelType["OBJECT"] = "Object"; +})(GeckoModelType || (GeckoModelType = {})); /***/ }), -/***/ "./easing.ts": -/*!*******************!*\ - !*** ./easing.ts ***! - \*******************/ +/***/ "./ts/easing.ts": +/*!**********************!*\ + !*** ./ts/easing.ts ***! + \**********************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; @@ -8863,6 +7070,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export */ EASING_OPTIONS: () => (/* binding */ EASING_OPTIONS), /* harmony export */ easingFunctions: () => (/* binding */ easingFunctions), /* harmony export */ getEasingArgDefault: () => (/* binding */ getEasingArgDefault), +/* harmony export */ isArgsEasing: () => (/* binding */ isArgsEasing), /* harmony export */ parseEasingArg: () => (/* binding */ parseEasingArg), /* harmony export */ reverseEasing: () => (/* binding */ reverseEasing) /* harmony export */ }); @@ -8923,9 +7131,7 @@ function stepRange(steps, stop = 1) { if (steps < 2) throw new Error("steps must be > 2, got:" + steps); const stepLength = stop / steps; - return Array.from({ - length: steps - }, (_, i) => i * stepLength); + return Array.from({ length: steps }, (_, i) => i * stepLength); } // The MIT license notice below applies to the Easing class /** @@ -9095,112 +7301,499 @@ class Easing { }; } } -const quart = Easing.poly(4); -const quint = Easing.poly(5); -const back = (direction, scalar, t) => direction(Easing.back(1.70158 * scalar))(t); -const elastic = (direction, bounciness, t) => direction(Easing.elastic(bounciness))(t); -const bounce = (direction, bounciness, t) => direction(Easing.bounce(bounciness))(t); -const easingFunctions = { - linear: Easing.linear, - step(steps, x) { - const intervals = stepRange(steps); - return intervals[findIntervalBorderIndex(x, intervals, false)]; - }, - easeInQuad: Easing.in(Easing.quad), - easeOutQuad: Easing.out(Easing.quad), - easeInOutQuad: Easing.inOut(Easing.quad), - easeInCubic: Easing.in(Easing.cubic), - easeOutCubic: Easing.out(Easing.cubic), - easeInOutCubic: Easing.inOut(Easing.cubic), - easeInQuart: Easing.in(quart), - easeOutQuart: Easing.out(quart), - easeInOutQuart: Easing.inOut(quart), - easeInQuint: Easing.in(quint), - easeOutQuint: Easing.out(quint), - easeInOutQuint: Easing.inOut(quint), - easeInSine: Easing.in(Easing.sin), - easeOutSine: Easing.out(Easing.sin), - easeInOutSine: Easing.inOut(Easing.sin), - easeInExpo: Easing.in(Easing.exp), - easeOutExpo: Easing.out(Easing.exp), - easeInOutExpo: Easing.inOut(Easing.exp), - easeInCirc: Easing.in(Easing.circle), - easeOutCirc: Easing.out(Easing.circle), - easeInOutCirc: Easing.inOut(Easing.circle), - easeInBack: back.bind(null, Easing.in), - easeOutBack: back.bind(null, Easing.out), - easeInOutBack: back.bind(null, Easing.inOut), - easeInElastic: elastic.bind(null, Easing.in), - easeOutElastic: elastic.bind(null, Easing.out), - easeInOutElastic: elastic.bind(null, Easing.inOut), - easeInBounce: bounce.bind(null, Easing.in), - easeOutBounce: bounce.bind(null, Easing.out), - easeInOutBounce: bounce.bind(null, Easing.inOut), -}; -// Object with the same keys as easingFunctions and values of the stringified key names -const EASING_OPTIONS = Object.freeze(Object.fromEntries(Object.entries(easingFunctions).map(entry => ([entry[0], entry[0]])))); -const EASING_DEFAULT = 'linear'; -const getEasingArgDefault = (kf) => { - switch (kf.easing) { - case EASING_OPTIONS.easeInBack: - case EASING_OPTIONS.easeOutBack: - case EASING_OPTIONS.easeInOutBack: - case EASING_OPTIONS.easeInElastic: - case EASING_OPTIONS.easeOutElastic: - case EASING_OPTIONS.easeInOutElastic: - return 1; - case EASING_OPTIONS.easeInBounce: - case EASING_OPTIONS.easeOutBounce: - case EASING_OPTIONS.easeInOutBounce: - return 0.5; - case EASING_OPTIONS.step: - return 5; - default: - return null; - } -}; -const parseEasingArg = (kf, value) => { - switch (kf.easing) { - case EASING_OPTIONS.easeInBack: - case EASING_OPTIONS.easeOutBack: - case EASING_OPTIONS.easeInOutBack: - case EASING_OPTIONS.easeInElastic: - case EASING_OPTIONS.easeOutElastic: - case EASING_OPTIONS.easeInOutElastic: - case EASING_OPTIONS.easeInBounce: - case EASING_OPTIONS.easeOutBounce: - case EASING_OPTIONS.easeInOutBounce: - return parseFloat(value); - case EASING_OPTIONS.step: - return Math.max(parseInt(value, 10), 2); - default: - return parseInt(value, 10); +const quart = Easing.poly(4); +const quint = Easing.poly(5); +const back = (direction, scalar, t) => direction(Easing.back(1.70158 * scalar))(t); +const elastic = (direction, bounciness, t) => direction(Easing.elastic(bounciness))(t); +const bounce = (direction, bounciness, t) => direction(Easing.bounce(bounciness))(t); +const easingFunctions = { + linear: Easing.linear, + step(steps, x) { + const intervals = stepRange(steps); + return intervals[findIntervalBorderIndex(x, intervals, false)]; + }, + easeInQuad: Easing.in(Easing.quad), + easeOutQuad: Easing.out(Easing.quad), + easeInOutQuad: Easing.inOut(Easing.quad), + easeInCubic: Easing.in(Easing.cubic), + easeOutCubic: Easing.out(Easing.cubic), + easeInOutCubic: Easing.inOut(Easing.cubic), + easeInQuart: Easing.in(quart), + easeOutQuart: Easing.out(quart), + easeInOutQuart: Easing.inOut(quart), + easeInQuint: Easing.in(quint), + easeOutQuint: Easing.out(quint), + easeInOutQuint: Easing.inOut(quint), + easeInSine: Easing.in(Easing.sin), + easeOutSine: Easing.out(Easing.sin), + easeInOutSine: Easing.inOut(Easing.sin), + easeInExpo: Easing.in(Easing.exp), + easeOutExpo: Easing.out(Easing.exp), + easeInOutExpo: Easing.inOut(Easing.exp), + easeInCirc: Easing.in(Easing.circle), + easeOutCirc: Easing.out(Easing.circle), + easeInOutCirc: Easing.inOut(Easing.circle), + easeInBack: back.bind(null, Easing.in), + easeOutBack: back.bind(null, Easing.out), + easeInOutBack: back.bind(null, Easing.inOut), + easeInElastic: elastic.bind(null, Easing.in), + easeOutElastic: elastic.bind(null, Easing.out), + easeInOutElastic: elastic.bind(null, Easing.inOut), + easeInBounce: bounce.bind(null, Easing.in), + easeOutBounce: bounce.bind(null, Easing.out), + easeInOutBounce: bounce.bind(null, Easing.inOut), +}; +// Object with the same keys as easingFunctions and values of the stringified key names +const EASING_OPTIONS = Object.freeze(Object.fromEntries(Object.entries(easingFunctions).map(entry => ([entry[0], entry[0]])))); +const EASING_DEFAULT = 'linear'; +const getEasingArgDefault = (kf) => { + switch (kf.easing) { + case EASING_OPTIONS.easeInBack: + case EASING_OPTIONS.easeOutBack: + case EASING_OPTIONS.easeInOutBack: + case EASING_OPTIONS.easeInElastic: + case EASING_OPTIONS.easeOutElastic: + case EASING_OPTIONS.easeInOutElastic: + return 1; + case EASING_OPTIONS.easeInBounce: + case EASING_OPTIONS.easeOutBounce: + case EASING_OPTIONS.easeInOutBounce: + return 0.5; + case EASING_OPTIONS.step: + return 5; + default: + return null; + } +}; +const parseEasingArg = (kf, value) => { + switch (kf.easing) { + case EASING_OPTIONS.easeInBack: + case EASING_OPTIONS.easeOutBack: + case EASING_OPTIONS.easeInOutBack: + case EASING_OPTIONS.easeInElastic: + case EASING_OPTIONS.easeOutElastic: + case EASING_OPTIONS.easeInOutElastic: + case EASING_OPTIONS.easeInBounce: + case EASING_OPTIONS.easeOutBounce: + case EASING_OPTIONS.easeInOutBounce: + return parseFloat(value); + case EASING_OPTIONS.step: + return Math.max(parseInt(value, 10), 2); + default: + return parseInt(value, 10); + } +}; +function reverseEasing(easing) { + if (!easing) + return easing; + if (easing.startsWith("easeInOut")) + return easing; + if (easing.startsWith("easeIn")) + return easing.replace("easeIn", "easeOut"); + if (easing.startsWith("easeOut")) + return easing.replace("easeOut", "easeIn"); + return easing; +} +const isArgsEasing = (easing = "") => easing.includes("Back") || + easing.includes("Elastic") || + easing.includes("Bounce") || + easing === EASING_OPTIONS.step; + + +/***/ }), + +/***/ "./ts/events.ts": +/*!**********************!*\ + !*** ./ts/events.ts ***! + \**********************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ addEventListeners: () => (/* binding */ addEventListeners), +/* harmony export */ removeEventListeners: () => (/* binding */ removeEventListeners) +/* harmony export */ }); +/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ "./ts/utils.ts"); +/* harmony import */ var _keyframe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./keyframe */ "./ts/keyframe.ts"); +/* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./constants */ "./ts/constants.ts"); +/* harmony import */ var _codec__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./codec */ "./ts/codec.ts"); + + + + +function addEventListeners() { + (0,_utils__WEBPACK_IMPORTED_MODULE_0__.addCodecCallback)(Codecs.project, 'parse', (0,_utils__WEBPACK_IMPORTED_MODULE_0__.onlyIfGeckoLib)(onProjectParse)); + (0,_utils__WEBPACK_IMPORTED_MODULE_0__.addCodecCallback)(Codecs.bedrock, 'compile', (0,_utils__WEBPACK_IMPORTED_MODULE_0__.onlyIfGeckoLib)(onBedrockCompile)); + (0,_utils__WEBPACK_IMPORTED_MODULE_0__.addEventListener)('select_mode', (0,_utils__WEBPACK_IMPORTED_MODULE_0__.onlyIfGeckoLib)(onModeSelect)); + (0,_utils__WEBPACK_IMPORTED_MODULE_0__.addEventListener)('select_project', (0,_utils__WEBPACK_IMPORTED_MODULE_0__.onlyIfGeckoLib)(onProjectSelect)); + (0,_utils__WEBPACK_IMPORTED_MODULE_0__.addEventListener)('update_project_settings', (0,_utils__WEBPACK_IMPORTED_MODULE_0__.onlyIfGeckoLib)(onSettingsChanged)); + (0,_utils__WEBPACK_IMPORTED_MODULE_0__.addEventListener)('save_project', (0,_utils__WEBPACK_IMPORTED_MODULE_0__.onlyIfGeckoLib)(onProjectSave)); + (0,_utils__WEBPACK_IMPORTED_MODULE_0__.addMonkeypatch)(Animator, null, "buildFile", monkeypatchAnimatorBuildFile); + (0,_utils__WEBPACK_IMPORTED_MODULE_0__.addMonkeypatch)(Animator, null, "loadFile", monkeypatchAnimatorLoadFile); + (0,_utils__WEBPACK_IMPORTED_MODULE_0__.addMonkeypatch)(Blockbench, null, "export", monkeypatchBlockbenchExport); + (0,_utils__WEBPACK_IMPORTED_MODULE_0__.addMonkeypatch)(BarItems, 'project_window', "click", monkeypatchProjectWindowClick); +} +function removeEventListeners() { + (0,_utils__WEBPACK_IMPORTED_MODULE_0__.removeCodecCallback)(Codecs.project, 'parse', (0,_utils__WEBPACK_IMPORTED_MODULE_0__.onlyIfGeckoLib)(onProjectParse)); + (0,_utils__WEBPACK_IMPORTED_MODULE_0__.removeCodecCallback)(Codecs.bedrock, 'compile', (0,_utils__WEBPACK_IMPORTED_MODULE_0__.onlyIfGeckoLib)(onBedrockCompile)); + (0,_utils__WEBPACK_IMPORTED_MODULE_0__.removeEventListener)('select_mode', (0,_utils__WEBPACK_IMPORTED_MODULE_0__.onlyIfGeckoLib)(onModeSelect)); + (0,_utils__WEBPACK_IMPORTED_MODULE_0__.removeEventListener)('select_project', (0,_utils__WEBPACK_IMPORTED_MODULE_0__.onlyIfGeckoLib)(onProjectSelect)); + (0,_utils__WEBPACK_IMPORTED_MODULE_0__.removeEventListener)('update_project_settings', (0,_utils__WEBPACK_IMPORTED_MODULE_0__.onlyIfGeckoLib)(onSettingsChanged)); + (0,_utils__WEBPACK_IMPORTED_MODULE_0__.removeMonkeypatches)(); +} +/** + * When an existing GeckoLib project is being read from file + */ +function onProjectParse(e) { + onSettingsChanged(); + // Because the project hasn't had its model properties applied at this stage + Format.display_mode = (e.model[_constants__WEBPACK_IMPORTED_MODULE_1__.PROPERTY_MODEL_TYPE] && e.model[_constants__WEBPACK_IMPORTED_MODULE_1__.PROPERTY_MODEL_TYPE] === _constants__WEBPACK_IMPORTED_MODULE_1__.GeckoModelType.ITEM) || settings[_constants__WEBPACK_IMPORTED_MODULE_1__.SETTING_ALWAYS_SHOW_DISPLAY].value; +} +/** + * When the Blockbench project is being saved + *
+ * Only called for GeckoLib projects + */ +function onProjectSave(e) { + if (!settings[_constants__WEBPACK_IMPORTED_MODULE_1__.SETTING_REMEMBER_EXPORT_LOCATIONS].value) + e.model[_constants__WEBPACK_IMPORTED_MODULE_1__.PROPERTY_FILEPATH_CACHE] = {}; +} +/** + * When the GeckoLib project settings are changed, or a GeckoLib project is being opened or swapped to + *
+ * Only called for GeckoLib projects + */ +function onSettingsChanged() { + Modes.selected.select(); + Format.display_mode = (0,_utils__WEBPACK_IMPORTED_MODULE_0__.hasModelDisplaySettings)(); + if (Project instanceof ModelProject && Project[_constants__WEBPACK_IMPORTED_MODULE_1__.PROPERTY_MODEL_TYPE] === _constants__WEBPACK_IMPORTED_MODULE_1__.GeckoModelType.ITEM && (!Project.parent || Project.parent !== 'builtin/entity')) { + Project.parent = 'builtin/entity'; + Project.saved = false; + } +} +/** + * When opening a project tab, whether from an existing project, creating a new one, or swapping open tabs + *
+ * Only called for GeckoLib projects + */ +function onProjectSelect() { + onSettingsChanged(); +} +/** + * When selecting edit/paint/display/animate/etc + *
+ * Only called for GeckoLib projects + */ +function onModeSelect(e) { + // Offset the display emulator to account for GeckoLib's +0.51 manual offset + // This is a legacy patch as Blockbench no longer does this internally + if (e.mode.id == 'display') + Project.model_3d.position.y = 0; +} +/** + * When the model geometry is being compiled for export + *
+ * Only called for GeckoLib projects + */ +function onBedrockCompile(e) { + var _a; + // Remove display transforms from non-bedrock geometry + (_a = e.model["minecraft:geometry"]) === null || _a === void 0 ? void 0 : _a.forEach((geo) => delete geo["item_display_transforms"]); + // Force-suppress specific version advancement for non-bedrock models to prevent legacy version crashes until a better system is established + switch (e.model.format_version) { + case "1.14.0": + case "1.21.0": + case "1.21.20": + e.model.format_version = "1.12.0"; + break; + default: + break; + } +} +/** + * When the project settings window is being opened, either via a new project or the File -> Project... menu item + *
+ * The project may not be a GeckoLib project, so check it as necessary + */ +function monkeypatchProjectWindowClick() { + if ((0,_utils__WEBPACK_IMPORTED_MODULE_0__.isGeckoLibModel)()) { + (0,_codec__WEBPACK_IMPORTED_MODULE_2__.openProjectSettingsDialog)(); + } + else { + _utils__WEBPACK_IMPORTED_MODULE_0__.Monkeypatches.get(BarItems).click(); + } +} +/** + * When any file is being exported to disk by Blockbench + *
+ * The project may not be a GeckoLib project, so check it as necessary + */ +function monkeypatchBlockbenchExport(options, cb) { + if (!(0,_utils__WEBPACK_IMPORTED_MODULE_0__.isGeckoLibModel)()) { + _utils__WEBPACK_IMPORTED_MODULE_0__.Monkeypatches.get(Blockbench).export(options, cb); + return; + } + if (Project instanceof ModelProject) { + if (options.resource_id === 'animation' && options.type === 'JSON Animation') { // Animation JSON + const fileName = Project.model_identifier && Project.model_identifier + ".animation"; + options.startpath = Project[_constants__WEBPACK_IMPORTED_MODULE_1__.PROPERTY_FILEPATH_CACHE].animation; + const parentCallback = cb; + cb = file_path => { + if (parentCallback) + parentCallback(file_path); + const oldPath = Project[_constants__WEBPACK_IMPORTED_MODULE_1__.PROPERTY_FILEPATH_CACHE].animation; + Project[_constants__WEBPACK_IMPORTED_MODULE_1__.PROPERTY_FILEPATH_CACHE].animation = settings[_constants__WEBPACK_IMPORTED_MODULE_1__.SETTING_REMEMBER_EXPORT_LOCATIONS].value ? file_path : undefined; + if (oldPath !== Project[_constants__WEBPACK_IMPORTED_MODULE_1__.PROPERTY_FILEPATH_CACHE].animation) + Project.saved = false; + }; + if (fileName) + options.name = fileName; + } + else if (options.resource_id === 'model' && options.type === 'Bedrock Model') { // Geo + const fileName = Project.model_identifier && Project.model_identifier + ".geo"; + options.startpath = Project[_constants__WEBPACK_IMPORTED_MODULE_1__.PROPERTY_FILEPATH_CACHE].model; + const parentWriter = options.custom_writer; + const parentCallback = cb; + if (parentWriter) { + options.custom_writer = (content, filePath, callback) => { + parentWriter(content, filePath, callback); + callback(filePath); + }; + } + cb = file_path => { + if (parentCallback) + parentCallback(file_path); + const oldPath = Project[_constants__WEBPACK_IMPORTED_MODULE_1__.PROPERTY_FILEPATH_CACHE].model; + Project[_constants__WEBPACK_IMPORTED_MODULE_1__.PROPERTY_FILEPATH_CACHE].model = settings[_constants__WEBPACK_IMPORTED_MODULE_1__.SETTING_REMEMBER_EXPORT_LOCATIONS].value ? file_path : undefined; + if (oldPath !== Project[_constants__WEBPACK_IMPORTED_MODULE_1__.PROPERTY_FILEPATH_CACHE].model) + Project.saved = false; + }; + if (fileName) + options.name = fileName; + } + } + _utils__WEBPACK_IMPORTED_MODULE_0__.Monkeypatches.get(Blockbench).export(options, cb); +} +/** + * When the animation file is being loaded into the project + *
+ * The project may not be a GeckoLib project, so check it as necessary + */ +function monkeypatchAnimatorLoadFile(file, exportingAnims) { + // eslint-disable-next-line no-undef + const json = file.json || autoParseJSON(file.content); + const path = file.path; + const new_animations = []; + function geoLoopToBbLoop(jsonLoop) { + if (jsonLoop) { + if (typeof jsonLoop === 'boolean') + return jsonLoop ? 'loop' : 'once'; + if (typeof jsonLoop === 'string') { + if (jsonLoop === "hold_on_last_frame") + return 'hold'; + if (jsonLoop === "loop" || jsonLoop === "true") + return 'loop'; + } + } + return 'once'; + } + function getKeyframeDataPoints(source) { + if (source instanceof Array) + return [{ x: source[0], y: source[1], z: source[2], }]; + if (['number', 'string'].includes(typeof source)) + return [{ x: source, y: source, z: source }]; + if (typeof source == 'object') { + if (source.vector) + return getKeyframeDataPoints(source.vector); + const points = []; + if (source.pre) + points.push(getKeyframeDataPoints(source.pre)[0]); + if (source.post) + points.push(getKeyframeDataPoints(source.post)[0]); + return points; + } + } + if (json && typeof json.animations === 'object') { + for (const animName in json.animations) { + if (exportingAnims && !exportingAnims.includes(animName)) + continue; + //Animation + const animData = json.animations[animName]; + const animation = new Blockbench.Animation({ + name: animName, + path, + loop: geoLoopToBbLoop(animData.loop), + override: animData.override_previous_animation, + anim_time_update: (typeof animData.anim_time_update == 'string' + ? animData.anim_time_update.replace(/;(?!$)/, ';\n') + : animData.anim_time_update), + blend_weight: (typeof animData.blend_weight == 'string' + ? animData.blend_weight.replace(/;(?!$)/, ';\n') + : animData.blend_weight), + length: animData.animation_length + }).add(); + //Bones + if (animData.bones) { + for (const boneName in animData.bones) { + const bone = animData.bones[boneName]; + const lowercase_bone_name = boneName.toLowerCase(); + const group = Group.all.find(group => group.name.toLowerCase() == lowercase_bone_name); + const uuid = group ? group.uuid : guid(); + let ga; // eslint-disable-line @typescript-eslint/no-unused-vars + const boneAnimator = new _keyframe__WEBPACK_IMPORTED_MODULE_3__.GeckolibBoneAnimator(uuid, animation, boneName); + animation.animators[uuid] = boneAnimator; + //Channels + for (const channel in bone) { + if (Animator.possible_channels[channel]) { + if (typeof bone[channel] === 'string' || typeof bone[channel] === 'number' || bone[channel] instanceof Array) { + boneAnimator.addKeyframe({ + time: 0, + channel, + easing: bone[channel].easing, + easingArgs: bone[channel].easingArgs, + data_points: getKeyframeDataPoints(bone[channel]), + }); + } + else if (typeof bone[channel] === 'object' && bone[channel].post) { + boneAnimator.addKeyframe({ + time: 0, + channel, + easing: bone[channel].easing, + easingArgs: bone[channel].easingArgs, + interpolation: bone[channel].lerp_mode, + data_points: getKeyframeDataPoints(bone[channel]), + }); + } + else if (typeof bone[channel] === 'object') { + for (const timestamp in bone[channel]) { + boneAnimator.addKeyframe({ + time: parseFloat(timestamp), + channel, + easing: bone[channel][timestamp].easing, + easingArgs: bone[channel][timestamp].easingArgs, + interpolation: bone[channel][timestamp].lerp_mode, + data_points: getKeyframeDataPoints(bone[channel][timestamp]), + }); + } + } + } + } + } + } + if (animData.sound_effects) { + if (!animation.animators.effects) + animation.animators.effects = new EffectAnimator(animation); + for (const timestamp in animData.sound_effects) { + const sounds = animData.sound_effects[timestamp]; + animation.animators.effects.addKeyframe({ + channel: 'sound', + time: parseFloat(timestamp), + data_points: sounds instanceof Array ? sounds : [sounds] + }); + } + } + if (animData.particle_effects) { + if (!animation.animators.effects) + animation.animators.effects = new EffectAnimator(animation); + for (const timestamp in animData.particle_effects) { + let particles = animData.particle_effects[timestamp]; + if (!(particles instanceof Array)) + particles = [particles]; + particles.forEach(particle => { + if (particle) + particle.script = particle.pre_effect_script; + }); + animation.animators.effects.addKeyframe({ + channel: 'particle', + time: parseFloat(timestamp), + data_points: particles + }); + } + } + if (animData.timeline) { + if (!animation.animators.effects) + animation.animators.effects = new EffectAnimator(animation); + for (const timestamp in animData.timeline) { + const entry = animData.timeline[timestamp]; + const script = entry instanceof Array ? entry.join('\n') : entry; + animation.animators.effects.addKeyframe({ + channel: 'timeline', + time: parseFloat(timestamp), + data_points: [{ script }] + }); + } + } + animation.calculateSnappingFromKeyframes(); + if (!Blockbench.Animation.selected && Animator.open) + animation.select(); + new_animations.push(animation); + } + } + return new_animations; +} +/** + * When the animations json is being compiled for export + *
+ * The project may not be a GeckoLib project, so check it as necessary + */ +function monkeypatchAnimatorBuildFile() { + const result = _utils__WEBPACK_IMPORTED_MODULE_0__.Monkeypatches.get(Animator).buildFile.apply(this, arguments); + if ((0,_utils__WEBPACK_IMPORTED_MODULE_0__.isGeckoLibModel)()) { + result.geckolib_format_version = 2; + // Convert exported bedrock animations to non-bedrock + // Only applies to projects that had its animations made in a non-GeckoLib model format + if (settings[_constants__WEBPACK_IMPORTED_MODULE_1__.SETTING_CONVERT_BEDROCK_ANIMATIONS].value && result.animations) { + for (const animation in result.animations) { + const bones = result.animations[animation].bones; + if (bones) { + for (const boneName in bones) { + const bone = bones[boneName]; + for (const animationGroupType in bone) { + const animationGroup = bone[animationGroupType]; + for (const timestamp in animationGroup) { + const keyframe = animationGroup[timestamp]; + if (!keyframe) + continue; + let bedrockKeyframe = keyframe.pre; + let bedrockKeyframeData = undefined; + if (bedrockKeyframe !== undefined) { + bedrockKeyframeData = bedrockKeyframe; + delete keyframe.pre; + } + bedrockKeyframe = keyframe.post; + if (bedrockKeyframe !== undefined) { + bedrockKeyframeData = bedrockKeyframe; + delete keyframe.post; + } + if (bedrockKeyframeData !== undefined) { + Object.assign(keyframe, bedrockKeyframeData); + if (keyframe.lerp_mode) + delete keyframe.lerp_mode; + } + } + } + } + } + } + } } -}; -function reverseEasing(easing) { - if (!easing) - return easing; - if (easing.startsWith("easeInOut")) - return easing; - if (easing.startsWith("easeIn")) - return easing.replace("easeIn", "easeOut"); - if (easing.startsWith("easeOut")) - return easing.replace("easeOut", "easeIn"); - return easing; + return result; } /***/ }), -/***/ "./keyframe.ts": -/*!*********************!*\ - !*** ./keyframe.ts ***! - \*********************/ +/***/ "./ts/keyframe.ts": +/*!************************!*\ + !*** ./ts/keyframe.ts ***! + \************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ GeckolibBoneAnimator: () => (/* binding */ GeckolibBoneAnimator), /* harmony export */ loadKeyframeOverrides: () => (/* binding */ loadKeyframeOverrides), /* harmony export */ unloadKeyframeOverrides: () => (/* binding */ unloadKeyframeOverrides) /* harmony export */ }); @@ -9208,14 +7801,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var lodash_groupBy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(lodash_groupBy__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var lodash_mapValues__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! lodash/mapValues */ "./node_modules/lodash/mapValues.js"); /* harmony import */ var lodash_mapValues__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(lodash_mapValues__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./utils */ "./utils.ts"); -/* harmony import */ var _easing__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./easing */ "./easing.ts"); +/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./utils */ "./ts/utils.ts"); +/* harmony import */ var _easing__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./easing */ "./ts/easing.ts"); var Keyframe = Blockbench.Keyframe; -//#region Keyframe Mixins function loadKeyframeOverrides() { (0,_utils__WEBPACK_IMPORTED_MODULE_2__.addMonkeypatch)(Keyframe, "prototype", "getLerp", keyframeGetLerp); (0,_utils__WEBPACK_IMPORTED_MODULE_2__.addMonkeypatch)(Keyframe, "prototype", "compileBedrockKeyframe", keyframeCompileBedrock); @@ -9226,6 +7818,12 @@ function loadKeyframeOverrides() { function unloadKeyframeOverrides() { //No-op for now since monkeypatches are unloaded automatically } +// This subclass isn't strictly needed at runtime but was required to appease the compiler due to our monkeypatch +class GeckolibBoneAnimator extends BoneAnimator { + addKeyframe(data, uuid) { + return super.addKeyframe(data, uuid); + } +} function lerp(start, stop, amt) { return amt * (stop - start) + start; } @@ -9233,10 +7831,10 @@ function lerp(start, stop, amt) { function keyframeGetLerp(other, axis, amount, allow_expression) { const easing = other.easing || _easing__WEBPACK_IMPORTED_MODULE_3__.EASING_DEFAULT; if (Format.id !== "animated_entity_model") { - return _utils__WEBPACK_IMPORTED_MODULE_2__.Original.get(Keyframe).getLerp.apply(this, arguments); + return _utils__WEBPACK_IMPORTED_MODULE_2__.Monkeypatches.get(Keyframe).getLerp.apply(this, arguments); } let easingFunc = _easing__WEBPACK_IMPORTED_MODULE_3__.easingFunctions[easing]; - if ((0,_utils__WEBPACK_IMPORTED_MODULE_2__.hasArgs)(easing)) { + if ((0,_easing__WEBPACK_IMPORTED_MODULE_3__.isArgsEasing)(easing)) { const arg1 = Array.isArray(other.easingArgs) && other.easingArgs.length > 0 ? other.easingArgs[0] : (0,_easing__WEBPACK_IMPORTED_MODULE_3__.getEasingArgDefault)(other); @@ -9258,14 +7856,14 @@ function geckolibGetArray(data_point = 0) { let result = getArray.apply(this, [data_point]); if (Format.id === "animated_entity_model") { result = { vector: result, easing }; - if ((0,_utils__WEBPACK_IMPORTED_MODULE_2__.hasArgs)(easing)) + if ((0,_easing__WEBPACK_IMPORTED_MODULE_3__.isArgsEasing)(easing)) result.easingArgs = easingArgs; } return result; } function keyframeCompileBedrock() { if (Format.id !== "animated_entity_model" || !this.transform) { - return _utils__WEBPACK_IMPORTED_MODULE_2__.Original.get(Keyframe).compileBedrockKeyframe.apply(this, arguments); + return _utils__WEBPACK_IMPORTED_MODULE_2__.Monkeypatches.get(Keyframe).compileBedrockKeyframe.apply(this, arguments); } if (this.interpolation == 'catmullrom') { const previous = this.getPreviousKeyframe.apply(this); @@ -9297,10 +7895,10 @@ function keyframeCompileBedrock() { } function keyframeGetUndoCopy() { const { easing, easingArgs } = this; - const result = _utils__WEBPACK_IMPORTED_MODULE_2__.Original.get(Keyframe).getUndoCopy.apply(this, arguments); + const result = _utils__WEBPACK_IMPORTED_MODULE_2__.Monkeypatches.get(Keyframe).getUndoCopy.apply(this, arguments); if (Format.id === "animated_entity_model") { Object.assign(result, { easing }); - if ((0,_utils__WEBPACK_IMPORTED_MODULE_2__.hasArgs)(easing)) + if ((0,_easing__WEBPACK_IMPORTED_MODULE_3__.isArgsEasing)(easing)) result.easingArgs = easingArgs; } // console.log('keyframeGetUndoCopy arguments:', arguments, 'this:', this, 'result:', result); @@ -9331,12 +7929,12 @@ function keyframeExtend(dataIn) { } } } - const result = _utils__WEBPACK_IMPORTED_MODULE_2__.Original.get(Keyframe).extend.apply(this, arguments); + const result = _utils__WEBPACK_IMPORTED_MODULE_2__.Monkeypatches.get(Keyframe).extend.apply(this, arguments); // console.log('keyframeExtend 2 arguments:', arguments, 'this:', this, 'result:', result); return result; } function onReverseKeyframes() { - _utils__WEBPACK_IMPORTED_MODULE_2__.Original.get(BarItems.reverse_keyframes).click.apply(this, arguments); + _utils__WEBPACK_IMPORTED_MODULE_2__.Monkeypatches.get(BarItems.reverse_keyframes).click.apply(this, arguments); // console.log('@@@ onReverseKeyframes selected:', Timeline.selected); // There's not really an easy way to merge our undo operation with the original one so we'll make a new one instead Undo.initEdit({ keyframes: Timeline.selected }); @@ -9373,146 +7971,165 @@ function onReverseKeyframes() { updateKeyframeSelection(); Animator.preview(); } -//#endregion Keyframe Mixins /***/ }), -/***/ "./settings.ts": +/***/ "./ts/utils.ts": /*!*********************!*\ - !*** ./settings.ts ***! + !*** ./ts/utils.ts ***! \*********************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ GECKO_SETTINGS_DEFAULT: () => (/* binding */ GECKO_SETTINGS_DEFAULT), -/* harmony export */ MOD_SDKS: () => (/* binding */ MOD_SDKS), -/* harmony export */ MOD_SDK_1_15_FABRIC: () => (/* binding */ MOD_SDK_1_15_FABRIC), -/* harmony export */ MOD_SDK_1_15_FORGE: () => (/* binding */ MOD_SDK_1_15_FORGE), -/* harmony export */ MOD_SDK_OPTIONS: () => (/* binding */ MOD_SDK_OPTIONS), -/* harmony export */ OBJ_TYPE_ARMOR: () => (/* binding */ OBJ_TYPE_ARMOR), -/* harmony export */ OBJ_TYPE_BLOCK_ITEM: () => (/* binding */ OBJ_TYPE_BLOCK_ITEM), -/* harmony export */ OBJ_TYPE_ENTITY: () => (/* binding */ OBJ_TYPE_ENTITY), -/* harmony export */ OBJ_TYPE_OPTIONS: () => (/* binding */ OBJ_TYPE_OPTIONS), -/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__), -/* harmony export */ onSettingsChanged: () => (/* binding */ onSettingsChanged) -/* harmony export */ }); -/* harmony import */ var _armorTemplate_json__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./armorTemplate.json */ "./armorTemplate.json"); - -const makeOptions = (arr) => Object.fromEntries(arr.map(x => [x, x])); -const MOD_SDK_1_15_FORGE = 'Forge 1.12 - 1.16'; -const MOD_SDK_1_15_FABRIC = 'Fabric 1.15 - 1.16'; -const MOD_SDKS = [MOD_SDK_1_15_FORGE, MOD_SDK_1_15_FABRIC]; -const MOD_SDK_OPTIONS = makeOptions(MOD_SDKS); -const OBJ_TYPE_ENTITY = 'OBJ_TYPE_ENTITY'; -const OBJ_TYPE_ARMOR = 'OBJ_TYPE_ARMOR'; -const OBJ_TYPE_BLOCK_ITEM = 'OBJ_TYPE_ITEM_BLOCK'; -const OBJ_TYPE_OPTIONS = { - [OBJ_TYPE_ENTITY]: 'Entity', - [OBJ_TYPE_ARMOR]: 'Armor', - [OBJ_TYPE_BLOCK_ITEM]: 'Block/Item', -}; -const GECKO_SETTINGS_DEFAULT = { - formatVersion: 2, - modSDK: MOD_SDK_1_15_FORGE, - objectType: OBJ_TYPE_ENTITY, - entityType: 'Entity', - javaPackage: 'com.example.mod', - animFileNamespace: 'MODID', - animFilePath: 'animations/ANIMATIONFILE.json', -}; -Object.freeze(GECKO_SETTINGS_DEFAULT); -const geckoSettings = Object.assign({}, GECKO_SETTINGS_DEFAULT); -function onSettingsChanged() { - if (Format.id === "animated_entity_model") { - Format.display_mode = geckoSettings.objectType === OBJ_TYPE_BLOCK_ITEM || (Project && Object.keys(Project.display_settings).length != 0); - } - Modes.selected.select(); - switch (geckoSettings.objectType) { - case OBJ_TYPE_ARMOR: { - if (Outliner.root.length === 0) { - Codecs.project.parse(_armorTemplate_json__WEBPACK_IMPORTED_MODULE_0__, null); - } - else { - alert('Unable to load Armor Template as this would overwrite the current model. Please select Armor type on an empty project if you want to use the Armor Template.'); - } - break; - } - case OBJ_TYPE_BLOCK_ITEM: { - if (Project) - Project.parent = 'builtin/entity'; - break; - } - } -} -/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (geckoSettings); - - -/***/ }), - -/***/ "./utils.ts": -/*!******************!*\ - !*** ./utils.ts ***! - \******************/ -/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ Original: () => (/* binding */ Original), +/* harmony export */ Monkeypatches: () => (/* binding */ Monkeypatches), +/* harmony export */ addCodecCallback: () => (/* binding */ addCodecCallback), +/* harmony export */ addEventListener: () => (/* binding */ addEventListener), /* harmony export */ addMonkeypatch: () => (/* binding */ addMonkeypatch), -/* harmony export */ hasArgs: () => (/* binding */ hasArgs), +/* harmony export */ hasModelDisplaySettings: () => (/* binding */ hasModelDisplaySettings), +/* harmony export */ isEmpty: () => (/* binding */ isEmpty), +/* harmony export */ isGeckoLibModel: () => (/* binding */ isGeckoLibModel), +/* harmony export */ isValidNamespace: () => (/* binding */ isValidNamespace), +/* harmony export */ isValidPath: () => (/* binding */ isValidPath), +/* harmony export */ make: () => (/* binding */ make), +/* harmony export */ onlyIfGeckoLib: () => (/* binding */ onlyIfGeckoLib), +/* harmony export */ removeCodecCallback: () => (/* binding */ removeCodecCallback), +/* harmony export */ removeEventListener: () => (/* binding */ removeEventListener), /* harmony export */ removeMonkeypatches: () => (/* binding */ removeMonkeypatches) /* harmony export */ }); -/* harmony import */ var _easing__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./easing */ "./easing.ts"); +/* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./constants */ "./ts/constants.ts"); -const hasArgs = (easing = "") => easing.includes("Back") || - easing.includes("Elastic") || - easing.includes("Bounce") || - easing === _easing__WEBPACK_IMPORTED_MODULE_0__.EASING_OPTIONS.step; -const Original = new Map(); +const VALID_NAMESPACE_PATTERN = new RegExp('^[_\\-.a-z0-9]+$'); +const VALID_PATH_PATTERN = new RegExp('^[_\\-/.a-z0-9]+$'); +const Monkeypatches = new Map(); +/** + * Add what is effectively an override of another javascript function in a target object. + *
+ * The patched function should call the original first and operate on the result to ensure compatibility + *
+ * + * @param symbol The target object + * @param path The property of the target to access, or null to access the root target itself + * @param functionKey The name of the function to replace + * @param newFunction The function to patch in to replace the target + */ const addMonkeypatch = (symbol, path, functionKey, newFunction) => { const pathAccessor = path ? symbol[path] : symbol; - if (!Original.get(symbol)) - Original.set(symbol, { _pathAccessor: pathAccessor }); - Original.get(symbol)[functionKey] = pathAccessor[functionKey]; + if (!Monkeypatches.get(symbol)) + Monkeypatches.set(symbol, { _pathAccessor: pathAccessor }); + Monkeypatches.get(symbol)[functionKey] = pathAccessor[functionKey]; pathAccessor[functionKey] = newFunction; }; +/** + * Remove all previously added monkeypatches, reverting their operation to prior to the patch + */ const removeMonkeypatches = () => { - Original.forEach(symbol => { + Monkeypatches.forEach(symbol => { Object.keys(symbol).forEach(functionKey => { if (functionKey.startsWith('_')) return; symbol._pathAccessor[functionKey] = symbol[functionKey]; }); }); - Original.clear(); + Monkeypatches.clear(); +}; +/** + * Wrap a callback object with a conditional check on the project being a GeckoLib project, for safety + */ +const onlyIfGeckoLib = (callback) => { + return e => { + if (isGeckoLibModel()) + callback(e); + }; +}; +/** + * Add an event listener to Blockbench's event callback system. + *
+ * This should be done in codec.ts#loadCodec
or in the plugin creation in index.ts
+ */
+const addEventListener = (eventName, callback) => {
+ Blockbench.on(eventName, callback);
+};
+/**
+ * Remove a previously registered event listener from Blockbench's event callback system.
+ *
+ * All registered event listeners should be removed when the plugin or codec is unloaded + */ +const removeEventListener = (eventName, callback) => { + Blockbench.removeListener(eventName, callback); +}; +/** + * Add a callback to a codec to be called after the task has been completed + *
+ * This should be done in codec.ts#loadCodec
or in the plugin creation in index.ts
+ */
+const addCodecCallback = (codec, taskName, callback) => {
+ codec.on(taskName, callback);
+};
+/**
+ * Helper function that allows instantiation of an object and simultaneous property-modification without needing a local variable
+ */
+function make(obj, consumer) {
+ consumer(obj);
+ return obj;
+}
+/**
+ * Remove a previously added codec task completion callback
+ *
+ * All registered coded callbacks should be removed when the plugin or codec is unloaded + */ +const removeCodecCallback = (codec, taskName, callback) => { + codec.removeListener(taskName, callback); }; +/** + * Whether a given string is a valid ResourceLocation path for Minecraft + */ +const isValidPath = (path) => { + return VALID_PATH_PATTERN.test(path); +}; +/** + * Whether a given string is a valid ResourceLocation namespace for Minecraft + */ +const isValidNamespace = (namespace) => { + return VALID_NAMESPACE_PATTERN.test(namespace); +}; +/** + * Whether a map-like object has no defined keys or values + */ +const isEmpty = (object = {}) => Object.keys(object).length === 0; +/** + * Whether the currently focussed model is a GeckoLib model + */ +const isGeckoLibModel = () => Format.id === _constants__WEBPACK_IMPORTED_MODULE_0__.GECKOLIB_MODEL_ID; +/** + * Whether the current project is a GeckoLib model that has or uses item render perspective transforms + */ +const hasModelDisplaySettings = () => isGeckoLibModel() && Project && ((Project[_constants__WEBPACK_IMPORTED_MODULE_0__.PROPERTY_MODEL_TYPE] === _constants__WEBPACK_IMPORTED_MODULE_0__.GeckoModelType.ITEM || !isEmpty(Project.display_settings)) || settings[_constants__WEBPACK_IMPORTED_MODULE_0__.SETTING_ALWAYS_SHOW_DISPLAY].value); /***/ }), -/***/ "./armorTemplate.json": -/*!****************************!*\ - !*** ./armorTemplate.json ***! - \****************************/ +/***/ "./package.json": +/*!**********************!*\ + !*** ./package.json ***! + \**********************/ /***/ ((module) => { "use strict"; -module.exports = JSON.parse('{"meta":{"format_version":"3.2","model_format":"animated_entity_model","box_uv":true},"name":"CustomArmor","geo_name":"CustomArmor","resolution":{"width":64,"height":64},"elements":[{"name":"dontTouch","from":[-4,24,-4],"to":[4,32,4],"autouv":1,"color":0,"export":false,"locked":true,"origin":[0,0,0],"uuid":"9675593e-b27d-b70e-e1ea-1fc29f46a294"},{"name":"dontTouch","from":[-4,12,-2],"to":[4,24,2],"autouv":1,"color":0,"export":false,"locked":true,"origin":[0,24,0],"uuid":"fa43156a-2a62-948c-082f-483d525f6d1f"},{"name":"dontTouch","from":[4,12,-2],"to":[8,24,2],"autouv":1,"color":0,"export":false,"locked":true,"origin":[4,22,0],"uuid":"aa51170c-8b32-fb62-71f1-58ac0b7785a8"},{"name":"dontTouch","from":[-8,12,-2],"to":[-4,24,2],"autouv":1,"color":0,"export":false,"locked":true,"origin":[4,22,0],"uuid":"bf2c2539-20e3-cfcc-94c0-491734019889"},{"name":"dontTouch","from":[-4,0,-2],"to":[0,12,2],"autouv":1,"color":0,"export":false,"locked":true,"origin":[4,22,0],"uuid":"17b9bae0-356a-9bba-fad9-4672e2671191"},{"name":"dontTouch","from":[0,0,-2],"to":[4,12,2],"autouv":1,"color":0,"export":false,"locked":true,"origin":[4,22,0],"uuid":"7b31bac4-dc40-2b93-1204-7bbdcfe7d924"}],"outliner":[{"name":"bipedHead","uuid":"d340b6fa-56aa-9c0f-3560-7a067643b77d","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[0,24,0],"children":["9675593e-b27d-b70e-e1ea-1fc29f46a294",{"name":"armorHead","uuid":"6ab88dea-c816-d2bb-6be9-05ed7838da97","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[0,24,0],"children":[]}]},{"name":"bipedBody","uuid":"ce5b366c-fd87-41ae-9a73-e0a4d4b05f8d","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[0,24,0],"children":["fa43156a-2a62-948c-082f-483d525f6d1f",{"name":"armorBody","uuid":"282fcdbb-8ea9-4a13-4154-f2ed20d696c8","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[0,24,0],"children":[]}]},{"name":"bipedRightArm","uuid":"d8113cc7-7e10-0930-259e-b8e4211ce9da","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[4,22,0],"children":["aa51170c-8b32-fb62-71f1-58ac0b7785a8",{"name":"armorRightArm","uuid":"c5300e23-fd2f-b56c-3552-45d6650e11c6","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[4,22,0],"children":[]}]},{"name":"bipedLeftArm","uuid":"3b8901e8-3420-0834-51eb-76d64ff2ae8f","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[-4,22,0],"children":["bf2c2539-20e3-cfcc-94c0-491734019889",{"name":"armorLeftArm","uuid":"b0d41a53-f4ce-53c1-f899-5a2048c90ac2","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[-4,22,0],"children":[]}]},{"name":"bipedLeftLeg","uuid":"37231be7-a8ef-22ca-7fea-40aed58003bb","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[-2,12,0],"children":["17b9bae0-356a-9bba-fad9-4672e2671191",{"name":"armorLeftLeg","uuid":"e4b19746-2d17-1f56-befe-00718165ae50","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[-2,12,0],"children":[]},{"name":"armorLeftBoot","uuid":"9fe26b9a-ad66-9e6b-2fa2-4168e333b4be","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[-2,12,0],"children":[]}]},{"name":"bipedRightLeg","uuid":"45c031a5-b6be-e0a7-5454-b45d07f28429","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[2,12,0],"children":["7b31bac4-dc40-2b93-1204-7bbdcfe7d924",{"name":"armorRightLeg","uuid":"60238f18-e74b-c863-cb45-2e2f162221bd","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[2,12,0],"children":[]},{"name":"armorRightBoot","uuid":"eb3db34b-ccfe-dae9-ac4d-4e22c3222f70","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[2,12,0],"children":[]}]}],"textures":[]}'); +module.exports = JSON.parse('{"name":"animation_utils","version":"4.0","private":true,"description":"GeckoLib","main":"index.js","scripts":{"prebuild":"npm run test","build":"npm run build:only","build:only":"webpack && npm run update_manifest","update_manifest":"node scripts/updateManifest.mjs","start":"webpack --watch --mode=development","lint":"eslint .","lint:fix":"eslint --fix .","tsc":"tsc --noEmit","pretest":"npm run lint && npm run tsc","test":"npm run test:only","test:only":"jest"},"author":"Eliot Lash, Tslat, Gecko, McHorse","license":"MIT","blockbenchConfig":{"title":"GeckoLib Animation Utils","author":"Eliot Lash, Tslat, Gecko, McHorse","icon":"icon.png","description":"Create animated blocks, items, entities, and armor using the GeckoLib library and plugin.","has_changelog":true,"min_version":"4.11.0","max_version":"5.0.0","variant":"both","website":"https://github.com/bernie-g/geckolib/wiki","repository":"https://github.com/JannisX11/blockbench-plugins/tree/master/plugins/animation_utils","bug_tracker":"https://github.com/bernie-g/geckolib/issues"},"sideEffects":["./index.js"],"devDependencies":{"@types/jest":"^29.5.4","@types/lodash":"^4.14.197","@typescript-eslint/eslint-plugin":"^6.5.0","@typescript-eslint/parser":"^6.5.0","blockbench-types":"^4.9.0","eol":"0.9.1","eslint":"^7.7.0","indent-string":"^5.0.0","jest":"^29.6.4","ts-jest":"^29.1.1","ts-loader":"^9.4.4","typescript":"^4.9.5","webpack":"^5.88.2","webpack-cli":"^5.1.4"},"dependencies":{"lodash":"^4.17.21","semver":"7.3.2"}}'); /***/ }), -/***/ "./package.json": -/*!**********************!*\ - !*** ./package.json ***! - \**********************/ +/***/ "./resources/armorTemplate.json": +/*!**************************************!*\ + !*** ./resources/armorTemplate.json ***! + \**************************************/ /***/ ((module) => { "use strict"; -module.exports = JSON.parse('{"name":"animation_utils","version":"3.2.1","private":true,"description":"GeckoLib","main":"index.js","scripts":{"prebuild":"npm run test","build":"npm run build:only","build:only":"webpack && npm run update_manifest","update_manifest":"node scripts/updateManifest.mjs","start":"webpack --watch --mode=development","lint":"eslint .","lint:fix":"eslint --fix .","tsc":"tsc --noEmit","pretest":"npm run lint && npm run tsc","test":"npm run test:only","test:only":"jest"},"author":"Eliot Lash, Gecko, McHorse, AzureDoom","license":"MIT","blockbenchConfig":{"title":"GeckoLib Animation Utils","author":"Eliot Lash, Gecko, McHorse, AzureDoom, Tslat","icon":"icon.png","description":"Create animated blocks, items, entities, and armor using the GeckoLib library and plugin.","min_version":"4.11.0","max_version":"5.0.0","variant":"both"},"sideEffects":["./index.js"],"devDependencies":{"@types/jest":"^29.5.4","@types/lodash":"^4.14.197","@typescript-eslint/eslint-plugin":"^6.5.0","@typescript-eslint/parser":"^6.5.0","blockbench-types":"^4.9.0","eol":"0.9.1","eslint":"^7.7.0","indent-string":"^5.0.0","jest":"^29.6.4","ts-jest":"^29.1.1","ts-loader":"^9.4.4","typescript":"^4.9.5","webpack":"^5.88.2","webpack-cli":"^5.1.4"},"dependencies":{"lodash":"^4.17.21","semver":"7.3.2"}}'); +module.exports = JSON.parse('{"meta":{"format_version":"3.2","model_format":"animated_entity_model","box_uv":true},"name":"CustomArmor","geo_name":"CustomArmor","resolution":{"width":64,"height":64},"elements":[{"name":"dontTouch","from":[-4,24,-4],"to":[4,32,4],"autouv":1,"color":0,"export":false,"locked":true,"origin":[0,0,0],"uuid":"9675593e-b27d-b70e-e1ea-1fc29f46a294"},{"name":"dontTouch","from":[-4,12,-2],"to":[4,24,2],"autouv":1,"color":0,"export":false,"locked":true,"origin":[0,24,0],"uuid":"fa43156a-2a62-948c-082f-483d525f6d1f"},{"name":"dontTouch","from":[4,12,-2],"to":[8,24,2],"autouv":1,"color":0,"export":false,"locked":true,"origin":[4,22,0],"uuid":"aa51170c-8b32-fb62-71f1-58ac0b7785a8"},{"name":"dontTouch","from":[-8,12,-2],"to":[-4,24,2],"autouv":1,"color":0,"export":false,"locked":true,"origin":[4,22,0],"uuid":"bf2c2539-20e3-cfcc-94c0-491734019889"},{"name":"dontTouch","from":[-4,0,-2],"to":[0,12,2],"autouv":1,"color":0,"export":false,"locked":true,"origin":[4,22,0],"uuid":"17b9bae0-356a-9bba-fad9-4672e2671191"},{"name":"dontTouch","from":[0,0,-2],"to":[4,12,2],"autouv":1,"color":0,"export":false,"locked":true,"origin":[4,22,0],"uuid":"7b31bac4-dc40-2b93-1204-7bbdcfe7d924"}],"outliner":[{"name":"bipedHead","uuid":"d340b6fa-56aa-9c0f-3560-7a067643b77d","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[0,24,0],"children":["9675593e-b27d-b70e-e1ea-1fc29f46a294",{"name":"armorHead","uuid":"6ab88dea-c816-d2bb-6be9-05ed7838da97","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[0,24,0],"children":[]}]},{"name":"bipedBody","uuid":"ce5b366c-fd87-41ae-9a73-e0a4d4b05f8d","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[0,24,0],"children":["fa43156a-2a62-948c-082f-483d525f6d1f",{"name":"armorBody","uuid":"282fcdbb-8ea9-4a13-4154-f2ed20d696c8","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[0,24,0],"children":[]}]},{"name":"bipedRightArm","uuid":"d8113cc7-7e10-0930-259e-b8e4211ce9da","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[4,22,0],"children":["aa51170c-8b32-fb62-71f1-58ac0b7785a8",{"name":"armorRightArm","uuid":"c5300e23-fd2f-b56c-3552-45d6650e11c6","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[4,22,0],"children":[]}]},{"name":"bipedLeftArm","uuid":"3b8901e8-3420-0834-51eb-76d64ff2ae8f","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[-4,22,0],"children":["bf2c2539-20e3-cfcc-94c0-491734019889",{"name":"armorLeftArm","uuid":"b0d41a53-f4ce-53c1-f899-5a2048c90ac2","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[-4,22,0],"children":[]}]},{"name":"bipedLeftLeg","uuid":"37231be7-a8ef-22ca-7fea-40aed58003bb","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[-2,12,0],"children":["17b9bae0-356a-9bba-fad9-4672e2671191",{"name":"armorLeftLeg","uuid":"e4b19746-2d17-1f56-befe-00718165ae50","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[-2,12,0],"children":[]},{"name":"armorLeftBoot","uuid":"9fe26b9a-ad66-9e6b-2fa2-4168e333b4be","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[-2,12,0],"children":[]}]},{"name":"bipedRightLeg","uuid":"45c031a5-b6be-e0a7-5454-b45d07f28429","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[2,12,0],"children":["7b31bac4-dc40-2b93-1204-7bbdcfe7d924",{"name":"armorRightLeg","uuid":"60238f18-e74b-c863-cb45-2e2f162221bd","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[2,12,0],"children":[]},{"name":"armorRightBoot","uuid":"eb3db34b-ccfe-dae9-ac4d-4e22c3222f70","export":true,"isOpen":true,"visibility":true,"autouv":0,"origin":[2,12,0],"children":[]}]}],"textures":[]}'); /***/ }) @@ -9600,20 +8217,22 @@ var __webpack_exports__ = {}; // This entry need to be wrapped in an IIFE because it need to be in strict mode. (() => { "use strict"; -/*!******************!*\ - !*** ./index.ts ***! - \******************/ +/*!*********************!*\ + !*** ./ts/index.ts ***! + \*********************/ __webpack_require__.r(__webpack_exports__); /* harmony import */ var semver_functions_coerce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! semver/functions/coerce */ "./node_modules/semver/functions/coerce.js"); /* harmony import */ var semver_functions_coerce__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(semver_functions_coerce__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var semver_functions_satisfies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! semver/functions/satisfies */ "./node_modules/semver/functions/satisfies.js"); /* harmony import */ var semver_functions_satisfies__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(semver_functions_satisfies__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./package.json */ "./package.json"); -/* harmony import */ var _animationUi__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./animationUi */ "./animationUi.ts"); -/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./utils */ "./utils.ts"); -/* harmony import */ var _keyframe__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./keyframe */ "./keyframe.ts"); -/* harmony import */ var _settings__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./settings */ "./settings.ts"); -/* harmony import */ var _codec__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./codec */ "./codec.ts"); +/* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../package.json */ "./package.json"); +/* harmony import */ var _animationUi__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./animationUi */ "./ts/animationUi.ts"); +/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./utils */ "./ts/utils.ts"); +/* harmony import */ var _keyframe__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./keyframe */ "./ts/keyframe.ts"); +/* harmony import */ var _codec__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./codec */ "./ts/codec.ts"); +/* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./constants */ "./ts/constants.ts"); +/* harmony import */ var _events__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./events */ "./ts/events.ts"); + @@ -9624,84 +8243,185 @@ __webpack_require__.r(__webpack_exports__); const { version, blockbenchConfig } = _package_json__WEBPACK_IMPORTED_MODULE_2__; const SUPPORTED_BB_VERSION_RANGE = `${blockbenchConfig.min_version} - ${blockbenchConfig.max_version}`; -if (!semver_functions_satisfies__WEBPACK_IMPORTED_MODULE_1___default()(semver_functions_coerce__WEBPACK_IMPORTED_MODULE_0___default()(Blockbench.version), SUPPORTED_BB_VERSION_RANGE)) { +if (!semver_functions_satisfies__WEBPACK_IMPORTED_MODULE_1___default()(semver_functions_coerce__WEBPACK_IMPORTED_MODULE_0___default()(Blockbench.version), SUPPORTED_BB_VERSION_RANGE)) alert(`GeckoLib Animation Utils currently only supports Blockbench ${SUPPORTED_BB_VERSION_RANGE}. Please ensure you are using this version of Blockbench to avoid bugs and undefined behavior.`); -} +// Register the plugin and define what it adds (function () { - let exportAction; - let exportDisplayAction; - let button; + let pluginSettings; + let pluginProperties; + let pluginMenuItems; BBPlugin.register("animation_utils", Object.assign({}, blockbenchConfig, { name: blockbenchConfig.title, version, await_loading: true, onload() { - (0,_codec__WEBPACK_IMPORTED_MODULE_3__.loadCodec)(); + (0,_events__WEBPACK_IMPORTED_MODULE_3__.addEventListeners)(); (0,_animationUi__WEBPACK_IMPORTED_MODULE_4__.loadAnimationUI)(); (0,_keyframe__WEBPACK_IMPORTED_MODULE_5__.loadKeyframeOverrides)(); + pluginSettings = createPluginSettings(); + pluginProperties = createPluginProperties(); + pluginMenuItems = createPluginMenuItems(); + for (const menuItem of pluginMenuItems) { + MenuBar.addAction(menuItem.action, menuItem.menuCategory); + } console.log("Loaded GeckoLib plugin"); - exportAction = new Action("export_geckolib_model", { + }, + onunload() { + for (const setting of pluginSettings) { + setting.delete(); + } + for (const property of pluginProperties) { + property.delete(); + } + for (const menuItem of pluginMenuItems) { + menuItem.action.delete(); + } + (0,_keyframe__WEBPACK_IMPORTED_MODULE_5__.unloadKeyframeOverrides)(); + (0,_animationUi__WEBPACK_IMPORTED_MODULE_4__.unloadAnimationUI)(); + (0,_events__WEBPACK_IMPORTED_MODULE_3__.removeEventListeners)(); + _codec__WEBPACK_IMPORTED_MODULE_6__.format.delete(); + console.clear(); + }, + })); +})(); +/** + * Create and return the plugin's settings. + *
+ * These are found in the Settings panel in the plugin info window + */ +function createPluginSettings() { + return [ + new Setting(_constants__WEBPACK_IMPORTED_MODULE_7__.SETTING_AUTO_PARTICLE_TEXTURE, { + value: true, + category: "export", + name: "Auto-compute block/item particle texture", + description: "Attempt to auto-compute the particle texture for a GeckoLib block/item model if one isn't already specified when exporting the display settings json" + }), + new Setting(_constants__WEBPACK_IMPORTED_MODULE_7__.SETTING_CONVERT_BEDROCK_ANIMATIONS, { + value: true, + category: "export", + name: "Convert bedrock animations on export", + description: "Automatically convert bedrock-format animations to GeckoLib-compatible animations when exporting, if relevant. May have a performance improvement on larger projects" + }), + new Setting(_constants__WEBPACK_IMPORTED_MODULE_7__.SETTING_ALWAYS_SHOW_DISPLAY, { + value: false, + category: "edit", + name: "Always show display tab", + description: "Force the Display tab to always show, even when not an Item type model" + }), + new Setting(_constants__WEBPACK_IMPORTED_MODULE_7__.SETTING_REMEMBER_EXPORT_LOCATIONS, { + value: true, + category: "export", + name: "Remember file export locations", + description: "Remember where you export model/display/animation files to for re-use. Stores the file paths in the bbmodel project file." + }), + (0,_utils__WEBPACK_IMPORTED_MODULE_8__.make)(new Setting(_constants__WEBPACK_IMPORTED_MODULE_7__.SETTING_DEFAULT_MODID, { + // The below is absolutely disgusting, but I have no choice because this is a bug in Blockbench's API + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + type: 'text', + value: "", + category: "export", + name: "Default Mod ID", + description: "Default Mod ID for models (if applicable)" + }), setting => { + setting.onChange = function () { + const invalidNamespaceChar = new RegExp('[^_\\-.a-z0-9]+', 'g'); + const pseudoWhitepaceChar = new RegExp('[\\s&-]+', 'g'); + this.master_value = this.master_value.toLowerCase().replace(pseudoWhitepaceChar, "_").replace(invalidNamespaceChar, ""); + return {}; + }; + }) + ]; +} +/** + * Create and return the plugin's properties. + *
+ * These are metadata values stored in the project, usually used in project settings windows + */ +function createPluginProperties() { + return [ + (0,_utils__WEBPACK_IMPORTED_MODULE_8__.make)(new Property(ModelProject, "string", _constants__WEBPACK_IMPORTED_MODULE_7__.PROPERTY_MODID, { + label: "Mod ID", + condition: { + formats: [_constants__WEBPACK_IMPORTED_MODULE_7__.GECKOLIB_MODEL_ID] + }, + values: [], + merge_validation: _utils__WEBPACK_IMPORTED_MODULE_8__.isValidNamespace + }), property => { + property['placeholder'] = 'my_modid'; + property['description'] = 'The modid of the mod this model is for'; + property.getDefault = function () { + return settings[_constants__WEBPACK_IMPORTED_MODULE_7__.SETTING_DEFAULT_MODID].value; + }; + }), + (0,_utils__WEBPACK_IMPORTED_MODULE_8__.make)(new Property(ModelProject, "enum", _constants__WEBPACK_IMPORTED_MODULE_7__.PROPERTY_MODEL_TYPE, { + label: "Model Type", + condition: { + formats: [_constants__WEBPACK_IMPORTED_MODULE_7__.GECKOLIB_MODEL_ID] + }, + exposed: false, + options: _constants__WEBPACK_IMPORTED_MODULE_7__.GeckoModelType, + values: Object.values(_constants__WEBPACK_IMPORTED_MODULE_7__.GeckoModelType) + }), property => { + property['description'] = 'The type of GeckoLib object this model is for. Leave as the default value if unsure'; + }), + (0,_utils__WEBPACK_IMPORTED_MODULE_8__.make)(new Property(ModelProject, "instance", _constants__WEBPACK_IMPORTED_MODULE_7__.PROPERTY_FILEPATH_CACHE, { + label: "GeckoLib Filepath Cache", + condition: { + formats: [_constants__WEBPACK_IMPORTED_MODULE_7__.GECKOLIB_MODEL_ID] + }, + exposed: false, + values: [] + }), property => { + property.default = {}; + }) + ]; +} +/** + * Create and return the plugin's menu items + *
+ * These are added to Blockbench's menu bar or submenus
+ */
+function createPluginMenuItems() {
+ return [
+ {
+ action: new Action("export_geckolib_model", {
name: "Export GeckoLib Model",
icon: "archive",
- description: "Export your java animated model as a model for GeckoLib.",
+ description: "Export your model geometry as a model for GeckoLib.",
category: "file",
- condition: () => Format.id === "animated_entity_model",
+ condition: () => (0,_utils__WEBPACK_IMPORTED_MODULE_8__.isGeckoLibModel)(),
click: function () {
- _codec__WEBPACK_IMPORTED_MODULE_3__["default"].export();
+ _codec__WEBPACK_IMPORTED_MODULE_6__["default"].export();
},
- });
- MenuBar.addAction(exportAction, "file.export");
- exportDisplayAction = new Action("export_geckolib_display", {
+ }),
+ menuCategory: 'file.export'
+ },
+ {
+ action: new Action("export_geckolib_display", {
name: "Export GeckoLib Display Settings",
icon: "icon-bb_interface",
- description: "Export your java animated model display settings for GeckoLib.",
+ description: "Export your item/block display settings for GeckoLib.",
category: "file",
- condition: () => Format.id === "animated_entity_model" && _settings__WEBPACK_IMPORTED_MODULE_6__["default"].objectType === _settings__WEBPACK_IMPORTED_MODULE_6__.OBJ_TYPE_BLOCK_ITEM,
- click: _codec__WEBPACK_IMPORTED_MODULE_3__.maybeExportItemJson,
- });
- MenuBar.addAction(exportDisplayAction, "file.export");
- button = new Action('gecko_settings', {
- name: 'GeckoLib Model Settings...',
- description: 'Configure animated model.',
- icon: 'info',
- condition: () => Format.id === "animated_entity_model",
- click: function () {
- const dialog = new Dialog({
- id: 'project',
- title: 'GeckoLib Model Settings',
- width: 540,
- lines: [`GeckoLib Animation Utils v${version}`],
- form: {
- objectType: { label: 'Object Type', type: 'select', default: _settings__WEBPACK_IMPORTED_MODULE_6__["default"].objectType, options: _settings__WEBPACK_IMPORTED_MODULE_6__.OBJ_TYPE_OPTIONS },
- // modSDK: {label: 'Modding SDK', type: 'select', default: geckoSettings.modSDK, options: MOD_SDK_OPTIONS},
- // entityType: {label: 'Entity Type', value: geckoSettings.entityType},
- // javaPackage: {label: 'Java Package', value: geckoSettings.javaPackage},
- // animFileNamespace: {label: 'Animation File Namespace', value: geckoSettings.animFileNamespace},
- // animFilePath: {label: 'Animation File Path', value: geckoSettings.animFilePath},
- },
- onConfirm: function (formResult) {
- Object.assign(_settings__WEBPACK_IMPORTED_MODULE_6__["default"], formResult);
- (0,_settings__WEBPACK_IMPORTED_MODULE_6__.onSettingsChanged)();
- dialog.hide();
- }
- });
- dialog.show();
- }
- });
- MenuBar.addAction(button, 'file.1');
- },
- onunload() {
- exportAction.delete();
- exportDisplayAction.delete();
- button.delete();
- (0,_keyframe__WEBPACK_IMPORTED_MODULE_5__.unloadKeyframeOverrides)();
- (0,_animationUi__WEBPACK_IMPORTED_MODULE_4__.unloadAnimationUI)();
- (0,_codec__WEBPACK_IMPORTED_MODULE_3__.unloadCodec)();
- (0,_utils__WEBPACK_IMPORTED_MODULE_7__.removeMonkeypatches)();
- console.clear(); // eslint-disable-line no-console
+ condition: () => (0,_utils__WEBPACK_IMPORTED_MODULE_8__.isGeckoLibModel)() && (0,_utils__WEBPACK_IMPORTED_MODULE_8__.hasModelDisplaySettings)(),
+ click: _codec__WEBPACK_IMPORTED_MODULE_6__.buildDisplaySettingsJson,
+ }),
+ menuCategory: 'file.export'
},
- }));
-})();
+ {
+ action: new Action("export_geckolib_animations", {
+ name: "Export GeckoLib Animations",
+ icon: "movie",
+ description: "Export your model animations for GeckoLib.",
+ category: "file",
+ condition: () => (0,_utils__WEBPACK_IMPORTED_MODULE_8__.isGeckoLibModel)() && !(0,_utils__WEBPACK_IMPORTED_MODULE_8__.isEmpty)(AnimationItem.all) && typeof BarItems['export_animation_file'] === 'object',
+ click: e => BarItems['export_animation_file'].trigger(e),
+ }),
+ menuCategory: 'file.export'
+ }
+ ];
+}
})();
diff --git a/plugins/animation_utils/changelog.json b/plugins/animation_utils/changelog.json
new file mode 100644
index 00000000..21230726
--- /dev/null
+++ b/plugins/animation_utils/changelog.json
@@ -0,0 +1,131 @@
+{
+ "3.0.7": {
+ "title": "3.0.7",
+ "author": "Eliot Lash",
+ "categories": [
+ {
+ "title": "Changes",
+ "list": [
+ "Disable minification of JS bundle, fix some build errors on case sensitive filesystems, and upgrade to NodeJS v16.16"
+ ]
+ },
+ {
+ "title": "Bug Fixes",
+ "list": [
+ "Don't save `geckolib_format_version` in animation json for bedrock models",
+ "Remove hold menu hiding code that was causing issues for other plugins (regression of an old bug occurred in version 3.0.6)"
+ ]
+ }
+ ]
+ },
+ "3.1.0": {
+ "title": "3.1.0",
+ "author": "Eliot Lash",
+ "categories": [
+ {
+ "title": "New Features",
+ "list": [
+ "Added support for \"Reverse Keyframes\" action"
+ ]
+ },
+ {
+ "title": "Changes",
+ "list": [
+ "Update to new plugin format, bump minimum Blockbench version to 4.8.0",
+ "Ported plugin to TypeScript, added developer README and a few unit tests"
+ ]
+ }
+ ]
+ },
+ "3.1.1": {
+ "title": "3.1.1",
+ "author": "Tslat",
+ "categories": [
+ {
+ "title": "Bug Fixes",
+ "list": [
+ "Fix the item display settings being cleared if saving as an entity type model",
+ "Fix the armour template having swapped pivot points on the legs",
+ "Fix incorrect importing of loop type. Closes [#591](https://github.com/bernie-g/geckolib/issues/591)"
+ ]
+ }
+ ]
+ },
+ "3.2": {
+ "title": "3.2",
+ "author": "Tslat",
+ "categories": [
+ {
+ "title": "New Features",
+ "list": [
+ "Auto-export the particle texture entry in the textures list for block/item display jsons if not defined",
+ "Auto-convert bedrock animation jsons to GeckoLib-supported animation jsons when exporting"
+ ]
+ },
+ {
+ "title": "Bug Fixes",
+ "list": [
+ "Fix the particle texture entry not exporting if the name doesn't end in .png",
+ "Fixed item_display_transforms being shipped with .geo jsons for non-bedrock models",
+ "Forced known forward-compatible versions to export as 1.12.0 to maintain compatibility while we work out a better system"
+ ]
+ }
+ ]
+ },
+ "3.2.1": {
+ "title": "3.2.1",
+ "author": "Tslat",
+ "categories": [
+ {
+ "title": "Bug Fixes",
+ "list": [
+ "Fix some animation exporting issues with specific animation setup cases"
+ ]
+ }
+ ]
+ },
+ "4.0": {
+ "title": "4.0",
+ "date": "2024-10-06",
+ "author": "Tslat",
+ "categories": [
+ {
+ "title": "New Features",
+ "list": [
+ "Overhauled the new project window, adding new settings and a much more dynamic experience",
+ "Add export of animations json from File -> Export menu for GeckoLib projects",
+ "Add plugin setting for default modid for projects",
+ "Add plugin setting for forcing the Display tab to be visible",
+ "Add plugin setting for auto-computing the particle texture when otherwise unspecified for item display jsons",
+ "Add plugin setting for auto-converting Bedrock-format animations to GeckoLib-format animations when exporting to json, if relevant",
+ "New GeckoLib models will now state their model type in the tab name",
+ "Auto-apply modid and parent folder prefix to texture path in display settings",
+ "Remember export locations for model, animations and item display settings per-project, independently"
+ ]
+ },
+ {
+ "title": "Changes",
+ "list": [
+ "Allow exporting of item display settings if the model has them set, even if not in block/item mode",
+ "Skip auto-exporting particle entry for item display if not a valid path",
+ "Don't store filepath in BB project",
+ "Don't include .item in exported display jsons by default",
+ "Updated the plugin's about panel",
+ "Updated the plugin's icon",
+ "Added api-supported changelog",
+ "Tweaked various information and tooltip entries to be more clear",
+ "Large internal cleanup",
+ "Don't print plugin load console line until plugin is actually loaded",
+ "The Display tab will now disappear/reappear correctly when switching between projects"
+ ]
+ },
+ {
+ "title": "Bug Fixes",
+ "list": [
+ "Fix Geckolib item/block models not displaying properly in the display tab",
+ "Fix the item model parent sometimes not being applied"
+ ]
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/plugins/animation_utils/icon.png b/plugins/animation_utils/icon.png
index 4b8652bc..1e40b519 100644
Binary files a/plugins/animation_utils/icon.png and b/plugins/animation_utils/icon.png differ
diff --git a/plugins/animation_utils/src/codec.ts b/plugins/animation_utils/src/codec.ts
deleted file mode 100644
index a707d81f..00000000
--- a/plugins/animation_utils/src/codec.ts
+++ /dev/null
@@ -1,429 +0,0 @@
-import omit from 'lodash/omit';
-import geckoSettings, {GECKO_SETTINGS_DEFAULT, onSettingsChanged} from './settings';
-import {addMonkeypatch, Original} from './utils';
-import type { EasingKey } from './easing';
-
-interface GeckolibKeyframeOptions extends KeyframeOptions {
- easing: EasingKey
- easingArgs: number[] | null | undefined
-}
-
-// This subclass isn't strictly needed at runtime but was required to appease the compiler due to our monkeypatch
-class GeckolibBoneAnimator extends BoneAnimator {
- public addKeyframe(data: GeckolibKeyframeOptions, uuid?: string): _Keyframe {
- return super.addKeyframe(data, uuid);
- }
-}
-
-/* eslint-disable no-useless-escape */
-
-//#region Codec Helpers / Export Settings
-
-export function loadCodec() {
- // The actual Codec is automatically registered by superclass constructor
- Codecs.project.on('compile', onProjectCompile);
- Codecs.project.on('parse', onProjectParse);
- Codecs.bedrock.on('compile', onBedrockCompile);
- addMonkeypatch(Animator, null, "buildFile", animatorBuildFile);
- addMonkeypatch(Animator, null, "loadFile", animatorLoadFile);
-}
-
-export function unloadCodec() {
- Codecs.project.removeListener('compile', onProjectCompile);
- Codecs.project.removeListener('parse', onProjectParse);
- Codecs.bedrock.removeListener('compile', onBedrockCompile);
- format.delete();
-}
-
-function onProjectCompile(e: any) {
- if (Format.id !== "animated_entity_model") return;
- e.model.geckoSettings = geckoSettings;
- // console.log(`compileCallback model:`, e.model);
-}
-
-function onProjectParse(e: any) {
- // console.log(`onProjectParse:`, e);
- if (e.model && typeof e.model.geckoSettings === 'object') {
- Object.assign(geckoSettings, omit(e.model.geckoSettings, ['formatVersion']));
- } else {
- Object.assign(geckoSettings, GECKO_SETTINGS_DEFAULT);
- }
- onSettingsChanged();
-}
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-function onBedrockCompile(e: any) {
- if (Format.id !== "animated_entity_model") return;
-
- // Remove display transforms from non-bedrock geometry
- const geometry = e.model["minecraft:geometry"];
-
- if (geometry) {
- geometry.forEach((geo: Map
+ * The contents of this is mostly a copy of
+ * Only called for GeckoLib projects
+ */
+export function buildDisplaySettingsJson(options = {}) {
+ if (!Project)
+ return;
+
+ const modelProperties: any = {}
+
+ if (options['comment'] || settings.credit.value)
+ modelProperties.credit = settings.credit.value
+
+ if (options['parent'] || Project.parent != '')
+ modelProperties.parent = Project.parent
+
+ if (options['ambientocclusion'] || Project.ambientocclusion === false)
+ modelProperties.ambientocclusion = false
+
+ if (Project.texture_width !== 16 || Project.texture_height !== 16)
+ modelProperties.texture_size = [Project.texture_width, Project.texture_height]
+
+ if (options['front_gui_light'] || Project.front_gui_light)
+ modelProperties.gui_light = 'front';
+
+ if (options['overrides'] || Project.overrides)
+ modelProperties.overrides = Project.overrides;
+
+ if (options['display'] || !isEmpty(Project.display_settings)) {
+ const nonDefaultDisplays = {}
+
+ for (const slot in DisplayMode.slots) {
+ const perspective = DisplayMode.slots[slot]
+ // eslint-disable-next-line no-prototype-builtins
+ if (DisplayMode.slots.hasOwnProperty(slot) && Project.display_settings[perspective]) {
+ const display: any = Project.display_settings[perspective].export();
+
+ if (display)
+ nonDefaultDisplays[perspective] = display
+ }
+
+ }
+
+ if (!isEmpty(nonDefaultDisplays))
+ modelProperties.display = nonDefaultDisplays
+ }
+
+ if (options['textures'] || !isEmpty(Project.textures)) {
+ for (const texture of Project.textures) {
+ if (texture.particle || (settings[SETTING_AUTO_PARTICLE_TEXTURE].value && Object.keys(Project.textures).length === 1)) {
+ let name = texture.name;
+
+ if (name.indexOf(".png") > 0)
+ name = name.substring(0, name.indexOf(".png"))
+
+ if (!texture.particle) {
+ if (!isValidPath(name)) {
+ name = name.toLowerCase().replace(" ", "_")
+
+ if (!isValidPath(name))
+ continue;
+ }
+ }
+
+ name = (Project[PROPERTY_MODEL_TYPE] == GeckoModelType.BLOCK ? "block/" : "item/") + name;
+
+ if (Project[PROPERTY_MODID])
+ name = Project[PROPERTY_MODID] + ":" + name
+
+ modelProperties.textures = {'particle': name};
+
+ break
+ }
+ }
+ }
+
+ Blockbench.export({
+ resource_id: 'model',
+ type: Codecs.java_block.name,
+ extensions: ['json'],
+ name: Project.model_identifier ? (Project.model_identifier + ".json") : codec.fileName().replace(".geo", ""),
+ startpath: Project[PROPERTY_FILEPATH_CACHE].display,
+ content: JSON.stringify(modelProperties, null, 2),
+ }, file_path => {
+ const oldPath = Project[PROPERTY_FILEPATH_CACHE].display;
+ Project[PROPERTY_FILEPATH_CACHE].display = settings[SETTING_REMEMBER_EXPORT_LOCATIONS].value ? file_path : undefined;
+
+ if (oldPath !== Project[PROPERTY_FILEPATH_CACHE].display)
+ Project.saved = false;
+ });
+
+ return this;
+}
+
+export default codec;
\ No newline at end of file
diff --git a/plugins/animation_utils/src/ts/constants.ts b/plugins/animation_utils/src/ts/constants.ts
new file mode 100644
index 00000000..2d7f7ee5
--- /dev/null
+++ b/plugins/animation_utils/src/ts/constants.ts
@@ -0,0 +1,32 @@
+/**
+ * GeckoLib plugin model format ID. Used to identify model types generated from this plugin
+ */
+export const GECKOLIB_MODEL_ID = "animated_entity_model"
+
+// Setting name constants
+export const SETTING_AUTO_PARTICLE_TEXTURE = 'geckolib_auto_particle_texture';
+export const SETTING_CONVERT_BEDROCK_ANIMATIONS = 'geckolib_convert_bedrock_animations';
+export const SETTING_ALWAYS_SHOW_DISPLAY = 'geckolib_always_show_display';
+export const SETTING_REMEMBER_EXPORT_LOCATIONS = 'geckolib_remember_export_locations';
+export const SETTING_DEFAULT_MODID = 'geckolib_default_modid';
+
+// Property name constants
+export const PROPERTY_MODID = 'geckolib_modid';
+export const PROPERTY_MODEL_TYPE = 'geckolib_model_type';
+export const PROPERTY_FILEPATH_CACHE = 'geckolib_filepath_cache';
+
+/**
+ * Available GeckoLib model types
+ */
+export enum GeckoModelType {
+ ENTITY = 'Entity',
+ BLOCK = 'Block',
+ ITEM = 'Item',
+ ARMOR = 'Armor',
+ OBJECT = 'Object'
+}
+
+/**
+ * Statically defined type for the filepath cache property, for ease of use
+ */
+export type GeckoFilepathCache = {model?: string, animation?: string, display?: string}
\ No newline at end of file
diff --git a/plugins/animation_utils/src/easing.ts b/plugins/animation_utils/src/ts/easing.ts
similarity index 86%
rename from plugins/animation_utils/src/easing.ts
rename to plugins/animation_utils/src/ts/easing.ts
index 7979de69..f8260078 100644
--- a/plugins/animation_utils/src/easing.ts
+++ b/plugins/animation_utils/src/ts/easing.ts
@@ -32,32 +32,37 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* @returns {Number}
*/
function findIntervalBorderIndex(point: number, intervals: number[], useRightBorder: boolean) {
- //If point is beyond given intervals
- if (point < intervals[0])
- return 0
- if (point > intervals[intervals.length - 1])
- return intervals.length - 1
- //If point is inside interval
- //Start searching on a full range of intervals
- let indexOfNumberToCompare = 0;
- let leftBorderIndex = 0;
- let rightBorderIndex = intervals.length - 1
- //Reduce searching range till it find an interval point belongs to using binary search
- while (rightBorderIndex - leftBorderIndex !== 1) {
- indexOfNumberToCompare = leftBorderIndex + Math.floor((rightBorderIndex - leftBorderIndex) / 2)
- point >= intervals[indexOfNumberToCompare] ?
- leftBorderIndex = indexOfNumberToCompare :
- rightBorderIndex = indexOfNumberToCompare
+ //If point is beyond given intervals
+ if (point < intervals[0])
+ return 0
+
+ if (point > intervals[intervals.length - 1])
+ return intervals.length - 1
+
+ //If point is inside interval
+ //Start searching on a full range of intervals
+ let indexOfNumberToCompare = 0;
+ let leftBorderIndex = 0;
+ let rightBorderIndex = intervals.length - 1
+
+ //Reduce searching range till it find an interval point belongs to using binary search
+ while (rightBorderIndex - leftBorderIndex !== 1) {
+ indexOfNumberToCompare = leftBorderIndex + Math.floor((rightBorderIndex - leftBorderIndex) / 2)
+ point >= intervals[indexOfNumberToCompare] ?
+ leftBorderIndex = indexOfNumberToCompare :
+ rightBorderIndex = indexOfNumberToCompare
}
+
return useRightBorder ? rightBorderIndex : leftBorderIndex
}
function stepRange(steps: number, stop = 1) {
- if (steps < 2) throw new Error("steps must be > 2, got:" + steps);
- const stepLength = stop / steps;
- return Array.from({
- length: steps
- }, (_, i) => i * stepLength);
+ if (steps < 2)
+ throw new Error("steps must be > 2, got:" + steps);
+
+ const stepLength = stop / steps;
+
+ return Array.from({length: steps}, (_, i) => i * stepLength);
}
type EasingFunction = (t: number) => number;
@@ -235,12 +240,9 @@ class Easing {
const quart = Easing.poly(4);
const quint = Easing.poly(5);
-const back = (direction: EasingDirection, scalar: number, t: number) =>
- direction(Easing.back(1.70158 * scalar))(t);
-const elastic = (direction: EasingDirection, bounciness: number, t: number) =>
- direction(Easing.elastic(bounciness))(t);
-const bounce = (direction: EasingDirection, bounciness: number, t: number) =>
- direction(Easing.bounce(bounciness))(t);
+const back = (direction: EasingDirection, scalar: number, t: number) => direction(Easing.back(1.70158 * scalar))(t);
+const elastic = (direction: EasingDirection, bounciness: number, t: number) => direction(Easing.elastic(bounciness))(t);
+const bounce = (direction: EasingDirection, bounciness: number, t: number) => direction(Easing.bounce(bounciness))(t);
export const easingFunctions = {
linear: Easing.linear,
@@ -335,9 +337,23 @@ export const parseEasingArg = (kf: GeckolibKeyframe, value: string) => {
};
export function reverseEasing(easing?: EasingKey): EasingKey {
- if (!easing) return easing;
- if (easing.startsWith("easeInOut")) return easing;
- if (easing.startsWith("easeIn")) return easing.replace("easeIn", "easeOut") as EasingKey;
- if (easing.startsWith("easeOut")) return easing.replace("easeOut", "easeIn") as EasingKey;
+ if (!easing)
+ return easing;
+
+ if (easing.startsWith("easeInOut"))
+ return easing;
+
+ if (easing.startsWith("easeIn"))
+ return easing.replace("easeIn", "easeOut") as EasingKey;
+
+ if (easing.startsWith("easeOut"))
+ return easing.replace("easeOut", "easeIn") as EasingKey;
+
return easing;
}
+
+export const isArgsEasing = (easing = "") =>
+ easing.includes("Back") ||
+ easing.includes("Elastic") ||
+ easing.includes("Bounce") ||
+ easing === EASING_OPTIONS.step;
diff --git a/plugins/animation_utils/src/ts/events.ts b/plugins/animation_utils/src/ts/events.ts
new file mode 100644
index 00000000..82c8e077
--- /dev/null
+++ b/plugins/animation_utils/src/ts/events.ts
@@ -0,0 +1,442 @@
+import {
+ addCodecCallback,
+ addEventListener,
+ addMonkeypatch, hasModelDisplaySettings, isGeckoLibModel, Monkeypatches,
+ onlyIfGeckoLib,
+ removeCodecCallback,
+ removeEventListener, removeMonkeypatches
+} from "./utils";
+import {GeckolibBoneAnimator} from "./keyframe";
+import {
+ GeckoModelType,
+ PROPERTY_FILEPATH_CACHE,
+ PROPERTY_MODEL_TYPE, SETTING_ALWAYS_SHOW_DISPLAY, SETTING_CONVERT_BEDROCK_ANIMATIONS,
+ SETTING_REMEMBER_EXPORT_LOCATIONS
+} from "./constants";
+import {openProjectSettingsDialog} from "./codec";
+
+export function addEventListeners() {
+ addCodecCallback(Codecs.project, 'parse', onlyIfGeckoLib(onProjectParse))
+ addCodecCallback(Codecs.bedrock, 'compile', onlyIfGeckoLib(onBedrockCompile))
+ addEventListener('select_mode', onlyIfGeckoLib(onModeSelect));
+ addEventListener('select_project', onlyIfGeckoLib(onProjectSelect));
+ addEventListener('update_project_settings', onlyIfGeckoLib(onSettingsChanged));
+ addEventListener('save_project', onlyIfGeckoLib(onProjectSave));
+ addMonkeypatch(Animator, null, "buildFile", monkeypatchAnimatorBuildFile);
+ addMonkeypatch(Animator, null, "loadFile", monkeypatchAnimatorLoadFile);
+ addMonkeypatch(Blockbench, null, "export", monkeypatchBlockbenchExport);
+ addMonkeypatch(BarItems, 'project_window', "click", monkeypatchProjectWindowClick);
+}
+
+export function removeEventListeners() {
+ removeCodecCallback(Codecs.project, 'parse', onlyIfGeckoLib(onProjectParse))
+ removeCodecCallback(Codecs.bedrock, 'compile', onlyIfGeckoLib(onBedrockCompile))
+ removeEventListener('select_mode', onlyIfGeckoLib(onModeSelect));
+ removeEventListener('select_project', onlyIfGeckoLib(onProjectSelect));
+ removeEventListener('update_project_settings', onlyIfGeckoLib(onSettingsChanged));
+ removeMonkeypatches();
+}
+
+/**
+ * When an existing GeckoLib project is being read from file
+ */
+function onProjectParse(e: any) {
+ onSettingsChanged();
+
+ // Because the project hasn't had its model properties applied at this stage
+ Format.display_mode = (e.model[PROPERTY_MODEL_TYPE] && e.model[PROPERTY_MODEL_TYPE] === GeckoModelType.ITEM) || settings[SETTING_ALWAYS_SHOW_DISPLAY].value;
+}
+
+/**
+ * When the Blockbench project is being saved
+ *
+ * Only called for GeckoLib projects
+ */
+function onProjectSave(e: {model: object, options: any }) {
+ if (!settings[SETTING_REMEMBER_EXPORT_LOCATIONS].value)
+ e.model[PROPERTY_FILEPATH_CACHE] = {}
+}
+
+/**
+ * When the GeckoLib project settings are changed, or a GeckoLib project is being opened or swapped to
+ *
+ * Only called for GeckoLib projects
+ */
+function onSettingsChanged() {
+ Modes.selected.select();
+
+ Format.display_mode = hasModelDisplaySettings();
+
+ if (Project instanceof ModelProject && Project[PROPERTY_MODEL_TYPE] === GeckoModelType.ITEM && (!Project.parent || Project.parent !== 'builtin/entity')) {
+ Project.parent = 'builtin/entity';
+ Project.saved = false;
+ }
+}
+
+/**
+ * When opening a project tab, whether from an existing project, creating a new one, or swapping open tabs
+ *
+ * Only called for GeckoLib projects
+ */
+function onProjectSelect() {
+ onSettingsChanged()
+}
+
+/**
+ * When selecting edit/paint/display/animate/etc
+ *
+ * Only called for GeckoLib projects
+ */
+function onModeSelect(e: any) {
+ // Offset the display emulator to account for GeckoLib's +0.51 manual offset
+ // This is a legacy patch as Blockbench no longer does this internally
+ if (e.mode.id == 'display')
+ (Project as ModelProject).model_3d.position.y = 0;
+}
+
+/**
+ * When the model geometry is being compiled for export
+ *
+ * Only called for GeckoLib projects
+ */
+function onBedrockCompile(e: any) {
+ // Remove display transforms from non-bedrock geometry
+ e.model["minecraft:geometry"]?.forEach((geo: Map
+ * The project may not be a GeckoLib project, so check it as necessary
+ */
+function monkeypatchProjectWindowClick() {
+ if (isGeckoLibModel()) {
+ openProjectSettingsDialog();
+ }
+ else {
+ Monkeypatches.get(BarItems).click();
+ }
+}
+
+/**
+ * When any file is being exported to disk by Blockbench
+ *
+ * The project may not be a GeckoLib project, so check it as necessary
+ */
+function monkeypatchBlockbenchExport(options, cb) {
+ if (!isGeckoLibModel()) {
+ Monkeypatches.get(Blockbench).export(options, cb);
+
+ return;
+ }
+
+ if (Project instanceof ModelProject) {
+ if (options.resource_id === 'animation' && options.type === 'JSON Animation') { // Animation JSON
+ const fileName = Project.model_identifier && Project.model_identifier + ".animation";
+ options.startpath = Project[PROPERTY_FILEPATH_CACHE].animation;
+ const parentCallback = cb;
+ cb = file_path => {
+ if (parentCallback)
+ parentCallback(file_path);
+
+ const oldPath = Project[PROPERTY_FILEPATH_CACHE].animation;
+ Project[PROPERTY_FILEPATH_CACHE].animation = settings[SETTING_REMEMBER_EXPORT_LOCATIONS].value ? file_path : undefined;
+
+ if (oldPath !== Project[PROPERTY_FILEPATH_CACHE].animation)
+ Project.saved = false;
+ }
+
+ if (fileName)
+ options.name = fileName;
+ }
+ else if (options.resource_id === 'model' && options.type === 'Bedrock Model') { // Geo
+ const fileName = Project.model_identifier && Project.model_identifier + ".geo";
+ options.startpath = Project[PROPERTY_FILEPATH_CACHE].model;
+ const parentWriter = options.custom_writer;
+ const parentCallback = cb;
+
+ if (parentWriter) {
+ options.custom_writer = (content, filePath, callback) => {
+ parentWriter(content, filePath, callback);
+ callback(filePath)
+ }
+ }
+
+ cb = file_path => {
+ if (parentCallback)
+ parentCallback(file_path);
+
+ const oldPath = Project[PROPERTY_FILEPATH_CACHE].model;
+ Project[PROPERTY_FILEPATH_CACHE].model = settings[SETTING_REMEMBER_EXPORT_LOCATIONS].value ? file_path : undefined;
+
+ if (oldPath !== Project[PROPERTY_FILEPATH_CACHE].model)
+ Project.saved = false;
+ }
+
+ if (fileName)
+ options.name = fileName;
+ }
+ }
+
+ Monkeypatches.get(Blockbench).export(options, cb);
+}
+
+/**
+ * When the animation file is being loaded into the project
+ *
+ * The project may not be a GeckoLib project, so check it as necessary
+ */
+function monkeypatchAnimatorLoadFile(file, exportingAnims) {
+ // eslint-disable-next-line no-undef
+ const json = file.json || autoParseJSON(file.content);
+ const path = file.path;
+ const new_animations = [];
+
+ function geoLoopToBbLoop(jsonLoop) {
+ if (jsonLoop) {
+ if (typeof jsonLoop === 'boolean')
+ return jsonLoop ? 'loop' : 'once'
+
+ if (typeof jsonLoop === 'string') {
+ if (jsonLoop === "hold_on_last_frame")
+ return 'hold'
+
+ if (jsonLoop === "loop" || jsonLoop === "true")
+ return 'loop'
+ }
+ }
+
+ return 'once'
+ }
+
+ function getKeyframeDataPoints(source: any) {
+ if (source instanceof Array)
+ return [{x: source[0], y: source[1], z: source[2],}]
+
+ if (['number', 'string'].includes(typeof source))
+ return [{x: source, y: source, z: source}]
+
+ if (typeof source == 'object') {
+ if (source.vector)
+ return getKeyframeDataPoints(source.vector);
+
+ const points = [];
+
+ if (source.pre)
+ points.push(getKeyframeDataPoints(source.pre)[0])
+
+ if (source.post)
+ points.push(getKeyframeDataPoints(source.post)[0])
+
+ return points;
+ }
+ }
+
+ if (json && typeof json.animations === 'object') {
+ for (const animName in json.animations) {
+ if (exportingAnims && !exportingAnims.includes(animName))
+ continue;
+
+ //Animation
+ const animData = json.animations[animName]
+ const animation = new Blockbench.Animation({
+ name: animName,
+ path,
+ loop: geoLoopToBbLoop(animData.loop),
+ override: animData.override_previous_animation,
+ anim_time_update: (typeof animData.anim_time_update == 'string'
+ ? animData.anim_time_update.replace(/;(?!$)/, ';\n')
+ : animData.anim_time_update),
+ blend_weight: (typeof animData.blend_weight == 'string'
+ ? animData.blend_weight.replace(/;(?!$)/, ';\n')
+ : animData.blend_weight),
+ length: animData.animation_length
+ }).add()
+ //Bones
+ if (animData.bones) {
+ for (const boneName in animData.bones) {
+ const bone = animData.bones[boneName]
+ const lowercase_bone_name = boneName.toLowerCase();
+ const group = Group.all.find(group => group.name.toLowerCase() == lowercase_bone_name)
+ const uuid = group ? group.uuid : guid();
+ let ga : GeneralAnimator; // eslint-disable-line @typescript-eslint/no-unused-vars
+
+ const boneAnimator = new GeckolibBoneAnimator(uuid, animation, boneName);
+ animation.animators[uuid] = boneAnimator;
+ //Channels
+ for (const channel in bone) {
+ if (Animator.possible_channels[channel]) {
+ if (typeof bone[channel] === 'string' || typeof bone[channel] === 'number' || bone[channel] instanceof Array) {
+ boneAnimator.addKeyframe({
+ time: 0,
+ channel,
+ easing: bone[channel].easing,
+ easingArgs: bone[channel].easingArgs,
+ data_points: getKeyframeDataPoints(bone[channel]),
+ })
+ }
+ else if (typeof bone[channel] === 'object' && bone[channel].post) {
+ boneAnimator.addKeyframe({
+ time: 0,
+ channel,
+ easing: bone[channel].easing,
+ easingArgs: bone[channel].easingArgs,
+ interpolation: bone[channel].lerp_mode,
+ data_points: getKeyframeDataPoints(bone[channel]),
+ });
+ }
+ else if (typeof bone[channel] === 'object') {
+ for (const timestamp in bone[channel]) {
+ boneAnimator.addKeyframe({
+ time: parseFloat(timestamp),
+ channel,
+ easing: bone[channel][timestamp].easing,
+ easingArgs: bone[channel][timestamp].easingArgs,
+ interpolation: bone[channel][timestamp].lerp_mode,
+ data_points: getKeyframeDataPoints(bone[channel][timestamp]),
+ });
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (animData.sound_effects) {
+ if (!animation.animators.effects)
+ animation.animators.effects = new EffectAnimator(animation);
+
+ for (const timestamp in animData.sound_effects) {
+ const sounds = animData.sound_effects[timestamp];
+
+ animation.animators.effects.addKeyframe({
+ channel: 'sound',
+ time: parseFloat(timestamp),
+ data_points: sounds instanceof Array ? sounds : [sounds]
+ })
+ }
+ }
+
+ if (animData.particle_effects) {
+ if (!animation.animators.effects)
+ animation.animators.effects = new EffectAnimator(animation);
+
+ for (const timestamp in animData.particle_effects) {
+ let particles = animData.particle_effects[timestamp];
+
+ if (!(particles instanceof Array))
+ particles = [particles];
+
+ particles.forEach(particle => {
+ if (particle)
+ particle.script = particle.pre_effect_script;
+ })
+
+ animation.animators.effects.addKeyframe({
+ channel: 'particle',
+ time: parseFloat(timestamp),
+ data_points: particles
+ })
+ }
+ }
+
+ if (animData.timeline) {
+ if (!animation.animators.effects)
+ animation.animators.effects = new EffectAnimator(animation);
+
+ for (const timestamp in animData.timeline) {
+ const entry = animData.timeline[timestamp];
+ const script = entry instanceof Array ? entry.join('\n') : entry;
+
+ animation.animators.effects.addKeyframe({
+ channel: 'timeline',
+ time: parseFloat(timestamp),
+ data_points: [{script}]
+ })
+ }
+ }
+
+ animation.calculateSnappingFromKeyframes();
+
+ if (!Blockbench.Animation.selected && Animator.open)
+ animation.select()
+
+ new_animations.push(animation)
+ }
+ }
+
+ return new_animations
+}
+
+/**
+ * When the animations json is being compiled for export
+ *
+ * The project may not be a GeckoLib project, so check it as necessary
+ */
+function monkeypatchAnimatorBuildFile() {
+ const result = Monkeypatches.get(Animator).buildFile.apply(this, arguments);
+
+ if (isGeckoLibModel()) {
+ result.geckolib_format_version = 2
+
+ // Convert exported bedrock animations to non-bedrock
+ // Only applies to projects that had its animations made in a non-GeckoLib model format
+ if (settings[SETTING_CONVERT_BEDROCK_ANIMATIONS].value && result.animations) {
+ for (const animation in result.animations) {
+ const bones = result.animations[animation].bones;
+
+ if (bones) {
+ for (const boneName in bones) {
+ const bone = bones[boneName];
+
+ for (const animationGroupType in bone) {
+ const animationGroup = bone[animationGroupType];
+
+ for (const timestamp in animationGroup) {
+ const keyframe = animationGroup[timestamp];
+
+ if (!keyframe)
+ continue
+
+ let bedrockKeyframe : Map
+ * These are found in the Settings panel in the plugin info window
+ */
+function createPluginSettings(): Setting[] {
+ return [
+ new Setting(SETTING_AUTO_PARTICLE_TEXTURE, {
+ value: true,
+ category: "export",
+ name: "Auto-compute block/item particle texture",
+ description: "Attempt to auto-compute the particle texture for a GeckoLib block/item model if one isn't already specified when exporting the display settings json"
+ }),
+ new Setting(SETTING_CONVERT_BEDROCK_ANIMATIONS, {
+ value: true,
+ category: "export",
+ name: "Convert bedrock animations on export",
+ description: "Automatically convert bedrock-format animations to GeckoLib-compatible animations when exporting, if relevant. May have a performance improvement on larger projects"
+ }),
+ new Setting(SETTING_ALWAYS_SHOW_DISPLAY, {
+ value: false,
+ category: "edit",
+ name: "Always show display tab",
+ description: "Force the Display tab to always show, even when not an Item type model"
+ }),
+ new Setting(SETTING_REMEMBER_EXPORT_LOCATIONS, {
+ value: true,
+ category: "export",
+ name: "Remember file export locations",
+ description: "Remember where you export model/display/animation files to for re-use. Stores the file paths in the bbmodel project file."
+ }),
+ make(
+ new Setting(SETTING_DEFAULT_MODID, {
+ // The below is absolutely disgusting, but I have no choice because this is a bug in Blockbench's API
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ type: 'text',
+ value: "",
+ category: "export",
+ name: "Default Mod ID",
+ description: "Default Mod ID for models (if applicable)"
+ }),
+ setting => {
+ setting.onChange = function() {
+ const invalidNamespaceChar = new RegExp('[^_\\-.a-z0-9]+', 'g')
+ const pseudoWhitepaceChar = new RegExp('[\\s&-]+', 'g')
+
+ this.master_value = this.master_value.toLowerCase().replace(pseudoWhitepaceChar, "_").replace(invalidNamespaceChar, "")
+
+ return {};
+ }
+ }
+ )
+ ];
+}
+
+/**
+ * Create and return the plugin's properties.
+ *
+ * These are metadata values stored in the project, usually used in project settings windows
+ */
+function createPluginProperties(): Property[] {
+ return [
+ make(
+ new Property(ModelProject, "string", PROPERTY_MODID, {
+ label: "Mod ID",
+ condition: {
+ formats: [GECKOLIB_MODEL_ID]
+ },
+ values: [],
+ merge_validation: isValidNamespace
+ }),
+ property => {
+ property['placeholder'] = 'my_modid';
+ property['description'] = 'The modid of the mod this model is for';
+ property.getDefault = function() {
+ return settings[SETTING_DEFAULT_MODID].value;
+ };
+ }
+ ),
+ make(
+ new Property(ModelProject, "enum", PROPERTY_MODEL_TYPE, {
+ label: "Model Type",
+ condition: {
+ formats: [GECKOLIB_MODEL_ID]
+ },
+ exposed: false,
+ options: GeckoModelType,
+ values: Object.values(GeckoModelType)
+ }),
+ property => {
+ property['description'] = 'The type of GeckoLib object this model is for. Leave as the default value if unsure';
+ }
+ ),
+ make(
+ new Property(ModelProject, "instance", PROPERTY_FILEPATH_CACHE, {
+ label: "GeckoLib Filepath Cache",
+ condition: {
+ formats: [GECKOLIB_MODEL_ID]
+ },
+ exposed: false,
+ values: []
+ }),
+ property => {
+ property.default = {} as GeckoFilepathCache;
+ }
+ )
+ ];
+}
+
+/**
+ * Create and return the plugin's menu items
+ *
+ * These are added to Blockbench's menu bar or submenus
+ */
+function createPluginMenuItems(): { action: Action, menuCategory: string }[] {
+ return [
+ {
+ action: new Action("export_geckolib_model", {
+ name: "Export GeckoLib Model",
+ icon: "archive",
+ description: "Export your model geometry as a model for GeckoLib.",
+ category: "file",
+ condition: () => isGeckoLibModel(),
+ click: function () {
+ codec.export();
+ },
+ }),
+ menuCategory: 'file.export'
+ },
+ {
+ action: new Action("export_geckolib_display", {
+ name: "Export GeckoLib Display Settings",
+ icon: "icon-bb_interface",
+ description: "Export your item/block display settings for GeckoLib.",
+ category: "file",
+ condition: () => isGeckoLibModel() && hasModelDisplaySettings(),
+ click: buildDisplaySettingsJson,
+ }),
+ menuCategory: 'file.export'
+ },
+ {
+ action: new Action("export_geckolib_animations", {
+ name: "Export GeckoLib Animations",
+ icon: "movie",
+ description: "Export your model animations for GeckoLib.",
+ category: "file",
+ condition: () => isGeckoLibModel() && !isEmpty(AnimationItem.all) && typeof BarItems['export_animation_file'] === 'object',
+ click: e => (BarItems['export_animation_file'] as Action).trigger(e),
+ }),
+ menuCategory: 'file.export'
+ }
+ ];
+}
diff --git a/plugins/animation_utils/src/keyframe.ts b/plugins/animation_utils/src/ts/keyframe.ts
similarity index 86%
rename from plugins/animation_utils/src/keyframe.ts
rename to plugins/animation_utils/src/ts/keyframe.ts
index 161dd89a..aef6d975 100644
--- a/plugins/animation_utils/src/keyframe.ts
+++ b/plugins/animation_utils/src/ts/keyframe.ts
@@ -1,17 +1,16 @@
import groupBy from 'lodash/groupBy';
import mapValues from 'lodash/mapValues';
-import {addMonkeypatch, hasArgs, Original} from './utils';
+import {addMonkeypatch, Monkeypatches} from './utils';
import {
EASING_DEFAULT,
- easingFunctions,
+ easingFunctions, type EasingKey,
EasingProperties,
GeckolibKeyframe,
- getEasingArgDefault,
+ getEasingArgDefault, isArgsEasing,
reverseEasing
} from './easing';
import Keyframe = Blockbench.Keyframe;
-//#region Keyframe Mixins
export function loadKeyframeOverrides() {
addMonkeypatch(Keyframe, "prototype", "getLerp", keyframeGetLerp);
addMonkeypatch(Keyframe, "prototype", "compileBedrockKeyframe", keyframeCompileBedrock);
@@ -25,6 +24,18 @@ export function unloadKeyframeOverrides() {
//No-op for now since monkeypatches are unloaded automatically
}
+interface GeckolibKeyframeOptions extends KeyframeOptions {
+ easing: EasingKey
+ easingArgs: number[] | null | undefined
+}
+
+// This subclass isn't strictly needed at runtime but was required to appease the compiler due to our monkeypatch
+export class GeckolibBoneAnimator extends BoneAnimator {
+ public addKeyframe(data: GeckolibKeyframeOptions, uuid?: string): _Keyframe {
+ return super.addKeyframe(data, uuid);
+ }
+}
+
function lerp(start: number, stop: number, amt: number) {
return amt * (stop - start) + start;
}
@@ -33,10 +44,10 @@ function lerp(start: number, stop: number, amt: number) {
function keyframeGetLerp(other, axis, amount, allow_expression) {
const easing = other.easing || EASING_DEFAULT;
if (Format.id !== "animated_entity_model") {
- return Original.get(Keyframe).getLerp.apply(this, arguments);
+ return Monkeypatches.get(Keyframe).getLerp.apply(this, arguments);
}
let easingFunc = easingFunctions[easing];
- if (hasArgs(easing)) {
+ if (isArgsEasing(easing)) {
const arg1 = Array.isArray(other.easingArgs) && other.easingArgs.length > 0
? other.easingArgs[0]
: getEasingArgDefault(other);
@@ -60,7 +71,7 @@ function geckolibGetArray(data_point: number = 0) {
if (Format.id === "animated_entity_model") {
result = {vector: result, easing};
- if (hasArgs(easing)) result.easingArgs = easingArgs;
+ if (isArgsEasing(easing)) result.easingArgs = easingArgs;
}
return result;
}
@@ -68,7 +79,7 @@ function geckolibGetArray(data_point: number = 0) {
function keyframeCompileBedrock() {
if (Format.id !== "animated_entity_model" || !this.transform) {
- return Original.get(Keyframe).compileBedrockKeyframe.apply(this, arguments);
+ return Monkeypatches.get(Keyframe).compileBedrockKeyframe.apply(this, arguments);
}
if (this.interpolation == 'catmullrom') {
@@ -99,10 +110,10 @@ function keyframeCompileBedrock() {
function keyframeGetUndoCopy() {
const {easing, easingArgs} = this;
- const result = Original.get(Keyframe).getUndoCopy.apply(this, arguments);
+ const result = Monkeypatches.get(Keyframe).getUndoCopy.apply(this, arguments);
if (Format.id === "animated_entity_model") {
Object.assign(result, {easing});
- if (hasArgs(easing)) result.easingArgs = easingArgs;
+ if (isArgsEasing(easing)) result.easingArgs = easingArgs;
}
// console.log('keyframeGetUndoCopy arguments:', arguments, 'this:', this, 'result:', result);
return result;
@@ -132,13 +143,13 @@ function keyframeExtend(dataIn) {
}
}
}
- const result = Original.get(Keyframe).extend.apply(this, arguments);
+ const result = Monkeypatches.get(Keyframe).extend.apply(this, arguments);
// console.log('keyframeExtend 2 arguments:', arguments, 'this:', this, 'result:', result);
return result;
}
function onReverseKeyframes() {
- Original.get(BarItems.reverse_keyframes).click.apply(this, arguments);
+ Monkeypatches.get(BarItems.reverse_keyframes).click.apply(this, arguments);
// console.log('@@@ onReverseKeyframes selected:', Timeline.selected);
// There's not really an easy way to merge our undo operation with the original one so we'll make a new one instead
Undo.initEdit({keyframes: Timeline.selected})
@@ -177,6 +188,4 @@ function onReverseKeyframes() {
Undo.finishEdit('Reverse keyframe easing')
updateKeyframeSelection();
Animator.preview();
-}
-
-//#endregion Keyframe Mixins
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/plugins/animation_utils/src/ts/utils.ts b/plugins/animation_utils/src/ts/utils.ts
new file mode 100644
index 00000000..417d3264
--- /dev/null
+++ b/plugins/animation_utils/src/ts/utils.ts
@@ -0,0 +1,125 @@
+import {GECKOLIB_MODEL_ID, GeckoModelType, PROPERTY_MODEL_TYPE, SETTING_ALWAYS_SHOW_DISPLAY} from "./constants";
+
+const VALID_NAMESPACE_PATTERN = new RegExp('^[_\\-.a-z0-9]+$')
+const VALID_PATH_PATTERN = new RegExp('^[_\\-/.a-z0-9]+$')
+export const Monkeypatches = new Map();
+
+/**
+ * Add what is effectively an override of another javascript function in a target object.
+ *
+ * The patched function should call the original first and operate on the result to ensure compatibility
+ *
+ * This should be done in
+ * All registered event listeners should be removed when the plugin or codec is unloaded
+ */
+export const removeEventListener = (eventName: EventName, callback: (data: object) => void) => {
+ Blockbench.removeListener(eventName, callback)
+}
+
+/**
+ * Add a callback to a codec to be called after the task has been completed
+ *
+ * This should be done in
+ * All registered coded callbacks should be removed when the plugin or codec is unloaded
+ */
+export const removeCodecCallback = (codec: Codec, taskName: string, callback: (data: object) => void) => {
+ codec.removeListener(taskName, callback);
+}
+
+/**
+ * Whether a given string is a valid ResourceLocation path for Minecraft
+ */
+export const isValidPath = (path: string) => {
+ return VALID_PATH_PATTERN.test(path)
+}
+
+/**
+ * Whether a given string is a valid ResourceLocation namespace for Minecraft
+ */
+export const isValidNamespace = (namespace: string) => {
+ return VALID_NAMESPACE_PATTERN.test(namespace)
+}
+
+/**
+ * Whether a map-like object has no defined keys or values
+ */
+export const isEmpty = (object = {}) => Object.keys(object).length === 0;
+
+/**
+ * Whether the currently focussed model is a GeckoLib model
+ */
+export const isGeckoLibModel = () => Format.id === GECKOLIB_MODEL_ID;
+
+/**
+ * Whether the current project is a GeckoLib model that has or uses item render perspective transforms
+ */
+export const hasModelDisplaySettings = () => isGeckoLibModel() && Project && ((Project[PROPERTY_MODEL_TYPE] === GeckoModelType.ITEM || !isEmpty(Project.display_settings)) || settings[SETTING_ALWAYS_SHOW_DISPLAY].value);
\ No newline at end of file
diff --git a/plugins/animation_utils/src/utils.ts b/plugins/animation_utils/src/utils.ts
deleted file mode 100644
index ac8212dc..00000000
--- a/plugins/animation_utils/src/utils.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { EASING_OPTIONS } from './easing';
-
-export const hasArgs = (easing = "") =>
- easing.includes("Back") ||
- easing.includes("Elastic") ||
- easing.includes("Bounce") ||
- easing === EASING_OPTIONS.step;
-
-export const Original = new Map();
-export const addMonkeypatch = (symbol, path, functionKey, newFunction) => {
- const pathAccessor = path ? symbol[path] : symbol;
- if(!Original.get(symbol)) Original.set(symbol, { _pathAccessor: pathAccessor });
- Original.get(symbol)[functionKey] = pathAccessor[functionKey];
- pathAccessor[functionKey] = newFunction;
-};
-
-export const removeMonkeypatches = () => {
- Original.forEach(symbol => {
- Object.keys(symbol).forEach(functionKey => {
- if(functionKey.startsWith('_')) return;
- symbol._pathAccessor[functionKey] = symbol[functionKey];
- });
- });
- Original.clear();
-}
\ No newline at end of file
diff --git a/plugins/animation_utils/src/webpack.config.js b/plugins/animation_utils/src/webpack.config.js
index 071d9496..a7d3d208 100644
--- a/plugins/animation_utils/src/webpack.config.js
+++ b/plugins/animation_utils/src/webpack.config.js
@@ -4,7 +4,7 @@ module.exports = {
mode: 'development',
devtool: false,
target: 'node',
- entry: './index.ts',
+ entry: './ts/index.ts',
module: {
rules: [
{
model_identifier
form element in dialog windows
+ */
+function getObjectIdPlaceholder(formResult?: {[key: string]: FormResultValue}) {
+ const name = formResult?.['name'] as string;
+ const modelType = formResult?.[PROPERTY_MODEL_TYPE] as string;
+
+ if (!name && !modelType)
+ return 'my_entity';
+
+ const invalidPathChar = new RegExp('[^_\\-/.a-z0-9]+', 'g')
+ const pseudoWhitepaceChar = new RegExp('[\\s&-]+', 'g')
+
+ if (name)
+ return name.toLowerCase().replace(pseudoWhitepaceChar, "_").replace(invalidPathChar, "");
+
+ switch (GeckoModelType[modelType]) {
+ case GeckoModelType.ENTITY: return 'my_entity'
+ case GeckoModelType.BLOCK: return 'my_block';
+ case GeckoModelType.ITEM: return 'my_item';
+ case GeckoModelType.ARMOR: return 'my_armor';
+ case GeckoModelType.OBJECT: return 'my_object';
+ default: return 'my_entity';
+ }
+}
+
+/**
+ * Create the Project Settings dialog form for use in both new projects and editing existing ones
+ */
+function createProjectSettingsForm(Project: ModelProject) {
+ const form = {format: {type: 'info', label: 'data.format', text: Format.name||'unknown', description: Format.description} as DialogFormElement}
+ const properties = ModelProject['properties'];
+
+ const modelType = properties[PROPERTY_MODEL_TYPE];
+
+ if (modelType) {
+ form[PROPERTY_MODEL_TYPE] = {
+ label: modelType.label,
+ description: modelType.description,
+ default: GeckoModelType.ENTITY.toUpperCase(),
+ value: GeckoModelType[Project[PROPERTY_MODEL_TYPE].toUpperCase()].toUpperCase(),
+ placeholder: modelType.placeholder,
+ type: 'select',
+ options: typeof modelType.options == 'function' ? modelType.options() : modelType.options,
+ }
+ }
+
+ for (const key in properties) {
+ const property = properties[key];
+
+ if (property.exposed === false || !Condition(property.condition))
+ continue;
+
+ const entry = form[property.name] = {
+ label: property.label,
+ description: property.description,
+ value: Project[property.name],
+ placeholder: property.placeholder,
+ type: property.type
+ }
+
+ if (property.name === 'name') {
+ entry.label = 'Project Name'
+ entry.placeholder = 'My Project'
+ entry.description = 'The name of the Blockbench project'
+ }
+ else if (property.name === 'model_identifier') {
+ entry.label = 'Object ID'
+ entry.description = 'The registered id of the object this model represents, for exporting purposes'
+ entry.placeholder = getObjectIdPlaceholder()
+ }
+
+ switch (property.type) {
+ case 'boolean':
+ entry.type = 'checkbox'
+ break;
+ case 'string':
+ entry.type = 'text';
+ break;
+ default:
+ if (property.options) {
+ entry['options'] = typeof property.options == 'function' ? property.options() : property.options;
+ entry.type = 'select';
+ }
+ break;
+ }
+ }
+
+ if (form['name'] && (Project.save_path || Project.export_path || Format.image_editor) && !Format['legacy_editable_file_name'])
+ delete form['name'];
+
+ form['uv_mode'] = {
+ label: 'dialog.project.default_uv_mode',
+ description: 'dialog.project.default_uv_mode.description',
+ type: 'select',
+ condition: Format.optional_box_uv,
+ options: {
+ face_uv: 'dialog.project.uv_mode.face_uv',
+ box_uv: 'dialog.project.uv_mode.box_uv',
+ },
+ value: Project.box_uv ? 'box_uv' : 'face_uv',
+ };
+
+ form['texture_size'] = {
+ label: 'dialog.project.texture_size',
+ type: 'vector',
+ dimensions: 2,
+ value: [Project.texture_width, Project.texture_height],
+ min: 1
+ };
+
+ return form;
+}
+
+/**
+ * Create the 'new project' popup dialogue for GeckoLib projects.
+ * project.js
"project_window" action declaration (Copyright Blockbench)
+ * Periodically check this is up-to-date with Blockbench to ensure ongoing compatibility
+ * @return false if the user clicks cancel
, otherwise true
+ */
+function createProjectSettingsDialog(Project: ModelProject, form: {[formElement: string]: '_' | DialogFormElement}) {
+ const dialog = new Dialog({
+ id: 'project',
+ title: 'dialog.project.title',
+ width: 500,
+ form,
+ onConfirm: function(formResult) {
+ let save;
+ const box_uv = formResult['uv_mode'] == 'box_uv';
+ const texture_width = Math.clamp(formResult['texture_size'][0], 1, Infinity);
+ const texture_height = Math.clamp(formResult['texture_size'][1], 1, Infinity);
+
+ if (Project.box_uv != box_uv || Project.texture_width != texture_width || Project.texture_height != texture_height) {
+ // Adjust UV Mapping if resolution changed
+ if (!Project.box_uv && !box_uv && !Format['per_texture_uv_size'] && (Project.texture_width != texture_width || Project.texture_height != texture_height)) {
+ save = Undo.initEdit({elements: [...Cube.all, ...Mesh.all], uv_only: true, uv_mode: true} as UndoAspects)
+
+ Cube.all.forEach(cube => {
+ for (const key in cube.faces) {
+ const uv = cube.faces[key].uv;
+ uv[0] *= texture_width / Project.texture_width;
+ uv[2] *= texture_width / Project.texture_width;
+ uv[1] *= texture_height / Project.texture_height;
+ uv[3] *= texture_height / Project.texture_height;
+ }
+ })
+ Mesh.all.forEach(mesh => {
+ for (const key in mesh.faces) {
+ const uv = mesh.faces[key].uv;
+
+ for (const vkey in uv) {
+ uv[vkey][0] *= texture_width / Project.texture_width;
+ uv[vkey][1] *= texture_height / Project.texture_height;
+ }
+ }
+ })
+ }
+ // Convert UV mode per element
+ if (Project.box_uv != box_uv && ((box_uv && !Cube.all.find(cube => cube['box_uv'])) || (!box_uv && !Cube.all.find(cube => !cube['box_uv'])))) {
+ if (!save)
+ save = Undo.initEdit({elements: Cube.all, uv_only: true, uv_mode: true} as UndoAspects);
+
+ Cube.all.forEach(cube => cube.setUVMode(box_uv));
+ }
+
+ if (!save)
+ save = Undo.initEdit({uv_mode: true});
+
+ Project.texture_width = texture_width;
+ Project.texture_height = texture_height;
+
+ if (Format.optional_box_uv)
+ Project.box_uv = box_uv;
+
+ Canvas.updateAllUVs();
+ updateSelection();
+ }
+
+ const properties = ModelProject['properties'];
+
+ for (const key in properties) {
+ properties[key].merge(Project, formResult);
+ }
+
+ Project.name = Project.name.trim();
+ Project.model_identifier = Project.model_identifier.trim();
+
+ if (save)
+ Undo.finishEdit('Change project UV settings');
+
+ Blockbench.dispatchEvent('update_project_settings', formResult);
+ BARS.updateConditions();
+
+ if (Project.EditSession) {
+ const metadata = {
+ texture_width: Project.texture_width,
+ texture_height: Project.texture_height,
+ box_uv: Project.box_uv
+ };
+
+ for (const key in properties) {
+ properties[key].copy(Project, metadata);
+ }
+
+ Project.EditSession.sendAll('change_project_meta', JSON.stringify(metadata));
+ }
+
+ const modelType = GeckoModelType[formResult[PROPERTY_MODEL_TYPE]]
+ Project[PROPERTY_MODEL_TYPE] = modelType;
+
+ if (modelType == GeckoModelType.ITEM)
+ Project.parent = 'builtin/entity';
+
+ if (Project.name === Format.name || Project.name === '')
+ Project.name = "GeckoLib " + Project[PROPERTY_MODEL_TYPE];
+
+ switch (modelType) {
+ case GeckoModelType.ARMOR:
+ if (Outliner.root.length === 0) {
+ Codecs.project.parse(armorTemplate, null);
+ }
+ else {
+ alert('Unable to generate Armor Template over an existing model. Please select Armor on a new or empty project to use this model type.')
+
+ return false;
+ }
+ break;
+ default:
+ break;
+ }
+
+ Format.display_mode = modelType === GeckoModelType.ITEM || settings[SETTING_ALWAYS_SHOW_DISPLAY].value;
+
+ dialog.hide();
+ },
+ onFormChange(formResult) {
+ try {
+ document.getElementById('model_identifier')['placeholder'] = getObjectIdPlaceholder(formResult)
+ }// eslint-disable-next-line no-empty
+ catch (ex) {}
+ },
+ })
+
+ dialog.show()
+
+ return true;
+}
+
+/**
+ * Export the item display json
+ * codec.ts#loadCodec
or in the plugin creation in index.ts
+ */
+export const addEventListener = (eventName: EventName, callback: (data: object) => void) => {
+ Blockbench.on(eventName, callback)
+}
+
+/**
+ * Remove a previously registered event listener from Blockbench's event callback system.
+ * codec.ts#loadCodec
or in the plugin creation in index.ts
+ */
+export const addCodecCallback = (codec: Codec, taskName: string, callback: (data: object) => void) => {
+ codec.on(taskName, callback);
+}
+
+/**
+ * Helper function that allows instantiation of an object and simultaneous property-modification without needing a local variable
+ */
+export function make