diff --git a/RAPPORT_CORRECTIONS.md b/RAPPORT_CORRECTIONS.md new file mode 100644 index 000000000..dd56cc33a --- /dev/null +++ b/RAPPORT_CORRECTIONS.md @@ -0,0 +1,67 @@ +# Rapport des corrections – Plugin PoseKey + +## 1. Undo (CTRL+Z) – `posekey.js` + +### Note originale +> Undo currently does not work. "purpose" is not a valid property. Instead, you need to init the edit with the empty list of keyframes, and then finish it with all the newly created keyframes. + +### Correction appliquée +- **Avant** : `Undo.initEdit({purpose: 'PoseKey Single'})` et `Undo.finishEdit()` sans paramètres. +- **Après** : `Undo.initEdit({animations: [anim]})` et `Undo.finishEdit('PoseKey', {animations: [anim]})`. + +L’aspect `animations` enregistre l’état complet de l’animation (y compris les keyframes) avant et après les modifications, ce qui permet à CTRL+Z de fonctionner correctement. + +--- + +## 2. Description du plugin – `posekey.js` + +### Note originale +> Could you please rephrase the description so that it is a bit clearer what this plugin does and what it can be used for? + +### Correction appliquée +- **Avant** : `"Poses a unique keyframe on the entire group."` +- **Après** : `"Adds a single keyframe (position, rotation, scale) at the current timeline position for all selected groups and their descendants. Useful for quickly setting key poses without affecting other keyframes."` + +La nouvelle description précise : +- ce que fait le plugin (ajout d’une keyframe P+R+S), +- où elle est placée (position actuelle de la timeline), +- sur quels éléments (groupes sélectionnés et descendants), +- et son utilité (poses rapides sans modifier les autres keyframes). + +--- + +## 3. Champ invalide dans `plugins.json` + +### Note originale +> This field does not exist + +### Champ concerné +Le champ **`file`** n’existe pas dans le schéma du registre de plugins BlockBench. Le fichier du plugin est déduit de l’ID du plugin (ex. `posekey` → `posekey.js`). + +### Correction à appliquer +Supprimer la ligne `"file": "posekey.js"` de l’entrée `posekey` dans `plugins.json`. + +### Entrée corrigée pour `plugins.json` + +```json +"posekey": { + "title": "PoseKey", + "author": "FroXaL", + "description": "Adds a single keyframe (position, rotation, scale) at the current timeline position for all selected groups and their descendants. Useful for quickly setting key poses without affecting other keyframes.", + "icon": "key", + "variant": "both", + "version": "3.0.0", + "min_version": "4.0.0", + "tags": ["animation", "keyframe", "workflow"] +} +``` + +--- + +## Résumé + +| Élément | Statut | +|--------|--------| +| Undo (CTRL+Z) | Corrigé dans `posekey.js` | +| Description | Corrigée dans `posekey.js` | +| Champ `file` dans plugins.json | À supprimer lors de la soumission | diff --git a/plugins.json b/plugins.json index 390451cf8..651cfd0dd 100644 --- a/plugins.json +++ b/plugins.json @@ -1391,5 +1391,16 @@ "website": "https://pastelito.dev", "has_changelog": true, "repository": "https://github.com/PasteDev/blockbench-plugins/tree/master/plugins/hytale_avatar_loader" + }, + "posekey": { + "title": "PoseKey", + "author": "FroXaL", + "description": "Poses a unique keyframe on the entire group.", + "icon": "key", + "variant": "both", + "version": "1.0.0", + "min_version": "4.0.0", + "tags": ["animation", "keyframe", "workflow"], + "file": "posekey.js" } } diff --git a/plugins/posekey.js b/plugins/posekey.js new file mode 100644 index 000000000..0f5445729 --- /dev/null +++ b/plugins/posekey.js @@ -0,0 +1,103 @@ +(function() { + let action_posekey; + + function getAllDescendants(groups) { + let all_groups = []; + function recurse(arr) { + arr.forEach(item => { + if (item.type === 'group') { + if (!all_groups.includes(item)) all_groups.push(item); + if (item.children && item.children.length > 0) recurse(item.children); + } + }); + } + recurse(groups); + return all_groups; + } + + BBPlugin.register('posekey', { + title: 'PoseKey', + author: 'FroXaL', + icon: 'key', + description: 'Poses a unique keyframe on the entire group.', + version: '3.0.0', + variant: 'both', + + onload() { + action_posekey = new Action('pose_key_action_id', { + name: 'PoseKey', + description: 'Unique P+R+S keyframe at cursor', + icon: 'key', + category: 'animation', + condition: () => Animator.open, + + click: function() { + let anim = Animator.animation; + if (!anim && Animation.selected) anim = Animation.selected; + + if (!anim) { + Blockbench.showQuickMessage("⚠️ No active animation!"); + return; + } + + let selection_base = []; + if (Group.selected.length > 0) selection_base.push(...Group.selected); + + if (Cube.selected.length > 0) { + Cube.selected.forEach(cube => { + if (cube.parent && cube.parent.type === 'group') { + if (!selection_base.includes(cube.parent)) selection_base.push(cube.parent); + } + }); + } + + if (selection_base.length === 0) { + Blockbench.showQuickMessage("⚠️ Select an object!"); + return; + } + + let final_targets = getAllDescendants(selection_base); + + Undo.initEdit({purpose: 'PoseKey Single'}); + + let currentTime = Timeline.time; + + final_targets.forEach(group => { + let uuid = group.uuid; + + let bone_animator = anim.animators[uuid]; + if (!bone_animator) { + bone_animator = new GeneralAnimator(uuid, anim); + bone_animator.init(); + anim.animators[uuid] = bone_animator; + } + + ['position', 'rotation', 'scale'].forEach(channel => { + + let channel_array = bone_animator[channel]; + let was_empty = channel_array.length === 0; + + bone_animator.getOrMakeKeyframe(channel, currentTime); + + if (was_empty && currentTime > 0 && channel_array.length > 1) { + let key_at_zero = channel_array.find(k => k.time === 0); + if (key_at_zero) { + key_at_zero.remove(); + } + } + }); + }); + + Undo.finishEdit(); + if (Animator.open) Animator.preview(); + + Blockbench.showQuickMessage(`🔑 Unique keyframe set on ${final_targets.length} groups!`); + } + }); + }, + + onunload() { + if (action_posekey) action_posekey.delete(); + } + }); +})(); \ No newline at end of file diff --git a/posekey.js b/posekey.js new file mode 100644 index 000000000..1fce479ff --- /dev/null +++ b/posekey.js @@ -0,0 +1,103 @@ +(function() { + let action_posekey; + + function getAllDescendants(groups) { + let all_groups = []; + function recurse(arr) { + arr.forEach(item => { + if (item.type === 'group') { + if (!all_groups.includes(item)) all_groups.push(item); + if (item.children && item.children.length > 0) recurse(item.children); + } + }); + } + recurse(groups); + return all_groups; + } + + BBPlugin.register('posekey', { + title: 'PoseKey', + author: 'FroXaL', + icon: 'key', + description: 'Adds a single keyframe (position, rotation, scale) at the current timeline position for all selected groups and their descendants. Useful for quickly setting key poses without affecting other keyframes.', + version: '3.0.0', + variant: 'both', + + onload() { + action_posekey = new Action('pose_key_action_id', { + name: 'PoseKey', + description: 'Unique P+R+S keyframe at cursor', + icon: 'key', + category: 'animation', + condition: () => Animator.open, + + click: function() { + let anim = Animator.animation; + if (!anim && Animation.selected) anim = Animation.selected; + + if (!anim) { + Blockbench.showQuickMessage("⚠️ No active animation!"); + return; + } + + let selection_base = []; + if (Group.selected.length > 0) selection_base.push(...Group.selected); + + if (Cube.selected.length > 0) { + Cube.selected.forEach(cube => { + if (cube.parent && cube.parent.type === 'group') { + if (!selection_base.includes(cube.parent)) selection_base.push(cube.parent); + } + }); + } + + if (selection_base.length === 0) { + Blockbench.showQuickMessage("⚠️ Select an object!"); + return; + } + + let final_targets = getAllDescendants(selection_base); + + Undo.initEdit({animations: [anim]}); + + let currentTime = Timeline.time; + + final_targets.forEach(group => { + let uuid = group.uuid; + + let bone_animator = anim.animators[uuid]; + if (!bone_animator) { + bone_animator = new GeneralAnimator(uuid, anim); + bone_animator.init(); + anim.animators[uuid] = bone_animator; + } + + ['position', 'rotation', 'scale'].forEach(channel => { + + let channel_array = bone_animator[channel]; + let was_empty = channel_array.length === 0; + + bone_animator.getOrMakeKeyframe(channel, currentTime); + + if (was_empty && currentTime > 0 && channel_array.length > 1) { + let key_at_zero = channel_array.find(k => k.time === 0); + if (key_at_zero) { + key_at_zero.remove(); + } + } + }); + }); + + Undo.finishEdit('PoseKey', {animations: [anim]}); + if (Animator.open) Animator.preview(); + + Blockbench.showQuickMessage(`🔑 Unique keyframe set on ${final_targets.length} groups!`); + } + }); + }, + + onunload() { + if (action_posekey) action_posekey.delete(); + } + }); +})();