Files
wtd-libreflow-addon/config.py
T
Nero Frame 13 4617c47ad0 Added sanity check baseline made by claude.
Also changed the location to a panel in VIEW_3D instead of menu.
2026-10-13 16:33:03 +02:00

215 lines
7.8 KiB
Python

"""Production specs used by the checks.
Every UPPERCASE value can be overridden by a studio JSON file, see
``apply_overrides`` (path set in the add-on preferences or through the
``WTD_SANITY_CONFIG`` environment variable). Checks must read values at run
time (``config.FPS``), never copy them at import time, so overrides apply.
Values marked TODO are placeholders waiting for the real production specs.
"""
import copy
import json
import os
import sys
# --------------------------------------------------------------------------- #
# Scene / render
# --------------------------------------------------------------------------- #
FPS = 24
FPS_BASE = 1.0
RESOLUTION = (2048, 858)
RESOLUTION_PERCENTAGE = 100
PIXEL_ASPECT = (1.0, 1.0)
# Units. TODO: confirm with production (metric/meters assumed).
UNIT_SYSTEM = "METRIC"
UNIT_SCALE_LENGTH = 1.0
UNIT_LENGTH = "METERS"
# Color management. TODO: colorspace still to be determined by production.
# A value of None means "not checked".
COLOR_MANAGEMENT = {
"display_device": "sRGB",
"view_transform": "Standard",
"look": "None",
"exposure": 0.0,
"gamma": 1.0,
"sequencer_colorspace": "sRGB",
"working_space": None, # e.g. "Linear Rec.709" (Blender 5+). Check only, never auto-fixed.
}
# --------------------------------------------------------------------------- #
# File names. TODO: replace with the real Libreflow/Kitsu naming.
# Named group "asset" is used to guess the asset/character name.
# --------------------------------------------------------------------------- #
FILENAME_PATTERNS = {
"GENERAL": r"^(?:prp|set|chr)_(?P<asset>[A-Za-z0-9]+)_(?:mod|lineart|shading)(?:_v\d{3})?\.blend$",
"CHAR_MOD": r"^chr_(?P<asset>[A-Za-z0-9]+)_mod(?:_v\d{3})?\.blend$",
"CHAR_LINEART": r"^chr_(?P<asset>[A-Za-z0-9]+)_(?:lineart|shading)(?:_v\d{3})?\.blend$",
"LAYOUT": r"^(?P<sequence>sq\d{3})_(?P<shot>sh\d{4})_layout(?:_v\d{3})?\.blend$",
}
# --------------------------------------------------------------------------- #
# Naming
# --------------------------------------------------------------------------- #
# Body of a name after its prefix: snake_case or PascalCase tokens joined by "_".
NAME_BODY_PATTERN = r"[A-Za-z0-9]+(?:_[A-Za-z0-9]+)*"
# Object prefix per object type. Types not listed are not checked.
OBJECT_PREFIXES = {
"MESH": "GEO_",
"CURVE": "CRV_",
"SURFACE": "SRF_",
"FONT": "TXT_",
"EMPTY": "NUL_",
"ARMATURE": "RIG_",
"LATTICE": "LAT_",
"GREASEPENCIL": "LA_",
"CAMERA": "CAM_",
"LIGHT": "LGT_",
}
# Prefix per bpy.data collection.
DATABLOCK_PREFIXES = {
"collections": "COL_",
"materials": "MAT_",
"node_groups": "NG_",
}
# Mesh datablocks must carry the same name as their (single) object.
MESH_DATA_MATCHES_OBJECT = True
# Blender default names that must never survive in a published file.
DEFAULT_NAMES = {
"cube", "plane", "sphere", "uvsphere", "icosphere", "cylinder", "cone",
"torus", "circle", "grid", "suzanne", "monkey", "empty", "camera",
"light", "lamp", "point", "sun", "spot", "area", "material", "collection",
"beziercurve", "beziercircle", "nurbscurve", "nurbspath", "text",
"armature", "lattice", "nodegroup", "geometrynodes", "stroke", "gpencil",
"lineart", "scene", "world", "object", "mesh",
}
# --------------------------------------------------------------------------- #
# Structure (placeholder, real hierarchy to be defined)
# --------------------------------------------------------------------------- #
# Collections that must exist. "{asset}" is replaced by the asset name.
REQUIRED_COLLECTIONS = []
# --------------------------------------------------------------------------- #
# Geometry
# --------------------------------------------------------------------------- #
MAX_FACE_SIDES = 4
# sin() of the reflex angle above which a corner counts as concave.
CONCAVE_TOLERANCE = 1e-4
# Open borders are reported as non-manifold unless this is True.
NON_MANIFOLD_ALLOW_BOUNDARY = False
UV_NEGATIVE_TOLERANCE = 1e-6
# --------------------------------------------------------------------------- #
# Objects
# --------------------------------------------------------------------------- #
ALLOWED_MODIFIERS = {"SUBSURF", "MULTIRES"}
ALLOWED_MODIFIERS_LINEART = {
"SUBSURF", "MULTIRES", "LINEART", "GREASE_PENCIL_THICKNESS",
}
# Which transform channels must be applied.
APPLY_TRANSFORM = {"location": False, "rotation": True, "scale": True}
APPLY_TRANSFORM_TYPES = {"MESH", "CURVE", "SURFACE", "FONT", "META"}
TRANSFORM_TOLERANCE = 1e-5
# Object types that must not be present in asset files.
FORBIDDEN_ASSET_OBJECT_TYPES = {"LIGHT", "CAMERA"}
# --------------------------------------------------------------------------- #
# Characters
# --------------------------------------------------------------------------- #
# Reference JSON exported from the master character (see panel > Export
# Character Reference). Overridden by the add-on preferences or by the
# WTD_CHAR_REFERENCE environment variable.
CHAR_REFERENCE_PATH = ""
# Name of the character body object. "{asset}" is the character name.
CHAR_BODY_NAME = "GEO_{asset}_Body"
# Name keywords that identify inner mouth / eye parts (case-insensitive).
CHAR_PART_KEYWORDS = [
"eye", "iris", "pupil", "cornea", "sclera", "lid", "lash",
"mouth", "teeth", "tooth", "tongue", "gum", "palate",
]
# Vertex groups the body mesh must have. TODO: real list.
CHAR_BODY_VERTEX_GROUPS = []
# --------------------------------------------------------------------------- #
# Layout
# --------------------------------------------------------------------------- #
# Instanced collections matching this pattern are sets that must sit at the origin.
SET_COLLECTION_PATTERN = r"^(?:SET|ENV|set|env)[_-]"
# Version token in a file or folder name (Libreflow stores v001/ folders).
VERSION_PATTERN = r"(?<![A-Za-z0-9])v(\d{3,4})(?![0-9])"
# Datablock types scanned for ".001" duplicated names.
DUPLICATE_NAME_TYPES = ["objects", "collections", "cameras", "materials", "actions", "node_groups"]
# --------------------------------------------------------------------------- #
# UI
# --------------------------------------------------------------------------- #
CATEGORY_ORDER = [
"Scene", "File", "Naming", "Structure", "Geometry", "UVs", "Objects",
"Shading", "Animation", "Character", "Lineart", "Shot", "Sound",
]
MAX_ISSUES_DRAWN = 30
# --------------------------------------------------------------------------- #
# Overrides
# --------------------------------------------------------------------------- #
_DEFAULTS = None
def _snapshot():
global _DEFAULTS
if _DEFAULTS is None:
module = sys.modules[__name__]
_DEFAULTS = {
k: copy.deepcopy(v) for k, v in vars(module).items() if k.isupper() and not k.startswith("_")
}
return _DEFAULTS
def apply_overrides(path=""):
"""Reset to defaults, then apply a JSON file of {"NAME": value} overrides.
``path`` falls back to the WTD_SANITY_CONFIG environment variable.
Returns the list of keys that were overridden.
"""
module = sys.modules[__name__]
defaults = _snapshot()
for key, value in defaults.items():
setattr(module, key, copy.deepcopy(value))
path = path or os.environ.get("WTD_SANITY_CONFIG", "")
if not path:
return []
with open(path, encoding="utf-8") as fh:
data = json.load(fh)
applied = []
for key, value in data.items():
if key not in defaults:
print(f"[WTD Sanity] Unknown config key ignored: {key}")
continue
default = defaults[key]
if isinstance(default, (set, frozenset)):
value = set(value)
elif isinstance(default, tuple):
value = tuple(value)
elif isinstance(default, dict) and isinstance(value, dict):
value = {**default, **value}
setattr(module, key, value)
applied.append(key)
return applied