feat(label): add label page
This commit is contained in:
605
components/label/store.ts
Normal file
605
components/label/store.ts
Normal file
@@ -0,0 +1,605 @@
|
||||
import { del, get, set } from "idb-keyval"
|
||||
import superjson from "superjson"
|
||||
import { create } from "zustand"
|
||||
import { persist, PersistStorage, StorageValue } from "zustand/middleware"
|
||||
import { Comment } from "./api/label/typing"
|
||||
import { safeClone } from "./utils/clone"
|
||||
|
||||
type LabelData = Map<string, Map<string, [number, any[], any, any[]][]>>
|
||||
type LabelPathTuple = [number, any[], any, any[]]
|
||||
type LabelTime = {
|
||||
label: number
|
||||
review1: number
|
||||
review2: number
|
||||
}
|
||||
|
||||
const isPaperPointLike = (value: any) => {
|
||||
return (
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
!Array.isArray(value) &&
|
||||
typeof value.x === "number" &&
|
||||
typeof value.y === "number"
|
||||
)
|
||||
}
|
||||
|
||||
const isPaperSegmentLike = (value: any) => {
|
||||
return (
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
!Array.isArray(value) &&
|
||||
value.point &&
|
||||
isPaperPointLike(value.point)
|
||||
)
|
||||
}
|
||||
|
||||
const normalizePointCollection = (value: any): any => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => normalizePointCollection(item))
|
||||
}
|
||||
if (isPaperSegmentLike(value)) {
|
||||
return [value.point.x, value.point.y]
|
||||
}
|
||||
if (isPaperPointLike(value)) {
|
||||
return [value.x, value.y]
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
const normalizeLabelPathTuple = (path: LabelPathTuple): LabelPathTuple => {
|
||||
return [
|
||||
path[0],
|
||||
normalizePointCollection(path[1]),
|
||||
path[2],
|
||||
normalizePointCollection(path[3]),
|
||||
]
|
||||
}
|
||||
|
||||
interface LabelState {
|
||||
// pathId pathPoints detail hollowPathPoints
|
||||
label: LabelData
|
||||
labelTime: LabelTime
|
||||
labelDefaultComments: Comment[]
|
||||
setLabel: (label: LabelData) => void
|
||||
stateStack: LabelData[]
|
||||
setStateStack: (data: LabelData[]) => void
|
||||
popStateStack: () => LabelData | null
|
||||
pushStateStack: (newData: LabelData) => void
|
||||
getLabelDetailDataByKeys: (
|
||||
imageId: string,
|
||||
categoryId: string,
|
||||
id: number
|
||||
) => any
|
||||
updateLabelDetailDataByKeys: (
|
||||
imageId: string,
|
||||
categoryId: string,
|
||||
id: number,
|
||||
newDetail: any
|
||||
) => void
|
||||
addDetail: (
|
||||
imageId: string,
|
||||
categoryId: string,
|
||||
id: number,
|
||||
obj: {
|
||||
[keys: string]: any
|
||||
}
|
||||
) => void
|
||||
setLabelTime: (
|
||||
labelTime: LabelTime | ((prev: LabelTime) => LabelTime)
|
||||
) => void
|
||||
setLabelDefaultComments: (arr: Comment[]) => void
|
||||
setImage: (
|
||||
imageId: string,
|
||||
operation: Map<string, [number, any[], any, any[]][]>
|
||||
) => void
|
||||
setOperation: (
|
||||
imageId: string,
|
||||
operationId: string,
|
||||
path: [number, any[], any, any[]]
|
||||
) => void
|
||||
deleteOperation: (
|
||||
imageId: string,
|
||||
operationId: string,
|
||||
pathId: number
|
||||
) => void
|
||||
resetLabel: () => void
|
||||
}
|
||||
|
||||
interface ObjectState {
|
||||
pathStatus: {
|
||||
// key = imageId + pathId
|
||||
[key: string]: {
|
||||
["HIDE"]?: boolean
|
||||
}
|
||||
}
|
||||
selectedPath: {
|
||||
// key = imageId
|
||||
[key: string]: number[]
|
||||
}
|
||||
operationStatus: {
|
||||
// key = imageId + operationId
|
||||
[key: string]: {
|
||||
["HIDE"]?: boolean
|
||||
["INPUT"]?: string
|
||||
["COLLAPSE"]?: boolean
|
||||
}
|
||||
}
|
||||
groupStatus: {
|
||||
// key = imageId + groupId
|
||||
[key: string]: {
|
||||
["COLLAPSE"]?: boolean
|
||||
}
|
||||
}
|
||||
imageStatus: {
|
||||
// key = imageId
|
||||
[key: string]: {
|
||||
["INPUT"]?: string
|
||||
["COLLAPSE"]?: boolean
|
||||
["FILTERCOLOR"]?: {
|
||||
[key: string]: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
updatePathStatus: (
|
||||
id: string,
|
||||
statusKey: "HIDE",
|
||||
statusValue: boolean
|
||||
) => void
|
||||
updateSelectedPath: (
|
||||
id: string,
|
||||
type: "ADD" | "DELETE" | "ADDSOME",
|
||||
pathValue?: number
|
||||
) => void
|
||||
resetPathAndOperationStatus: () => void
|
||||
updateOperationStatus: (
|
||||
id: string,
|
||||
statusKey: "HIDE" | "COLLAPSE" | "INPUT",
|
||||
statusValue: string | boolean
|
||||
) => void
|
||||
updateGroupStatus: (
|
||||
id: string,
|
||||
statusKey: "COLLAPSE",
|
||||
statusValue: string | boolean
|
||||
) => void
|
||||
updateImageStatus: (
|
||||
id: string,
|
||||
statusKey: "INPUT" | "COLLAPSE" | "FILTERCOLOR",
|
||||
statusValue:
|
||||
| string
|
||||
| boolean
|
||||
| {
|
||||
[key: string]: boolean
|
||||
}
|
||||
) => void
|
||||
}
|
||||
|
||||
interface KeyEventState {
|
||||
shift: boolean
|
||||
focusInput: boolean
|
||||
setShift: (value: boolean) => void
|
||||
setFocusInput: (value: boolean) => void
|
||||
}
|
||||
|
||||
const initialLabelState = {
|
||||
label: new Map(),
|
||||
labelTime: {
|
||||
label: 0,
|
||||
review1: 0,
|
||||
review2: 0,
|
||||
},
|
||||
}
|
||||
|
||||
export const initialDetail = {
|
||||
comment: [],
|
||||
create_timestamp: 0,
|
||||
first_modified_timestamp: 0,
|
||||
first_modified_uid: 0,
|
||||
last_modified_timestamp: 0,
|
||||
last_modified_uid: 0,
|
||||
sub_attributes: {},
|
||||
prelabel: false,
|
||||
}
|
||||
|
||||
// 自定义 storage 对象
|
||||
export const storage: PersistStorage<any> = {
|
||||
getItem: async (name: string) => {
|
||||
// console.log(name, "has been retrieved");
|
||||
let str = await get(name)
|
||||
if (!str) return null
|
||||
return superjson.parse(str) || null
|
||||
},
|
||||
setItem: async (name: string, value: StorageValue<any>): Promise<void> => {
|
||||
// console.log(name, "with value", value, "has been saved");
|
||||
await set(name, superjson.stringify(value))
|
||||
},
|
||||
removeItem: async (name: string): Promise<void> => {
|
||||
console.log(name, "has been deleted")
|
||||
await del(name)
|
||||
},
|
||||
}
|
||||
|
||||
// store path data
|
||||
export const useLabelStore = create(
|
||||
persist<LabelState>(
|
||||
(storeSet) => ({
|
||||
label: initialLabelState.label,
|
||||
labelTime: initialLabelState.labelTime,
|
||||
labelDefaultComments: [],
|
||||
stateStack: [],
|
||||
setLabel: (label) =>
|
||||
storeSet((state) => {
|
||||
return {
|
||||
...state,
|
||||
label,
|
||||
}
|
||||
}),
|
||||
setStateStack: (data) =>
|
||||
storeSet((state) => {
|
||||
return {
|
||||
...state,
|
||||
stateStack: data,
|
||||
}
|
||||
}),
|
||||
popStateStack: () => {
|
||||
const arr: LabelData[] = useLabelStore.getState().stateStack
|
||||
console.log("弹出栈", arr)
|
||||
if (!arr.length) return null
|
||||
if (arr.length === 1) {
|
||||
return arr[0]
|
||||
} else {
|
||||
arr.pop()
|
||||
storeSet((state) => {
|
||||
return {
|
||||
...state,
|
||||
stateStack: arr,
|
||||
}
|
||||
})
|
||||
return arr[arr.length - 1]
|
||||
}
|
||||
},
|
||||
pushStateStack: (newData) => {
|
||||
const arr: LabelData[] = useLabelStore.getState().stateStack
|
||||
if (arr.length === 10) {
|
||||
arr.shift()
|
||||
}
|
||||
arr.push(safeClone(newData))
|
||||
console.log(arr)
|
||||
return storeSet((state) => {
|
||||
return {
|
||||
...state,
|
||||
stateStack: arr,
|
||||
}
|
||||
})
|
||||
},
|
||||
getLabelDetailDataByKeys: (
|
||||
imageId: string,
|
||||
categoryId: string,
|
||||
id: number
|
||||
) => {
|
||||
const data = useLabelStore.getState().label
|
||||
if (!data.has(imageId)) return null
|
||||
const image = data.get(imageId)
|
||||
if (!image?.has(categoryId)) return null
|
||||
const category = image.get(categoryId)
|
||||
let detail = null
|
||||
category?.map((item) => {
|
||||
if (item[0] === id) detail = item[2]
|
||||
})
|
||||
return detail
|
||||
},
|
||||
updateLabelDetailDataByKeys: (imageId, categoryId, id, newData) => {
|
||||
const labelData = useLabelStore.getState().label
|
||||
if (!labelData.has(imageId)) return
|
||||
const image = labelData.get(imageId)
|
||||
if (!image?.has(categoryId)) return
|
||||
const category = image.get(categoryId)
|
||||
category?.map((item) => {
|
||||
if (item[0] === id) item[2] = newData
|
||||
})
|
||||
},
|
||||
addDetail: (imageId, categoryId, id, obj) => {
|
||||
const labelData = useLabelStore.getState().label
|
||||
if (!labelData.has(imageId)) return
|
||||
const image = labelData.get(imageId)
|
||||
if (!image?.has(categoryId)) return
|
||||
const category = image.get(categoryId)
|
||||
category?.map((item) => {
|
||||
if (item[0] === id) item[2] = Object.assign(item[2], obj)
|
||||
})
|
||||
},
|
||||
setLabelTime: (labelTime) =>
|
||||
storeSet((state) => {
|
||||
return {
|
||||
...state,
|
||||
labelTime:
|
||||
typeof labelTime === "function"
|
||||
? labelTime(state.labelTime)
|
||||
: labelTime,
|
||||
}
|
||||
}),
|
||||
setLabelDefaultComments: (labelDefaultComments) =>
|
||||
storeSet((state) => {
|
||||
return {
|
||||
...state,
|
||||
labelDefaultComments,
|
||||
}
|
||||
}),
|
||||
setImage: (imageId, operation) =>
|
||||
storeSet((state) => {
|
||||
state.label.set(imageId, operation)
|
||||
return state
|
||||
}),
|
||||
setOperation: (imageId, operationId, path) =>
|
||||
storeSet((state) => {
|
||||
const normalizedPath = normalizeLabelPathTuple(path)
|
||||
const labelData = useLabelStore.getState().label
|
||||
if (!labelData.has(imageId)) {
|
||||
labelData.set(imageId, new Map())
|
||||
}
|
||||
if (labelData.get(imageId)?.get(operationId)?.length) {
|
||||
let result = [
|
||||
...labelData.get(imageId)?.get(operationId)!,
|
||||
normalizedPath,
|
||||
]
|
||||
.reduce((acc, curr) => {
|
||||
const key = curr[0]
|
||||
acc.set(key, curr)
|
||||
return acc
|
||||
}, new Map())
|
||||
.values()
|
||||
labelData.get(imageId)?.set(operationId, Array.from(result))
|
||||
} else {
|
||||
labelData.get(imageId)?.set(operationId, [normalizedPath])
|
||||
}
|
||||
console.log("绘制时", labelData)
|
||||
useLabelStore.getState().pushStateStack(safeClone(labelData))
|
||||
return { ...state, label: safeClone(labelData) }
|
||||
}),
|
||||
|
||||
// 删除path
|
||||
deleteOperation: (imageId, operationId, pathId) =>
|
||||
storeSet((state) => {
|
||||
if (state.label.get(imageId)?.get(operationId)?.length) {
|
||||
let result =
|
||||
state.label
|
||||
.get(imageId)
|
||||
?.get(operationId)
|
||||
?.filter((item) => item[0] !== pathId) || []
|
||||
state.label.get(imageId)?.set(operationId, result)
|
||||
}
|
||||
return state
|
||||
}),
|
||||
resetLabel: () =>
|
||||
storeSet((state) => {
|
||||
return {
|
||||
...state,
|
||||
label: initialLabelState.label,
|
||||
labelTime: initialLabelState.labelTime,
|
||||
}
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: "label-data-storage",
|
||||
storage: storage,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
const initialObjectState = {
|
||||
pathStatus: {},
|
||||
selectedPath: {},
|
||||
operationStatus: {},
|
||||
groupStatus: {},
|
||||
imageStatus: {},
|
||||
}
|
||||
|
||||
// store object list status
|
||||
export const useObjectStore = create(
|
||||
persist<ObjectState>(
|
||||
(storeSet) => ({
|
||||
pathStatus: initialObjectState.pathStatus,
|
||||
selectedPath: initialObjectState.selectedPath,
|
||||
operationStatus: initialObjectState.operationStatus,
|
||||
groupStatus: initialObjectState.groupStatus,
|
||||
imageStatus: initialObjectState.imageStatus,
|
||||
updatePathStatus: (id: string, statusKey: "HIDE", statusValue: boolean) =>
|
||||
storeSet((state) => {
|
||||
let idStatus = state.pathStatus[id] || {}
|
||||
idStatus = Object.assign(idStatus, {
|
||||
[statusKey]: statusValue,
|
||||
})
|
||||
return {
|
||||
...state,
|
||||
pathStatus: {
|
||||
...state.pathStatus,
|
||||
[id]: idStatus,
|
||||
},
|
||||
}
|
||||
}),
|
||||
updateSelectedPath: (
|
||||
id: string,
|
||||
type: "ADD" | "DELETE" | "ADDSOME",
|
||||
pathValue?: number
|
||||
) =>
|
||||
storeSet((state) => {
|
||||
let idSelectedPath = state.selectedPath[id] || []
|
||||
if (type === "ADD") {
|
||||
if (useKeyEventStore.getState().shift) {
|
||||
idSelectedPath = pathValue
|
||||
? [...new Set([...idSelectedPath, pathValue])]
|
||||
: idSelectedPath
|
||||
} else {
|
||||
idSelectedPath = pathValue ? [pathValue] : []
|
||||
}
|
||||
} else if (type === "ADDSOME") {
|
||||
idSelectedPath = pathValue
|
||||
? [...new Set([...idSelectedPath, pathValue])]
|
||||
: idSelectedPath
|
||||
} else if (type === "DELETE") {
|
||||
idSelectedPath =
|
||||
idSelectedPath?.filter((id) => id !== pathValue) || []
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
selectedPath: {
|
||||
...state.selectedPath,
|
||||
[id]: idSelectedPath,
|
||||
},
|
||||
}
|
||||
}),
|
||||
resetPathAndOperationStatus: () =>
|
||||
storeSet((state) => {
|
||||
return {
|
||||
...state,
|
||||
selectedPath: {},
|
||||
pathStatus: {},
|
||||
operationStatus: {},
|
||||
}
|
||||
}),
|
||||
updateOperationStatus: (
|
||||
id: string,
|
||||
statusKey: "HIDE" | "COLLAPSE" | "INPUT",
|
||||
statusValue: string | boolean
|
||||
) =>
|
||||
storeSet((state) => {
|
||||
let idStatus = state.operationStatus[id] || {}
|
||||
idStatus = Object.assign(idStatus, {
|
||||
[statusKey]: statusValue,
|
||||
})
|
||||
return {
|
||||
...state,
|
||||
operationStatus: {
|
||||
...state.operationStatus,
|
||||
[id]: idStatus,
|
||||
},
|
||||
}
|
||||
}),
|
||||
updateGroupStatus: (
|
||||
id: string,
|
||||
statusKey: "COLLAPSE",
|
||||
statusValue: string | boolean
|
||||
) =>
|
||||
storeSet((state) => {
|
||||
let idStatus = state.groupStatus[id] || {}
|
||||
idStatus = Object.assign(idStatus, {
|
||||
[statusKey]: statusValue,
|
||||
})
|
||||
return {
|
||||
...state,
|
||||
groupStatus: {
|
||||
...state.groupStatus,
|
||||
[id]: idStatus,
|
||||
},
|
||||
}
|
||||
}),
|
||||
updateImageStatus: (
|
||||
id: string,
|
||||
statusKey: "INPUT" | "COLLAPSE" | "FILTERCOLOR",
|
||||
statusValue:
|
||||
| string
|
||||
| boolean
|
||||
| {
|
||||
[key: string]: boolean
|
||||
}
|
||||
) =>
|
||||
storeSet((state) => {
|
||||
let idStatus = state.imageStatus[id] || {}
|
||||
idStatus = Object.assign(idStatus, {
|
||||
[statusKey]: statusValue,
|
||||
})
|
||||
return {
|
||||
...state,
|
||||
imageStatus: {
|
||||
...state.imageStatus,
|
||||
[id]: idStatus,
|
||||
},
|
||||
}
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: "object-data-storage",
|
||||
storage: storage,
|
||||
partialize: (state: ObjectState) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(state).filter(
|
||||
([key]) => !["selectedPath"].includes(key)
|
||||
)
|
||||
) as any,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
const initialKeyEventState = {
|
||||
shift: false,
|
||||
focusInput: false,
|
||||
}
|
||||
|
||||
// store keyboard and mouse status
|
||||
export const useKeyEventStore = create<KeyEventState>((set) => ({
|
||||
shift: initialKeyEventState.shift,
|
||||
setShift: (value: boolean) =>
|
||||
set((state: KeyEventState) => ({ ...state, shift: value })),
|
||||
focusInput: initialKeyEventState.focusInput,
|
||||
setFocusInput: (value: boolean) =>
|
||||
set((state: KeyEventState) => ({ ...state, focusInput: value })),
|
||||
}))
|
||||
|
||||
interface ImageState {
|
||||
images: Map<string, string>
|
||||
setImages: (value: any) => void
|
||||
}
|
||||
|
||||
interface VideoFrameState {
|
||||
videoFrames: Map<string, string[]>
|
||||
setVideoFrames: (key: string, value: string[]) => void
|
||||
removeVideoFrames: (key: string) => void
|
||||
}
|
||||
|
||||
export const useImagesStore = create(
|
||||
persist<ImageState>(
|
||||
(storeSet) => ({
|
||||
images: new Map(),
|
||||
setImages: (value: any) =>
|
||||
storeSet((state) => {
|
||||
return {
|
||||
...state,
|
||||
images: value,
|
||||
}
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: "image-data-storage",
|
||||
storage: storage,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
export const useVideoFrameStore = create(
|
||||
persist<VideoFrameState>(
|
||||
(storeSet) => ({
|
||||
videoFrames: new Map(),
|
||||
setVideoFrames: (key: string, value: string[]) =>
|
||||
storeSet((state) => {
|
||||
const nextMap = new Map(state.videoFrames)
|
||||
nextMap.set(key, value)
|
||||
return {
|
||||
...state,
|
||||
videoFrames: nextMap,
|
||||
}
|
||||
}),
|
||||
removeVideoFrames: (key: string) =>
|
||||
storeSet((state) => {
|
||||
const nextMap = new Map(state.videoFrames)
|
||||
nextMap.delete(key)
|
||||
return {
|
||||
...state,
|
||||
videoFrames: nextMap,
|
||||
}
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: "video-frame-storage",
|
||||
storage: storage,
|
||||
}
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user