diff --git a/packages/core/src/canvas/page-guides.ts b/packages/core/src/canvas/page-guides.ts index d30d267b8..0dfa2de8e 100644 --- a/packages/core/src/canvas/page-guides.ts +++ b/packages/core/src/canvas/page-guides.ts @@ -7,9 +7,22 @@ import Matrix from '@open-pencil/scene-graph/matrix' import type { RenderOverlays, SkiaRenderer } from './renderer' const GUIDE_COLOR = { r: 0.85, g: 0.29, b: 0.2, a: 0.78 } +const HOVERED_GUIDE_COLOR = { r: 0.96, g: 0.4, b: 0.26, a: 1 } const SELECTED_GUIDE_COLOR = { r: 0.1, g: 0.45, b: 0.95, a: 1 } const GUIDE_DASH = [3, 4] +function guideColor(state: 'idle' | 'hovered' | 'selected') { + if (state === 'selected') return SELECTED_GUIDE_COLOR + if (state === 'hovered') return HOVERED_GUIDE_COLOR + return GUIDE_COLOR +} + +function guideState(selected: boolean, hovered: boolean): 'idle' | 'hovered' | 'selected' { + if (selected) return 'selected' + if (hovered) return 'hovered' + return 'idle' +} + function drawOwnedGuide( r: SkiaRenderer, canvas: Canvas, @@ -18,9 +31,9 @@ function drawOwnedGuide( axis: 'x' | 'y', position: number, preview: boolean, - selected = false + state: 'idle' | 'hovered' | 'selected' = 'idle' ): void { - const color = selected ? SELECTED_GUIDE_COLOR : GUIDE_COLOR + const color = guideColor(state) r.auxStroke.setColor(r.ck.Color4f(color.r, color.g, color.b, color.a)) const matrix = getWorldMatrix(owner, graph) const start = Matrix.mapPoint( @@ -51,6 +64,7 @@ export function drawPageGuides( canvas: Canvas, graph: SceneGraph, preview?: RenderOverlays['guidePreview'], + hoveredGuide?: RenderOverlays['hoveredGuide'], selectedGuide?: RenderOverlays['selectedGuide'] ): void { const page = graph.getNode(r.pageId ?? graph.rootId) @@ -60,8 +74,10 @@ export function drawPageGuides( r.auxStroke.setColor(r.ck.Color4f(GUIDE_COLOR.r, GUIDE_COLOR.g, GUIDE_COLOR.b, GUIDE_COLOR.a)) for (const guide of page.guides) { + if (preview?.source?.ownerId === page.id && preview.source.guideId === guide.id) continue const selected = selectedGuide?.ownerId === page.id && selectedGuide.guideId === guide.id - const color = selected ? SELECTED_GUIDE_COLOR : GUIDE_COLOR + const hovered = hoveredGuide?.ownerId === page.id && hoveredGuide.guideId === guide.id + const color = guideColor(guideState(selected, hovered)) r.auxStroke.setColor(r.ck.Color4f(color.r, color.g, color.b, color.a)) if (guide.axis === 'x') { const x = guide.position * r.zoom + r.panX @@ -74,6 +90,9 @@ export function drawPageGuides( const visit = (node: SceneNode) => { for (const guide of node.guides) { + if (preview?.source?.ownerId === node.id && preview.source.guideId === guide.id) continue + const selected = selectedGuide?.ownerId === node.id && selectedGuide.guideId === guide.id + const hovered = hoveredGuide?.ownerId === node.id && hoveredGuide.guideId === guide.id drawOwnedGuide( r, canvas, @@ -82,7 +101,7 @@ export function drawPageGuides( guide.axis, guide.position, false, - selectedGuide?.ownerId === node.id && selectedGuide.guideId === guide.id + guideState(selected, hovered) ) } for (const childId of node.childIds) { @@ -97,6 +116,7 @@ export function drawPageGuides( if (preview) { const owner = graph.getNode(preview.ownerId) - if (owner) drawOwnedGuide(r, canvas, owner, graph, preview.axis, preview.position, true) + if (owner) + drawOwnedGuide(r, canvas, owner, graph, preview.axis, preview.position, true, 'selected') } } diff --git a/packages/core/src/canvas/renderer/pipeline.ts b/packages/core/src/canvas/renderer/pipeline.ts index bdde1b077..24c4a24aa 100644 --- a/packages/core/src/canvas/renderer/pipeline.ts +++ b/packages/core/src/canvas/renderer/pipeline.ts @@ -62,6 +62,7 @@ export function renderFromEditorState( marquee: state.marquee, snapGuides: state.snapGuides, guidePreview: state.guidePreview, + hoveredGuide: state.hoveredGuide, selectedGuide: state.selectedGuide, rotationPreview: state.rotationPreview, dropTargetId: state.dropTargetId, @@ -256,7 +257,14 @@ export function render( drawInteractiveOverlays(r, canvas, graph, selectedIds, overlays) r.drawFlashes(canvas, graph) - drawPageGuides(r, canvas, graph, overlays.guidePreview, overlays.selectedGuide) + drawPageGuides( + r, + canvas, + graph, + overlays.guidePreview, + overlays.hoveredGuide, + overlays.selectedGuide + ) r.drawSnapGuides(canvas, overlays.snapGuides) r.drawMarquee(canvas, overlays.marquee) r.drawLayoutInsertIndicator(canvas, overlays.layoutInsertIndicator) diff --git a/packages/core/src/canvas/renderer/types.ts b/packages/core/src/canvas/renderer/types.ts index b048e4eea..58f39c131 100644 --- a/packages/core/src/canvas/renderer/types.ts +++ b/packages/core/src/canvas/renderer/types.ts @@ -23,6 +23,7 @@ export interface RenderOverlays { marquee?: Rect | null snapGuides?: SnapGuide[] guidePreview?: GuidePreview | null + hoveredGuide?: GuideSelection | null selectedGuide?: GuideSelection | null rotationPreview?: { nodeId: string; angle: number } | null dropTargetId?: string | null diff --git a/packages/core/src/editor/create.ts b/packages/core/src/editor/create.ts index 579d71efe..53a1cd54a 100644 --- a/packages/core/src/editor/create.ts +++ b/packages/core/src/editor/create.ts @@ -210,6 +210,7 @@ export function createEditor(options?: EditorOptions) { state.measurementMode = 'off' state.snapGuides = [] state.guidePreview = null + state.hoveredGuide = null state.selectedGuide = null state.layoutInsertIndicator = null state.dropTargetId = null diff --git a/packages/core/src/editor/guide-preview.ts b/packages/core/src/editor/guide-preview.ts index e72f5ce44..0f1c81189 100644 --- a/packages/core/src/editor/guide-preview.ts +++ b/packages/core/src/editor/guide-preview.ts @@ -7,4 +7,5 @@ export interface GuidePreview { ownerId: string axis: 'x' | 'y' position: number + source?: GuideSelection } diff --git a/packages/core/src/editor/selection/overlays.ts b/packages/core/src/editor/selection/overlays.ts index c688d708d..b731e511f 100644 --- a/packages/core/src/editor/selection/overlays.ts +++ b/packages/core/src/editor/selection/overlays.ts @@ -20,6 +20,13 @@ export function createSelectionOverlayActions(ctx: EditorContext) { ctx.requestRepaint() } + function setHoveredGuide(selection: typeof ctx.state.hoveredGuide) { + const current = ctx.state.hoveredGuide + if (current?.ownerId === selection?.ownerId && current?.guideId === selection?.guideId) return + ctx.state.hoveredGuide = selection + ctx.requestRepaint() + } + function setSelectedGuide(selection: typeof ctx.state.selectedGuide) { ctx.state.selectedGuide = selection if (selection) ctx.setSelectedIds(new Set()) @@ -73,6 +80,7 @@ export function createSelectionOverlayActions(ctx: EditorContext) { setMarquee, setSnapGuides, setGuidePreview, + setHoveredGuide, setSelectedGuide, setRotationPreview, setHoveredNode, diff --git a/packages/core/src/editor/state/view.ts b/packages/core/src/editor/state/view.ts index 459b8b77d..336a6f293 100644 --- a/packages/core/src/editor/state/view.ts +++ b/packages/core/src/editor/state/view.ts @@ -8,6 +8,7 @@ export function createDefaultEditorViewState(pageId: string): EditorViewState { marquee: null, snapGuides: [], guidePreview: null, + hoveredGuide: null, selectedGuide: null, rotationPreview: null, dropTargetId: null, @@ -38,6 +39,7 @@ export function copyEditorViewState(source: EditorViewState): EditorViewState { marquee: structuredClone(source.marquee), snapGuides: structuredClone(source.snapGuides), guidePreview: structuredClone(source.guidePreview), + hoveredGuide: structuredClone(source.hoveredGuide), selectedGuide: structuredClone(source.selectedGuide), rotationPreview: structuredClone(source.rotationPreview), layoutInsertIndicator: structuredClone(source.layoutInsertIndicator), diff --git a/packages/core/src/editor/types.ts b/packages/core/src/editor/types.ts index 237353450..3ca077b38 100644 --- a/packages/core/src/editor/types.ts +++ b/packages/core/src/editor/types.ts @@ -54,6 +54,7 @@ export interface EditorViewState { marquee: Rect | null snapGuides: SnapGuide[] guidePreview: GuidePreview | null + hoveredGuide: GuideSelection | null selectedGuide: GuideSelection | null rotationPreview: { nodeId: string; angle: number } | null dropTargetId: string | null diff --git a/packages/vue/src/canvas/useCanvasInput.ts b/packages/vue/src/canvas/useCanvasInput.ts index 2ff590894..7822ca664 100644 --- a/packages/vue/src/canvas/useCanvasInput.ts +++ b/packages/vue/src/canvas/useCanvasInput.ts @@ -293,10 +293,28 @@ export function useCanvasInput( return axis === 'x' ? 'ew-resize' : 'ns-resize' } + function rulerGuideAxis(sx: number, sy: number): 'x' | 'y' | null { + if (sy < RULER_SIZE) return 'y' + if (sx < RULER_SIZE) return 'x' + return null + } + + function updateGuideHoverCursor(sx: number, sy: number, cx: number, cy: number) { + const guideHit = guideHitTest(sx, sy) + editor.setHoveredGuide( + guideHit ? { ownerId: guideHit.ownerId, guideId: guideHit.guideId } : null + ) + if (guideHit) return guideCursor(guideHit.axis) + const rulerAxis = rulerGuideAxis(sx, sy) + if (rulerAxis) return guideCursor(rulerAxis) + return updateHoverCursor(cx, cy, editor, hitFns, editor.state.measurementMode === 'deep') + } + function startExistingGuideDrag(sx: number, sy: number): boolean { const hit = guideHitTest(sx, sy) if (!hit) return false editor.setSelectedGuide({ ownerId: hit.ownerId, guideId: hit.guideId }) + editor.setHoveredGuide(null) cursorOverride.value = guideCursor(hit.axis) setDrag({ type: 'guide', @@ -407,10 +425,7 @@ export function useCanvasInput( if (!drag.value && editor.state.activeTool === 'SELECT') { const { sx, sy, cx, cy } = coords - const guideHit = guideHitTest(sx, sy) - cursorOverride.value = guideHit - ? guideCursor(guideHit.axis) - : updateHoverCursor(cx, cy, editor, hitFns, editor.state.measurementMode === 'deep') + cursorOverride.value = updateGuideHoverCursor(sx, sy, cx, cy) editor.setAutoLayoutHover( editor.state.measurementMode === 'off' ? resolveAutoLayoutHover(cx, cy, editor) : null ) @@ -437,7 +452,15 @@ export function useCanvasInput( const local = owner && owner.type !== 'CANVAS' ? canvasToLocal(cx, cy, owner.id) : null d.ownerId = target.id d.position = d.axis === 'x' ? (local?.lx ?? cx) : (local?.ly ?? cy) - editor.setGuidePreview({ ownerId: d.ownerId, axis: d.axis, position: d.position }) + editor.setGuidePreview({ + ownerId: d.ownerId, + axis: d.axis, + position: d.position, + source: + d.guideId && d.originalOwnerId + ? { ownerId: d.originalOwnerId, guideId: d.guideId } + : undefined + }) return } @@ -495,6 +518,7 @@ export function useCanvasInput( } } editor.setGuidePreview(null) + editor.setHoveredGuide(null) } function onMouseUp() { @@ -542,6 +566,7 @@ export function useCanvasInput( editor.setLayoutInsertIndicator(null) editor.setDropTarget(null) editor.setGuidePreview(null) + editor.setHoveredGuide(null) } function cancelPointerInteraction() { @@ -568,6 +593,7 @@ export function useCanvasInput( editor.setMeasurementMode('off') if (!drag.value) { editor.setHoveredNode(null) + editor.setHoveredGuide(null) } }) useEventListener(