fix: Beute-Ziehen bricht bei Pinch ab, Rähmchen-Druck zieht keine Zarge
- zweiter Finger während Druck oder Ziehen bricht ab (kein Ablegen) - Druck auf ein Rähmchen im Editor erreicht die Zarge nicht mehr - Canvas-Rechteck bei Scroll neu gemessen, Motion-Ziele nur bei Abstand oder bewegtem Teil neu berechnet, toter Re-Export entfernt Refs #21 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
45274f4301
commit
19550d1394
6 files changed
+66
-12
No files matched your search
@@ -122,6 +122,23 @@ describe("useHiveDrag", () => {
|
||||
expect(apply).toHaveBeenCalledWith([{ op: "movePart", id: "c", to: 4 }]);
|
||||
});
|
||||
|
||||
it("cancels a press or drag when a second finger comes down (a pinch starts, no part drag)", () => {
|
||||
const { result, apply, announce, begin, move, up } = setup();
|
||||
begin({ partId: "c" });
|
||||
act(() => void window.dispatchEvent(pointer("pointerdown", 300, 100, 2)));
|
||||
move(160);
|
||||
expect(result.current.drag).toBeNull();
|
||||
|
||||
begin({ partId: "c" });
|
||||
move(160);
|
||||
expect(result.current.drag).not.toBeNull();
|
||||
act(() => void window.dispatchEvent(pointer("pointerdown", 300, 100, 2)));
|
||||
expect(result.current.drag).toBeNull();
|
||||
expect(announce).toHaveBeenLastCalledWith("Ziehen abgebrochen.");
|
||||
up(160);
|
||||
expect(apply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("cancels when the window loses focus (Alt+Tab) or the tab is hidden", () => {
|
||||
const first = setup();
|
||||
first.begin({ partTypeId: "box" });
|
||||
|
||||
@@ -11,7 +11,8 @@ import type { DraftApplyResult } from "./use-hive-draft";
|
||||
// so mouse, pen and finger behave the same. A press becomes a drag after a
|
||||
// few pixels (a tap stays a tap); the scene reports the slot under the
|
||||
// pointer; release drops there, on „Entfernen“ removes; Esc, leaving the
|
||||
// window (Alt+Tab, hidden tab) or a mouse released outside cancel.
|
||||
// window (Alt+Tab, hidden tab), a mouse released outside or a second finger
|
||||
// (pinch) cancel.
|
||||
// Window listeners, no pointer capture: a finger is captured to the element it
|
||||
// started on anyway, so the scene reads the position from `pointer`.
|
||||
|
||||
@@ -180,6 +181,11 @@ export function useHiveDrag(options: UseHiveDragOptions): HiveDrag {
|
||||
if (samePointer(event)) end();
|
||||
};
|
||||
|
||||
// A second finger means a pinch zoom: the part under the first finger is not dragged.
|
||||
const onDown = (event: Event) => {
|
||||
if (!samePointer(event)) abort();
|
||||
};
|
||||
|
||||
/** Ends without dropping; says so when a drag was running. */
|
||||
function abort() {
|
||||
const active = dragRef.current !== null;
|
||||
@@ -201,6 +207,7 @@ export function useHiveDrag(options: UseHiveDragOptions): HiveDrag {
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("pointerdown", onDown);
|
||||
window.addEventListener("pointermove", onMove);
|
||||
window.addEventListener("pointerup", onUp);
|
||||
window.addEventListener("pointercancel", onCancel);
|
||||
@@ -210,6 +217,7 @@ export function useHiveDrag(options: UseHiveDragOptions): HiveDrag {
|
||||
return () => {
|
||||
window.removeEventListener("blur", onBlur);
|
||||
document.removeEventListener("visibilitychange", onVisibility);
|
||||
window.removeEventListener("pointerdown", onDown);
|
||||
window.removeEventListener("pointermove", onMove);
|
||||
window.removeEventListener("pointerup", onUp);
|
||||
window.removeEventListener("pointercancel", onCancel);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ChoiceGroup } from "@/components/form/choice-group";
|
||||
import { coveredSlots, feederRange, frameFeederOps, rangeText } from "@/lib/hive/feeder-slots";
|
||||
import { coveredSlots, feederRange, rangeText } from "@/lib/hive/feeder-slots";
|
||||
import type { CatalogPartType, HiveFrame } from "@/lib/hive/types";
|
||||
import { StateFieldControl } from "./state-field-control";
|
||||
import type { FieldDraft } from "./state-field-control";
|
||||
@@ -25,9 +25,6 @@ function rangeAt(start: number, type: CatalogPartType) {
|
||||
return feederRange({ id: "", partTypeId: type.id, state: {}, slot: start }, type);
|
||||
}
|
||||
|
||||
/** Kept for existing imports; lives in lib/hive/feeder-slots. */
|
||||
export { frameFeederOps };
|
||||
|
||||
interface FrameFeederPickerProps {
|
||||
boxId: string;
|
||||
type: CatalogPartType;
|
||||
|
||||
@@ -126,12 +126,12 @@ export function DropTargets({ anchors, hover, pointer, subscribePointer, onHover
|
||||
};
|
||||
}, [materials]);
|
||||
|
||||
// The canvas rect only changes with the canvas size (the editor does not scroll).
|
||||
// The canvas rect changes with the canvas size and position (r3f measures scroll and resize).
|
||||
const rect = useRef<DOMRect | null>(null);
|
||||
useEffect(() => {
|
||||
const element = gl.domElement as HTMLElement | undefined;
|
||||
rect.current = element?.getBoundingClientRect ? element.getBoundingClientRect() : null;
|
||||
}, [gl, size.width, size.height]);
|
||||
}, [gl, size.width, size.height, size.top, size.left]);
|
||||
|
||||
// Render only when the pointer moved; flights and the opening stack request their own frames.
|
||||
useEffect(() => subscribePointer(() => invalidate()), [subscribePointer, invalidate]);
|
||||
|
||||
@@ -20,6 +20,17 @@ import { SceneKitProvider } from "./scene-kit";
|
||||
// The scene content runs on the r3f test renderer (no WebGL). Labels are DOM
|
||||
// portals (drei Html), so they are replaced by a marker here.
|
||||
const anchorCalls = vi.hoisted(() => ({ count: 0 }));
|
||||
const motionCalls = vi.hoisted(() => ({ count: 0 }));
|
||||
vi.mock("./use-hive-motion", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./use-hive-motion")>();
|
||||
return {
|
||||
...actual,
|
||||
hiveMotionTargets: (...args: Parameters<typeof actual.hiveMotionTargets>) => {
|
||||
motionCalls.count += 1;
|
||||
return actual.hiveMotionTargets(...args);
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("./drop-anchors", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./drop-anchors")>();
|
||||
return {
|
||||
@@ -291,6 +302,22 @@ describe("HiveSceneContent in the editor", () => {
|
||||
expect(stopPropagation).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("takes a press on a frame of the open box there: the host box does not start a drag", async () => {
|
||||
const editor = editorOf();
|
||||
const { renderer, store } = await renderScene("full", editor);
|
||||
await act(async () => store.openBox("b1"));
|
||||
const stopPropagation = vi.fn();
|
||||
await renderer.fireEvent(hit(renderer, "b1#4"), "onPointerDown", {
|
||||
pointerType: "mouse",
|
||||
pointerId: 3,
|
||||
clientX: 10,
|
||||
clientY: 20,
|
||||
stopPropagation,
|
||||
});
|
||||
expect(stopPropagation).toHaveBeenCalled();
|
||||
expect(editor.onPartPress).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows one glowing marker per legal slot while dragging and opens the stack", async () => {
|
||||
const rest = await renderScene("off");
|
||||
await rest.renderer.advanceFrames(1, 1 / 60);
|
||||
@@ -309,7 +336,7 @@ describe("HiveSceneContent in the editor", () => {
|
||||
});
|
||||
|
||||
describe("HiveSceneContent drop anchors", () => {
|
||||
it("computes the slot anchors once per drag, not again on every hover change", async () => {
|
||||
it("computes the slot anchors and motion targets once per drag, not again on every hover change", async () => {
|
||||
const state = hiveState();
|
||||
const layout = hiveLayout(state, catalog);
|
||||
const store = createHiveViewStore();
|
||||
@@ -341,11 +368,15 @@ describe("HiveSceneContent drop anchors", () => {
|
||||
</HiveViewProvider>
|
||||
);
|
||||
anchorCalls.count = 0;
|
||||
motionCalls.count = 0;
|
||||
const renderer = await ReactThreeTestRenderer.create(element(editor(null)));
|
||||
const afterMount = anchorCalls.count;
|
||||
const motionAfterMount = motionCalls.count;
|
||||
await renderer.update(element(editor(slots[0])));
|
||||
await renderer.update(element(editor(slots[1])));
|
||||
expect(anchorCalls.count).toBe(afterMount);
|
||||
// The motion targets depend on the gap and the moved part, not on the hover.
|
||||
expect(motionCalls.count).toBe(motionAfterMount);
|
||||
await renderer.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -110,15 +110,15 @@ export function HiveSceneContent({
|
||||
const motionView = useMemo(() => ({ ...view, selectedFrame }), [view, selectedFrame]);
|
||||
const drag = editor?.drag ?? null;
|
||||
const gap = drag ? DRAG_GAP : REST_GAP;
|
||||
const movedId = drag && "partId" in drag.source ? drag.source.partId : null;
|
||||
const targets: HiveMotionTargets = useMemo(() => {
|
||||
const computed = hiveMotionTargets(layout, motionView, gap);
|
||||
const movedId = drag && "partId" in drag.source ? drag.source.partId : null;
|
||||
const moved = movedId ? computed.nodes.get(movedId) : undefined;
|
||||
if (!movedId || !moved) return computed;
|
||||
const nodes = new Map(computed.nodes);
|
||||
nodes.set(movedId, { ...moved, opacity: Math.min(moved.opacity, DRAGGED_OPACITY), interactive: false });
|
||||
return { ...computed, nodes };
|
||||
}, [layout, motionView, gap, drag]);
|
||||
}, [layout, motionView, gap, movedId]);
|
||||
// Once per drag: the hover changes the drag object, not its source or its slots.
|
||||
const dragSource = drag?.source ?? null;
|
||||
const dragSlots = drag?.slots ?? null;
|
||||
@@ -199,9 +199,10 @@ export function HiveSceneContent({
|
||||
onPointerDown(event) {
|
||||
pointerType.current = event.pointerType ?? "mouse";
|
||||
const parsed = parseKey(key);
|
||||
// Only the nearest hit takes the press (inserts and frames inside a box, thin parts on top).
|
||||
// Frames are not dragged: a press on one must not reach the box around it.
|
||||
if (editor && parsed) event.stopPropagation();
|
||||
if (editor && parsed?.kind === "part") {
|
||||
// Only the nearest hit takes the press (inserts and frames inside a box, thin parts on top).
|
||||
event.stopPropagation();
|
||||
if (signals.pinching) return;
|
||||
// A press on a part may become a part drag: the rig must not turn the model for it.
|
||||
markPartPress(signals, true);
|
||||
|
||||
Reference in new issue
Block a user