Advance N-015 through N-018 bounded workflows
This commit is contained in:
93
web/protocol/nonmesh-interaction.ts
Normal file
93
web/protocol/nonmesh-interaction.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
export const CURVE_GIZMO_SCHEMA = 1 as const;
|
||||
|
||||
export const CURVE_GIZMO_BUDGET = {
|
||||
maxHandles: 256,
|
||||
maxIdLength: 256,
|
||||
maxCoordinate: 1_000_000,
|
||||
} as const;
|
||||
|
||||
export type CurveGizmoHandleSide = "LEFT" | "RIGHT" | "CONTROL";
|
||||
export type CurveGizmoPhase = "PREVIEW" | "COMMIT";
|
||||
|
||||
export interface CurveGizmoHandleIR {
|
||||
pointIndex: number;
|
||||
side: CurveGizmoHandleSide;
|
||||
position: [number, number, number];
|
||||
}
|
||||
|
||||
export interface CurveGizmoDragIR {
|
||||
schemaVersion: typeof CURVE_GIZMO_SCHEMA;
|
||||
dataId: string;
|
||||
baseRevision: number;
|
||||
phase: CurveGizmoPhase;
|
||||
axis: 0 | 1 | 2;
|
||||
delta: [number, number, number];
|
||||
handles: CurveGizmoHandleIR[];
|
||||
}
|
||||
|
||||
export interface AppliedCurveGizmoDragIR {
|
||||
dataId: string;
|
||||
phase: CurveGizmoPhase;
|
||||
axis: 0 | 1 | 2;
|
||||
revision: number;
|
||||
handles: CurveGizmoHandleIR[];
|
||||
}
|
||||
|
||||
function fail(path: string, message: string): never {
|
||||
throw new Error(`CURVE_GIZMO_INVALID: ${path} ${message}`);
|
||||
}
|
||||
|
||||
function record(value: unknown, path: string): Record<string, unknown> {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) fail(path, "must be an object");
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function integer(value: unknown, path: string, minimum = 0): number {
|
||||
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum) fail(path, "must be a bounded integer");
|
||||
return value;
|
||||
}
|
||||
|
||||
function finite(value: unknown, path: string): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || Math.abs(value) > CURVE_GIZMO_BUDGET.maxCoordinate) fail(path, "is outside the finite coordinate budget");
|
||||
return value;
|
||||
}
|
||||
|
||||
function vector(value: unknown, path: string): [number, number, number] {
|
||||
if (!Array.isArray(value) || value.length !== 3) fail(path, "must contain three coordinates");
|
||||
return [finite(value[0], `${path}[0]`), finite(value[1], `${path}[1]`), finite(value[2], `${path}[2]` )];
|
||||
}
|
||||
|
||||
export function parseCurveGizmoDrag(value: unknown, expectedRevision?: number): CurveGizmoDragIR {
|
||||
const drag = record(value, "drag");
|
||||
if (drag.schemaVersion !== CURVE_GIZMO_SCHEMA) fail("schemaVersion", "is unsupported");
|
||||
if (typeof drag.dataId !== "string" || drag.dataId.length === 0 || drag.dataId.length > CURVE_GIZMO_BUDGET.maxIdLength) fail("dataId", "is invalid");
|
||||
const baseRevision = integer(drag.baseRevision, "baseRevision");
|
||||
if (expectedRevision !== undefined && baseRevision !== expectedRevision) throw new Error("REVISION_CONFLICT: Curve gizmo request is stale");
|
||||
if (drag.phase !== "PREVIEW" && drag.phase !== "COMMIT") fail("phase", "is invalid");
|
||||
if (drag.axis !== 0 && drag.axis !== 1 && drag.axis !== 2) fail("axis", "must be X, Y or Z");
|
||||
const delta = vector(drag.delta, "delta");
|
||||
if (delta.some((component, axis) => axis !== drag.axis && component !== 0)) fail("delta", "must only move along the selected axis");
|
||||
if (!Array.isArray(drag.handles) || drag.handles.length === 0 || drag.handles.length > CURVE_GIZMO_BUDGET.maxHandles) fail("handles", "exceeds the handle budget");
|
||||
const seen = new Set<string>();
|
||||
const handles = drag.handles.map((item, index) => {
|
||||
const handle = record(item, `handles[${index}]`);
|
||||
const pointIndex = integer(handle.pointIndex, `handles[${index}].pointIndex`);
|
||||
if (handle.side !== "LEFT" && handle.side !== "RIGHT" && handle.side !== "CONTROL") fail(`handles[${index}].side`, "is invalid");
|
||||
const side = handle.side as CurveGizmoHandleSide;
|
||||
const key = `${pointIndex}:${side}`;
|
||||
if (seen.has(key)) fail(`handles[${index}]`, "duplicates a selected handle");
|
||||
seen.add(key);
|
||||
return { pointIndex, side, position: vector(handle.position, `handles[${index}].position`) };
|
||||
});
|
||||
return { schemaVersion: CURVE_GIZMO_SCHEMA, dataId: drag.dataId, baseRevision, phase: drag.phase, axis: drag.axis, delta, handles };
|
||||
}
|
||||
|
||||
export function applyCurveGizmoDelta(value: unknown, expectedRevision?: number): AppliedCurveGizmoDragIR {
|
||||
const drag = parseCurveGizmoDrag(value, expectedRevision);
|
||||
const handles = drag.handles.map((handle) => ({
|
||||
...handle,
|
||||
position: [handle.position[0] + drag.delta[0], handle.position[1] + drag.delta[1], handle.position[2] + drag.delta[2]] as [number, number, number],
|
||||
}));
|
||||
handles.forEach((handle, index) => vector(handle.position, `handles[${index}].position`));
|
||||
return { dataId: drag.dataId, phase: drag.phase, axis: drag.axis, revision: drag.baseRevision + (drag.phase === "COMMIT" ? 1 : 0), handles };
|
||||
}
|
||||
Reference in New Issue
Block a user