fix(canvas): render live guide drag feedback
This commit is contained in:
parent
581d774d0e
commit
197364923b
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ export interface GuidePreview {
|
|||
ownerId: string
|
||||
axis: 'x' | 'y'
|
||||
position: number
|
||||
source?: GuideSelection
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue