Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -1534,5 +1534,16 @@
"website": "https://svdex.moe",
"min_version": "4.8.0",
"has_changelog": true
},
"name_guard": {
"title": "Name Guard",
"author": "Dedou3D",
"description": "Clean folder and cube names for safe ModelEngine exports.",
"icon": "drive_file_rename_outline",
"version": "1.0.0",
"variant": "both",
"min_version": "4.0.0",
"new_repository_format": true,
"tags": ["Minecraft", "ModelEngine", "Utility"]
}
}
31 changes: 31 additions & 0 deletions plugins/name_guard/about.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Name Guard

Name Guard cleans folder and cube names so your models export safely.

Before:

`Right Arm`
- `Cube`
- `Cube`

After:

`right_arm`
- `right_arm`
- `right_arm`

## What It Does

- Converts names to lowercase
- Removes accents
- Replaces spaces and invalid characters with `_`
- Cleans folder names
- Renames cubes from their parent folder name
- Keeps root cubes safe by cleaning their own names

## Usage

Open your model, then run:

`Filter > Clean Names`

114 changes: 114 additions & 0 deletions plugins/name_guard/name_guard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
(function() {
'use strict';

const PLUGIN_ID = 'name_guard';
const FALLBACK_NAME = 'part';

let cleanNamesAction;

function sanitizeName(value, fallback = FALLBACK_NAME) {
const sanitized = String(value || '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9_.-]+/g, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '');

return sanitized || fallback;
}

function uniqueName(baseName, usedNames) {
let name = baseName;
let index = 2;

while (usedNames.has(name)) {
name = `${baseName}_${index}`;
index += 1;
}

usedNames.add(name);
return name;
}

function getChildGroups(group) {
return group.children.filter(child => child instanceof Group);
}

function getChildCubes(group) {
return group.children.filter(child => child instanceof Cube);
}

function renameGroupTree(group, usedNamesByGroup) {
const parent = group.parent instanceof Group ? group.parent : null;
const usedNames = usedNamesByGroup.get(parent) || new Set();
usedNamesByGroup.set(parent, usedNames);

group.name = uniqueName(sanitizeName(group.name), usedNames);

const childGroupNames = new Set();
usedNamesByGroup.set(group, childGroupNames);
getChildGroups(group).forEach(childGroup => renameGroupTree(childGroup, usedNamesByGroup));

const cubeBaseName = sanitizeName(group.name);
getChildCubes(group).forEach(cube => {
cube.name = cubeBaseName;
});
}

function cleanRootCubes() {
const usedNames = new Set();
Cube.all
.filter(cube => !(cube.parent instanceof Group))
.forEach(cube => {
cube.name = uniqueName(sanitizeName(cube.name), usedNames);
});
}

function cleanNames() {
if (!Project) {
Blockbench.showQuickMessage('Open or create a project first.');
return;
}

Undo.initEdit({outliner: true, elements: Cube.all});

const rootGroups = Group.all.filter(group => !(group.parent instanceof Group));
const usedNamesByGroup = new Map();
rootGroups.forEach(group => renameGroupTree(group, usedNamesByGroup));
cleanRootCubes();

Undo.finishEdit('Clean names');
Canvas.updateAll();
Blockbench.showQuickMessage('Names cleaned.');
}

Plugin.register(PLUGIN_ID, {
title: 'Name Guard',
author: 'Dedou3D',
description: 'Clean folder and cube names for safe ModelEngine exports.',
icon: 'drive_file_rename_outline',
version: '1.0.0',
variant: 'both',
min_version: '4.0.0',
new_repository_format: true,
tags: ['Minecraft', 'ModelEngine', 'Utility'],
onload() {
cleanNamesAction = new Action('clean_name_guard_names', {
name: 'Clean Names',
description: 'Clean folder and cube names for safe exports.',
icon: 'drive_file_rename_outline',
category: 'edit',
click: cleanNames
});

MenuBar.addAction(cleanNamesAction, 'filter');
},
onunload() {
if (cleanNamesAction) {
cleanNamesAction.delete();
cleanNamesAction = null;
}
}
});
})();