Rename panel: batch rename selected objects (or their materials) as prefix_name_side_number_suffix. Numbering continues after existing names, object data follows its object, and a preview shows the result. Shot Viewer (button in the Tools panel): opens a window with its own workspace showing the shots (sequencer scene strips) through their camera, with the edit's sequencer at the bottom. Scrubbing the edit switches shots, playing a shot moves the edit playhead, Loop Shot loops playback on the shot. Shots can be added and deleted, and character / prop rigs selected in pose mode from the sidebar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
324 lines
9.6 KiB
Python
324 lines
9.6 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 viewer
|
|
# --------------------------------------------------------------------------- #
|
|
# 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)[_-]",
|
|
}
|
|
|
|
# Shots added by the shot viewer. TODO: confirm with production.
|
|
SHOT_NAME_FORMAT = "sh{:04d}"
|
|
SHOT_NUMBER_STEP = 10
|
|
SHOT_FRAME_START = 101
|
|
SHOT_DEFAULT_DURATION = 48
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# 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
|