feat(canvas): match hover outline to selection style for components and instances

hover outlines now use purple solid for reusable components and #9281f7
dashed for instances, consistent with their selection border styling
This commit is contained in:
Fini 2026-02-21 04:47:08 +08:00
parent 009ae2abad
commit a93a6fa515

View file

@ -1,12 +1,29 @@
import { useEffect } from 'react'
import { useCanvasStore } from '@/stores/canvas-store'
import { useDocumentStore } from '@/stores/document-store'
import type { FabricObjectWithPenId } from './canvas-object-factory'
import { resolveTargetAtDepth, getChildIds } from './selection-context'
import { COMPONENT_COLOR, INSTANCE_COLOR } from './canvas-constants'
import type { PenNode } from '@/types/pen'
const HOVER_COLOR = '#3b82f6'
const HOVER_LINE_WIDTH = 1.5
const CHILD_DASH = [4, 4]
function collectReusableIds(nodes: PenNode[], result: Set<string>) {
for (const node of nodes) {
if ('reusable' in node && node.reusable === true) result.add(node.id)
if ('children' in node && node.children) collectReusableIds(node.children, result)
}
}
function collectInstanceIds(nodes: PenNode[], result: Set<string>) {
for (const node of nodes) {
if (node.type === 'ref') result.add(node.id)
if ('children' in node && node.children) collectInstanceIds(node.children, result)
}
}
export function useCanvasHover() {
useEffect(() => {
const interval = setInterval(() => {
@ -66,6 +83,12 @@ export function useCanvasHover() {
const zoom = vpt[0]
const dpr = el.width / el.offsetWidth
const docChildren = useDocumentStore.getState().document.children
const reusableIds = new Set<string>()
const instanceIds = new Set<string>()
collectReusableIds(docChildren, reusableIds)
collectInstanceIds(docChildren, instanceIds)
ctx.save()
ctx.setTransform(
vpt[0] * dpr, vpt[1] * dpr,
@ -76,13 +99,13 @@ export function useCanvasHover() {
// Solid outline on hovered target — skip if already selected
// (Fabric draws its own selection handles)
if (!isSelected) {
drawNodeOutline(ctx, hoveredId, false, zoom)
drawNodeOutline(ctx, hoveredId, false, zoom, reusableIds, instanceIds)
}
// Dashed outlines on direct children — always draw on hover
const childIds = getChildIds(hoveredId)
for (const childId of childIds) {
drawNodeOutline(ctx, childId, true, zoom)
drawNodeOutline(ctx, childId, true, zoom, reusableIds, instanceIds)
}
ctx.restore()
@ -93,6 +116,8 @@ export function useCanvasHover() {
nodeId: string,
dashed: boolean,
zoom: number,
reusableIds: Set<string>,
instanceIds: Set<string>,
) {
const objects = canvas!.getObjects() as FabricObjectWithPenId[]
const obj = objects.find((o) => o.penNodeId === nodeId)
@ -104,6 +129,9 @@ export function useCanvasHover() {
const h = (obj.height ?? 0) * (obj.scaleY ?? 1)
const angle = obj.angle ?? 0
const isReusable = reusableIds.has(nodeId)
const isInstance = instanceIds.has(nodeId)
ctx.save()
if (angle !== 0) {
@ -114,9 +142,15 @@ export function useCanvasHover() {
ctx.translate(-cx, -cy)
}
ctx.strokeStyle = HOVER_COLOR
ctx.strokeStyle = isReusable
? COMPONENT_COLOR
: isInstance
? INSTANCE_COLOR
: HOVER_COLOR
ctx.lineWidth = HOVER_LINE_WIDTH / zoom
if (dashed) {
if (isInstance) {
ctx.setLineDash(CHILD_DASH.map((d) => d / zoom))
} else if (dashed) {
ctx.setLineDash(CHILD_DASH.map((d) => d / zoom))
} else {
ctx.setLineDash([])