From 5acc2235c628b582109d76cb8410d6da46b27436 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Thu, 20 Aug 2026 08:56:33 +0300 Subject: [PATCH] fix(canvas): polish guide ruler interactions --- CHANGELOG.md | 1 + packages/vue/src/canvas/guides/input.ts | 14 ++++-- tests/engine/vue/input/guides.test.ts | 59 +++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 tests/engine/vue/input/guides.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7693c96c2..b73507823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Added +- Create, select, move, transfer, and delete canvas and frame guides directly from rulers, with undoable edits and `.fig` round-trip fidelity. - Snap vector points, moved layers, and resized edges to nearby geometry, sibling layer bounds, canvas and frame layout guides, and whole-pixel coordinates with visible alignment guides, fractional-coordinate preservation when pixel snapping is off, and persistent geometry, object, and pixel-grid controls in General settings and the Preferences menu. - Run Pi through AI SDK HarnessAgent as a configurable desktop provider with multiple saved model profiles, secure credentials, existing MCP design tools, and per-profile thinking and permission settings. - Open multiple selected design files in separate tabs. diff --git a/packages/vue/src/canvas/guides/input.ts b/packages/vue/src/canvas/guides/input.ts index 96c05b14e..1214cf6b6 100644 --- a/packages/vue/src/canvas/guides/input.ts +++ b/packages/vue/src/canvas/guides/input.ts @@ -63,14 +63,18 @@ export function createGuideInput({ } function updateHover(sx: number, sy: number): string | null { + const axis = rulerAxis(sx, sy) + if (axis) { + editor.setHoveredGuide(null) + return cursor(axis) + } const hit = hitTest(sx, sy) editor.setHoveredGuide(hit ? { ownerId: hit.ownerId, guideId: hit.guideId } : null) - if (hit) return cursor(hit.axis) - const axis = rulerAxis(sx, sy) - return axis ? cursor(axis) : null + return hit ? cursor(hit.axis) : null } function tryStartExisting(sx: number, sy: number): boolean { + if (rulerAxis(sx, sy)) return false const hit = hitTest(sx, sy) if (!hit) return false editor.setSelectedGuide({ ownerId: hit.ownerId, guideId: hit.guideId }) @@ -134,8 +138,10 @@ export function createGuideInput({ function finish(drag: DragGuide): void { if (drag.dragStarted) { if (drag.currentScreenX < RULER_SIZE || drag.currentScreenY < RULER_SIZE) { - if (drag.guideId && drag.originalOwnerId) + if (drag.guideId && drag.originalOwnerId) { editor.removeGuide(drag.originalOwnerId, drag.guideId) + editor.setSelectedGuide(null) + } } else if (drag.guideId && drag.originalOwnerId) { if (drag.ownerId === drag.originalOwnerId) editor.moveGuide(drag.ownerId, drag.guideId, drag.position) diff --git a/tests/engine/vue/input/guides.test.ts b/tests/engine/vue/input/guides.test.ts new file mode 100644 index 000000000..a058a1323 --- /dev/null +++ b/tests/engine/vue/input/guides.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test } from 'bun:test' + +import { ref } from 'vue' + +import { createEditor } from '@open-pencil/core/editor' + +import { createGuideInput } from '#vue/canvas/guides/input' +import type { DragState } from '#vue/shared/input/types' + +function setup() { + const editor = createEditor() + Object.assign(editor.state, { showRulers: true }) + let drag: DragState | null = null + const input = createGuideInput({ + canvasRef: ref(null), + editor, + canvasToLocal: (cx, cy) => ({ lx: cx, ly: cy }), + setDrag: (next) => { + drag = next + }, + setCursor: () => undefined + }) + return { editor, input, getDrag: () => drag } +} + +describe('guide canvas input', () => { + test('does not create a guide from a ruler click without movement', () => { + const { editor, input, getDrag } = setup() + expect(input.tryStartFromRuler(100, 5, 100, 5)).toBe(true) + const drag = getDrag() + expect(drag?.type).toBe('guide') + if (drag?.type === 'guide') input.finish(drag) + expect(editor.graph.getNode(editor.state.currentPageId)?.guides).toEqual([]) + }) + + test('publishes live preview after the drag threshold and commits on release', () => { + const { editor, input, getDrag } = setup() + input.tryStartFromRuler(100, 5, 100, 5) + const drag = getDrag() + if (drag?.type !== 'guide') throw new Error('Expected guide drag') + + input.handleMove(drag, 100, 40, 100, 40) + expect(editor.state.guides.preview).toMatchObject({ axis: 'y', position: 40 }) + expect(editor.graph.getNode(editor.state.currentPageId)?.guides).toEqual([]) + + input.finish(drag) + expect(editor.graph.getNode(editor.state.currentPageId)?.guides[0]).toMatchObject({ + axis: 'y', + position: 40 + }) + }) + + test('ruler hover takes precedence over an intersecting existing guide', () => { + const { editor, input } = setup() + editor.addGuide(editor.state.currentPageId, 'x', 100) + expect(input.updateHover(100, 5)).toBe('ns-resize') + expect(editor.state.guides.hovered).toBeNull() + }) +})