Advance WebGPU volume and bounded workflows
This commit is contained in:
@@ -21,14 +21,26 @@ export interface CurveGizmoDragIR {
|
||||
baseRevision: number;
|
||||
phase: CurveGizmoPhase;
|
||||
axis: 0 | 1 | 2;
|
||||
axisVector?: [number, number, number];
|
||||
delta: [number, number, number];
|
||||
handles: CurveGizmoHandleIR[];
|
||||
}
|
||||
|
||||
export interface CurveGizmoFrameIR {
|
||||
origin: [number, number, number];
|
||||
axes: [[number, number, number], [number, number, number], [number, number, number]];
|
||||
}
|
||||
|
||||
export interface CurveGizmoScreenFrameIR {
|
||||
origin: [number, number];
|
||||
axes: [[number, number], [number, number], [number, number]];
|
||||
}
|
||||
|
||||
export interface AppliedCurveGizmoDragIR {
|
||||
dataId: string;
|
||||
phase: CurveGizmoPhase;
|
||||
axis: 0 | 1 | 2;
|
||||
axisVector?: [number, number, number];
|
||||
revision: number;
|
||||
handles: CurveGizmoHandleIR[];
|
||||
}
|
||||
@@ -57,6 +69,64 @@ function vector(value: unknown, path: string): [number, number, number] {
|
||||
return [finite(value[0], `${path}[0]`), finite(value[1], `${path}[1]`), finite(value[2], `${path}[2]` )];
|
||||
}
|
||||
|
||||
function length(value: readonly number[]): number {
|
||||
return Math.hypot(value[0], value[1], value[2]);
|
||||
}
|
||||
|
||||
function normalize(value: readonly number[], path: string): [number, number, number] {
|
||||
const magnitude = length(value);
|
||||
if (!Number.isFinite(magnitude) || magnitude < 1e-8) fail(path, "must have a finite non-zero direction");
|
||||
return [value[0] / magnitude, value[1] / magnitude, value[2] / magnitude];
|
||||
}
|
||||
|
||||
function dot(left: readonly number[], right: readonly number[]): number {
|
||||
return left[0] * right[0] + left[1] * right[1] + left[2] * right[2];
|
||||
}
|
||||
|
||||
function cross(left: readonly number[], right: readonly number[]): [number, number, number] {
|
||||
return [left[1] * right[2] - left[2] * right[1], left[2] * right[0] - left[0] * right[2], left[0] * right[1] - left[1] * right[0]];
|
||||
}
|
||||
|
||||
export function deriveCurveHandleGizmoFrame(controlPoints: ArrayLike<number>, handles: readonly CurveGizmoHandleIR[]): CurveGizmoFrameIR {
|
||||
if (controlPoints.length === 0 || controlPoints.length % 3 !== 0) fail("controlPoints", "must contain finite XYZ coordinates");
|
||||
for (let index = 0; index < controlPoints.length; index += 1) {
|
||||
if (!Number.isFinite(controlPoints[index])) fail("controlPoints", "must contain finite XYZ coordinates");
|
||||
}
|
||||
if (handles.length === 0 || handles.length > CURVE_GIZMO_BUDGET.maxHandles) fail("handles", "exceeds the handle budget");
|
||||
const ordered = [...handles].sort((left, right) => left.pointIndex - right.pointIndex || left.side.localeCompare(right.side));
|
||||
const origin: [number, number, number] = [0, 0, 0];
|
||||
const directions: Array<[number, number, number]> = [];
|
||||
for (const [index, handle] of ordered.entries()) {
|
||||
if (!Number.isSafeInteger(handle.pointIndex) || handle.pointIndex < 0 || handle.pointIndex * 3 + 2 >= controlPoints.length || handle.side === "CONTROL") fail(`handles[${index}]`, "must identify a Curve handle with an existing control point");
|
||||
vector(handle.position, `handles[${index}].position`);
|
||||
origin[0] += handle.position[0];
|
||||
origin[1] += handle.position[1];
|
||||
origin[2] += handle.position[2];
|
||||
const point = handle.pointIndex * 3;
|
||||
directions.push(normalize([
|
||||
handle.position[0] - controlPoints[point],
|
||||
handle.position[1] - controlPoints[point + 1],
|
||||
handle.position[2] - controlPoints[point + 2],
|
||||
], `handles[${index}].direction`));
|
||||
}
|
||||
origin[0] /= ordered.length;
|
||||
origin[1] /= ordered.length;
|
||||
origin[2] /= ordered.length;
|
||||
const reference = directions[0];
|
||||
const aligned = directions.map((direction) => dot(direction, reference) < 0 ? direction.map((value) => -value) as [number, number, number] : direction);
|
||||
const axisX = normalize(aligned.reduce<[number, number, number]>((sum, direction) => [sum[0] + direction[0], sum[1] + direction[1], sum[2] + direction[2]], [0, 0, 0]), "handles.directionAverage");
|
||||
const up: [number, number, number] = Math.abs(axisX[2]) < 0.9 ? [0, 0, 1] : [0, 1, 0];
|
||||
const axisY = normalize(cross(up, axisX), "gizmo.axisY");
|
||||
const axisZ = normalize(cross(axisX, axisY), "gizmo.axisZ");
|
||||
return { origin, axes: [axisX, axisY, axisZ] };
|
||||
}
|
||||
|
||||
export function curveGizmoAxisDelta(frame: CurveGizmoFrameIR, axis: 0 | 1 | 2, amount: number): [number, number, number] {
|
||||
if (!Number.isFinite(amount) || Math.abs(amount) > CURVE_GIZMO_BUDGET.maxCoordinate) fail("amount", "is outside the finite coordinate budget");
|
||||
const direction = normalize(frame.axes[axis], `frame.axes[${axis}]`);
|
||||
return [direction[0] * amount, direction[1] * amount, direction[2] * amount];
|
||||
}
|
||||
|
||||
export function parseCurveGizmoDrag(value: unknown, expectedRevision?: number): CurveGizmoDragIR {
|
||||
const drag = record(value, "drag");
|
||||
if (drag.schemaVersion !== CURVE_GIZMO_SCHEMA) fail("schemaVersion", "is unsupported");
|
||||
@@ -66,7 +136,12 @@ export function parseCurveGizmoDrag(value: unknown, expectedRevision?: number):
|
||||
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");
|
||||
const axisVector = drag.axisVector === undefined ? undefined : normalize(vector(drag.axisVector, "axisVector"), "axisVector");
|
||||
if (axisVector) {
|
||||
const deltaLength = length(delta);
|
||||
if (deltaLength > 0 && length(cross(delta, axisVector)) > Math.max(1e-7, deltaLength * 1e-6)) fail("delta", "must be parallel to the selected local axis");
|
||||
}
|
||||
else 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) => {
|
||||
@@ -79,7 +154,7 @@ export function parseCurveGizmoDrag(value: unknown, expectedRevision?: number):
|
||||
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 };
|
||||
return { schemaVersion: CURVE_GIZMO_SCHEMA, dataId: drag.dataId, baseRevision, phase: drag.phase, axis: drag.axis, axisVector, delta, handles };
|
||||
}
|
||||
|
||||
export function applyCurveGizmoDelta(value: unknown, expectedRevision?: number): AppliedCurveGizmoDragIR {
|
||||
@@ -89,5 +164,5 @@ export function applyCurveGizmoDelta(value: unknown, expectedRevision?: number):
|
||||
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 };
|
||||
return { dataId: drag.dataId, phase: drag.phase, axis: drag.axis, axisVector: drag.axisVector, revision: drag.baseRevision + (drag.phase === "COMMIT" ? 1 : 0), handles };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user