diff --git a/furniture-3d-system/src/app/api/installation-demo-workbench/drawing-text-annotations/route.ts b/furniture-3d-system/src/app/api/installation-demo-workbench/drawing-text-annotations/route.ts new file mode 100644 index 00000000..dad767f4 --- /dev/null +++ b/furniture-3d-system/src/app/api/installation-demo-workbench/drawing-text-annotations/route.ts @@ -0,0 +1,50 @@ +import { NextRequest } from "next/server"; +import { AppError } from "@/server/core/errors"; +import { handleRequest, jsonError, jsonSuccess } from "@/server/core/response"; +import { installationDemoDrawingTextAnnotationService } from "@/server/services/installation-demo/installation-demo-drawing-text-annotation.service"; + +function respondWithAppError(error: unknown, requestId: string) { + if (error instanceof AppError) { + return jsonError(error.code, error.message, error.statusCode, requestId); + } + throw error; +} + +export const dynamic = "force-dynamic"; + +export async function GET(request: NextRequest) { + return handleRequest( + request, + async (ctx) => { + try { + const snapshot = await installationDemoDrawingTextAnnotationService.get( + ctx.user, + request.nextUrl.searchParams.get("drawingId"), + ); + return jsonSuccess({ snapshot }, 200, ctx.requestId); + } catch (error) { + return respondWithAppError(error, ctx.requestId); + } + }, + { requireAuth: true }, + ); +} + +export async function PATCH(request: NextRequest) { + return handleRequest( + request, + async (ctx) => { + try { + const snapshot = + await installationDemoDrawingTextAnnotationService.updateTarget( + ctx.user, + await request.json(), + ); + return jsonSuccess({ snapshot }, 200, ctx.requestId); + } catch (error) { + return respondWithAppError(error, ctx.requestId); + } + }, + { requireAuth: true }, + ); +} diff --git a/furniture-3d-system/src/app/installation-demo-share/[token]/InstallationDemoShareDrawingPage.tsx b/furniture-3d-system/src/app/installation-demo-share/[token]/InstallationDemoShareDrawingPage.tsx index efab831c..7366f261 100644 --- a/furniture-3d-system/src/app/installation-demo-share/[token]/InstallationDemoShareDrawingPage.tsx +++ b/furniture-3d-system/src/app/installation-demo-share/[token]/InstallationDemoShareDrawingPage.tsx @@ -65,6 +65,9 @@ function InstallationDemoShareDrawingPanel({ imageMode={imageMode} snapshotUrls={snapshot.numberingSnapshotUrls} snapshotLineArtUrls={snapshotLineArtUrls} + textAnnotationsByTargetKey={ + snapshot.drawingDocument?.textAnnotationsByTargetKey ?? {} + } allowManualCapture={false} readOnly /> diff --git a/furniture-3d-system/src/app/installation-demo-share/[token]/installationDemoShareDrawingDocument.test.ts b/furniture-3d-system/src/app/installation-demo-share/[token]/installationDemoShareDrawingDocument.test.ts index b6c7fda1..779e36ef 100644 --- a/furniture-3d-system/src/app/installation-demo-share/[token]/installationDemoShareDrawingDocument.test.ts +++ b/furniture-3d-system/src/app/installation-demo-share/[token]/installationDemoShareDrawingDocument.test.ts @@ -41,6 +41,7 @@ function createSnapshot(): InstallationDemoShareSnapshot { actions: [], inventoryTransforms: {}, }, + textAnnotationsByTargetKey: {}, snapshotLineArtUrls: {}, toolSnapshotLineArtUrls: {}, }, diff --git a/furniture-3d-system/src/app/installation-demo-workbench/InstallationDemoWorkbenchClient.tsx b/furniture-3d-system/src/app/installation-demo-workbench/InstallationDemoWorkbenchClient.tsx index f188ff3b..6e37fa52 100644 --- a/furniture-3d-system/src/app/installation-demo-workbench/InstallationDemoWorkbenchClient.tsx +++ b/furniture-3d-system/src/app/installation-demo-workbench/InstallationDemoWorkbenchClient.tsx @@ -107,6 +107,7 @@ import { InstallationDrawingToolLineArtGenerator } from "./components/Installati import { useInstallationDrawingCapture } from "./hooks/useInstallationDrawingCapture"; import { useInstallationDrawingAutoCapture } from "./hooks/useInstallationDrawingAutoCapture"; import { useInstallationDrawingConfirmation } from "./hooks/useInstallationDrawingConfirmation"; +import { useInstallationDrawingTextAnnotations } from "./hooks/useInstallationDrawingTextAnnotations"; import { buildInstallationDrawingCaptureFingerprint } from "./lib/installationDrawingCaptureFingerprint"; import { isInstallationDrawingActionCaptureTarget, @@ -401,6 +402,10 @@ export default function InstallationDemoWorkbenchClient({ setMode, setActiveStepId: stepEditor.setActiveStepId, }); + const drawingTextAnnotations = useInstallationDrawingTextAnnotations( + selectedDrawingId, + catalog.access.canEdit, + ); const drawingOverviewCaptureActive = isInstallationDrawingOverviewCaptureTarget(drawingCapture.captureTarget); const drawingActionCaptureTarget = isInstallationDrawingActionCaptureTarget( @@ -1732,6 +1737,8 @@ export default function InstallationDemoWorkbenchClient({ templateId: "set-1", imageMode: drawingImageMode ?? "line-art", captures: drawingCapture.captures, + textAnnotationsByTargetKey: + drawingTextAnnotations.annotationsByTargetKey, snapshotLineArtUrls: shareNumberingSnapshotBatch.snapshotLineArtUrls ?? {}, toolSnapshotLineArtUrls: @@ -1763,6 +1770,7 @@ export default function InstallationDemoWorkbenchClient({ numberingModelParts.length, numberingPlaying, drawingCapture.captures, + drawingTextAnnotations.annotationsByTargetKey, drawingTools.selectedTools, numberingVisibleModelParts, playing, @@ -2614,6 +2622,10 @@ export default function InstallationDemoWorkbenchClient({ numberingLineArtGenerating || Boolean(missingDrawingToolLineArt) } captures={drawingCapture.captures} + textAnnotationsByTargetKey={ + drawingTextAnnotations.annotationsByTargetKey + } + textAnnotationMessage={drawingTextAnnotations.message} initialScrollTop={ drawingSheetScrollPosition.drawingId === selectedDrawingId ? drawingSheetScrollPosition.scrollTop @@ -2638,6 +2650,9 @@ export default function InstallationDemoWorkbenchClient({ numberingMarkerCountOverrides, ); }} + onUpdateTextAnnotations={ + drawingTextAnnotations.saveTargetAnnotations + } autoCapture={{ phase: drawingAutoCapture.phase, error: drawingAutoCapture.error, diff --git a/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationDrawingActionsPage.tsx b/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationDrawingActionsPage.tsx index 9db6a7cb..36b032b8 100644 --- a/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationDrawingActionsPage.tsx +++ b/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationDrawingActionsPage.tsx @@ -1,14 +1,22 @@ -import { ListChecks, Maximize2 } from 'lucide-react' +import { ListChecks, Maximize2, Type } from 'lucide-react' import type { InstallationDrawingActionCapture, InstallationDrawingCaptureTarget, InstallationDrawingImageMode, InstallationDrawingNumberMarker, } from '../lib/installationDrawingCaptureTypes' -import { isInstallationDrawingImageCaptured } from '../lib/installationDrawingCaptureTypes' +import { + getInstallationDrawingCaptureTargetKey, + isInstallationDrawingImageCaptured, +} from '../lib/installationDrawingCaptureTypes' import type { InstallationDrawingActionGroup } from '../lib/installationDrawingLayout' +import type { + InstallationDrawingTextAnnotation, + InstallationDrawingTextAnnotationsByTarget, +} from '../lib/installationDrawingTextAnnotations' import { InstallationDrawingImageFrame } from './InstallationDrawingImageFrame' import { InstallationDrawingNumberMarkers } from './InstallationDrawingNumberMarkers' +import { InstallationDrawingTextAnnotations } from './InstallationDrawingTextAnnotations' interface Props { groups: InstallationDrawingActionGroup[] @@ -23,6 +31,12 @@ interface Props { target: InstallationDrawingCaptureTarget, markers: InstallationDrawingNumberMarker[], ) => void + textAnnotationsByTargetKey?: InstallationDrawingTextAnnotationsByTarget + onManageTextAnnotations?: (target: InstallationDrawingCaptureTarget) => void + onCommitTextAnnotations?: ( + target: InstallationDrawingCaptureTarget, + annotations: InstallationDrawingTextAnnotation[], + ) => void allowManualCapture: boolean readOnly?: boolean } @@ -77,6 +91,9 @@ export function InstallationDrawingActionsPage({ onAdjustImage, onManageNumberingMarkers, onCommitNumberingMarkers, + textAnnotationsByTargetKey = {}, + onManageTextAnnotations, + onCommitTextAnnotations, allowManualCapture, readOnly = false, }: Props) { @@ -100,6 +117,8 @@ export function InstallationDrawingActionsPage({ actionId: action.actionId, sequence: action.sequence, } + const targetKey = getInstallationDrawingCaptureTargetKey(target) + const textAnnotations = textAnnotationsByTargetKey[targetKey] ?? [] const captured = capture ? isInstallationDrawingImageCaptured(capture.image) : false return (
) : null} + {captured && textAnnotations.length > 0 ? ( + onCommitTextAnnotations(target, annotations) + : undefined} + /> + ) : null} {capture && capture.tools.length > 0 ? ( @@ -155,6 +183,17 @@ export function InstallationDrawingActionsPage({ ) : null} + {capture && captured ? ( + + ) : null} {capture && captured ? ( - ) : ( - - )} - {hasDraftBatch && !running ? ( - - 已完成 {completedCount}/{totalCount} - + <> +
+ {settled ? ( + + ) : running ? ( + + ) : ( + + )} + {hasDraftBatch && !running ? ( + + 已完成 {completedCount}/{totalCount} + + ) : null} + {configurationChanged ? ( + + 配置已变更 + + ) : null} + {error && !running ? ( + + {error} + + ) : null} +
+ + {restartDialogOpen ? ( +
+
+
+
+ +

+ 重新批量带入全部画面? +

+
+ +
+
+

+ 将重新抓取 {totalCount} 张原图和打印线稿。当前版本会保留到新批次全部完成,已有画面位置和编号设置继续沿用。 +

+
+ + +
+
+
+
) : null} - {configurationChanged ? ( - - 配置已变更 - - ) : null} - {error && !running ? ( - - {error} - - ) : null} - + ); } diff --git a/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationDrawingSheet.tsx b/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationDrawingSheet.tsx index 527d0237..66e7ae03 100644 --- a/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationDrawingSheet.tsx +++ b/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationDrawingSheet.tsx @@ -12,6 +12,7 @@ import type { } from "../types"; import type { InstallationDrawingCaptures, + InstallationDrawingActionCaptureTarget, InstallationDrawingCaptureTarget, InstallationDrawingCapturedImage, InstallationDrawingImageMode, @@ -20,11 +21,16 @@ import type { } from "../lib/installationDrawingCaptureTypes"; import { DEFAULT_INSTALLATION_DRAWING_IMAGE_TRANSFORM, + getInstallationDrawingCaptureTargetKey, isInstallationDrawingImageAvailable, isInstallationDrawingImageCaptured, resolveInstallationDrawingConfirmedAtForMode, resolveInstallationDrawingImageMode, } from "../lib/installationDrawingCaptureTypes"; +import type { + InstallationDrawingTextAnnotation, + InstallationDrawingTextAnnotationsByTarget, +} from "../lib/installationDrawingTextAnnotations"; import { buildInstallationDrawingLayout } from "../lib/installationDrawingLayout"; import { useInstallationDrawingMarkerInventory } from "../hooks/useInstallationDrawingMarkerInventory"; import { @@ -39,6 +45,7 @@ import { InstallationDrawingInventoryPage } from "./InstallationDrawingInventory import { InstallationDrawingOverviewPage } from "./InstallationDrawingOverviewPage"; import { InstallationDrawingImageAdjustDialog } from "./InstallationDrawingImageAdjustDialog"; import { InstallationDrawingMarkerManagerDialog } from "./InstallationDrawingMarkerManagerDialog"; +import { InstallationDrawingTextAnnotationDialog } from "./InstallationDrawingTextAnnotationDialog"; import { buildInstallationDrawingInventoryItems, type InstallationDrawingOverviewTool, @@ -57,6 +64,8 @@ interface InstallationDrawingSheetProps { toolSnapshotLineArtUrls: Record; lineArtGenerating?: boolean; captures: InstallationDrawingCaptures; + textAnnotationsByTargetKey: InstallationDrawingTextAnnotationsByTarget; + textAnnotationMessage?: string | null; initialScrollTop: number; onCaptureTarget: ( target: InstallationDrawingCaptureTarget, @@ -68,6 +77,10 @@ interface InstallationDrawingSheetProps { numberingMarkers: InstallationDrawingNumberMarker[], numberingMarkerCountOverrides: Record, ) => void; + onUpdateTextAnnotations: ( + targetKey: string, + annotations: InstallationDrawingTextAnnotation[], + ) => void; autoCapture: { phase: | "loading" @@ -162,9 +175,12 @@ export function InstallationDrawingSheet({ toolSnapshotLineArtUrls, lineArtGenerating = false, captures, + textAnnotationsByTargetKey, + textAnnotationMessage = null, initialScrollTop, onCaptureTarget, onUpdateImageLayout, + onUpdateTextAnnotations, autoCapture, confirmation, onClose, @@ -180,6 +196,8 @@ export function InstallationDrawingSheet({ ); const [adjustTarget, setAdjustTarget] = useState(null); + const [textAnnotationTarget, setTextAnnotationTarget] = + useState(null); const scrollContainerRef = useRef(null); const { markerManagerTarget, @@ -297,6 +315,23 @@ export function InstallationDrawingSheet({ image.numberingMarkerCountOverrides, ); }; + const commitTextAnnotations = ( + target: InstallationDrawingActionCaptureTarget, + annotations: InstallationDrawingTextAnnotation[], + ) => { + const image = resolveCapturedImage(reconciledCaptures, target); + if (!image) return; + onUpdateTextAnnotations( + getInstallationDrawingCaptureTargetKey(target), + annotations, + ); + onUpdateImageLayout( + target, + image.transform, + image.numberingMarkers, + image.numberingMarkerCountOverrides, + ); + }; const captureFromCurrentPosition = ( target: InstallationDrawingCaptureTarget, ) => { @@ -429,6 +464,14 @@ export function InstallationDrawingSheet({ {confirmation.message}

) : null} + {textAnnotationMessage ? ( +

+ {textAnnotationMessage} +

+ ) : null}
{ + if (target.kind === "action") setTextAnnotationTarget(target); + }} + onCommitTextAnnotations={(target, annotations) => { + if (target.kind === "action") { + commitTextAnnotations(target, annotations); + } + }} allowManualCapture={ !autoCapture.running && !autoCapture.hasDraftBatch } @@ -647,6 +699,21 @@ export function InstallationDrawingSheet({ onClose={() => setMarkerManagerTarget(null)} /> ) : null} + {textAnnotationTarget ? ( + { + commitTextAnnotations(textAnnotationTarget, annotations); + setTextAnnotationTarget(null); + }} + onClose={() => setTextAnnotationTarget(null)} + /> + ) : null}
, document.body, ); diff --git a/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationDrawingSheetScroll.test.tsx b/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationDrawingSheetScroll.test.tsx index 1fbf77f8..0703228c 100644 --- a/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationDrawingSheetScroll.test.tsx +++ b/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationDrawingSheetScroll.test.tsx @@ -80,9 +80,11 @@ function buildProps(initialScrollTop: number, onCaptureTarget: jest.Mock) { actions: [], inventoryTransforms: {}, }, + textAnnotationsByTargetKey: {}, initialScrollTop, onCaptureTarget, onUpdateImageLayout: jest.fn(), + onUpdateTextAnnotations: jest.fn(), autoCapture: { phase: "idle" as const, error: null, diff --git a/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationDrawingTextAnnotationDialog.tsx b/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationDrawingTextAnnotationDialog.tsx new file mode 100644 index 00000000..002fe343 --- /dev/null +++ b/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationDrawingTextAnnotationDialog.tsx @@ -0,0 +1,199 @@ +"use client"; + +import { useState } from "react"; +import { Check, Minus, Plus, Trash2, Type, X } from "lucide-react"; +import { + MAX_INSTALLATION_DRAWING_TEXT_ANNOTATIONS_PER_TARGET, + MAX_INSTALLATION_DRAWING_TEXT_FONT_SIZE, + MAX_INSTALLATION_DRAWING_TEXT_LENGTH, + MIN_INSTALLATION_DRAWING_TEXT_FONT_SIZE, + clampInstallationDrawingTextFontSize, + cloneInstallationDrawingTextAnnotations, + createInstallationDrawingTextAnnotation, + type InstallationDrawingTextAnnotation, +} from "../lib/installationDrawingTextAnnotations"; + +interface Props { + title: string; + annotations: InstallationDrawingTextAnnotation[]; + onSave: (annotations: InstallationDrawingTextAnnotation[]) => void; + onClose: () => void; +} + +export function InstallationDrawingTextAnnotationDialog({ + title, + annotations, + onSave, + onClose, +}: Props) { + const [items, setItems] = useState(() => + cloneInstallationDrawingTextAnnotations(annotations), + ); + + const updateItem = ( + id: string, + patch: Partial, + ) => { + setItems((current) => + current.map((item) => (item.id === id ? { ...item, ...patch } : item)), + ); + }; + + return ( +
+
+
+
+

文字标注 · {title}

+

+ 保存后直接在动作画面中拖动文字到目标位置。 +

+
+ +
+ +
+
+ 共 {items.length} 个文本框 + +
+ + {items.length === 0 ? ( +
+ 点击“添加文本”创建第一个文字标注 +
+ ) : ( +
+ {items.map((item, index) => ( +
+ + {index + 1} + +