Files
workinf_Blender_Wasm/web/protocol/editor-view2d.ts
mes123456 f3e21c6b98
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
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>
2026-11-10 11:18:01 +08:00

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;
}