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. + +![Rotation Checker Dialog](https://raw.githubusercontent.com/JannisX11/blockbench-plugins/master/plugins/check_invalid_rotation/demo.png) + +## 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 ` + + `; + }).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