Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
103 lines
4.7 KiB
Python
103 lines
4.7 KiB
Python
"""RNA-derived property descriptions and validation inside the native Blender job."""
|
|
import math
|
|
|
|
MAX_OPERATOR_ARRAY_LENGTH = 64
|
|
MAX_OPERATOR_STRING_BYTES = 4096
|
|
|
|
|
|
def operator_property_spec(prop):
|
|
kind = str(getattr(prop, 'type', '')).upper()
|
|
if prop.identifier == 'rna_type' or kind not in ('BOOLEAN', 'INT', 'FLOAT', 'DOUBLE', 'ENUM', 'STRING'):
|
|
return None
|
|
length = int(getattr(prop, 'array_length', 0))
|
|
dimensions = tuple(getattr(prop, 'array_dimensions', ()))
|
|
if length > MAX_OPERATOR_ARRAY_LENGTH or sum(value > 0 for value in dimensions) > 1:
|
|
return None
|
|
if length and kind not in ('BOOLEAN', 'INT', 'FLOAT', 'DOUBLE'):
|
|
return None
|
|
flag = kind == 'ENUM' and bool(getattr(prop, 'is_enum_flag', False))
|
|
value_type = 'boolean' if kind == 'BOOLEAN' else 'number' if kind in ('INT', 'FLOAT', 'DOUBLE') else 'enum' if kind == 'ENUM' else 'string'
|
|
if length:
|
|
value_type += '-array'
|
|
elif flag:
|
|
value_type = 'enum-set'
|
|
spec = {'name': prop.identifier, 'type': value_type, 'required': bool(getattr(prop, 'is_required', False))}
|
|
for key, attr in (('label', 'name'), ('description', 'description'), ('subtype', 'subtype'), ('unit', 'unit')):
|
|
value = getattr(prop, attr, None)
|
|
if value:
|
|
spec[key] = str(value)
|
|
if length:
|
|
spec['arrayLength'] = length
|
|
if kind == 'INT':
|
|
spec['integer'] = True
|
|
try:
|
|
if length:
|
|
default = list(prop.default_array)
|
|
elif flag:
|
|
default = sorted(prop.default_flag)
|
|
else:
|
|
default = prop.default
|
|
if isinstance(default, (bool, int, float, str, list)):
|
|
values = default if isinstance(default, list) else [default]
|
|
if all(not isinstance(v, float) or math.isfinite(v) for v in values):
|
|
spec['defaultValue'] = default
|
|
except (AttributeError, TypeError, ValueError):
|
|
pass
|
|
for key, attr in (('min', 'hard_min'), ('max', 'hard_max')):
|
|
value = getattr(prop, attr, None)
|
|
if type(value) in (int, float) and math.isfinite(value):
|
|
spec[key] = value
|
|
if kind == 'ENUM':
|
|
items = [{'value': str(item.identifier), 'label': str(item.name)} for item in prop.enum_items if item.identifier]
|
|
if items:
|
|
spec['enumItems'] = items
|
|
return spec
|
|
|
|
|
|
def validated_operator_properties(target, properties):
|
|
"""Validate against the operator's live RNA immediately before execution."""
|
|
specs = {prop.identifier: operator_property_spec(prop) for prop in target.get_rna_type().properties}
|
|
result = {}
|
|
for name, value in properties.items():
|
|
spec = specs.get(name)
|
|
if spec is None:
|
|
raise ValueError('SERVER_OPERATOR_PROPERTY_BLOCKED: ' + name)
|
|
kind = spec['type']
|
|
if kind.endswith('-array'):
|
|
if not isinstance(value, list) or len(value) != spec['arrayLength']:
|
|
raise ValueError('SERVER_OPERATOR_PROPERTY_LENGTH: ' + name)
|
|
scalar_kind = kind.removesuffix('-array')
|
|
values = value
|
|
elif kind == 'enum-set':
|
|
if not isinstance(value, list) or len(value) > MAX_OPERATOR_ARRAY_LENGTH or any(type(v) is not str for v in value) or len(set(value)) != len(value):
|
|
raise ValueError('SERVER_OPERATOR_PROPERTY_TYPE: ' + name)
|
|
scalar_kind = 'enum'
|
|
values = value
|
|
else:
|
|
scalar_kind = kind
|
|
values = [value]
|
|
for item in values:
|
|
valid = False
|
|
if scalar_kind == 'boolean':
|
|
valid = type(item) is bool
|
|
elif scalar_kind == 'number':
|
|
valid = type(item) in (int, float) and math.isfinite(item)
|
|
if valid and spec.get('integer'):
|
|
valid = float(item).is_integer() and abs(item) <= 9007199254740991
|
|
if valid:
|
|
valid = spec.get('min', -math.inf) <= item <= spec.get('max', math.inf)
|
|
elif scalar_kind in ('string', 'enum'):
|
|
valid = type(item) is str and len(item.encode()) <= MAX_OPERATOR_STRING_BYTES
|
|
# Context-dependent enum callbacks may expose no items while describing.
|
|
if valid and scalar_kind == 'enum' and spec.get('enumItems'):
|
|
valid = item in {entry['value'] for entry in spec['enumItems']}
|
|
if not valid:
|
|
raise ValueError('SERVER_OPERATOR_PROPERTY_TYPE_OR_RANGE: ' + name)
|
|
if kind == 'enum-set':
|
|
result[name] = set(value)
|
|
elif spec.get('integer'):
|
|
result[name] = [int(v) for v in value] if isinstance(value, list) else int(value)
|
|
else:
|
|
result[name] = value
|
|
return result
|