73 lines
3.5 KiB
TypeScript
73 lines
3.5 KiB
TypeScript
import { CorrectiveCommandRegistry } from "./corrective-command-registry";
|
|
import { CorrectiveCaseRunnerError, MainMutationCommand } from "./corrective-case-runner";
|
|
import { InMemoryMainStore, runAtomicMutation } from "./corrective-main-transaction";
|
|
|
|
export interface MeshVertexState {
|
|
id: string;
|
|
x: number;
|
|
y: number;
|
|
z: number;
|
|
}
|
|
|
|
export interface MeshMain {
|
|
vertices: MeshVertexState[];
|
|
}
|
|
|
|
const VERTEX_ID = /^[A-Za-z0-9._:-]{1,128}$/u;
|
|
const COORDINATE_LIMIT = 1_000_000;
|
|
|
|
function coordinate(value: unknown, label: string): number {
|
|
if (typeof value !== "number" || !Number.isFinite(value) || Math.abs(value) > COORDINATE_LIMIT) {
|
|
throw new CorrectiveCaseRunnerError("COMMAND_MALFORMED", `${label} must be a finite coordinate within bounds`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function meshPayload(command: MainMutationCommand): { vertexId: string; delta: { x: number; y: number; z: number } } {
|
|
const input = command.payload;
|
|
if (!input || typeof input !== "object" || Array.isArray(input) ||
|
|
typeof input.vertexId !== "string" || !VERTEX_ID.test(input.vertexId) ||
|
|
!input.delta || typeof input.delta !== "object" || Array.isArray(input.delta) ||
|
|
Object.keys(input).some((key) => !["vertexId", "delta"].includes(key))) {
|
|
throw new CorrectiveCaseRunnerError("COMMAND_MALFORMED", "mesh payload must contain only vertexId and delta");
|
|
}
|
|
const delta = input.delta as Record<string, unknown>;
|
|
if (Object.keys(delta).some((key) => !["x", "y", "z"].includes(key)) ||
|
|
!Object.prototype.hasOwnProperty.call(delta, "x") || !Object.prototype.hasOwnProperty.call(delta, "y") || !Object.prototype.hasOwnProperty.call(delta, "z")) {
|
|
throw new CorrectiveCaseRunnerError("COMMAND_MALFORMED", "mesh delta must contain only x, y, and z");
|
|
}
|
|
return { vertexId: input.vertexId, delta: { x: coordinate(delta.x, "delta.x"), y: coordinate(delta.y, "delta.y"), z: coordinate(delta.z, "delta.z") } };
|
|
}
|
|
|
|
function cloneMeshMain(value: MeshMain): MeshMain {
|
|
return { vertices: value.vertices.map((vertex) => ({ ...vertex })) };
|
|
}
|
|
|
|
export function createMeshRegistry(main: InMemoryMainStore<MeshMain>): CorrectiveCommandRegistry {
|
|
return new CorrectiveCommandRegistry().register({
|
|
commandType: "meshTranslateVertex",
|
|
targetKinds: ["MESH"],
|
|
authorizations: ["USER_EDIT", "SYSTEM"],
|
|
execute: async (command) => {
|
|
const change = meshPayload(command);
|
|
return runAtomicMutation(main, command, (draft) => {
|
|
const vertex = draft.vertices.find((candidate) => candidate.id === change.vertexId);
|
|
if (!vertex) throw new CorrectiveCaseRunnerError("COMMAND_UNSUPPORTED", `vertex ${change.vertexId} does not exist`);
|
|
const before = { x: vertex.x, y: vertex.y, z: vertex.z };
|
|
const after = { x: before.x + change.delta.x, y: before.y + change.delta.y, z: before.z + change.delta.z };
|
|
if ([after.x, after.y, after.z].some((value) => !Number.isFinite(value) || Math.abs(value) > COORDINATE_LIMIT)) {
|
|
throw new CorrectiveCaseRunnerError("COMMAND_MALFORMED", "mesh translation exceeds coordinate bounds");
|
|
}
|
|
vertex.x = after.x;
|
|
vertex.y = after.y;
|
|
vertex.z = after.z;
|
|
return { delta: { vertexId: vertex.id, before, after }, persistence: { dirty: true } };
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
export function createMeshMain(vertices: readonly MeshVertexState[], revision = 0, dirty = false): InMemoryMainStore<MeshMain> {
|
|
return new InMemoryMainStore({ vertices: vertices.map((vertex) => ({ ...vertex })) }, revision, dirty, cloneMeshMain);
|
|
}
|