import { EDITOR_WORKFLOW_BUDGET, EditorWorkflowValidationError, parseEditorWorkflow, type EditorAreaIR, type EditorRegionIR, type EditorWorkflowIR, type EditorWorkspaceIR, } from "./editor-workflow"; export interface EditorView2DRectIR { xmin: number; xmax: number; ymin: number; ymax: number } export interface EditorView2DStateIR { cur: EditorView2DRectIR; mask: { xmin: number; xmax: number; ymin: number; ymax: number } } export type EditorView2DRegionIR = EditorRegionIR & { view2d?: EditorView2DStateIR }; export type EditorView2DAreaIR = Omit & { regions: EditorView2DRegionIR[] }; export type EditorView2DWorkspaceIR = Omit & { areas: EditorView2DAreaIR[] }; export type EditorWorkflowWithView2DIR = Omit & { workspaces: EditorView2DWorkspaceIR[] }; const COORDINATE_LIMIT = 1e12; const MASK_LIMIT = 2_147_483_647; const record = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value); const finite = (value: unknown, name: string, min: number, max: number): number => { if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${name} is outside the bounded range`); return value; }; const integer = (value: unknown, name: string, min: number, max: number): number => { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min || value > max) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${name} is outside the bounded range`); return value; }; function parseRect(value: unknown, name: string): EditorView2DRectIR { if (!record(value)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${name} is invalid`); const rect = { xmin: finite(value.xmin, `${name}.xmin`, -COORDINATE_LIMIT, COORDINATE_LIMIT), xmax: finite(value.xmax, `${name}.xmax`, -COORDINATE_LIMIT, COORDINATE_LIMIT), ymin: finite(value.ymin, `${name}.ymin`, -COORDINATE_LIMIT, COORDINATE_LIMIT), ymax: finite(value.ymax, `${name}.ymax`, -COORDINATE_LIMIT, COORDINATE_LIMIT), }; if (rect.xmax < rect.xmin || rect.ymax < rect.ymin) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${name} bounds are inverted`); return rect; } function parseState(value: unknown, name: string): EditorView2DStateIR { if (!record(value) || !record(value.mask)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${name} is invalid`); const mask = { xmin: integer(value.mask.xmin, `${name}.mask.xmin`, -MASK_LIMIT, MASK_LIMIT), xmax: integer(value.mask.xmax, `${name}.mask.xmax`, -MASK_LIMIT, MASK_LIMIT), ymin: integer(value.mask.ymin, `${name}.mask.ymin`, -MASK_LIMIT, MASK_LIMIT), ymax: integer(value.mask.ymax, `${name}.mask.ymax`, -MASK_LIMIT, MASK_LIMIT), }; if (mask.xmax < mask.xmin || mask.ymax < mask.ymin) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${name}.mask bounds are inverted`); return { cur: parseRect(value.cur, `${name}.cur`), mask }; } export function parseEditorWorkflowWithView2D(value: unknown): EditorWorkflowWithView2DIR { const workflow = parseEditorWorkflow(value); if (!record(value) || !Array.isArray(value.workspaces)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", "Editor workflow workspaces are invalid"); const clone = structuredClone(workflow) as EditorWorkflowWithView2DIR; value.workspaces.forEach((workspaceValue, workspaceIndex) => { if (!record(workspaceValue) || !Array.isArray(workspaceValue.areas)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `workspaces[${workspaceIndex}] is invalid`); workspaceValue.areas.forEach((areaValue, areaIndex) => { if (!record(areaValue) || !Array.isArray(areaValue.regions)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `workspaces[${workspaceIndex}].areas[${areaIndex}] is invalid`); const targetArea = clone.workspaces[workspaceIndex]?.areas[areaIndex]; if (!targetArea) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `workspaces[${workspaceIndex}].areas[${areaIndex}] is missing`); areaValue.regions.forEach((regionValue, regionIndex) => { if (!record(regionValue) || regionValue.view2d === undefined) return; const targetRegion = targetArea.regions[regionIndex]; if (!targetRegion) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `workspaces[${workspaceIndex}].areas[${areaIndex}].regions[${regionIndex}] is missing`); targetRegion.view2d = parseState(regionValue.view2d, `workspaces[${workspaceIndex}].areas[${areaIndex}].regions[${regionIndex}].view2d`); }); }); }); if (clone.workspaces.length > EDITOR_WORKFLOW_BUDGET.maxWorkspaces) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_BUDGET_EXCEEDED", "Editor workflow exceeds the budget"); return clone; }