import { blockedGate, capabilityIssue, readyGate, type CapabilityGateResult } from "./capability-gates"; import type { ErrorCode } from "./error"; import type {NativeUVPackOptions} from './uv-pack-options'; import {parseNativeUVPackTarget,type NativeUVPackTarget} from './uv-pack-target'; import {parseNativeUVBounds,type NativeUVBounds,type NativeUVCustomRegion} from './uv-custom-region'; import type { NativeViewportState } from './viewport-camera'; import { isModifierCurveProfile } from './modifier'; export const EDITOR_WORKFLOW_SCHEMA = 1 as const; export const EDITOR_WORKFLOW_BUDGET = { maxWorkspaces: 64, maxAreas: 1_024, maxRegions: 4_096, maxSelection: 100_000, maxKeymaps: 4_096, maxPropertiesDataFields: 128 } as const; export const EDITOR_TYPES = ["VIEW_3D", "OUTLINER", "PROPERTIES", "UV_IMAGE", "NODE", "GRAPH", "DOPE_SHEET", "NLA", "SPREADSHEET", "TIMELINE", "CLIP", "MASK", "SEQUENCER", "TEXT", "CONSOLE", "INFO", "FILE_BROWSER", "PREFERENCES", "TOPBAR", "STATUSBAR", "UNSUPPORTED"] as const; export type EditorTypeIR = typeof EDITOR_TYPES[number]; export type EditorRegionKind = "HEADER" | "MAIN" | "TOOLBAR" | "SIDEBAR" | "FOOTER" | "OTHER"; export const EDITOR_MODES = ["OBJECT", "EDIT", "POSE", "SCULPT", "TEXTURE_PAINT", "VERTEX_PAINT", "WEIGHT_PAINT"] as const; export type EditorMode = typeof EDITOR_MODES[number]; export const GRAPH_INTERPOLATION_MODES = [ ["CONSTANT", "Constant", "Interpolation"], ["LINEAR", "Linear", "Interpolation"], ["BEZIER", "Bézier", "Interpolation"], ["SINE", "Sinusoidal", "Easing (by strength)"], ["QUAD", "Quadratic", "Easing (by strength)"], ["CUBIC", "Cubic", "Easing (by strength)"], ["QUART", "Quartic", "Easing (by strength)"], ["QUINT", "Quintic", "Easing (by strength)"], ["EXPO", "Exponential", "Easing (by strength)"], ["CIRC", "Circular", "Easing (by strength)"], ["BACK", "Back", "Dynamic Effects"], ["BOUNCE", "Bounce", "Dynamic Effects"], ["ELASTIC", "Elastic", "Dynamic Effects"], ] as const; export type GraphInterpolation = typeof GRAPH_INTERPOLATION_MODES[number][0]; export interface EditorRegionIR { id: string; kind: EditorRegionKind; visible: boolean; active?: boolean; quadView?: boolean; toggleable?: boolean; flippable?: boolean; alignment?: 'TOP' | 'BOTTOM' | 'LEFT' | 'RIGHT' | 'OTHER' } export interface TextCursorIR { line: number; column: number } export const UV_UNWRAP_METHODS = ['ANGLE_BASED', 'CONFORMAL', 'MINIMUM_STRETCH'] as const; export type NativeUVTransform = { mode: 'TRANSLATE'; value: [number,number] } | (({ mode: 'SCALE'; value: [number,number] } | { mode: 'ROTATE'; angle: number }) & { center?: [number,number] }); export const UV_SELECTION_MODES = ['VERTEX', 'EDGE', 'FACE'] as const; export const UV_STICKY_MODES = ['DISABLED', 'SHARED_LOCATION', 'SHARED_VERTEX'] as const; export type NativeUVEdit = { operation: 'SET_PACK_TILE'; tile: number } | { operation: 'CUSTOM_REGION_SET'; bounds: NativeUVBounds } | { operation: 'CUSTOM_REGION_ENABLE'; enabled: boolean } | { operation: 'PACK_ISLANDS'; options: NativeUVPackOptions } | { operation: 'SELECT_STICKY'; mode: typeof UV_STICKY_MODES[number] } | { operation: 'SELECT_MODE'; mode: typeof UV_SELECTION_MODES[number] } | { operation: 'SELECT_SYNC' | 'SELECT_ISLAND'; enabled: boolean } | { operation: 'SELECT'; location: [number, number]; pixelsPerUV: number; toggle?: boolean } | { operation: 'SELECT_ALL' | 'DESELECT_ALL' | 'INVERT_SELECTION' | 'PIN' | 'UNPIN' | 'INVERT_PIN' } | { operation: 'UNWRAP'; options: NativeUVUnwrapOptions } | { operation: 'TRANSFORM'; transform: NativeUVTransform } | { operation: 'STITCH'; options: NativeUVStitchOptions }; export const UV_STITCH_BOOLEANS = ['use_limit','snap_islands','midpoint_snap','clear_seams'] as const; export type NativeUVStitchOptions = Partial & {mode:'VERTEX'|'EDGE';limit:number;static_island:number;active_object_index:number}>; export const UV_UNWRAP_MARGIN_METHODS = ['SCALED', 'ADD', 'FRACTION'] as const; export const UV_UNWRAP_BOOLEANS = ['fill_holes', 'correct_aspect', 'use_subsurf_data', 'use_original_bounds', 'no_flip', 'use_weights'] as const; export type NativeUVUnwrapOptions = Partial & { method: typeof UV_UNWRAP_METHODS[number]; margin_method: typeof UV_UNWRAP_MARGIN_METHODS[number]; margin: number; iterations: number; weight_factor: number; weight_group: string; }>; export interface TextSelectionIR { anchor: TextCursorIR; focus: TextCursorIR } export interface NativeTextEdit { operation: 'INDENT' | 'UNINDENT' | 'DUPLICATE_LINE' | 'COMMENT_TOGGLE' | 'COMMENT' | 'UNCOMMENT' | 'LINE_BREAK' | 'WHITESPACE_SPACES' | 'WHITESPACE_TABS' | 'DELETE_PREVIOUS_CHARACTER' | 'DELETE_NEXT_CHARACTER' | 'DELETE_PREVIOUS_WORD' | 'DELETE_NEXT_WORD'; dataId: string; source: string; selection: TextSelectionIR } export interface NativeImageView { zoom: number; offsetX: number; offsetY: number; width: number; height: number; aspectX: number; aspectY: number } export const PROPERTIES_OBJECT_BOOLEAN_FIELDS = ['hide_viewport', 'hide_render', 'hide_select', 'show_name', 'show_axis', 'show_wire', 'show_all_edges', 'show_bounds', 'use_camera_lock_parent'] as const; export const PROPERTIES_OBJECT_ENUM_FIELDS = ['display_type', 'display_bounds_type', 'rotation_mode', 'track_axis', 'up_axis'] as const; export const PROPERTIES_OBJECT_RELATION_FIELDS = ['use_camera_lock_parent', 'track_axis', 'up_axis'] as const; export type PropertiesObjectRelationField = typeof PROPERTIES_OBJECT_RELATION_FIELDS[number]; export type PropertiesObjectField = typeof PROPERTIES_OBJECT_BOOLEAN_FIELDS[number] | typeof PROPERTIES_OBJECT_ENUM_FIELDS[number]; export const PROPERTIES_OBJECT_TRANSFORM_LENGTHS = { location: 3, rotation_euler: 3, rotation_quaternion: 4, rotation_axis_angle: 4, scale: 3, dimensions: 3, delta_location: 3, delta_rotation_euler: 3, delta_rotation_quaternion: 4, delta_scale: 3, } as const; export type PropertiesObjectTransform = keyof typeof PROPERTIES_OBJECT_TRANSFORM_LENGTHS; export const PROPERTIES_OBJECT_LOCK_LENGTHS = { lock_location: 3, lock_rotation: 3, lock_scale: 3, lock_rotation_w: 1, lock_rotations_4d: 1, } as const; export type PropertiesObjectLock = keyof typeof PROPERTIES_OBJECT_LOCK_LENGTHS; export interface NativePropertiesLock { id: PropertiesObjectLock; label: string; values: boolean[]; editable: boolean[]; } export interface NativePropertiesTransform { id: PropertiesObjectTransform; label: string; values: number[]; editable: boolean[]; minimum: number; maximum: number; } export type PropertiesParentSetting = 'parent_vertices' | 'use_parent_final_indices' | 'parent_bone_head_tail_factor'; export interface NativePropertiesParentSetting { id: PropertiesParentSetting; index: number; label: string; value: number | boolean; minimum: number; maximum: number; editable: boolean; } export interface NativePropertiesObject { id: string; name: string; meshId?: string | null; fields: Array<{ id: PropertiesObjectField; label: string; editable: boolean; value: boolean | string; items: Array<{ id: string; label: string }> }>; transforms?: NativePropertiesTransform[]; deltaTransforms?: NativePropertiesTransform[]; dimensions?: NativePropertiesTransform[]; locks?: NativePropertiesLock[]; relations?: { parentId: string | null; parentName: string | null; editable: boolean; candidates: Array<{ id: string; name: string }>; truncated: boolean; parenting?: { type: string | null; types: Array<{ id: string; label: string }>; typeEditable: boolean; bone: string; boneEditable: boolean; bones: string[]; bonesTruncated: boolean; settings?: NativePropertiesParentSetting[]; }; }; modifiers?: Array<{ uuid: string; type: string; name: string; enabled: boolean; showViewport: boolean; showRender: boolean; showEditMode?: boolean; showOnCage?: boolean; diagnostics?: Array<{ kind: 'INFO' | 'ERROR'; message: string }>; bind?: { bound: boolean; enabled: boolean; reason: string }; duplicate?: { enabled: boolean; reason: string }; apply?: { enabled: boolean; reason: string; requiresSingleUser: boolean; supportsAllKeyframes?: boolean; supportsMergeCustomData?: boolean }; applyAsShapeKey?: { enabled: boolean; reason: string }; copyToSelected?: { enabled: boolean; reason: string; targets: string[] }; skinActions?: Array<{ id: 'CREATE_ARMATURE' | 'ADD_SKIN_DATA' | 'MARK_LOOSE' | 'CLEAR_LOOSE' | 'MARK_ROOT' | 'EQUALIZE_RADII'; label: string; enabled: boolean; reason: string }>; hookActions?: Array<{ id: 'RESET' | 'RECENTER' | 'SELECT' | 'ASSIGN'; label: string; enabled: boolean; reason: string }>; dataLayoutActions?: Array<{ id: 'GENERATE_DATA_LAYERS'; label: string; enabled: boolean; reason: string }>; multiresActions?: Array<{ id: 'DELETE_HIGHER_LEVELS' | 'SUBDIVIDE' | 'RESHAPE' | 'BASE_APPLY' | 'UNSUBDIVIDE' | 'REBUILD_SUBDIV' | 'EXTERNAL_SAVE' | 'EXTERNAL_PACK'; label: string; enabled: boolean; reason: string }>; parameters: Record; editableParameters?: Array<{ key: string; label?: string; description?: string; editable?: boolean; nullable?: boolean; visible?: boolean; enabled?: boolean; suggestionsOnly?: boolean; maxLength?: number; unit?: 'ANGLE'; kind: 'boolean' | 'number' | 'enum' | 'enum_set' | 'object' | 'collection' | 'texture' | 'cachefile' | 'curve_mapping' | 'string' | 'profile'; value: unknown; integer: boolean; length: number; minimum?: number; maximum?: number; items?: Array<{ id: string; label: string }> }>; }>; modifierTypes?: Array<{ id: string; label: string; category: string; enabled: boolean; reason: string }>; activeModifierUuid?: string | null; constraints?: Array<{ reorderable?: boolean; name: string; type: string; enabled: boolean; influence: number; targetObjectId?: string | null; targetBoneName?: string }>; activeConstraintName?: string | null; stackEditable?: boolean; constraintTargets?: Array<{ id: string; name: string; kind: 'ARMATURE' | 'CURVE' | 'MESH' | 'LATTICE' | 'OTHER'; bones: string[]; vertexGroups?: string[] }>; poseBones?: string[]; poseConstraints?: Array<{ reorderable?: boolean; targetBoneName?: string; boneName: string; name: string; type: string; enabled: boolean; influence: number; targetObjectId?: string | null }>; activePoseConstraint?: { boneName: string; name: string } | null; } export type PropertiesDataValue = boolean | number | string | number[]; export interface NativeArmatureConstraintTarget { objectId: string; bone: string; weight: number; } export interface NativePropertiesDataField { id: string; label: string; description?: string; editable: boolean; value: PropertiesDataValue; integer?: boolean; arrayLayout?: 'COLOR' | 'VECTOR' | 'MATRIX' | 'SEQUENCE'; minimum?: number; maximum?: number; status?: string; items: Array<{ id: string; label: string }>; } export interface NativePropertiesData { lightgroups?: { sceneId: string; layerName: string; editable: boolean; activeIndex: number; rows: string[] }; aovs?: { sceneId: string; layerName: string; editable: boolean; activeIndex: number; rows: Array<{ name: string; type: 0 | 1; isValid: boolean }> }; id: string; name: string; fields: NativePropertiesDataField[]; shapeKeys?: { objectId: string; keyId: string; activeUid: number; editable: boolean; rows: Array<{ uid: number; name: string; selected: boolean; muted: boolean; locked: boolean; fields?: NativePropertiesDataField[] }>; operations: Array<{ id: string; label: string; enabled: boolean; reason: string; sources?: string[] }>; }; shapeKeyMirror?: { objectId: string; keyId: string; activeUid: number; name: string; enabled: boolean; topologyEnabled: boolean; reason: string; }; constraintOperations?: Array<{ id: string; label: string; description: string; enabled: boolean; reason: string; menu?: boolean; parameters?: Array<{ id: string; label: string; description: string; defaultValue: number; minimum: number; maximum: number }> }>; armatureTargets?: NativeArmatureConstraintTarget[]; targetsEditable?: boolean; armatures?: Array<{ id: string; name: string; bones: string[] }>; cacheLayers?: Array<{ filepath: string; hidden: boolean }>; cacheActiveLayer?: number; cacheLayersEditable?: boolean; } export interface NativePropertiesContext { context: string | null; contexts: Array<{ id: string; label: string }>; viewLayers?: Array<{ name: string; active: boolean; editable: boolean }>; pinned: boolean; canPin: boolean; pinId: string | null; pinName: string | null; outlinerSync?: { value: string | null; items: Array<{ id: string; label: string }>; editable: boolean }; object?: NativePropertiesObject | null; data?: NativePropertiesData | null; } export interface NativeSpreadsheetRule { enabled: boolean; show_expanded: boolean; column_name: string; operation: 'EQUAL' | 'GREATER' | 'LESS'; value_float: number; value_int: number; value_float2: [number, number]; value_float3: [number, number, number]; value_float4: [number, number, number, number]; value_color: [number, number, number, number]; value_int2: [number, number]; value_int3: [number, number, number]; value_string: string; value_boolean: boolean; threshold: number; } export const SPREADSHEET_CONTEXT_PROPERTIES = ['object_eval_state', 'geometry_component_type', 'attribute_domain'] as const; export type SpreadsheetContextProperty = typeof SPREADSHEET_CONTEXT_PROPERTIES[number]; export const SPREADSHEET_VALUE_PROPERTIES = ['value_float', 'value_int', 'value_boolean', 'value_string', 'value_float2', 'value_float3', 'value_float4', 'value_color', 'value_int2', 'value_int3'] as const; export interface NativeSpreadsheetColumn { name: string; valueProperty: typeof SPREADSHEET_VALUE_PROPERTIES[number] | null; comparison: boolean; threshold: boolean; } export type SpreadsheetClosure = 'NONE' | 'INPUT' | 'OUTPUT'; export type SpreadsheetNavigationTarget = 'VIEWER_OUTPUTS' | 'VIEWER_BUNDLE_CHILDREN' | 'GEOMETRY_BUNDLE_CHILDREN' | 'INSTANCE_REFERENCES'; export interface NativeSpreadsheetBundleNavigation { active: boolean; available: boolean; path: string[]; pathValid: boolean; closure: SpreadsheetClosure; hasInputs: boolean; hasOutputs: boolean; children: string[]; childrenTruncated: boolean; childrenPage: number; childrenPageCount: number; } export interface NativeSpreadsheetViewerNavigation { available: boolean; itemIdentifier: number; items: Array<{ identifier: number; label: string }>; itemsTruncated: boolean; itemsPage: number; itemsPageCount: number; selection: NativeSpreadsheetBundleNavigation; } export interface NativeSpreadsheetContext { bundle?: NativeSpreadsheetBundleNavigation; viewer?: NativeSpreadsheetViewerNavigation; useFilter: boolean; rules: NativeSpreadsheetRule[]; instances?: { path: number[]; breadcrumbs: string[]; geometryAvailable: boolean; pathValid: boolean; children: Array<{ referenceIndex: number; label: string; userCount: number }>; childrenTruncated: boolean; childrenPage: number; childrenPageCount: number; }; viewState?: { pinned: boolean; canPin: boolean; showOnlySelected: boolean; selectionFilterAvailable: boolean; dataSourceAvailable: boolean; }; context?: Record }>; columns?: NativeSpreadsheetColumn[]; columnsTruncated?: boolean; } export type SpreadsheetRuleEdit = { [K in keyof NativeSpreadsheetRule]: { operation: 'UPDATE_RULE'; index: number; property: K; value: NativeSpreadsheetRule[K]; } }[keyof NativeSpreadsheetRule]; export type NativeSpreadsheetEdit = | { operation: 'ADD_RULE' } | { operation: 'REMOVE_RULE'; index: number } | { operation: 'SET_FILTER'; enabled: boolean } | { operation: 'SET_PIN'; pinned: boolean } | { operation: 'SET_SELECTED_ONLY'; enabled: boolean } | { operation: 'SET_INSTANCE_PATH'; path: number[] } | { operation: 'SET_BUNDLE_PATH'; path: string[]; closure: SpreadsheetClosure } | { operation: 'SET_VIEWER_PATH'; itemIdentifier: number; path: string[]; closure: SpreadsheetClosure } | { operation: 'SET_NAVIGATION_PAGE'; target: SpreadsheetNavigationTarget; page: number } | { operation: 'SET_CONTEXT'; property: SpreadsheetContextProperty; value: string } | SpreadsheetRuleEdit; export function validSpreadsheetBundlePath(value: unknown): value is string[] { return Array.isArray(value) && value.length <= 64 && value.every(key => typeof key === 'string' && key.length > 0 && !key.includes('\0') && new TextEncoder().encode(key).byteLength <= 4096); } export function validSpreadsheetBundleSelection( bundle: NativeSpreadsheetBundleNavigation, path: unknown, closure: unknown, ): boolean { if (!bundle.available || !validSpreadsheetBundlePath(path) || !['NONE', 'INPUT', 'OUTPUT'].includes(closure as string)) return false; const ancestor = path.length <= bundle.path.length && path.every((key, depth) => key === bundle.path[depth]); if (closure === 'NONE' && ancestor) return true; if (!bundle.pathValid) return false; const same = ancestor && path.length === bundle.path.length; if (same && ((closure === 'INPUT' && bundle.hasInputs) || (closure === 'OUTPUT' && bundle.hasOutputs))) return true; return closure === 'NONE' && path.length === bundle.path.length + 1 && bundle.path.every((key, depth) => key === path[depth]) && bundle.children.includes(path[path.length - 1]); } export function validSpreadsheetInstancePath(value: unknown): value is number[] { return Array.isArray(value) && value.length <= 64 && value.every(index => Number.isInteger(index) && index >= 0 && index <= 2147483647); } export function validSpreadsheetViewerSelection( viewer: NativeSpreadsheetViewerNavigation, itemIdentifier: unknown, path: unknown, closure: unknown, ): boolean { if (!viewer.available || !viewer.selection.active || !Number.isInteger(itemIdentifier) || !viewer.items.some(item => item.identifier === itemIdentifier) || !validSpreadsheetBundlePath(path)) return false; if (path.length === 0 && closure === 'NONE') return true; return itemIdentifier === viewer.itemIdentifier && validSpreadsheetBundleSelection(viewer.selection, path, closure); } export function validSpreadsheetRuleValue(property: string, value: unknown): boolean { const numeric = (item: unknown, integral = false) => typeof item === 'number' && Number.isFinite(item) && (integral ? Number.isInteger(item) && item >= -2147483648 && item <= 2147483647 : Math.abs(item) <= 3.4028234663852886e38); if (['enabled', 'show_expanded', 'value_boolean'].includes(property)) return typeof value === 'boolean'; if (property === 'column_name' || property === 'value_string') return typeof value === 'string' && !value.includes('\0') && new TextEncoder().encode(value).byteLength <= (property === 'column_name' ? 63 : 4096); if (property === 'operation') return typeof value === 'string' && ['EQUAL', 'GREATER', 'LESS'].includes(value); if (property === 'value_int') return numeric(value, true); if (property === 'value_float' || property === 'threshold') return numeric(value) && (property !== 'threshold' || (value as number) >= 0); const sizes: Record = { value_float2: 2, value_float3: 3, value_float4: 4, value_color: 4, value_int2: 2, value_int3: 3 }; const size = sizes[property]; return size !== undefined && Array.isArray(value) && value.length === size && value.every(item => numeric(item, property.startsWith('value_int'))); } export type NativeImageViewEdit = { dataId: string; viewport: { width: number; height: number } } & ( | { operation: 'FRAME_ALL' | 'FIT' } | { operation: 'ZOOM_RATIO'; ratio: number } | { operation: 'PAN'; pixelDelta: [number, number] } | { operation: 'ZOOM_IN' | 'ZOOM_OUT'; pixelPosition: [number, number] } | { operation: 'ZOOM_BORDER'; pixelBounds: [number, number, number, number]; zoomOut: boolean } ); export interface NativeAreaLayout { editorType: string; editorTypes: { id: string; label: string }[]; joinTargets: string[]; editable: boolean; screenState: 'NORMAL' | 'MAXIMIZED' | 'FULLSCREEN'; headerMenusVisible?: boolean; } export interface NativeOutlinerRow { id: string; label: string; depth: number; selected: boolean; active: boolean; expanded: boolean; hasChildren: boolean; objectId: string | null; remapId?: string | null } export interface NativeOutlinerRemapID { id: string; name: string; idType: number; library: string; editable: boolean } export interface NativeOutlinerTree { rows: NativeOutlinerRow[]; truncated: boolean; syncSelection: boolean; remapIds?: NativeOutlinerRemapID[] } export interface EditorAreaIR { nativeLayout?: NativeAreaLayout; id: string; editor: EditorTypeIR; regions: EditorRegionIR[]; rect: { x: number; y: number; width: number; height: number }; maximized: boolean; filterText?: string; space?: { outliner?: NativeOutlinerTree; kind: string; dataId?: string | null; view?: NativeViewportState; textSelection?: TextSelectionIR; imageView?: NativeImageView; uvPackTarget?: NativeUVPackTarget; properties?: NativePropertiesContext; spreadsheet?: NativeSpreadsheetContext } } export interface EditorWorkspaceIR { id: string; name: string; screenId?: string; writable?: boolean; areas: EditorAreaIR[]; activeAreaId: string; revision: number } // ACTION operator RNA identifiers; the native caller resolves them against the // registered operator before changing Main. export const TIMELINE_KEY_TYPES = { SET_KEY_INTERPOLATION: ['CONSTANT', 'LINEAR', 'BEZIER', 'SINE', 'QUAD', 'CUBIC', 'QUART', 'QUINT', 'EXPO', 'CIRC', 'BACK', 'BOUNCE', 'ELASTIC'], SET_KEY_EASING: ['AUTO', 'EASE_IN', 'EASE_OUT', 'EASE_IN_OUT'], SET_KEY_HANDLE: ['FREE', 'ALIGNED', 'VECTOR', 'AUTO', 'AUTO_CLAMPED'], SET_KEY_TYPE: ['KEYFRAME', 'BREAKDOWN', 'MOVING_HOLD', 'EXTREME', 'JITTER', 'GENERATED'], SNAP_KEYS: ['CFRA', 'NEAREST_FRAME', 'NEAREST_SECOND', 'NEAREST_MARKER'], MIRROR_KEYS: ['CFRA', 'MARKER', 'XAXIS'], } as const; export type NativeTimelineKeySetting = { [K in keyof typeof TIMELINE_KEY_TYPES]: { operation: K; type: typeof TIMELINE_KEY_TYPES[K][number] } }[keyof typeof TIMELINE_KEY_TYPES]; // Shared with ACTION_OT_paste's original keyframes_general.cc RNA enums. export const TIMELINE_PASTE_OFFSETS = ['START', 'END', 'RELATIVE', 'NONE'] as const; export const TIMELINE_PASTE_MERGES = ['MIX', 'OVER_ALL', 'OVER_RANGE', 'OVER_RANGE_ALL'] as const; export const TIMELINE_KEY_COLUMN_MODES = ['KEYS', 'CFRA', 'MARKERS_COLUMN', 'MARKERS_BETWEEN'] as const; export type NativeTimelineEdit = | { operation: 'SELECT_FRAME_KEYS'; frame: number; extend: boolean; deselect: boolean } | { operation: 'SELECT_KEY_COLUMNS'; mode: typeof TIMELINE_KEY_COLUMN_MODES[number] } | { operation: 'SELECT_KEYS_SIDE'; side: 'LEFT' | 'RIGHT'; extend: boolean } | NativeTimelineKeySetting | { operation: 'COPY_KEYS' } | { operation: 'DUPLICATE_MOVE_KEYS' | 'MOVE_KEYS'; delta: number } | { operation: 'SCALE_KEYS'; factor: number } | { operation: 'PASTE_KEYS'; offset: typeof TIMELINE_PASTE_OFFSETS[number]; merge: typeof TIMELINE_PASTE_MERGES[number]; flipped: boolean } | { operation: 'DELETE_KEYS' | 'JUMP_SELECTED_KEYS' } | { operation: 'FRAME_ALL_KEYS' | 'FRAME_SELECTED_KEYS' | 'CENTER_CURRENT_FRAME' | 'SELECT_ALL_KEYS' | 'DESELECT_ALL_KEYS' | 'INVERT_KEYS' | 'PREVIEW_SELECTED_KEYS' | 'CLEAR_PREVIEW_RANGE' } | { operation: 'PLAY_FORWARD' | 'PLAY_REVERSE' | 'PAUSE_PLAYBACK' | 'CANCEL_PLAYBACK' } | { operation: 'FRAME_OFFSET' | 'MOVE_MARKERS' | 'DUPLICATE_MARKERS'; delta: number } | { operation: 'JUMP_START' | 'JUMP_END' | 'PREVIOUS_KEYFRAME' | 'NEXT_KEYFRAME' | 'PREVIOUS_MARKER' | 'NEXT_MARKER' | 'ADD_MARKER' | 'DELETE_MARKERS' | 'SELECT_ALL_MARKERS' | 'DESELECT_ALL_MARKERS' | 'INVERT_MARKERS' } | { operation: 'RENAME_MARKER'; name: string }; // Playback needs the native WM timer/event pump; synchronous edits use registered storage. export function isNativeTimelinePlayback(edit: NativeTimelineEdit): boolean { return ['PLAY_FORWARD', 'PLAY_REVERSE', 'PAUSE_PLAYBACK', 'CANCEL_PLAYBACK'].includes(edit.operation); } export type NativeView3DNavigation = | { operation: 'FRAME_ALL' | 'FRAME_SELECTED'; allRegions: boolean } | { operation: 'VIEW_AXIS'; axis: 'FRONT' | 'BACK' | 'LEFT' | 'RIGHT' | 'TOP' | 'BOTTOM'; alignActive: boolean } | { operation: 'PAN' | 'ORBIT'; direction: 'LEFT' | 'RIGHT' | 'UP' | 'DOWN' } | { operation: 'ROLL'; direction: 'LEFT' | 'RIGHT' } | { operation: 'ZOOM'; delta: -1 | 1 } | { operation: 'ZOOM_BORDER' } | { operation: 'TOGGLE_PROJECTION' | 'TOGGLE_CAMERA' | 'TOGGLE_LOCAL_VIEW' | 'REMOVE_FROM_LOCAL_VIEW' }; export type NativeView3DSelection = | { operation: 'SELECT_ALL' | 'DESELECT_ALL' | 'INVERT_SELECTION' } | { operation: 'GP_SELECT_MODE'; mode: 'POINT' | 'STROKE' | 'SEGMENT' } | { operation: 'GP_SELECT_PICK'; x: number; y: number; coordinateSpace?: 'WINDOW' | 'REGION'; deselectAll?: boolean } | { operation: 'GP_SELECT_LINKED'; deselect: boolean } | { operation: 'GP_SELECT_ALTERNATE'; deselectEnds: boolean } | { operation: 'GP_SELECT_ENDS'; amountStart: number; amountEnd: number } | { operation: 'GP_SELECT_SIMILAR'; mode: 'LAYER' | 'MATERIAL' | 'VERTEX_COLOR' | 'RADIUS' | 'OPACITY'; threshold: number } | { operation: 'GP_SELECT_RANDOM'; ratio: number; seed: number } | { operation: 'GP_SELECT_FILL' } | { operation: 'GP_SELECT_MATERIAL'; deselect: boolean } | { operation: 'GP_SELECT_BY_STROKE_TYPE'; type: 'STROKE' | 'FILL'; deselect: boolean } | { operation: 'TEXT_SELECT_ALL' | 'TEXT_DESELECT_ALL' | 'TEXT_SELECT_WORD' | 'TEXT_SELECT_POINTER' } | { operation: 'TEXT_MOVE'; extend: boolean; type: 'LINE_BEGIN' | 'LINE_END' | 'TEXT_BEGIN' | 'TEXT_END' | 'PREVIOUS_CHARACTER' | 'NEXT_CHARACTER' | 'PREVIOUS_WORD' | 'NEXT_WORD' | 'PREVIOUS_LINE' | 'NEXT_LINE' | 'PREVIOUS_PAGE' | 'NEXT_PAGE' } | { operation: 'SET_PAINT_MASK'; domain: 'NONE' | 'FACE' | 'VERTEX' } | { operation: 'PAINT_HIDE'; unselected: boolean } | { operation: 'PAINT_REVEAL'; select: boolean } | { operation: 'PAINT_SELECT_UNGROUPED'; extend: boolean } // WINDOW uses top-left canvas backing pixels; omitted/REGION uses native mval. | { operation: 'SELECT_LINKED_PICK'; select: boolean; x: number; y: number; coordinateSpace?: 'WINDOW' | 'REGION' } | { operation: 'SELECT_NEXT_ACTIVE' | 'SELECT_PREVIOUS_ACTIVE' } | { operation: 'SELECT_EDGE_LOOPS'; delimit?: ('SEAM' | 'SHARP' | 'NGONS' | 'INNER_CORNERS' | 'OUTER_CORNERS')[] } | { operation: 'SELECT_EDGE_RINGS'; delimit?: ('SEAM' | 'SHARP' | 'MATERIAL' | 'NGONS')[] } | { operation: 'SELECT_BOUNDARY_LOOPS'; extend: boolean; delimit?: ('SEAM' | 'SHARP' | 'NGONS' | 'INNER_CORNERS' | 'OUTER_CORNERS')[] } | { operation: 'LOOP_TO_REGION'; selectBigger: boolean } | { operation: 'REGION_TO_LOOP' } | { operation: 'SELECT_SHORTEST_PATH'; useFaceStep: boolean; useTopologyDistance: boolean; useFill: boolean; edgeMode?: 'SELECT' | 'SEAM' | 'SHARP' | 'CREASE' | 'BEVEL' | 'FREESTYLE'; skip: number; nth: number; offset: number } | { operation: 'SELECT_LINKED_FLAT_FACES'; sharpnessDegrees: number } | { operation: 'SELECT_MORE' | 'SELECT_LESS'; useFaceStep: boolean } | { operation: 'SELECT_HIERARCHY'; direction: 'PARENT' | 'CHILD'; extend: boolean } | { operation: 'SELECT_LINKED'; objectType: 'OBDATA' | 'MATERIAL' | 'DUPGROUP' | 'PARTICLE' | 'LIBRARY' | 'LIBRARY_OBDATA'; extend: boolean; delimit?: ('NORMAL' | 'MATERIAL' | 'SEAM' | 'SHARP' | 'UV')[] } | { operation: 'BOX' | 'CIRCLE' } | { operation: 'LASSO'; mode: 'SET' | 'ADD' | 'SUB' | 'XOR' | 'AND' }; export type EditorContextEdit = { workspaceId: string; screenId: string } & ( | { action: "SWITCH_WORKSPACE" } | { action: "SET_ACTIVE_AREA"; areaId: string } | ({ action: 'TIMELINE_EDIT'; areaId: string } & NativeTimelineEdit) | ({ action: 'VIEW3D_NAVIGATE'; areaId: string; regionId: string } & NativeView3DNavigation) | ({ action: 'VIEW3D_SELECT'; areaId: string; regionId: string } & NativeView3DSelection) | { action: 'OUTLINER_REFRESH'; areaId: string } | { action: 'OUTLINER_TOGGLE_EXPAND'; areaId: string; rowId: string; toggleAll: boolean } | { action: 'OUTLINER_TREE_SELECT'; areaId: string; rowId: string; extend: boolean; range: boolean; activateData?: boolean; activateProperties?: boolean; recursive?: boolean } | { action: 'OUTLINER_REMAP'; areaId: string; rowId: string; oldId: string; newId: string } | { action: 'OUTLINER_SELECT'; areaId: string; objectId: string; extend: boolean; range: boolean } | { action: 'OUTLINER_SELECT_ALL'; areaId: string; operation: 'SELECT' | 'DESELECT' | 'INVERT' | 'TOGGLE' } | { action: "SET_EDITOR_TYPE"; areaId: string; editorType: string } | { action: "SPLIT_AREA"; areaId: string; direction: 'HORIZONTAL' | 'VERTICAL'; factor: number } | { action: "JOIN_AREA"; areaId: string; targetAreaId: string } | { action: "SWAP_AREAS"; areaId: string; targetAreaId: string } | { action: "MOVE_AREA_EDGE"; areaId: string; targetAreaId: string; delta: number; snap: boolean } | { action: "TOGGLE_MAXIMIZE_AREA"; areaId: string } | { action: "TOGGLE_FULLSCREEN_AREA"; areaId: string } | { action: "TOGGLE_QUAD_VIEW" | "SET_ACTIVE_REGION"; areaId: string; regionId: string } | { action: "TOGGLE_HEADER_MENUS"; areaId: string } | { action: "RESIZE_REGION"; areaId: string; regionId: string; size: number } | { action: "SET_TEXT" | "SET_IMAGE"; areaId: string; dataId: string | null } | ({ action: "TEXT_EDIT"; areaId: string } & NativeTextEdit) | ({ action: "IMAGE_VIEW"; areaId: string } & NativeImageViewEdit) | { action: "NLA_EDIT"; areaId: string; operation: 'SELECT_ALL' | 'DESELECT_ALL' | 'INVERT_SELECTION' | 'TOGGLE_MUTE' | 'TRANSITION_ADD' | 'META_ADD' | 'META_REMOVE' | 'TIME_WARP_ENABLE' | 'TIME_WARP_DISABLE' | 'TIME_WARP_CYCLIC_ENABLE' | 'TIME_WARP_CYCLIC_DISABLE' | 'DELETE_STRIP_TIME_KEY' | 'INFLUENCE_ENABLE' | 'INFLUENCE_DISABLE' | 'DELETE_INFLUENCE_KEY' | 'TRACK_SELECT_ALL' | 'TRACK_DESELECT_ALL' | 'TRACK_INVERT_SELECTION' | 'TRACK_ADD' | 'TRACK_ADD_ABOVE' | 'TRACK_DELETE' | 'TRACK_MOVE_TOP' | 'TRACK_MOVE_UP' | 'TRACK_MOVE_DOWN' | 'TRACK_MOVE_BOTTOM' | 'DUPLICATE' | 'DUPLICATE_LINKED' | 'DELETE' | 'SPLIT' | 'SNAP_CURRENT_FRAME' | 'SYNC_ACTION_LENGTH' | 'SYNC_SELECTED_ACTION_LENGTHS' } | { action: "NLA_EDIT"; areaId: string; operation: 'KEY_STRIP_TIME'; stripTime: number } | { action: "NLA_EDIT"; areaId: string; operation: 'KEY_INFLUENCE'; influence: number } | { action: "NLA_EDIT"; areaId: string; operation: 'TWEAK_ENTER'; isolateAction: boolean; useUpperStack: boolean } | { action: "NLA_EDIT"; areaId: string; operation: 'TWEAK_EXIT'; clearSolo: boolean } | ({ action: "UV_EDIT"; areaId: string } & NativeUVEdit) | { action: "SEQUENCER_EDIT"; areaId: string; operation: 'SELECT_ALL' | 'DESELECT_ALL' | 'INVERT_SELECTION' | 'MUTE' | 'UNMUTE' } | { action: "NODE_EDIT"; areaId: string; operation: 'SELECT_ALL' | 'DESELECT_ALL' | 'INVERT_SELECTION' | 'TOGGLE_MUTE' | 'DELETE' } | { action: "GRAPH_EDIT"; areaId: string; operation: 'SELECT_ALL' | 'DESELECT_ALL' | 'INVERT_SELECTION' | 'DELETE' } | { action: "GRAPH_EDIT"; areaId: string; operation: 'SET_INTERPOLATION'; interpolation: GraphInterpolation } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'SET_CONTEXT'; context: string } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'VIEW_LAYER_OPERATION'; method: 'SELECT' | 'ADD' | 'REMOVE'; target?: string; expectedLayers: string[]; expectedActive: string } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'SET_PIN'; pinned: boolean; expectedPinId?: string | null } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'SET_OUTLINER_SYNC'; value: string } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'SET_FRAME_RATE'; context: 'OUTPUT' | 'RENDER'; dataId: string; fps: number; base: number; expectedFps: number; expectedBase: number } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'SET_TIME_REMAP'; context: 'OUTPUT'; dataId: string; old: number; new: number; expectedOld: number; expectedNew: number } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'LIGHTGROUP_SYNC'; context: 'VIEW_LAYER'; dataId: string; layerName: string; method: 'ADD_USED' | 'REMOVE_UNUSED'; expectedGroups: string[] } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'LIGHTGROUP_RENAME'; context: 'VIEW_LAYER'; dataId: string; layerName: string; expectedGroups: string[]; renames: Array<{ from: string; to: string }> } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'RENDER_VIEW_OPERATION'; context: 'OUTPUT'; dataId: string; method: 'ADD' | 'REMOVE'; expectedViews: string[]; expectedActiveIndex: number } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'SET_CONTEXT_PROPERTY'; context: string; dataId: string; property: string; value: PropertiesDataValue; expectedValue?: PropertiesDataValue; expectedCacheFileId?: string; expectedShapeObjectId?: string; expectedKeyId?: string; expectedActiveUid?: number; shapeRowUid?: number } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'CAMERA_PRESET'; context: 'DATA'; dataId: string; method: 'ADD'; name: string; useFocalLength: boolean } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'CAMERA_PRESET'; context: 'DATA'; dataId: string; method: 'REMOVE'; path: string } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'CAMERA_CUSTOM_UPDATE'; context: 'DATA'; dataId: string } // SELECT_RANGE extends selection from expectedActiveUid without moving the anchor. | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'SHAPE_KEY_OPERATION'; objectId: string; expectedKeyId: string; expectedActiveUid: number; method: string; uid?: number; expectedOrder?: number[]; expectedSelection?: number[]; dropLocation?: 'BEFORE' | 'AFTER'; expectedSources?: string[]; transferMode?: 'OFFSET' | 'RELATIVE_FACE' | 'RELATIVE_EDGE'; useClamp?: boolean } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'RELOAD_CACHE'; context: string; dataId: string; expectedCacheFileId: string } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'CACHE_OPEN'; dataId: string; expectedCacheFileId: string; path: string } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'CACHE_LAYER'; dataId: string; expectedCacheFileId: string; expectedLayers: Array<{ filepath: string; hidden: boolean }>; expectedActiveLayer: number; method: 'ADD' | 'SELECT' | 'REMOVE' | 'MOVE_UP' | 'MOVE_DOWN' | 'SET_PATH' | 'SET_HIDE'; index: number; value?: string | boolean } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'CONSTRAINT_OPERATION'; context: string; dataId: string; operator: string; parameters?: Record } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'EDIT_ARMATURE_TARGET'; dataId: string; method: 'ADD' | 'REMOVE' | 'CLEAR' | 'MOVE' | 'SET'; expectedTargets: NativeArmatureConstraintTarget[]; index?: number; step?: -1 | 1; target?: NativeArmatureConstraintTarget } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'SET_ACTIVE_MODIFIER'; objectId: string; modifierUuid: string | null; expectedModifierUuid?: string | null } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'MODIFIER_BIND'; objectId: string; modifierUuid: string; expectedBound: boolean } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'DUPLICATE_MODIFIER'; objectId: string; modifierUuid: string } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'APPLY_MODIFIER'; objectId: string; modifierUuid: string; singleUser: boolean; mergeCustomData: boolean; allKeyframes?: boolean } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'APPLY_MODIFIER_AS_SHAPE_KEY'; objectId: string; modifierUuid: string; keepModifier: boolean } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'SHAPE_KEY_MIRROR'; objectId: string; keyId: string; activeUid: number; useTopology: boolean } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'COPY_MODIFIER_TO_SELECTED'; objectId: string; modifierUuid: string; expectedTargets: string[] } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'HOOK_OPERATION'; objectId: string; modifierUuid: string; operator: 'RESET' | 'RECENTER' | 'SELECT' | 'ASSIGN' } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'SKIN_OPERATION'; objectId: string; modifierUuid: string; operator: 'CREATE_ARMATURE' | 'ADD_SKIN_DATA' | 'MARK_LOOSE' | 'CLEAR_LOOSE' | 'MARK_ROOT' | 'EQUALIZE_RADII' } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'DATA_LAYOUT_OPERATION'; objectId: string; modifierUuid: string; operator: 'GENERATE_DATA_LAYERS' } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'MULTIRES_OPERATION'; objectId: string; modifierUuid: string; operator: 'DELETE_HIGHER_LEVELS' | 'SUBDIVIDE' | 'RESHAPE' | 'BASE_APPLY' | 'UNSUBDIVIDE' | 'REBUILD_SUBDIV' | 'EXTERNAL_SAVE' | 'EXTERNAL_PACK'; path?: string } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'SET_ACTIVE_CONSTRAINT'; objectId: string; constraintName: string | null; expectedConstraintName?: string | null } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'SET_ACTIVE_POSE_CONSTRAINT'; objectId: string; boneName: string; constraintName: string | null; expectedBoneName?: string | null; expectedConstraintName?: string | null } | { action: 'PROPERTIES_EDIT'; areaId: string; operation: 'SET_OBJECT_PROPERTY'; objectId: string; property: PropertiesObjectField; value: boolean | string; expectedValue?: boolean | string; expectedParentId?: string | null } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'SET_OBJECT_TRANSFORM'; objectId: string; property: PropertiesObjectTransform; index: number; value: number } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'SET_OBJECT_LOCK'; objectId: string; property: PropertiesObjectLock; index: number; value: boolean } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'SET_OBJECT_PARENT'; objectId: string; expectedParentId: string | null; parentId: string; keepTransform: boolean } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'CLEAR_OBJECT_PARENT'; objectId: string; expectedParentId: string; mode: 'CLEAR' | 'CLEAR_KEEP_TRANSFORM' | 'CLEAR_INVERSE' } | { action: "PROPERTIES_EDIT"; areaId: string; operation: 'SET_OBJECT_PARENT_TYPE' | 'SET_OBJECT_PARENT_BONE'; objectId: string; expectedParentId: string; expectedParentType: string | null; expectedParentBone: string; value: string } | { action: 'PROPERTIES_EDIT'; areaId: string; operation: 'SET_OBJECT_PARENT_SETTING'; objectId: string; expectedParentId: string; expectedParentType: string | null; expectedParentBone: string; property: PropertiesParentSetting; index: number; expectedValue: number | boolean; value: number | boolean } | ({ action: 'SPREADSHEET_EDIT'; areaId: string } & NativeSpreadsheetEdit) | { action: "SET_VIEW"; areaId: string; view: NativeViewportState } | { action: "TOGGLE_REGION"; areaId: string; regionId: string; visible: boolean } | { action: "FLIP_REGION"; areaId: string; regionId: string } ); export interface EditorContextIR { workspaceId: string; activeAreaId: string; activeEditor: EditorTypeIR; mode: EditorMode; activeObjectId: string | null; selection: string[]; viewLayer: string; pinnedData: string | null; revision: number } export interface KeymapBindingIR { id: string; key: string; modifiers: string[]; command: string; enabled: boolean; workspaceIds?: string[]; editors?: EditorTypeIR[]; modes?: EditorMode[]; } // Native directed UV edge: start/end coordinates, start selection, edge selection, start pin. export type NativeUVEdge = [number, number, number, number, boolean, boolean, boolean]; export interface NativeUVSelection { objects: Array<{ objectId: string; uvMap: string; edges: NativeUVEdge[] }>; syncSelection: boolean; truncated: boolean; selectionMode?: typeof UV_SELECTION_MODES[number]; meshSelectMode?: number; islandSelection?: boolean; stickyMode?: typeof UV_STICKY_MODES[number]; customRegion?: NativeUVCustomRegion } export interface EditorWorkflowIR { schemaVersion: typeof EDITOR_WORKFLOW_SCHEMA; workspaces: EditorWorkspaceIR[]; context: EditorContextIR; keymaps: KeymapBindingIR[]; uvSelection?: NativeUVSelection } export interface KeyChordIR { key: string; modifiers: Array<"ALT" | "CTRL" | "META" | "SHIFT"> } export type EditorWorkflowEditIR = | { type: "SWITCH_WORKSPACE"; revision: number; workspaceId: string } | { type: "SET_ACTIVE_AREA"; revision: number; areaId: string } | { type: "SET_SELECTION"; revision: number; selectedIds: string[]; activeObjectId: string | null } | { type: "TOGGLE_REGION"; revision: number; areaId: string; regionId: string; visible: boolean }; export class EditorWorkflowValidationError extends Error { readonly code: ErrorCode; constructor(code: ErrorCode, message: string) { super(`${code}: ${message}`); this.name = "EditorWorkflowValidationError"; this.code = code; } } const EDITOR_REGION_KINDS = new Set(["HEADER", "MAIN", "TOOLBAR", "SIDEBAR", "FOOTER", "OTHER"]); function record(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function text(value: unknown, name: string, maximum = 256): string { if (typeof value !== "string" || value.length === 0 || value.length > maximum) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${name} is invalid`); return value; } function finite(value: unknown, name: string, minimum: number, maximum: number): number { if (typeof value !== "number" || !Number.isFinite(value) || value < minimum || value > maximum) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${name} is outside the bounded range`); return value; } function integer(value: unknown, name: string, minimum: number, maximum: number): number { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${name} is outside the bounded range`); return value; } function parseSpreadsheetBundleNavigation(bundle: unknown): NativeSpreadsheetBundleNavigation { if (!record(bundle) || !validSpreadsheetBundlePath(bundle.path) || typeof bundle.active !== 'boolean' || typeof bundle.available !== 'boolean' || typeof bundle.pathValid !== 'boolean' || typeof bundle.hasInputs !== 'boolean' || typeof bundle.hasOutputs !== 'boolean' || typeof bundle.childrenTruncated !== 'boolean' || typeof bundle.childrenPage !== 'number' || typeof bundle.childrenPageCount !== 'number' || !Number.isSafeInteger(bundle.childrenPage) || !Number.isSafeInteger(bundle.childrenPageCount) || bundle.childrenPage < 0 || bundle.childrenPageCount < 0 || bundle.childrenPage > 4095 || bundle.childrenPageCount > 4096 || !['NONE', 'INPUT', 'OUTPUT'].includes(bundle.closure as string) || !Array.isArray(bundle.children) || bundle.children.length > 256 || bundle.children.some(key => !validSpreadsheetBundlePath([key])) || new Set(bundle.children).size !== bundle.children.length || (bundle.pathValid && !bundle.available) || (!bundle.available && (bundle.path.length > 0 || bundle.children.length > 0)) || (bundle.childrenPageCount === 0 && bundle.childrenPage !== 0) || (bundle.childrenPageCount > 0 && bundle.childrenPage >= bundle.childrenPageCount) || (bundle.pathValid && bundle.closure === 'INPUT' && !bundle.hasInputs) || (bundle.pathValid && bundle.closure === 'OUTPUT' && !bundle.hasOutputs)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Spreadsheet bundle navigation'); } return { active: bundle.active, available: bundle.available, path: [...bundle.path], pathValid: bundle.pathValid, closure: bundle.closure as SpreadsheetClosure, hasInputs: bundle.hasInputs, hasOutputs: bundle.hasOutputs, children: [...bundle.children] as string[], childrenTruncated: bundle.childrenTruncated, childrenPage: bundle.childrenPage, childrenPageCount: bundle.childrenPageCount, }; } function rect(value: unknown, name: string): EditorAreaIR["rect"] { if (!record(value)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${name} is invalid`); const next = { x: finite(value.x, `${name}.x`, 0, 1), y: finite(value.y, `${name}.y`, 0, 1), width: finite(value.width, `${name}.width`, 0.0001, 1), height: finite(value.height, `${name}.height`, 0.0001, 1) }; if (next.x + next.width > 1 || next.y + next.height > 1) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${name} exceeds workspace bounds`); return next; } function overlap(a: EditorAreaIR["rect"], b: EditorAreaIR["rect"]): boolean { return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y; } function scopeOverlaps(left: readonly T[] | undefined, right: readonly T[] | undefined): boolean { return !left?.length || !right?.length || left.some((value) => right.includes(value)); } function keymapScopesOverlap(left: KeymapBindingIR, right: KeymapBindingIR): boolean { return scopeOverlaps(left.workspaceIds, right.workspaceIds) && scopeOverlaps(left.editors, right.editors) && scopeOverlaps(left.modes, right.modes); } export function parseNativeUVTransform(value: unknown): NativeUVTransform { if (!record(value) || !['TRANSLATE','ROTATE','SCALE'].includes(value.mode as string)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'UV transform mode is invalid'); const number = (item: unknown) => typeof item === 'number' && Number.isFinite(item) && Number.isFinite(Math.fround(item)); const vector = (item: unknown) => Array.isArray(item) && item.length === 2 && item.every(number); const allowed = value.mode === 'ROTATE' ? ['mode','angle','center'] : value.mode === 'SCALE' ? ['mode','value','center'] : ['mode','value']; if (Object.keys(value).some(key => !allowed.includes(key)) || (value.mode === 'ROTATE' ? !number(value.angle) : !vector(value.value)) || (value.center !== undefined && !vector(value.center))) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'UV transform requires finite native values and a valid center'); return value as unknown as NativeUVTransform; } export function parseNativeUVStitchOptions(value: unknown): NativeUVStitchOptions { if (!record(value) || Object.keys(value).length > 8) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'UV Stitch options must be a bounded object'); for (const [key,option] of Object.entries(value)) { const valid = (UV_STITCH_BOOLEANS as readonly string[]).includes(key) ? typeof option === 'boolean' : key === 'mode' ? option === 'VERTEX' || option === 'EDGE' : key === 'limit' ? typeof option === 'number' && Number.isFinite(option) && Number.isFinite(Math.fround(option)) && option >= 0 : key === 'static_island' || key === 'active_object_index' ? typeof option === 'number' && Number.isSafeInteger(option) && option >= 0 && option <= 0x7fffffff : false; if (!valid) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `Invalid UV Stitch option: ${key}`); } return {...value} as NativeUVStitchOptions; } export function parseNativeUVUnwrapOptions(value: unknown): NativeUVUnwrapOptions { if (!record(value) || Object.keys(value).length > 12) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'UV Unwrap options must be a bounded object'); for (const [key, option] of Object.entries(value)) { let valid = false; if ((UV_UNWRAP_BOOLEANS as readonly string[]).includes(key)) valid = typeof option === 'boolean'; else if (key === 'method') valid = typeof option === 'string' && (UV_UNWRAP_METHODS as readonly string[]).includes(option); else if (key === 'margin_method') valid = typeof option === 'string' && (UV_UNWRAP_MARGIN_METHODS as readonly string[]).includes(option); else if (key === 'margin') valid = typeof option === 'number' && Number.isFinite(option) && option >= 0 && option <= 1; else if (key === 'iterations') valid = typeof option === 'number' && Number.isSafeInteger(option) && option >= 0 && option <= 10000; else if (key === 'weight_factor') valid = typeof option === 'number' && Number.isFinite(option) && Math.abs(option) <= 10000; else if (key === 'weight_group') valid = typeof option === 'string' && !option.includes('\0') && new TextEncoder().encode(option).byteLength < 64; if (!valid) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `Invalid UV Unwrap option: ${key}`); } return { ...value } as NativeUVUnwrapOptions; } function parseUVSelection(value: unknown): NativeUVSelection | undefined { if (value === undefined) return undefined; if (!record(value) || !Array.isArray(value.objects) || value.objects.length > 128 || typeof value.syncSelection !== 'boolean' || typeof value.truncated !== 'boolean') throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Native UV selection metadata is invalid'); if ((value.selectionMode !== undefined && !UV_SELECTION_MODES.includes(value.selectionMode as typeof UV_SELECTION_MODES[number])) || (value.meshSelectMode !== undefined && (typeof value.meshSelectMode !== 'number' || !Number.isInteger(value.meshSelectMode) || value.meshSelectMode < 1 || value.meshSelectMode > 7)) || (value.islandSelection !== undefined && typeof value.islandSelection !== 'boolean')) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Native UV selection settings are invalid'); let edgeCount = 0; let customRegion: NativeUVCustomRegion | undefined; if (value.customRegion !== undefined) { if (!record(value.customRegion) || typeof value.customRegion.enabled !== 'boolean') throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Native UV custom region is invalid'); customRegion = {enabled:value.customRegion.enabled,bounds:parseNativeUVBounds(value.customRegion.bounds,false)}; } if (value.stickyMode !== undefined && !UV_STICKY_MODES.includes(value.stickyMode as typeof UV_STICKY_MODES[number])) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Native UV sticky selection mode is invalid'); const ids = new Set(), objects: NativeUVSelection['objects'] = []; for (const item of value.objects) { if (!record(item) || !Array.isArray(item.edges) || (edgeCount += item.edges.length) > 16384) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_BUDGET_EXCEEDED', 'Native UV edges exceed the budget or have invalid rows'); const objectId = text(item.objectId, 'uvSelection.objectId'), uvMap = text(item.uvMap, 'uvSelection.uvMap'); if (ids.has(objectId)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Duplicate native UV object'); ids.add(objectId); const edges: NativeUVEdge[] = []; for (const edge of item.edges) { if (!Array.isArray(edge) || edge.length !== 7 || edge.slice(0,4).some(n => typeof n !== 'number' || !Number.isFinite(n)) || edge.slice(4).some(flag => typeof flag !== 'boolean')) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Native UV edge tuple is invalid'); edges.push([...edge] as NativeUVEdge); } objects.push({ objectId, uvMap, edges }); } return { objects, syncSelection: value.syncSelection, truncated: value.truncated, selectionMode: value.selectionMode as NativeUVSelection['selectionMode'], meshSelectMode: value.meshSelectMode as number | undefined, islandSelection: value.islandSelection as boolean | undefined, stickyMode: value.stickyMode as NativeUVSelection['stickyMode'], customRegion }; } export function parseEditorWorkflow(value: unknown): EditorWorkflowIR { if (!record(value) || value.schemaVersion !== EDITOR_WORKFLOW_SCHEMA || !Array.isArray(value.workspaces) || !record(value.context) || !Array.isArray(value.keymaps)) throw new EditorWorkflowValidationError("PROTOCOL_MISMATCH", "Unsupported editor workflow schema"); if (value.workspaces.length > EDITOR_WORKFLOW_BUDGET.maxWorkspaces || value.keymaps.length > EDITOR_WORKFLOW_BUDGET.maxKeymaps) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_BUDGET_EXCEEDED", "Editor workflow exceeds the budget"); let areaCount = 0; let regionCount = 0; const workspaceIds = new Set(); const workspaces = value.workspaces.map((workspaceValue, workspaceIndex): EditorWorkspaceIR => { const name = `workspaces[${workspaceIndex}]`; if (!record(workspaceValue) || !Array.isArray(workspaceValue.areas)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${name} is invalid`); const id = text(workspaceValue.id, `${name}.id`); if (workspaceIds.has(id)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `Duplicate workspace ${id}`); workspaceIds.add(id); areaCount += workspaceValue.areas.length; if (areaCount > EDITOR_WORKFLOW_BUDGET.maxAreas) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_BUDGET_EXCEEDED", "Area count exceeds the budget"); const areaIds = new Set(); const areas = workspaceValue.areas.map((areaValue, areaIndex): EditorAreaIR => { const areaName = `${name}.areas[${areaIndex}]`; if (!record(areaValue) || !EDITOR_TYPES.includes(areaValue.editor as EditorTypeIR) || !Array.isArray(areaValue.regions)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${areaName} is invalid`); const areaId = text(areaValue.id, `${areaName}.id`); if (areaIds.has(areaId)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `Duplicate area ${areaId}`); areaIds.add(areaId); regionCount += areaValue.regions.length; if (regionCount > EDITOR_WORKFLOW_BUDGET.maxRegions) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_BUDGET_EXCEEDED", "Region count exceeds the budget"); const regionIds = new Set(); const regions = areaValue.regions.map((regionValue, regionIndex): EditorRegionIR => { const regionName = `${areaName}.regions[${regionIndex}]`; if (!record(regionValue) || !EDITOR_REGION_KINDS.has(regionValue.kind as EditorRegionKind)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${regionName} is invalid`); const regionId = text(regionValue.id, `${regionName}.id`); if (regionIds.has(regionId)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `Duplicate region ${regionId}`); regionIds.add(regionId); const alignment = regionValue.alignment; if (alignment !== undefined && !['TOP', 'BOTTOM', 'LEFT', 'RIGHT', 'OTHER'].includes(alignment as string)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `${regionName}.alignment is invalid`); return { id: regionId, kind: regionValue.kind as EditorRegionKind, visible: regionValue.visible !== false, active: regionValue.active === true, quadView: regionValue.quadView === true, toggleable: regionValue.toggleable === true, flippable: regionValue.flippable === true, alignment: alignment as EditorRegionIR['alignment'], }; }); let nativeLayout: NativeAreaLayout | undefined; if (areaValue.nativeLayout !== undefined) { const layout = areaValue.nativeLayout; if (!record(layout) || typeof layout.editorType !== 'string' || layout.editorType.length > 128 || !Array.isArray(layout.editorTypes) || layout.editorTypes.length > 128 || !Array.isArray(layout.joinTargets) || layout.joinTargets.length > EDITOR_WORKFLOW_BUDGET.maxAreas || typeof layout.editable !== 'boolean' || (layout.screenState !== undefined && !['NORMAL', 'MAXIMIZED', 'FULLSCREEN'].includes(layout.screenState as string)) || (layout.headerMenusVisible !== undefined && typeof layout.headerMenusVisible !== 'boolean')) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `${areaName}.nativeLayout is invalid`); } nativeLayout = { editorType: layout.editorType, editorTypes: layout.editorTypes.map((item, index) => { if (!record(item)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid native editor choice'); return { id: text(item.id, `${areaName}.editorTypes[${index}].id`, 128), label: text(item.label, `${areaName}.editorTypes[${index}].label`, 256) }; }), joinTargets: layout.joinTargets.map(item => text(item, `${areaName}.joinTarget`)), editable: layout.editable, screenState: layout.screenState as NativeAreaLayout['screenState'] ?? 'NORMAL', headerMenusVisible: layout.headerMenusVisible as boolean | undefined, }; } const filterText = areaValue.filterText; if (filterText !== undefined && (typeof filterText !== "string" || filterText.length > 64)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${areaName}.filterText is invalid`); let space: EditorAreaIR['space']; if (areaValue.space !== undefined) { if (!record(areaValue.space)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `${areaName}.space is invalid`); space = { kind: text(areaValue.space.kind, `${areaName}.space.kind`, 64) }; if (areaValue.space.dataId !== undefined) space.dataId = areaValue.space.dataId === null ? null : text(areaValue.space.dataId, `${areaName}.space.dataId`); if (areaValue.space.outliner !== undefined) { const tree = areaValue.space.outliner; if (areaValue.editor !== 'OUTLINER' || space.kind !== 'OUTLINER' || !record(tree) || !Array.isArray(tree.rows) || tree.rows.length > 65536 || typeof tree.truncated !== 'boolean' || typeof tree.syncSelection !== 'boolean') { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid native Outliner tree'); } const rowIds = new Set(); const rows = tree.rows.map((row): NativeOutlinerRow => { if (!record(row) || !Number.isInteger(row.depth) || Number(row.depth) < 0 || Number(row.depth) > 64 || typeof row.label !== 'string' || row.label.length > 65536 || ['selected', 'active', 'expanded', 'hasChildren'].some(key => typeof row[key] !== 'boolean')) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid native Outliner row'); } const id = text(row.id, 'outliner.row.id', 65536); if (rowIds.has(id)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Duplicate native Outliner row'); rowIds.add(id); return { id, label: row.label, depth: Number(row.depth), selected: row.selected as boolean, active: row.active as boolean, expanded: row.expanded as boolean, hasChildren: row.hasChildren as boolean, objectId: row.objectId === null ? null : text(row.objectId, 'outliner.row.objectId'), remapId: row.remapId == null ? null : text(row.remapId, 'outliner.row.remapId', 65536) }; }); let remapIds: NativeOutlinerRemapID[] | undefined; if (tree.remapIds !== undefined) { if (!Array.isArray(tree.remapIds)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Remap IDs'); remapIds = tree.remapIds.map(value => { if (!record(value) || !Number.isInteger(value.idType) || typeof value.editable !== 'boolean' || typeof value.library !== 'string' || value.library.length > 65536) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Remap ID'); } return { id: text(value.id, 'outliner.remap.id', 65536), name: text(value.name, 'outliner.remap.name', 65536), idType: Number(value.idType), library: value.library, editable: value.editable }; }); } space.outliner = { rows, truncated: tree.truncated, syncSelection: tree.syncSelection, remapIds }; } if (areaValue.space.spreadsheet !== undefined) { const spreadsheet = areaValue.space.spreadsheet; if (areaValue.editor !== 'SPREADSHEET' || space.kind !== 'SPREADSHEET' || !record(spreadsheet) || typeof spreadsheet.useFilter !== 'boolean' || !Array.isArray(spreadsheet.rules)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `${areaName}.spreadsheet is invalid`); } const fields = ['enabled', 'show_expanded', 'column_name', 'operation', 'value_float', 'value_int', 'value_float2', 'value_float3', 'value_float4', 'value_color', 'value_int2', 'value_int3', 'value_string', 'value_boolean', 'threshold'] as const; const rules = spreadsheet.rules.map((rule, index) => { if (!record(rule) || fields.some(field => !validSpreadsheetRuleValue(field, rule[field]))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `${areaName}.rules[${index}] is invalid`); } return Object.fromEntries(fields.map(field => [field, Array.isArray(rule[field]) ? [...(rule[field] as number[])] : rule[field]])) as unknown as NativeSpreadsheetRule; }); space.spreadsheet = { useFilter: spreadsheet.useFilter, rules }; if (spreadsheet.instances !== undefined) { const instances = spreadsheet.instances; if (!record(instances) || !validSpreadsheetInstancePath(instances.path) || !Array.isArray(instances.breadcrumbs) || instances.breadcrumbs.length !== instances.path.length + 1 || typeof instances.geometryAvailable !== 'boolean' || typeof instances.pathValid !== 'boolean' || typeof instances.childrenTruncated !== 'boolean' || typeof instances.childrenPage !== 'number' || typeof instances.childrenPageCount !== 'number' || !Number.isSafeInteger(instances.childrenPage) || !Number.isSafeInteger(instances.childrenPageCount) || instances.childrenPage < 0 || instances.childrenPageCount < 0 || instances.childrenPage > 4095 || instances.childrenPageCount > 4096 || !Array.isArray(instances.children) || instances.children.length > 256 || (instances.pathValid && !instances.geometryAvailable) || (!instances.geometryAvailable && instances.path.length !== 0) || (!instances.pathValid && (instances.children.length !== 0 || instances.childrenTruncated || instances.childrenPage !== 0 || instances.childrenPageCount !== 0)) || (instances.childrenPageCount === 0 && instances.childrenPage !== 0) || (instances.childrenPageCount > 0 && instances.childrenPage >= instances.childrenPageCount) || instances.childrenTruncated !== (instances.childrenPageCount > 1)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Spreadsheet instance navigation'); } const children = instances.children.map(child => { if (!record(child) || !validSpreadsheetInstancePath([child.referenceIndex]) || typeof child.userCount !== 'number' || !Number.isInteger(child.userCount) || child.userCount < 0 || child.userCount > 2147483647) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Spreadsheet instance reference'); } return { referenceIndex: child.referenceIndex as number, label: text(child.label, 'Spreadsheet instance name', 4096), userCount: child.userCount }; }); if (new Set(children.map(child => child.referenceIndex)).size !== children.length) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Duplicate Spreadsheet instance reference'); } space.spreadsheet.instances = { path: [...instances.path], breadcrumbs: instances.breadcrumbs.map(name => text(name, 'Spreadsheet instance name', 4096)), geometryAvailable: instances.geometryAvailable, pathValid: instances.pathValid, children, childrenTruncated: instances.childrenTruncated, childrenPage: instances.childrenPage, childrenPageCount: instances.childrenPageCount, }; } if (spreadsheet.bundle !== undefined) { space.spreadsheet.bundle = parseSpreadsheetBundleNavigation(spreadsheet.bundle); } if (spreadsheet.viewer !== undefined) { const viewer = spreadsheet.viewer; if (!record(viewer) || typeof viewer.available !== 'boolean' || typeof viewer.itemsPage !== 'number' || typeof viewer.itemsPageCount !== 'number' || typeof viewer.itemsTruncated !== 'boolean' || !Array.isArray(viewer.items) || viewer.items.length > 256 || !Number.isSafeInteger(viewer.itemsPage) || !Number.isSafeInteger(viewer.itemsPageCount) || viewer.itemsPage < 0 || viewer.itemsPageCount < 0 || viewer.itemsPage > 4095 || viewer.itemsPageCount > 4096 || (viewer.itemsPageCount === 0 && viewer.itemsPage !== 0) || (viewer.itemsPageCount > 0 && viewer.itemsPage >= viewer.itemsPageCount) || (!viewer.available && (viewer.items.length > 0 || viewer.itemsTruncated || viewer.itemsPageCount !== 0))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Spreadsheet Viewer outputs'); } const itemIdentifier = integer(viewer.itemIdentifier, 'Viewer output identifier', -2147483648, 2147483647); const items = viewer.items.map(item => { if (!record(item) || typeof item.label !== 'string' || item.label.length > 4096 || item.label.includes('\0')) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Spreadsheet Viewer output'); } return { identifier: integer(item.identifier, 'Viewer output identifier', -2147483648, 2147483647), label: item.label }; }); const selection = parseSpreadsheetBundleNavigation(viewer.selection); if (new Set(items.map(item => item.identifier)).size !== items.length || (viewer.available && !selection.active) || (selection.available && !viewer.available)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Spreadsheet Viewer selection'); } space.spreadsheet.viewer = { available: viewer.available, itemIdentifier, items, itemsTruncated: viewer.itemsTruncated, itemsPage: viewer.itemsPage, itemsPageCount: viewer.itemsPageCount, selection, }; } if (spreadsheet.viewState !== undefined) { const view = spreadsheet.viewState; if (!record(view) || typeof view.pinned !== 'boolean' || typeof view.canPin !== 'boolean' || typeof view.showOnlySelected !== 'boolean' || typeof view.selectionFilterAvailable !== 'boolean' || typeof view.dataSourceAvailable !== 'boolean' || (view.selectionFilterAvailable && !view.dataSourceAvailable)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Spreadsheet view state'); } space.spreadsheet.viewState = { pinned: view.pinned, canPin: view.canPin, showOnlySelected: view.showOnlySelected, selectionFilterAvailable: view.selectionFilterAvailable, dataSourceAvailable: view.dataSourceAvailable, }; } if (spreadsheet.context !== undefined) { const settings = spreadsheet.context; if (!record(settings)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `${areaName}.spreadsheet.context is invalid`); const parsed = {} as NonNullable; for (const property of SPREADSHEET_CONTEXT_PROPERTIES) { const setting = settings[property]; if (!record(setting) || !Array.isArray(setting.choices) || setting.choices.length > 64) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `${areaName}.spreadsheet.${property} is invalid`); } const choices = setting.choices.map(item => { if (!record(item)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Spreadsheet context choice'); return { id: text(item.id, 'Spreadsheet choice id', 64), label: text(item.label, 'Spreadsheet choice label', 256) }; }); const value = setting.value === null ? null : text(setting.value, 'Spreadsheet context value', 64); if (new Set(choices.map(item => item.id)).size !== choices.length || (value !== null && !choices.some(item => item.id === value))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Spreadsheet context selection'); } parsed[property] = { value, choices }; } space.spreadsheet.context = parsed; } if (spreadsheet.columns !== undefined) { if (!Array.isArray(spreadsheet.columns) || spreadsheet.columns.length > 512 || typeof spreadsheet.columnsTruncated !== 'boolean') { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Spreadsheet columns'); } space.spreadsheet.columns = spreadsheet.columns.map(column => { if (!record(column) || typeof column.comparison !== 'boolean' || typeof column.threshold !== 'boolean' || (column.valueProperty !== null && !(SPREADSHEET_VALUE_PROPERTIES as readonly unknown[]).includes(column.valueProperty))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Spreadsheet column type'); } return { name: text(column.name, 'Spreadsheet column name', 4096), valueProperty: column.valueProperty as NativeSpreadsheetColumn['valueProperty'], comparison: column.comparison, threshold: column.threshold }; }); space.spreadsheet.columnsTruncated = spreadsheet.columnsTruncated; } } if (areaValue.space.properties !== undefined) { const properties = areaValue.space.properties; if (areaValue.editor !== 'PROPERTIES' || space.kind !== 'PROPERTIES' || !record(properties) || !Array.isArray(properties.contexts) || properties.contexts.length > 64 || typeof properties.pinned !== 'boolean' || typeof properties.canPin !== 'boolean') { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `${areaName}.properties is invalid`); } const contexts = properties.contexts.map((item, index) => { if (!record(item)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `${areaName}.contexts[${index}] is invalid`); return { id: text(item.id, `${areaName}.context.id`, 64), label: text(item.label, `${areaName}.context.label`) }; }); const context = properties.context === null ? null : text(properties.context, `${areaName}.context`, 64); const pinId = properties.pinId === null ? null : text(properties.pinId, `${areaName}.pinId`); const pinName = properties.pinName === null ? null : text(properties.pinName, `${areaName}.pinName`); if (new Set(contexts.map(item => item.id)).size !== contexts.length || (context !== null && !contexts.some(item => item.id === context)) || properties.pinned !== (pinId !== null) || properties.pinned !== (pinName !== null)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `${areaName}.properties context or pin is inconsistent`); } space.properties = { context, contexts, pinned: properties.pinned, canPin: properties.canPin, pinId, pinName }; if (properties.viewLayers !== undefined) { if (!Array.isArray(properties.viewLayers) || properties.viewLayers.length > 64) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties view layers'); space.properties.viewLayers = properties.viewLayers.map(item => { if (!record(item) || typeof item.active !== 'boolean' || typeof item.editable !== 'boolean') throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties view layer entry'); return { name: text(item.name, `${areaName}.viewLayer.name`, 63), active: item.active, editable: item.editable }; }); } if (properties.outlinerSync !== undefined) { const sync = properties.outlinerSync; if (!record(sync) || typeof sync.editable !== 'boolean' || !Array.isArray(sync.items) || sync.items.length > 64) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties Outliner sync'); } const items = sync.items.map(item => { if (!record(item)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties sync item'); return { id: text(item.id, `${areaName}.sync.id`, 64), label: text(item.label, `${areaName}.sync.label`) }; }); const value = sync.value === null ? null : text(sync.value, `${areaName}.sync.value`, 64); if (new Set(items.map(item => item.id)).size !== items.length || (value !== null && !items.some(item => item.id === value))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Inconsistent Properties sync value'); } space.properties.outlinerSync = { value, items, editable: sync.editable }; } if (properties.object !== undefined && properties.object !== null) { const object = properties.object; if (context === null || !['OBJECT', 'MODIFIER', 'CONSTRAINT', 'BONE', 'BONE_CONSTRAINT'].includes(context) || !record(object) || !Array.isArray(object.fields) || object.fields.length > PROPERTIES_OBJECT_BOOLEAN_FIELDS.length + PROPERTIES_OBJECT_ENUM_FIELDS.length) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties object'); } const fields = object.fields.map((field): NativePropertiesObject['fields'][number] => { if (!record(field) || typeof field.editable !== 'boolean' || !Array.isArray(field.items) || field.items.length > 32) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties object field'); } const id = text(field.id, 'Properties object field', 64) as PropertiesObjectField; const items = field.items.map(item => { if (!record(item)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties field option'); return { id: text(item.id, 'Properties field option', 64), label: text(item.label, 'Properties field label') }; }); const booleanField = (PROPERTIES_OBJECT_BOOLEAN_FIELDS as readonly string[]).includes(id); const enumField = (PROPERTIES_OBJECT_ENUM_FIELDS as readonly string[]).includes(id); if (new Set(items.map(item => item.id)).size !== items.length || (!booleanField && !enumField) || (booleanField && (typeof field.value !== 'boolean' || items.length !== 0)) || (enumField && (typeof field.value !== 'string' || !items.some(item => item.id === field.value)))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties object value'); } return { id, label: text(field.label, 'Properties field label'), editable: field.editable, value: field.value as boolean | string, items }; }); if (new Set(fields.map(field => field.id)).size !== fields.length) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Duplicate Properties object field'); } const meshId = object.meshId === undefined || object.meshId === null ? null : text(object.meshId, 'Properties mesh ID'); const parseStack = (value: unknown, label: string) => { if (value === undefined) return undefined; if (!Array.isArray(value) || value.length > 256) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `Invalid ${label}`); return value.map((entry, index) => { if (!record(entry)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `Invalid ${label}[${index}]`); if (label === 'Properties modifiers') text(entry.uuid, `${label}.uuid`, 256); const name = text(entry.name, `${label}.name`, 256); const type = text(entry.type, `${label}.type`, 128); if (typeof entry.enabled !== 'boolean' || (label === 'Properties modifiers' && (typeof entry.showViewport !== 'boolean' || typeof entry.showRender !== 'boolean' || !record(entry.parameters))) || (label !== 'Properties modifiers' && (typeof entry.influence !== 'number' || !Number.isFinite(entry.influence)))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `Invalid ${label} entry`); } if (label === 'Properties modifiers' && entry.editableParameters !== undefined) { if (!Array.isArray(entry.editableParameters) || entry.editableParameters.length > 64) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier editable parameters'); } for (const parameter of entry.editableParameters) { if (!record(parameter) || typeof parameter.key !== 'string' || parameter.key.length === 0 || typeof parameter.kind !== 'string' || typeof parameter.length !== 'number' || !['boolean', 'number', 'enum', 'enum_set', 'object', 'collection', 'texture', 'cachefile', 'curve_mapping', 'string', 'profile'].includes(parameter.kind) || !Number.isSafeInteger(parameter.length) || parameter.length < 0 || parameter.length > 4) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier parameter metadata'); } if ((parameter.label !== undefined && typeof parameter.label !== 'string') || (parameter.description !== undefined && typeof parameter.description !== 'string') || (parameter.visible !== undefined && typeof parameter.visible !== 'boolean') || (parameter.enabled !== undefined && typeof parameter.enabled !== 'boolean') || (parameter.editable !== undefined && typeof parameter.editable !== 'boolean') || (parameter.nullable !== undefined && typeof parameter.nullable !== 'boolean') || (parameter.display !== undefined && typeof parameter.display !== 'string') || (parameter.unit !== undefined && parameter.unit !== 'ANGLE')) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier parameter access metadata'); } if (parameter.kind === 'string' && (typeof parameter.value !== 'string' || typeof parameter.maxLength !== 'number' || typeof parameter.suggestionsOnly !== 'boolean' || !Number.isSafeInteger(parameter.maxLength) || parameter.maxLength < 0 || parameter.maxLength > 65535 || parameter.length !== 0)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier string resource metadata'); } if (parameter.kind === 'profile' && (parameter.key !== 'custom_profile' || parameter.length !== 0 || !isModifierCurveProfile(parameter.value))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier custom profile metadata'); } if (parameter.kind === 'curve_mapping' && (!['map_curve', 'falloff_curve'].includes(parameter.key) || parameter.length !== 0 || !record(parameter.value) || !Array.isArray(parameter.value.clip) || parameter.value.clip.length !== 4 || !parameter.value.clip.every(value => typeof value === 'number' && Number.isFinite(value)) || !Array.isArray(parameter.value.points) || parameter.value.points.length < 2 || parameter.value.points.length > 256 || !parameter.value.points.every(point => Array.isArray(point) && point.length === 2 && point.every(value => typeof value === 'number' && Number.isFinite(value))))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier CurveMapping metadata'); } if (parameter.kind === 'curve_mapping' && record(parameter.value) && ['useClip', 'extend', 'handles', 'wrapping'].some(key => Object.prototype.hasOwnProperty.call(parameter.value, key))) { const curve = parameter.value; if (typeof curve.useClip !== 'boolean' || typeof curve.wrapping !== 'boolean' || typeof curve.extend !== 'string' || !['HORIZONTAL', 'EXTRAPOLATED'].includes(curve.extend) || !Array.isArray(curve.points) || !Array.isArray(curve.handles) || curve.handles.length !== curve.points.length || !curve.handles.every(handle => typeof handle === 'string' && ['AUTO', 'VECTOR', 'AUTO_CLAMPED'].includes(handle))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier CurveMapping tools metadata'); } } if (parameter.kind === 'curve_mapping' && record(parameter.value) && ['view', 'selected', 'samples'].some(key => Object.prototype.hasOwnProperty.call(parameter.value, key))) { const curve = parameter.value; if (!Array.isArray(curve.view) || curve.view.length !== 4 || !curve.view.every(value => typeof value === 'number' && Number.isFinite(value)) || curve.view[0] >= curve.view[2] || curve.view[1] >= curve.view[3] || !Array.isArray(curve.points) || !Array.isArray(curve.selected) || curve.selected.length !== curve.points.length || !curve.selected.every(value => typeof value === 'boolean') || !Array.isArray(curve.samples) || curve.samples.length !== 257 || !curve.samples.every(point => Array.isArray(point) && point.length === 2 && point.every(value => typeof value === 'number' && Number.isFinite(value)))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid native CurveMapping graph metadata'); } } if (parameter.kind === 'curve_mapping' && record(parameter.value) && ['canZoomIn', 'canZoomOut', 'active'].some(key => Object.prototype.hasOwnProperty.call(parameter.value, key))) { const curve = parameter.value; if (typeof curve.canZoomIn !== 'boolean' || typeof curve.canZoomOut !== 'boolean' || !Number.isInteger(curve.active) || Number(curve.active) < -1 || !Array.isArray(curve.points) || Number(curve.active) >= curve.points.length) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid native CurveMapping view controls'); } } if (parameter.kind === 'enum' || parameter.kind === 'enum_set' || parameter.kind === 'object' || parameter.kind === 'collection' || parameter.kind === 'texture' || parameter.kind === 'cachefile' || parameter.kind === 'string') { if (!Array.isArray(parameter.items) || (parameter.kind === 'enum' && parameter.items.length === 0) || !parameter.items.every(item => record(item) && typeof item.id === 'string' && typeof item.label === 'string')) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier enum metadata'); } } const enumItems = parameter.items; if (parameter.kind === 'enum_set' && (parameter.length !== 0 || !Array.isArray(parameter.value) || !Array.isArray(enumItems) || enumItems.length > 64 || new Set(enumItems.map(item => item.id)).size !== enumItems.length || parameter.value.length > enumItems.length || !parameter.value.every(value => typeof value === 'string' && enumItems.some(item => item.id === value)) || new Set(parameter.value).size !== parameter.value.length)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier enum flag selection'); } if (parameter.kind === 'object' && parameter.value !== null && (typeof parameter.value !== 'string' || !parameter.value.startsWith('object:'))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier object resource value'); } if (parameter.kind === 'collection' && parameter.value !== null && (typeof parameter.value !== 'string' || !parameter.value.startsWith('collection:'))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier Collection resource value'); } if (parameter.kind === 'texture' && parameter.value !== null && (typeof parameter.value !== 'string' || !parameter.value.startsWith('texture:'))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier Texture resource value'); } if (parameter.kind === 'cachefile' && parameter.value !== null && (typeof parameter.value !== 'string' || !parameter.value.startsWith('cachefile:'))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier CacheFile resource value'); } } } if (label === 'Properties modifiers' && entry.diagnostics !== undefined) { if (!Array.isArray(entry.diagnostics) || entry.diagnostics.length > 8) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier diagnostics'); } for (const diagnostic of entry.diagnostics) { if (!record(diagnostic) || !['INFO', 'ERROR'].includes(String(diagnostic.kind))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier diagnostic'); } text(diagnostic.message, 'Modifier diagnostic message', 4096); } } if (label === 'Properties modifiers' && entry.bind !== undefined && (!record(entry.bind) || !['LaplacianDeform', 'CorrectiveSmooth', 'MeshDeform', 'SurfaceDeform'].includes(type) || typeof entry.bind.bound !== 'boolean' || typeof entry.bind.enabled !== 'boolean' || typeof entry.bind.reason !== 'string' || entry.bind.reason.length > 512)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier bind action'); } if (label === 'Properties modifiers' && entry.duplicate !== undefined && (!record(entry.duplicate) || typeof entry.duplicate.enabled !== 'boolean' || typeof entry.duplicate.reason !== 'string' || entry.duplicate.reason.length > 512)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier duplicate action'); } if (label === 'Properties modifiers' && entry.apply !== undefined && (!record(entry.apply) || typeof entry.apply.enabled !== 'boolean' || typeof entry.apply.reason !== 'string' || entry.apply.reason.length > 512 || typeof entry.apply.requiresSingleUser !== 'boolean' || (entry.apply.supportsAllKeyframes !== undefined && typeof entry.apply.supportsAllKeyframes !== 'boolean') || (entry.apply.supportsMergeCustomData !== undefined && typeof entry.apply.supportsMergeCustomData !== 'boolean'))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier apply action'); } if (label === 'Properties modifiers' && entry.applyAsShapeKey !== undefined && (!record(entry.applyAsShapeKey) || typeof entry.applyAsShapeKey.enabled !== 'boolean' || typeof entry.applyAsShapeKey.reason !== 'string' || entry.applyAsShapeKey.reason.length > 512)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier shape key action'); } if (label === 'Properties modifiers' && entry.copyToSelected !== undefined) { const copy = entry.copyToSelected; if (!record(copy) || typeof copy.enabled !== 'boolean' || typeof copy.reason !== 'string' || copy.reason.length > 512 || !Array.isArray(copy.targets) || copy.targets.length > 1024 || copy.targets.some((id: unknown) => typeof id !== 'string' || !id.startsWith('id:OB') || id.length > 256)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier copy destinations'); } } if (label === 'Properties modifiers' && entry.hookActions !== undefined) { const ids = ['RESET', 'RECENTER', 'SELECT', 'ASSIGN']; if (type !== 'Hook' || !Array.isArray(entry.hookActions) || entry.hookActions.length !== ids.length || entry.hookActions.some((action: unknown, actionIndex: number) => !record(action) || action.id !== ids[actionIndex] || typeof action.enabled !== 'boolean' || typeof action.reason !== 'string' || action.reason.length > 512 || typeof action.label !== 'string' || action.label.length > 128)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid native Hook actions'); } } if (label === 'Properties modifiers' && entry.skinActions !== undefined) { const ids = ['CREATE_ARMATURE', 'ADD_SKIN_DATA', 'MARK_LOOSE', 'CLEAR_LOOSE', 'MARK_ROOT', 'EQUALIZE_RADII']; if (type !== 'Skin' || !Array.isArray(entry.skinActions) || entry.skinActions.length !== ids.length || entry.skinActions.some((action: unknown, actionIndex: number) => !record(action) || action.id !== ids[actionIndex] || typeof action.enabled !== 'boolean' || typeof action.reason !== 'string' || action.reason.length > 512 || typeof action.label !== 'string' || action.label.length > 128)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid native Skin actions'); } } if (label === 'Properties modifiers' && entry.dataLayoutActions !== undefined) { if (type !== 'Data Transfer' || !Array.isArray(entry.dataLayoutActions) || entry.dataLayoutActions.length !== 1 || !record(entry.dataLayoutActions[0]) || entry.dataLayoutActions[0].id !== 'GENERATE_DATA_LAYERS' || typeof entry.dataLayoutActions[0].enabled !== 'boolean' || typeof entry.dataLayoutActions[0].reason !== 'string' || entry.dataLayoutActions[0].reason.length > 512 || typeof entry.dataLayoutActions[0].label !== 'string' || entry.dataLayoutActions[0].label.length > 128) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid native Data Transfer actions'); } } if (label === 'Properties modifiers' && entry.multiresActions !== undefined) { const ids = ['DELETE_HIGHER_LEVELS', 'SUBDIVIDE', 'RESHAPE', 'BASE_APPLY', 'UNSUBDIVIDE', 'REBUILD_SUBDIV', 'EXTERNAL_SAVE', 'EXTERNAL_PACK']; if (type !== 'Multires' || !Array.isArray(entry.multiresActions) || entry.multiresActions.length !== ids.length || entry.multiresActions.some((action: unknown, actionIndex: number) => !record(action) || action.id !== ids[actionIndex] || typeof action.enabled !== 'boolean' || typeof action.reason !== 'string' || action.reason.length > 512 || typeof action.label !== 'string' || action.label.length > 128)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid native Multires actions'); } } if ((label === 'Properties constraints' || label === 'Properties pose constraints') && entry.reorderable !== undefined && typeof entry.reorderable !== 'boolean') { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid constraint reorder availability'); } if (label === 'Properties pose constraints') { text(entry.boneName, `${label}.boneName`, 63); if (entry.targetBoneName !== undefined && (typeof entry.targetBoneName !== 'string' || entry.targetBoneName.length >= 64)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid constraint target bone'); } } if (label === 'Properties constraints' && entry.targetBoneName !== undefined && (typeof entry.targetBoneName !== 'string' || entry.targetBoneName.length >= 64)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid object constraint target bone'); } return entry as any; }); }; const modifiers = parseStack(object.modifiers, 'Properties modifiers') as NativePropertiesObject['modifiers']; let modifierTypes: NativePropertiesObject['modifierTypes']; if (object.modifierTypes !== undefined) { if (!Array.isArray(object.modifierTypes) || object.modifierTypes.length > 256) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier choices'); } const ids = new Set(); modifierTypes = object.modifierTypes.map(item => { if (!record(item) || typeof item.enabled !== 'boolean' || typeof item.category !== 'string' || item.category.length > 256 || typeof item.reason !== 'string' || item.reason.length > 1024) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid modifier choice'); } const id = text(item.id, 'Modifier choice ID', 256); if (ids.has(id)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Duplicate modifier choice'); ids.add(id); return { id, label: text(item.label, 'Modifier choice label', 256), category: item.category, enabled: item.enabled, reason: item.reason }; }); } const constraints = parseStack(object.constraints, 'Properties constraints') as NativePropertiesObject['constraints']; const poseConstraints = parseStack(object.poseConstraints, 'Properties pose constraints') as NativePropertiesObject['poseConstraints']; const activeModifierUuid = object.activeModifierUuid === undefined || object.activeModifierUuid === null ? null : text(object.activeModifierUuid, 'Properties active modifier UUID', 256); const activeConstraintName = object.activeConstraintName === undefined || object.activeConstraintName === null ? null : text(object.activeConstraintName, 'Properties active constraint name', 63); let activePoseConstraint: NativePropertiesObject['activePoseConstraint'] = null; if (object.activePoseConstraint !== undefined && object.activePoseConstraint !== null) { if (!record(object.activePoseConstraint)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid active pose constraint'); activePoseConstraint = { boneName: text(object.activePoseConstraint.boneName, 'Properties active pose bone', 63), name: text(object.activePoseConstraint.name, 'Properties active pose constraint', 63) }; } space.properties.object = { id: text(object.id, 'Properties object ID'), name: text(object.name, 'Properties object name'), fields, meshId, modifiers, modifierTypes, activeModifierUuid, constraints, activeConstraintName, poseConstraints, activePoseConstraint }; if (object.stackEditable !== undefined) { if (typeof object.stackEditable !== 'boolean') throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid stack editability'); space.properties.object.stackEditable = object.stackEditable; } if (object.poseBones !== undefined) { if (!Array.isArray(object.poseBones) || object.poseBones.length > 65536) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid pose bones'); space.properties.object.poseBones = object.poseBones.map(bone => text(bone, 'Properties pose bone', 63)); } if (object.constraintTargets !== undefined) { if (!Array.isArray(object.constraintTargets)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid constraint target choices'); } space.properties.object.constraintTargets = object.constraintTargets.map(item => { if (!record(item) || !['ARMATURE', 'CURVE', 'MESH', 'LATTICE', 'OTHER'].includes(item.kind as string) || !Array.isArray(item.bones) || item.bones.length > 65536) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid constraint target choice'); } const id = text(item.id, 'Constraint target ID'); if (!id.startsWith('object:')) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid constraint target identity'); if (item.vertexGroups !== undefined && (!Array.isArray(item.vertexGroups) || item.vertexGroups.length > 65536)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid constraint target vertex groups'); } return { id, name: text(item.name, 'Constraint target name'), kind: item.kind as 'ARMATURE' | 'CURVE' | 'MESH' | 'LATTICE' | 'OTHER', bones: item.bones.map(bone => text(bone, 'Constraint target bone', 63)), ...(Array.isArray(item.vertexGroups) ? { vertexGroups: item.vertexGroups.map(group => text(group, 'Constraint target vertex group', 63)), } : {}) }; }); } if (object.relations !== undefined) { const relations = object.relations; if (!record(relations) || typeof relations.editable !== 'boolean' || typeof relations.truncated !== 'boolean' || !Array.isArray(relations.candidates) || relations.candidates.length > 4096) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties relations'); } const parentId = relations.parentId === null ? null : text(relations.parentId, 'Properties parent ID'); const parentName = relations.parentName === null ? null : text(relations.parentName, 'Properties parent name'); const candidates = relations.candidates.map(item => { if (!record(item)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties parent candidate'); return { id: text(item.id, 'Properties parent candidate ID'), name: text(item.name, 'Properties parent candidate name') }; }); if ((parentId === null) !== (parentName === null) || parentId === object.id || candidates.some(item => item.id === object.id) || new Set(candidates.map(item => item.id)).size !== candidates.length) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Inconsistent Properties relations'); } space.properties.object.relations = { parentId, parentName, editable: relations.editable, candidates, truncated: relations.truncated }; if (relations.parenting !== undefined) { const parenting = relations.parenting; const validBone = (value: unknown): value is string => typeof value === 'string' && !value.includes('\0') && new TextEncoder().encode(value).length < 64; if (!record(parenting) || !Array.isArray(parenting.types) || parenting.types.length > 6 || typeof parenting.typeEditable !== 'boolean' || typeof parenting.boneEditable !== 'boolean' || !validBone(parenting.bone) || !Array.isArray(parenting.bones) || parenting.bones.length > 4096 || !parenting.bones.every(value => validBone(value) && value.length > 0) || typeof parenting.bonesTruncated !== 'boolean') { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties parenting settings'); } const types = parenting.types.map(item => { if (!record(item) || !['OBJECT', 'ARMATURE', 'LATTICE', 'VERTEX', 'VERTEX_3', 'BONE'].includes(String(item.id))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties parent type'); } return { id: text(item.id, 'Parent type ID'), label: text(item.label, 'Parent type label') }; }); const type = parenting.type === null ? null : text(parenting.type, 'Parent type'); if ((type !== null && !types.some(item => item.id === type)) || new Set(types.map(item => item.id)).size !== types.length || new Set(parenting.bones).size !== parenting.bones.length || (parenting.typeEditable && (!relations.editable || !parentId)) || (parenting.boneEditable && (!relations.editable || !parentId || type !== 'BONE'))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Inconsistent Properties parenting settings'); } space.properties.object.relations.parenting = { type, types, typeEditable: parenting.typeEditable, bone: parenting.bone, boneEditable: parenting.boneEditable, bones: parenting.bones as string[], bonesTruncated: parenting.bonesTruncated }; if (parenting.settings !== undefined) { if (!Array.isArray(parenting.settings) || parenting.settings.length > 4) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid parent detail settings'); } const keys = new Set(); space.properties.object.relations.parenting.settings = parenting.settings.map(item => { if (!record(item) || !['parent_vertices', 'use_parent_final_indices', 'parent_bone_head_tail_factor'].includes(String(item.id)) || !Number.isSafeInteger(item.index) || typeof item.index !== 'number' || typeof item.editable !== 'boolean' || typeof item.minimum !== 'number' || typeof item.maximum !== 'number' || !Number.isFinite(item.minimum) || !Number.isFinite(item.maximum) || item.minimum > item.maximum) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid parent detail field'); } const vertices = item.id === 'parent_vertices'; const finalIndices = item.id === 'use_parent_final_indices'; const vertexMode = type === 'VERTEX' || type === 'VERTEX_3'; const key = `${item.id}:${item.index}`; if (!parentId || keys.has(key) || ((vertices || finalIndices) ? !vertexMode : type !== 'BONE') || item.index < 0 || item.index >= (vertices && type === 'VERTEX_3' ? 3 : 1) || (item.editable && !relations.editable) || (vertices ? !Number.isSafeInteger(item.minimum) || !Number.isSafeInteger(item.maximum) || item.minimum < 0 || item.maximum > 2147483647 : item.minimum !== 0 || item.maximum !== 1) || (finalIndices ? typeof item.value !== 'boolean' : typeof item.value !== 'number' || !Number.isFinite(item.value) || item.value < item.minimum || item.value > item.maximum || (vertices && !Number.isSafeInteger(item.value)))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Inconsistent parent detail field'); } keys.add(key); return { id: item.id as PropertiesParentSetting, index: item.index, label: text(item.label, 'Parent setting label'), value: item.value as number | boolean, minimum: item.minimum, maximum: item.maximum, editable: item.editable }; }); } } } for (const group of ['transforms', 'deltaTransforms', 'dimensions'] as const) { if (object[group] === undefined) continue; const mode = fields.find(field => field.id === 'rotation_mode')?.value; const rotation = mode === 'QUATERNION' ? 'rotation_quaternion' : mode === 'AXIS_ANGLE' ? 'rotation_axis_angle' : 'rotation_euler'; const expected = group === 'dimensions' ? ['dimensions'] : group === 'transforms' ? ['location', rotation, 'scale'] : ['delta_location', ...(mode === 'AXIS_ANGLE' ? [] : [`delta_${rotation}`]), 'delta_scale']; const nativeTransforms = object[group]; if (!Array.isArray(nativeTransforms) || nativeTransforms.length !== expected.length) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties transforms'); } const transforms = nativeTransforms.map((field): NativePropertiesTransform => { if (!record(field)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties transform'); const id = text(field.id, 'Properties transform field', 64) as PropertiesObjectTransform; const length = Object.prototype.hasOwnProperty.call(PROPERTIES_OBJECT_TRANSFORM_LENGTHS, id) ? PROPERTIES_OBJECT_TRANSFORM_LENGTHS[id] : 0; if (!length || !Array.isArray(field.values) || field.values.length !== length || !Array.isArray(field.editable) || field.editable.length !== length || !field.editable.every(value => typeof value === 'boolean')) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties transform components'); } const minimum = finite(field.minimum, 'Properties transform minimum', -3.4028234663852886e38, 3.4028234663852886e38); const maximum = finite(field.maximum, 'Properties transform maximum', minimum, 3.4028234663852886e38); if (id === 'dimensions' && minimum < 0) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties dimensions range'); } return { id, label: text(field.label, 'Properties transform label'), minimum, maximum, values: field.values.map(value => finite(value, 'Properties transform value', -3.4028234663852886e38, 3.4028234663852886e38)), editable: field.editable as boolean[] }; }); if ((group !== 'dimensions' && typeof mode !== 'string') || new Set(transforms.map(field => field.id)).size !== expected.length || !expected.every(id => transforms.some(field => field.id === id))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Properties transform mode is inconsistent'); } space.properties.object[group] = transforms; } if (object.locks !== undefined) { if (!Array.isArray(object.locks) || object.locks.length !== 5) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties transform locks'); } const locks = object.locks.map((field): NativePropertiesLock => { if (!record(field)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties transform lock'); const id = text(field.id, 'Properties transform lock', 64) as PropertiesObjectLock; const length = Object.prototype.hasOwnProperty.call(PROPERTIES_OBJECT_LOCK_LENGTHS, id) ? PROPERTIES_OBJECT_LOCK_LENGTHS[id] : 0; if (!length || !Array.isArray(field.values) || field.values.length !== length || !field.values.every(value => typeof value === 'boolean') || !Array.isArray(field.editable) || field.editable.length !== length || !field.editable.every(value => typeof value === 'boolean')) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties transform lock components'); } return { id, label: text(field.label, 'Properties transform lock label'), values: field.values as boolean[], editable: field.editable as boolean[] }; }); if (new Set(locks.map(field => field.id)).size !== 5) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Duplicate Properties transform lock'); } space.properties.object.locks = locks; } } else if (properties.object === null) space.properties.object = null; if (properties.data !== undefined && properties.data !== null) { const data = properties.data; if (!record(data) || !Array.isArray(data.fields) || data.fields.length > EDITOR_WORKFLOW_BUDGET.maxPropertiesDataFields) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties data context'); } const fields = data.fields.map((field): NativePropertiesDataField => { if (!record(field)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties data field'); const arrayLayout = field.arrayLayout; const array = Array.isArray(field.value) ? field.value : null; const arrayShapeValid = array !== null && ( arrayLayout === 'MATRIX' ? array.length === 16 : arrayLayout === 'SEQUENCE' ? field.id === 'joint_bindings' && array.length <= 256 && (array.length > 0 || field.editable === false) : array.length >= 2 && array.length <= 4); if (arrayLayout !== undefined && (!['COLOR', 'VECTOR', 'MATRIX', 'SEQUENCE'].includes(arrayLayout as string) || !arrayShapeValid)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties array layout'); } if (!record(field) || typeof field.editable !== 'boolean' || !Array.isArray(field.items) || (typeof field.value !== 'boolean' && typeof field.value !== 'number' && typeof field.value !== 'string' && !(Array.isArray(field.value) && arrayShapeValid && field.value.every(component => typeof component === 'number' && Number.isFinite(component)))) || (typeof field.value === 'number' && !Number.isFinite(field.value))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties data field'); } const items = field.items.map(item => { if (!record(item)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties data option'); return { id: item.id === '' ? '' : text(item.id, 'Properties data option'), label: text(item.label, 'Properties data option label') }; }); if (new Set(items.map(item => item.id)).size !== items.length || (items.length > 0 && (typeof field.value !== 'string' || !items.some(item => item.id === field.value)))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Inconsistent Properties data field'); } const minimum = field.minimum; const maximum = field.maximum; if (field.integer !== undefined && (typeof field.integer !== 'boolean' || (field.integer && !Number.isSafeInteger(field.value)))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties integer field'); } if ((minimum !== undefined && (typeof minimum !== 'number' || !Number.isFinite(minimum))) || (maximum !== undefined && (typeof maximum !== 'number' || !Number.isFinite(maximum))) || (minimum !== undefined && maximum !== undefined && minimum > maximum)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties data field range'); } if (field.status !== undefined && (typeof field.status !== 'string' || field.status.length > 4096)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties data field status'); } if (field.description !== undefined && (typeof field.description !== 'string' || field.description.length > 4096)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Properties data field description'); } return { id: text(field.id, 'Properties data field', 64), label: text(field.label, 'Properties data field label'), editable: field.editable, value: field.value, items, ...(field.description !== undefined ? { description: field.description } : {}), ...(field.integer !== undefined ? { integer: field.integer as boolean } : {}), ...(arrayLayout !== undefined ? { arrayLayout: arrayLayout as NativePropertiesDataField['arrayLayout'] } : {}), ...(minimum !== undefined ? { minimum } : {}), ...(maximum !== undefined ? { maximum } : {}), ...(field.status !== undefined ? { status: field.status } : {}) }; }); if (new Set(fields.map(field => field.id)).size !== fields.length) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Duplicate Properties data field'); } if (context === null || !['TOOL', 'SCENE', 'RENDER', 'OUTPUT', 'VIEW_LAYER', 'WORLD', 'COLLECTION', 'OBJECT', 'CONSTRAINT', 'MODIFIER', 'DATA', 'BONE', 'BONE_CONSTRAINT', 'MATERIAL', 'TEXTURE', 'PARTICLES', 'PHYSICS', 'SHADERFX', 'STRIP', 'STRIP_MODIFIER'].includes(context)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `Unsupported Properties data context: ${String(context)} at ${areaName}`); space.properties.data = { id: text(data.id, 'Properties data ID'), name: text(data.name, 'Properties data name'), fields }; if (data.aovs !== undefined) { const aovs = data.aovs; if (context !== 'VIEW_LAYER' || !record(aovs) || typeof aovs.editable !== 'boolean' || !Array.isArray(aovs.rows)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid View Layer AOV context'); const rows = aovs.rows.map(row => { if (!record(row) || typeof row.isValid !== 'boolean') throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid View Layer AOV row'); return { name: text(row.name, 'AOV name'), type: integer(row.type, 'AOV type', 0, 1) as 0 | 1, isValid: row.isValid }; }); space.properties.data.aovs = { sceneId: text(aovs.sceneId, 'AOV Scene'), layerName: text(aovs.layerName, 'AOV View Layer'), editable: aovs.editable, activeIndex: integer(aovs.activeIndex, 'AOV active index', -1, Math.max(-1, rows.length - 1)), rows }; } if (data.lightgroups !== undefined) { const groups = data.lightgroups; if (context !== 'VIEW_LAYER' || !record(groups) || typeof groups.editable !== 'boolean' || !Array.isArray(groups.rows)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid View Layer Light Groups context'); const rows = groups.rows.map(name => text(name, 'Light Group name')); space.properties.data.lightgroups = { sceneId: text(groups.sceneId, 'Light Group Scene'), layerName: text(groups.layerName, 'Light Group View Layer'), editable: groups.editable, activeIndex: integer(groups.activeIndex, 'Light Group active index', -1, Math.max(-1, rows.length - 1)), rows }; } if (data.shapeKeys !== undefined) { const shapes = data.shapeKeys; if (context !== 'DATA' || !record(shapes) || typeof shapes.editable !== 'boolean' || typeof shapes.activeUid !== 'number' || !Number.isSafeInteger(shapes.activeUid) || !Array.isArray(shapes.rows) || !Array.isArray(shapes.operations)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Shape Key context'); } const rows = shapes.rows.map(row => { if (!record(row) || typeof row.uid !== 'number' || !Number.isSafeInteger(row.uid) || typeof row.selected !== 'boolean' || typeof row.muted !== 'boolean' || typeof row.locked !== 'boolean') { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Shape Key row'); } return { uid: row.uid, name: text(row.name, 'Shape Key name'), selected: row.selected, muted: row.muted, locked: row.locked }; }); const operations = shapes.operations.map(operation => { if (!record(operation) || typeof operation.enabled !== 'boolean' || typeof operation.reason !== 'string' || operation.reason.length > 4096 || (operation.sources !== undefined && (!Array.isArray(operation.sources) || operation.sources.length > 1024))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Shape Key operation'); } return { id: text(operation.id, 'Shape Key operation'), label: text(operation.label, 'Shape Key label'), enabled: operation.enabled, reason: operation.reason, ...(Array.isArray(operation.sources) ? { sources: operation.sources.map(source => text(source, 'Shape Key source', 2048)) } : {}) }; }); if (new Set(rows.map(row => row.uid)).size !== rows.length || new Set(operations.map(operation => operation.id)).size !== operations.length || (shapes.activeUid !== 0 && !rows.some(row => row.uid === shapes.activeUid))) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Inconsistent Shape Key context'); } space.properties.data.shapeKeys = { objectId: text(shapes.objectId, 'Shape Key object ID'), keyId: shapes.keyId === '' ? '' : text(shapes.keyId, 'Shape Key data ID'), activeUid: shapes.activeUid, editable: shapes.editable, rows, operations, }; } if (data.shapeKeyMirror !== undefined) { const mirror = data.shapeKeyMirror; if (context !== 'DATA' || !record(mirror) || typeof mirror.activeUid !== 'number' || !Number.isSafeInteger(mirror.activeUid) || typeof mirror.enabled !== 'boolean' || typeof mirror.topologyEnabled !== 'boolean' || typeof mirror.reason !== 'string' || mirror.reason.length > 4096 || (mirror.topologyEnabled && !mirror.enabled)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Shape Key mirror context'); } space.properties.data.shapeKeyMirror = { objectId: text(mirror.objectId, 'Shape Key object ID'), keyId: mirror.keyId === '' ? '' : text(mirror.keyId, 'Shape Key data ID'), activeUid: mirror.activeUid, name: mirror.name === '' ? '' : text(mirror.name, 'Shape Key name'), enabled: mirror.enabled, topologyEnabled: mirror.topologyEnabled, reason: mirror.reason, }; } if (data.cacheLayers !== undefined) { if (!['MODIFIER', 'CONSTRAINT', 'BONE_CONSTRAINT'].includes(context) || !Array.isArray(data.cacheLayers) || data.cacheLayers.length > 128 || typeof data.cacheActiveLayer !== 'number' || !Number.isInteger(data.cacheActiveLayer) || data.cacheActiveLayer < -1 || data.cacheActiveLayer >= data.cacheLayers.length || typeof data.cacheLayersEditable !== 'boolean') { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid CacheFile layers'); } space.properties.data.cacheLayers = data.cacheLayers.map(layer => { if (!record(layer) || typeof layer.hidden !== 'boolean') { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid CacheFile layer'); } return { filepath: text(layer.filepath, 'CacheFile layer path', 4096), hidden: layer.hidden }; }); space.properties.data.cacheActiveLayer = data.cacheActiveLayer; space.properties.data.cacheLayersEditable = data.cacheLayersEditable; } if (data.constraintOperations !== undefined) { if (!['CONSTRAINT', 'BONE_CONSTRAINT'].includes(context) || !Array.isArray(data.constraintOperations)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid constraint operations'); } space.properties.data.constraintOperations = data.constraintOperations.map(operation => { if (!record(operation) || typeof operation.enabled !== 'boolean' || typeof operation.reason !== 'string' || (operation.menu !== undefined && typeof operation.menu !== 'boolean') || operation.reason.length > 4096 || typeof operation.description !== 'string') { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid native constraint operation'); } if (operation.parameters !== undefined && !Array.isArray(operation.parameters)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid constraint operation parameters'); } const parameters = (operation.parameters ?? []).map(parameter => { if (!record(parameter) || typeof parameter.description !== 'string' || !Number.isSafeInteger(parameter.minimum) || !Number.isSafeInteger(parameter.maximum) || !Number.isSafeInteger(parameter.defaultValue) || Number(parameter.minimum) > Number(parameter.maximum)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid native constraint integer parameter'); } return { id: text(parameter.id, 'Constraint parameter', 64), label: text(parameter.label, 'Constraint parameter label'), description: parameter.description, minimum: Number(parameter.minimum), maximum: Number(parameter.maximum), defaultValue: finite(parameter.defaultValue, 'Constraint parameter default', Number(parameter.minimum), Number(parameter.maximum)) }; }); if (new Set(parameters.map(parameter => parameter.id)).size !== parameters.length) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Duplicate constraint operation parameter'); } return { id: text(operation.id, 'Constraint operator', 128), label: text(operation.label, 'Constraint operation label'), description: operation.description, enabled: operation.enabled, reason: operation.reason, menu: operation.menu === true, parameters }; }); } if (data.armatureTargets !== undefined) { if (!['CONSTRAINT', 'BONE_CONSTRAINT'].includes(context) || !Array.isArray(data.armatureTargets) || typeof data.targetsEditable !== 'boolean' || !Array.isArray(data.armatures)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Armature target collection'); } space.properties.data.armatureTargets = data.armatureTargets.map(target => { if (!record(target)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Armature target'); return { objectId: target.objectId === '' ? '' : text(target.objectId, 'Armature target object'), bone: target.bone === '' ? '' : text(target.bone, 'Armature target bone', 63), weight: finite(target.weight, 'Armature target weight', 0, 1) }; }); space.properties.data.targetsEditable = data.targetsEditable; space.properties.data.armatures = data.armatures.map(armature => { if (!record(armature) || !Array.isArray(armature.bones)) { throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', 'Invalid Armature target candidates'); } return { id: text(armature.id, 'Armature candidate'), name: text(armature.name, 'Armature name'), bones: armature.bones.map(bone => text(bone, 'Armature bone', 63)) }; }); } } else if (properties.data === null) space.properties.data = null; } if (areaValue.space.imageView !== undefined) { const view = areaValue.space.imageView; if (!record(view)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `${areaName}.imageView is invalid`); space.imageView = { zoom: finite(view.zoom, `${areaName}.zoom`, Number.MIN_VALUE, Number.MAX_VALUE), offsetX: finite(view.offsetX, `${areaName}.offsetX`, -Number.MAX_VALUE, Number.MAX_VALUE), offsetY: finite(view.offsetY, `${areaName}.offsetY`, -Number.MAX_VALUE, Number.MAX_VALUE), width: integer(view.width, `${areaName}.imageWidth`, 1, 2147483647), height: integer(view.height, `${areaName}.imageHeight`, 1, 2147483647), aspectX: finite(view.aspectX, `${areaName}.aspectX`, Number.MIN_VALUE, Number.MAX_VALUE), aspectY: finite(view.aspectY, `${areaName}.aspectY`, Number.MIN_VALUE, Number.MAX_VALUE), }; } if (areaValue.space.uvPackTarget !== undefined) space.uvPackTarget = parseNativeUVPackTarget(areaValue.space.uvPackTarget); if (areaValue.space.textSelection !== undefined) { const selection = areaValue.space.textSelection; if (!record(selection)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `${areaName}.textSelection is invalid`); const position = (key: 'anchor' | 'focus'): TextCursorIR => { const cursor = selection[key]; if (!record(cursor)) throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID', `${areaName}.${key} is invalid`); return {line: integer(cursor.line, `${areaName}.${key}.line`, 0, 65535), column: integer(cursor.column, `${areaName}.${key}.column`, 0, 1048576)}; }; space.textSelection = {anchor: position('anchor'), focus: position('focus')}; } if (areaValue.space.view !== undefined) { const source=areaValue.space.view; if(!record(source)||!Array.isArray(source.offset)||source.offset.length!==3||!Array.isArray(source.rotation)||source.rotation.length!==4||![...source.offset,...source.rotation,source.distance,source.lens,source.near,source.far].every(item=>typeof item==='number'&&Number.isFinite(item))||!['PERSPECTIVE','ORTHOGRAPHIC','CAMERA'].includes(source.projection as string))throw new EditorWorkflowValidationError('EDITOR_LAYOUT_INVALID',`${areaName}.view is invalid`); space.view={offset:[...source.offset] as NativeViewportState['offset'],rotation:[...source.rotation] as NativeViewportState['rotation'],distance:source.distance as number,lens:source.lens as number,near:source.near as number,far:source.far as number,projection:source.projection as NativeViewportState['projection'],locked:source.locked===true}; } } return { id: areaId, editor: areaValue.editor as EditorTypeIR, regions, rect: rect(areaValue.rect, `${areaName}.rect`), maximized: areaValue.maximized === true, nativeLayout, filterText: filterText as string | undefined, space }; }); const activeAreaId = text(workspaceValue.activeAreaId, `${name}.activeAreaId`); if (!areaIds.has(activeAreaId)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${name}.activeAreaId is missing`); for (let index = 0; index < areas.length; index++) for (let other = index + 1; other < areas.length; other++) if (!areas[index].maximized && !areas[other].maximized && overlap(areas[index].rect, areas[other].rect)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${name} has overlapping areas`); areas.forEach((area, areaIndex) => area.regions.forEach((region, regionIndex) => { const source = (workspaceValue.areas as Array<{ regions: Array> }>)[areaIndex].regions[regionIndex]; region.toggleable = source.toggleable === true; })); return { id, name: text(workspaceValue.name, `${name}.name`), screenId: workspaceValue.screenId === undefined ? undefined : text(workspaceValue.screenId, `${name}.screenId`), writable: workspaceValue.writable === true, areas, activeAreaId, revision: integer(workspaceValue.revision, `${name}.revision`, 0, Number.MAX_SAFE_INTEGER) }; }); const contextValue = value.context; const workspaceId = text(contextValue.workspaceId, "context.workspaceId"); const workspace = workspaces.find((item) => item.id === workspaceId); if (!workspace) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", "Context workspace is missing"); const activeAreaId = text(contextValue.activeAreaId, "context.activeAreaId"); const activeArea = workspace.areas.find((item) => item.id === activeAreaId); if (!activeArea || activeArea.editor !== contextValue.activeEditor) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", "Context active area/editor is inconsistent"); if (!EDITOR_TYPES.includes(contextValue.activeEditor as EditorTypeIR) || !EDITOR_MODES.includes(contextValue.mode as EditorMode) || !Array.isArray(contextValue.selection)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", "Context is invalid"); if (contextValue.selection.length > EDITOR_WORKFLOW_BUDGET.maxSelection || contextValue.selection.some((item) => typeof item !== "string" || item.length === 0)) throw new EditorWorkflowValidationError("EDITOR_SELECTION_INVALID", "Selection exceeds the budget"); if (contextValue.activeObjectId !== null && typeof contextValue.activeObjectId !== "string") throw new EditorWorkflowValidationError("EDITOR_SELECTION_INVALID", "Active object is invalid"); const keymapIds = new Set(); const keymaps: KeymapBindingIR[] = []; value.keymaps.forEach((bindingValue, index) => { const name = `keymaps[${index}]`; if (!record(bindingValue) || !Array.isArray(bindingValue.modifiers)) throw new EditorWorkflowValidationError("EDITOR_KEYMAP_INVALID", `${name} is invalid`); const id = text(bindingValue.id, `${name}.id`); if (keymapIds.has(id)) throw new EditorWorkflowValidationError("EDITOR_KEYMAP_INVALID", `Duplicate keymap ${id}`); keymapIds.add(id); const modifiers = bindingValue.modifiers.map((modifier, modifierIndex) => text(modifier, `${name}.modifiers[${modifierIndex}]`, 16).toUpperCase()); if (new Set(modifiers).size !== modifiers.length || modifiers.some((modifier) => !["ALT", "CTRL", "META", "SHIFT"].includes(modifier))) throw new EditorWorkflowValidationError("EDITOR_KEYMAP_INVALID", `${name} modifiers are invalid or duplicate`); modifiers.sort(); const parseScope = (field: "workspaceIds" | "editors" | "modes", allowed?: readonly T[]): T[] | undefined => { const source = bindingValue[field]; if (source === undefined) return undefined; if (!Array.isArray(source) || source.length === 0 || source.length > 64) throw new EditorWorkflowValidationError("EDITOR_KEYMAP_INVALID", `${name}.${field} is invalid`); const parsed = source.map((item, scopeIndex) => text(item, `${name}.${field}[${scopeIndex}]`, 256) as T); if (new Set(parsed).size !== parsed.length || (allowed && parsed.some((item) => !allowed.includes(item)))) throw new EditorWorkflowValidationError("EDITOR_KEYMAP_INVALID", `${name}.${field} is invalid or duplicated`); return parsed; }; const binding: KeymapBindingIR = { id, key: text(bindingValue.key, `${name}.key`, 32).toUpperCase(), modifiers, command: text(bindingValue.command, `${name}.command`, 128), enabled: bindingValue.enabled !== false, workspaceIds: parseScope("workspaceIds"), editors: parseScope("editors", EDITOR_TYPES), modes: parseScope("modes", EDITOR_MODES) }; if (binding.workspaceIds?.some((workspace) => !workspaceIds.has(workspace))) throw new EditorWorkflowValidationError("EDITOR_KEYMAP_INVALID", `${name}.workspaceIds references a missing workspace`); if (binding.enabled && keymaps.some((candidate) => candidate.enabled && candidate.key === binding.key && candidate.modifiers.join("+") === binding.modifiers.join("+") && keymapScopesOverlap(candidate, binding))) throw new EditorWorkflowValidationError("EDITOR_KEYMAP_INVALID", `${name} conflicts with another enabled keymap in the same context`); keymaps.push(binding); }); return { schemaVersion: EDITOR_WORKFLOW_SCHEMA, workspaces, uvSelection: parseUVSelection(value.uvSelection), context: { workspaceId, activeAreaId, activeEditor: contextValue.activeEditor as EditorTypeIR, mode: contextValue.mode as EditorMode, activeObjectId: contextValue.activeObjectId as string | null, selection: [...contextValue.selection] as string[], viewLayer: text(contextValue.viewLayer, "context.viewLayer"), pinnedData: contextValue.pinnedData === null ? null : text(contextValue.pinnedData, "context.pinnedData"), revision: integer(contextValue.revision, "context.revision", 0, Number.MAX_SAFE_INTEGER) }, keymaps }; } export function keyChordFromKeyboardEvent(event: Pick): KeyChordIR { const key = text(event.key, "event.key", 32).toUpperCase(); const modifiers: KeyChordIR["modifiers"] = []; if (event.altKey) modifiers.push("ALT"); if (event.ctrlKey) modifiers.push("CTRL"); if (event.metaKey) modifiers.push("META"); if (event.shiftKey) modifiers.push("SHIFT"); return { key, modifiers }; } export function resolveKeymapCommand(value: unknown, chordValue: unknown): string | null { const workflow = parseEditorWorkflow(value); if (!record(chordValue) || !Array.isArray(chordValue.modifiers)) throw new EditorWorkflowValidationError("EDITOR_KEYMAP_INVALID", "Key chord is invalid"); const chord = { key: text(chordValue.key, "keyChord.key", 32).toUpperCase(), modifiers: chordValue.modifiers.map((modifier, index) => text(modifier, `keyChord.modifiers[${index}]`, 16).toUpperCase()).sort() }; if (new Set(chord.modifiers).size !== chord.modifiers.length || chord.modifiers.some((modifier) => !["ALT", "CTRL", "META", "SHIFT"].includes(modifier))) throw new EditorWorkflowValidationError("EDITOR_KEYMAP_INVALID", "Key chord modifiers are invalid"); return workflow.keymaps.find((binding) => binding.enabled && binding.key === chord.key && binding.modifiers.join("+") === chord.modifiers.join("+") && (!binding.workspaceIds || binding.workspaceIds.includes(workflow.context.workspaceId)) && (!binding.editors || binding.editors.includes(workflow.context.activeEditor)) && (!binding.modes || binding.modes.includes(workflow.context.mode)))?.command ?? null; } export function applyEditorWorkflowEdit(value: unknown, edit: EditorWorkflowEditIR): EditorWorkflowIR { const workflow = parseEditorWorkflow(value); if (edit.revision !== workflow.context.revision) throw new EditorWorkflowValidationError("REVISION_CONFLICT", "Editor context revision is stale"); const clone = structuredClone(workflow); if (edit.type === "SWITCH_WORKSPACE") { const workspace = clone.workspaces.find((item) => item.id === edit.workspaceId); if (!workspace) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `Unknown workspace ${edit.workspaceId}`); clone.context.workspaceId = workspace.id; clone.context.activeAreaId = workspace.activeAreaId; clone.context.activeEditor = workspace.areas.find((item) => item.id === workspace.activeAreaId)?.editor ?? "VIEW_3D"; } else if (edit.type === "SET_ACTIVE_AREA") { const workspace = clone.workspaces.find((item) => item.id === clone.context.workspaceId); const area = workspace?.areas.find((item) => item.id === edit.areaId); if (!area) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `Unknown area ${edit.areaId}`); clone.context.activeAreaId = area.id; clone.context.activeEditor = area.editor; } else if (edit.type === "SET_SELECTION") { if (new Set(edit.selectedIds).size !== edit.selectedIds.length || edit.selectedIds.length > EDITOR_WORKFLOW_BUDGET.maxSelection || edit.selectedIds.some((id) => typeof id !== "string" || !id)) throw new EditorWorkflowValidationError("EDITOR_SELECTION_INVALID", "Selection is invalid"); if (edit.activeObjectId !== null && !edit.selectedIds.includes(edit.activeObjectId)) throw new EditorWorkflowValidationError("EDITOR_SELECTION_INVALID", "Active object must be selected"); clone.context.selection = [...edit.selectedIds]; clone.context.activeObjectId = edit.activeObjectId; } else { const area = clone.workspaces.find((item) => item.id === clone.context.workspaceId)?.areas.find((item) => item.id === edit.areaId); const region = area?.regions.find((item) => item.id === edit.regionId); if (!region) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `Unknown region ${edit.regionId}`); region.visible = edit.visible; } clone.context.revision += 1; return parseEditorWorkflow(clone); } export function gateEditorOperation(operation: "READ_ONLY_VIEW" | "SELECTION_SYNC" | "KEYMAP" | "WRITER" | "GIZMO" | "TOUCH_DRAG"): CapabilityGateResult { if (["READ_ONLY_VIEW", "SELECTION_SYNC", "KEYMAP"].includes(operation)) return readyGate("N-024", operation); return blockedGate("N-024", operation, [capabilityIssue(operation === "GIZMO" || operation === "TOUCH_DRAG" ? "EDITOR_GIZMO_UNAVAILABLE" : "EDITOR_WRITER_UNAVAILABLE", `${operation} requires editor-specific Main transaction and interaction verification`)]); }