From 5324a91fecdc3d242b08ec09f681d3372bc3e3b1 Mon Sep 17 00:00:00 2001 From: Fini Date: Sat, 21 Feb 2026 04:47:08 +0800 Subject: [PATCH] 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 --- src/canvas/use-canvas-hover.ts | 42 ++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/src/canvas/use-canvas-hover.ts b/src/canvas/use-canvas-hover.ts index f7181da28..1697dd69b 100644 --- a/src/canvas/use-canvas-hover.ts +++ b/src/canvas/use-canvas-hover.ts @@ -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) { + 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) { + 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() + const instanceIds = new Set() + 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, + instanceIds: Set, ) { 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([])