chore: checkpoint web Blender migration source, tools and docs
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>
This commit is contained in:
74
web/protocol/editor-view2d.ts
Normal file
74
web/protocol/editor-view2d.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user