From 99a4542d463b34bc2d4d65fa07c2e36d37c13e08 Mon Sep 17 00:00:00 2001 From: zhangheng Date: Fri, 17 Apr 2026 11:43:19 +0800 Subject: [PATCH] fix(label): ,/ --- components/label/LabelNossr.tsx | 222 +------- components/label/usePaperStore.ts | 901 ++++++++++++++++++++---------- 2 files changed, 611 insertions(+), 512 deletions(-) diff --git a/components/label/LabelNossr.tsx b/components/label/LabelNossr.tsx index 42eb107..a00c241 100644 --- a/components/label/LabelNossr.tsx +++ b/components/label/LabelNossr.tsx @@ -1692,29 +1692,6 @@ const LabelPage = ({ [] ) - const removeObjectFromGroupMap = useCallback( - (imageId: string, id: number) => { - const nextPathGroupMap = safeClone( - useRightToolsStore.getState().pathGroupMap - ) - const imageGroupMap = nextPathGroupMap.get(imageId) - if (!imageGroupMap) return - - for (let [groupId, pathIds] of imageGroupMap.entries()) { - const nextPathIds = pathIds.filter((pathId) => pathId !== id) - if (nextPathIds.length > 1) { - imageGroupMap.set(groupId, nextPathIds) - } else { - imageGroupMap.delete(groupId) - } - } - - if (!imageGroupMap.size) nextPathGroupMap.delete(imageId) - useRightToolsStore.getState().setPathGroupMap(nextPathGroupMap) - }, - [] - ) - const handleContinueSelectedPolygon = useCallback(() => { const selectedContext = resolveSelectedObjectContext() if (!selectedContext) { @@ -1758,70 +1735,6 @@ const LabelPage = ({ setPressA, ]) - const hasShapeSegments = useCallback((shape: any) => { - if (!shape) return false - if (shape instanceof paper.CompoundPath) { - return (shape.children as paper.Path[]).some( - (child) => child.segments?.length - ) - } - if (shape instanceof paper.Path) { - return !!shape.segments?.length - } - return false - }, []) - - const createWorkingPolygonShape = useCallback((items: any[]) => { - const compound = items.find((item) => item instanceof paper.CompoundPath) - if (compound) { - return (compound as paper.CompoundPath).clone({ insert: false }) as - | paper.Path - | paper.CompoundPath - } - - const paths = items.filter( - (item) => item instanceof paper.Path - ) as paper.Path[] - if (!paths.length) return null - if (paths.length === 1) { - return paths[0].clone({ insert: false }) as - | paper.Path - | paper.CompoundPath - } - - const merged = new paper.CompoundPath({ insert: false }) - paths.forEach((path) => { - merged.addChild(path.clone({ insert: false })) - }) - return merged as paper.Path | paper.CompoundPath - }, []) - - const collectPolygonPoints = useCallback( - (shape: paper.Path | paper.CompoundPath) => { - const polygonPoints: any[] = [] - const polygonHollowPoints: any[] = [] - - const collectPath = (path: paper.Path) => { - if (!path.segments?.length) return - const pathData = JSON.parse(path.exportJSON({ precision: 20 })) - if (path.clockwise) { - polygonPoints.push(pathData[1]?.segments) - } else { - polygonHollowPoints.push(pathData[1]?.segments) - } - } - - if (shape instanceof paper.CompoundPath) { - ;(shape.children as paper.Path[]).forEach((path) => collectPath(path)) - } else { - collectPath(shape) - } - - return { polygonPoints, polygonHollowPoints } - }, - [] - ) - const applySelectedPolygonOverlap = useCallback( (mode: "subtract" | "unite") => { const selectedContext = resolveSelectedObjectContext() @@ -1842,138 +1755,9 @@ const LabelPage = ({ }) return } - - const group = usePaperStore.getState().group - if (!group) return - - const targetItems = group - .getItems({ data: { id: selectedContext.objectId } }) - .filter((item) => item.data?.type === "polygon") - if (!targetItems.length) return - - let workingShape = createWorkingPolygonShape(targetItems) - if (!workingShape || !hasShapeSegments(workingShape)) return - - const otherPolygonPaths: paper.Path[] = [] - group.children.forEach((item: any) => { - if ( - !item.visible || - !item.data?.id || - item.data.id === selectedContext.objectId || - item.data.type !== "polygon" - ) - return - if (item instanceof paper.Path) { - if (item.segments?.length) otherPolygonPaths.push(item) - } else if (item instanceof paper.CompoundPath) { - ;(item.children as paper.Path[]).forEach((child) => { - if (child.segments?.length) otherPolygonPaths.push(child) - }) - } - }) - - let changed = false - - for (const eachPath of otherPolygonPaths) { - if (!workingShape || !hasShapeSegments(workingShape)) break - - const eachPathClone = eachPath.clone({ insert: false }) as paper.Path - const intersectionPath = (workingShape as any).intersect( - eachPathClone, - { - insert: false, - } - ) as paper.Path | paper.CompoundPath - const hasIntersection = hasShapeSegments(intersectionPath) - intersectionPath?.remove() - if (!hasIntersection) { - eachPathClone.remove() - continue - } - - const nextShape: paper.Path | paper.CompoundPath = - mode === "subtract" - ? ((workingShape as any).subtract(eachPathClone, { - insert: false, - }) as paper.Path | paper.CompoundPath) - : ((workingShape as any).unite(eachPathClone, { - insert: false, - }) as paper.Path | paper.CompoundPath) - eachPathClone.remove() - workingShape?.remove() - workingShape = nextShape - changed = true - } - - if (!changed) { - workingShape?.remove() - return - } - if (!workingShape) return - - const raster = usePaperStore.getState().rasterPath - if (raster && hasShapeSegments(workingShape)) { - const clippedShape = (workingShape as any).intersect(raster, { - insert: false, - trace: false, - }) as paper.Path | paper.CompoundPath - workingShape?.remove() - workingShape = clippedShape - } - if (!workingShape) return - - const { polygonPoints, polygonHollowPoints } = - collectPolygonPoints(workingShape) - workingShape?.remove() - - const nextLabel = safeClone(useLabelStore.getState().label) - const imageLabel = nextLabel.get(selectedContext.imageId) - if (!imageLabel) return - const operationPaths = safeClone( - imageLabel.get(selectedContext.operationId) || [] - ) - const selectedIndex = operationPaths.findIndex( - (item) => item[0] === selectedContext.objectId - ) - if (selectedIndex === -1) return - - const currentTuple = operationPaths[selectedIndex] - if (polygonPoints.length) { - operationPaths[selectedIndex] = [ - selectedContext.objectId, - polygonPoints, - safeClone(currentTuple?.[2] || {}), - polygonHollowPoints, - ] - imageLabel.set(selectedContext.operationId, operationPaths) - } else { - imageLabel.set( - selectedContext.operationId, - operationPaths.filter((item) => item[0] !== selectedContext.objectId) - ) - removeObjectFromGroupMap( - selectedContext.imageId, - selectedContext.objectId - ) - } - - useLabelStore.getState().setLabel(nextLabel) - useLabelStore.getState().pushStateStack(nextLabel) - - rerenderActiveImageByLabel( - selectedContext.imageId, - imageLabel, - polygonPoints.length ? [selectedContext.objectId] : [] - ) + usePaperStore.getState().applySelectedPolygonBooleanOperation(mode) }, - [ - collectPolygonPoints, - createWorkingPolygonShape, - hasShapeSegments, - removeObjectFromGroupMap, - rerenderActiveImageByLabel, - resolveSelectedObjectContext, - ] + [resolveSelectedObjectContext] ) const applySelectedBrushIntersectUnite = useCallback(() => { @@ -2638,7 +2422,7 @@ const LabelPage = ({ useEffect(() => { if (!isView) { - const delay = 10 * 60000 + const delay = 5 * 60000 // 编辑模式下,修改labelData时启动或更新定时器 useTimerStore.getState().startTimer(() => { useTopToolsStore.getState().setIsView(true) diff --git a/components/label/usePaperStore.ts b/components/label/usePaperStore.ts index 420cda8..db9b32f 100644 --- a/components/label/usePaperStore.ts +++ b/components/label/usePaperStore.ts @@ -183,6 +183,12 @@ interface PaperState { getItemsById: (id: number) => Array setContinuePolygonTarget: (target: ContinuePolygonTarget | null) => void beginContinuePolygonDraft: () => boolean + applySelectedPolygonBooleanOperation: ( + mode: "subtract" | "unite", + options?: { + draftPath?: paper.Path | null + } + ) => boolean setLoadingData: (flag: boolean) => void refreshNodeVisualSize: () => void resizeGhostRequestToken: number @@ -1049,6 +1055,7 @@ export const usePaperStore = create((set) => ({ currentMousePosition: [0, 0], loadingData: false, beginContinuePolygonDraft: () => false, + applySelectedPolygonBooleanOperation: () => false, init: ( initEl: HTMLCanvasElement, initScope: paper.PaperScope, @@ -3878,188 +3885,89 @@ export const usePaperStore = create((set) => ({ return nextData } - const getSegmentPoint = (segment: any): [number, number] | null => { - if ( - Array.isArray(segment) && - segment.length >= 2 && - typeof segment[0] === "number" && - typeof segment[1] === "number" - ) { - return [segment[0], segment[1]] - } - if ( - Array.isArray(segment) && - Array.isArray(segment[0]) && - segment[0].length >= 2 && - typeof segment[0][0] === "number" && - typeof segment[0][1] === "number" - ) { - return [segment[0][0], segment[0][1]] - } - if ( - segment?.point && - typeof segment.point.x === "number" && - typeof segment.point.y === "number" - ) { - return [segment.point.x, segment.point.y] - } - if ( - segment?.point && - Array.isArray(segment.point) && - segment.point.length >= 2 && - typeof segment.point[0] === "number" && - typeof segment.point[1] === "number" - ) { - return [segment.point[0], segment.point[1]] - } - return null + const restoreSelectedPolygonFillColor = () => { + const activeImage = useBottomToolsStore.getState().activeImage + const selectedIds = + useObjectStore.getState().selectedPath[activeImage] || [] + if (!selectedIds.length) return + + selectedIds.forEach((id) => { + usePaperStore + .getState() + .getItemsById(id) + .forEach((item) => { + if (item.data?.type !== "polygon") return + if (item instanceof paper.Path) { + if (!item.data?.isHollowPolygon) { + item.fillColor = item.data.fillColor || item.fillColor || null + } + return + } + if (item instanceof paper.CompoundPath) { + item.fillColor = item.data.fillColor || item.fillColor || null + } + }) + }) } - const normalizePolygonFromSegments = (segments: any[]) => { - const polygon = (segments || []) - .map((segment) => getSegmentPoint(segment)) - .filter((point): point is [number, number] => !!point) - - if (polygon.length > 1) { - const [firstX, firstY] = polygon[0] - const [lastX, lastY] = polygon[polygon.length - 1] - if ( - Math.abs(firstX - lastX) < 0.000001 && - Math.abs(firstY - lastY) < 0.000001 - ) { - polygon.pop() - } - } - return polygon - } - - const isPointInPolygon = ( - point: [number, number], - polygon: [number, number][] + const hasShapeSegments = ( + shape: paper.Path | paper.CompoundPath | null ) => { - if (polygon.length < 3) return false - const [x, y] = point - let inside = false - for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { - const [xi, yi] = polygon[i] - const [xj, yj] = polygon[j] - const dy = yj - yi - if (yi > y !== yj > y) { - const crossX = ((xj - xi) * (y - yi)) / (dy || 0.000001) + xi - if (x < crossX) inside = !inside - } + if (!shape) return false + if (shape instanceof paper.CompoundPath) { + return (shape.children as paper.Path[]).some( + (child) => child.segments?.length + ) } - return inside + return !!shape.segments?.length } - const getHollowFlagsByContainment = (allSegments: any[][]) => { - const polygons = allSegments.map((segments) => - normalizePolygonFromSegments(segments) + const createWorkingPolygonShape = (items: Array) => { + const compound = items.find((item) => item instanceof paper.CompoundPath) + if (compound instanceof paper.CompoundPath) { + return compound.clone({ insert: false }) as + | paper.Path + | paper.CompoundPath + } + + const paths = items.filter( + (item): item is paper.Path => item instanceof paper.Path ) - return polygons.map((polygon, index) => { - if (polygon.length < 3) return false - const samplePoint = polygon[0] - let containDepth = 0 - polygons.forEach((otherPolygon, otherIndex) => { - if (otherIndex === index || otherPolygon.length < 3) return - if (isPointInPolygon(samplePoint, otherPolygon)) containDepth += 1 - }) - return containDepth % 2 === 1 + if (!paths.length) return null + if (paths.length === 1) { + return paths[0].clone({ insert: false }) as paper.Path + } + + const merged = new paper.CompoundPath({ insert: false }) + paths.forEach((path) => { + merged.addChild(path.clone({ insert: false })) }) + return merged as paper.CompoundPath } - const handleSelectedPolygonSubtract = (draftPath: paper.Path) => { + const getSelectedPolygonContext = () => { const activeImage = useBottomToolsStore.getState().activeImage const selectedIds = useObjectStore.getState().selectedPath[activeImage] || [] - if (selectedIds.length !== 1) return false + if (selectedIds.length !== 1) return null const selectedId = selectedIds[0] const targetShapes = usePaperStore .getState() .getItemsById(selectedId) .filter((item) => item.data?.type === "polygon") - - const targetShape = - targetShapes.find((item) => item instanceof paper.CompoundPath) || - targetShapes.find( - (item) => item instanceof paper.Path && !item.data?.isHollowPolygon - ) || - targetShapes.find((item) => item instanceof paper.Path) - - if (!targetShape) return false - - let clippedDraft: paper.Path | paper.CompoundPath | null = - draftPath.intersect(usePaperStore.getState().rasterPath!, { - insert: false, - trace: false, - }) as paper.Path | paper.CompoundPath - - draftPath.remove() - - const draftHasSegments = - clippedDraft instanceof paper.CompoundPath - ? (clippedDraft.children as paper.Path[]).some( - (child) => child.segments?.length - ) - : clippedDraft.segments?.length - if (!draftHasSegments) { - clippedDraft?.remove() - clippedDraft = null - return false - } - - const targetData = getSafePathData(targetShape.data || {}) - let nextPolygon = ( - targetShape as paper.Path | paper.CompoundPath - ).subtract(clippedDraft) as paper.Path | paper.CompoundPath - clippedDraft?.remove() - clippedDraft = null - targetShape.remove() - - const points: any[] = [] - const hollowPoints: any[] = [] - let lastOuterContourPoints: paper.Point[] = [] - const nextPaths = - nextPolygon instanceof paper.CompoundPath - ? (nextPolygon.children as paper.Path[]) - : ([nextPolygon] as paper.Path[]) - const nextPathSegments = nextPaths.map((path) => { - if (!path.segments?.length) return null - const pathData = JSON.parse(path.exportJSON({ precision: 20 })) - return pathData?.[1]?.segments || null - }) - const hollowFlags = getHollowFlagsByContainment( - nextPathSegments.map((segments) => segments || []) - ) - - if (nextPolygon instanceof paper.CompoundPath) { - nextPolygon.data = Object.assign({}, targetData) - } - nextPaths.forEach((path, index) => { - const segments = nextPathSegments[index] - if (!segments?.length) return - const isHollowPolygon = !!hollowFlags[index] - path.data = Object.assign({}, targetData, { - isHollowPolygon, - }) - if (isHollowPolygon) { - hollowPoints.push(segments) - } else { - points.push(segments) - lastOuterContourPoints = getContinueContourPoints(segments) - } - }) + if (!targetShapes.length) return null const operationId = ( - targetData.operationId || + targetShapes.find((item) => !!item.data)?.data?.operationId || useTopToolsStore.getState().activeOperation || "" ).toString() - if (!operationId) { - nextPolygon.remove() - return false - } + if (!operationId) return null + + const targetData = getSafePathData( + targetShapes.find((item) => !!item.data)?.data || {} + ) const prevDetail = safeClone( useLabelStore .getState() @@ -4067,169 +3975,564 @@ export const usePaperStore = create((set) => ({ initialDetail ) - if (points.length) { - useLabelStore - .getState() - .setOperation(activeImage, operationId, [ - selectedId, - points, - prevDetail, - hollowPoints, - ]) - setContinueDraftHint( - { - imageId: activeImage, - operationId, - objectId: selectedId, - }, - points.length - 1, - lastOuterContourPoints - ) - useObjectStore - .getState() - .updateSelectedPath(activeImage, "ADD", selectedId) - } else { - nextPolygon.remove() - useLabelStore - .getState() - .deleteOperation(activeImage, operationId, selectedId) - useObjectStore.getState().updateSelectedPath(activeImage, "ADD") - const nextLabel = safeClone(useLabelStore.getState().label) - useLabelStore.getState().setLabel(nextLabel) - useLabelStore.getState().pushStateStack(nextLabel) + return { + activeImage, + selectedId, + targetData, + targetShapes, + operationId, + prevDetail, } - - forceUpdate() - return true } - const handleSelectedPolygonAdd = (draftPath: paper.Path) => { - const activeImage = useBottomToolsStore.getState().activeImage - const selectedIds = - useObjectStore.getState().selectedPath[activeImage] || [] - if (selectedIds.length !== 1) return false + const removeSelectedPolygonRootItems = ( + context: NonNullable> + ) => { + const group = usePaperStore.getState().group + context.targetShapes + .filter((item) => item.parent === group) + .forEach((item) => { + item.remove() + }) + } - const selectedId = selectedIds[0] - const targetShapes = usePaperStore - .getState() - .getItemsById(selectedId) - .filter((item) => item.data?.type === "polygon") + const prepareWorkingPolygonShape = ( + shape: paper.Path | paper.CompoundPath, + context: NonNullable> + ) => { + const visible = context.targetData.visible !== false + const fillColor = + context.targetData.fillColor || + usePaperStore.getState().toolOption.fillColor + const blankColor = + context.targetData.blankColor || + fillColor || + usePaperStore.getState().toolOption.blankColor + const strokeColor = + context.targetData.strokeColor || + usePaperStore.getState().toolOption.strokeColor + const strokeWidth = + context.targetData.strokeWidth || + usePaperStore.getState().toolOption.strokeWidth + const baseData = Object.assign({}, context.targetData, { + type: "polygon", + id: context.selectedId, + imageId: context.activeImage, + operationId: context.operationId, + fillColor, + blankColor, + selected: true, + }) - const targetShape = - targetShapes.find((item) => item instanceof paper.CompoundPath) || - targetShapes.find( - (item) => item instanceof paper.Path && !item.data?.isHollowPolygon - ) || - targetShapes.find((item) => item instanceof paper.Path) - - if (!targetShape) return false - - let clippedDraft: paper.Path | paper.CompoundPath | null = - draftPath.intersect(usePaperStore.getState().rasterPath!, { - insert: false, - trace: false, - }) as paper.Path | paper.CompoundPath - - draftPath.remove() - - const draftHasSegments = - clippedDraft instanceof paper.CompoundPath - ? (clippedDraft.children as paper.Path[]).some( - (child) => child.segments?.length - ) - : clippedDraft.segments?.length - if (!draftHasSegments) { - clippedDraft?.remove() - clippedDraft = null - return false + let selectedAssigned = false + const applyPathStyle = (path: paper.Path, isHollowPolygon: boolean) => { + path.data = Object.assign( + {}, + baseData, + getSafePathData(path.data || {}), + { + isHollowPolygon, + selected: !selectedAssigned && !isHollowPolygon, + } + ) + path.strokeColor = strokeColor + path.strokeWidth = strokeWidth + path.visible = visible + if (!isHollowPolygon) { + path.fillColor = fillColor + } + path.closed = true + if (!selectedAssigned && !isHollowPolygon) { + selectedAssigned = true + } } - const targetData = getSafePathData(targetShape.data || {}) - let nextPolygon = (targetShape as paper.Path | paper.CompoundPath).unite( - clippedDraft - ) as paper.Path | paper.CompoundPath - clippedDraft?.remove() - clippedDraft = null - targetShape.remove() + if (shape instanceof paper.CompoundPath) { + const hollowChildren = new Set() + const contourEntries = (shape.children as paper.Path[]) + .filter((child) => child.segments?.length) + .map((child, index) => ({ + child, + index, + samplePoint: getPathInteriorPoint(child), + isHollow: false, + })) + + contourEntries.forEach((entry, entryIndex) => { + const nestingDepth = contourEntries.reduce( + (count, other, otherIndex) => { + if (entryIndex === otherIndex) return count + return other.child.contains(entry.samplePoint) ? count + 1 : count + }, + 0 + ) + entry.isHollow = nestingDepth % 2 === 1 + }) + + if ( + contourEntries.length && + !contourEntries.some((entry) => !entry.isHollow) + ) { + const largestContour = contourEntries.reduce((result, entry) => { + if (!result) return entry + return Math.abs(entry.child.area) > Math.abs(result.child.area) + ? entry + : result + }, contourEntries[0]) + if (largestContour) { + largestContour.isHollow = false + } + } + + contourEntries.forEach((entry) => { + if (entry.isHollow) { + hollowChildren.add(entry.child) + } + }) + + shape.data = Object.assign({}, baseData) + shape.strokeColor = strokeColor + shape.strokeWidth = strokeWidth + shape.fillColor = fillColor + shape.visible = visible + ;(shape.children as paper.Path[]).forEach((child) => { + applyPathStyle(child, hollowChildren.has(child)) + }) + if (!selectedAssigned && shape.children[0] instanceof paper.Path) { + ;(shape.children[0] as paper.Path).data = Object.assign( + {}, + shape.children[0].data || {}, + { selected: true } + ) + } + } else { + applyPathStyle(shape, !!shape.data?.isHollowPolygon) + } + + const group = usePaperStore.getState().group + if (group && shape.parent !== group) { + group.addChild(shape) + } + insertItemBelowTags(shape) + return shape + } + + const getPathInteriorPoint = (path: paper.Path) => { + const interiorPoint = (path as any).interiorPoint + if ( + interiorPoint instanceof paper.Point && + path.contains(interiorPoint) + ) { + return new paper.Point(interiorPoint) + } + + if (path.contains(path.position)) { + return new paper.Point(path.position) + } + + const boundsCenter = path.bounds.center + if (path.contains(boundsCenter)) { + return new paper.Point(boundsCenter) + } + + if (path.segments?.length) { + const centroid = path.segments.reduce( + (result, segment) => result.add(segment.point), + new paper.Point(0, 0) + ) + const averagePoint = centroid.divide(path.segments.length) + if (path.contains(averagePoint)) { + return averagePoint + } + + const firstPoint = path.segments[0].point + const centerPoint = firstPoint.add(boundsCenter).divide(2) + if (path.contains(centerPoint)) { + return centerPoint + } + + return new paper.Point(firstPoint) + } + + return new paper.Point(path.position) + } + + const serializeCompoundPolygonContours = ( + currentPolygon: paper.CompoundPath + ) => { + const hollowChildren = new Set() + const contourEntries = (currentPolygon.children as paper.Path[]) + .filter((path) => path.segments?.length) + .map((path, index) => { + const pathData = JSON.parse(path.exportJSON({ precision: 20 })) + return { + index, + path, + samplePoint: getPathInteriorPoint(path), + segments: pathData[1]?.segments || [], + isHollow: false, + } + }) + + contourEntries.forEach((entry, entryIndex) => { + const nestingDepth = contourEntries.reduce( + (count, other, otherIndex) => { + if (entryIndex === otherIndex) return count + return other.path.contains(entry.samplePoint) ? count + 1 : count + }, + 0 + ) + entry.isHollow = nestingDepth % 2 === 1 + }) + + if ( + contourEntries.length && + !contourEntries.some((entry) => !entry.isHollow) + ) { + const largestContour = contourEntries.reduce((result, entry) => { + if (!result) return entry + return Math.abs(entry.path.area) > Math.abs(result.path.area) + ? entry + : result + }, contourEntries[0]) + if (largestContour) { + largestContour.isHollow = false + } + } + + contourEntries.forEach((entry) => { + if (entry.isHollow) { + hollowChildren.add(entry.path) + } + }) const points: any[] = [] const hollowPoints: any[] = [] let lastOuterContourPoints: paper.Point[] = [] - const nextPaths = - nextPolygon instanceof paper.CompoundPath - ? (nextPolygon.children as paper.Path[]) - : ([nextPolygon] as paper.Path[]) - const nextPathSegments = nextPaths.map((path) => { - if (!path.segments?.length) return null - const pathData = JSON.parse(path.exportJSON({ precision: 20 })) - return pathData?.[1]?.segments || null - }) - const hollowFlags = getHollowFlagsByContainment( - nextPathSegments.map((segments) => segments || []) - ) - if (nextPolygon instanceof paper.CompoundPath) { - nextPolygon.data = Object.assign({}, targetData) - } - nextPaths.forEach((path, index) => { - const segments = nextPathSegments[index] - if (!segments?.length) return - const isHollowPolygon = !!hollowFlags[index] - path.data = Object.assign({}, targetData, { - isHollowPolygon, - }) - if (isHollowPolygon) { - hollowPoints.push(segments) - } else { - points.push(segments) - lastOuterContourPoints = getContinueContourPoints(segments) + contourEntries.forEach((entry) => { + if (!entry.segments.length) return + if (hollowChildren.has(entry.path)) { + hollowPoints.push(entry.segments) + return } + points.push(entry.segments) + lastOuterContourPoints = getContinueContourPoints(entry.segments) }) - const operationId = ( - targetData.operationId || - useTopToolsStore.getState().activeOperation || - "" - ).toString() - if (!operationId) { - nextPolygon.remove() - return false + return { + points, + hollowPoints, + lastOuterContourPoints, } - const prevDetail = safeClone( - useLabelStore - .getState() - .getLabelDetailDataByKeys(activeImage, operationId, selectedId) || - initialDetail - ) + } - if (points.length) { + const rebuildPersistedSelectedPolygon = ( + context: NonNullable> + ) => { + const group = usePaperStore.getState().group + if (!group) return null + + const persistedTuple = useLabelStore .getState() - .setOperation(activeImage, operationId, [ - selectedId, - points, - prevDetail, - hollowPoints, + .label.get(context.activeImage) + ?.get(context.operationId) + ?.find((item) => item[0] === context.selectedId) || null + if (!persistedTuple) return null + + const visible = + !useObjectStore.getState().pathStatus[ + context.activeImage + context.selectedId + ]?.HIDE + const selected = ( + useObjectStore.getState().selectedPath[context.activeImage] || [] + ).includes(context.selectedId) + const fillColor = + context.targetData.fillColor || + usePaperStore.getState().toolOption.fillColor + const blankColor = + context.targetData.blankColor || + fillColor || + usePaperStore.getState().toolOption.blankColor + const strokeColor = + context.targetData.strokeColor || + usePaperStore.getState().toolOption.strokeColor + const strokeWidth = + context.targetData.strokeWidth || + usePaperStore.getState().toolOption.strokeWidth + const baseData = Object.assign({}, context.targetData, { + type: "polygon", + id: context.selectedId, + imageId: context.activeImage, + operationId: context.operationId, + fillColor, + blankColor, + strokeColor, + strokeWidth, + selected, + }) + + group + .getItems({ data: { id: context.selectedId } }) + .filter( + (item) => item.data?.type === "polygon" && item.parent === group + ) + .forEach((item) => { + item.remove() + }) + + const buildPolygonPath = (segments: any[], isHollowPolygon: boolean) => { + const contourPoints = (segments || []) + .map((segment) => getPointFromSegment(segment)) + .filter((point): point is paper.Point => point !== null) + if (!contourPoints.length) return null + + return usePaperStore.getState().addPolygon( + renderPath, + contourPoints, + { + strokeColor, + strokeWidth, + fillColor: isHollowPolygon ? fillColor : blankColor, + visible, + }, + Object.assign({}, baseData, { + isHollowPolygon, + }) + ) + } + + const outerPaths = + persistedTuple[1] + ?.map((segments) => buildPolygonPath(segments, false)) + .filter((item): item is paper.Path => !!item) || [] + const innerPaths = + persistedTuple[3] + ?.map((segments) => buildPolygonPath(segments, true)) + .filter((item): item is paper.Path => !!item) || [] + const children = [...outerPaths, ...innerPaths] + if (!children.length) return null + + return usePaperStore.getState().addCompoundPath( + renderCompoundPath, + children, + { + strokeColor, + strokeWidth, + fillColor: blankColor, + visible, + }, + baseData + ) + } + + const clearPolygonBooleanHelperItems = () => { + if (selectedCircles.length) { + selectedCircles.forEach((item) => { + item.remove() + }) + selectedCircles = [] + } + clearPolygonMagnet() + } + + const getEditedPolygonDetail = ( + context: NonNullable> + ) => { + const currentDetail = useLabelStore + .getState() + .getLabelDetailDataByKeys( + context.activeImage, + context.operationId, + context.selectedId + ) + return currentDetail + ? { + ...currentDetail, + first_modified_timestamp: + currentDetail.first_modified_timestamp || dayjs().unix(), + first_modified_uid: currentDetail.first_modified_uid || user_id, + last_modified_timestamp: dayjs().unix(), + last_modified_uid: user_id, + } + : { + ...initialDetail, + create_timestamp: dayjs().unix(), + category_id: Number(context.operationId), + } + } + + const persistSelectedPolygonResult = ( + context: NonNullable> + ) => { + const currentPolygon = polygon + const newDetail = getEditedPolygonDetail(context) + + if (currentPolygon instanceof paper.CompoundPath) { + const { points, hollowPoints, lastOuterContourPoints } = + serializeCompoundPolygonContours(currentPolygon) + + if (points.length) { + useLabelStore + .getState() + .setOperation(context.activeImage, context.operationId, [ + context.selectedId, + points, + newDetail, + hollowPoints, + ]) + setContinueDraftHint( + { + imageId: context.activeImage, + operationId: context.operationId, + objectId: context.selectedId, + }, + points.length - 1, + lastOuterContourPoints + ) + useObjectStore + .getState() + .updateSelectedPath(context.activeImage, "ADD", context.selectedId) + } else { + useLabelStore + .getState() + .deleteOperation( + context.activeImage, + context.operationId, + context.selectedId + ) + cleanupDeletedObjectItems( + context.activeImage, + context.selectedId, + context.targetData.parentGroupId + ) + } + } else if ( + currentPolygon instanceof paper.Path && + currentPolygon.segments?.length + ) { + const pathData = JSON.parse( + currentPolygon.exportJSON({ precision: 20 }) + ) + const currentContourSegments = pathData[1]?.segments || [] + useLabelStore + .getState() + .setOperation(context.activeImage, context.operationId, [ + context.selectedId, + [currentContourSegments], + newDetail, + [], ]) setContinueDraftHint( { - imageId: activeImage, - operationId, - objectId: selectedId, + imageId: context.activeImage, + operationId: context.operationId, + objectId: context.selectedId, }, - points.length - 1, - lastOuterContourPoints + 0, + getContinueContourPoints(currentContourSegments) ) useObjectStore .getState() - .updateSelectedPath(activeImage, "ADD", selectedId) + .updateSelectedPath(context.activeImage, "ADD", context.selectedId) } else { - nextPolygon.remove() + useLabelStore + .getState() + .deleteOperation( + context.activeImage, + context.operationId, + context.selectedId + ) + cleanupDeletedObjectItems( + context.activeImage, + context.selectedId, + context.targetData.parentGroupId + ) + } + + rebuildPersistedSelectedPolygon(context) + restoreSelectedPolygonFillColor() + forceUpdate() + return true + } + + const applySelectedPolygonBooleanOperation = ( + mode: "subtract" | "unite", + options: { + draftPath?: paper.Path | null + } = {} + ) => { + const context = getSelectedPolygonContext() + if (!context) return false + const prevDrawOption = useTopToolsStore.getState().drawOption + const nextDrawOption = mode === "subtract" ? "intersect" : "unite" + const baseShape = createWorkingPolygonShape(context.targetShapes) + let clippedDraft: paper.Path | paper.CompoundPath | null = null + + if (!baseShape || !hasShapeSegments(baseShape)) { + options.draftPath?.remove() + baseShape?.remove() return false } - forceUpdate() - return true + if (options.draftPath) { + clippedDraft = options.draftPath.intersect( + usePaperStore.getState().rasterPath!, + { + insert: false, + trace: false, + } + ) as paper.Path | paper.CompoundPath + options.draftPath.remove() + + if (!hasShapeSegments(clippedDraft)) { + clippedDraft?.remove() + baseShape.remove() + return false + } + } + + removeSelectedPolygonRootItems(context) + let workingShape = prepareWorkingPolygonShape(baseShape, context) + + if (clippedDraft) { + const nextShape = + mode === "subtract" + ? (workingShape.subtract(clippedDraft, { + insert: false, + }) as paper.Path | paper.CompoundPath) + : (workingShape.unite(clippedDraft, { + insert: false, + }) as paper.Path | paper.CompoundPath) + clippedDraft.remove() + workingShape.remove() + workingShape = prepareWorkingPolygonShape(nextShape, context) + } + + polygon = workingShape + + useTopToolsStore.getState().setDrawOption(nextDrawOption) + try { + if (useTopToolsStore.getState().drawOption === "intersect") { + handlePolygonIntersect(polygon as paper.Path) + handleCircles(polygon) + } else if (useTopToolsStore.getState().drawOption === "unite") { + handlePolygonUnite() + handleCircles(polygon) + } + + handlePolygonIntersectRaster(polygon as any) + if (polygon && hasShapeSegments(polygon)) { + polygon = prepareWorkingPolygonShape(polygon, context) + } + const persisted = persistSelectedPolygonResult(context) + clearPolygonBooleanHelperItems() + polygon = null + return persisted + } finally { + useTopToolsStore.getState().setDrawOption(prevDrawOption) + } } polygonTool.onMouseDown = (e: { @@ -4310,7 +4613,9 @@ export const usePaperStore = create((set) => ({ if (useKeyboardStore.getState().drawAction === "subtract") { ;(polygon as paper.Path).closePath() - handleSelectedPolygonSubtract(polygon as paper.Path) + applySelectedPolygonBooleanOperation("subtract", { + draftPath: polygon as paper.Path, + }) selectedCircles.forEach((item) => { item.remove() @@ -4339,7 +4644,9 @@ export const usePaperStore = create((set) => ({ : null if (selectedItem?.data?.type === "polygon") { ;(polygon as paper.Path).closePath() - handleSelectedPolygonAdd(polygon as paper.Path) + applySelectedPolygonBooleanOperation("unite", { + draftPath: polygon as paper.Path, + }) selectedCircles.forEach((item) => { item.remove() @@ -4625,6 +4932,12 @@ export const usePaperStore = create((set) => ({ .forEach((item) => { resetPaperShapeFillColor(item) }) + if ( + useTopToolsStore.getState().pressA && + useKeyboardStore.getState().drawAction !== "none" + ) { + restoreSelectedPolygonFillColor() + } } let temp = renderPath(usePaperStore.getState().group!, {}) @@ -4804,6 +5117,8 @@ export const usePaperStore = create((set) => ({ ...state, polygonTool: polygonTool, beginContinuePolygonDraft: () => startContinuePolygonDraft(), + applySelectedPolygonBooleanOperation: (mode, options) => + applySelectedPolygonBooleanOperation(mode, options), })) }, addRectangle: (