Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
75 lines
4.9 KiB
TypeScript
75 lines
4.9 KiB
TypeScript
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<EditorAreaIR, "regions"> & { regions: EditorView2DRegionIR[] };
|
|
export type EditorView2DWorkspaceIR = Omit<EditorWorkspaceIR, "areas"> & { areas: EditorView2DAreaIR[] };
|
|
export type EditorWorkflowWithView2DIR = Omit<EditorWorkflowIR, "workspaces"> & { workspaces: EditorView2DWorkspaceIR[] };
|
|
|
|
const COORDINATE_LIMIT = 1e12;
|
|
const MASK_LIMIT = 2_147_483_647;
|
|
const record = (value: unknown): value is Record<string, unknown> => 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;
|
|
}
|