mirror of
https://github.com/flash555588/ai-model-workbench.git
synced 2026-07-22 06:56:38 +00:00
Coalesce disassembly drag updates
This commit is contained in:
parent
0a72194f61
commit
096cdccaec
4 changed files with 493 additions and 398 deletions
|
|
@ -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
792
main.js
File diff suppressed because one or more lines are too long
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue