Advance WebGPU volume and bounded workflows

This commit is contained in:
mes123456
2026-08-14 18:08:29 -04:00
parent 3da1dfc804
commit 68d50f810f
119 changed files with 9028 additions and 430 deletions

View File

@@ -12,8 +12,18 @@ export interface EditorRegionIR { id: string; kind: EditorRegionKind; visible: b
export interface EditorAreaIR { id: string; editor: EditorTypeIR; regions: EditorRegionIR[]; rect: { x: number; y: number; width: number; height: number }; maximized: boolean }
export interface EditorWorkspaceIR { id: string; name: string; areas: EditorAreaIR[]; activeAreaId: string; revision: number }
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 }
export interface KeymapBindingIR {
id: string;
key: string;
modifiers: string[];
command: string;
enabled: boolean;
workspaceIds?: string[];
editors?: EditorTypeIR[];
modes?: EditorMode[];
}
export interface EditorWorkflowIR { schemaVersion: typeof EDITOR_WORKFLOW_SCHEMA; workspaces: EditorWorkspaceIR[]; context: EditorContextIR; keymaps: KeymapBindingIR[] }
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 }
@@ -38,6 +48,8 @@ function rect(value: unknown, name: string): EditorAreaIR["rect"] {
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<T>(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 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");
@@ -64,10 +76,47 @@ export function parseEditorWorkflow(value: unknown): EditorWorkflowIR {
if (!EDITOR_TYPES.includes(contextValue.activeEditor as EditorTypeIR) || !["OBJECT", "EDIT", "POSE"].includes(contextValue.mode as string) || !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<string>(); const keymaps = value.keymaps.map((bindingValue, index): KeymapBindingIR => { 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)); if (new Set(modifiers).size !== modifiers.length) throw new EditorWorkflowValidationError("EDITOR_KEYMAP_INVALID", `${name} modifiers duplicate`); return { id, key: text(bindingValue.key, `${name}.key`, 32), modifiers, command: text(bindingValue.command, `${name}.command`, 128), enabled: bindingValue.enabled !== false }; });
const keymapIds = new Set<string>(); 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 = <T extends string>(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", ["OBJECT", "EDIT", "POSE"] as const) };
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, 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<KeyboardEvent, "key" | "altKey" | "ctrlKey" | "metaKey" | "shiftKey">): 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"; }