Advance Blender WebEngine N-015 through N-022 parity
This commit is contained in:
@@ -1,63 +1,296 @@
|
||||
import { blockedGate, capabilityIssue, readyGate, type CapabilityGateResult } from "./capability-gates";
|
||||
import type { ErrorCode } from "./error";
|
||||
|
||||
export const SELECTION_HISTORY_SCHEMA = 1 as const;
|
||||
export const SELECTION_HISTORY_BUDGET = { maxEntries: 256, maxObjects: 100_000, maxElements: 1_000_000 } as const;
|
||||
export const SELECTION_HISTORY_SCHEMA = 2 as const;
|
||||
export const SELECTION_HISTORY_BUDGET = {
|
||||
maxEntries: 256,
|
||||
maxObjects: 100_000,
|
||||
maxTargets: 100_000,
|
||||
maxElements: 1_000_000,
|
||||
maxRangePatches: 4096,
|
||||
} as const;
|
||||
|
||||
export type SelectionElementMode = "VERT" | "EDGE" | "FACE";
|
||||
export type NonMeshSelectionKind = "CONTROL_POINT" | "HANDLE_LEFT" | "HANDLE_RIGHT";
|
||||
export interface SelectionStateIR { activeObjectId: string | null; objectIds: string[]; meshId: string | null; elementMode: SelectionElementMode; elementIndices: number[]; nonMeshKind?: NonMeshSelectionKind }
|
||||
export interface SelectionHistoryIR { schemaVersion: typeof SELECTION_HISTORY_SCHEMA; revision: number; cursor: number; entries: SelectionStateIR[] }
|
||||
export interface RaycastSelectionHitIR { sourceRevision: number; dataId: string; mode: SelectionElementMode; index: number; distance: number; point: [number, number, number]; nonMeshKind?: NonMeshSelectionKind }
|
||||
|
||||
export interface SelectionElementTargetIR {
|
||||
objectId: string;
|
||||
dataId: string;
|
||||
mode: SelectionElementMode;
|
||||
indices: number[];
|
||||
nonMeshKind?: NonMeshSelectionKind;
|
||||
}
|
||||
|
||||
export interface SelectionStateIR {
|
||||
activeObjectId: string | null;
|
||||
objectIds: string[];
|
||||
targets: SelectionElementTargetIR[];
|
||||
}
|
||||
|
||||
export interface SelectionHistoryIR {
|
||||
schemaVersion: typeof SELECTION_HISTORY_SCHEMA;
|
||||
revision: number;
|
||||
cursor: number;
|
||||
entries: SelectionStateIR[];
|
||||
}
|
||||
|
||||
export interface SelectionRangePatchIR {
|
||||
objectId: string;
|
||||
dataId: string;
|
||||
mode: SelectionElementMode;
|
||||
start: number;
|
||||
end: number;
|
||||
selected: boolean;
|
||||
nonMeshKind?: NonMeshSelectionKind;
|
||||
}
|
||||
|
||||
export interface RaycastSelectionHitIR {
|
||||
sourceRevision: number;
|
||||
objectId?: string;
|
||||
dataId: string;
|
||||
mode: SelectionElementMode;
|
||||
index: number;
|
||||
distance: number;
|
||||
point: [number, number, number];
|
||||
nonMeshKind?: NonMeshSelectionKind;
|
||||
}
|
||||
|
||||
export class SelectionHistoryValidationError extends Error {
|
||||
readonly code: ErrorCode;
|
||||
constructor(code: ErrorCode, message: string) { super(`${code}: ${message}`); this.name = "SelectionHistoryValidationError"; this.code = code; }
|
||||
|
||||
constructor(code: ErrorCode, message: string) {
|
||||
super(`${code}: ${message}`);
|
||||
this.name = "SelectionHistoryValidationError";
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
function record(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
||||
function integer(value: unknown, name: string, minimum: number, maximum: number): number { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", `${name} is outside the bounded range`); return value; }
|
||||
function ids(value: unknown, name: string, maximum: number): string[] { if (!Array.isArray(value) || value.length > maximum || value.some((item) => typeof item !== "string" || item.length === 0 || item.length > 256) || new Set(value).size !== value.length) throw new SelectionHistoryValidationError(value instanceof Array && value.length > maximum ? "SELECTION_HISTORY_BUDGET_EXCEEDED" : "SELECTION_HISTORY_INVALID", `${name} is invalid`); return [...value] as string[]; }
|
||||
function record(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function integer(value: unknown, name: string, minimum: number, maximum: number): number {
|
||||
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
||||
throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", `${name} is outside the bounded range`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function id(value: unknown, name: string): string {
|
||||
if (typeof value !== "string" || value.length === 0 || value.length > 256) {
|
||||
throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", `${name} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function ids(value: unknown, name: string, maximum: number): string[] {
|
||||
if (!Array.isArray(value) || value.length > maximum || value.some((item) => typeof item !== "string" || item.length === 0 || item.length > 256) || new Set(value).size !== value.length) {
|
||||
throw new SelectionHistoryValidationError(
|
||||
value instanceof Array && value.length > maximum ? "SELECTION_HISTORY_BUDGET_EXCEEDED" : "SELECTION_HISTORY_INVALID",
|
||||
`${name} is invalid`,
|
||||
);
|
||||
}
|
||||
return [...value] as string[];
|
||||
}
|
||||
|
||||
function mode(value: unknown, name: string): SelectionElementMode {
|
||||
if (!["VERT", "EDGE", "FACE"].includes(value as string)) {
|
||||
throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", `${name} is invalid`);
|
||||
}
|
||||
return value as SelectionElementMode;
|
||||
}
|
||||
|
||||
function kind(value: unknown, code: "SELECTION_HISTORY_INVALID" | "RAYCAST_HIT_INVALID"): NonMeshSelectionKind | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (!["CONTROL_POINT", "HANDLE_LEFT", "HANDLE_RIGHT"].includes(value as string)) {
|
||||
throw new SelectionHistoryValidationError(code, "Non-mesh selection identity is invalid");
|
||||
}
|
||||
return value as NonMeshSelectionKind;
|
||||
}
|
||||
|
||||
function indices(value: unknown, name: string): number[] {
|
||||
if (!Array.isArray(value) || value.length > SELECTION_HISTORY_BUDGET.maxElements || value.some((item) => !Number.isSafeInteger(item) || item < 0) || new Set(value).size !== value.length) {
|
||||
throw new SelectionHistoryValidationError(
|
||||
value instanceof Array && value.length > SELECTION_HISTORY_BUDGET.maxElements ? "SELECTION_HISTORY_BUDGET_EXCEEDED" : "SELECTION_HISTORY_INVALID",
|
||||
`${name} is invalid`,
|
||||
);
|
||||
}
|
||||
return [...value].sort((left, right) => left - right) as number[];
|
||||
}
|
||||
|
||||
function targetKey(target: Pick<SelectionElementTargetIR, "objectId" | "dataId" | "mode" | "nonMeshKind">): string {
|
||||
return `${target.objectId}\0${target.dataId}\0${target.mode}\0${target.nonMeshKind ?? "MESH"}`;
|
||||
}
|
||||
|
||||
function parseTarget(value: unknown, path: string): SelectionElementTargetIR {
|
||||
if (!record(value)) throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", `${path} is invalid`);
|
||||
const parsedKind = kind(value.nonMeshKind, "SELECTION_HISTORY_INVALID");
|
||||
return {
|
||||
objectId: id(value.objectId, `${path}.objectId`),
|
||||
dataId: id(value.dataId, `${path}.dataId`),
|
||||
mode: mode(value.mode, `${path}.mode`),
|
||||
indices: indices(value.indices, `${path}.indices`),
|
||||
...(parsedKind ? { nonMeshKind: parsedKind } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function migrateLegacyTarget(value: Record<string, unknown>, objectIds: string[], activeObjectId: string | null): SelectionElementTargetIR[] {
|
||||
const legacyIndices = value.elementIndices === undefined ? [] : indices(value.elementIndices, "elementIndices");
|
||||
if (legacyIndices.length === 0) return [];
|
||||
const dataId = value.meshId === null ? null : id(value.meshId, "meshId");
|
||||
if (!dataId) throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", "Element selection requires dataId");
|
||||
const objectId = activeObjectId ?? objectIds[0];
|
||||
if (!objectId) throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", "Element selection requires an owning object");
|
||||
const parsedKind = kind(value.nonMeshKind, "SELECTION_HISTORY_INVALID");
|
||||
return [{
|
||||
objectId,
|
||||
dataId,
|
||||
mode: mode(value.elementMode, "elementMode"),
|
||||
indices: legacyIndices,
|
||||
...(parsedKind ? { nonMeshKind: parsedKind } : {}),
|
||||
}];
|
||||
}
|
||||
|
||||
export function parseSelectionState(value: unknown): SelectionStateIR {
|
||||
if (!record(value) || !["VERT", "EDGE", "FACE"].includes(value.elementMode as string)) throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", "Selection state is invalid");
|
||||
if (!record(value)) throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", "Selection state is invalid");
|
||||
const objectIds = ids(value.objectIds, "objectIds", SELECTION_HISTORY_BUDGET.maxObjects);
|
||||
const activeObjectId = value.activeObjectId === null ? null : typeof value.activeObjectId === "string" ? value.activeObjectId : (() => { throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", "activeObjectId is invalid"); })();
|
||||
if (activeObjectId !== null && !objectIds.includes(activeObjectId)) throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", "Active object must be selected");
|
||||
if (!Array.isArray(value.elementIndices) || value.elementIndices.length > SELECTION_HISTORY_BUDGET.maxElements || value.elementIndices.some((item) => !Number.isSafeInteger(item) || item < 0) || new Set(value.elementIndices).size !== value.elementIndices.length) throw new SelectionHistoryValidationError(value.elementIndices instanceof Array && value.elementIndices.length > SELECTION_HISTORY_BUDGET.maxElements ? "SELECTION_HISTORY_BUDGET_EXCEEDED" : "SELECTION_HISTORY_INVALID", "elementIndices are invalid");
|
||||
const meshId = value.meshId === null ? null : typeof value.meshId === "string" && value.meshId.length > 0 ? value.meshId : (() => { throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", "meshId is invalid"); })();
|
||||
if (meshId === null && value.elementIndices.length > 0) throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", "Element selection requires meshId");
|
||||
const nonMeshKind = value.nonMeshKind === undefined ? undefined : ["CONTROL_POINT", "HANDLE_LEFT", "HANDLE_RIGHT"].includes(value.nonMeshKind as string) ? value.nonMeshKind as NonMeshSelectionKind : (() => { throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", "nonMeshKind is invalid"); })();
|
||||
return { activeObjectId, objectIds, meshId, elementMode: value.elementMode as SelectionElementMode, elementIndices: [...value.elementIndices].sort((a, b) => a - b) as number[], ...(nonMeshKind ? { nonMeshKind } : {}) };
|
||||
const activeObjectId = value.activeObjectId === null ? null : id(value.activeObjectId, "activeObjectId");
|
||||
if (activeObjectId !== null && !objectIds.includes(activeObjectId)) {
|
||||
throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", "Active object must be selected");
|
||||
}
|
||||
const targets = Array.isArray(value.targets)
|
||||
? value.targets.map((target, index) => parseTarget(target, `targets[${index}]`))
|
||||
: migrateLegacyTarget(value, objectIds, activeObjectId);
|
||||
if (targets.length > SELECTION_HISTORY_BUDGET.maxTargets) {
|
||||
throw new SelectionHistoryValidationError("SELECTION_HISTORY_BUDGET_EXCEEDED", "Selection target count exceeds the budget");
|
||||
}
|
||||
let elementCount = 0;
|
||||
const keys = new Set<string>();
|
||||
for (const target of targets) {
|
||||
if (!objectIds.includes(target.objectId)) {
|
||||
throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", "Element target owner must be selected");
|
||||
}
|
||||
const key = targetKey(target);
|
||||
if (keys.has(key)) throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", "Selection targets must be unique");
|
||||
keys.add(key);
|
||||
elementCount += target.indices.length;
|
||||
if (!Number.isSafeInteger(elementCount) || elementCount > SELECTION_HISTORY_BUDGET.maxElements) {
|
||||
throw new SelectionHistoryValidationError("SELECTION_HISTORY_BUDGET_EXCEEDED", "Selected element count exceeds the budget");
|
||||
}
|
||||
}
|
||||
targets.sort((left, right) => targetKey(left).localeCompare(targetKey(right)));
|
||||
return { activeObjectId, objectIds: [...objectIds].sort(), targets };
|
||||
}
|
||||
|
||||
export function parseSelectionHistory(value: unknown): SelectionHistoryIR {
|
||||
if (!record(value) || value.schemaVersion !== SELECTION_HISTORY_SCHEMA || !Array.isArray(value.entries)) throw new SelectionHistoryValidationError("PROTOCOL_MISMATCH", "Unsupported selection history schema");
|
||||
if (value.entries.length === 0 || value.entries.length > SELECTION_HISTORY_BUDGET.maxEntries) throw new SelectionHistoryValidationError("SELECTION_HISTORY_BUDGET_EXCEEDED", "Selection history entry count exceeds the budget");
|
||||
return { schemaVersion: SELECTION_HISTORY_SCHEMA, revision: integer(value.revision, "revision", 0, Number.MAX_SAFE_INTEGER), cursor: integer(value.cursor, "cursor", 0, value.entries.length - 1), entries: value.entries.map(parseSelectionState) };
|
||||
if (!record(value) || ![1, SELECTION_HISTORY_SCHEMA].includes(value.schemaVersion as number) || !Array.isArray(value.entries)) {
|
||||
throw new SelectionHistoryValidationError("PROTOCOL_MISMATCH", "Unsupported selection history schema");
|
||||
}
|
||||
if (value.entries.length === 0 || value.entries.length > SELECTION_HISTORY_BUDGET.maxEntries) {
|
||||
throw new SelectionHistoryValidationError("SELECTION_HISTORY_BUDGET_EXCEEDED", "Selection history entry count exceeds the budget");
|
||||
}
|
||||
return {
|
||||
schemaVersion: SELECTION_HISTORY_SCHEMA,
|
||||
revision: integer(value.revision, "revision", 0, Number.MAX_SAFE_INTEGER),
|
||||
cursor: integer(value.cursor, "cursor", 0, value.entries.length - 1),
|
||||
entries: value.entries.map(parseSelectionState),
|
||||
};
|
||||
}
|
||||
|
||||
function equalState(a: SelectionStateIR, b: SelectionStateIR): boolean { return a.activeObjectId === b.activeObjectId && a.meshId === b.meshId && a.elementMode === b.elementMode && a.nonMeshKind === b.nonMeshKind && a.objectIds.join("\0") === b.objectIds.join("\0") && a.elementIndices.join(",") === b.elementIndices.join(","); }
|
||||
function equalState(left: SelectionStateIR, right: SelectionStateIR): boolean {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
|
||||
export function recordSelection(value: unknown, revision: number, stateValue: unknown): SelectionHistoryIR {
|
||||
const history = parseSelectionHistory(value); if (revision !== history.revision) throw new SelectionHistoryValidationError("REVISION_CONFLICT", "Selection history revision is stale"); const state = parseSelectionState(stateValue);
|
||||
const history = parseSelectionHistory(value);
|
||||
if (revision !== history.revision) throw new SelectionHistoryValidationError("REVISION_CONFLICT", "Selection history revision is stale");
|
||||
const state = parseSelectionState(stateValue);
|
||||
if (equalState(history.entries[history.cursor], state)) return history;
|
||||
const entries = history.entries.slice(0, history.cursor + 1); entries.push(state); if (entries.length > SELECTION_HISTORY_BUDGET.maxEntries) entries.shift();
|
||||
const entries = history.entries.slice(0, history.cursor + 1);
|
||||
entries.push(state);
|
||||
if (entries.length > SELECTION_HISTORY_BUDGET.maxEntries) entries.shift();
|
||||
return parseSelectionHistory({ schemaVersion: SELECTION_HISTORY_SCHEMA, revision: history.revision + 1, cursor: entries.length - 1, entries });
|
||||
}
|
||||
|
||||
export function stepSelectionHistory(value: unknown, revision: number, direction: "UNDO" | "REDO"): SelectionHistoryIR {
|
||||
const history = parseSelectionHistory(value); if (revision !== history.revision) throw new SelectionHistoryValidationError("REVISION_CONFLICT", "Selection history revision is stale"); const cursor = history.cursor + (direction === "UNDO" ? -1 : 1);
|
||||
if (cursor < 0 || cursor >= history.entries.length) throw new SelectionHistoryValidationError("SELECTION_UNDO_UNAVAILABLE", `${direction} has no selection entry`);
|
||||
const history = parseSelectionHistory(value);
|
||||
if (revision !== history.revision) throw new SelectionHistoryValidationError("REVISION_CONFLICT", "Selection history revision is stale");
|
||||
const cursor = history.cursor + (direction === "UNDO" ? -1 : 1);
|
||||
if (cursor < 0 || cursor >= history.entries.length) {
|
||||
throw new SelectionHistoryValidationError("SELECTION_UNDO_UNAVAILABLE", `${direction} has no selection entry`);
|
||||
}
|
||||
return { ...history, revision: history.revision + 1, cursor };
|
||||
}
|
||||
|
||||
export function patchSelectionRanges(stateValue: unknown, patchesValue: unknown): SelectionStateIR {
|
||||
const state = parseSelectionState(stateValue);
|
||||
if (!Array.isArray(patchesValue) || patchesValue.length === 0 || patchesValue.length > SELECTION_HISTORY_BUDGET.maxRangePatches) {
|
||||
throw new SelectionHistoryValidationError(
|
||||
Array.isArray(patchesValue) && patchesValue.length > SELECTION_HISTORY_BUDGET.maxRangePatches ? "SELECTION_HISTORY_BUDGET_EXCEEDED" : "SELECTION_HISTORY_INVALID",
|
||||
"Selection range patches are invalid",
|
||||
);
|
||||
}
|
||||
const targets = new Map(state.targets.map((target) => [targetKey(target), { ...target, indices: new Set(target.indices) }]));
|
||||
let touched = 0;
|
||||
for (let patchIndex = 0; patchIndex < patchesValue.length; patchIndex++) {
|
||||
const value = patchesValue[patchIndex];
|
||||
if (!record(value) || typeof value.selected !== "boolean") {
|
||||
throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", `patches[${patchIndex}] is invalid`);
|
||||
}
|
||||
const parsedKind = kind(value.nonMeshKind, "SELECTION_HISTORY_INVALID");
|
||||
const patch: SelectionRangePatchIR = {
|
||||
objectId: id(value.objectId, `patches[${patchIndex}].objectId`),
|
||||
dataId: id(value.dataId, `patches[${patchIndex}].dataId`),
|
||||
mode: mode(value.mode, `patches[${patchIndex}].mode`),
|
||||
start: integer(value.start, `patches[${patchIndex}].start`, 0, Number.MAX_SAFE_INTEGER),
|
||||
end: integer(value.end, `patches[${patchIndex}].end`, 0, Number.MAX_SAFE_INTEGER),
|
||||
selected: value.selected,
|
||||
...(parsedKind ? { nonMeshKind: parsedKind } : {}),
|
||||
};
|
||||
if (patch.end < patch.start) throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", `patches[${patchIndex}] range is reversed`);
|
||||
const length = patch.end - patch.start + 1;
|
||||
touched += length;
|
||||
if (!Number.isSafeInteger(touched) || touched > SELECTION_HISTORY_BUDGET.maxElements) {
|
||||
throw new SelectionHistoryValidationError("SELECTION_HISTORY_BUDGET_EXCEEDED", "Selection range patch span exceeds the budget");
|
||||
}
|
||||
if (!state.objectIds.includes(patch.objectId)) {
|
||||
throw new SelectionHistoryValidationError("SELECTION_HISTORY_INVALID", "Selection range patch owner must be selected");
|
||||
}
|
||||
const key = targetKey(patch);
|
||||
const target = targets.get(key) ?? { objectId: patch.objectId, dataId: patch.dataId, mode: patch.mode, indices: new Set<number>(), ...(patch.nonMeshKind ? { nonMeshKind: patch.nonMeshKind } : {}) };
|
||||
for (let index = patch.start; index <= patch.end; index++) {
|
||||
if (patch.selected) target.indices.add(index);
|
||||
else target.indices.delete(index);
|
||||
}
|
||||
if (target.indices.size === 0) targets.delete(key);
|
||||
else targets.set(key, target);
|
||||
}
|
||||
return parseSelectionState({
|
||||
activeObjectId: state.activeObjectId,
|
||||
objectIds: state.objectIds,
|
||||
targets: [...targets.values()].map((target) => ({ ...target, indices: [...target.indices] })),
|
||||
});
|
||||
}
|
||||
|
||||
export function parseRaycastSelectionHit(value: unknown, expectedRevision: number): RaycastSelectionHitIR {
|
||||
if (!record(value) || value.sourceRevision !== expectedRevision || typeof value.dataId !== "string" || !value.dataId || !["VERT", "EDGE", "FACE"].includes(value.mode as string) || !Number.isSafeInteger(value.index) || (value.index as number) < 0 || typeof value.distance !== "number" || !Number.isFinite(value.distance) || value.distance < 0 || !Array.isArray(value.point) || value.point.length !== 3 || value.point.some((item) => typeof item !== "number" || !Number.isFinite(item))) throw new SelectionHistoryValidationError("RAYCAST_HIT_INVALID", "Raycast hit is stale or invalid");
|
||||
const nonMeshKind = value.nonMeshKind === undefined ? undefined : ["CONTROL_POINT", "HANDLE_LEFT", "HANDLE_RIGHT"].includes(value.nonMeshKind as string) ? value.nonMeshKind as NonMeshSelectionKind : (() => { throw new SelectionHistoryValidationError("RAYCAST_HIT_INVALID", "Raycast non-mesh identity is invalid"); })();
|
||||
return { sourceRevision: value.sourceRevision as number, dataId: value.dataId, mode: value.mode as SelectionElementMode, index: value.index as number, distance: value.distance, point: value.point as [number, number, number], ...(nonMeshKind ? { nonMeshKind } : {}) };
|
||||
if (!record(value) || value.sourceRevision !== expectedRevision || typeof value.dataId !== "string" || !value.dataId || !["VERT", "EDGE", "FACE"].includes(value.mode as string) || !Number.isSafeInteger(value.index) || (value.index as number) < 0 || typeof value.distance !== "number" || !Number.isFinite(value.distance) || value.distance < 0 || !Array.isArray(value.point) || value.point.length !== 3 || value.point.some((item) => typeof item !== "number" || !Number.isFinite(item))) {
|
||||
throw new SelectionHistoryValidationError("RAYCAST_HIT_INVALID", "Raycast hit is stale or invalid");
|
||||
}
|
||||
const parsedKind = kind(value.nonMeshKind, "RAYCAST_HIT_INVALID");
|
||||
const objectId = value.objectId === undefined ? undefined : id(value.objectId, "objectId");
|
||||
return {
|
||||
sourceRevision: value.sourceRevision as number,
|
||||
...(objectId ? { objectId } : {}),
|
||||
dataId: value.dataId,
|
||||
mode: value.mode as SelectionElementMode,
|
||||
index: value.index as number,
|
||||
distance: value.distance,
|
||||
point: value.point as [number, number, number],
|
||||
...(parsedKind ? { nonMeshKind: parsedKind } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function gateSelectionInteraction(operation: "RAYCAST" | "HISTORY" | "GIZMO"): CapabilityGateResult {
|
||||
if (operation !== "GIZMO") return readyGate("N-015", operation);
|
||||
return blockedGate("N-015", operation, [capabilityIssue("CAPABILITY_MISSING", "Curve/non-mesh gizmo interaction is not implemented")]);
|
||||
return blockedGate("N-015", operation, [capabilityIssue("CAPABILITY_MISSING", "Curve gizmo preview remains unavailable; bounded multi-handle commit is supported")]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user