mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-11 05:35:25 +03:00
object masks and deletion of all types
This commit is contained in:
parent
93b206d6b4
commit
848d881976
@ -7,7 +7,7 @@ import { Polygon, PolygonType } from "@/types/canvas";
|
|||||||
import { interpolatePoints, parseCoordinates } from "@/utils/canvasUtil";
|
import { interpolatePoints, parseCoordinates } from "@/utils/canvasUtil";
|
||||||
import { Skeleton } from "../ui/skeleton";
|
import { Skeleton } from "../ui/skeleton";
|
||||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||||
import { LuExternalLink, LuInfo, LuPlus } from "react-icons/lu";
|
import { LuExternalLink, LuPlus } from "react-icons/lu";
|
||||||
import {
|
import {
|
||||||
HoverCard,
|
HoverCard,
|
||||||
HoverCardContent,
|
HoverCardContent,
|
||||||
@ -25,12 +25,6 @@ import ObjectMaskEditPane from "./ObjectMaskEditPane";
|
|||||||
import PolygonItem from "./PolygonItem";
|
import PolygonItem from "./PolygonItem";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
// export type ZoneObjects = {
|
|
||||||
// camera: string;
|
|
||||||
// zoneName: string;
|
|
||||||
// objects: string[];
|
|
||||||
// };
|
|
||||||
|
|
||||||
type MasksAndZoneProps = {
|
type MasksAndZoneProps = {
|
||||||
selectedCamera: string;
|
selectedCamera: string;
|
||||||
selectedZoneMask?: PolygonType[];
|
selectedZoneMask?: PolygonType[];
|
||||||
@ -209,12 +203,12 @@ export default function MasksAndZones({
|
|||||||
setActivePolygonIndex(allPolygons.length);
|
setActivePolygonIndex(allPolygons.length);
|
||||||
|
|
||||||
let polygonColor = [128, 128, 0];
|
let polygonColor = [128, 128, 0];
|
||||||
|
|
||||||
if (type == "motion_mask") {
|
if (type == "motion_mask") {
|
||||||
polygonColor = [0, 0, 220];
|
polygonColor = [0, 0, 220];
|
||||||
}
|
}
|
||||||
if (type == "object_mask") {
|
if (type == "object_mask") {
|
||||||
polygonColor = [128, 128, 128];
|
polygonColor = [128, 128, 128];
|
||||||
// TODO - get this from config object after mutation so label can be set
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setEditingPolygons([
|
setEditingPolygons([
|
||||||
@ -224,6 +218,7 @@ export default function MasksAndZones({
|
|||||||
isFinished: false,
|
isFinished: false,
|
||||||
// isUnsaved: true,
|
// isUnsaved: true,
|
||||||
type,
|
type,
|
||||||
|
typeIndex: 9999,
|
||||||
name: "",
|
name: "",
|
||||||
objects: [],
|
objects: [],
|
||||||
camera: selectedCamera,
|
camera: selectedCamera,
|
||||||
@ -305,30 +300,49 @@ export default function MasksAndZones({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// this can be an array or a string
|
let motionMasks: Polygon[] = [];
|
||||||
const motionMasks = Object.entries(
|
let globalObjectMasks: Polygon[] = [];
|
||||||
Array.isArray(cameraConfig.motion.mask)
|
let objectMasks: Polygon[] = [];
|
||||||
? cameraConfig.motion.mask
|
|
||||||
: [cameraConfig.motion.mask],
|
|
||||||
).map(([, maskData], index) => ({
|
|
||||||
type: "motion_mask" as PolygonType,
|
|
||||||
typeIndex: index,
|
|
||||||
camera: cameraConfig.name,
|
|
||||||
name: `Motion Mask ${index + 1}`,
|
|
||||||
objects: [],
|
|
||||||
points: interpolatePoints(
|
|
||||||
parseCoordinates(maskData),
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
scaledWidth,
|
|
||||||
scaledHeight,
|
|
||||||
),
|
|
||||||
isFinished: true,
|
|
||||||
color: [0, 0, 255],
|
|
||||||
}));
|
|
||||||
|
|
||||||
const globalObjectMasks = Object.entries(cameraConfig.objects.mask).map(
|
if (
|
||||||
([, maskData], index) => ({
|
cameraConfig.motion.mask !== null &&
|
||||||
|
cameraConfig.motion.mask !== undefined
|
||||||
|
) {
|
||||||
|
// this can be an array or a string
|
||||||
|
motionMasks = (
|
||||||
|
Array.isArray(cameraConfig.motion.mask)
|
||||||
|
? cameraConfig.motion.mask
|
||||||
|
: cameraConfig.motion.mask
|
||||||
|
? [cameraConfig.motion.mask]
|
||||||
|
: []
|
||||||
|
).map((maskData, index) => ({
|
||||||
|
type: "motion_mask" as PolygonType,
|
||||||
|
typeIndex: index,
|
||||||
|
camera: cameraConfig.name,
|
||||||
|
name: `Motion Mask ${index + 1}`,
|
||||||
|
objects: [],
|
||||||
|
points: interpolatePoints(
|
||||||
|
parseCoordinates(maskData),
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
scaledWidth,
|
||||||
|
scaledHeight,
|
||||||
|
),
|
||||||
|
isFinished: true,
|
||||||
|
color: [0, 0, 255],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalObjectMasksArray = Array.isArray(cameraConfig.objects.mask)
|
||||||
|
? cameraConfig.objects.mask
|
||||||
|
: cameraConfig.objects.mask
|
||||||
|
? [cameraConfig.objects.mask]
|
||||||
|
: [];
|
||||||
|
if (
|
||||||
|
cameraConfig.objects.mask !== null &&
|
||||||
|
cameraConfig.objects.mask !== undefined
|
||||||
|
) {
|
||||||
|
globalObjectMasks = globalObjectMasksArray.map((maskData, index) => ({
|
||||||
type: "object_mask" as PolygonType,
|
type: "object_mask" as PolygonType,
|
||||||
typeIndex: index,
|
typeIndex: index,
|
||||||
camera: cameraConfig.name,
|
camera: cameraConfig.name,
|
||||||
@ -342,41 +356,66 @@ export default function MasksAndZones({
|
|||||||
scaledHeight,
|
scaledHeight,
|
||||||
),
|
),
|
||||||
isFinished: true,
|
isFinished: true,
|
||||||
// isUnsaved: false,
|
color: [128, 128, 128],
|
||||||
color: [0, 0, 255],
|
}));
|
||||||
}),
|
}
|
||||||
);
|
|
||||||
|
// if (globalObjectMasks && !Array.isArray(globalObjectMasks)) {
|
||||||
|
// globalObjectMasks = [globalObjectMasks];
|
||||||
|
// }
|
||||||
|
|
||||||
|
console.log("global", globalObjectMasks);
|
||||||
|
|
||||||
const globalObjectMasksCount = globalObjectMasks.length;
|
const globalObjectMasksCount = globalObjectMasks.length;
|
||||||
|
|
||||||
const objectMasks = Object.entries(cameraConfig.objects.filters).flatMap(
|
console.log("filters", cameraConfig.objects.filters);
|
||||||
([objectName, { mask }]): Polygon[] =>
|
|
||||||
mask !== null && mask !== undefined
|
let index = 0;
|
||||||
? mask.flatMap((maskItem, subIndex) =>
|
objectMasks = Object.entries(cameraConfig.objects.filters)
|
||||||
maskItem !== null && maskItem !== undefined
|
.filter(([_, { mask }]) => mask || Array.isArray(mask))
|
||||||
? [
|
.flatMap(([objectName, { mask }]): Polygon[] => {
|
||||||
{
|
console.log("index", index);
|
||||||
type: "object_mask" as PolygonType,
|
console.log("outer", objectName, mask);
|
||||||
typeIndex: subIndex,
|
|
||||||
camera: cameraConfig.name,
|
const maskArray = Array.isArray(mask) ? mask : mask ? [mask] : [];
|
||||||
name: `Object Mask ${globalObjectMasksCount + subIndex + 1} (${objectName})`,
|
|
||||||
objects: [objectName],
|
return maskArray.flatMap((maskItem, subIndex) => {
|
||||||
points: interpolatePoints(
|
const maskItemString = maskItem;
|
||||||
parseCoordinates(maskItem),
|
|
||||||
1,
|
const newMask = {
|
||||||
1,
|
type: "object_mask" as PolygonType,
|
||||||
scaledWidth,
|
typeIndex: subIndex,
|
||||||
scaledHeight,
|
camera: cameraConfig.name,
|
||||||
),
|
name: `Object Mask ${globalObjectMasksCount + index + 1} (${objectName})`,
|
||||||
isFinished: true,
|
objects: [objectName],
|
||||||
// isUnsaved: false,
|
points: interpolatePoints(
|
||||||
color: [128, 128, 128],
|
parseCoordinates(maskItem),
|
||||||
},
|
1,
|
||||||
]
|
1,
|
||||||
: [],
|
scaledWidth,
|
||||||
|
scaledHeight,
|
||||||
|
),
|
||||||
|
isFinished: true,
|
||||||
|
color: [128, 128, 128],
|
||||||
|
};
|
||||||
|
index++;
|
||||||
|
|
||||||
|
if (
|
||||||
|
globalObjectMasksArray.some(
|
||||||
|
(globalMask) => globalMask === maskItemString,
|
||||||
)
|
)
|
||||||
: [],
|
) {
|
||||||
);
|
index--;
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
return [newMask];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(Object.entries(cameraConfig.objects.filters));
|
||||||
|
|
||||||
|
console.log("final object masks", objectMasks);
|
||||||
|
|
||||||
// console.log("setting all and editing");
|
// console.log("setting all and editing");
|
||||||
setAllPolygons([
|
setAllPolygons([
|
||||||
@ -404,9 +443,9 @@ export default function MasksAndZones({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [cameraConfig, containerRef, scaledHeight, scaledWidth]);
|
}, [cameraConfig, containerRef, scaledHeight, scaledWidth]);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
console.log("editing polygons changed:", editingPolygons);
|
// console.log("editing polygons changed:", editingPolygons);
|
||||||
}, [editingPolygons]);
|
// }, [editingPolygons]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editPane === undefined) {
|
if (editPane === undefined) {
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import {
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Toaster } from "../ui/sonner";
|
import { Toaster } from "../ui/sonner";
|
||||||
|
import ActivityIndicator from "../indicators/activity-indicator";
|
||||||
|
|
||||||
type MotionMaskEditPaneProps = {
|
type MotionMaskEditPaneProps = {
|
||||||
polygons?: Polygon[];
|
polygons?: Polygon[];
|
||||||
@ -105,7 +106,9 @@ export default function MotionMaskEditPane({
|
|||||||
|
|
||||||
let index = Array.isArray(cameraConfig.motion.mask)
|
let index = Array.isArray(cameraConfig.motion.mask)
|
||||||
? cameraConfig.motion.mask.length
|
? cameraConfig.motion.mask.length
|
||||||
: 1;
|
: cameraConfig.motion.mask
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
|
||||||
console.log("are we an array?", Array.isArray(cameraConfig.motion.mask));
|
console.log("are we an array?", Array.isArray(cameraConfig.motion.mask));
|
||||||
console.log("index", index);
|
console.log("index", index);
|
||||||
@ -114,20 +117,14 @@ export default function MotionMaskEditPane({
|
|||||||
// editing existing mask, not creating a new one
|
// editing existing mask, not creating a new one
|
||||||
if (editingMask) {
|
if (editingMask) {
|
||||||
index = polygon.typeIndex;
|
index = polygon.typeIndex;
|
||||||
if (polygon.name) {
|
console.log("editing, index", index);
|
||||||
const match = polygon.name.match(/\d+/);
|
|
||||||
if (match) {
|
|
||||||
// index = parseInt(match[0]) - 1;
|
|
||||||
console.log("editing, index", index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredMask = Array.isArray(cameraConfig.motion.mask)
|
const filteredMask = (
|
||||||
? cameraConfig.motion.mask
|
Array.isArray(cameraConfig.motion.mask)
|
||||||
: [cameraConfig.motion.mask].filter(
|
? cameraConfig.motion.mask
|
||||||
(_, currentIndex) => currentIndex !== index,
|
: [cameraConfig.motion.mask]
|
||||||
);
|
).filter((_, currentIndex) => currentIndex !== index);
|
||||||
console.log("filtered", filteredMask);
|
console.log("filtered", filteredMask);
|
||||||
|
|
||||||
// if (editingMask) {
|
// if (editingMask) {
|
||||||
@ -163,7 +160,7 @@ export default function MotionMaskEditPane({
|
|||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success(`Zone ${name} saved.`, {
|
toast.success(`${polygon.name || "Motion Mask"} has been saved.`, {
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
// setChangedValue(false);
|
// setChangedValue(false);
|
||||||
@ -274,8 +271,20 @@ export default function MotionMaskEditPane({
|
|||||||
<Button className="flex flex-1" onClick={onCancel}>
|
<Button className="flex flex-1" onClick={onCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="select" className="flex flex-1" type="submit">
|
<Button
|
||||||
Save
|
variant="select"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="flex flex-1"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<ActivityIndicator />
|
||||||
|
<span>Saving...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"Save"
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -19,20 +19,33 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { ATTRIBUTE_LABELS, FrigateConfig } from "@/types/frigateConfig";
|
import { ATTRIBUTE_LABELS, FrigateConfig } from "@/types/frigateConfig";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Polygon } from "@/types/canvas";
|
import { ObjectMaskFormValuesType, Polygon } from "@/types/canvas";
|
||||||
import PolygonEditControls from "./PolygonEditControls";
|
import PolygonEditControls from "./PolygonEditControls";
|
||||||
import { FaCheckCircle } from "react-icons/fa";
|
import { FaCheckCircle } from "react-icons/fa";
|
||||||
|
import {
|
||||||
|
flattenPoints,
|
||||||
|
interpolatePoints,
|
||||||
|
parseCoordinates,
|
||||||
|
} from "@/utils/canvasUtil";
|
||||||
|
import axios from "axios";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Toaster } from "../ui/sonner";
|
||||||
|
import ActivityIndicator from "../indicators/activity-indicator";
|
||||||
|
|
||||||
type ObjectMaskEditPaneProps = {
|
type ObjectMaskEditPaneProps = {
|
||||||
polygons?: Polygon[];
|
polygons?: Polygon[];
|
||||||
setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
|
setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
|
||||||
activePolygonIndex?: number;
|
activePolygonIndex?: number;
|
||||||
|
scaledWidth?: number;
|
||||||
|
scaledHeight?: number;
|
||||||
|
isLoading: boolean;
|
||||||
|
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
onSave?: () => void;
|
onSave?: () => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
};
|
};
|
||||||
@ -41,9 +54,15 @@ export default function ObjectMaskEditPane({
|
|||||||
polygons,
|
polygons,
|
||||||
setPolygons,
|
setPolygons,
|
||||||
activePolygonIndex,
|
activePolygonIndex,
|
||||||
|
scaledWidth,
|
||||||
|
scaledHeight,
|
||||||
|
isLoading,
|
||||||
|
setIsLoading,
|
||||||
onSave,
|
onSave,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: ObjectMaskEditPaneProps) {
|
}: ObjectMaskEditPaneProps) {
|
||||||
|
const { data: config, mutate: updateConfig } =
|
||||||
|
useSWR<FrigateConfig>("config");
|
||||||
// const { data: config } = useSWR<FrigateConfig>("config");
|
// const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
// const cameras = useMemo(() => {
|
// const cameras = useMemo(() => {
|
||||||
@ -64,6 +83,12 @@ export default function ObjectMaskEditPane({
|
|||||||
}
|
}
|
||||||
}, [polygons, activePolygonIndex]);
|
}, [polygons, activePolygonIndex]);
|
||||||
|
|
||||||
|
const cameraConfig = useMemo(() => {
|
||||||
|
if (polygon?.camera && config) {
|
||||||
|
return config.cameras[polygon.camera];
|
||||||
|
}
|
||||||
|
}, [polygon, config]);
|
||||||
|
|
||||||
const defaultName = useMemo(() => {
|
const defaultName = useMemo(() => {
|
||||||
if (!polygons) {
|
if (!polygons) {
|
||||||
return;
|
return;
|
||||||
@ -101,20 +126,157 @@ export default function ObjectMaskEditPane({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const saveToConfig = useCallback(
|
||||||
|
async (
|
||||||
|
{ objects: form_objects }: ObjectMaskFormValuesType, // values submitted via the form
|
||||||
|
objects: string[],
|
||||||
|
) => {
|
||||||
|
if (!scaledWidth || !scaledHeight || !polygon || !cameraConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// console.log("loitering time", loitering_time);
|
||||||
|
// const alertsZones = config?.cameras[camera]?.review.alerts.required_zones;
|
||||||
|
|
||||||
|
// const detectionsZones =
|
||||||
|
// config?.cameras[camera]?.review.detections.required_zones;
|
||||||
|
|
||||||
|
// console.log("out of try except", mutatedConfig);
|
||||||
|
|
||||||
|
console.log("form objects:", form_objects);
|
||||||
|
console.log("objects:", objects);
|
||||||
|
console.log(cameraConfig.objects.filters);
|
||||||
|
|
||||||
|
const coordinates = flattenPoints(
|
||||||
|
interpolatePoints(polygon.points, scaledWidth, scaledHeight, 1, 1),
|
||||||
|
).join(",");
|
||||||
|
|
||||||
|
let queryString = "";
|
||||||
|
let configObject;
|
||||||
|
let createFilter = false;
|
||||||
|
let globalMask = false;
|
||||||
|
let filteredMask = [coordinates];
|
||||||
|
const editingMask = polygon.name.length > 0;
|
||||||
|
|
||||||
|
// global mask on camera for all objects
|
||||||
|
if (form_objects == "all_labels") {
|
||||||
|
configObject = cameraConfig.objects.mask;
|
||||||
|
globalMask = true;
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
cameraConfig.objects.filters[form_objects] &&
|
||||||
|
cameraConfig.objects.filters[form_objects].mask !== null
|
||||||
|
) {
|
||||||
|
configObject = cameraConfig.objects.filters[form_objects].mask;
|
||||||
|
} else {
|
||||||
|
createFilter = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createFilter) {
|
||||||
|
let index = Array.isArray(configObject)
|
||||||
|
? configObject.length
|
||||||
|
: configObject
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (editingMask) {
|
||||||
|
index = polygon.typeIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("are we an array?", Array.isArray(configObject));
|
||||||
|
console.log("index", index);
|
||||||
|
|
||||||
|
// editing existing mask, not creating a new one
|
||||||
|
if (editingMask) {
|
||||||
|
index = polygon.typeIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredMask = (
|
||||||
|
Array.isArray(configObject) ? configObject : [configObject as string]
|
||||||
|
).filter((_, currentIndex) => currentIndex !== index);
|
||||||
|
|
||||||
|
console.log("filtered", filteredMask);
|
||||||
|
|
||||||
|
filteredMask.splice(index, 0, coordinates);
|
||||||
|
console.log("filtered after splice", filteredMask);
|
||||||
|
}
|
||||||
|
|
||||||
|
queryString = filteredMask
|
||||||
|
.map((pointsArray) => {
|
||||||
|
const coordinates = flattenPoints(parseCoordinates(pointsArray)).join(
|
||||||
|
",",
|
||||||
|
);
|
||||||
|
return globalMask
|
||||||
|
? `cameras.${polygon?.camera}.objects.mask=${coordinates}&`
|
||||||
|
: `cameras.${polygon?.camera}.objects.filters.${form_objects}.mask=${coordinates}&`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
console.log("polygon", polygon);
|
||||||
|
console.log(queryString);
|
||||||
|
|
||||||
|
// console.log(
|
||||||
|
// `config/set?cameras.${polygon?.camera}.objects.mask=${coordinates}&${queryString}`,
|
||||||
|
// );
|
||||||
|
// console.log("object masks", cameraConfig.objects.mask);
|
||||||
|
// console.log("new coords", coordinates);
|
||||||
|
// return;
|
||||||
|
|
||||||
|
if (!queryString) {
|
||||||
|
console.log("no query string");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
axios
|
||||||
|
.put(`config/set?${queryString}`, {
|
||||||
|
requires_restart: 0,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success(`${polygon.name || "Object Mask"} has been saved.`, {
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
|
// setChangedValue(false);
|
||||||
|
updateConfig();
|
||||||
|
} else {
|
||||||
|
toast.error(`Failed to save config changes: ${res.statusText}`, {
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(
|
||||||
|
`Failed to save config changes: ${error.response.data.message}`,
|
||||||
|
{ position: "top-center" },
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[updateConfig, polygon, scaledWidth, scaledHeight, setIsLoading],
|
||||||
|
);
|
||||||
|
|
||||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
console.log("form values", values);
|
if (activePolygonIndex === undefined || !values || !polygons) {
|
||||||
// if (activePolygonIndex === undefined || !polygons) {
|
return;
|
||||||
// return;
|
}
|
||||||
// }
|
setIsLoading(true);
|
||||||
|
// polygons[activePolygonIndex].name = values.name;
|
||||||
|
// console.log("form values", values);
|
||||||
|
// console.log(
|
||||||
|
// "string",
|
||||||
|
|
||||||
// const updatedPolygons = [...polygons];
|
// flattenPoints(
|
||||||
// const activePolygon = updatedPolygons[activePolygonIndex];
|
// interpolatePoints(polygon.points, scaledWidth, scaledHeight, 1, 1),
|
||||||
// updatedPolygons[activePolygonIndex] = {
|
// ).join(","),
|
||||||
// ...activePolygon,
|
// );
|
||||||
// name: defaultName ?? "foo",
|
// console.log("active polygon", polygons[activePolygonIndex]);
|
||||||
// };
|
|
||||||
// setPolygons(updatedPolygons);
|
|
||||||
|
|
||||||
|
saveToConfig(
|
||||||
|
values as ObjectMaskFormValuesType,
|
||||||
|
polygons[activePolygonIndex].objects,
|
||||||
|
);
|
||||||
if (onSave) {
|
if (onSave) {
|
||||||
onSave();
|
onSave();
|
||||||
}
|
}
|
||||||
@ -126,6 +288,7 @@ export default function ObjectMaskEditPane({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Toaster position="top-center" />
|
||||||
<Heading as="h3" className="my-2">
|
<Heading as="h3" className="my-2">
|
||||||
{polygon.name.length ? "Edit" : "New"} Object Mask
|
{polygon.name.length ? "Edit" : "New"} Object Mask
|
||||||
</Heading>
|
</Heading>
|
||||||
@ -211,8 +374,20 @@ export default function ObjectMaskEditPane({
|
|||||||
<Button className="flex flex-1" onClick={onCancel}>
|
<Button className="flex flex-1" onClick={onCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="select" className="flex flex-1" type="submit">
|
<Button
|
||||||
Save
|
variant="select"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="flex flex-1"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<ActivityIndicator />
|
||||||
|
<span>Saving...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"Save"
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -106,6 +106,7 @@ export default function PolygonDrawer({
|
|||||||
onMouseOver={isActive ? handleGroupMouseOver : undefined}
|
onMouseOver={isActive ? handleGroupMouseOver : undefined}
|
||||||
onTouchStart={isActive ? handleGroupMouseOver : undefined}
|
onTouchStart={isActive ? handleGroupMouseOver : undefined}
|
||||||
onMouseOut={isActive ? handleGroupMouseOut : undefined}
|
onMouseOut={isActive ? handleGroupMouseOut : undefined}
|
||||||
|
zIndex={isActive ? 999 : 100}
|
||||||
>
|
>
|
||||||
<Line
|
<Line
|
||||||
points={flattenedPoints}
|
points={flattenedPoints}
|
||||||
|
|||||||
@ -44,6 +44,10 @@ export default function PolygonEditControls({
|
|||||||
setPolygons(updatedPolygons);
|
setPolygons(updatedPolygons);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (activePolygonIndex === undefined || !polygons) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row justify-center gap-2">
|
<div className="flex flex-row justify-center gap-2">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@ -51,6 +55,7 @@ export default function PolygonEditControls({
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="size-6 p-1 rounded-md text-background bg-secondary-foreground"
|
className="size-6 p-1 rounded-md text-background bg-secondary-foreground"
|
||||||
|
disabled={!polygons[activePolygonIndex].points.length}
|
||||||
onClick={undo}
|
onClick={undo}
|
||||||
>
|
>
|
||||||
<MdUndo />
|
<MdUndo />
|
||||||
@ -63,6 +68,7 @@ export default function PolygonEditControls({
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="size-6 p-1 rounded-md text-background bg-secondary-foreground"
|
className="size-6 p-1 rounded-md text-background bg-secondary-foreground"
|
||||||
|
disabled={!polygons[activePolygonIndex].points.length}
|
||||||
onClick={reset}
|
onClick={reset}
|
||||||
>
|
>
|
||||||
<MdOutlineRestartAlt />
|
<MdOutlineRestartAlt />
|
||||||
|
|||||||
@ -37,7 +37,6 @@ import { reviewQueries } from "@/utils/zoneEdutUtil";
|
|||||||
type PolygonItemProps = {
|
type PolygonItemProps = {
|
||||||
polygon: Polygon;
|
polygon: Polygon;
|
||||||
setAllPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
|
setAllPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
|
||||||
setReindexPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
|
|
||||||
index: number;
|
index: number;
|
||||||
activePolygonIndex: number | undefined;
|
activePolygonIndex: number | undefined;
|
||||||
hoveredPolygonIndex: number | null;
|
hoveredPolygonIndex: number | null;
|
||||||
@ -95,13 +94,6 @@ export default function PolygonItem({
|
|||||||
}
|
}
|
||||||
if (polygon.type == "motion_mask") {
|
if (polygon.type == "motion_mask") {
|
||||||
console.log("deleting", polygon.typeIndex);
|
console.log("deleting", polygon.typeIndex);
|
||||||
if (polygon.name) {
|
|
||||||
const match = polygon.name.match(/\d+/);
|
|
||||||
if (match) {
|
|
||||||
// index = parseInt(match[0]) - 1;
|
|
||||||
console.log("deleting, index", polygon.typeIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredMask = (
|
const filteredMask = (
|
||||||
Array.isArray(cameraConfig.motion.mask)
|
Array.isArray(cameraConfig.motion.mask)
|
||||||
@ -118,11 +110,94 @@ export default function PolygonItem({
|
|||||||
return `cameras.${polygon?.camera}.motion.mask=${coordinates}&`;
|
return `cameras.${polygon?.camera}.motion.mask=${coordinates}&`;
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
// deleting last mask
|
||||||
|
url = `cameras.${polygon?.camera}.motion.mask&`;
|
||||||
|
}
|
||||||
console.log(url);
|
console.log(url);
|
||||||
|
|
||||||
// return;
|
// return;
|
||||||
// url = `config/set?cameras.${polygon.camera}.motion.mask`;
|
// url = `config/set?cameras.${polygon.camera}.motion.mask`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (polygon.type == "object_mask") {
|
||||||
|
console.log("deleting", polygon.typeIndex, polygon);
|
||||||
|
let configObject;
|
||||||
|
let globalMask = false;
|
||||||
|
console.log("polygon objects", polygon.objects, !polygon.objects);
|
||||||
|
|
||||||
|
// global mask on camera for all objects
|
||||||
|
if (!polygon.objects.length) {
|
||||||
|
console.log("deleting global");
|
||||||
|
configObject = cameraConfig.objects.mask;
|
||||||
|
globalMask = true;
|
||||||
|
} else {
|
||||||
|
configObject = cameraConfig.objects.filters[polygon.objects[0]].mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!configObject) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalObjectMasksArray = Array.isArray(cameraConfig.objects.mask)
|
||||||
|
? cameraConfig.objects.mask
|
||||||
|
: cameraConfig.objects.mask
|
||||||
|
? [cameraConfig.objects.mask]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
let filteredMask;
|
||||||
|
if (globalMask) {
|
||||||
|
filteredMask = (
|
||||||
|
Array.isArray(configObject) ? configObject : [configObject]
|
||||||
|
).filter((_, currentIndex) => currentIndex !== polygon.typeIndex);
|
||||||
|
} else {
|
||||||
|
console.log("not globals config object:", configObject);
|
||||||
|
|
||||||
|
filteredMask = (
|
||||||
|
Array.isArray(configObject) ? configObject : [configObject]
|
||||||
|
)
|
||||||
|
.filter((mask) => !globalObjectMasksArray.includes(mask))
|
||||||
|
.filter((_, currentIndex) => {
|
||||||
|
console.log(
|
||||||
|
"current index",
|
||||||
|
currentIndex,
|
||||||
|
"global length:",
|
||||||
|
globalObjectMasksArray.length,
|
||||||
|
"polygon typeindex",
|
||||||
|
polygon.typeIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
return currentIndex !== polygon.typeIndex;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("filtered:", filteredMask);
|
||||||
|
|
||||||
|
url = filteredMask
|
||||||
|
.map((pointsArray) => {
|
||||||
|
const coordinates = flattenPoints(
|
||||||
|
parseCoordinates(pointsArray),
|
||||||
|
).join(",");
|
||||||
|
return globalMask
|
||||||
|
? `cameras.${polygon?.camera}.objects.mask=${coordinates}&`
|
||||||
|
: `cameras.${polygon?.camera}.objects.filters.${polygon.objects[0]}.mask=${coordinates}&`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
// deleting last mask
|
||||||
|
url = globalMask
|
||||||
|
? `cameras.${polygon?.camera}.objects.mask&`
|
||||||
|
: `cameras.${polygon?.camera}.objects.filters.${polygon.objects[0]}.mask`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("url:", url);
|
||||||
|
|
||||||
|
// return;
|
||||||
|
// url = `config/set?cameras.${polygon.camera}.motion.mask`;
|
||||||
|
}
|
||||||
|
|
||||||
await axios
|
await axios
|
||||||
.put(`config/set?${url}`, { requires_restart: 0 })
|
.put(`config/set?${url}`, { requires_restart: 0 })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@ -169,15 +244,16 @@ export default function PolygonItem({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (type: string, typeIndex: number) => {
|
const handleDelete = (type: string, typeIndex: number) => {
|
||||||
setAllPolygons((oldPolygons) => {
|
// setAllPolygons((oldPolygons) => {
|
||||||
const filteredPolygons = oldPolygons.filter(
|
// console.log("old polygons", oldPolygons);
|
||||||
(polygon) =>
|
// const filteredPolygons = oldPolygons.filter(
|
||||||
!(polygon.type === type && polygon.typeIndex === typeIndex),
|
// (polygon) =>
|
||||||
);
|
// !(polygon.type === type && polygon.typeIndex === typeIndex),
|
||||||
console.log("filtered", filteredPolygons);
|
// );
|
||||||
// console.log("reindexed", reindexPolygons(filteredPolygons));
|
// console.log("filtered", filteredPolygons);
|
||||||
return filteredPolygons;
|
// // console.log("reindexed", reindexPolygons(filteredPolygons));
|
||||||
});
|
// return filteredPolygons;
|
||||||
|
// });
|
||||||
setActivePolygonIndex(undefined);
|
setActivePolygonIndex(undefined);
|
||||||
saveToConfig(polygon);
|
saveToConfig(polygon);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,11 +14,10 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { ATTRIBUTE_LABELS, FrigateConfig } from "@/types/frigateConfig";
|
import { ATTRIBUTE_LABELS, FrigateConfig } from "@/types/frigateConfig";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { isMobile } from "react-device-detect";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { FormValuesType, Polygon } from "@/types/canvas";
|
import { ZoneFormValuesType, Polygon } from "@/types/canvas";
|
||||||
import { reviewQueries } from "@/utils/zoneEdutUtil";
|
import { reviewQueries } from "@/utils/zoneEdutUtil";
|
||||||
import { Switch } from "../ui/switch";
|
import { Switch } from "../ui/switch";
|
||||||
import { Label } from "../ui/label";
|
import { Label } from "../ui/label";
|
||||||
@ -187,13 +186,13 @@ export default function ZoneEditPane({
|
|||||||
const saveToConfig = useCallback(
|
const saveToConfig = useCallback(
|
||||||
async (
|
async (
|
||||||
{
|
{
|
||||||
name,
|
name: zoneName,
|
||||||
inertia,
|
inertia,
|
||||||
loitering_time,
|
loitering_time,
|
||||||
objects: form_objects,
|
objects: form_objects,
|
||||||
review_alerts,
|
review_alerts,
|
||||||
review_detections,
|
review_detections,
|
||||||
}: FormValuesType, // values submitted via the form
|
}: ZoneFormValuesType, // values submitted via the form
|
||||||
objects: string[],
|
objects: string[],
|
||||||
) => {
|
) => {
|
||||||
if (!scaledWidth || !scaledHeight || !polygon) {
|
if (!scaledWidth || !scaledHeight || !polygon) {
|
||||||
@ -206,7 +205,7 @@ export default function ZoneEditPane({
|
|||||||
// config?.cameras[camera]?.review.detections.required_zones;
|
// config?.cameras[camera]?.review.detections.required_zones;
|
||||||
let mutatedConfig = config;
|
let mutatedConfig = config;
|
||||||
|
|
||||||
const renamingZone = name != polygon.name && polygon.name != "";
|
const renamingZone = zoneName != polygon.name && polygon.name != "";
|
||||||
|
|
||||||
if (renamingZone) {
|
if (renamingZone) {
|
||||||
// rename - delete old zone and replace with new
|
// rename - delete old zone and replace with new
|
||||||
@ -252,7 +251,7 @@ export default function ZoneEditPane({
|
|||||||
let objectQueries = objects
|
let objectQueries = objects
|
||||||
.map(
|
.map(
|
||||||
(object) =>
|
(object) =>
|
||||||
`&cameras.${polygon?.camera}.zones.${name}.objects=${object}`,
|
`&cameras.${polygon?.camera}.zones.${zoneName}.objects=${object}`,
|
||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
@ -265,11 +264,11 @@ export default function ZoneEditPane({
|
|||||||
// deleting objects
|
// deleting objects
|
||||||
if (!objectQueries && !same_objects && !renamingZone) {
|
if (!objectQueries && !same_objects && !renamingZone) {
|
||||||
// console.log("deleting objects");
|
// console.log("deleting objects");
|
||||||
objectQueries = `&cameras.${polygon?.camera}.zones.${name}.objects`;
|
objectQueries = `&cameras.${polygon?.camera}.zones.${zoneName}.objects`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { alertQueries, detectionQueries } = reviewQueries(
|
const { alertQueries, detectionQueries } = reviewQueries(
|
||||||
name,
|
zoneName,
|
||||||
review_alerts,
|
review_alerts,
|
||||||
review_detections,
|
review_detections,
|
||||||
polygon.camera,
|
polygon.camera,
|
||||||
@ -289,12 +288,12 @@ export default function ZoneEditPane({
|
|||||||
|
|
||||||
axios
|
axios
|
||||||
.put(
|
.put(
|
||||||
`config/set?cameras.${polygon?.camera}.zones.${name}.coordinates=${coordinates}&cameras.${polygon?.camera}.zones.${name}.inertia=${inertia}&cameras.${polygon?.camera}.zones.${name}.loitering_time=${loitering_time}${objectQueries}${alertQueries}${detectionQueries}`,
|
`config/set?cameras.${polygon?.camera}.zones.${zoneName}.coordinates=${coordinates}&cameras.${polygon?.camera}.zones.${zoneName}.inertia=${inertia}&cameras.${polygon?.camera}.zones.${zoneName}.loitering_time=${loitering_time}${objectQueries}${alertQueries}${detectionQueries}`,
|
||||||
{ requires_restart: 0 },
|
{ requires_restart: 0 },
|
||||||
)
|
)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success(`Zone ${name} saved.`, {
|
toast.success(`Zone (${zoneName}) has been saved.`, {
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
// setChangedValue(false);
|
// setChangedValue(false);
|
||||||
@ -343,7 +342,7 @@ export default function ZoneEditPane({
|
|||||||
// console.log("active polygon", polygons[activePolygonIndex]);
|
// console.log("active polygon", polygons[activePolygonIndex]);
|
||||||
|
|
||||||
saveToConfig(
|
saveToConfig(
|
||||||
values as FormValuesType,
|
values as ZoneFormValuesType,
|
||||||
polygons[activePolygonIndex].objects,
|
polygons[activePolygonIndex].objects,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -599,13 +598,13 @@ export function ZoneObjectSelector({
|
|||||||
|
|
||||||
const labels = new Set<string>();
|
const labels = new Set<string>();
|
||||||
|
|
||||||
Object.values(config.cameras).forEach((camera) => {
|
// Object.values(config.cameras).forEach((camera) => {
|
||||||
camera.objects.track.forEach((label) => {
|
// camera.objects.track.forEach((label) => {
|
||||||
if (!ATTRIBUTE_LABELS.includes(label)) {
|
// if (!ATTRIBUTE_LABELS.includes(label)) {
|
||||||
labels.add(label);
|
// labels.add(label);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
|
|
||||||
cameraConfig.objects.track.forEach((label) => {
|
cameraConfig.objects.track.forEach((label) => {
|
||||||
if (!ATTRIBUTE_LABELS.includes(label)) {
|
if (!ATTRIBUTE_LABELS.includes(label)) {
|
||||||
|
|||||||
@ -55,10 +55,8 @@ export default function Settings() {
|
|||||||
const [filterZoneMask, setFilterZoneMask] = useState<PolygonType[]>();
|
const [filterZoneMask, setFilterZoneMask] = useState<PolygonType[]>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cameras) {
|
if (cameras.length) {
|
||||||
// TODO: fixme
|
|
||||||
setSelectedCamera(cameras[0].name);
|
setSelectedCamera(cameras[0].name);
|
||||||
console.log("setting selected cam");
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -97,7 +95,6 @@ export default function Settings() {
|
|||||||
updateZoneMaskFilter={setFilterZoneMask}
|
updateZoneMaskFilter={setFilterZoneMask}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* {isEditing && page == "masks / zones" && (<PolygonEditControls /)} */}
|
|
||||||
<CameraSelectButton
|
<CameraSelectButton
|
||||||
allCameras={cameras}
|
allCameras={cameras}
|
||||||
selectedCamera={selectedCamera}
|
selectedCamera={selectedCamera}
|
||||||
@ -140,7 +137,7 @@ function CameraSelectButton({
|
|||||||
}: CameraSelectButtonProps) {
|
}: CameraSelectButtonProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
if (!allCameras) {
|
if (!allCameras.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export type Polygon = {
|
|||||||
color: number[];
|
color: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FormValuesType = {
|
export type ZoneFormValuesType = {
|
||||||
name: string;
|
name: string;
|
||||||
inertia: number;
|
inertia: number;
|
||||||
loitering_time: number;
|
loitering_time: number;
|
||||||
@ -21,3 +21,11 @@ export type FormValuesType = {
|
|||||||
review_alerts: boolean;
|
review_alerts: boolean;
|
||||||
review_detections: boolean;
|
review_detections: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ObjectMaskFormValuesType = {
|
||||||
|
objects: string;
|
||||||
|
polygon: {
|
||||||
|
isFinished: boolean;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@ -56,8 +56,8 @@ export const interpolatePoints = (
|
|||||||
const newPoints: number[][] = [];
|
const newPoints: number[][] = [];
|
||||||
|
|
||||||
for (const [x, y] of points) {
|
for (const [x, y] of points) {
|
||||||
const newX = (x * newWidth) / width;
|
const newX = +((x * newWidth) / width).toFixed(3);
|
||||||
const newY = (y * newHeight) / height;
|
const newY = +((y * newHeight) / height).toFixed(3);
|
||||||
newPoints.push([newX, newY]);
|
newPoints.push([newX, newY]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,3 +88,15 @@ export const toRGBColorString = (color: number[], darkened: boolean) => {
|
|||||||
|
|
||||||
return `rgba(${color[2]},${color[1]},${color[0]},${darkened ? "0.7" : "0.3"})`;
|
return `rgba(${color[2]},${color[1]},${color[0]},${darkened ? "0.7" : "0.3"})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const masksAreIdentical = (arr1: string[], arr2: string[]): boolean => {
|
||||||
|
if (arr1.length !== arr2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < arr1.length; i++) {
|
||||||
|
if (arr1[i] !== arr2[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user