From 4f83f6a0216c632be75253a267d9346448012586 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 22 Oct 2025 15:37:43 -0600 Subject: [PATCH] Design optimizations --- .../locales/en/views/classificationModel.json | 2 +- .../ClassificationModelWizardDialog.tsx | 10 +- .../classification/wizard/Step2StateArea.tsx | 291 ++++++++++-------- 3 files changed, 171 insertions(+), 132 deletions(-) diff --git a/web/public/locales/en/views/classificationModel.json b/web/public/locales/en/views/classificationModel.json index a9d9381de..f956a5e0e 100644 --- a/web/public/locales/en/views/classificationModel.json +++ b/web/public/locales/en/views/classificationModel.json @@ -52,7 +52,6 @@ "categorizeImage": "Classify Image", "wizard": { "title": "Create New Classification", - "description": "Create a new state or object classification model.", "steps": { "nameAndDefine": "Name & Define", "stateArea": "State Area", @@ -60,6 +59,7 @@ "train": "Train" }, "step1": { + "description": "Create a new state or object classification model.", "name": "Name", "namePlaceholder": "Enter model name...", "type": "Type", diff --git a/web/src/components/classification/ClassificationModelWizardDialog.tsx b/web/src/components/classification/ClassificationModelWizardDialog.tsx index 9711fe246..9301ed5d4 100644 --- a/web/src/components/classification/ClassificationModelWizardDialog.tsx +++ b/web/src/components/classification/ClassificationModelWizardDialog.tsx @@ -150,8 +150,16 @@ export default function ClassificationModelWizardDialog({ {t("wizard.title")} {wizardState.currentStep === 0 && ( - {t("wizard.description")} + + {t("wizard.step1.description")} + )} + {wizardState.currentStep === 1 && + wizardState.step1Data?.modelType === "state" && ( + + {t("wizard.step2.description")} + + )}
diff --git a/web/src/components/classification/wizard/Step2StateArea.tsx b/web/src/components/classification/wizard/Step2StateArea.tsx index 7ed089b92..ca2503956 100644 --- a/web/src/components/classification/wizard/Step2StateArea.tsx +++ b/web/src/components/classification/wizard/Step2StateArea.tsx @@ -15,6 +15,7 @@ import Konva from "konva"; import { useResizeObserver } from "@/hooks/resize-observer"; import { useApiHost } from "@/api"; import { resolveCameraName } from "@/hooks/use-camera-friendly-name"; +import Heading from "@/components/ui/heading"; export type CameraAreaConfig = { camera: string; @@ -45,6 +46,7 @@ export default function Step2StateArea({ ); const [selectedCameraIndex, setSelectedCameraIndex] = useState(0); const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [imageLoaded, setImageLoaded] = useState(false); const containerRef = useRef(null); const imageRef = useRef(null); @@ -108,15 +110,39 @@ export default function Step2StateArea({ const handleAddCamera = useCallback( (cameraName: string) => { + // Calculate a square crop in pixel space + const camera = config?.cameras[cameraName]; + if (!camera) return; + + const cameraAspect = camera.detect.width / camera.detect.height; + const cropSize = 0.3; + let x1, y1, x2, y2; + + if (cameraAspect >= 1) { + const pixelSize = cropSize * camera.detect.height; + const normalizedWidth = pixelSize / camera.detect.width; + x1 = (1 - normalizedWidth) / 2; + y1 = (1 - cropSize) / 2; + x2 = x1 + normalizedWidth; + y2 = y1 + cropSize; + } else { + const pixelSize = cropSize * camera.detect.width; + const normalizedHeight = pixelSize / camera.detect.height; + x1 = (1 - cropSize) / 2; + y1 = (1 - normalizedHeight) / 2; + x2 = x1 + cropSize; + y2 = y1 + normalizedHeight; + } + const newArea: CameraAreaConfig = { camera: cameraName, - crop: [0.385, 0.385, 0.535, 0.535], + crop: [x1, y1, x2, y2], }; setCameraAreas([...cameraAreas, newArea]); setSelectedCameraIndex(cameraAreas.length); setIsPopoverOpen(false); }, - [cameraAreas], + [cameraAreas, config], ); const handleRemoveCamera = useCallback( @@ -142,17 +168,27 @@ export default function Step2StateArea({ [cameraAreas, selectedCameraIndex], ); + useEffect(() => { + setImageLoaded(false); + }, [selectedCamera]); + useEffect(() => { const rect = rectRef.current; const transformer = transformerRef.current; - if (rect && transformer) { + if ( + rect && + transformer && + selectedCamera && + imageSize.width > 0 && + imageLoaded + ) { rect.scaleX(1); rect.scaleY(1); transformer.nodes([rect]); transformer.getLayer()?.batchDraw(); } - }, [selectedCamera, imageSize]); + }, [selectedCamera, imageSize, imageLoaded]); const handleRectChange = useCallback(() => { const rect = rectRef.current; @@ -186,10 +222,6 @@ export default function Step2StateArea({ return (
-
- {t("wizard.step2.description")} -
-
@@ -205,7 +237,7 @@ export default function Step2StateArea({ type="button" variant="ghost" size="icon" - className="h-6 w-6 p-0" + className="size-6 p-0" aria-label="Add camera" > @@ -218,17 +250,17 @@ export default function Step2StateArea({ onOpenAutoFocus={(e) => e.preventDefault()} >
-

+ {t("wizard.step2.selectCamera")} -

-
+ +
{availableCameras.map((cam) => (
- {selectedCamera && selectedCameraConfig ? ( -
- {imageSize.width > 0 && ( -
+ {selectedCamera && selectedCameraConfig && imageSize.width > 0 ? ( +
+ {resolveCameraName(config, setImageLoaded(true)} + /> + - {resolveCameraName(config, - - - { - const rect = rectRef.current; - if (!rect) return pos; + + { + const rect = rectRef.current; + if (!rect) return pos; - const size = rect.width(); - const x = Math.max( - 0, - Math.min(pos.x, imageSize.width - size), - ); - const y = Math.max( - 0, - Math.min(pos.y, imageSize.height - size), - ); + const size = rect.width(); + const x = Math.max( + 0, + Math.min(pos.x, imageSize.width - size), + ); + const y = Math.max( + 0, + Math.min(pos.y, imageSize.height - size), + ); - return { x, y }; - }} - onDragEnd={handleRectChange} - onTransformEnd={handleRectChange} - /> - { - const minSize = 50; - const maxSize = Math.min( - imageSize.width, - imageSize.height, - ); + return { x, y }; + }} + onDragEnd={handleRectChange} + onTransformEnd={handleRectChange} + /> + { + const minSize = 50; + const maxSize = Math.min( + imageSize.width, + imageSize.height, + ); - // Clamp dimensions to stage bounds first - const clampedWidth = Math.max( - minSize, - Math.min(newBox.width, maxSize), - ); - const clampedHeight = Math.max( - minSize, - Math.min(newBox.height, maxSize), - ); + // Clamp dimensions to stage bounds first + const clampedWidth = Math.max( + minSize, + Math.min(newBox.width, maxSize), + ); + const clampedHeight = Math.max( + minSize, + Math.min(newBox.height, maxSize), + ); - // Enforce square using average - const size = (clampedWidth + clampedHeight) / 2; + // Enforce square using average + const size = (clampedWidth + clampedHeight) / 2; - // Clamp position to keep square within bounds - const x = Math.max( - 0, - Math.min(newBox.x, imageSize.width - size), - ); - const y = Math.max( - 0, - Math.min(newBox.y, imageSize.height - size), - ); + // Clamp position to keep square within bounds + const x = Math.max( + 0, + Math.min(newBox.x, imageSize.width - size), + ); + const y = Math.max( + 0, + Math.min(newBox.y, imageSize.height - size), + ); - return { - ...newBox, - x, - y, - width: size, - height: size, - }; - }} - /> - - -
- )} -
- ) : ( -
- {t("wizard.step2.selectCameraPrompt")} -
- )} + return { + ...newBox, + x, + y, + width: size, + height: size, + }; + }} + /> + + +
+ ) : ( +
+ {t("wizard.step2.selectCameraPrompt")} +
+ )} +