- started painer on video feed will finish
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-konva": "^19.2.0",
|
||||
"react-tabs": "^6.1.0",
|
||||
"react-use-websocket": "3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
import VideoFeedCard from "./VideoFeedCard";
|
||||
import { useState } from "react";
|
||||
|
||||
import VideoFeedGridPainter from "./VideoFeedGridPainter";
|
||||
import CameraSettings from "./CameraSettings";
|
||||
import type { Region } from "../../../types/types";
|
||||
|
||||
const CameraGrid = () => {
|
||||
const [regions, setRegions] = useState<Region[]>([
|
||||
{ name: "Region 1", brushColour: "#ff0000" },
|
||||
{ name: "Region 2", brushColour: "#00ff00" },
|
||||
]);
|
||||
const [selectedRegionIndex, setSelectedRegionIndex] = useState(0);
|
||||
|
||||
const updateRegionColour = (index: number, newColour: string) => {
|
||||
setRegions((prev) => prev.map((r, i) => (i === index ? { ...r, brushColour: newColour } : r)));
|
||||
};
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2">
|
||||
<VideoFeedCard />
|
||||
<VideoFeedGridPainter regions={regions} selectedRegionIndex={selectedRegionIndex} />
|
||||
<CameraSettings
|
||||
regions={regions}
|
||||
selectedRegionIndex={selectedRegionIndex}
|
||||
onSelectRegion={setSelectedRegionIndex}
|
||||
onChangeRegionColour={updateRegionColour}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
43
src/features/cameras/components/CameraSettings.tsx
Normal file
43
src/features/cameras/components/CameraSettings.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import Card from "../../../ui/Card";
|
||||
import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
|
||||
import "react-tabs/style/react-tabs.css";
|
||||
import RegionSelector from "./RegionSelector";
|
||||
import type { Region } from "../../../types/types";
|
||||
|
||||
type CameraSettingsProps = {
|
||||
regions: Region[];
|
||||
selectedRegionIndex: number;
|
||||
onSelectRegion: (index: number) => void;
|
||||
onChangeRegionColour: (index: number, colour: string) => void;
|
||||
};
|
||||
|
||||
const CameraSettings = ({
|
||||
regions,
|
||||
selectedRegionIndex,
|
||||
onSelectRegion,
|
||||
onChangeRegionColour,
|
||||
}: CameraSettingsProps) => {
|
||||
return (
|
||||
<Card className="p-4 min-h-screen">
|
||||
<Tabs selectedTabClassName="bg-gray-300 text-gray-900 font-semibold border-none rounded-sm">
|
||||
<TabList>
|
||||
<Tab>Target Detection</Tab>
|
||||
<Tab>Camera 1</Tab>
|
||||
</TabList>
|
||||
<TabPanel>
|
||||
<RegionSelector
|
||||
regions={regions}
|
||||
selectedRegionIndex={selectedRegionIndex}
|
||||
onSelectRegion={onSelectRegion}
|
||||
onChangeRegionColour={onChangeRegionColour}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div>Camera details</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CameraSettings;
|
||||
11
src/features/cameras/components/ColourPicker.tsx
Normal file
11
src/features/cameras/components/ColourPicker.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
type ColourPickerProps = {
|
||||
colour: string;
|
||||
setColour: (colour: string) => void;
|
||||
};
|
||||
|
||||
const ColourPicker = ({ colour, setColour }: ColourPickerProps) => {
|
||||
console.log(colour);
|
||||
return <input type="color" name="" id="" value={colour} onChange={(e) => setColour(e.target.value)} />;
|
||||
};
|
||||
|
||||
export default ColourPicker;
|
||||
37
src/features/cameras/components/RegionSelector.tsx
Normal file
37
src/features/cameras/components/RegionSelector.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import ColourPicker from "./colourPicker";
|
||||
import type { Region } from "../../../types/types";
|
||||
|
||||
type RegionSelectorProps = {
|
||||
regions: Region[];
|
||||
selectedRegionIndex: number;
|
||||
onSelectRegion: (index: number) => void;
|
||||
onChangeRegionColour: (index: number, colour: string) => void;
|
||||
};
|
||||
|
||||
const RegionSelector = ({
|
||||
regions,
|
||||
selectedRegionIndex,
|
||||
onSelectRegion,
|
||||
onChangeRegionColour,
|
||||
}: RegionSelectorProps) => {
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h2>Region Select</h2>
|
||||
</div>
|
||||
<div>
|
||||
{regions.map((region, idx) => (
|
||||
<div key={region.name}>
|
||||
<label style={{ marginRight: "0.5rem" }}>
|
||||
<input type="radio" checked={selectedRegionIndex === idx} onChange={() => onSelectRegion(idx)} />{" "}
|
||||
{region.name}
|
||||
</label>
|
||||
<ColourPicker colour={region.brushColour} setColour={(c: string) => onChangeRegionColour(idx, c)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegionSelector;
|
||||
@@ -1,13 +0,0 @@
|
||||
import Card from "../../../ui/Card";
|
||||
|
||||
import VideoFeedGridPainter from "./VideoFeedGridPainter";
|
||||
|
||||
const VideoFeedCard = () => {
|
||||
return (
|
||||
<Card className="w-full md:w-[70%]">
|
||||
<VideoFeedGridPainter />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoFeedCard;
|
||||
@@ -1,74 +1,104 @@
|
||||
import { useRef, type RefObject } from "react";
|
||||
import { Stage, Layer, Image, Rect } from "react-konva";
|
||||
import { Stage, Layer, Image, Shape } from "react-konva";
|
||||
import type { KonvaEventObject } from "konva/lib/Node";
|
||||
import { useCreateVideoSnapshot } from "../hooks/useGetvideoSnapshots";
|
||||
import Card from "../../../ui/Card";
|
||||
import type { Region } from "../../../types/types";
|
||||
|
||||
const VideoFeedGridPainter = () => {
|
||||
const rows = 40;
|
||||
const cols = 40;
|
||||
const size = 20;
|
||||
const gap = 0;
|
||||
|
||||
type VideoFeedGridPainterProps = {
|
||||
regions: Region[];
|
||||
selectedRegionIndex: number;
|
||||
};
|
||||
|
||||
const VideoFeedGridPainter = ({ regions, selectedRegionIndex }: VideoFeedGridPainterProps) => {
|
||||
const { latestBitmapRef } = useCreateVideoSnapshot();
|
||||
const isDrawing = useRef(false);
|
||||
|
||||
const rows = 100;
|
||||
const cols = 100;
|
||||
const size = 10;
|
||||
const gap = 0;
|
||||
const isDrawingRef = useRef(false);
|
||||
const paintedCellsRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const squares = [];
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
squares.push(
|
||||
<Rect
|
||||
key={`${row}-${col}`}
|
||||
x={col * (size + gap)}
|
||||
y={row * (size + gap)}
|
||||
width={size}
|
||||
height={size}
|
||||
fill="#ddd"
|
||||
stroke="black"
|
||||
strokeWidth={0.5}
|
||||
opacity={0.5}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getCoords = (e) => {
|
||||
isDrawing.current = true;
|
||||
};
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (!isDrawing.current) {
|
||||
return;
|
||||
}
|
||||
const pos = e.target.getStage().getPointerPosition();
|
||||
console.log(pos);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
isDrawing.current = false;
|
||||
};
|
||||
const paintLayerRef = useRef<any>(null);
|
||||
|
||||
const draw = (bmp: RefObject<ImageBitmap | null>) => {
|
||||
if (!bmp || !bmp.current) {
|
||||
return;
|
||||
} else {
|
||||
const frame = bmp.current;
|
||||
return frame;
|
||||
}
|
||||
if (!bmp || !bmp.current) return null;
|
||||
return bmp.current;
|
||||
};
|
||||
|
||||
const paintCell = (x: number, y: number) => {
|
||||
const col = Math.floor(x / (size + gap));
|
||||
const row = Math.floor(y / (size + gap));
|
||||
|
||||
if (row < 0 || row >= rows || col < 0 || col >= cols) return;
|
||||
|
||||
const key = `${row}-${col}`;
|
||||
const set = paintedCellsRef.current;
|
||||
if (set.has(key)) return;
|
||||
|
||||
set.add(key);
|
||||
|
||||
paintLayerRef.current?.batchDraw();
|
||||
};
|
||||
|
||||
const handleStageMouseDown = (e: KonvaEventObject<MouseEvent>) => {
|
||||
if (!selectedRegionIndex) return;
|
||||
isDrawingRef.current = true;
|
||||
const pos = e.target.getStage()?.getPointerPosition();
|
||||
if (pos) paintCell(pos.x, pos.y);
|
||||
};
|
||||
|
||||
const handleStageMouseMove = (e: KonvaEventObject<MouseEvent>) => {
|
||||
if (!isDrawingRef.current) return;
|
||||
if (!selectedRegionIndex) return;
|
||||
const pos = e.target.getStage()?.getPointerPosition();
|
||||
if (pos) paintCell(pos.x, pos.y);
|
||||
};
|
||||
|
||||
const handleStageMouseUp = () => {
|
||||
isDrawingRef.current = false;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-187.5 place-self-start">
|
||||
<Stage
|
||||
width={640}
|
||||
height={360}
|
||||
onMouseDown={getCoords}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
width={740}
|
||||
height={460}
|
||||
onMouseDown={handleStageMouseDown}
|
||||
onMouseMove={handleStageMouseMove}
|
||||
onMouseUp={handleStageMouseUp}
|
||||
onMouseLeave={handleStageMouseUp}
|
||||
>
|
||||
<Layer>
|
||||
<Image image={draw(latestBitmapRef)} width={640} height={360} />
|
||||
<Image image={draw(latestBitmapRef)} width={740} height={460} />
|
||||
</Layer>
|
||||
|
||||
<Layer ref={paintLayerRef} opacity={0.6}>
|
||||
<Shape
|
||||
sceneFunc={(ctx, shape) => {
|
||||
const cells = paintedCellsRef.current;
|
||||
cells.forEach((key) => {
|
||||
const [rowStr, colStr] = key.split("-");
|
||||
const row = Number(rowStr);
|
||||
const col = Number(colStr);
|
||||
|
||||
const x = col * (size + gap);
|
||||
const y = row * (size + gap);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.rect(x, y, size, size);
|
||||
ctx.fillStyle = regions[selectedRegionIndex]?.brushColour;
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
ctx.fillStrokeShape(shape);
|
||||
}}
|
||||
/>
|
||||
</Layer>
|
||||
<Layer opacity={0.3}>{squares}</Layer>
|
||||
</Stage>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export const useCreateVideoSnapshot = () => {
|
||||
|
||||
try {
|
||||
const bitmap = await createImageBitmap(snapShot);
|
||||
if (!bitmap) return;
|
||||
latestBitmapRef.current = bitmap;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
@@ -10,3 +10,8 @@ export type InfoBarData = {
|
||||
"memory-cpu-status": string;
|
||||
"thread-count": string;
|
||||
};
|
||||
|
||||
export type Region = {
|
||||
name: string;
|
||||
brushColour: string;
|
||||
};
|
||||
|
||||
38
yarn.lock
38
yarn.lock
@@ -1293,7 +1293,7 @@ chokidar@^3.6.0:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
clsx@^2.1.1:
|
||||
clsx@^2.0.0, clsx@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
|
||||
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
|
||||
@@ -1748,7 +1748,7 @@ jiti@^2.6.1:
|
||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.6.1.tgz#178ef2fc9a1a594248c20627cd820187a4d78d92"
|
||||
integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==
|
||||
|
||||
js-tokens@^4.0.0:
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
@@ -1891,6 +1891,13 @@ lodash.merge@^4.6.2:
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||
|
||||
loose-envify@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
dependencies:
|
||||
js-tokens "^3.0.0 || ^4.0.0"
|
||||
|
||||
lru-cache@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
|
||||
@@ -1962,6 +1969,11 @@ normalize-range@^0.1.2:
|
||||
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
|
||||
integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==
|
||||
|
||||
object-assign@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
|
||||
|
||||
optionator@^0.9.3:
|
||||
version "0.9.4"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734"
|
||||
@@ -2049,6 +2061,15 @@ prettier@^3.5.0:
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393"
|
||||
integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==
|
||||
|
||||
prop-types@^15.5.0:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
dependencies:
|
||||
loose-envify "^1.4.0"
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.13.1"
|
||||
|
||||
punycode@^2.1.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||
@@ -2066,6 +2087,11 @@ react-dom@^19.2.0:
|
||||
dependencies:
|
||||
scheduler "^0.27.0"
|
||||
|
||||
react-is@^16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react-konva@^19.2.0:
|
||||
version "19.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-konva/-/react-konva-19.2.0.tgz#b4cc5d73cd6d642569e4df36a0139996c3dcf8e6"
|
||||
@@ -2088,6 +2114,14 @@ react-refresh@^0.18.0:
|
||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.18.0.tgz#2dce97f4fe932a4d8142fa1630e475c1729c8062"
|
||||
integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==
|
||||
|
||||
react-tabs@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-6.1.0.tgz#a1fc9d9b8db4c6e7bb327a1b6783bc51a1c457a1"
|
||||
integrity sha512-6QtbTRDKM+jA/MZTTefvigNxo0zz+gnBTVFw2CFVvq+f2BuH0nF0vDLNClL045nuTAdOoK/IL1vTP0ZLX0DAyQ==
|
||||
dependencies:
|
||||
clsx "^2.0.0"
|
||||
prop-types "^15.5.0"
|
||||
|
||||
react-use-websocket@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-use-websocket/-/react-use-websocket-3.0.0.tgz#754cb8eea76f55d31c5676d4abe3e573bc2cea04"
|
||||
|
||||
Reference in New Issue
Block a user