diff --git a/package.json b/package.json index 4f25338..be15300 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "react-dom": "^19.2.0", "react-konva": "^19.2.0", "react-tabs": "^6.1.0", - "react-use-websocket": "3.0.0" + "react-use-websocket": "3.0.0", + "sonner": "^2.0.7" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/src/features/cameras/components/CameraGrid.tsx b/src/features/cameras/components/CameraGrid.tsx index c1f0033..7cb2353 100644 --- a/src/features/cameras/components/CameraGrid.tsx +++ b/src/features/cameras/components/CameraGrid.tsx @@ -8,20 +8,24 @@ const CameraGrid = () => { const [regions, setRegions] = useState([ { name: "Region 1", brushColour: "#ff0000" }, { name: "Region 2", brushColour: "#00ff00" }, + { name: "Region 3", brushColour: "#0400ff" }, ]); const [selectedRegionIndex, setSelectedRegionIndex] = useState(0); + const [isErasing, setErasing] = useState(false); const updateRegionColour = (index: number, newColour: string) => { setRegions((prev) => prev.map((r, i) => (i === index ? { ...r, brushColour: newColour } : r))); }; return (
- +
); diff --git a/src/features/cameras/components/CameraSettings.tsx b/src/features/cameras/components/CameraSettings.tsx index 751dd8c..e0669cd 100644 --- a/src/features/cameras/components/CameraSettings.tsx +++ b/src/features/cameras/components/CameraSettings.tsx @@ -9,6 +9,8 @@ type CameraSettingsProps = { selectedRegionIndex: number; onSelectRegion: (index: number) => void; onChangeRegionColour: (index: number, colour: string) => void; + isErasing: boolean; + onSelectErasing: (isErasing: boolean) => void; }; const CameraSettings = ({ @@ -16,9 +18,11 @@ const CameraSettings = ({ selectedRegionIndex, onSelectRegion, onChangeRegionColour, + isErasing, + onSelectErasing, }: CameraSettingsProps) => { return ( - + Target Detection @@ -30,6 +34,8 @@ const CameraSettings = ({ selectedRegionIndex={selectedRegionIndex} onSelectRegion={onSelectRegion} onChangeRegionColour={onChangeRegionColour} + isErasing={isErasing} + onSelectErasing={onSelectErasing} /> diff --git a/src/features/cameras/components/ColourPicker.tsx b/src/features/cameras/components/ColourPicker.tsx index 2180e09..f097cc4 100644 --- a/src/features/cameras/components/ColourPicker.tsx +++ b/src/features/cameras/components/ColourPicker.tsx @@ -4,7 +4,6 @@ type ColourPickerProps = { }; const ColourPicker = ({ colour, setColour }: ColourPickerProps) => { - console.log(colour); return setColour(e.target.value)} />; }; diff --git a/src/features/cameras/components/RegionSelector.tsx b/src/features/cameras/components/RegionSelector.tsx index 55276ad..04d9e06 100644 --- a/src/features/cameras/components/RegionSelector.tsx +++ b/src/features/cameras/components/RegionSelector.tsx @@ -1,4 +1,4 @@ -import ColourPicker from "./colourPicker"; +import ColourPicker from "./ColourPicker"; import type { Region } from "../../../types/types"; type RegionSelectorProps = { @@ -6,6 +6,8 @@ type RegionSelectorProps = { selectedRegionIndex: number; onSelectRegion: (index: number) => void; onChangeRegionColour: (index: number, colour: string) => void; + isErasing: boolean; + onSelectErasing: (isErasing: boolean) => void; }; const RegionSelector = ({ @@ -13,7 +15,13 @@ const RegionSelector = ({ selectedRegionIndex, onSelectRegion, onChangeRegionColour, + isErasing, + onSelectErasing, }: RegionSelectorProps) => { + const handleChange = () => { + onSelectErasing(!isErasing); + }; + return (
@@ -29,6 +37,10 @@ const RegionSelector = ({ onChangeRegionColour(idx, c)} />
))} +
); diff --git a/src/features/cameras/components/VideoFeedGridPainter.tsx b/src/features/cameras/components/VideoFeedGridPainter.tsx index 3511a68..960cdc2 100644 --- a/src/features/cameras/components/VideoFeedGridPainter.tsx +++ b/src/features/cameras/components/VideoFeedGridPainter.tsx @@ -13,19 +13,26 @@ const gap = 0; type VideoFeedGridPainterProps = { regions: Region[]; selectedRegionIndex: number; + isErasing: boolean; }; -const VideoFeedGridPainter = ({ regions, selectedRegionIndex }: VideoFeedGridPainterProps) => { +type PaintedCell = { + colour: string; +}; + +const VideoFeedGridPainter = ({ regions, selectedRegionIndex, isErasing }: VideoFeedGridPainterProps) => { const { latestBitmapRef } = useCreateVideoSnapshot(); - const isDrawingRef = useRef(false); - const paintedCellsRef = useRef>(new Set()); - + const paintedCellsRef = useRef>(new Map()); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const paintLayerRef = useRef(null); - const draw = (bmp: RefObject) => { - if (!bmp || !bmp.current) return null; - return bmp.current; + const draw = (bmp: RefObject): ImageBitmap | null => { + if (!bmp || !bmp.current) { + return null; + } + const image = bmp.current; + return image; }; const paintCell = (x: number, y: number) => { @@ -34,17 +41,32 @@ const VideoFeedGridPainter = ({ regions, selectedRegionIndex }: VideoFeedGridPai if (row < 0 || row >= rows || col < 0 || col >= cols) return; - const key = `${row}-${col}`; - const set = paintedCellsRef.current; - if (set.has(key)) return; + const activeRegion = regions[selectedRegionIndex]; + if (!activeRegion) return; - set.add(key); + const key = `${row}-${col}`; + const currentColour = regions[selectedRegionIndex].brushColour; + + const map = paintedCellsRef.current; + const existing = map.get(key); + + if (isErasing) { + if (map.has(key)) { + map.delete(key); + paintLayerRef.current?.batchDraw(); + } + return; + } + + if (existing && existing.colour === currentColour) return; + + map.set(key, { colour: currentColour }); paintLayerRef.current?.batchDraw(); }; const handleStageMouseDown = (e: KonvaEventObject) => { - if (!selectedRegionIndex) return; + if (!regions[selectedRegionIndex]) return; isDrawingRef.current = true; const pos = e.target.getStage()?.getPointerPosition(); if (pos) paintCell(pos.x, pos.y); @@ -52,7 +74,7 @@ const VideoFeedGridPainter = ({ regions, selectedRegionIndex }: VideoFeedGridPai const handleStageMouseMove = (e: KonvaEventObject) => { if (!isDrawingRef.current) return; - if (!selectedRegionIndex) return; + if (!regions[selectedRegionIndex]) return; const pos = e.target.getStage()?.getPointerPosition(); if (pos) paintCell(pos.x, pos.y); }; @@ -79,7 +101,7 @@ const VideoFeedGridPainter = ({ regions, selectedRegionIndex }: VideoFeedGridPai { const cells = paintedCellsRef.current; - cells.forEach((key) => { + cells.forEach((cell, key) => { const [rowStr, colStr] = key.split("-"); const row = Number(rowStr); const col = Number(colStr); @@ -89,7 +111,7 @@ const VideoFeedGridPainter = ({ regions, selectedRegionIndex }: VideoFeedGridPai ctx.beginPath(); ctx.rect(x, y, size, size); - ctx.fillStyle = regions[selectedRegionIndex]?.brushColour; + ctx.fillStyle = cell.colour; ctx.fill(); }); diff --git a/src/routes/baywatch.tsx b/src/routes/baywatch.tsx index 9271b97..f7401ab 100644 --- a/src/routes/baywatch.tsx +++ b/src/routes/baywatch.tsx @@ -1,5 +1,6 @@ import { createFileRoute } from "@tanstack/react-router"; import CameraGrid from "../features/cameras/components/CameraGrid"; +import { Toaster } from "sonner"; export const Route = createFileRoute("/baywatch")({ component: RouteComponent, @@ -10,6 +11,7 @@ function RouteComponent() {

Cameras

+
); } diff --git a/yarn.lock b/yarn.lock index 992702f..b567169 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2240,6 +2240,11 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +sonner@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/sonner/-/sonner-2.0.7.tgz#810c1487a67ec3370126e0f400dfb9edddc3e4f6" + integrity sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w== + source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"