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

@@ -118,6 +118,24 @@ export interface TrackingMaskProjectIR {
bindings: TrackingMaskBindingIR[];
}
export interface MaskRaycastHitIR {
maskId: string;
layerId: string;
splineId: string;
kind: "POINT" | "SEGMENT";
pointId: string;
nextPointId?: string;
distance: number;
parameter?: number;
}
export interface MaskPointSelectionIR {
maskId: string;
layerId: string;
splineId: string;
pointId: string;
}
export type TrackingMaskEditIR =
| { type: "SET_MARKER"; revision: number; clipId: string; trackId: string; marker: TrackingMarkerIR }
| { type: "DELETE_MARKER"; revision: number; clipId: string; trackId: string; frame: number }
@@ -327,3 +345,86 @@ export function gateTrackingOperation(operation: "MARKER_EDIT" | "MASK_EDIT" | "
if (operation === "BROWSER_TRACKING" && browserProbe === "VERIFIED") return readyGate("N-022", operation);
return blockedGate("N-022", operation, [capabilityIssue("TRACKING_SOLVE_UNAVAILABLE", operation === "CAMERA_SOLVE" ? "Camera solve requires a verified server Blender implementation" : "Browser tracking requires an explicit feature probe")]);
}
function bezierPoint(a: Vec2, b: Vec2, c: Vec2, d: Vec2, t: number): Vec2 {
const inverse = 1 - t;
return [inverse ** 3 * a[0] + 3 * inverse ** 2 * t * b[0] + 3 * inverse * t ** 2 * c[0] + t ** 3 * d[0], inverse ** 3 * a[1] + 3 * inverse ** 2 * t * b[1] + 3 * inverse * t ** 2 * c[1] + t ** 3 * d[1]];
}
export function raycastMaskProject(value: unknown, positionValue: unknown, thresholdValue = 0.02, segmentSamples = 24): MaskRaycastHitIR | null {
const project = parseTrackingMaskProject(value);
const position = vec2(positionValue, "position", -4, 4);
const threshold = finite(thresholdValue, "threshold", 0.000001, 1);
const samples = integer(segmentSamples, "segmentSamples", 2, 128);
let best: MaskRaycastHitIR | null = null;
const consider = (hit: MaskRaycastHitIR): void => { if (hit.distance <= threshold && (!best || hit.distance < best.distance || (hit.distance === best.distance && hit.kind === "POINT" && best.kind === "SEGMENT"))) best = hit; };
for (const mask of project.masks) for (const layer of mask.layers) {
if (!layer.visible || layer.locked || layer.opacity <= 0) continue;
for (const spline of layer.splines) {
for (const point of spline.points) consider({ maskId: mask.id, layerId: layer.id, splineId: spline.id, kind: "POINT", pointId: point.id, distance: Math.hypot(point.co[0] - position[0], point.co[1] - position[1]) });
const segmentCount = spline.cyclic ? spline.points.length : spline.points.length - 1;
for (let segment = 0; segment < segmentCount; segment++) {
const first = spline.points[segment]; const next = spline.points[(segment + 1) % spline.points.length];
for (let sample = 0; sample <= samples; sample++) {
const parameter = sample / samples;
const point = bezierPoint(first.co, first.handleRight, next.handleLeft, next.co, parameter);
consider({ maskId: mask.id, layerId: layer.id, splineId: spline.id, kind: "SEGMENT", pointId: first.id, nextPointId: next.id, distance: Math.hypot(point[0] - position[0], point[1] - position[1]), parameter });
}
}
}
}
return best;
}
function maskSelectionKey(selection: MaskPointSelectionIR): string {
return `${selection.maskId}\0${selection.layerId}\0${selection.splineId}\0${selection.pointId}`;
}
/** Applies deterministic replace/add/toggle marquee selection to editable Mask control points. */
export function selectMaskPointsInBounds(
value: unknown,
minimumValue: unknown,
maximumValue: unknown,
currentValue: unknown = [],
mode: "REPLACE" | "ADD" | "TOGGLE" = "REPLACE",
): MaskPointSelectionIR[] {
const project = parseTrackingMaskProject(value);
if (!(["REPLACE", "ADD", "TOGGLE"] as const).includes(mode)) throw new TrackingMaskValidationError("MASK_SCHEMA_INVALID", "Mask selection mode is invalid");
const minimum = vec2(minimumValue, "minimum", -4, 4);
const maximum = vec2(maximumValue, "maximum", -4, 4);
if (minimum[0] > maximum[0] || minimum[1] > maximum[1]) throw new TrackingMaskValidationError("MASK_SCHEMA_INVALID", "Mask selection bounds are inverted");
if (!Array.isArray(currentValue) || currentValue.length > TRACKING_MASK_BUDGET.maxMaskPoints) throw new TrackingMaskValidationError("TRACKING_BUDGET_EXCEEDED", "Mask selection exceeds the point budget");
const all: MaskPointSelectionIR[] = [];
const editable = new Set<string>();
for (const mask of project.masks) for (const layer of mask.layers) for (const spline of layer.splines) for (const point of spline.points) {
const selection = { maskId: mask.id, layerId: layer.id, splineId: spline.id, pointId: point.id };
all.push(selection);
if (layer.visible && !layer.locked && layer.opacity > 0) editable.add(maskSelectionKey(selection));
}
const allKeys = new Set(all.map(maskSelectionKey));
const current = new Set<string>();
currentValue.forEach((item, index) => {
if (!record(item)) throw new TrackingMaskValidationError("MASK_SCHEMA_INVALID", `current[${index}] is invalid`);
const selection = { maskId: text(item.maskId, `current[${index}].maskId`), layerId: text(item.layerId, `current[${index}].layerId`), splineId: text(item.splineId, `current[${index}].splineId`), pointId: text(item.pointId, `current[${index}].pointId`) };
const key = maskSelectionKey(selection);
if (!allKeys.has(key)) throw new TrackingMaskValidationError("TRACKING_BINDING_MISSING", `current[${index}] references a missing Mask point`);
if (current.has(key)) throw new TrackingMaskValidationError("MASK_SCHEMA_INVALID", `current[${index}] is duplicated`);
current.add(key);
});
const hits = new Set<string>();
for (const mask of project.masks) for (const layer of mask.layers) {
if (!layer.visible || layer.locked || layer.opacity <= 0) continue;
for (const spline of layer.splines) for (const point of spline.points) {
if (point.co[0] >= minimum[0] && point.co[0] <= maximum[0] && point.co[1] >= minimum[1] && point.co[1] <= maximum[1]) {
hits.add(maskSelectionKey({ maskId: mask.id, layerId: layer.id, splineId: spline.id, pointId: point.id }));
}
}
}
const selected = mode === "REPLACE" ? new Set<string>() : new Set(current);
for (const key of hits) {
if (!editable.has(key)) continue;
if (mode === "TOGGLE" && selected.has(key)) selected.delete(key);
else selected.add(key);
}
return all.filter((selection) => selected.has(maskSelectionKey(selection)));
}