diff --git a/plugins.json b/plugins.json
index bb4135a2..e7f0aec1 100644
--- a/plugins.json
+++ b/plugins.json
@@ -220,6 +220,17 @@
"repository": "https://github.com/ewanhowell5195/blockbenchPlugins/tree/main/cem_template_loader",
"bug_tracker": "https://github.com/ewanhowell5195/blockbenchPlugins/issues/new?title=[CEM Template Loader]"
},
+ "check_invalid_rotation": {
+ "title": "Invalid Rotation Checker",
+ "author": "AnnJ",
+ "description": "Detects non-standard rotations and warns before save/export.",
+ "icon": "warning",
+ "version": "1.0.0",
+ "min_version": "4.8.0",
+ "variant": "both",
+ "tags": ["Rotation", "Validation", "Export"],
+ "repository": "https://github.com/JannisX11/blockbench-plugins/tree/master/plugins/check_invalid_rotation"
+ },
"optifine_player_models": {
"title": "OptiFine Player Models",
"icon": "icon-player",
diff --git a/plugins/check_invalid_rotation/about.md b/plugins/check_invalid_rotation/about.md
new file mode 100644
index 00000000..956b7e11
--- /dev/null
+++ b/plugins/check_invalid_rotation/about.md
@@ -0,0 +1,39 @@
+# Minecraft Java Edition Rotation Checker
+
+Detects and fixes non-standard rotations for Minecraft Java Edition 1.8-1.18.x before they cause display issues in-game.
+
+
+
+## Features
+
+- **Automatic Detection**: Scans rotations during save and export operations
+- **Quick Fix**: Automatically fix all rotations or fix individually (nearest/furthest values)
+- **Multi-Language Support**: English and French translations
+
+## Minecraft Java Edition Standards
+
+Minecraft Java Edition 1.8-1.18.x supports rotations in 22.5° increments: **-45°, -22.5°, 0°, 22.5°, 45°**
+
+## Supported Operations
+
+The plugin automatically checks rotations before any save or export operation:
+
+### Save Operations
+- **Keyboard Shortcut**: Ctrl+Alt+S
+- **Menu**: File > Save Model
+- **Toolbar**: Save button
+
+### Save As Operations
+- **Keyboard Shortcut**: Ctrl+Shift+S
+- **Menu**: File > Save As
+- **Toolbar**: Save As button
+
+### Increment Save Operations
+- **Menu**: File > Increment Save
+- **Toolbar**: Increment Save button
+
+### Export Operations
+- **Java Block Export**: File > Export > Java Block (.java)
+- **Bedrock Export**: File > Export > Bedrock (.json)
+
+**Note**: Minecraft 1.19+ may have relaxed these restrictions. This plugin targets older versions where rotation restrictions are still enforced.
diff --git a/plugins/check_invalid_rotation/changelog.json b/plugins/check_invalid_rotation/changelog.json
new file mode 100644
index 00000000..a43b38ae
--- /dev/null
+++ b/plugins/check_invalid_rotation/changelog.json
@@ -0,0 +1,15 @@
+{
+ "1.0.0": {
+ "title": "1.0.0",
+ "author": "AnnJ",
+ "date": "2025-01-08",
+ "categories": [
+ {
+ "title": "Initial Release",
+ "list": [
+ "Created Plugin"
+ ]
+ }
+ ]
+ }
+}
diff --git a/plugins/check_invalid_rotation/check_invalid_rotation.js b/plugins/check_invalid_rotation/check_invalid_rotation.js
new file mode 100644
index 00000000..d8191e2d
--- /dev/null
+++ b/plugins/check_invalid_rotation/check_invalid_rotation.js
@@ -0,0 +1,1149 @@
+(function() {
+ 'use strict';
+
+ const translations = {
+ en: {
+ title: 'Invalid Rotation Checker',
+ description: 'Detects non-standard rotations and warns before save/export.',
+ about: `Minecraft Java Edition Rotation Checker
+
+For Minecraft Java Edition 1.8 - 1.18.x (versions with rotation restrictions)
+
+
What It Does
+Comprehensive rotation validation that protects your models from compatibility issues. Works with ALL save and export operations to catch non-standard rotations before they cause display problems in Minecraft.
+
+How It Works
+1. Universal Protection: Scans rotations during save (Ctrl+Alt+S), save as (Ctrl+Shift+Alt+S), and export operations
+2. Smart Detection: Identifies rotations that don't follow the 22.5° standard
+3. Context-Aware Warnings: Shows operation-specific dialogs with appropriate button text
+4. Quick Fix Options: Automatically fix rotations or choose to fix individually with nearest/furthest options
+5. User Control: Continue the operation or cancel to fix rotations first
+`,
+ dialogTitle: 'Non-Standard Rotations Detected',
+ warningMessage: 'non-standard rotation(s) found',
+ warningDescription: 'These rotations may cause display issues in Minecraft. Consider fixing them before saving/exporting.',
+ rotationIssues: 'Rotation Issues:',
+ quickFix: 'Quick Fix',
+ quickFixDescription: 'Automatically fix all rotations to their nearest standard values.',
+ fixAll: 'Fix All',
+ fix: 'Fix',
+ fixed: 'Fixed ✓',
+ rotationsFixed: 'rotations fixed ✓',
+ cancelExport: 'Cancel Export',
+ continueExport: 'Continue Export',
+ chooseFixStrategy: 'Choose Fix Strategy',
+ useClosest: 'Use Closest',
+ useFurthest: 'Use Furthest',
+ useClosestDescription: 'Fix each rotation to its nearest standard value',
+ useFurthestDescription: 'Fix each rotation to its second nearest standard value',
+ fixAllDescription: 'This affects all rotations that have multiple options.',
+ rotationsWillBeCorrected: 'rotation(s) will be corrected',
+ clickOptionThenButtons: 'Click on an option above, then use the buttons below',
+ fixAllSuccess: 'All rotations have been fixed!',
+ },
+ fr: {
+ title: 'Vérificateur de Rotations',
+ description: 'Détecte les rotations non-standard et avertit avant sauvegarde/export.',
+ rotationIssues: 'Problèmes de Rotation :',
+ quickFix: 'Correction Rapide',
+ continueExport: 'Continuer l\'Export',
+ cancel: 'Annuler',
+ fixAll: 'Tout Corriger',
+ fix: 'Corriger',
+ fixed: 'Corrigé ✓',
+ rotationsFixed: 'rotations corrigées ✓',
+ chooseFixStrategy: 'Choisir la Stratégie de Correction',
+ useClosest: 'Utiliser le Plus Proche',
+ useFurthest: 'Utiliser le Plus Éloigné',
+ useClosestDescription: 'Corrige chaque rotation vers sa valeur standard la plus proche',
+ useFurthestDescription: 'Corrige chaque rotation vers sa deuxième valeur standard la plus proche',
+ fixAllDescription: 'Ceci affecte toutes les rotations qui ont plusieurs options.',
+ rotationsWillBeCorrected: 'rotation(s) sera(ont) corrigée(s)',
+ clickOptionThenButtons: 'Cliquez sur une option ci-dessus, puis utilisez les boutons ci-dessous',
+ fixAllSuccess: 'Toutes les rotations ont été corrigées !',
+ about: `Vérificateur de Rotations Minecraft Java Edition
+
+Pour Minecraft Java Edition 1.8 - 1.18.x (versions avec restrictions de rotation)
+
+Fonctionnalités
+Validation complète des rotations qui protège vos modèles des problèmes de compatibilité. Fonctionne avec TOUTES les opérations de sauvegarde et d'export pour détecter les rotations non-standard avant qu'elles ne causent des problèmes d'affichage dans Minecraft.
+
+Comment Ça Marche
+1. Protection Universelle : Analyse les rotations lors de la sauvegarde (Ctrl+Alt+S), enregistrer sous (Ctrl+Shift+Alt+S), et les opérations d'export
+2. Détection Intelligente : Identifie les rotations qui ne suivent pas la norme 22,5°
+3. Avertissements Contextuels : Affiche des dialogues spécifiques à l'opération avec le texte de bouton approprié
+4. Options de Correction Rapide : Corrige automatiquement les rotations ou choisissez de les corriger individuellement avec options plus proche/plus éloigné
+5. Contrôle Utilisateur : Continuer l'opération ou annuler pour corriger les rotations d'abord
+`,
+ dialogTitle: 'Rotations Non-Standard Détectées',
+ warningMessage: 'rotation(s) non-standard trouvée(s)',
+ warningDescription: 'Ces rotations peuvent causer des problèmes d\'affichage dans Minecraft. Considérez de les corriger avant la sauvegarde/export.',
+ rotationIssues: 'Problèmes de Rotation :',
+ quickFix: 'Correction Rapide',
+ quickFixDescription: 'Corriger automatiquement toutes les rotations vers leurs valeurs standard les plus proches.',
+ fixAll: 'Tout Corriger',
+ fix: 'Corriger',
+ fixed: 'Corrigé ✓',
+ rotationsFixed: 'Rotations corrigées ✓',
+ cancelExport: 'Annuler l\'Export',
+ continueExport: 'Continuer l\'Export',
+ }
+ };
+
+ function detectLanguage() {
+ if (typeof Settings !== 'undefined' && Settings.language) {
+ return Settings.language.startsWith('fr') ? 'fr' : 'en';
+ }
+
+ const docLang = document.documentElement.lang || navigator.language;
+ if (docLang && docLang.startsWith('fr')) {
+ return 'fr';
+ }
+
+ return 'en';
+ }
+
+ function t(key) {
+ const lang = detectLanguage();
+ return translations[lang][key] || translations.en[key] || key;
+ }
+
+ Plugin.register('check_invalid_rotation', {
+ title: t('title'),
+ author: 'AnnJ',
+ description: t('description'),
+ icon: 'warning',
+ version: '1.0.0',
+ variant: 'both',
+ tags: ['Rotation', 'Validation', 'Export'],
+ about: t('about'),
+ onload() {
+ initialize();
+ setTimeout(() => {
+ addSaveIntegration();
+ }, 3000);
+ },
+ onunload() {
+ if (clickHandler) {
+ document.removeEventListener('click', clickHandler, true);
+ clickHandler = null;
+ }
+
+ if (window.rotationCheckerOriginalFunctions && typeof Codecs !== 'undefined') {
+ if (window.rotationCheckerOriginalFunctions.java_block && Codecs.java_block) {
+ Codecs.java_block.export = window.rotationCheckerOriginalFunctions.java_block;
+ }
+ if (window.rotationCheckerOriginalFunctions.bedrock && Codecs.bedrock) {
+ Codecs.bedrock.export = window.rotationCheckerOriginalFunctions.bedrock;
+ }
+ delete window.rotationCheckerOriginalFunctions;
+ }
+
+ if (window.rotationCheckerOriginalSaveFunctions) {
+ if (MenuBar && MenuBar.actions) {
+ const saveAction = MenuBar.actions.find(action => action.id === 'file.save');
+ const saveAsAction = MenuBar.actions.find(action => action.id === 'file.save_as');
+
+ if (saveAction && window.rotationCheckerOriginalSaveFunctions.save) {
+ saveAction.click = window.rotationCheckerOriginalSaveFunctions.save;
+ }
+ if (saveAsAction && window.rotationCheckerOriginalSaveFunctions.saveAs) {
+ saveAsAction.click = window.rotationCheckerOriginalSaveFunctions.saveAs;
+ }
+ }
+
+ delete window.rotationCheckerOriginalSaveFunctions;
+ }
+ }
+ });
+
+ const STANDARD_ROTATIONS = [-45, -22.5, 0, 22.5, 45];
+ const ROTATION_TOLERANCE = 0.1;
+
+ function isStandardRotation(rotation) {
+ let normalizedRotation = ((rotation % 360) + 360) % 360;
+
+ let isStandard = STANDARD_ROTATIONS.some(standard =>
+ Math.abs(normalizedRotation - standard) <= ROTATION_TOLERANCE
+ );
+
+ if (!isStandard) {
+ isStandard = STANDARD_ROTATIONS.some(standard =>
+ Math.abs(rotation - standard) <= ROTATION_TOLERANCE
+ );
+ }
+
+ return isStandard;
+ }
+
+ function getNearestStandard(rotation) {
+ let nearest = 0;
+ let minDiff = Infinity;
+
+ STANDARD_ROTATIONS.forEach(standard => {
+ let diff = Math.abs(rotation - standard);
+
+ if (diff > 180) {
+ diff = 360 - diff;
+ }
+
+ if (diff < minDiff) {
+ minDiff = diff;
+ nearest = standard;
+ }
+ });
+
+ return nearest;
+ }
+
+ function getNearestStandards(rotation, count = 2) {
+ const distances = STANDARD_ROTATIONS.map(standard => {
+ let diff = Math.abs(rotation - standard);
+ if (diff > 180) {
+ diff = 360 - diff;
+ }
+ return { standard, distance: diff };
+ });
+
+ distances.sort((a, b) => a.distance - b.distance);
+ return distances.slice(0, count).map(item => item.standard);
+ }
+
+ function extractRotations(modelData) {
+ const nonStandardRotations = [];
+ const processedRotations = new Set();
+
+ function checkElement(element, path = '') {
+ if (!element) return;
+
+ const rotationProps = ['rotation', 'rot', 'rotation_x', 'rotation_y', 'rotation_z', 'rx', 'ry', 'rz'];
+
+ rotationProps.forEach(prop => {
+ if (element[prop] !== undefined) {
+ const rotationValue = element[prop];
+
+ if (Array.isArray(rotationValue)) {
+ rotationValue.forEach((rotation, index) => {
+ const rotationNum = parseFloat(rotation);
+ if (!isNaN(rotationNum) && !isStandardRotation(rotationNum)) {
+ const axis = ['X', 'Y', 'Z'][index] || index;
+ const rotationKey = `${path}:${prop}[${axis}]:${rotationNum}:${element.uuid || 'no-uuid'}`;
+
+ if (!processedRotations.has(rotationKey)) {
+ processedRotations.add(rotationKey);
+ nonStandardRotations.push({
+ path: path || 'root',
+ property: `${prop}[${axis}]`,
+ value: rotationNum,
+ element: element
+ });
+ }
+ }
+ });
+ } else {
+ const rotation = parseFloat(rotationValue);
+ if (!isNaN(rotation) && !isStandardRotation(rotation)) {
+ const rotationKey = `${path}:${prop}:${rotation}:${element.uuid || 'no-uuid'}`;
+
+ if (!processedRotations.has(rotationKey)) {
+ processedRotations.add(rotationKey);
+ nonStandardRotations.push({
+ path: path || 'root',
+ property: prop,
+ value: rotation,
+ element: element
+ });
+ }
+ }
+ }
+ }
+ });
+
+ if (element.children) {
+ element.children.forEach((child, index) => {
+ checkElement(child, `${path}.children[${index}]`);
+ });
+ }
+
+ if (element.elements) {
+ element.elements.forEach((elem, index) => {
+ checkElement(elem, `${path}.elements[${index}]`);
+ });
+ }
+ }
+
+ const processedElements = new Set();
+
+ if (modelData.elements) {
+ modelData.elements.forEach((element, index) => {
+ if (!processedElements.has(element)) {
+ processedElements.add(element);
+ checkElement(element, `elements[${index}]`);
+ }
+ });
+ }
+
+ if (modelData.bones) {
+ modelData.bones.forEach((bone, index) => {
+ if (!processedElements.has(bone)) {
+ processedElements.add(bone);
+ checkElement(bone, `bones[${index}]`);
+ }
+ });
+ }
+
+ if (modelData.children) {
+ modelData.children.forEach((child, index) => {
+ if (!processedElements.has(child)) {
+ processedElements.add(child);
+ checkElement(child, `children[${index}]`);
+ }
+ });
+ }
+
+ const rootProperties = Object.keys(modelData).filter(key =>
+ !['elements', 'bones', 'children'].includes(key)
+ );
+
+ if (rootProperties.length > 0 && !processedElements.has(modelData)) {
+ let hasProcessedChildren = false;
+ if (modelData.elements) {
+ hasProcessedChildren = modelData.elements.some(element => processedElements.has(element));
+ }
+ if (modelData.bones) {
+ hasProcessedChildren = hasProcessedChildren || modelData.bones.some(bone => processedElements.has(bone));
+ }
+ if (modelData.children) {
+ hasProcessedChildren = hasProcessedChildren || modelData.children.some(child => processedElements.has(child));
+ }
+
+ if (!hasProcessedChildren) {
+ checkElement(modelData, 'root');
+ }
+ }
+
+ return nonStandardRotations;
+ }
+
+ function applyRotationFix(rotation) {
+ const standardValue = getNearestStandard(rotation.value);
+ return applyRotationFixWithValue(rotation, standardValue);
+ }
+
+ function applyRotationFixWithValue(rotation, standardValue) {
+ const pathParts = rotation.property.match(/^(.+?)\[([XYZ])\]$/);
+ if (pathParts) {
+ const baseProperty = pathParts[1];
+ const axis = pathParts[2];
+ const axisIndex = ['X', 'Y', 'Z'].indexOf(axis);
+
+ if (axisIndex !== -1 && rotation.element[baseProperty] && Array.isArray(rotation.element[baseProperty])) {
+ rotation.element[baseProperty][axisIndex] = standardValue;
+
+ if (rotation.element.uuid && Outliner && Outliner.elements) {
+ const sceneElement = Outliner.elements.find(el => el.uuid === rotation.element.uuid);
+ if (sceneElement && sceneElement.rotation) {
+ sceneElement.rotation[axisIndex] = standardValue;
+ }
+ }
+
+ if (Project && Project.geometry && Project.geometry.elements) {
+ const projectElement = Project.geometry.elements.find(el => el.uuid === rotation.element.uuid);
+ if (projectElement && projectElement[baseProperty]) {
+ projectElement[baseProperty][axisIndex] = standardValue;
+ }
+ }
+
+ return true;
+ }
+ } else {
+ if (rotation.element[rotation.property] !== undefined) {
+ rotation.element[rotation.property] = standardValue;
+
+ if (rotation.element.uuid && Outliner && Outliner.elements) {
+ const sceneElement = Outliner.elements.find(el => el.uuid === rotation.element.uuid);
+ if (sceneElement) {
+ sceneElement[rotation.property] = standardValue;
+ }
+ }
+
+ if (Project && Project.geometry && Project.geometry.elements) {
+ const projectElement = Project.geometry.elements.find(el => el.uuid === rotation.element.uuid);
+ if (projectElement) {
+ projectElement[rotation.property] = standardValue;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ function applyAllRotationFixes(rotations, optionIndex = 0) {
+ let fixedCount = 0;
+ const fixedRotations = new Set();
+
+ rotations.forEach(rotation => {
+ const rotationKey = `${rotation.path}:${rotation.property}:${rotation.value}`;
+
+ if (!fixedRotations.has(rotationKey)) {
+ const nearestStandards = getNearestStandards(rotation.value, 2);
+ const selectedStandard = nearestStandards[optionIndex] || nearestStandards[0];
+
+ if (applyRotationFixWithValue(rotation, selectedStandard)) {
+ fixedCount++;
+ fixedRotations.add(rotationKey);
+ }
+ }
+ });
+
+ if (fixedCount > 0) {
+ refreshScene();
+ }
+
+ return fixedCount;
+ }
+
+ function refreshScene() {
+ if (typeof Canvas !== 'undefined' && Canvas.updateAll) {
+ Canvas.updateAll();
+ }
+ if (typeof Outliner !== 'undefined' && Outliner.update) {
+ Outliner.update();
+ }
+ if (typeof Interface !== 'undefined' && Interface.update) {
+ Interface.update();
+ }
+ }
+
+ function showRotationWarningDialog(rotations, onConfirm, onCancel, context = 'export') {
+ const getButtonText = (buttonType) => {
+ const lang = detectLanguage();
+ const translations = {
+ en: {
+ export: {
+ fixAll: 'Fix All',
+ cancel: 'Cancel Export',
+ continue: 'Continue Export'
+ },
+ save: {
+ fixAll: 'Fix All',
+ cancel: 'Cancel Save',
+ continue: 'Continue Save'
+ },
+ saveAs: {
+ fixAll: 'Fix All',
+ cancel: 'Cancel Save As',
+ continue: 'Continue Save As'
+ },
+ saveIncrement: {
+ fixAll: 'Fix All',
+ cancel: 'Cancel Save with Increment',
+ continue: 'Continue Save with Increment'
+ }
+ },
+ fr: {
+ export: {
+ fixAll: 'Tout Corriger',
+ cancel: 'Annuler l\'Export',
+ continue: 'Continuer l\'Export'
+ },
+ save: {
+ fixAll: 'Tout Corriger',
+ cancel: 'Annuler la Sauvegarde',
+ continue: 'Continuer la Sauvegarde'
+ },
+ saveAs: {
+ fixAll: 'Tout Corriger',
+ cancel: 'Annuler Enregistrer Sous',
+ continue: 'Continuer Enregistrer Sous'
+ },
+ saveIncrement: {
+ fixAll: 'Tout Corriger',
+ cancel: 'Annuler Enregistrer avec Incrément',
+ continue: 'Continuer Enregistrer avec Incrément'
+ }
+ }
+ };
+ return translations[lang]?.[context]?.[buttonType] || translations.en[context]?.[buttonType] || buttonType;
+ };
+
+ let rotationListHTML = '';
+ rotations.forEach((rotation, index) => {
+ const elementName = rotation.element.name || rotation.path || 'Unknown Element';
+ const propertyName = rotation.property;
+ const currentValue = rotation.value;
+ const nearestStandards = getNearestStandards(rotation.value, 2);
+
+ const optionsHTML = nearestStandards.map((standard, optionIndex) => {
+ const isClosest = optionIndex === 0;
+ const buttonStyle = isClosest ?
+ 'background: #4caf50; color: white;' :
+ 'background: #2196f3; color: white;';
+ const buttonText = isClosest ?
+ `${t('fix')} → ${standard}°` :
+ `${standard}°`;
+
+ return `
+
+ ${buttonText}
+
+ `;
+ }).join('');
+
+ rotationListHTML += `
+
+
+
+
${elementName}
+
+ ${propertyName}: ${currentValue}°
+
+
+
+ ${optionsHTML}
+
+
+
+ Choose: ${nearestStandards.map(s => `${s}°`).join(' or ')}
+
+
+ `;
+ });
+
+ const dialog = new Dialog('rotation_warning_dialog', {
+ title: t('dialogTitle'),
+ width: 600,
+ buttons: [getButtonText('fixAll'), getButtonText('cancel'), getButtonText('continue')],
+ cancelIndex: 1,
+ confirmIndex: 2,
+ component: {
+ data() {
+ return {
+ rotations: rotations,
+ fixedCount: 0,
+ isFixed: false
+ };
+ },
+ template: `
+
+
+
+ error_outline
+ ${rotations.length} ${t('warningMessage')}
+
+
+ ${t('warningDescription')}
+
+
+
+
+ check_circle
+ {{ fixedCount }} ${t('rotationsFixed')}
+
+
+ All rotations have been fixed to standard values. You can now export safely or cancel to review the changes.
+
+
+
+
+ auto_fix_high
+ ${t('quickFix')}
+
+
+ ${t('quickFixDescription')}
+
+
+
${t('rotationIssues')}
+
+ ${rotationListHTML}
+
+
+ `
+ }
+ });
+
+ dialog.onButton = function(index) {
+ if (index === 0) {
+ showFixAllChoiceDialog(rotations, dialog);
+ return false;
+ } else if (index === 1) {
+ onCancel();
+ return true;
+ } else if (index === 2) {
+ onConfirm();
+ return true;
+ }
+ return true;
+ };
+
+ const setupEventDelegation = () => {
+ const dialogElement = document.querySelector('.dialog[data-dialog="rotation_warning_dialog"]') ||
+ document.querySelector('#rotation_warning_dialog') ||
+ document.querySelector('.dialog');
+
+ if (dialogElement) {
+ dialogElement.addEventListener('click', (event) => {
+ const target = event.target;
+ if (target && target.id && target.id.startsWith('fix_btn_')) {
+ const idParts = target.id.replace('fix_btn_', '').split('_');
+ const rotationIndex = parseInt(idParts[0]);
+ const optionIndex = parseInt(idParts[1]);
+ const rotation = rotations[rotationIndex];
+
+ if (rotation) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const nearestStandards = getNearestStandards(rotation.value, 2);
+ const selectedStandard = nearestStandards[optionIndex];
+
+ if (applyRotationFixWithValue(rotation, selectedStandard)) {
+ const allButtons = dialogElement.querySelectorAll(`[id^="fix_btn_${rotationIndex}_"]`);
+ allButtons.forEach(btn => {
+ btn.innerHTML = t('fixed');
+ btn.style.background = '#4caf50';
+ btn.disabled = true;
+ });
+ setTimeout(() => {
+ refreshScene();
+ }, 100);
+ }
+ }
+ }
+ });
+ } else {
+ setTimeout(setupEventDelegation, 50);
+ }
+ };
+
+ setupEventDelegation();
+ dialog.show();
+ }
+
+ function showFixAllChoiceDialog(rotations, parentDialog) {
+ const choiceDialog = new Dialog('fix_all_choice_dialog', {
+ title: t('chooseFixStrategy'),
+ width: 500,
+ height: 300,
+ buttons: [t('fixAll'), t('cancel')],
+ cancelIndex: 1,
+ component: {
+ template: `
+
+
+
+ ${t('chooseFixStrategy')}
+
+
+ ${t('fixAllDescription')}
+
+
+
+
+
+
+
+ ✓
+
+
+ ${t('useClosest')}
+
+
+
+ ${t('useClosestDescription')}
+
+
+ Ex: 23° → 22.5°
+
+
+
+
+
+
+ →
+
+
+ ${t('useFurthest')}
+
+
+
+ ${t('useFurthestDescription')}
+
+
+ Ex: 23° → 45°
+
+
+
+
+
+
+ ${rotations.length} ${t('rotationsWillBeCorrected')}
+
+
+ ${t('clickOptionThenButtons')}
+
+
+
+ `,
+ data() {
+ return {
+ selectedStrategy: 'closest'
+ };
+ },
+ methods: {
+ selectStrategy(strategy) {
+ this.selectedStrategy = strategy;
+ }
+ }
+ },
+ onButton(index) {
+ if (index === 0) {
+ const selectedStrategy = choiceDialog.component.data.selectedStrategy;
+ const strategyIndex = selectedStrategy === 'closest' ? 0 : 1;
+ const fixedCount = applyAllRotationFixes(rotations, strategyIndex);
+ if (fixedCount > 0) {
+ refreshScene();
+ parentDialog.component.data.fixedCount = fixedCount;
+ parentDialog.component.data.isFixed = true;
+
+ rotations.forEach((rotation, rotationIndex) => {
+ const allButtons = document.querySelectorAll(`[id^="fix_btn_${rotationIndex}_"]`);
+ allButtons.forEach(btn => {
+ btn.innerHTML = t('fixed');
+ btn.style.background = '#4caf50';
+ btn.disabled = true;
+ });
+ });
+
+ Blockbench.showQuickMessage(t('fixAllSuccess'), 2000);
+ if (typeof Interface !== 'undefined' && Interface.update) {
+ Interface.update();
+ }
+ }
+ return true;
+ } else {
+ return true;
+ }
+ }
+ });
+
+ choiceDialog.show();
+ }
+
+ let clickHandler = null;
+
+ function initialize() {
+ if (typeof Codecs !== 'undefined') {
+ const originalExportFunctions = {};
+
+ if (Codecs.java_block && Codecs.java_block.export) {
+ originalExportFunctions.java_block = Codecs.java_block.export;
+ Codecs.java_block.export = function() {
+ const nonStandardRotations = extractRotations(Project);
+ if (nonStandardRotations.length > 0) {
+ showRotationWarningDialog(
+ nonStandardRotations,
+ () => originalExportFunctions.java_block.call(this),
+ () => {},
+ 'export'
+ );
+ } else {
+ originalExportFunctions.java_block.call(this);
+ }
+ };
+ }
+
+ if (Codecs.bedrock && Codecs.bedrock.export) {
+ originalExportFunctions.bedrock = Codecs.bedrock.export;
+ Codecs.bedrock.export = function() {
+ const nonStandardRotations = extractRotations(Project);
+ if (nonStandardRotations.length > 0) {
+ showRotationWarningDialog(
+ nonStandardRotations,
+ () => originalExportFunctions.bedrock.call(this),
+ () => {},
+ 'export'
+ );
+ } else {
+ originalExportFunctions.bedrock.call(this);
+ }
+ };
+ }
+
+ window.rotationCheckerOriginalFunctions = originalExportFunctions;
+ }
+ }
+
+ function addSaveIntegration() {
+ if (!window.rotationCheckerOriginalSaveFunctions) {
+ window.rotationCheckerOriginalSaveFunctions = {};
+ }
+
+ if (typeof Blockbench !== 'undefined') {
+ if (Blockbench.save && !window.rotationCheckerOriginalSaveFunctions.blockbenchSave) {
+ window.rotationCheckerOriginalSaveFunctions.blockbenchSave = Blockbench.save;
+ Blockbench.save = function() {
+ const nonStandardRotations = extractRotations(Project);
+ if (nonStandardRotations.length > 0) {
+ showRotationWarningDialog(
+ nonStandardRotations,
+ () => {
+ window.rotationCheckerOriginalSaveFunctions.blockbenchSave();
+ },
+ () => {},
+ 'save'
+ );
+ } else {
+ window.rotationCheckerOriginalSaveFunctions.blockbenchSave();
+ }
+ };
+ }
+
+ if (Blockbench.saveAs && !window.rotationCheckerOriginalSaveFunctions.blockbenchSaveAs) {
+ window.rotationCheckerOriginalSaveFunctions.blockbenchSaveAs = Blockbench.saveAs;
+ Blockbench.saveAs = function() {
+ const nonStandardRotations = extractRotations(Project);
+ if (nonStandardRotations.length > 0) {
+ showRotationWarningDialog(
+ nonStandardRotations,
+ () => {
+ window.rotationCheckerOriginalSaveFunctions.blockbenchSaveAs();
+ },
+ () => {},
+ 'saveAs'
+ );
+ } else {
+ window.rotationCheckerOriginalSaveFunctions.blockbenchSaveAs();
+ }
+ };
+ }
+
+ if (Blockbench.saveIncrement && !window.rotationCheckerOriginalSaveFunctions.blockbenchSaveIncrement) {
+ window.rotationCheckerOriginalSaveFunctions.blockbenchSaveIncrement = Blockbench.saveIncrement;
+ Blockbench.saveIncrement = function() {
+ const nonStandardRotations = extractRotations(Project);
+ if (nonStandardRotations.length > 0) {
+ showRotationWarningDialog(
+ nonStandardRotations,
+ () => {
+ window.rotationCheckerOriginalSaveFunctions.blockbenchSaveIncrement();
+ },
+ () => {},
+ 'saveIncrement'
+ );
+ } else {
+ window.rotationCheckerOriginalSaveFunctions.blockbenchSaveIncrement();
+ }
+ };
+ }
+ }
+
+ if (MenuBar && MenuBar.menus && MenuBar.menus.file && MenuBar.menus.file.structure) {
+ let saveItem = MenuBar.menus.file.structure.find(item =>
+ (typeof item === 'string' && item === 'save_project') ||
+ (typeof item === 'object' && item && item.id === 'save_project')
+ );
+ let saveAsItem = MenuBar.menus.file.structure.find(item =>
+ (typeof item === 'string' && item === 'save_project_as') ||
+ (typeof item === 'object' && item && item.id === 'save_project_as')
+ );
+ let saveIncrementItem = MenuBar.menus.file.structure.find(item =>
+ (typeof item === 'string' && item === 'save_project_incremental') ||
+ (typeof item === 'object' && item && item.id === 'save_project_incremental')
+ );
+
+ if (!saveItem || !saveAsItem || !saveIncrementItem) {
+ MenuBar.menus.file.structure.forEach(item => {
+ if (item && item.children && Array.isArray(item.children)) {
+ item.children.forEach(child => {
+ if (child && child.id) {
+ if (child.id === 'save_project' && !saveItem) {
+ saveItem = child;
+ }
+ if (child.id === 'save_project_as' && !saveAsItem) {
+ saveAsItem = child;
+ }
+ if (child.id === 'save_project_incremental' && !saveIncrementItem) {
+ saveIncrementItem = child;
+ }
+ }
+ });
+ }
+ });
+ }
+
+ if (!saveItem || !saveAsItem || !saveIncrementItem) {
+ const allItems = [];
+
+ MenuBar.menus.file.structure.forEach(item => {
+ if (item && typeof item === 'object' && item.id) {
+ allItems.push(item);
+ if (item.children && Array.isArray(item.children)) {
+ item.children.forEach(child => {
+ if (child && typeof child === 'object' && child.id) {
+ allItems.push(child);
+ }
+ });
+ }
+ }
+ });
+
+ saveItem = allItems.find(item => item.id === 'save_project' || item.id === 'save' || item.id.includes('save_project'));
+ saveAsItem = allItems.find(item => item.id === 'save_project_as' || item.id === 'save_as' || item.id.includes('save_project_as'));
+ saveIncrementItem = allItems.find(item => item.id === 'save_project_incremental' || item.id === 'save_incremental' || item.id.includes('save_project_incremental'));
+ }
+
+ if (saveItem && typeof saveItem === 'string') {
+ if (MenuBar.actions) {
+ saveItem = MenuBar.actions.find(action => action.id === saveItem);
+ } else {
+ saveItem = MenuBar.menus.file.structure.find(item =>
+ typeof item === 'object' && item && item.id === saveItem
+ );
+ }
+ }
+ if (saveAsItem && typeof saveAsItem === 'string') {
+ if (MenuBar.actions) {
+ saveAsItem = MenuBar.actions.find(action => action.id === saveAsItem);
+ } else {
+ saveAsItem = MenuBar.menus.file.structure.find(item =>
+ typeof item === 'object' && item && item.id === saveAsItem
+ );
+ }
+ }
+ if (saveIncrementItem && typeof saveIncrementItem === 'string') {
+ if (MenuBar.actions) {
+ saveIncrementItem = MenuBar.actions.find(action => action.id === saveIncrementItem);
+ } else {
+ saveIncrementItem = MenuBar.menus.file.structure.find(item =>
+ typeof item === 'object' && item && item.id === saveIncrementItem
+ );
+ }
+ }
+
+ if (saveItem && typeof saveItem === 'object' && saveItem.click && !window.rotationCheckerOriginalSaveFunctions.save) {
+ window.rotationCheckerOriginalSaveFunctions.save = saveItem.click;
+ saveItem.click = function() {
+ checkRotationsBeforeSave(window.rotationCheckerOriginalSaveFunctions.save, 'save');
+ };
+ }
+
+ if (saveAsItem && typeof saveAsItem === 'object' && saveAsItem.click && !window.rotationCheckerOriginalSaveFunctions.saveAs) {
+ window.rotationCheckerOriginalSaveFunctions.saveAs = saveAsItem.click;
+ saveAsItem.click = function() {
+ const nonStandardRotations = extractRotations(Project);
+ if (nonStandardRotations.length > 0) {
+ showRotationWarningDialog(
+ nonStandardRotations,
+ () => {
+ window.rotationCheckerOriginalSaveFunctions.saveAs();
+ },
+ () => {},
+ 'saveAs'
+ );
+ } else {
+ window.rotationCheckerOriginalSaveFunctions.saveAs();
+ }
+ };
+ }
+
+ if (saveIncrementItem && typeof saveIncrementItem === 'object' && saveIncrementItem.click && !window.rotationCheckerOriginalSaveFunctions.saveIncrement) {
+ window.rotationCheckerOriginalSaveFunctions.saveIncrement = saveIncrementItem.click;
+ saveIncrementItem.click = function() {
+ checkRotationsBeforeSave(window.rotationCheckerOriginalSaveFunctions.saveIncrement, 'saveIncrement');
+ };
+ }
+ }
+
+ try {
+ if (typeof BarItems !== 'undefined') {
+ if (BarItems.save_project_as && typeof BarItems.save_project_as.click === 'function' && !window.rotationCheckerOriginalSaveFunctions.barSaveAs) {
+ window.rotationCheckerOriginalSaveFunctions.barSaveAs = BarItems.save_project_as.click;
+ BarItems.save_project_as.click = function() {
+ const nonStandardRotations = extractRotations(Project);
+ if (nonStandardRotations.length > 0) {
+ showRotationWarningDialog(
+ nonStandardRotations,
+ () => {
+ Promise.resolve().then(() => requestAnimationFrame(() => requestAnimationFrame(() => {
+ window.rotationCheckerOriginalSaveFunctions.barSaveAs.call(this);
+ })));
+ },
+ () => {},
+ 'saveAs'
+ );
+ } else {
+ window.rotationCheckerOriginalSaveFunctions.barSaveAs.call(this);
+ }
+ };
+ }
+ if (BarItems.save_project && typeof BarItems.save_project.click === 'function' && !window.rotationCheckerOriginalSaveFunctions.barSave) {
+ window.rotationCheckerOriginalSaveFunctions.barSave = BarItems.save_project.click;
+ BarItems.save_project.click = function() {
+ checkRotationsBeforeSave(window.rotationCheckerOriginalSaveFunctions.barSave.bind(this), 'save');
+ };
+ }
+ if (BarItems.save_project_incremental && typeof BarItems.save_project_incremental.click === 'function' && !window.rotationCheckerOriginalSaveFunctions.barSaveInc) {
+ window.rotationCheckerOriginalSaveFunctions.barSaveInc = BarItems.save_project_incremental.click;
+ BarItems.save_project_incremental.click = function() {
+ checkRotationsBeforeSave(window.rotationCheckerOriginalSaveFunctions.barSaveInc.bind(this), 'saveIncrement');
+ };
+ }
+ }
+ } catch (e) {
+ }
+
+ const triggerSaveAsAction = () => {
+ try {
+ if (typeof BarItems !== 'undefined' && BarItems.save_project_as && typeof BarItems.save_project_as.click === 'function') {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ BarItems.save_project_as.click();
+ });
+ });
+ return true;
+ }
+ } catch (e) {
+ }
+ try {
+ const el = document.querySelector('li[menu_item="save_project_as"]');
+ if (el) {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ el.click();
+ });
+ });
+ return true;
+ }
+ } catch (e) {
+ }
+ try {
+ if (typeof window.saveAs === 'function') {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ window.saveAs();
+ });
+ });
+ return true;
+ }
+ } catch (e) {
+ }
+ return false;
+ };
+
+ const addMenuClickListeners = () => {
+ const saveMenuItem = document.querySelector('li[menu_item="save_project"]');
+ if (saveMenuItem && !saveMenuItem.hasAttribute('data-rotation-checker-hooked')) {
+ saveMenuItem.setAttribute('data-rotation-checker-hooked', 'true');
+ const originalClick = saveMenuItem.onclick;
+ saveMenuItem.onclick = function(event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const nonStandardRotations = extractRotations(Project);
+ if (nonStandardRotations.length > 0) {
+ showRotationWarningDialog(
+ nonStandardRotations,
+ () => {
+ if (originalClick) originalClick.call(this, event);
+ },
+ () => {},
+ 'save'
+ );
+ } else {
+ if (originalClick) originalClick.call(this, event);
+ }
+ };
+ }
+
+ const saveAsMenuItem = document.querySelector('li[menu_item="save_project_as"]');
+ if (saveAsMenuItem && !saveAsMenuItem.hasAttribute('data-rotation-checker-hooked')) {
+ saveAsMenuItem.setAttribute('data-rotation-checker-hooked', 'true');
+ const originalClick = saveAsMenuItem.onclick;
+ saveAsMenuItem.onclick = function(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ if (event.stopImmediatePropagation) event.stopImmediatePropagation();
+
+ const nonStandardRotations = extractRotations(Project);
+ if (nonStandardRotations.length > 0) {
+ showRotationWarningDialog(
+ nonStandardRotations,
+ () => {
+ Promise.resolve().then(() => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ const ok = triggerSaveAsAction();
+ if (!ok && originalClick) {
+ setTimeout(() => originalClick.call(this, event), 300);
+ }
+ });
+ });
+ });
+ },
+ () => {},
+ 'saveAs'
+ );
+ } else {
+ const ok = triggerSaveAsAction();
+ if (!ok && originalClick) originalClick.call(this, event);
+ }
+ };
+ }
+
+ const saveIncrementMenuItem = document.querySelector('li[menu_item="save_project_incremental"]');
+ if (saveIncrementMenuItem && !saveIncrementMenuItem.hasAttribute('data-rotation-checker-hooked')) {
+ saveIncrementMenuItem.setAttribute('data-rotation-checker-hooked', 'true');
+ const originalClick = saveIncrementMenuItem.onclick;
+ saveIncrementMenuItem.onclick = function(event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const nonStandardRotations = extractRotations(Project);
+ if (nonStandardRotations.length > 0) {
+ showRotationWarningDialog(
+ nonStandardRotations,
+ () => {
+ if (originalClick) originalClick.call(this, event);
+ },
+ () => {},
+ 'saveIncrement'
+ );
+ } else {
+ if (originalClick) originalClick.call(this, event);
+ }
+ };
+ }
+ };
+
+ addMenuClickListeners();
+ }
+
+ function checkRotationsBeforeSave(originalSaveFunction, context = 'save') {
+ const nonStandardRotations = extractRotations(Project);
+ if (nonStandardRotations.length > 0) {
+ showRotationWarningDialog(
+ nonStandardRotations,
+ () => {
+ if (originalSaveFunction) {
+ originalSaveFunction.call(this);
+ }
+ },
+ () => {
+ },
+ context
+ );
+ } else {
+ if (originalSaveFunction) {
+ originalSaveFunction.call(this);
+ }
+ }
+ }
+})();
diff --git a/plugins/check_invalid_rotation/demo.png b/plugins/check_invalid_rotation/demo.png
new file mode 100644
index 00000000..104756de
Binary files /dev/null and b/plugins/check_invalid_rotation/demo.png differ
diff --git a/plugins/check_invalid_rotation/members.yml b/plugins/check_invalid_rotation/members.yml
new file mode 100644
index 00000000..93e8213d
--- /dev/null
+++ b/plugins/check_invalid_rotation/members.yml
@@ -0,0 +1,5 @@
+maintainers:
+ - PlanesZwalker
+
+developers:
+ - PlanesZwalker