Merge remote-tracking branch 'origin/main-demo' into main-dev
This commit is contained in:
@@ -3,7 +3,7 @@ import { CameraControls } from '@react-three/drei';
|
||||
import { ThreeEvent, useThree } from '@react-three/fiber';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import { useActiveTool, useDeletableFloorItem, useSelectedFloorItem, useToggleView, useZoneAssetId } from '../../../../../../store/builder/store';
|
||||
import { useActiveTool, useDeletableFloorItem, useResourceManagementId, useSelectedFloorItem, useToggleView, useZoneAssetId } from '../../../../../../store/builder/store';
|
||||
import useModuleStore, { useSubModuleStore } from '../../../../../../store/useModuleStore';
|
||||
import { useSocketStore } from '../../../../../../store/builder/store';
|
||||
import { useSceneContext } from '../../../../../scene/sceneContext';
|
||||
@@ -36,6 +36,7 @@ export function useModelEventHandlers({
|
||||
const { push3D } = undoRedo3DStore();
|
||||
const { getAssetById, removeAsset } = assetStore();
|
||||
const { zoneAssetId, setZoneAssetId } = useZoneAssetId();
|
||||
const { resourceManagementId, setResourceManagementId } = useResourceManagementId();
|
||||
const { removeEvent, getEventByModelUuid } = eventStore();
|
||||
const { getIsEventInProduct, addPoint, deleteEvent } = productStore();
|
||||
const { setSelectedAsset, clearSelectedAsset } = useSelectedAsset();
|
||||
@@ -74,7 +75,17 @@ export function useModelEventHandlers({
|
||||
if (zoneAssetId.id === asset.modelUuid) {
|
||||
handleDblClick(asset);
|
||||
}
|
||||
|
||||
}, [zoneAssetId])
|
||||
useEffect(() => {
|
||||
if (!resourceManagementId) return
|
||||
if (resourceManagementId === asset.modelUuid) {
|
||||
|
||||
|
||||
handleDblClick(asset);
|
||||
}
|
||||
|
||||
}, [resourceManagementId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedFloorItem) {
|
||||
@@ -83,10 +94,8 @@ export function useModelEventHandlers({
|
||||
}, [selectedFloorItem])
|
||||
|
||||
const handleDblClick = (asset: Asset) => {
|
||||
if (asset && activeTool === "cursor" && boundingBox && groupRef.current && activeModule === 'builder') {
|
||||
|
||||
if (asset && activeTool === "cursor" && boundingBox && groupRef.current && (activeModule === 'builder' || (activeModule === 'simulation' && resourceManagementId))) {
|
||||
const frontView = false;
|
||||
|
||||
if (frontView) {
|
||||
const size = boundingBox.getSize(new THREE.Vector3());
|
||||
const center = boundingBox.getCenter(new THREE.Vector3());
|
||||
@@ -107,6 +116,7 @@ export function useModelEventHandlers({
|
||||
paddingBottom: 5,
|
||||
paddingRight: 5,
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
const collisionPos = new THREE.Vector3();
|
||||
@@ -127,6 +137,7 @@ export function useModelEventHandlers({
|
||||
}
|
||||
|
||||
setSelectedFloorItem(groupRef.current);
|
||||
setResourceManagementId("");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ export default function Builder() {
|
||||
const { setHoveredPoint, setHoveredLine } = useBuilderStore();
|
||||
const { userId, organization } = getUserData();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!toggleView) {
|
||||
setHoveredLine(null);
|
||||
|
||||
@@ -1,222 +1,243 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useThree } from '@react-three/fiber';
|
||||
import { CameraControls, Html, ScreenSpace } from '@react-three/drei';
|
||||
import { useContextActionStore, useRenameModeStore, useSelectedAssets } from '../../../../store/builder/store';
|
||||
import ContextMenu from '../../../../components/ui/menu/contextMenu';
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import { CameraControls, Html, ScreenSpace } from "@react-three/drei";
|
||||
import {
|
||||
useContextActionStore,
|
||||
useRenameModeStore,
|
||||
useSelectedAssets,
|
||||
} from "../../../../store/builder/store";
|
||||
import ContextMenu from "../../../../components/ui/menu/contextMenu";
|
||||
|
||||
function ContextControls() {
|
||||
const { gl, controls } = useThree();
|
||||
const [canRender, setCanRender] = useState(false);
|
||||
const [visibility, setVisibility] = useState({ rename: true, focus: true, flipX: true, flipZ: true, move: true, rotate: true, duplicate: true, copy: true, paste: true, modifier: false, group: false, array: false, delete: true, });
|
||||
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
|
||||
const { selectedAssets } = useSelectedAssets();
|
||||
const { setContextAction } = useContextActionStore();
|
||||
const { setIsRenameMode } = useRenameModeStore();
|
||||
const rightDrag = useRef(false);
|
||||
const isRightMouseDown = useRef(false);
|
||||
const { gl, controls } = useThree();
|
||||
const [canRender, setCanRender] = useState(false);
|
||||
const [visibility, setVisibility] = useState({
|
||||
rename: true,
|
||||
focus: true,
|
||||
flipX: true,
|
||||
flipZ: true,
|
||||
move: true,
|
||||
rotate: true,
|
||||
duplicate: true,
|
||||
copy: true,
|
||||
paste: true,
|
||||
modifier: false,
|
||||
group: false,
|
||||
array: false,
|
||||
delete: true,
|
||||
});
|
||||
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
|
||||
const { selectedAssets } = useSelectedAssets();
|
||||
const { setContextAction } = useContextActionStore();
|
||||
const { setIsRenameMode } = useRenameModeStore();
|
||||
const rightDrag = useRef(false);
|
||||
const isRightMouseDown = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedAssets.length === 1) {
|
||||
setVisibility({
|
||||
rename: true,
|
||||
focus: true,
|
||||
flipX: true,
|
||||
flipZ: true,
|
||||
move: true,
|
||||
rotate: true,
|
||||
duplicate: true,
|
||||
copy: true,
|
||||
paste: true,
|
||||
modifier: false,
|
||||
group: false,
|
||||
array: false,
|
||||
delete: true,
|
||||
});
|
||||
} else if (selectedAssets.length > 1) {
|
||||
setVisibility({
|
||||
rename: false,
|
||||
focus: true,
|
||||
flipX: true,
|
||||
flipZ: true,
|
||||
move: true,
|
||||
rotate: true,
|
||||
duplicate: true,
|
||||
copy: true,
|
||||
paste: true,
|
||||
modifier: false,
|
||||
group: true,
|
||||
array: false,
|
||||
delete: true,
|
||||
});
|
||||
} else {
|
||||
setVisibility({
|
||||
rename: false,
|
||||
focus: false,
|
||||
flipX: false,
|
||||
flipZ: false,
|
||||
move: false,
|
||||
rotate: false,
|
||||
duplicate: false,
|
||||
copy: false,
|
||||
paste: false,
|
||||
modifier: false,
|
||||
group: false,
|
||||
array: false,
|
||||
delete: false,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (selectedAssets.length === 1) {
|
||||
setVisibility({
|
||||
rename: true,
|
||||
focus: true,
|
||||
flipX: true,
|
||||
flipZ: true,
|
||||
move: true,
|
||||
rotate: true,
|
||||
duplicate: true,
|
||||
copy: true,
|
||||
paste: true,
|
||||
modifier: false,
|
||||
group: false,
|
||||
array: false,
|
||||
delete: true,
|
||||
});
|
||||
} else if (selectedAssets.length > 1) {
|
||||
setVisibility({
|
||||
rename: false,
|
||||
focus: true,
|
||||
flipX: true,
|
||||
flipZ: true,
|
||||
move: true,
|
||||
rotate: true,
|
||||
duplicate: true,
|
||||
copy: true,
|
||||
paste: true,
|
||||
modifier: false,
|
||||
group: true,
|
||||
array: false,
|
||||
delete: true,
|
||||
});
|
||||
} else {
|
||||
setVisibility({
|
||||
rename: false,
|
||||
focus: false,
|
||||
flipX: false,
|
||||
flipZ: false,
|
||||
move: false,
|
||||
rotate: false,
|
||||
duplicate: false,
|
||||
copy: false,
|
||||
paste: false,
|
||||
modifier: false,
|
||||
group: false,
|
||||
array: false,
|
||||
delete: false,
|
||||
});
|
||||
}
|
||||
}, [selectedAssets]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvasElement = gl.domElement;
|
||||
|
||||
const onPointerDown = (evt: any) => {
|
||||
if (evt.button === 2) {
|
||||
isRightMouseDown.current = true;
|
||||
rightDrag.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerMove = () => {
|
||||
if (isRightMouseDown.current) {
|
||||
rightDrag.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerUp = (evt: any) => {
|
||||
if (evt.button === 2) {
|
||||
isRightMouseDown.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleContextClick = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
if (rightDrag.current) return;
|
||||
if (selectedAssets.length > 0) {
|
||||
setMenuPosition({
|
||||
x: event.clientX - gl.domElement.width / 2,
|
||||
y: event.clientY - gl.domElement.height / 2,
|
||||
});
|
||||
setCanRender(true);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = false;
|
||||
}
|
||||
}, [selectedAssets]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvasElement = gl.domElement;
|
||||
|
||||
const onPointerDown = (evt: any) => {
|
||||
if (evt.button === 2) {
|
||||
isRightMouseDown.current = true;
|
||||
rightDrag.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerMove = () => {
|
||||
if (isRightMouseDown.current) {
|
||||
rightDrag.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerUp = (evt: any) => {
|
||||
if (evt.button === 2) {
|
||||
isRightMouseDown.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleContextClick = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
if (rightDrag.current) return;
|
||||
if (selectedAssets.length > 0) {
|
||||
setMenuPosition({ x: event.clientX - gl.domElement.width / 2, y: event.clientY - gl.domElement.height / 2 });
|
||||
setCanRender(true);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = false;
|
||||
}
|
||||
} else {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (selectedAssets.length > 0) {
|
||||
canvasElement.addEventListener('pointerdown', onPointerDown);
|
||||
canvasElement.addEventListener('pointermove', onPointerMove);
|
||||
canvasElement.addEventListener('pointerup', onPointerUp);
|
||||
canvasElement.addEventListener('contextmenu', handleContextClick)
|
||||
} else {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
setMenuPosition({ x: 0, y: 0 });
|
||||
}
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener('pointerdown', onPointerDown);
|
||||
canvasElement.removeEventListener('pointermove', onPointerMove);
|
||||
canvasElement.removeEventListener('pointerup', onPointerUp);
|
||||
canvasElement.removeEventListener('contextmenu', handleContextClick);
|
||||
};
|
||||
}, [gl, selectedAssets]);
|
||||
|
||||
const handleAssetRename = () => {
|
||||
} else {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
setContextAction("renameAsset");
|
||||
setIsRenameMode(true);
|
||||
}
|
||||
const handleAssetFocus = () => {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
setContextAction("focusAsset");
|
||||
}
|
||||
const handleAssetMove = () => {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
setContextAction("moveAsset")
|
||||
}
|
||||
const handleAssetRotate = () => {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
setContextAction("rotateAsset")
|
||||
}
|
||||
const handleAssetCopy = () => {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
setContextAction("copyAsset")
|
||||
}
|
||||
const handleAssetPaste = () => {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
setContextAction("pasteAsset")
|
||||
}
|
||||
const handleAssetDelete = () => {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
setContextAction("deleteAsset")
|
||||
}
|
||||
const handleAssetDuplicate = () => {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
setContextAction("duplicateAsset")
|
||||
}
|
||||
};
|
||||
|
||||
if (selectedAssets.length > 0) {
|
||||
canvasElement.addEventListener("pointerdown", onPointerDown);
|
||||
canvasElement.addEventListener("pointermove", onPointerMove);
|
||||
canvasElement.addEventListener("pointerup", onPointerUp);
|
||||
canvasElement.addEventListener("contextmenu", handleContextClick);
|
||||
} else {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
setMenuPosition({ x: 0, y: 0 });
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{canRender && (
|
||||
<ScreenSpace depth={1} >
|
||||
<Html
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: menuPosition.y,
|
||||
left: menuPosition.x,
|
||||
zIndex: 1000
|
||||
}}
|
||||
>
|
||||
<ContextMenu
|
||||
visibility={visibility}
|
||||
onRename={() => handleAssetRename()}
|
||||
onFocus={() => handleAssetFocus()}
|
||||
onFlipX={() => console.log("Flip to X")}
|
||||
onFlipZ={() => console.log("Flip to Z")}
|
||||
onMove={() => handleAssetMove()}
|
||||
onRotate={() => handleAssetRotate()}
|
||||
onDuplicate={() => handleAssetDuplicate()}
|
||||
onCopy={() => handleAssetCopy()}
|
||||
onPaste={() => handleAssetPaste()}
|
||||
onGroup={() => console.log("Group")}
|
||||
onArray={() => console.log("Array")}
|
||||
onDelete={() => handleAssetDelete()}
|
||||
/>
|
||||
</Html>
|
||||
</ScreenSpace>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return () => {
|
||||
canvasElement.removeEventListener("pointerdown", onPointerDown);
|
||||
canvasElement.removeEventListener("pointermove", onPointerMove);
|
||||
canvasElement.removeEventListener("pointerup", onPointerUp);
|
||||
canvasElement.removeEventListener("contextmenu", handleContextClick);
|
||||
};
|
||||
}, [controls, gl, selectedAssets]);
|
||||
|
||||
const handleAssetRename = () => {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
setContextAction("renameAsset");
|
||||
setIsRenameMode(true);
|
||||
};
|
||||
const handleAssetFocus = () => {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
setContextAction("focusAsset");
|
||||
};
|
||||
const handleAssetMove = () => {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
setContextAction("moveAsset");
|
||||
};
|
||||
const handleAssetRotate = () => {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
setContextAction("rotateAsset");
|
||||
};
|
||||
const handleAssetCopy = () => {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
setContextAction("copyAsset");
|
||||
};
|
||||
const handleAssetPaste = () => {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
setContextAction("pasteAsset");
|
||||
};
|
||||
const handleAssetDelete = () => {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
setContextAction("deleteAsset");
|
||||
};
|
||||
const handleAssetDuplicate = () => {
|
||||
setCanRender(false);
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = true;
|
||||
}
|
||||
setContextAction("duplicateAsset");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{canRender && (
|
||||
<ScreenSpace depth={1}>
|
||||
<Html
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: menuPosition.y,
|
||||
left: menuPosition.x,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<ContextMenu
|
||||
visibility={visibility}
|
||||
onRename={() => handleAssetRename()}
|
||||
onFocus={() => handleAssetFocus()}
|
||||
onFlipX={() => console.log("Flip to X")}
|
||||
onFlipZ={() => console.log("Flip to Z")}
|
||||
onMove={() => handleAssetMove()}
|
||||
onRotate={() => handleAssetRotate()}
|
||||
onDuplicate={() => handleAssetDuplicate()}
|
||||
onCopy={() => handleAssetCopy()}
|
||||
onPaste={() => handleAssetPaste()}
|
||||
onGroup={() => console.log("Group")}
|
||||
onArray={() => console.log("Array")}
|
||||
onDelete={() => handleAssetDelete()}
|
||||
/>
|
||||
</Html>
|
||||
</ScreenSpace>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ContextControls;
|
||||
|
||||
@@ -18,6 +18,7 @@ import ContextControls from "./contextControls/contextControls";
|
||||
import SelectionControls2D from "./selectionControls/selection2D/selectionControls2D";
|
||||
import UndoRedo2DControls from "./undoRedoControls/undoRedo2D/undoRedo2DControls";
|
||||
import UndoRedo3DControls from "./undoRedoControls/undoRedo3D/undoRedo3DControls";
|
||||
import { useCameraShortcuts } from "../../../hooks/useCameraShortcuts";
|
||||
|
||||
export default function Controls() {
|
||||
const controlsRef = useRef<CameraControls>(null);
|
||||
@@ -116,6 +117,7 @@ export default function Controls() {
|
||||
stopInterval();
|
||||
};
|
||||
}, [toggleView, state, socket]);
|
||||
useCameraShortcuts(controlsRef);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -64,6 +64,7 @@ function MoveControls3D({ boundingBoxRef }: any) {
|
||||
setContextAction(null);
|
||||
moveAssets()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [contextAction])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -176,6 +177,7 @@ function MoveControls3D({ boundingBoxRef }: any) {
|
||||
canvasElement.removeEventListener("keydown", onKeyDown);
|
||||
canvasElement?.removeEventListener("keyup", onKeyUp);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [camera, controls, scene, toggleView, selectedAssets, socket, pastedObjects, duplicatedObjects, movedObjects, rotatedObjects, keyEvent, initialStates]);
|
||||
|
||||
const calculateDragOffset = useCallback((point: THREE.Object3D, hitPoint: THREE.Vector3) => {
|
||||
@@ -223,6 +225,7 @@ function MoveControls3D({ boundingBoxRef }: any) {
|
||||
setDragOffset(newOffset);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [axisConstraint, camera, movedObjects])
|
||||
|
||||
useFrame(() => {
|
||||
|
||||
@@ -67,6 +67,7 @@ const SelectionControls3D: React.FC = () => {
|
||||
setContextAction(null);
|
||||
deleteSelection()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [contextAction])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -222,12 +223,14 @@ const SelectionControls3D: React.FC = () => {
|
||||
helper.enabled = false;
|
||||
helper.dispose();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [camera, controls, scene, toggleView, selectedAssets, copiedObjects, pastedObjects, duplicatedObjects, movedObjects, socket, rotatedObjects, activeModule, toolMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeModule !== "builder" || toolMode !== 'cursor' || toggleView) {
|
||||
clearSelection();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeModule, toolMode, toggleView]);
|
||||
|
||||
const selectAssets = useCallback(() => {
|
||||
@@ -362,7 +365,7 @@ const SelectionControls3D: React.FC = () => {
|
||||
removeAsset(uuid);
|
||||
});
|
||||
|
||||
echo.success("Selected models removed!");
|
||||
echo.warn("Selected models removed!");
|
||||
clearSelection();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getAllProjects } from "../../services/dashboard/getAllProjects";
|
||||
import { getUserData } from "../../functions/getUserData";
|
||||
import { useLoadingProgress, useSocketStore } from "../../store/builder/store";
|
||||
import { Color, SRGBColorSpace } from "three";
|
||||
import { compressImage } from "../../utils/compressImage";
|
||||
|
||||
export default function Scene({ layout }: { readonly layout: "Main Layout" | "Comparison Layout"; }) {
|
||||
const map = useMemo(() => [
|
||||
@@ -31,23 +32,24 @@ export default function Scene({ layout }: { readonly layout: "Main Layout" | "Co
|
||||
const { loadingProgress } = useLoadingProgress();
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectId && loadingProgress > 1) return;
|
||||
if (!projectId || loadingProgress !== 0) return;
|
||||
getAllProjects(userId, organization).then((projects) => {
|
||||
if (!projects || !projects.Projects) return;
|
||||
let project = projects.Projects.find((val: any) => val.projectUuid === projectId || val._id === projectId);
|
||||
const canvas = document.getElementById("sceneCanvas")?.getElementsByTagName("canvas")[0];
|
||||
if (!canvas) return;
|
||||
const screenshotDataUrl = (canvas as HTMLCanvasElement)?.toDataURL("image/png");
|
||||
const updateProjects = {
|
||||
projectId: project?._id,
|
||||
organization,
|
||||
userId,
|
||||
projectName: project?.projectName,
|
||||
thumbnail: screenshotDataUrl,
|
||||
};
|
||||
if (projectSocket) {
|
||||
projectSocket.emit("v1:project:update", updateProjects);
|
||||
}
|
||||
compressImage((canvas as HTMLCanvasElement)?.toDataURL("image/png")).then((screenshotDataUrl) => {
|
||||
const updateProjects = {
|
||||
projectId: project?._id,
|
||||
organization,
|
||||
userId,
|
||||
projectName: project?.projectName,
|
||||
thumbnail: screenshotDataUrl,
|
||||
};
|
||||
if (projectSocket) {
|
||||
projectSocket.emit("v1:project:update", updateProjects);
|
||||
}
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
@@ -64,7 +66,7 @@ export default function Scene({ layout }: { readonly layout: "Main Layout" | "Co
|
||||
onContextMenu={(e) => { e.preventDefault(); }}
|
||||
performance={{ min: 0.9, max: 1.0 }}
|
||||
onCreated={(e) => { e.scene.background = layout === "Main Layout" ? null : new Color(0x19191d); }}
|
||||
gl={{ outputColorSpace: SRGBColorSpace, powerPreference: "high-performance", antialias: true }}
|
||||
gl={{ outputColorSpace: SRGBColorSpace, powerPreference: "high-performance", antialias: true, preserveDrawingBuffer: true }}
|
||||
>
|
||||
<Setup />
|
||||
<Collaboration />
|
||||
|
||||
@@ -1,241 +1,272 @@
|
||||
import * as THREE from "three";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useThree, useFrame } from "@react-three/fiber";
|
||||
import { useToolMode } from "../../../store/builder/store";
|
||||
import { Html } from "@react-three/drei";
|
||||
import { Html, Line } from "@react-three/drei";
|
||||
|
||||
const MeasurementTool = () => {
|
||||
const { gl, raycaster, pointer, camera, scene } = useThree();
|
||||
const { toolMode } = useToolMode();
|
||||
const { gl, raycaster, pointer, camera, scene } = useThree();
|
||||
const { toolMode } = useToolMode();
|
||||
|
||||
const [points, setPoints] = useState<THREE.Vector3[]>([]);
|
||||
const [tubeGeometry, setTubeGeometry] = useState<THREE.TubeGeometry | null>(
|
||||
null
|
||||
);
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const [startConePosition, setStartConePosition] =
|
||||
useState<THREE.Vector3 | null>(null);
|
||||
const [endConePosition, setEndConePosition] = useState<THREE.Vector3 | null>(
|
||||
null
|
||||
);
|
||||
const [startConeQuaternion, setStartConeQuaternion] = useState(
|
||||
new THREE.Quaternion()
|
||||
);
|
||||
const [endConeQuaternion, setEndConeQuaternion] = useState(
|
||||
new THREE.Quaternion()
|
||||
);
|
||||
const [coneSize, setConeSize] = useState({ radius: 0.2, height: 0.5 });
|
||||
const [points, setPoints] = useState<THREE.Vector3[]>([]);
|
||||
const [linePoints, setLinePoints] = useState<THREE.Vector3[] | null>(null);
|
||||
const [axisLock, setAxisLock] = useState<"x" | "y" | "z" | null>(null);
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const keysPressed = useRef<Set<string>>(new Set());
|
||||
|
||||
const MIN_RADIUS = 0.001, MAX_RADIUS = 0.1;
|
||||
const MIN_CONE_RADIUS = 0.01, MAX_CONE_RADIUS = 0.4;
|
||||
const MIN_CONE_HEIGHT = 0.035, MAX_CONE_HEIGHT = 2.0;
|
||||
// Axis lock key handling
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.altKey) {
|
||||
if (e.key.toLowerCase() === "x") keysPressed.current.add("x");
|
||||
else if (e.key.toLowerCase() === "y") keysPressed.current.add("y");
|
||||
else if (e.key.toLowerCase() === "z") keysPressed.current.add("z");
|
||||
|
||||
useEffect(() => {
|
||||
const canvasElement = gl.domElement;
|
||||
let drag = false;
|
||||
let isLeftMouseDown = false;
|
||||
|
||||
const onMouseDown = () => {
|
||||
isLeftMouseDown = true;
|
||||
drag = false;
|
||||
};
|
||||
|
||||
const onMouseUp = (evt: any) => {
|
||||
isLeftMouseDown = false;
|
||||
if (evt.button === 0 && !drag) {
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersects = raycaster
|
||||
.intersectObjects(scene.children, true)
|
||||
.filter(
|
||||
(intersect) =>
|
||||
!intersect.object.name.includes("Roof") &&
|
||||
!intersect.object.name.includes("MeasurementReference") &&
|
||||
!intersect.object.name.includes("agv-collider") &&
|
||||
!intersect.object.name.includes("zonePlane") &&
|
||||
!intersect.object.name.includes("SelectionGroup") &&
|
||||
!intersect.object.name.includes("selectionAssetGroup") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBoxLine") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBox") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
|
||||
intersect.object.type !== "GridHelper"
|
||||
);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const intersectionPoint = intersects[0].point.clone();
|
||||
if (points.length < 2) {
|
||||
setPoints([...points, intersectionPoint]);
|
||||
} else {
|
||||
setPoints([intersectionPoint]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseMove = () => {
|
||||
if (isLeftMouseDown) drag = true;
|
||||
};
|
||||
|
||||
const onContextMenu = (evt: any) => {
|
||||
evt.preventDefault();
|
||||
if (!drag) {
|
||||
evt.preventDefault();
|
||||
setPoints([]);
|
||||
setTubeGeometry(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (toolMode === "MeasurementScale") {
|
||||
canvasElement.addEventListener("pointerdown", onMouseDown);
|
||||
canvasElement.addEventListener("pointermove", onMouseMove);
|
||||
canvasElement.addEventListener("pointerup", onMouseUp);
|
||||
canvasElement.addEventListener("contextmenu", onContextMenu);
|
||||
} else {
|
||||
resetMeasurement();
|
||||
setPoints([]);
|
||||
}
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("pointerdown", onMouseDown);
|
||||
canvasElement.removeEventListener("pointermove", onMouseMove);
|
||||
canvasElement.removeEventListener("pointerup", onMouseUp);
|
||||
canvasElement.removeEventListener("contextmenu", onContextMenu);
|
||||
};
|
||||
}, [toolMode, camera, raycaster, pointer, scene, points]);
|
||||
|
||||
useFrame(() => {
|
||||
if (points.length === 1) {
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersects = raycaster
|
||||
.intersectObjects(scene.children, true)
|
||||
.filter(
|
||||
(intersect) =>
|
||||
!intersect.object.name.includes("Roof") &&
|
||||
!intersect.object.name.includes("MeasurementReference") &&
|
||||
!intersect.object.name.includes("agv-collider") &&
|
||||
!intersect.object.name.includes("zonePlane") &&
|
||||
!intersect.object.name.includes("SelectionGroup") &&
|
||||
!intersect.object.name.includes("selectionAssetGroup") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBoxLine") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBox") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
|
||||
intersect.object.type !== "GridHelper"
|
||||
);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
updateMeasurement(points[0], intersects[0].point);
|
||||
}
|
||||
} else if (points.length === 2) {
|
||||
updateMeasurement(points[0], points[1]);
|
||||
} else {
|
||||
resetMeasurement();
|
||||
}
|
||||
});
|
||||
|
||||
const updateMeasurement = (start: THREE.Vector3, end: THREE.Vector3) => {
|
||||
const distance = start.distanceTo(end);
|
||||
|
||||
const radius = THREE.MathUtils.clamp(distance * 0.02, MIN_RADIUS, MAX_RADIUS);
|
||||
const coneRadius = THREE.MathUtils.clamp(distance * 0.05, MIN_CONE_RADIUS, MAX_CONE_RADIUS);
|
||||
const coneHeight = THREE.MathUtils.clamp(distance * 0.2, MIN_CONE_HEIGHT, MAX_CONE_HEIGHT);
|
||||
|
||||
setConeSize({ radius: coneRadius, height: coneHeight });
|
||||
|
||||
const direction = new THREE.Vector3().subVectors(end, start).normalize();
|
||||
|
||||
const offset = direction.clone().multiplyScalar(coneHeight * 0.5);
|
||||
|
||||
let tubeStart = start.clone().add(offset);
|
||||
let tubeEnd = end.clone().sub(offset);
|
||||
|
||||
tubeStart.y = Math.max(tubeStart.y, 0);
|
||||
tubeEnd.y = Math.max(tubeEnd.y, 0);
|
||||
|
||||
const curve = new THREE.CatmullRomCurve3([tubeStart, tubeEnd]);
|
||||
setTubeGeometry(new THREE.TubeGeometry(curve, 20, radius, 8, false));
|
||||
|
||||
setStartConePosition(tubeStart);
|
||||
setEndConePosition(tubeEnd);
|
||||
setStartConeQuaternion(getArrowOrientation(start, end));
|
||||
setEndConeQuaternion(getArrowOrientation(end, start));
|
||||
if (keysPressed.current.has("x")) setAxisLock("x");
|
||||
else if (keysPressed.current.has("y")) setAxisLock("y");
|
||||
else if (keysPressed.current.has("z")) setAxisLock("z");
|
||||
} else if (e.key === "Escape") {
|
||||
setPoints([]);
|
||||
setLinePoints(null);
|
||||
setAxisLock(null);
|
||||
}
|
||||
};
|
||||
|
||||
const resetMeasurement = () => {
|
||||
setTubeGeometry(null);
|
||||
setStartConePosition(null);
|
||||
setEndConePosition(null);
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
keysPressed.current.delete(e.key.toLowerCase());
|
||||
if (keysPressed.current.has("x")) setAxisLock("x");
|
||||
else if (keysPressed.current.has("y")) setAxisLock("y");
|
||||
else if (keysPressed.current.has("z")) setAxisLock("z");
|
||||
else setAxisLock(null);
|
||||
};
|
||||
|
||||
const getArrowOrientation = (start: THREE.Vector3, end: THREE.Vector3) => {
|
||||
const direction = new THREE.Vector3()
|
||||
.subVectors(end, start)
|
||||
.normalize()
|
||||
.negate();
|
||||
const quaternion = new THREE.Quaternion();
|
||||
quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction);
|
||||
return quaternion;
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
window.removeEventListener("keyup", handleKeyUp);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getLineColor = useCallback(() => {
|
||||
switch (axisLock) {
|
||||
case "x":
|
||||
return "#d94522"; // Red for X axis
|
||||
case "y":
|
||||
return "#22ab2e"; // Green for Y axis
|
||||
case "z":
|
||||
return "#227bd9"; // Blue for Z axis
|
||||
default:
|
||||
return "#b18ef1"; // Default purple
|
||||
}
|
||||
}, [axisLock]);
|
||||
|
||||
// Apply axis lock to a point
|
||||
const applyAxisLock = useCallback(
|
||||
(point: THREE.Vector3, referencePoint: THREE.Vector3) => {
|
||||
const lockedPoint = point.clone();
|
||||
|
||||
switch (axisLock) {
|
||||
case "x":
|
||||
lockedPoint.y = referencePoint.y;
|
||||
lockedPoint.z = referencePoint.z;
|
||||
break;
|
||||
case "y":
|
||||
lockedPoint.x = referencePoint.x;
|
||||
lockedPoint.z = referencePoint.z;
|
||||
break;
|
||||
case "z":
|
||||
lockedPoint.x = referencePoint.x;
|
||||
lockedPoint.y = referencePoint.y;
|
||||
break;
|
||||
}
|
||||
|
||||
return lockedPoint;
|
||||
},
|
||||
[axisLock]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const canvasElement = gl.domElement;
|
||||
let drag = false;
|
||||
let isLeftMouseDown = false;
|
||||
|
||||
const onMouseDown = () => {
|
||||
isLeftMouseDown = true;
|
||||
drag = false;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (points.length === 2) {
|
||||
// console.log(points[0].distanceTo(points[1]));
|
||||
const onMouseUp = (evt: any) => {
|
||||
isLeftMouseDown = false;
|
||||
if (evt.button === 0 && !drag) {
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersects = raycaster
|
||||
.intersectObjects(scene.children, true)
|
||||
.filter(
|
||||
(intersect) =>
|
||||
!intersect.object.name.includes("Roof") &&
|
||||
!intersect.object.name.includes("MeasurementReference") &&
|
||||
!intersect.object.name.includes("agv-collider") &&
|
||||
!intersect.object.name.includes("zonePlane") &&
|
||||
!intersect.object.name.includes("SelectionGroup") &&
|
||||
!intersect.object.name.includes("selectionAssetGroup") &&
|
||||
!intersect.object.name.includes(
|
||||
"SelectionGroupBoundingBoxLine"
|
||||
) &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBox") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
|
||||
intersect.object.type !== "GridHelper"
|
||||
);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
let intersectionPoint = intersects[0].point.clone();
|
||||
if (axisLock && points.length > 0) {
|
||||
intersectionPoint = applyAxisLock(
|
||||
intersectionPoint,
|
||||
points[points.length - 1]
|
||||
);
|
||||
}
|
||||
if (points.length < 2) {
|
||||
setPoints([...points, intersectionPoint]);
|
||||
} else {
|
||||
setPoints([intersectionPoint]);
|
||||
}
|
||||
}
|
||||
}, [points]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<group ref={groupRef} name="MeasurementGroup">
|
||||
{startConePosition && (
|
||||
<mesh
|
||||
name="MeasurementReference"
|
||||
position={startConePosition}
|
||||
quaternion={startConeQuaternion}
|
||||
>
|
||||
<coneGeometry args={[coneSize.radius, coneSize.height, 16]} />
|
||||
<meshBasicMaterial color="yellow" />
|
||||
</mesh>
|
||||
)}
|
||||
{endConePosition && (
|
||||
<mesh
|
||||
name="MeasurementReference"
|
||||
position={endConePosition}
|
||||
quaternion={endConeQuaternion}
|
||||
>
|
||||
<coneGeometry args={[coneSize.radius, coneSize.height, 16]} />
|
||||
<meshBasicMaterial color="yellow" />
|
||||
</mesh>
|
||||
)}
|
||||
{tubeGeometry && (
|
||||
<mesh name="MeasurementReference" geometry={tubeGeometry}>
|
||||
<meshBasicMaterial color="yellow" />
|
||||
</mesh>
|
||||
)}
|
||||
const onMouseMove = () => {
|
||||
if (isLeftMouseDown) drag = true;
|
||||
};
|
||||
|
||||
{startConePosition && endConePosition && (
|
||||
<Html
|
||||
scale={THREE.MathUtils.clamp(
|
||||
startConePosition.distanceTo(endConePosition) * 0.25,
|
||||
0,
|
||||
10
|
||||
)}
|
||||
position={[
|
||||
(startConePosition.x + endConePosition.x) / 2,
|
||||
(startConePosition.y + endConePosition.y) / 2,
|
||||
(startConePosition.z + endConePosition.z) / 2,
|
||||
]}
|
||||
// class
|
||||
wrapperClass="distance-text-wrapper"
|
||||
className="distance-text"
|
||||
// other
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
sprite
|
||||
>
|
||||
<div>
|
||||
{(startConePosition.distanceTo(endConePosition) + (coneSize.height)).toFixed(2)} m
|
||||
</div>
|
||||
</Html>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
const onContextMenu = (evt: any) => {
|
||||
evt.preventDefault();
|
||||
if (!drag) {
|
||||
setPoints([]);
|
||||
setLinePoints(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (toolMode === "MeasurementScale") {
|
||||
canvasElement.addEventListener("pointerdown", onMouseDown);
|
||||
canvasElement.addEventListener("pointermove", onMouseMove);
|
||||
canvasElement.addEventListener("pointerup", onMouseUp);
|
||||
canvasElement.addEventListener("contextmenu", onContextMenu);
|
||||
} else {
|
||||
setPoints([]);
|
||||
setLinePoints(null);
|
||||
}
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("pointerdown", onMouseDown);
|
||||
canvasElement.removeEventListener("pointermove", onMouseMove);
|
||||
canvasElement.removeEventListener("pointerup", onMouseUp);
|
||||
canvasElement.removeEventListener("contextmenu", onContextMenu);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [toolMode, camera, raycaster, pointer, scene, points, axisLock]);
|
||||
|
||||
useFrame(() => {
|
||||
if (points.length === 1) {
|
||||
// live preview for second point
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersects = raycaster
|
||||
.intersectObjects(scene.children, true)
|
||||
.filter(
|
||||
(intersect) =>
|
||||
!intersect.object.name.includes("Roof") &&
|
||||
!intersect.object.name.includes("MeasurementReference") &&
|
||||
!intersect.object.name.includes("agv-collider") &&
|
||||
!intersect.object.name.includes("zonePlane") &&
|
||||
!intersect.object.name.includes("SelectionGroup") &&
|
||||
!intersect.object.name.includes("selectionAssetGroup") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBoxLine") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBox") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
|
||||
intersect.object.type !== "GridHelper"
|
||||
);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
let tempEnd = intersects[0].point.clone();
|
||||
if (axisLock) {
|
||||
tempEnd = applyAxisLock(tempEnd, points[0]);
|
||||
}
|
||||
updateMeasurement(points[0], tempEnd);
|
||||
}
|
||||
} else if (points.length === 2) {
|
||||
// second point already fixed
|
||||
updateMeasurement(points[0], points[1]);
|
||||
} else {
|
||||
setLinePoints(null);
|
||||
}
|
||||
});
|
||||
|
||||
const updateMeasurement = (start: THREE.Vector3, end: THREE.Vector3) => {
|
||||
setLinePoints([start.clone(), end.clone()]);
|
||||
};
|
||||
|
||||
return (
|
||||
<group ref={groupRef} name="MeasurementGroup">
|
||||
{linePoints && (
|
||||
<>
|
||||
{/* Outline line */}
|
||||
<Line
|
||||
points={linePoints}
|
||||
color="black"
|
||||
lineWidth={6} // thicker than main line
|
||||
depthTest={false}
|
||||
depthWrite={false}
|
||||
renderOrder={998} // render behind main line
|
||||
/>
|
||||
|
||||
{/* Main line */}
|
||||
<Line
|
||||
points={linePoints}
|
||||
color={getLineColor()}
|
||||
lineWidth={2} // actual line width
|
||||
depthTest={false}
|
||||
depthWrite={false}
|
||||
transparent={false}
|
||||
opacity={1}
|
||||
renderOrder={999} // render on top
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{points.map((point, index) => (
|
||||
<Html
|
||||
key={index}
|
||||
position={point}
|
||||
scale={0.5}
|
||||
wrapperClass="measurement-label-wrapper"
|
||||
className="measurement-label"
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
sprite
|
||||
>
|
||||
<div className="measurement-point"></div>
|
||||
</Html>
|
||||
))}
|
||||
|
||||
{linePoints && linePoints.length === 2 && (
|
||||
<Html
|
||||
position={[
|
||||
(linePoints[0].x + linePoints[1].x) / 2,
|
||||
(linePoints[0].y + linePoints[1].y) / 2,
|
||||
(linePoints[0].z + linePoints[1].z) / 2,
|
||||
]}
|
||||
scale={0.5}
|
||||
wrapperClass="distance-text-wrapper"
|
||||
className="distance-text"
|
||||
zIndexRange={[2, 1]}
|
||||
prepend
|
||||
sprite
|
||||
>
|
||||
<div>{linePoints[0].distanceTo(linePoints[1]).toFixed(2)} m</div>
|
||||
</Html>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
export default MeasurementTool;
|
||||
|
||||
@@ -241,21 +241,18 @@ const DisplayZone: React.FC<DisplayZoneProps> = ({
|
||||
{Object.keys(zonesData).length !== 0 ? (
|
||||
<>
|
||||
{Object.values(zonesData).map((zone, index) => (
|
||||
<>
|
||||
{ }
|
||||
<div
|
||||
key={index}
|
||||
className={`zone ${selectedZone.zoneUuid === zone.zoneUuid ? "active" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
<div
|
||||
key={`${index}_${zone.zoneName}`}
|
||||
className={`zone ${selectedZone.zoneUuid === zone.zoneUuid ? "active" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
|
||||
handleSelect2dZoneData(zonesData[zone.zoneUuid]?.zoneUuid, zone.zoneName)
|
||||
}
|
||||
}
|
||||
>
|
||||
{zone.zoneName}
|
||||
</div>
|
||||
</>
|
||||
handleSelect2dZoneData(zonesData[zone.zoneUuid]?.zoneUuid, zone.zoneName)
|
||||
}
|
||||
}
|
||||
>
|
||||
{zone.zoneName}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user