feat(tool): shape

This commit is contained in:
zhangheng
2026-04-03 20:40:04 +08:00
parent b1dca7f2a8
commit a949987ee5
4 changed files with 140 additions and 0 deletions

View File

@@ -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<unknown> | undefined
) => {
const { size } = props
const canvasRef = useRef<HTMLCanvasElement>(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 (
<canvas
ref={canvasRef}
style={{
backgroundColor: "transparent",
position: "absolute",
zIndex: 52,
pointerEvents: "none",
}}
/>
)
}
export default forwardRef(AssistShapeComponent)

View File

@@ -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<any>(null)
const assistShapeComponentRef = useRef<any>(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 && (
<AssistShapeComponent
size={assistToolSize}
ref={assistShapeComponentRef}
/>
)}
{imgSize?.clientWidth && imgSize?.clientHeight && (
<canvas
id="paper-canvas"

View File

@@ -214,6 +214,10 @@ const TopTools = (
setImageFilter,
crosshairStatus,
setCrosshairStatus,
assistToolEnabled,
setAssistToolEnabled,
assistToolSize,
setAssistToolSize,
needBackup,
setNeedBackup,
checkSize,
@@ -2030,6 +2034,36 @@ const TopTools = (
/>
)}
</Flex>
<Flex justify="space-between" align="center" p={8} gap={4}>
<ActionIcon
variant="transparent"
style={{ cursor: "pointer" }}
c={assistToolEnabled ? "blue" : "var(--mantine-color-text)"}
onClick={() => {
setAssistToolEnabled(!assistToolEnabled)
}}>
<Eclipse size={16} />
</ActionIcon>
<NumberInput
value={assistToolSize}
w={58}
min={1}
size="xs"
hideControls
onChange={(v) => {
const next = Number(v)
if (Number.isFinite(next) && next >= 1) {
setAssistToolSize(next)
}
}}
onFocus={() => {
useKeyEventStore.getState().setFocusInput(true)
}}
onBlur={() => {
useKeyEventStore.getState().setFocusInput(false)
}}
/>
</Flex>
<Divider orientation="vertical" />
<Flex justify="center" align="center">
<ActionIcon

View File

@@ -28,6 +28,10 @@ interface TopToolsState {
) => 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<TopToolsState>()(
...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) => ({