Advance WebGPU volume and bounded workflows
This commit is contained in:
@@ -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)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user