Added sanity check baseline made by claude.
Also changed the location to a panel in VIEW_3D instead of menu.
This commit is contained in:
1 parent
71b1a61b5c
commit
4617c47ad0
33 files changed
+2771
-52
No files matched your search
@@ -0,0 +1,135 @@
|
||||
import re
|
||||
|
||||
import bpy
|
||||
|
||||
from ... import config
|
||||
from ...core import Check, Issue, Severity, contexts, is_local
|
||||
|
||||
_SUFFIX_RE = re.compile(r"\.\d{3,}$")
|
||||
|
||||
|
||||
def _strip_known_prefix(name, prefixes):
|
||||
for prefix in sorted(prefixes, key=len, reverse=True):
|
||||
if name.upper().startswith(prefix.upper()):
|
||||
return name[len(prefix):]
|
||||
return name
|
||||
|
||||
|
||||
def suggest_name(name, prefix, all_prefixes):
|
||||
"""Best-effort conforming name, or None when a human has to choose."""
|
||||
body = _SUFFIX_RE.sub("", name)
|
||||
body = _strip_known_prefix(body, all_prefixes)
|
||||
body = body.encode("ascii", "ignore").decode()
|
||||
body = re.sub(r"[^A-Za-z0-9]+", "_", body).strip("_")
|
||||
if not body or _is_default(body):
|
||||
return None
|
||||
candidate = prefix + body
|
||||
return candidate if candidate != name else None
|
||||
|
||||
|
||||
def _is_default(body):
|
||||
return re.sub(r"[^a-z]", "", body.lower()) in config.DEFAULT_NAMES
|
||||
|
||||
|
||||
def _validate(name, prefix):
|
||||
"""Return the list of problems of a name for the given prefix."""
|
||||
problems = []
|
||||
if _SUFFIX_RE.search(name):
|
||||
problems.append("duplicate suffix")
|
||||
if not name.isascii():
|
||||
problems.append("non-ASCII characters")
|
||||
if " " in name:
|
||||
problems.append("spaces")
|
||||
if prefix and not name.startswith(prefix):
|
||||
problems.append(f"missing prefix '{prefix}'")
|
||||
body = _SUFFIX_RE.sub("", name)
|
||||
body = body[len(prefix):] if prefix and body.startswith(prefix) else body
|
||||
if _is_default(body):
|
||||
problems.append("Blender default name")
|
||||
elif not re.fullmatch(config.NAME_BODY_PATTERN, body):
|
||||
problems.append("invalid characters or empty tokens")
|
||||
return problems
|
||||
|
||||
|
||||
class NamingCheck(Check):
|
||||
id = "naming"
|
||||
label = "Naming convention"
|
||||
description = (
|
||||
"Objects, mesh data, collections, materials and node groups follow the type prefix "
|
||||
"convention (GEO_, COL_, MAT_...), with no default names, '.001' suffixes, spaces or accents."
|
||||
)
|
||||
category = "Naming"
|
||||
contexts = contexts.ASSET
|
||||
editable_names = True
|
||||
fix_description = "Rename to the suggested name when one can be derived (prefix, suffix and character clean-up)"
|
||||
|
||||
def run(self, ctx):
|
||||
all_prefixes = set(config.OBJECT_PREFIXES.values()) | set(config.DATABLOCK_PREFIXES.values())
|
||||
|
||||
def check(id_block, prefix, kind):
|
||||
problems = _validate(id_block.name, prefix)
|
||||
if problems:
|
||||
suggestion = suggest_name(id_block.name, prefix, all_prefixes)
|
||||
hint = f" -> {suggestion}" if suggestion else ""
|
||||
return Issue.for_id(
|
||||
f"{kind} '{id_block.name}': {', '.join(problems)}{hint}",
|
||||
id_block,
|
||||
data={"suggestion": suggestion},
|
||||
)
|
||||
return None
|
||||
|
||||
objects = ctx.objects()
|
||||
for ob in objects:
|
||||
prefix = config.OBJECT_PREFIXES.get(ob.type)
|
||||
if prefix is None:
|
||||
continue
|
||||
issue = check(ob, prefix, "Object")
|
||||
if issue:
|
||||
yield issue
|
||||
|
||||
if config.MESH_DATA_MATCHES_OBJECT:
|
||||
for ob in objects:
|
||||
me = ob.data
|
||||
if ob.type != "MESH" or me is None or not is_local(me) or me.users > 1:
|
||||
continue
|
||||
if me.name != ob.name:
|
||||
yield Issue.for_id(
|
||||
f"Mesh data '{me.name}' should be named like its object '{ob.name}'",
|
||||
me,
|
||||
severity=Severity.WARNING,
|
||||
data={"match_object": True},
|
||||
)
|
||||
|
||||
for collection_name, prefix in config.DATABLOCK_PREFIXES.items():
|
||||
kind = collection_name.rstrip("s").replace("_", " ").capitalize()
|
||||
for id_block in getattr(bpy.data, collection_name):
|
||||
if not is_local(id_block) or id_block.users == 0:
|
||||
continue
|
||||
if collection_name == "node_groups" and id_block.bl_idname == "CompositorNodeTree":
|
||||
continue
|
||||
issue = check(id_block, prefix, kind)
|
||||
if issue:
|
||||
yield issue
|
||||
|
||||
def fix(self, ctx, issues):
|
||||
skipped = []
|
||||
# Mesh data last: it takes the name of its object once that is fixed.
|
||||
for issue in sorted(issues, key=lambda i: i.data.get("match_object", False)):
|
||||
id_block = issue.get_id()
|
||||
if id_block is None:
|
||||
continue
|
||||
if issue.data.get("match_object"):
|
||||
owners = [ob for ob in bpy.data.objects if ob.data == id_block]
|
||||
suggestion = owners[0].name if len(owners) == 1 else None
|
||||
else:
|
||||
suggestion = issue.data.get("suggestion")
|
||||
if not suggestion:
|
||||
skipped.append(f"'{issue.id_name}': no automatic name, rename it by hand")
|
||||
continue
|
||||
collection = getattr(bpy.data, issue.id_type)
|
||||
existing = collection.get(suggestion)
|
||||
if existing is not None and existing != id_block:
|
||||
skipped.append(f"'{issue.id_name}': '{suggestion}' is already taken")
|
||||
continue
|
||||
id_block.name = suggestion
|
||||
return skipped
|
||||
Reference in new issue
Block a user