Coalesce disassembly drag updates

This commit is contained in:
flash555588 2026-06-27 07:42:17 +08:00
parent 0a72194f61
commit 096cdccaec
4 changed files with 493 additions and 398 deletions

View file

@ -35,6 +35,7 @@
- Performance: cache Three.js root bounds and pre-index grouped part descendants so large converted models do less repeated scene traversal after loading.
- Performance: reuse Three.js child-mesh descendant indexes for picking, evidence grouping, and disassembly setup so large assemblies avoid repeated subtree scans during selection.
- Performance: reuse shared Three.js focus-dim materials across meshes with the same source material, reducing material churn when focusing parts in large assemblies.
- Performance: coalesce disassembly drag updates to animation frames and flush the final pointer position on release, reducing high-frequency transform work while moving parts in large assemblies.
- Performance: defer Three.js geometry quality snapshots and direct-view registered-part match previews so large models become interactive before diagnostic and cross-model matching work runs.
- Performance: capture disassembly original transforms only for dragged parts and cache repeated Live Preview embed path resolution, reducing large-assembly interaction and workspace editor setup work.
- Performance: update Three.js focus selection incrementally so switching selected parts in large assemblies no longer restores and re-dims every mesh.

792
main.js

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createPreviewDisassemblyController,
type PreviewDisassemblyAdapter,
@ -73,6 +73,10 @@ function createTestAdapter(parts: TestPart[]) {
}
describe("preview disassembly controller", () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it("captures original transforms lazily when a part is actually dragged", () => {
const parts = [
{ id: 1, position: 10 },
@ -119,4 +123,51 @@ describe("preview disassembly controller", () => {
expect(restoreCalls).toEqual([]);
expect(parts[0].position).toBe(10);
});
it("coalesces drag updates to animation frames and flushes the last move on release", () => {
const callbacks = new Map<number, FrameRequestCallback>();
let nextFrame = 1;
const requestAnimationFrame = vi.fn((callback: FrameRequestCallback) => {
const frame = nextFrame++;
callbacks.set(frame, callback);
return frame;
});
const cancelAnimationFrame = vi.fn((frame: number) => {
callbacks.delete(frame);
});
vi.stubGlobal("requestAnimationFrame", requestAnimationFrame);
vi.stubGlobal("cancelAnimationFrame", cancelAnimationFrame);
const parts = [{ id: 1, position: 0 }];
const updateCalls: number[] = [];
const { adapter, getSubscriptions } = createTestAdapter(parts);
const originalUpdateDrag = adapter.updateDrag;
adapter.updateDrag = (drag, event) => {
updateCalls.push(event.clientX);
originalUpdateDrag(drag, event);
};
const controller = createPreviewDisassemblyController(adapter);
controller.setEnabled(true);
const subscriptions = getSubscriptions();
subscriptions?.onPointerDown(parts[0], pointerEvent({ clientX: 0, clientY: 0 }));
subscriptions?.onPointerMove(pointerEvent({ clientX: 5, clientY: 0 }));
subscriptions?.onPointerMove(pointerEvent({ clientX: 8, clientY: 0 }));
subscriptions?.onPointerMove(pointerEvent({ clientX: 13, clientY: 0 }));
expect(updateCalls).toEqual([5]);
expect(requestAnimationFrame).toHaveBeenCalledTimes(1);
callbacks.get(1)?.(100);
expect(updateCalls).toEqual([5, 13]);
expect(parts[0].position).toBe(13);
subscriptions?.onPointerMove(pointerEvent({ clientX: 21, clientY: 0 }));
expect(requestAnimationFrame).toHaveBeenCalledTimes(2);
subscriptions?.onPointerUp(pointerEvent({ clientX: 21, clientY: 0 }));
expect(cancelAnimationFrame).toHaveBeenCalledWith(2);
expect(updateCalls).toEqual([5, 13, 21]);
expect(parts[0].position).toBe(21);
});
});

View file

@ -40,6 +40,8 @@ class PreviewDisassemblySessionController<TPart, TTransform, TDragState> impleme
private selected: TPart | null = null;
private activePointerId: number | null = null;
private frameCount = 0;
private pendingDragEvent: PointerEvent | null = null;
private dragUpdateFrame: number | null = null;
constructor(adapter: PreviewDisassemblyAdapter<TPart, TTransform, TDragState>) {
this.adapter = adapter;
@ -125,14 +127,19 @@ class PreviewDisassemblySessionController<TPart, TTransform, TDragState> impleme
if (this.adapter.isDisposed(pending.part)) return;
this.captureOriginalTransform(pending.part);
this.drag = this.adapter.beginDrag(pending.part, pending.event);
if (this.drag) {
this.adapter.updateDrag(this.drag, event);
}
return;
}
if (!this.drag) return;
this.adapter.updateDrag(this.drag, event);
this.scheduleDragUpdate(event);
}
private handlePointerUp(event: PointerEvent): void {
if (this.activePointerId !== null && event.pointerId !== this.activePointerId) return;
this.pendingDrag = null;
this.flushDragUpdate();
this.finishDrag();
this.activePointerId = null;
}
@ -147,11 +154,32 @@ class PreviewDisassemblySessionController<TPart, TTransform, TDragState> impleme
private finishDrag(): void {
this.pendingDrag = null;
this.flushDragUpdate();
this.adapter.endDrag(this.drag);
this.drag = null;
this.adapter.requestRender?.();
}
private scheduleDragUpdate(event: PointerEvent): void {
this.pendingDragEvent = event;
if (this.dragUpdateFrame !== null) return;
this.dragUpdateFrame = requestDragAnimationFrame(() => {
this.dragUpdateFrame = null;
this.flushDragUpdate();
});
}
private flushDragUpdate(): void {
if (this.dragUpdateFrame !== null) {
cancelDragAnimationFrame(this.dragUpdateFrame);
this.dragUpdateFrame = null;
}
const event = this.pendingDragEvent;
this.pendingDragEvent = null;
if (!this.drag || !event) return;
this.adapter.updateDrag(this.drag, event);
}
private setSelected(part: TPart | null): void {
this.selected = part;
this.frameCount = 0;
@ -167,6 +195,21 @@ class PreviewDisassemblySessionController<TPart, TTransform, TDragState> impleme
}
}
function requestDragAnimationFrame(callback: FrameRequestCallback): number {
if (typeof globalThis.requestAnimationFrame === "function") {
return globalThis.requestAnimationFrame(callback);
}
return globalThis.setTimeout(() => callback(performance.now()), 16) as unknown as number;
}
function cancelDragAnimationFrame(handle: number): void {
if (typeof globalThis.cancelAnimationFrame === "function") {
globalThis.cancelAnimationFrame(handle);
return;
}
globalThis.clearTimeout(handle);
}
export function createPreviewDisassemblyController<TPart, TTransform, TDragState>(
adapter: PreviewDisassemblyAdapter<TPart, TTransform, TDragState>,
): PreviewDisassemblyController {