Advance M8-M11 parity workflows
This commit is contained in:
@@ -11,13 +11,22 @@ import type { NonMeshElementKind } from "./nonmesh";
|
||||
import type { GreasePencilPointPreview, GreasePencilPointRef } from "./grease-pencil";
|
||||
import type { CurveGizmoFrameIR, CurveGizmoHandleIR } from "../../../protocol/nonmesh-interaction";
|
||||
import { cloneNanoVDBViewportAssets, nanoVDBViewportAssetTransferables, type NanoVDBViewportAssetIR } from "../volume/nanovdb-viewport";
|
||||
import type { GreasePencilDrawingScopeIR, GreasePencilMarqueeBoxIR, GreasePencilMarqueeResultIR } from "../../../protocol/grease-pencil-marquee";
|
||||
import {
|
||||
validatePaintDepthVisibilityRequest,
|
||||
validatePaintDepthVisibilityResult,
|
||||
type PaintDepthVisibilityRequestIR,
|
||||
type PaintDepthVisibilityResultIR,
|
||||
} from "../../../protocol/paint-depth-visibility";
|
||||
|
||||
export interface ViewportBackend {
|
||||
setSnapshot(snapshot: SceneSnapshotIR, geometryBuffers?: MeshGeometryBuffer[], nonMeshGeometryBuffers?: NonMeshGeometryChunk[]): void;
|
||||
setTextureAssets(assets: readonly GPUTextureAsset[]): void;
|
||||
setVolumeAssets(assets: readonly NanoVDBViewportAssetIR[]): void;
|
||||
setSelection(objectIds: ReadonlySet<string>, elementSelection?: ReadonlyMap<string, ReadonlyMap<NonMeshElementKind, ReadonlySet<number>>>, greasePencilPoints?: readonly GreasePencilPointRef[]): void;
|
||||
setSelection(objectIds: ReadonlySet<string>, elementSelection?: ReadonlyMap<string, ReadonlyMap<NonMeshElementKind, ReadonlySet<number>>>, greasePencilPoints?: readonly GreasePencilPointRef[], greasePencilSelectionRevision?: number): void;
|
||||
setInteractionMode(editMode: boolean, selectionMode: MeshElementMode): void;
|
||||
samplePaintVisibility(request: PaintDepthVisibilityRequestIR): Promise<PaintDepthVisibilityResultIR>;
|
||||
selectGreasePencilMarquee(drawing: GreasePencilDrawingScopeIR, box: GreasePencilMarqueeBoxIR, baseRevision: number, baseSelectionRevision: number, additive: boolean): void;
|
||||
setCurveHandlePreview(dataId: string, handles: readonly CurveGizmoHandleIR[] | null): void;
|
||||
setGreasePencilPointPreview(dataId: string, layerId: string, frame: number, points: readonly GreasePencilPointPreview[] | null): void;
|
||||
setCurveGizmoFrame(dataId: string | null, frame: CurveGizmoFrameIR | null): void;
|
||||
@@ -41,7 +50,8 @@ export function acquireOffscreenViewportRenderer(
|
||||
canvas: HTMLCanvasElement,
|
||||
onSelect?: (objectId: string, additive: boolean) => void,
|
||||
onElementSelect?: (meshId: string, mode: MeshElementMode, index: number, additive: boolean, nonMeshKind?: NonMeshElementKind) => void,
|
||||
onGreasePencilPointSelect?: (point: GreasePencilPointRef, additive: boolean) => void,
|
||||
onGreasePencilPointSelect?: (point: GreasePencilPointRef, additive: boolean, baseSelectionRevision: number) => void,
|
||||
onGreasePencilMarqueeSelect?: (result: GreasePencilMarqueeResultIR, additive: boolean) => void,
|
||||
): OffscreenViewportRenderer {
|
||||
const existing = sharedBackends.get(canvas);
|
||||
if (existing) {
|
||||
@@ -50,7 +60,7 @@ export function acquireOffscreenViewportRenderer(
|
||||
existing.references += 1;
|
||||
return existing.renderer;
|
||||
}
|
||||
const renderer = new OffscreenViewportRenderer(canvas, onSelect, onElementSelect, onGreasePencilPointSelect);
|
||||
const renderer = new OffscreenViewportRenderer(canvas, onSelect, onElementSelect, onGreasePencilPointSelect, onGreasePencilMarqueeSelect);
|
||||
sharedBackends.set(canvas, { renderer, references: 1 });
|
||||
return renderer;
|
||||
}
|
||||
@@ -73,23 +83,33 @@ export class OffscreenViewportRenderer implements ViewportBackend {
|
||||
private readonly resizeObserver: ResizeObserver;
|
||||
private readonly onSelect?: (objectId: string, additive: boolean) => void;
|
||||
private readonly onElementSelect?: (meshId: string, mode: MeshElementMode, index: number, additive: boolean, nonMeshKind?: NonMeshElementKind) => void;
|
||||
private readonly onGreasePencilPointSelect?: (point: GreasePencilPointRef, additive: boolean) => void;
|
||||
private readonly onGreasePencilPointSelect?: (point: GreasePencilPointRef, additive: boolean, baseSelectionRevision: number) => void;
|
||||
private readonly onGreasePencilMarqueeSelect?: (result: GreasePencilMarqueeResultIR, additive: boolean) => void;
|
||||
private pointer: { id: number; x: number; y: number; moved: boolean } | null = null;
|
||||
private lastSnapshot: SceneSnapshotIR | null = null;
|
||||
private lastGeometryBuffers: MeshGeometryBuffer[] | null = null;
|
||||
private lastNonMeshGeometryBuffers: NonMeshGeometryChunk[] | null = null;
|
||||
private greasePencilSelectionRevision = 0;
|
||||
private paintDepthRequestSequence = 0;
|
||||
private readonly pendingPaintDepth = new Map<string, {
|
||||
request: PaintDepthVisibilityRequestIR;
|
||||
resolve: (result: PaintDepthVisibilityResultIR) => void;
|
||||
reject: (error: Error) => void;
|
||||
}>();
|
||||
|
||||
constructor(
|
||||
canvas: HTMLCanvasElement,
|
||||
onSelect?: (objectId: string, additive: boolean) => void,
|
||||
onElementSelect?: (meshId: string, mode: MeshElementMode, index: number, additive: boolean, nonMeshKind?: NonMeshElementKind) => void,
|
||||
onGreasePencilPointSelect?: (point: GreasePencilPointRef, additive: boolean) => void,
|
||||
onGreasePencilPointSelect?: (point: GreasePencilPointRef, additive: boolean, baseSelectionRevision: number) => void,
|
||||
onGreasePencilMarqueeSelect?: (result: GreasePencilMarqueeResultIR, additive: boolean) => void,
|
||||
) {
|
||||
if (!supportsOffscreenViewport(canvas)) throw new Error("OffscreenCanvas viewport is unavailable");
|
||||
this.canvas = canvas;
|
||||
this.onSelect = onSelect;
|
||||
this.onElementSelect = onElementSelect;
|
||||
this.onGreasePencilPointSelect = onGreasePencilPointSelect;
|
||||
this.onGreasePencilMarqueeSelect = onGreasePencilMarqueeSelect;
|
||||
this.worker = new Worker(new URL("../workers/viewport-render.worker.ts", import.meta.url), { type: "module" });
|
||||
this.worker.onmessage = (event: MessageEvent<OffscreenViewportResponse>) => this.handleMessage(event.data);
|
||||
const offscreen = canvas.transferControlToOffscreen();
|
||||
@@ -151,6 +171,11 @@ export class OffscreenViewportRenderer implements ViewportBackend {
|
||||
this.canvas.dataset.textureStatus = "none";
|
||||
this.canvas.dataset.textureLoaded = "0";
|
||||
this.canvas.dataset.textureBytes = "0";
|
||||
this.canvas.dataset.textureBudgetStatus = "ready";
|
||||
this.canvas.dataset.textureBudgetCode = "";
|
||||
this.canvas.dataset.textureBudgetAssets = "0";
|
||||
this.canvas.dataset.textureBudgetPayloadBytes = "0";
|
||||
this.canvas.dataset.textureBudgetGpuBytes = "0";
|
||||
return;
|
||||
}
|
||||
const cloned = assets.map((asset) => ({ ...asset, data: asset.data.slice(0) }));
|
||||
@@ -163,15 +188,35 @@ export class OffscreenViewportRenderer implements ViewportBackend {
|
||||
this.worker.postMessage({ type: "volumeAssets", assets: cloned } satisfies OffscreenViewportRequest, nanoVDBViewportAssetTransferables(cloned));
|
||||
}
|
||||
|
||||
setSelection(objectIds: ReadonlySet<string>, elementSelection?: ReadonlyMap<string, ReadonlyMap<NonMeshElementKind, ReadonlySet<number>>>, greasePencilPoints: readonly GreasePencilPointRef[] = []): void {
|
||||
setSelection(objectIds: ReadonlySet<string>, elementSelection?: ReadonlyMap<string, ReadonlyMap<NonMeshElementKind, ReadonlySet<number>>>, greasePencilPoints: readonly GreasePencilPointRef[] = [], greasePencilSelectionRevision = 0): void {
|
||||
const elements = [...(elementSelection ?? new Map())].flatMap(([dataId, kinds]) => [...kinds].flatMap(([kind, indices]) => [...indices].map((index) => ({ dataId, kind, index }))));
|
||||
this.worker.postMessage({ type: "selection", objectIds: [...objectIds], elements, greasePencilPoints: [...greasePencilPoints] } satisfies OffscreenViewportRequest);
|
||||
this.greasePencilSelectionRevision = greasePencilSelectionRevision;
|
||||
this.worker.postMessage({ type: "selection", objectIds: [...objectIds], elements, greasePencilPoints: [...greasePencilPoints], greasePencilSelectionRevision } satisfies OffscreenViewportRequest);
|
||||
}
|
||||
|
||||
setInteractionMode(editMode: boolean, selectionMode: MeshElementMode): void {
|
||||
this.worker.postMessage({ type: "interaction", editMode, selectionMode } satisfies OffscreenViewportRequest);
|
||||
}
|
||||
|
||||
samplePaintVisibility(requestValue: PaintDepthVisibilityRequestIR): Promise<PaintDepthVisibilityResultIR> {
|
||||
const request = validatePaintDepthVisibilityRequest(requestValue, this.lastSnapshot?.revision ?? -1);
|
||||
const requestId = `paint-depth-${++this.paintDepthRequestSequence}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pendingPaintDepth.set(requestId, { request, resolve, reject });
|
||||
this.worker.postMessage({ type: "paintDepthVisibility", requestId, request } satisfies OffscreenViewportRequest);
|
||||
});
|
||||
}
|
||||
|
||||
selectGreasePencilMarquee(
|
||||
drawing: GreasePencilDrawingScopeIR,
|
||||
box: GreasePencilMarqueeBoxIR,
|
||||
baseRevision: number,
|
||||
baseSelectionRevision: number,
|
||||
additive: boolean,
|
||||
): void {
|
||||
this.worker.postMessage({ type: "greasePencilMarquee", drawing, box, baseRevision, baseSelectionRevision, additive } satisfies OffscreenViewportRequest);
|
||||
}
|
||||
|
||||
setCurveHandlePreview(dataId: string, handles: readonly CurveGizmoHandleIR[] | null): void {
|
||||
this.canvas.dataset.curveGizmoPreview = handles ? String(handles.length) : "0";
|
||||
this.worker.postMessage({ type: "curveHandlePreview", dataId, handles: handles ? handles.map((handle) => ({ ...handle, position: [...handle.position] as [number, number, number] })) : null } satisfies OffscreenViewportRequest);
|
||||
@@ -202,7 +247,12 @@ export class OffscreenViewportRenderer implements ViewportBackend {
|
||||
|
||||
private pointerDown = (event: PointerEvent): void => {
|
||||
this.pointer = { id: event.pointerId, x: event.clientX, y: event.clientY, moved: false };
|
||||
this.canvas.setPointerCapture(event.pointerId);
|
||||
try {
|
||||
this.canvas.setPointerCapture(event.pointerId);
|
||||
}
|
||||
catch {
|
||||
// Synthetic test events and browsers without pointer capture still support picking.
|
||||
}
|
||||
};
|
||||
|
||||
private pointerMove = (event: PointerEvent): void => {
|
||||
@@ -221,7 +271,7 @@ export class OffscreenViewportRenderer implements ViewportBackend {
|
||||
const bounds = this.canvas.getBoundingClientRect();
|
||||
const x = ((event.clientX - bounds.left) / Math.max(1, bounds.width)) * 2 - 1;
|
||||
const y = -((event.clientY - bounds.top) / Math.max(1, bounds.height)) * 2 + 1;
|
||||
this.worker.postMessage({ type: "pick", x, y, additive: event.shiftKey || event.ctrlKey || event.metaKey } satisfies OffscreenViewportRequest);
|
||||
this.worker.postMessage({ type: "pick", x, y, additive: event.shiftKey || event.ctrlKey || event.metaKey, baseSelectionRevision: this.greasePencilSelectionRevision } satisfies OffscreenViewportRequest);
|
||||
}
|
||||
this.pointer = null;
|
||||
};
|
||||
@@ -234,20 +284,69 @@ export class OffscreenViewportRenderer implements ViewportBackend {
|
||||
private handleMessage(message: OffscreenViewportResponse): void {
|
||||
if (message.type === "selected") this.onSelect?.(message.objectId, message.additive);
|
||||
else if (message.type === "elementSelected") this.onElementSelect?.(message.meshId, message.mode, message.index, message.additive, message.nonMeshKind);
|
||||
else if (message.type === "greasePencilPointSelected") this.onGreasePencilPointSelect?.(message.point, message.additive);
|
||||
else if (message.type === "frame") this.canvas.dataset.rendererPixels = String(message.visiblePixels);
|
||||
else if (message.type === "greasePencilPointSelected") this.onGreasePencilPointSelect?.(message.point, message.additive, message.baseSelectionRevision);
|
||||
else if (message.type === "greasePencilSelectionStatus") {
|
||||
this.canvas.dataset.greasePencilSelectionRevision = String(message.selectionRevision);
|
||||
this.canvas.dataset.greasePencilSelectionPointIds = message.pointIds.join(",");
|
||||
}
|
||||
else if (message.type === "greasePencilMarqueeSelected") {
|
||||
this.canvas.dataset.greasePencilMarqueeSelectionRevision = String(message.result.baseSelectionRevision);
|
||||
this.canvas.dataset.greasePencilMarqueeDrawingId = message.result.drawing.drawingId;
|
||||
this.canvas.dataset.greasePencilMarqueePointIds = message.result.selectedPoints.map((point) => point.pointId).join(",");
|
||||
this.canvas.dataset.greasePencilMarqueeStrokeIds = message.result.selectedStrokeIds.join(",");
|
||||
this.canvas.dataset.greasePencilMarqueeCount = String(message.result.selectedPoints.length);
|
||||
this.onGreasePencilMarqueeSelect?.(message.result, message.additive);
|
||||
}
|
||||
else if (message.type === "paintDepthVisibilityResult") {
|
||||
const pending = this.pendingPaintDepth.get(message.requestId);
|
||||
if (!pending) return;
|
||||
this.pendingPaintDepth.delete(message.requestId);
|
||||
try { pending.resolve(validatePaintDepthVisibilityResult(message.result, pending.request)); }
|
||||
catch (error) { pending.reject(error instanceof Error ? error : new Error(String(error))); }
|
||||
}
|
||||
else if (message.type === "paintDepthVisibilityError") {
|
||||
const pending = this.pendingPaintDepth.get(message.requestId);
|
||||
if (!pending) return;
|
||||
this.pendingPaintDepth.delete(message.requestId);
|
||||
pending.reject(new Error(message.message));
|
||||
}
|
||||
else if (message.type === "frame") {
|
||||
this.canvas.dataset.rendererPixels = String(message.visiblePixels);
|
||||
if (message.camera) {
|
||||
this.canvas.dataset.cameraPosition = message.camera.position.map((value) => Number(value.toFixed(6))).join(",");
|
||||
this.canvas.dataset.cameraTarget = message.camera.target.map((value) => Number(value.toFixed(6))).join(",");
|
||||
this.canvas.dataset.cameraYaw = message.camera.yaw.toFixed(6);
|
||||
this.canvas.dataset.cameraPitch = message.camera.pitch.toFixed(6);
|
||||
this.canvas.dataset.cameraDistance = message.camera.distance.toFixed(6);
|
||||
}
|
||||
}
|
||||
else if (message.type === "snapshotStatus") {
|
||||
this.canvas.dataset.nonMeshCount = String(message.nonMeshCount);
|
||||
this.canvas.dataset.nonMeshBlockedCount = String(message.nonMeshBlockedCount);
|
||||
this.canvas.dataset.greasePencilCount = String(message.greasePencilCount);
|
||||
this.canvas.dataset.greasePencilBlockedCount = String(message.greasePencilBlockedCount);
|
||||
this.canvas.dataset.greasePencilOnionStrokeCount = String(message.greasePencilOnionStrokeCount);
|
||||
this.canvas.dataset.renderBudgetBackend = message.renderBudget.backend;
|
||||
this.canvas.dataset.renderBudgetStatus = message.renderBudget.status.toLowerCase();
|
||||
this.canvas.dataset.renderBudgetCode = message.renderBudget.issues[0]?.code ?? "";
|
||||
this.canvas.dataset.renderBudgetLights = String(message.renderBudget.requestedLights);
|
||||
this.canvas.dataset.renderBudgetRenderedLights = String(message.renderBudget.renderedLightNodeIds.length);
|
||||
this.canvas.dataset.renderBudgetDroppedLights = String(message.renderBudget.droppedLightNodeIds.length);
|
||||
this.canvas.dataset.renderBudgetShadows = String(message.renderBudget.requestedShadowMaps);
|
||||
this.canvas.dataset.renderBudgetRenderedShadows = String(message.renderBudget.shadowLightNodeIds.length);
|
||||
this.canvas.dataset.renderBudgetBlockedShadows = String(message.renderBudget.shadowBlockedLightNodeIds.length);
|
||||
this.canvas.dataset.renderBudgetShadowMapDimension = String(message.renderBudget.budget.shadowMapDimension);
|
||||
}
|
||||
else if (message.type === "textureStatus") {
|
||||
this.canvas.dataset.textureStatus = message.rejected > 0 ? "blocked" : "ready";
|
||||
this.canvas.dataset.textureLoaded = String(message.loaded);
|
||||
this.canvas.dataset.textureBytes = String(message.bytes);
|
||||
this.canvas.dataset.textureErrorCode = message.errorCodes[0] ?? "";
|
||||
this.canvas.dataset.textureBudgetStatus = message.budget.status.toLowerCase();
|
||||
this.canvas.dataset.textureBudgetCode = message.budget.issues[0]?.code ?? "";
|
||||
this.canvas.dataset.textureBudgetAssets = String(message.budget.requestedAssets);
|
||||
this.canvas.dataset.textureBudgetPayloadBytes = String(message.budget.payloadBytes);
|
||||
this.canvas.dataset.textureBudgetGpuBytes = String(message.budget.decodedGPUBytes);
|
||||
}
|
||||
else if (message.type === "volumeStatus") {
|
||||
this.canvas.dataset.volumeStatus = message.status;
|
||||
@@ -269,5 +368,7 @@ export class OffscreenViewportRenderer implements ViewportBackend {
|
||||
this.canvas.removeEventListener("wheel", this.wheel);
|
||||
this.worker.postMessage({ type: "dispose" } satisfies OffscreenViewportRequest);
|
||||
this.worker.terminate();
|
||||
for (const pending of this.pendingPaintDepth.values()) pending.reject(new Error("PAINT_DEPTH_UNAVAILABLE: Offscreen viewport disposed"));
|
||||
this.pendingPaintDepth.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user