Files
wtd-libreflow-addon/config.py
T
NDemaerschalkandClaude Opus 5.5 bac7a6f5da Scene Manager: LOD switching; Shot Viewer renamed Shot Manager
LODs are child collections of the asset collection (COL_{asset}_LOD{n}).
Overrides hide the other LOD collections (viewport and render), keeping
the rig, pose and animation; collection instances instance the LOD
collection. Each instance has its own LOD, set per instance or for the
selection in the Loaded panel, kept through save, reload and updates.
Loading shows SCENE_MANAGER_DEFAULT_LOD (0).

The Shot Viewer panel is now "Shot Manager" and starts closed. Code
names and SHOT_VIEWER_* config keys are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:42:24 +01:00

383 lines
12 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
# --------------------------------------------------------------------------- #
# 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",
]
# --------------------------------------------------------------------------- #
# Rename tool
# --------------------------------------------------------------------------- #
# Names are built as {prefix}_{name}_{side}_{number}_{suffix}, empty parts dropped.
# "By Type" prefixes come from OBJECT_PREFIXES / DATABLOCK_PREFIXES.
RENAME_SEPARATOR = "_"
RENAME_PADDING = 3
# Orientation tokens. TODO: confirm with production.
RENAME_SIDES = ["center", "left", "right", "front", "back", "top", "bottom"]
# "By Type" suffix per object type ("MATERIAL" for materials). TODO: confirm with production.
RENAME_TYPE_SUFFIXES = {
"MESH": "mesh",
"CURVE": "curve",
"SURFACE": "surface",
"FONT": "text",
"EMPTY": "empty",
"ARMATURE": "rig",
"LATTICE": "lattice",
"GREASEPENCIL": "gp",
"CAMERA": "cam",
"LIGHT": "light",
"MATERIAL": "mat",
}
# --------------------------------------------------------------------------- #
# Shot manager
# --------------------------------------------------------------------------- #
# Groups of the rig quick selection: an armature goes to the first group whose
# pattern matches its name, one of its collections or its library file name.
# Unmatched armatures are listed under "Other". TODO: confirm with production.
SHOT_VIEWER_RIG_GROUPS = {
"Characters": r"(?i)^(?:chr|char)[_-]",
"Props": r"(?i)^(?:prp|prop)[_-]",
}
# Rigs loaded by the Scene Manager are grouped by their Libreflow asset type first.
SHOT_VIEWER_ASSET_TYPE_GROUPS = {
"main_character": "Characters",
"secondary_character": "Characters",
"tertiary_character": "Characters",
"main_props": "Props",
"secondary_props": "Props",
"vehicles": "Props",
}
# Shots added by the shot manager. TODO: confirm with production.
SHOT_NAME_FORMAT = "sh{:04d}"
# Sequence of the scene (number set in the Shot Manager panel): SEQUENCE_NAME_FORMAT.format(number).
SEQUENCE_NAME_FORMAT = "sq{:04d}"
# Cameras added by the shot manager. Tokens: {prefix} (OBJECT_PREFIXES["CAMERA"]),
# {sequence} (SEQUENCE_NAME_FORMAT) and {shot} (the shot name).
SHOT_CAMERA_NAME_FORMAT = "{prefix}{sequence}_{shot}"
# Their rig, same tokens with {prefix} = OBJECT_PREFIXES["ARMATURE"].
SHOT_CAMERA_RIG_NAME_FORMAT = "{prefix}{sequence}_{shot}"
# Mode of bpy.ops.object.build_camera_rig (Add Camera Rigs add-on) for 3D and 2D cameras.
SHOT_CAMERA_RIG_MODES = {"3D": "CRANE", "2D": "2D"}
SHOT_NUMBER_STEP = 10
SHOT_FRAME_START = 1001
SHOT_DEFAULT_DURATION = 48
# --------------------------------------------------------------------------- #
# Scene Manager (Libreflow)
# --------------------------------------------------------------------------- #
# Address and token of the Libreflow IPC server (kabaret.ipc).
LIBREFLOW_SERVER_FILE = "~/.kabaret/ipc/server.json"
# Seconds to wait for an answer (the server itself gives up after 10 s).
LIBREFLOW_TIMEOUT = 15
# Published file loaded for each asset type ("*" = the others): ordered
# "task/file.ext" glob patterns, the first file of the asset matching one is loaded.
# TODO: confirm the set publishes (only setdress_low seen so far).
SCENE_MANAGER_FILE_PATTERNS = {
"*": ["rigging/*.blend", "modeling/*.blend"],
"bg_location": ["setdress_low/*.blend"],
}
# Collection linked from an asset file: the first of these names found ({asset}
# = asset name), else the file's single top-level collection. "environments"
# holds the set in the setdress files.
SCENE_MANAGER_COLLECTION_NAMES = ["COL_{asset}", "{asset}", "environments"]
# How an asset type (Kitsu name) is brought in: "OVERRIDE" (library override, posable)
# or "INSTANCE" (collection instance). "*" = the other types. TODO: confirm with production.
SCENE_MANAGER_LINK_MODES = {
"main_character": "OVERRIDE",
"secondary_character": "OVERRIDE",
"tertiary_character": "OVERRIDE",
"main_props": "OVERRIDE",
"secondary_props": "OVERRIDE",
"vehicles": "OVERRIDE",
"*": "INSTANCE",
}
# Levels of detail: child collections of the asset collection, one per LOD ({asset} =
# asset name, {lod} = LOD number). An override shows one and hides the others; a
# collection instance instances it.
SCENE_MANAGER_LOD_COLLECTION = "COL_{asset}_LOD{lod}"
# LOD shown when an asset is loaded (the lowest one when missing). None: as in the file.
SCENE_MANAGER_DEFAULT_LOD = 0
# Collection gathering the loaded assets of a type, under the scene collection.
SCENE_MANAGER_TYPE_COLLECTION = "COL_{asset_type}"
# Default task of the assetize dialog.
SCENE_MANAGER_ASSETIZE_TASK = "modeling"
# --------------------------------------------------------------------------- #
# 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