Load Publish (Loaded panel) searches every asset of the project, then loads any of its published .blend files at any version, cast in the shot or not. The file the casting would load and the latest version on this site are preselected; a version not on this site is requested instead. Built on the existing wtd_list_assets / wtd_get_asset_files / wtd_get_revisions commands. New instances are numbered (COL_bob_01, tree_01...), counting on from the highest number in the file (SCENE_MANAGER_INSTANCE_NAME). README: Pick Shot only lists shots that exist in Libreflow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
388 lines
13 KiB
Python
388 lines
13 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 name, any name: COL_bob_LOD0, COL_bob_LODhigh...). An override
|
|
# shows one and hides the others; a collection instance instances it. Keep a fixed part
|
|
# around {lod} ("LOD"): every child collection matching the pattern counts as a LOD.
|
|
SCENE_MANAGER_LOD_COLLECTION = "COL_{asset}_LOD{lod}"
|
|
# Order of the LOD names; the first one the file has is shown when an asset is loaded.
|
|
# LODs not listed come after them: numbers in order (0, 1, 2...), then names A-Z.
|
|
SCENE_MANAGER_LODS = ["high", "mid", "low"]
|
|
# Name of each new instance: {name} is the asset collection (override) or the asset
|
|
# (collection instance), {number} counts on from the highest number in the file.
|
|
SCENE_MANAGER_INSTANCE_NAME = "{name}_{number:02d}"
|
|
# 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
|