From a949987ee5bd39a7338458da2c98f9d0a4d2b65a Mon Sep 17 00:00:00 2001 From: zhangheng Date: Fri, 3 Apr 2026 20:40:04 +0800 Subject: [PATCH] feat(tool): shape --- .../label/components/AssistShapeComponent.tsx | 77 +++++++++++++++++++ .../label/components/PaperContainer.tsx | 11 +++ components/label/components/TopTools.tsx | 34 ++++++++ components/label/useTopToolsStore.ts | 18 +++++ 4 files changed, 140 insertions(+) create mode 100644 components/label/components/AssistShapeComponent.tsx diff --git a/components/label/components/AssistShapeComponent.tsx b/components/label/components/AssistShapeComponent.tsx new file mode 100644 index 0000000..514d09a --- /dev/null +++ b/components/label/components/AssistShapeComponent.tsx @@ -0,0 +1,77 @@ +"use client" +import React, { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useRef, +} from "react" + +interface AssistShapeComponentProps { + size: number +} + +const AssistShapeComponent = ( + props: AssistShapeComponentProps, + ref: React.Ref | undefined +) => { + const { size } = props + const canvasRef = useRef(null) + + const drawAssistShape = useCallback( + (centerX: number, centerY: number) => { + const canvas = canvasRef.current + const ctx = canvas?.getContext("2d") + if (!canvas || !ctx) return + + const radius = Math.max(1, size) + const side = radius * 2 + const left = centerX - radius + const top = centerY - radius + + ctx.clearRect(0, 0, canvas.width, canvas.height) + ctx.beginPath() + ctx.strokeStyle = "#EF4444" + ctx.lineWidth = 2 + ctx.setLineDash([6, 4]) + ctx.strokeRect(left, top, side, side) + ctx.arc(centerX, centerY, radius, 0, Math.PI * 2) + ctx.stroke() + }, + [size] + ) + + useEffect(() => { + const canvas = canvasRef.current + if (!canvas) return + canvas.width = window.innerWidth + canvas.height = window.innerHeight + const rect = canvas.getBoundingClientRect() + drawAssistShape(rect.width / 2, rect.height / 2) + }, [drawAssistShape]) + + const updateAssistShape = (event: { clientX: number; clientY: number }) => { + const canvas = canvasRef.current + if (!canvas) return + const rect = canvas.getBoundingClientRect() + const centerX = event.clientX - rect.left + const centerY = event.clientY - rect.top + drawAssistShape(centerX, centerY) + } + + useImperativeHandle(ref, () => ({ updateAssistShape })) + + return ( + + ) +} + +export default forwardRef(AssistShapeComponent) diff --git a/components/label/components/PaperContainer.tsx b/components/label/components/PaperContainer.tsx index 9c9cc9d..b134bff 100644 --- a/components/label/components/PaperContainer.tsx +++ b/components/label/components/PaperContainer.tsx @@ -59,6 +59,7 @@ import { checkCommentsIsSame } from "../util" import { safeClone } from "../utils/clone" import { labelTypeMap } from "../utils/constants" import { adjustPoints } from "../utils/paperjs" +import AssistShapeComponent from "./AssistShapeComponent" import CrosshairComponent from "./CrosshairComponent" import { renderOperationIcon } from "./RightObjectTools" @@ -222,6 +223,8 @@ const PaperContainer = ( objectOperations, imageFilter, crosshairStatus, + assistToolEnabled, + assistToolSize, showTags, showTagsConfigs, } = useTopToolsStore() @@ -2671,9 +2674,11 @@ const PaperContainer = ( }, [activeImage]) const crosshairComponentRef = useRef(null) + const assistShapeComponentRef = useRef(null) const handleCrosshairMove = (event: { clientX: number; clientY: number }) => { crosshairComponentRef.current?.updateCrosshair(event) + assistShapeComponentRef.current?.updateAssistShape(event) } useImperativeHandle(ref, () => ({ @@ -2703,6 +2708,12 @@ const PaperContainer = ( ref={crosshairComponentRef} /> )} + {assistToolEnabled && ( + + )} {imgSize?.clientWidth && imgSize?.clientHeight && ( )} + + { + setAssistToolEnabled(!assistToolEnabled) + }}> + + + { + const next = Number(v) + if (Number.isFinite(next) && next >= 1) { + setAssistToolSize(next) + } + }} + onFocus={() => { + useKeyEventStore.getState().setFocusInput(true) + }} + onBlur={() => { + useKeyEventStore.getState().setFocusInput(false) + }} + /> + void crosshairStatus: "hidden" | "line" | "carve" setCrosshairStatus: (val: "hidden" | "line" | "carve") => void + assistToolEnabled: boolean + setAssistToolEnabled: (val: boolean) => void + assistToolSize: number + setAssistToolSize: (val: number) => void showTags: boolean setShowTags: (val: boolean) => void showTagsConfigs: string[] @@ -85,6 +89,8 @@ const initialTopToolsState = { brightness: 0, }, crosshairStatus: "hidden" as "hidden" | "line" | "carve", + assistToolEnabled: false, + assistToolSize: 10, drawOption: "default" as "default" | "intersect" | "unite", saveCurrentScale: false, scale: 1, @@ -156,6 +162,18 @@ export const useTopToolsStore = create()( ...state, crosshairStatus: val, })), + assistToolEnabled: initialTopToolsState.assistToolEnabled, + setAssistToolEnabled: (val) => + set((state: TopToolsState) => ({ + ...state, + assistToolEnabled: val, + })), + assistToolSize: initialTopToolsState.assistToolSize, + setAssistToolSize: (val) => + set((state: TopToolsState) => ({ + ...state, + assistToolSize: Math.max(1, val), + })), showTags: false, setShowTags: (val) => set((state: TopToolsState) => ({