import { describe, expect, it } from "vitest"; import { createEvent, defineToolcraft, fireEvent, renderControlsPanelWithSchema, screen, vi, } from "../testing/controls-panel-test-utils"; describe("ControlsPanel vector pad interactions", () => { it("maps default spatial vector pad movement to screen coordinates", () => { const schema = defineToolcraft({ canvas: { enabled: true }, panels: { controls: { sections: [ { controls: { position: { defaultValue: { x: "0.00", y: "0.00" }, label: "Position", target: "geometry.position", type: "vector", }, }, title: "Geometry", }, ], title: "Controls", }, }, }); renderControlsPanelWithSchema(schema); const pad = screen.getByRole("button", { name: "Position X/Y pad" }); expect(pad.getAttribute("data-vector-pad-coordinate-mode")).toBe("screen"); vi.spyOn(pad, "getBoundingClientRect").mockReturnValue({ bottom: 100, height: 100, left: 0, right: 100, toJSON: () => undefined, top: 0, width: 100, x: 0, y: 0, } as DOMRect); Object.defineProperty(pad, "setPointerCapture", { configurable: true, value: vi.fn(), }); fireEvent.pointerDown(pad, { buttons: 1, clientX: 0, clientY: 0, pointerId: 1, }); const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}"); expect(values["geometry.position"]).toEqual({ x: -1, y: -1 }); }); it("resets a vector pad to its default value on double click", () => { const schema = defineToolcraft({ canvas: { enabled: true }, panels: { controls: { sections: [ { controls: { position: { defaultValue: { x: "0.48", y: "-0.34" }, label: "Position", target: "geometry.position", type: "vector", }, }, title: "Geometry", }, ], title: "Controls", }, }, }); renderControlsPanelWithSchema(schema); const pad = screen.getByRole("button", { name: "Position X/Y pad" }); vi.spyOn(pad, "getBoundingClientRect").mockReturnValue({ bottom: 100, height: 100, left: 0, right: 100, toJSON: () => undefined, top: 0, width: 100, x: 0, y: 0, } as DOMRect); Object.defineProperty(pad, "setPointerCapture", { configurable: true, value: vi.fn(), }); fireEvent.pointerDown(pad, { buttons: 1, clientX: 0, clientY: 0, pointerId: 1, }); expect( JSON.parse(screen.getByTestId("values-json").textContent ?? "{}")[ "geometry.position" ], ).toEqual({ x: -1, y: -1 }); fireEvent.doubleClick(pad); const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}"); expect(values["geometry.position"]).toEqual({ x: 0.48, y: -0.34 }); expect(screen.getByText("0.48, -0.34")).toBeTruthy(); }); it("locks vector pad movement to the dominant axis while shift is held", () => { const schema = defineToolcraft({ canvas: { enabled: true }, panels: { controls: { sections: [ { controls: { position: { defaultValue: { x: "0.00", y: "0.00" }, label: "Position", target: "geometry.position", type: "vector", }, }, title: "Geometry", }, ], title: "Controls", }, }, }); renderControlsPanelWithSchema(schema); const pad = screen.getByRole("button", { name: "Position X/Y pad" }); vi.spyOn(pad, "getBoundingClientRect").mockReturnValue({ bottom: 100, height: 100, left: 0, right: 100, toJSON: () => undefined, top: 0, width: 100, x: 0, y: 0, } as DOMRect); Object.defineProperty(pad, "setPointerCapture", { configurable: true, value: vi.fn(), }); fireEvent.pointerDown(pad, { buttons: 1, clientX: 50, clientY: 50, pointerId: 1, }); fireEvent.pointerMove(pad, { buttons: 1, clientX: 90, clientY: 70, pointerId: 1, shiftKey: true, }); const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}"); expect(values["geometry.position"]).toEqual({ x: 0.8, y: 0 }); }); it("locks vector pad movement vertically when shift drag is y-dominant", () => { const schema = defineToolcraft({ canvas: { enabled: true }, panels: { controls: { sections: [ { controls: { position: { defaultValue: { x: "0.00", y: "0.00" }, label: "Position", target: "geometry.position", type: "vector", }, }, title: "Geometry", }, ], title: "Controls", }, }, }); renderControlsPanelWithSchema(schema); const pad = screen.getByRole("button", { name: "Position X/Y pad" }); vi.spyOn(pad, "getBoundingClientRect").mockReturnValue({ bottom: 100, height: 100, left: 0, right: 100, toJSON: () => undefined, top: 0, width: 100, x: 0, y: 0, } as DOMRect); Object.defineProperty(pad, "setPointerCapture", { configurable: true, value: vi.fn(), }); fireEvent.pointerDown(pad, { buttons: 1, clientX: 50, clientY: 50, pointerId: 1, }); fireEvent.pointerMove(pad, { buttons: 1, clientX: 70, clientY: 90, pointerId: 1, shiftKey: true, }); const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}"); expect(values["geometry.position"]).toEqual({ x: 0, y: 0.8 }); }); it("renders vector pad values as compact rounded coordinates", () => { const schema = defineToolcraft({ canvas: { enabled: true }, panels: { controls: { sections: [ { controls: { center: { defaultValue: { x: "-0.07070312499999998", y: "-0.00392795138", }, label: "Center", target: "geometry.center", type: "vector", }, }, title: "Geometry", }, ], title: "Controls", }, }, }); renderControlsPanelWithSchema(schema); expect(screen.getByText("-0.07, 0.00")).toBeTruthy(); expect(screen.queryByText("-0.07070312499999998, -0.00392795138")).toBeNull(); }); it("prevents native text selection while dragging a vector pad", () => { const schema = defineToolcraft({ canvas: { enabled: true }, panels: { controls: { sections: [ { controls: { center: { defaultValue: { x: "0.00", y: "0.00" }, label: "Center", target: "geometry.center", type: "vector", }, }, title: "Geometry", }, ], title: "Controls", }, }, }); renderControlsPanelWithSchema(schema); const pad = screen.getByRole("button", { name: "Center X/Y pad" }); vi.spyOn(pad, "getBoundingClientRect").mockReturnValue({ bottom: 100, height: 100, left: 0, right: 100, toJSON: () => undefined, top: 0, width: 100, x: 0, y: 0, } as DOMRect); Object.defineProperty(pad, "setPointerCapture", { configurable: true, value: vi.fn(), }); const pointerDown = createEvent.pointerDown(pad, { buttons: 1, clientX: 50, clientY: 50, pointerId: 1, }); fireEvent(pad, pointerDown); expect(pointerDown.defaultPrevented).toBe(true); expect(pad.className).toContain("select-none"); }); it("allows cartesian vector pads when a product intentionally uses y-up math", () => { const schema = defineToolcraft({ canvas: { enabled: true }, panels: { controls: { sections: [ { controls: { vector: { coordinateMode: "cartesian", defaultValue: { x: "0.00", y: "0.00" }, label: "Vector", target: "math.vector", type: "vector", }, }, title: "Math", }, ], title: "Controls", }, }, }); renderControlsPanelWithSchema(schema); const pad = screen.getByRole("button", { name: "Vector X/Y pad" }); expect(pad.getAttribute("data-vector-pad-coordinate-mode")).toBe("cartesian"); vi.spyOn(pad, "getBoundingClientRect").mockReturnValue({ bottom: 100, height: 100, left: 0, right: 100, toJSON: () => undefined, top: 0, width: 100, x: 0, y: 0, } as DOMRect); Object.defineProperty(pad, "setPointerCapture", { configurable: true, value: vi.fn(), }); fireEvent.pointerDown(pad, { buttons: 1, clientX: 0, clientY: 0, pointerId: 1, }); const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}"); expect(values["math.vector"]).toEqual({ x: -1, y: 1 }); }); });