Merge branch 'zh' into 'main'
Zh See merge request prism/lable/labelimage!16
This commit is contained in:
@@ -2024,10 +2024,22 @@ const LabelPage = ({
|
|||||||
const mode = usePaperStore.getState().mode
|
const mode = usePaperStore.getState().mode
|
||||||
const lowerKey = e.key.toLowerCase()
|
const lowerKey = e.key.toLowerCase()
|
||||||
const commandKey = e.ctrlKey || e.metaKey
|
const commandKey = e.ctrlKey || e.metaKey
|
||||||
|
const isShortcutGuideKey =
|
||||||
|
e.code === "Slash" || e.key === "?" || e.key === "/"
|
||||||
|
|
||||||
if (e.repeat && ["a", "b", "d", "h", "m", ",", "."].includes(lowerKey))
|
if (
|
||||||
|
e.repeat &&
|
||||||
|
["a", "b", "d", "h", "m", ",", ".", "/", "?"].includes(lowerKey)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if (!e.ctrlKey && !e.metaKey && !e.altKey && isShortcutGuideKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
const keyboardState = useKeyboardStore.getState()
|
||||||
|
keyboardState.setShowShortcutGuide(!keyboardState.showShortcutGuide)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "m") {
|
if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "m") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const topState = useTopToolsStore.getState()
|
const topState = useTopToolsStore.getState()
|
||||||
|
|||||||
@@ -121,6 +121,49 @@ type FrameSwitchGhostState = {
|
|||||||
animating: boolean
|
animating: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SHORTCUT_GUIDE_SECTIONS = [
|
||||||
|
{
|
||||||
|
id: "global",
|
||||||
|
title: "全局",
|
||||||
|
items: [
|
||||||
|
["? / /(Shift+/)", "显示或隐藏快捷键说明"],
|
||||||
|
["0-9", "切换当前标注类别"],
|
||||||
|
["滚轮", "缩放画布"],
|
||||||
|
["Ctrl / ⌘ + S", "保存当前标注"],
|
||||||
|
["Ctrl / ⌘ + Z", "撤销上一步"],
|
||||||
|
["Ctrl / ⌘ + C", "复制当前选中对象"],
|
||||||
|
["Ctrl / ⌘ + V", "粘贴到多帧"],
|
||||||
|
["Ctrl / ⌘ + F", "合并当前选中对象"],
|
||||||
|
["Ctrl / ⌘ + G", "创建对象组"],
|
||||||
|
["Alt + X", "提交任务"],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "edit",
|
||||||
|
title: "对象编辑",
|
||||||
|
items: [
|
||||||
|
["Delete", "删除当前选中对象或点"],
|
||||||
|
["H", "隐藏当前选中对象 / 全部对象"],
|
||||||
|
["M", "切换磁吸模式"],
|
||||||
|
["A", "进入选中对象补加绘制"],
|
||||||
|
["D", "进入选中对象裁剪绘制"],
|
||||||
|
["B", "续画当前选中的 polygon"],
|
||||||
|
[",", "对选中 polygon 执行相减"],
|
||||||
|
[".", "对选中 polygon / brush 执行融合"],
|
||||||
|
["Esc", "取消当前绘制或退出辅助态"],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tool",
|
||||||
|
title: "专项工具",
|
||||||
|
items: [
|
||||||
|
["F1", "进入辅助标注 / 在辅助模式下确认"],
|
||||||
|
["I", "执行追踪"],
|
||||||
|
["P", "QA 文本场景检查错词"],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
const clonePaperPoint = (point: paper.Point | null | undefined) => {
|
const clonePaperPoint = (point: paper.Point | null | undefined) => {
|
||||||
if (!point) return null
|
if (!point) return null
|
||||||
return new paper.Point(point.x, point.y)
|
return new paper.Point(point.x, point.y)
|
||||||
@@ -183,6 +226,11 @@ const PaperContainer = (
|
|||||||
false && updateLoadingFlag
|
false && updateLoadingFlag
|
||||||
const containerRef = useRef<any>(null)
|
const containerRef = useRef<any>(null)
|
||||||
const contextMenuRef = useRef<HTMLDivElement>(null)
|
const contextMenuRef = useRef<HTMLDivElement>(null)
|
||||||
|
const shortcutGuideCardRef = useRef<HTMLDivElement>(null)
|
||||||
|
const shortcutGuideDragRef = useRef<{
|
||||||
|
offsetX: number
|
||||||
|
offsetY: number
|
||||||
|
} | null>(null)
|
||||||
const [imgSize, setImageSize] = useState<{
|
const [imgSize, setImageSize] = useState<{
|
||||||
clientWidth: number
|
clientWidth: number
|
||||||
clientHeight: number
|
clientHeight: number
|
||||||
@@ -451,6 +499,19 @@ const PaperContainer = (
|
|||||||
showTags,
|
showTags,
|
||||||
showTagsConfigs,
|
showTagsConfigs,
|
||||||
} = useTopToolsStore()
|
} = useTopToolsStore()
|
||||||
|
const showShortcutGuide = useKeyboardStore((state) => state.showShortcutGuide)
|
||||||
|
const shortcutGuidePosition = useKeyboardStore(
|
||||||
|
(state) => state.shortcutGuidePosition
|
||||||
|
)
|
||||||
|
const setShortcutGuidePosition = useKeyboardStore(
|
||||||
|
(state) => state.setShortcutGuidePosition
|
||||||
|
)
|
||||||
|
const shortcutGuideCollapsedSections = useKeyboardStore(
|
||||||
|
(state) => state.shortcutGuideCollapsedSections
|
||||||
|
)
|
||||||
|
const toggleShortcutGuideSection = useKeyboardStore(
|
||||||
|
(state) => state.toggleShortcutGuideSection
|
||||||
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
pathStatus: paperPathStatus,
|
pathStatus: paperPathStatus,
|
||||||
@@ -469,8 +530,107 @@ const PaperContainer = (
|
|||||||
setContextMenuOperationId,
|
setContextMenuOperationId,
|
||||||
} = useOtherToolsStore()
|
} = useOtherToolsStore()
|
||||||
const { renderTag } = useRenderTag()
|
const { renderTag } = useRenderTag()
|
||||||
|
|
||||||
const [setup, setSetup] = useState(false)
|
const [setup, setSetup] = useState(false)
|
||||||
|
const [isShortcutGuideDragging, setIsShortcutGuideDragging] = useState(false)
|
||||||
|
|
||||||
|
const clampShortcutGuidePosition = useCallback(
|
||||||
|
(position: { x: number; y: number }) => {
|
||||||
|
const container = containerRef.current as HTMLDivElement | null
|
||||||
|
const card = shortcutGuideCardRef.current
|
||||||
|
if (!container || !card) return position
|
||||||
|
|
||||||
|
const padding = 12
|
||||||
|
const maxX = Math.max(
|
||||||
|
padding,
|
||||||
|
container.clientWidth - card.offsetWidth - padding
|
||||||
|
)
|
||||||
|
const maxY = Math.max(
|
||||||
|
padding,
|
||||||
|
container.clientHeight - card.offsetHeight - padding
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: Math.min(Math.max(position.x, padding), maxX),
|
||||||
|
y: Math.min(Math.max(position.y, padding), maxY),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleShortcutGuideDragStart = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (event.button !== 0) return
|
||||||
|
|
||||||
|
const container = containerRef.current as HTMLDivElement | null
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
const rect = container.getBoundingClientRect()
|
||||||
|
shortcutGuideDragRef.current = {
|
||||||
|
offsetX: event.clientX - rect.left - shortcutGuidePosition.x,
|
||||||
|
offsetY: event.clientY - rect.top - shortcutGuidePosition.y,
|
||||||
|
}
|
||||||
|
setIsShortcutGuideDragging(true)
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
},
|
||||||
|
[shortcutGuidePosition.x, shortcutGuidePosition.y]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isShortcutGuideDragging) return
|
||||||
|
|
||||||
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
|
const container = containerRef.current as HTMLDivElement | null
|
||||||
|
const dragState = shortcutGuideDragRef.current
|
||||||
|
if (!container || !dragState) return
|
||||||
|
|
||||||
|
const rect = container.getBoundingClientRect()
|
||||||
|
setShortcutGuidePosition(
|
||||||
|
clampShortcutGuidePosition({
|
||||||
|
x: event.clientX - rect.left - dragState.offsetX,
|
||||||
|
y: event.clientY - rect.top - dragState.offsetY,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopDragging = () => {
|
||||||
|
shortcutGuideDragRef.current = null
|
||||||
|
setIsShortcutGuideDragging(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", handleMouseMove)
|
||||||
|
window.addEventListener("mouseup", stopDragging)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("mousemove", handleMouseMove)
|
||||||
|
window.removeEventListener("mouseup", stopDragging)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
clampShortcutGuidePosition,
|
||||||
|
isShortcutGuideDragging,
|
||||||
|
setShortcutGuidePosition,
|
||||||
|
])
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!showShortcutGuide) return
|
||||||
|
|
||||||
|
const nextPosition = clampShortcutGuidePosition(shortcutGuidePosition)
|
||||||
|
if (
|
||||||
|
nextPosition.x !== shortcutGuidePosition.x ||
|
||||||
|
nextPosition.y !== shortcutGuidePosition.y
|
||||||
|
) {
|
||||||
|
setShortcutGuidePosition(nextPosition)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
clampShortcutGuidePosition,
|
||||||
|
imgSize?.clientHeight,
|
||||||
|
imgSize?.clientWidth,
|
||||||
|
setShortcutGuidePosition,
|
||||||
|
shortcutGuideCollapsedSections,
|
||||||
|
shortcutGuidePosition,
|
||||||
|
showShortcutGuide,
|
||||||
|
])
|
||||||
|
|
||||||
const labelRenderScaleRef = useRef<Record<string, number>>({})
|
const labelRenderScaleRef = useRef<Record<string, number>>({})
|
||||||
const renderedImageRef = useRef("")
|
const renderedImageRef = useRef("")
|
||||||
const frameImageCacheRef = useRef<Map<string, HTMLImageElement>>(new Map())
|
const frameImageCacheRef = useRef<Map<string, HTMLImageElement>>(new Map())
|
||||||
@@ -4105,6 +4265,158 @@ const PaperContainer = (
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{showShortcutGuide && (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: shortcutGuidePosition.y,
|
||||||
|
left: shortcutGuidePosition.x,
|
||||||
|
zIndex: 48,
|
||||||
|
width: 264,
|
||||||
|
maxWidth: "min(264px, calc(100% - 24px))",
|
||||||
|
pointerEvents: "none",
|
||||||
|
background: "transparent",
|
||||||
|
}}>
|
||||||
|
<Card
|
||||||
|
ref={shortcutGuideCardRef}
|
||||||
|
shadow="sm"
|
||||||
|
padding="xs"
|
||||||
|
radius="md"
|
||||||
|
withBorder
|
||||||
|
onWheel={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
pointerEvents: "auto",
|
||||||
|
maxHeight: "calc(100% - 24px)",
|
||||||
|
overflowY: "auto",
|
||||||
|
background:
|
||||||
|
"light-dark(rgba(255,255,255,0.78), rgba(24,28,36,0.74))",
|
||||||
|
borderColor:
|
||||||
|
"light-dark(rgba(34,139,230,0.18), rgba(114,192,252,0.18))",
|
||||||
|
backdropFilter: "blur(6px)",
|
||||||
|
}}>
|
||||||
|
<Stack gap={8}>
|
||||||
|
<Box
|
||||||
|
onMouseDown={handleShortcutGuideDragStart}
|
||||||
|
style={{
|
||||||
|
cursor: isShortcutGuideDragging ? "grabbing" : "grab",
|
||||||
|
userSelect: "none",
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: "8px 10px",
|
||||||
|
background:
|
||||||
|
"light-dark(rgba(34,139,230,0.08), rgba(76,171,247,0.12))",
|
||||||
|
border:
|
||||||
|
"1px solid light-dark(rgba(34,139,230,0.12), rgba(114,192,252,0.14))",
|
||||||
|
}}>
|
||||||
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
align="flex-start"
|
||||||
|
gap="xs"
|
||||||
|
wrap="nowrap">
|
||||||
|
<Box>
|
||||||
|
<Text fw={700} size="sm">
|
||||||
|
快捷键说明
|
||||||
|
</Text>
|
||||||
|
<Text size="10px" c="dimmed" style={{ lineHeight: 1.35 }}>
|
||||||
|
默认靠左显示,可拖动;按{" "}
|
||||||
|
<Text component="span" fw={700}>
|
||||||
|
?
|
||||||
|
</Text>{" "}
|
||||||
|
快速开关
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Text
|
||||||
|
size="10px"
|
||||||
|
fw={700}
|
||||||
|
px={6}
|
||||||
|
py={3}
|
||||||
|
style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
borderRadius: 999,
|
||||||
|
background:
|
||||||
|
"light-dark(rgba(255,255,255,0.82), rgba(255,255,255,0.08))",
|
||||||
|
}}>
|
||||||
|
拖动
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
{SHORTCUT_GUIDE_SECTIONS.map((section) => {
|
||||||
|
const collapsed = !!shortcutGuideCollapsedSections[section.id]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={section.id}
|
||||||
|
px="xs"
|
||||||
|
py={8}
|
||||||
|
style={{
|
||||||
|
borderRadius: 10,
|
||||||
|
background:
|
||||||
|
"light-dark(rgba(248,250,252,0.72), rgba(15,18,24,0.32))",
|
||||||
|
border:
|
||||||
|
"1px solid light-dark(rgba(34,139,230,0.08), rgba(114,192,252,0.08))",
|
||||||
|
}}>
|
||||||
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
align="center"
|
||||||
|
gap="xs"
|
||||||
|
wrap="nowrap"
|
||||||
|
onClick={() => {
|
||||||
|
toggleShortcutGuideSection(section.id)
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
userSelect: "none",
|
||||||
|
}}>
|
||||||
|
<Text size="xs" fw={700} c="blue">
|
||||||
|
{section.title}
|
||||||
|
</Text>
|
||||||
|
<Text size="10px" c="dimmed" fw={700}>
|
||||||
|
{collapsed ? "展开" : "收起"}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
{!collapsed && (
|
||||||
|
<Stack gap={6} mt={8}>
|
||||||
|
{section.items.map(([key, description]) => (
|
||||||
|
<Group
|
||||||
|
key={`${section.id}-${key}`}
|
||||||
|
justify="space-between"
|
||||||
|
align="flex-start"
|
||||||
|
gap={6}
|
||||||
|
wrap="nowrap">
|
||||||
|
<Text
|
||||||
|
size="10px"
|
||||||
|
fw={700}
|
||||||
|
px={5}
|
||||||
|
py={2}
|
||||||
|
style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
borderRadius: 6,
|
||||||
|
border:
|
||||||
|
"1px solid light-dark(rgba(34,139,230,0.16), rgba(114,192,252,0.16))",
|
||||||
|
background:
|
||||||
|
"light-dark(rgba(255,255,255,0.88), rgba(255,255,255,0.06))",
|
||||||
|
}}>
|
||||||
|
{key}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
size="10px"
|
||||||
|
c="dimmed"
|
||||||
|
ta="right"
|
||||||
|
style={{ lineHeight: 1.35 }}>
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
<Modal
|
<Modal
|
||||||
opened={confirmModalOpened}
|
opened={confirmModalOpened}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
Film,
|
Film,
|
||||||
FolderDownIcon,
|
FolderDownIcon,
|
||||||
Group as GroupIcon,
|
Group as GroupIcon,
|
||||||
|
Keyboard,
|
||||||
Locate,
|
Locate,
|
||||||
LocateFixed,
|
LocateFixed,
|
||||||
LocateOff,
|
LocateOff,
|
||||||
@@ -1560,6 +1561,10 @@ const TopTools = (
|
|||||||
|
|
||||||
const operationSchema = getOperationSchema(activeOperation)
|
const operationSchema = getOperationSchema(activeOperation)
|
||||||
const drawAction = useKeyboardStore((state) => state.drawAction)
|
const drawAction = useKeyboardStore((state) => state.drawAction)
|
||||||
|
const showShortcutGuide = useKeyboardStore((state) => state.showShortcutGuide)
|
||||||
|
const setShowShortcutGuide = useKeyboardStore(
|
||||||
|
(state) => state.setShowShortcutGuide
|
||||||
|
)
|
||||||
|
|
||||||
const subAttrForm = (
|
const subAttrForm = (
|
||||||
<Box w={256}>
|
<Box w={256}>
|
||||||
@@ -2529,6 +2534,23 @@ const TopTools = (
|
|||||||
<Component style={{ width: 20, height: 20 }} />
|
<Component style={{ width: 20, height: 20 }} />
|
||||||
<Text size="xs">对象</Text>
|
<Text size="xs">对象</Text>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
<Tooltip
|
||||||
|
label={
|
||||||
|
showShortcutGuide
|
||||||
|
? "关闭快捷键说明(?)"
|
||||||
|
: "打开快捷键说明(?)"
|
||||||
|
}>
|
||||||
|
<ActionIcon
|
||||||
|
variant="transparent"
|
||||||
|
c={showShortcutGuide ? "var(--mantine-color-text)" : "gray"}
|
||||||
|
style={{ width: "auto", display: "flex", gap: 4 }}
|
||||||
|
onClick={() => {
|
||||||
|
setShowShortcutGuide(!showShortcutGuide)
|
||||||
|
}}>
|
||||||
|
<Keyboard style={{ width: 20, height: 20 }} />
|
||||||
|
<Text size="xs">快捷键</Text>
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { create } from "zustand"
|
import { create } from "zustand"
|
||||||
|
import { persist } from "zustand/middleware"
|
||||||
|
|
||||||
export interface SelectedObjectEditTarget {
|
export interface SelectedObjectEditTarget {
|
||||||
imageId: string
|
imageId: string
|
||||||
@@ -12,6 +13,15 @@ interface KeyboardStore {
|
|||||||
setCtrl: (v: boolean) => void
|
setCtrl: (v: boolean) => void
|
||||||
shift: boolean
|
shift: boolean
|
||||||
setShift: (v: boolean) => void
|
setShift: (v: boolean) => void
|
||||||
|
showShortcutGuide: boolean
|
||||||
|
setShowShortcutGuide: (v: boolean) => void
|
||||||
|
shortcutGuidePosition: {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
setShortcutGuidePosition: (position: { x: number; y: number }) => void
|
||||||
|
shortcutGuideCollapsedSections: Record<string, boolean>
|
||||||
|
toggleShortcutGuideSection: (sectionId: string) => void
|
||||||
drawAction: "none" | "add" | "subtract"
|
drawAction: "none" | "add" | "subtract"
|
||||||
setDrawAction: (mode: "none" | "add" | "subtract") => void
|
setDrawAction: (mode: "none" | "add" | "subtract") => void
|
||||||
selectedObjectEditTarget: SelectedObjectEditTarget | null
|
selectedObjectEditTarget: SelectedObjectEditTarget | null
|
||||||
@@ -20,16 +30,51 @@ interface KeyboardStore {
|
|||||||
setCopyDataIds: (ids: number[]) => void
|
setCopyDataIds: (ids: number[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useKeyboardStore = create<KeyboardStore>((set) => ({
|
export const useKeyboardStore = create<KeyboardStore>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
shift: false,
|
shift: false,
|
||||||
|
showShortcutGuide: false,
|
||||||
|
shortcutGuidePosition: {
|
||||||
|
x: 16,
|
||||||
|
y: 16,
|
||||||
|
},
|
||||||
|
shortcutGuideCollapsedSections: {},
|
||||||
drawAction: "none",
|
drawAction: "none",
|
||||||
selectedObjectEditTarget: null,
|
selectedObjectEditTarget: null,
|
||||||
copyDataIds: [],
|
copyDataIds: [],
|
||||||
setCtrl: (v) => set((state) => ({ ...state, ctrl: v })),
|
setCtrl: (v) => set((state) => ({ ...state, ctrl: v })),
|
||||||
setShift: (v) => set((state) => ({ ...state, shift: v })),
|
setShift: (v) => set((state) => ({ ...state, shift: v })),
|
||||||
|
setShowShortcutGuide: (v) =>
|
||||||
|
set((state) => ({ ...state, showShortcutGuide: v })),
|
||||||
|
setShortcutGuidePosition: (position) =>
|
||||||
|
set((state) => ({
|
||||||
|
...state,
|
||||||
|
shortcutGuidePosition: {
|
||||||
|
x: Math.round(position.x),
|
||||||
|
y: Math.round(position.y),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
toggleShortcutGuideSection: (sectionId) =>
|
||||||
|
set((state) => ({
|
||||||
|
...state,
|
||||||
|
shortcutGuideCollapsedSections: {
|
||||||
|
...state.shortcutGuideCollapsedSections,
|
||||||
|
[sectionId]: !state.shortcutGuideCollapsedSections[sectionId],
|
||||||
|
},
|
||||||
|
})),
|
||||||
setDrawAction: (mode) => set((state) => ({ ...state, drawAction: mode })),
|
setDrawAction: (mode) => set((state) => ({ ...state, drawAction: mode })),
|
||||||
setSelectedObjectEditTarget: (target) =>
|
setSelectedObjectEditTarget: (target) =>
|
||||||
set((state) => ({ ...state, selectedObjectEditTarget: target })),
|
set((state) => ({ ...state, selectedObjectEditTarget: target })),
|
||||||
setCopyDataIds: (ids) => set((state) => ({ ...state, copyDataIds: ids })),
|
setCopyDataIds: (ids) => set((state) => ({ ...state, copyDataIds: ids })),
|
||||||
}))
|
}),
|
||||||
|
{
|
||||||
|
name: "keyboard-store",
|
||||||
|
partialize: (state) => ({
|
||||||
|
shortcutGuidePosition: state.shortcutGuidePosition,
|
||||||
|
shortcutGuideCollapsedSections: state.shortcutGuideCollapsedSections,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ const replacePaperWorkingPolygonShape = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
syncPaperCompoundPolygonChildrenData(nextShape, phase)
|
syncPaperCompoundPolygonChildrenData(nextShape, phase)
|
||||||
|
syncPaperObjectStrokeScaling(nextShape)
|
||||||
currentShape?.remove()
|
currentShape?.remove()
|
||||||
|
|
||||||
if (!nextShape.parent && originalParent && "insertChild" in originalParent) {
|
if (!nextShape.parent && originalParent && "insertChild" in originalParent) {
|
||||||
@@ -146,6 +147,7 @@ const createPaperPathFromGeometryContour = ({
|
|||||||
)
|
)
|
||||||
path.strokeColor = clonePaperColor(strokeColor)
|
path.strokeColor = clonePaperColor(strokeColor)
|
||||||
path.strokeWidth = strokeWidth
|
path.strokeWidth = strokeWidth
|
||||||
|
path.strokeScaling = false
|
||||||
path.fillColor = clonePaperColor(fillColor)
|
path.fillColor = clonePaperColor(fillColor)
|
||||||
path.visible = visible
|
path.visible = visible
|
||||||
path.opacity = opacity
|
path.opacity = opacity
|
||||||
@@ -215,6 +217,7 @@ const createPaperPolygonShapeFromGeometry = ({
|
|||||||
compoundPath.data = safeClone(baseData)
|
compoundPath.data = safeClone(baseData)
|
||||||
compoundPath.strokeColor = clonePaperColor(strokeColor)
|
compoundPath.strokeColor = clonePaperColor(strokeColor)
|
||||||
compoundPath.strokeWidth = strokeWidth
|
compoundPath.strokeWidth = strokeWidth
|
||||||
|
compoundPath.strokeScaling = false
|
||||||
compoundPath.fillColor = clonePaperColor(fillColor)
|
compoundPath.fillColor = clonePaperColor(fillColor)
|
||||||
compoundPath.visible = visible
|
compoundPath.visible = visible
|
||||||
compoundPath.opacity = opacity
|
compoundPath.opacity = opacity
|
||||||
@@ -899,6 +902,64 @@ const isPaperObjectType = (type: unknown) =>
|
|||||||
typeof type === "string" &&
|
typeof type === "string" &&
|
||||||
PAPER_OBJECT_TYPES.includes(type as (typeof PAPER_OBJECT_TYPES)[number])
|
PAPER_OBJECT_TYPES.includes(type as (typeof PAPER_OBJECT_TYPES)[number])
|
||||||
|
|
||||||
|
const syncPaperObjectStrokeScaling = (item: paper.Item | null | undefined) => {
|
||||||
|
if (!(item instanceof paper.Path || item instanceof paper.CompoundPath)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isPaperObjectType(item.data?.type)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item.strokeScaling = false
|
||||||
|
|
||||||
|
if (item instanceof paper.CompoundPath) {
|
||||||
|
;(item.children as paper.Path[]).forEach((child) => {
|
||||||
|
child.strokeScaling = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncAllPaperObjectStrokeScaling = () => {
|
||||||
|
const group = usePaperStore.getState().group
|
||||||
|
if (!group) return
|
||||||
|
|
||||||
|
group
|
||||||
|
.getItems({
|
||||||
|
match: (item: paper.Item) =>
|
||||||
|
(item instanceof paper.Path || item instanceof paper.CompoundPath) &&
|
||||||
|
isPaperObjectType(item.data?.type),
|
||||||
|
})
|
||||||
|
.forEach((item) => {
|
||||||
|
syncPaperObjectStrokeScaling(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ensureEditablePaperPathSelected = (objectId: number) => {
|
||||||
|
const objectPaths = usePaperStore
|
||||||
|
.getState()
|
||||||
|
.getItemsById(objectId)
|
||||||
|
.filter(
|
||||||
|
(item): item is paper.Path =>
|
||||||
|
item instanceof paper.Path && isPaperObjectType(item.data?.type)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!objectPaths.length) return null
|
||||||
|
|
||||||
|
const editablePaths = objectPaths.filter(
|
||||||
|
(item) => !item.data?.isHollowPolygon
|
||||||
|
)
|
||||||
|
const activePath =
|
||||||
|
editablePaths.find((item) => item.data?.selected) ||
|
||||||
|
editablePaths[0] ||
|
||||||
|
null
|
||||||
|
|
||||||
|
objectPaths.forEach((item) => {
|
||||||
|
item.data.selected = item === activePath
|
||||||
|
})
|
||||||
|
|
||||||
|
return activePath
|
||||||
|
}
|
||||||
|
|
||||||
const isPaperContextHitType = (type: unknown) =>
|
const isPaperContextHitType = (type: unknown) =>
|
||||||
typeof type === "string" &&
|
typeof type === "string" &&
|
||||||
PAPER_CONTEXT_HIT_TYPES.includes(
|
PAPER_CONTEXT_HIT_TYPES.includes(
|
||||||
@@ -1001,6 +1062,13 @@ const resetPaperShapeFillColor = (item: paper.Item) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const applyPaperSelectedPath = (item: paper.Path | paper.CompoundPath) => {
|
const applyPaperSelectedPath = (item: paper.Path | paper.CompoundPath) => {
|
||||||
|
const objectId = Number(item.data?.id)
|
||||||
|
const editablePath = Number.isFinite(objectId)
|
||||||
|
? ensureEditablePaperPathSelected(objectId)
|
||||||
|
: item instanceof paper.Path && !item.data?.isHollowPolygon
|
||||||
|
? item
|
||||||
|
: null
|
||||||
|
|
||||||
if (item.data.type === "rectangle" && item instanceof paper.Path) {
|
if (item.data.type === "rectangle" && item instanceof paper.Path) {
|
||||||
item.data.selected = true
|
item.data.selected = true
|
||||||
usePaperSupportStore.getState().handleBufferPaths(item)
|
usePaperSupportStore.getState().handleBufferPaths(item)
|
||||||
@@ -1008,19 +1076,25 @@ const applyPaperSelectedPath = (item: paper.Path | paper.CompoundPath) => {
|
|||||||
item.fillColor = item.data.fillColor || item.fillColor || null
|
item.fillColor = item.data.fillColor || item.fillColor || null
|
||||||
} else if (item.data.type === "brush") {
|
} else if (item.data.type === "brush") {
|
||||||
item.data.selected = true
|
item.data.selected = true
|
||||||
if (item instanceof paper.Path) {
|
if (editablePath) {
|
||||||
usePaperSupportStore.getState().handleMask(item)
|
usePaperSupportStore.getState().handleMask(editablePath)
|
||||||
usePaperSupportStore.getState().handleBufferPaths(item)
|
usePaperSupportStore.getState().handleBufferPaths(editablePath)
|
||||||
usePaperSupportStore.getState().handleCircles(item)
|
usePaperSupportStore.getState().handleCircles(editablePath)
|
||||||
}
|
}
|
||||||
} else if (item.data.type === "point" && item instanceof paper.Path) {
|
} else if (item.data.type === "point") {
|
||||||
item.data.selected = true
|
item.data.selected = true
|
||||||
usePaperSupportStore.getState().handleMask(item)
|
if (editablePath) {
|
||||||
usePaperSupportStore.getState().handleText(item)
|
usePaperSupportStore.getState().handleMask(editablePath)
|
||||||
usePaperSupportStore.getState().handleBufferPaths(item)
|
usePaperSupportStore.getState().handleText(editablePath)
|
||||||
usePaperSupportStore.getState().handleCircles(item)
|
usePaperSupportStore.getState().handleBufferPaths(editablePath)
|
||||||
|
usePaperSupportStore.getState().handleCircles(editablePath)
|
||||||
|
}
|
||||||
} else if (item.data.type === "polygon") {
|
} else if (item.data.type === "polygon") {
|
||||||
item.data.selected = true
|
item.data.selected = true
|
||||||
|
if (editablePath) {
|
||||||
|
usePaperSupportStore.getState().handleBufferPaths(editablePath)
|
||||||
|
usePaperSupportStore.getState().handleCircles(editablePath)
|
||||||
|
}
|
||||||
if (!(item instanceof paper.Path) || !item.data?.isHollowPolygon) {
|
if (!(item instanceof paper.Path) || !item.data?.isHollowPolygon) {
|
||||||
item.fillColor = item.data.fillColor || item.fillColor || null
|
item.fillColor = item.data.fillColor || item.fillColor || null
|
||||||
}
|
}
|
||||||
@@ -6649,19 +6723,16 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
}
|
}
|
||||||
return circle
|
return circle
|
||||||
},
|
},
|
||||||
getBuffPath: (
|
getBuffPath: (item: paper.Segment, index: number, id: number, color: any) => {
|
||||||
item: paper.Segment,
|
|
||||||
index: number,
|
|
||||||
id: number,
|
|
||||||
_color: any
|
|
||||||
) => {
|
|
||||||
if (!item.next) return null
|
if (!item.next) return null
|
||||||
|
const hitStrokeColor = clonePaperColor(color) || new paper.Color("#fff")
|
||||||
|
hitStrokeColor.alpha = 0.001
|
||||||
let bufferPath: paper.Path = new paper.Path(
|
let bufferPath: paper.Path = new paper.Path(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
parent: usePaperStore.getState().group!,
|
parent: usePaperStore.getState().group!,
|
||||||
segments: [item.point, item.next],
|
segments: [item.point, item.next],
|
||||||
strokeColor: new paper.Color(0, 0, 0, 0), // 保留 hit 区域,但本体不可见
|
strokeColor: hitStrokeColor, // 透明度不能为 0,否则部分命中/hover 事件会失效
|
||||||
strokeScaling: false,
|
strokeScaling: false,
|
||||||
strokeWidth: 6, // 适度放宽 hover 区域,但避免干扰对象外默认点击
|
strokeWidth: 6, // 适度放宽 hover 区域,但避免干扰对象外默认点击
|
||||||
strokeCap: "round",
|
strokeCap: "round",
|
||||||
@@ -6850,6 +6921,8 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
}
|
}
|
||||||
item.scale(1 / newScale)
|
item.scale(1 / newScale)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
syncAllPaperObjectStrokeScaling()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getAll: () => {
|
getAll: () => {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import paper from "paper"
|
import paper from "paper"
|
||||||
import { useLabelStore, useObjectStore } from "../store"
|
import { useLabelStore, useObjectStore } from "../store"
|
||||||
import { useOtherToolsStore } from "../useOtherToolsStore"
|
import { useOtherToolsStore } from "../useOtherToolsStore"
|
||||||
import { usePaperStore } from "../usePaperStore"
|
import {
|
||||||
|
ensureEditablePaperPathSelected,
|
||||||
|
usePaperStore,
|
||||||
|
} from "../usePaperStore"
|
||||||
import { usePaperSupportStore } from "../usePaperSupportStore"
|
import { usePaperSupportStore } from "../usePaperSupportStore"
|
||||||
import { useTopToolsStore } from "../useTopToolsStore"
|
import { useTopToolsStore } from "../useTopToolsStore"
|
||||||
|
|
||||||
@@ -93,6 +96,13 @@ const applySelectedItemVisual = (item: paper.Item) => {
|
|||||||
if (!["rectangle", "polygon", "brush", "point"].includes(itemType || ""))
|
if (!["rectangle", "polygon", "brush", "point"].includes(itemType || ""))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
const objectId = Number(item.data?.id)
|
||||||
|
const editablePath = Number.isFinite(objectId)
|
||||||
|
? ensureEditablePaperPathSelected(objectId)
|
||||||
|
: item instanceof paper.Path && !item.data?.isHollowPolygon
|
||||||
|
? item
|
||||||
|
: null
|
||||||
|
|
||||||
if (itemType === "rectangle" && item instanceof paper.Path) {
|
if (itemType === "rectangle" && item instanceof paper.Path) {
|
||||||
item.data.selected = true
|
item.data.selected = true
|
||||||
usePaperSupportStore.getState().handleBufferPaths(item)
|
usePaperSupportStore.getState().handleBufferPaths(item)
|
||||||
@@ -100,19 +110,25 @@ const applySelectedItemVisual = (item: paper.Item) => {
|
|||||||
item.fillColor = item.data.fillColor || item.fillColor || null
|
item.fillColor = item.data.fillColor || item.fillColor || null
|
||||||
} else if (itemType === "brush") {
|
} else if (itemType === "brush") {
|
||||||
item.data.selected = true
|
item.data.selected = true
|
||||||
if (item instanceof paper.Path) {
|
if (editablePath) {
|
||||||
usePaperSupportStore.getState().handleMask(item)
|
usePaperSupportStore.getState().handleMask(editablePath)
|
||||||
usePaperSupportStore.getState().handleBufferPaths(item)
|
usePaperSupportStore.getState().handleBufferPaths(editablePath)
|
||||||
usePaperSupportStore.getState().handleCircles(item)
|
usePaperSupportStore.getState().handleCircles(editablePath)
|
||||||
}
|
}
|
||||||
} else if (itemType === "point" && item instanceof paper.Path) {
|
} else if (itemType === "point") {
|
||||||
item.data.selected = true
|
item.data.selected = true
|
||||||
usePaperSupportStore.getState().handleMask(item)
|
if (editablePath) {
|
||||||
usePaperSupportStore.getState().handleText(item)
|
usePaperSupportStore.getState().handleMask(editablePath)
|
||||||
usePaperSupportStore.getState().handleBufferPaths(item)
|
usePaperSupportStore.getState().handleText(editablePath)
|
||||||
usePaperSupportStore.getState().handleCircles(item)
|
usePaperSupportStore.getState().handleBufferPaths(editablePath)
|
||||||
|
usePaperSupportStore.getState().handleCircles(editablePath)
|
||||||
|
}
|
||||||
} else if (itemType === "polygon") {
|
} else if (itemType === "polygon") {
|
||||||
item.data.selected = true
|
item.data.selected = true
|
||||||
|
if (editablePath) {
|
||||||
|
usePaperSupportStore.getState().handleBufferPaths(editablePath)
|
||||||
|
usePaperSupportStore.getState().handleCircles(editablePath)
|
||||||
|
}
|
||||||
if (!(item instanceof paper.Path) || !item.data?.isHollowPolygon) {
|
if (!(item instanceof paper.Path) || !item.data?.isHollowPolygon) {
|
||||||
item.fillColor = item.data.fillColor || item.fillColor || null
|
item.fillColor = item.data.fillColor || item.fillColor || null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user