This commit is contained in:
Nalvazhuthi
2025-10-17 15:34:20 +05:30
29 changed files with 512 additions and 639 deletions

View File

@@ -3,6 +3,7 @@ import React, { useState, useRef, useEffect } from "react";
import { dataModelManager } from "./data/dataModel";
import ControlPanel from "./ControlPanel";
import SwapModal from "./SwapModal";
import { Block } from "../../types/exportedTypes";
import DataModelPanel from "./components/models/DataModelPanel";
import { useSceneContext } from "../../modules/scene/sceneContext";
@@ -77,23 +78,35 @@ const DashboardEditor: React.FC = () => {
const currentBlock = blocks.find((b) => b.blockUuid === selectedBlock);
const currentElement = currentBlock?.elements.find((el) => el.elementUuid === selectedElement);
useEffect(() => {
console.log("blocks: ", blocks);
}, [blocks]);
useEffect(() => {
if (!projectId || !selectedVersion) return;
// getDashBoardBlocksApi(projectId, selectedVersion.versionId).then((data) => {
// if (data.data && data.data.blocks) {
// console.log("data: ", data);
// setBlocks(data.data.blocks);
// }
// });
getDashBoardBlocksApi(projectId, selectedVersion.versionId).then((data) => {
if (data.data?.blocks) {
console.log("data.data.blocks: ", data.data.blocks);
setBlocks(data.data.blocks);
}
});
}, [projectId, selectedVersion]);
const updateBackend = (blocks: Block[]) => {
if (!projectId || !selectedVersion) return;
upsetDashBoardBlocksApi({ projectId, versionId: selectedVersion.versionId, blocks }).then((data) => {
if (data.data?.blocks) {
console.log("data.data.blocks: ", data.data.blocks);
setBlocks(data.data.blocks);
}
});
};
useEffect(() => {
const unsubscribe = subscribe(() => {
if (!projectId || !selectedVersion) return;
// upsetDashBoardBlocksApi({ projectId, versionId: selectedVersion.versionId, blocks }).then((data) => {
// console.log("data: ", data);
// });
updateBackend(blocks);
});
return () => {
@@ -103,10 +116,11 @@ const DashboardEditor: React.FC = () => {
useCallBackOnKey(
() => {
console.log(blocks);
if (!projectId || !selectedVersion) return;
updateBackend(blocks);
},
"Ctrl+S",
{ dependencies: [blocks], noRepeat: true, allowOnInput: true }
{ dependencies: [blocks, projectId, selectedVersion], noRepeat: true, allowOnInput: true }
);
// Subscribe to data model changes
@@ -118,27 +132,27 @@ const DashboardEditor: React.FC = () => {
const keys = dataModelManager.getAvailableKeys();
const subscriptions: Array<[string, () => void]> = [];
keys.forEach((key) => {
for (const key of keys) {
const callback = () => handleDataChange();
dataModelManager.subscribe(key, callback);
subscriptions.push([key, callback]);
});
}
const interval = setInterval(() => {
const currentKeys = dataModelManager.getAvailableKeys();
const newKeys = currentKeys.filter((key) => !keys.includes(key));
newKeys.forEach((key) => {
for (const key of newKeys) {
const callback = () => handleDataChange();
dataModelManager.subscribe(key, callback);
subscriptions.push([key, callback]);
});
}
}, 1000);
return () => {
subscriptions.forEach(([key, callback]) => {
for (const [key, callback] of subscriptions) {
dataModelManager.unsubscribe(key, callback);
});
}
clearInterval(interval);
};
}, []);
@@ -239,7 +253,7 @@ const DashboardEditor: React.FC = () => {
const deltaY = e.clientY - resizeStart.y;
const currentBlock = blocks.find((b) => b.blockUuid === resizingBlock);
const minSize = currentBlock ? calculateMinBlockSize(currentBlock) : { width: 300, height: 200 };
const minSize = currentBlock ? calculateMinBlockSize(currentBlock) : { width: 100, height: 50 };
const newWidth = Math.max(minSize.width, resizeStart.width + deltaX);
const newHeight = Math.max(minSize.height, resizeStart.height + deltaY);

View File

@@ -1,8 +1,8 @@
import type { Block } from "../../../../types/exportedTypes";
export const calculateMinBlockSize = (block: Block): Size => {
let minWidth = 300;
let minHeight = 200;
let minWidth = 100;
let minHeight = 50;
block.elements.forEach((element) => {
if (element.positionType === "absolute") {

View File

@@ -30,7 +30,6 @@ export const handleBlockDragStart = (
setDraggingBlock: (blockId: string | null) => void,
setBlockDragOffset: (offset: Position) => void // Change to specific offset
): void => {
console.log("Block drag start:", blockId);
setDraggingBlock(blockId);
const element = event.currentTarget as HTMLElement;
const rect = element.getBoundingClientRect();

View File

@@ -1,243 +0,0 @@
import * as THREE from "three";
import { useEffect, useMemo, useRef, useCallback } from "react";
import { useThree } from "@react-three/fiber";
import { useHeatMapStore } from "../../../store/simulation/useHeatMapStore";
import * as CONSTANTS from "../../../types/world/worldConstants";
import { usePlayButtonStore } from "../../../store/ui/usePlayButtonStore";
import { useSceneContext } from "../../../modules/scene/sceneContext";
const RADIUS = 0.0025;
const OPACITY = 0.8;
const GROWTH_RATE = 20.0;
const BakedHeatMap = () => {
const { materialStore } = useSceneContext();
const { bakedPoints } = useHeatMapStore();
const materialRef = useRef<THREE.ShaderMaterial>(null);
const meshRef = useRef<THREE.Mesh>(null);
const { gl } = useThree();
const { isPlaying } = usePlayButtonStore();
const height = CONSTANTS.gridConfig.size;
const width = CONSTANTS.gridConfig.size;
const { materialHistory, materials } = materialStore();
const createPointTexture = useCallback(
(filteredPoints: typeof bakedPoints) => {
if (filteredPoints.length === 0) return null;
const data = new Float32Array(filteredPoints.length * 4);
filteredPoints.forEach((p, i) => {
const index = i * 4;
data[index] = (p.points.x + width / 2) / width;
data[index + 1] = (p.points.y + height / 2) / height;
data[index + 2] = 0.3;
data[index + 3] = 0.0;
});
const texture = new THREE.DataTexture(
data,
filteredPoints.length,
1,
THREE.RGBAFormat,
THREE.FloatType
);
texture.needsUpdate = true;
return texture;
},
[width, height]
);
const uniformsRef = useRef({
u_points: { value: null as THREE.DataTexture | null },
u_count: { value: 0 },
u_radius: { value: RADIUS },
u_opacity: { value: OPACITY },
u_growthRate: { value: GROWTH_RATE },
});
const renderHeatmapToImage = useCallback(
(type: string) => {
if (!meshRef.current) return null;
const filteredPoints = bakedPoints.filter((p) => p.type === type);
if (filteredPoints.length === 0) return null;
const pointTexture = createPointTexture(filteredPoints);
if (!pointTexture) return null;
uniformsRef.current.u_points.value = pointTexture;
uniformsRef.current.u_count.value = filteredPoints.length;
const exportCamera = new THREE.OrthographicCamera(
width / -2,
width / 2,
height / 2,
height / -2,
0.1,
10
);
exportCamera.position.set(0, 1, 0);
exportCamera.lookAt(0, 0, 0);
const renderTarget = new THREE.WebGLRenderTarget(1024, 1024, {
format: THREE.RGBAFormat,
type: THREE.UnsignedByteType,
});
const tempScene = new THREE.Scene();
tempScene.add(meshRef.current);
gl.setRenderTarget(renderTarget);
gl.render(tempScene, exportCamera);
gl.setRenderTarget(null);
const pixels = new Uint8Array(1024 * 1024 * 4);
gl.readRenderTargetPixels(renderTarget, 0, 0, 1024, 1024, pixels);
const canvas = document.createElement("canvas");
canvas.width = 1024;
canvas.height = 1024;
const ctx = canvas.getContext("2d");
if (ctx) {
const imageData = ctx.createImageData(1024, 1024);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
return canvas.toDataURL("image/png");
}
return null;
},
[gl, width, height, bakedPoints, createPointTexture]
);
// const downloadImage = (base64: string, filename: string) => {
// const link = document.createElement("a");
// link.href = base64;
// link.download = filename;
// document.body.appendChild(link);
// link.click();
// document.body.removeChild(link);
// };
const exportHeatmapAsPNG = useCallback(() => {
const types = ["human", "vehicle"];
const result = types.map((type) => {
const image = renderHeatmapToImage(type);
// console.log("image: ", type, image);
if (image) {
// downloadImage(image, `${type}-heatmap.png`);
}
return { type, image };
});
// console.log("Exported Heatmaps:", result);
return result;
}, [renderHeatmapToImage]);
useEffect(() => {
if (materials.length === 0 && materialHistory.length >= 0) {
// console.log("SimulationCompleted");
const getImage = exportHeatmapAsPNG();
// console.log("getImage: ", getImage);
}
}, [isPlaying, materials, materialHistory]);
const pointTexture = useMemo(
() => createPointTexture(bakedPoints),
[bakedPoints, createPointTexture]
);
useEffect(() => {
uniformsRef.current.u_points.value = pointTexture;
uniformsRef.current.u_count.value = bakedPoints.length;
}, [pointTexture, bakedPoints.length]);
return (
<>
<mesh ref={meshRef} rotation={[Math.PI / 2, 0, 0]} position={[0, 0.025, 0]}>
<planeGeometry args={[width, height]} />
<shaderMaterial
ref={materialRef}
transparent
depthWrite={false}
blending={THREE.AdditiveBlending}
uniforms={uniformsRef.current}
side={THREE.DoubleSide}
vertexShader={`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`}
fragmentShader={`
uniform sampler2D u_points;
precision highp float;
uniform int u_count;
uniform float u_radius;
uniform float u_opacity;
uniform float u_growthRate;
varying vec2 vUv;
float gauss(float dist, float radius) {
return exp(-pow(dist / radius, 2.0));
}
void main() {
float intensity = 0.0;
for (int i = 0; i < 10000; i++) {
if (i >= u_count) break;
float fi = float(i) + 0.5;
float u = fi / float(u_count);
vec4 point = texture2D(u_points, vec2(u, 0.5));
vec2 pos = point.rg;
float strength = point.b;
float d = distance(vUv, pos);
intensity += strength * gauss(d, u_radius);
}
float normalized = clamp(intensity / max(u_growthRate, 0.0001), 0.0, 1.0);
vec3 color = vec3(0.0);
if (normalized < 0.33) {
color = mix(vec3(0.0, 0.0, 1.0), vec3(0.0, 1.0, 0.0), normalized / 0.33);
} else if (normalized < 0.66) {
color = mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 1.0, 0.0), (normalized - 0.33) / 0.33);
} else {
color = mix(vec3(1.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), (normalized - 0.66) / 0.34);
}
gl_FragColor = vec4(color, normalized * u_opacity);
}
`}
/>
</mesh>
{/* <Html>
<button
style={{
position: "absolute",
top: "10px",
left: "10px",
padding: "8px 12px",
background: "#333",
color: "white",
border: "none",
cursor: "pointer",
zIndex: 10,
}}
onClick={exportHeatmapAsPNG}
>
Export Heatmap
</button>
</Html> */}
</>
);
};
export default BakedHeatMap;

View File

@@ -1,14 +1,11 @@
import RealTimeHeatMap from "./realTime/realTimeHeatMap";
import BakedHeatMap from "./baked/bakedHeatMap";
import { useIsComparing } from "../../store/builder/store";
import HeatMapRenderer from "./heatMapRenderer";
function HeatMap() {
return (
<>
<RealTimeHeatMap />
const { isComparing } = useIsComparing();
<BakedHeatMap />
</>
);
return <>{isComparing ? <HeatMapRenderer /> : <RealTimeHeatMap />}</>;
}
export default HeatMap;

View File

@@ -0,0 +1,68 @@
import React, { useMemo } from "react";
import { useSceneContext } from "../../modules/scene/sceneContext";
import { useSimulationManager } from "../../store/rough/useSimulationManagerStore";
import { useSimulationState } from "../../store/simulation/useSimulationStore";
import HeatmapPreview from "./heatmapPreview";
const HeatMapRenderer = () => {
const { versionStore, layout } = useSceneContext();
const { selectedVersion } = versionStore();
const { simulationRecords } = useSimulationManager();
const { mainScene, comparisonScene } = useSimulationState();
const { mainSceneHeatmaps, comparisonSceneHeatmaps } = useMemo(() => {
const getHeatmaps = (scene: any) => {
const heatmaps: Array<{ image: string | Blob; type: string }> = [];
if (!scene) return heatmaps;
simulationRecords.forEach((project) =>
project.versions.forEach((version) =>
version.products.forEach((product) =>
product.simulateData.forEach((simulateDataItem) => {
const isTargetScene =
product.productId === scene.product.productUuid &&
version.versionId === scene.version.versionUuid &&
selectedVersion?.versionId;
if (!isTargetScene) return;
product.heatMaps?.forEach((heatMap) => {
if (heatMap.type !== simulateDataItem.type) return;
const img = heatMap.image;
if (typeof img === "string") {
if (/\.(png|jpg)$/i.test(img)) {
heatmaps.push({ image: img, type: heatMap.type });
}
} else if (img instanceof Blob) {
heatmaps.push({ image: img, type: heatMap.type });
}
});
})
)
)
);
return heatmaps;
};
return {
mainSceneHeatmaps: getHeatmaps(mainScene),
comparisonSceneHeatmaps: getHeatmaps(comparisonScene),
};
}, [simulationRecords, mainScene, comparisonScene, selectedVersion]);
return (
<>
{layout === "Main Layout" &&
mainSceneHeatmaps.map((heatMap, idx) => (
<HeatmapPreview key={`main-${idx}`} image={heatMap.image} type={heatMap.type} />
))}
{layout === "Comparison Layout" &&
comparisonSceneHeatmaps.map((heatMap, idx) => (
<HeatmapPreview key={`comp-${idx}`} image={heatMap.image} type={heatMap.type} />
))}
</>
);
};
export default HeatMapRenderer;

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from "react";
import { TextureLoader, Texture, DoubleSide } from "three";
import * as CONSTANTS from "../../types/world/worldConstants";
import { useHeatmapTypeStore } from "../../store/builder/store";
interface HeatmapPreviewProps {
image: string | Blob | null;
@@ -9,6 +10,7 @@ interface HeatmapPreviewProps {
const HeatmapPreview: React.FC<HeatmapPreviewProps> = ({ image, type }) => {
const [texture, setTexture] = useState<Texture | null>(null);
const { selectedTypes } = useHeatmapTypeStore();
const width = CONSTANTS.gridConfig.size;
const height = CONSTANTS.gridConfig.size;
@@ -64,16 +66,16 @@ const HeatmapPreview: React.FC<HeatmapPreviewProps> = ({ image, type }) => {
return (
<>
{type === "human" && (
{type === "human" && selectedTypes.human && (
<mesh rotation={[Math.PI / 2, 0, 0]} position={[0, 0.035, 0]}>
<planeGeometry args={[width, height]} />
<meshStandardMaterial map={texture} transparent side={DoubleSide} />
<meshStandardMaterial map={texture.clone()} transparent side={DoubleSide} />
</mesh>
)}
{type === "vehicle" && (
{type === "vehicle" && selectedTypes.vehicle && (
<mesh rotation={[Math.PI / 2, 0, 0]} position={[0, 0.025, 0]}>
<planeGeometry args={[width, height]} />
<meshStandardMaterial map={texture} transparent side={DoubleSide} />
<meshStandardMaterial map={texture.clone()} transparent side={DoubleSide} />
</mesh>
)}
</>

View File

@@ -33,16 +33,8 @@ const RealTimeHeatMap = () => {
const debugModeMap = { solid: 0, grayscale: 1, normal: 2 } as const;
const { productStore } = useSceneContext();
const { getProductById, products, selectedProduct } = productStore();
const {
bakedPoints,
hasHuman,
hasVehicle,
monitoringHuman,
monitoringVehicle,
addMonitoringHuman,
addMonitoringVehicle,
addBakedPoint,
} = useHeatMapStore();
const { monitoringHuman, monitoringVehicle, clearBakedPoints, addBakedPoint } =
useHeatMapStore();
const { isPlaying } = usePlayButtonStore();
const { isReset } = useResetButtonStore();
const { isPaused } = usePauseButtonStore();
@@ -65,13 +57,14 @@ const RealTimeHeatMap = () => {
u_growthRate: { value: GROWTH_TIME_MULTIPLIER },
});
// useEffect(() => {
// if (isReset || !isPlaying) {
// setPoints([]);
// lastFrameTime.current = null;
// lastUpdateTime.current = 0;
// }
// }, [isReset, isPlaying]);
useEffect(() => {
if (isReset || !isPlaying) {
clearBakedPoints();
setPoints([]);
lastFrameTime.current = null;
lastUpdateTime.current = 0;
}
}, [isReset, isPlaying]);
// Added human or vehicle
// useEffect(() => {

View File

@@ -3,6 +3,8 @@ import {
useLoadingProgress,
useIsComparing,
useCreateNewWindow,
useLimitDistance,
useRenderDistance,
} from "../../../store/builder/store";
import { useSimulationState } from "../../../store/simulation/useSimulationStore";
import { usePlayButtonStore } from "../../../store/ui/usePlayButtonStore";
@@ -17,7 +19,9 @@ import { useParams } from "react-router-dom";
import { validateSimulationDataApi } from "../../../services/simulation/comparison/validateSimulationDataApi";
import { calculateSimulationData } from "./functions/calculateSimulationData";
import NewWindowScene from "../../ui/compareVersion/NewWindowScene";
import Button from "../../ui/compareVersion/Button";
import ComparisonToolbar from "../../ui/compareVersion/ComparisonToolbar";
import { findEnvironment } from "../../../services/builder/environment/findEnvironment";
type AssetData = {
activeTime: number;
idleTime: number;
@@ -56,7 +60,7 @@ function ComparisonScene() {
const { productStore, versionStore } = useSceneContext();
const { versionHistory, selectedVersion, setSelectedVersion } = versionStore();
const { products, selectedProduct } = productStore();
const { isComparing } = useIsComparing();
const { isComparing, setIsComparing } = useIsComparing();
const { activeModule } = useModuleStore();
const { mainScene, comparisonScene, setComparisonState } = useSimulationState();
const { loadingProgress } = useLoadingProgress();
@@ -65,8 +69,10 @@ function ComparisonScene() {
const { setCompareProductsData } = useCompareProductDataStore();
const [shouldShowComparisonResult, setShouldShowComparisonResult] = useState(false);
const { addSimulationRecord } = useSimulationManager();
const { createNewWindow } = useCreateNewWindow();
const { createNewWindow, setCreateNewWindow } = useCreateNewWindow();
const { clearComparisonState } = useSimulationState();
const { setRenderDistance } = useRenderDistance();
const { setLimitDistance } = useLimitDistance();
const handleSelectVersion = (option: string) => {
const version = versionHistory.find((version) => version.versionName === option);
if (version) {
@@ -178,6 +184,17 @@ function ComparisonScene() {
simulationRecords,
]);
const handleExit = () => {
setIsComparing(false);
setCreateNewWindow(false);
clearComparisonState();
if (!projectId) return;
findEnvironment(projectId).then((data) => {
if (!data) return;
setRenderDistance(data.renderDistance);
setLimitDistance(data.limitDistance);
});
};
return (
<>
{isComparing && activeModule === "simulation" && selectedProduct && (
@@ -208,9 +225,10 @@ function ComparisonScene() {
zIndex: 10,
}}
>
<Button />
<ComparisonToolbar />
</div>
)}
<CompareLayOut />
{createNewWindow && <NewWindowScene />}

View File

@@ -0,0 +1,24 @@
import { findEnvironment } from "../../../../services/builder/environment/findEnvironment";
import { useCreateNewWindow, useIsComparing, useLimitDistance, useRenderDistance } from "../../../../store/builder/store";
import { useSimulationState } from "../../../../store/simulation/useSimulationStore";
export function handleExit(projectId?: string) {
const setIsComparing = useIsComparing.getState().setIsComparing;
const setCreateNewWindow = useCreateNewWindow.getState().setCreateNewWindow;
const setRenderDistance = useRenderDistance.getState().setRenderDistance;
const setLimitDistance = useLimitDistance.getState().setLimitDistance;
const clearComparisonState = useSimulationState.getState().clearComparisonState;
// Reset UI comparison state
setIsComparing(false);
setCreateNewWindow(false);
clearComparisonState();
if (!projectId) return;
// 🔹 Call async part separately (no async/await in this function)
findEnvironment(projectId).then((data) => {
if (!data) return;
setRenderDistance(data.renderDistance);
setLimitDistance(data.limitDistance);
});
}

View File

@@ -217,16 +217,6 @@ export const RenderInNewWindow: React.FC<NewWindowProps> = ({
};
}, []);
useEffect(() => {
const handleResize = () => {
childWindowRef.current?.dispatchEvent(new Event("resize"));
};
childWindowRef.current?.addEventListener("resize", handleResize);
return () => {
childWindowRef.current?.removeEventListener("resize", handleResize);
};
}, []);
useEffect(() => {
const w = childWindowRef.current;
if (w && !w.closed) {

View File

@@ -19,6 +19,7 @@ import useRestStates from "../../../hooks/useResetStates";
import { getVersionHistoryApi } from "../../../services/builder/versionControl/getVersionHistoryApi";
import { validateSimulationDataApi } from "../../../services/simulation/comparison/validateSimulationDataApi";
import { useSimulationManager } from "../../../store/rough/useSimulationManagerStore";
import { handleExit } from "../../layout/scenes/functions/handleExit";
const CompareLayOut = () => {
const { clearComparisonState, comparisonScene, setComparisonState } = useSimulationState();
@@ -46,9 +47,7 @@ const CompareLayOut = () => {
const { limitDistance, setLimitDistance } = useLimitDistance();
const { simulationRecords } = useSimulationManager();
useEffect(() => {
// console.log("simulationRecords: ", simulationRecords);
}, [simulationRecords]);
useEffect(() => {}, [simulationRecords]);
useEffect(() => {
return () => {
resetStates();
@@ -225,7 +224,7 @@ const CompareLayOut = () => {
</Suspense>
</div>
)}
<button onClick={() => handleExit(projectId)}>Exit</button>
{width !== "0px" &&
!selectedVersion?.versionId && ( // Show only if no layout selected
<div className="chooseLayout-wrapper">

View File

@@ -1,35 +1,16 @@
import React, { useState } from "react";
import { useParams } from "react-router-dom";
import {
useCreateNewWindow,
useHeatmapTypeStore,
useIsComparing,
useLimitDistance,
useRenderDistance,
} from "../../../store/builder/store";
import { useSimulationState } from "../../../store/simulation/useSimulationStore";
import { useParams } from "react-router-dom";
import { findEnvironment } from "../../../services/builder/environment/findEnvironment";
import { handleExit } from "../../layout/scenes/functions/handleExit";
const Button = () => {
const { isComparing, setIsComparing } = useIsComparing();
const ComparisonToolbar = () => {
const { isComparing } = useIsComparing();
const { createNewWindow, setCreateNewWindow } = useCreateNewWindow();
const { clearComparisonState } = useSimulationState();
const { projectId } = useParams();
const { setRenderDistance } = useRenderDistance();
const { setLimitDistance } = useLimitDistance();
const { selectedTypes, toggleType } = useHeatmapTypeStore();
const handleExit = () => {
setIsComparing(false);
setCreateNewWindow(false);
clearComparisonState();
if (!projectId) return;
findEnvironment(projectId).then((data) => {
if (!data) return;
setRenderDistance(data.renderDistance);
setLimitDistance(data.limitDistance);
});
};
const { projectId } = useParams();
const handleOpenInNewWindow = () => {
setCreateNewWindow(true);
@@ -46,7 +27,7 @@ const Button = () => {
gap: "10px",
}}
>
{isComparing && <button onClick={handleExit}>Exit</button>}
{isComparing && <button onClick={() => handleExit(projectId)}>Exit</button>}
{isComparing && !createNewWindow && (
<button onClick={handleOpenInNewWindow}>Open in New Window</button>
)}
@@ -57,7 +38,7 @@ const Button = () => {
checked={selectedTypes.vehicle}
onChange={() => toggleType("vehicle")}
/>
Vehicle
<p>Vehicle</p>
</label>
<label>
@@ -66,10 +47,10 @@ const Button = () => {
checked={selectedTypes.human}
onChange={() => toggleType("human")}
/>
Human
<p>Human</p>
</label>
</div>
);
};
export default Button;
export default ComparisonToolbar;

View File

@@ -1,10 +1,10 @@
import { Suspense } from "react";
import { useCreateNewWindow, useLoadingProgress } from "../../../store/builder/store";
import { RenderInNewWindow } from "../../templates/CreateNewWindow";
import { useSceneContext } from "../../../modules/scene/sceneContext";
import { useCreateNewWindow, useLoadingProgress } from "../../../store/builder/store";
import Scene from "../../../modules/scene/scene";
import ComparisonResult from "./ComparisonResult";
import Button from "./Button";
import ComparisonToolbar from "./ComparisonToolbar";
import LoadingPage from "../../templates/LoadingPage";
const NewWindowScene = () => {
@@ -22,7 +22,6 @@ const NewWindowScene = () => {
title="3D Viewer"
onClose={() => setCreateNewWindow(false)}
>
{/* Wait a tick to access child window */}
<Scene layout="Comparison Layout" />
{loadingProgress > 0 && (
<LoadingPage
@@ -39,7 +38,7 @@ const NewWindowScene = () => {
zIndex: 1000,
}}
>
<Button />
<ComparisonToolbar />
</div>
{!loadingProgress && <ComparisonResult />}
</RenderInNewWindow>

View File

@@ -32,6 +32,7 @@ const TreeNode = ({
onDrop,
onToggleExpand,
onOptionClick,
onRename,
}: {
item: AssetGroupChild;
level?: number;
@@ -43,6 +44,7 @@ const TreeNode = ({
onClick: (e: React.MouseEvent, selectedItem: AssetGroupChild) => void;
onToggleExpand: (groupUuid: string, newExpanded: boolean) => void;
onOptionClick: (option: string, item: AssetGroupChild) => void;
onRename: (item: AssetGroupChild, newName: string) => void;
}) => {
const { assetGroupStore, assetStore } = useSceneContext();
const { hasSelectedAsset, selectedAssets } = assetStore();
@@ -136,7 +138,13 @@ const TreeNode = ({
<div className="node-icon">{isGroupNode ? <FolderIcon isOpen={isExpanded} /> : <CubeIcon />}</div>
<RenameInput value={itemName} onRename={() => {}} canEdit={true} />
<RenameInput
value={itemName}
onRename={(newName) => {
onRename(item, newName);
}}
canEdit={true}
/>
<div className="node-controls">
<button className="control-button" title={isVisible ? "Visible" : "Hidden"} onClick={(e) => handleOptionClick(e, "visibility")}>
@@ -171,6 +179,7 @@ const TreeNode = ({
onDrop={onDrop}
onToggleExpand={onToggleExpand}
onOptionClick={onOptionClick}
onRename={onRename}
/>
))}
</div>
@@ -195,6 +204,7 @@ export const AssetOutline = () => {
const {
groupHierarchy,
isGroup,
getItemId,
getGroupsContainingAsset,
getFlatGroupChildren,
setGroupExpanded,
@@ -207,6 +217,8 @@ export const AssetOutline = () => {
toggleSelectedGroup,
clearSelectedGroups,
hasSelectedGroup,
toggleGroupVisibility,
toggleGroupLock,
} = assetGroupStore();
const { projectId } = useParams();
const { push3D } = undoRedo3DStore();
@@ -232,6 +244,77 @@ export const AssetOutline = () => {
return flattened;
}, [groupHierarchy, isGroup]);
const handleAssetRenameUpdate = async (asset: Asset | null, newName: string) => {
if (!asset) return;
if (!builderSocket?.connected) {
setAssetsApi({
modelUuid: asset.modelUuid,
modelName: newName,
assetId: asset.assetId,
position: asset.position,
rotation: asset.rotation,
scale: asset.scale,
isCollidable: asset.isCollidable,
opacity: asset.opacity,
isLocked: asset.isLocked,
isVisible: asset.isVisible,
versionId: selectedVersion?.versionId || "",
projectId: projectId || "",
})
.then((data) => {
if (!data.message || !data.data) {
echo.error(`Error renaming asset: ${asset.modelName}`);
return;
}
if (data.message === "Model updated successfully" && data.data) {
const model: Asset = {
modelUuid: data.data.modelUuid,
modelName: data.data.modelName,
assetId: data.data.assetId,
position: data.data.position,
rotation: data.data.rotation,
scale: data.data.scale,
isLocked: data.data.isLocked,
isVisible: data.data.isVisible,
isCollidable: data.data.isCollidable,
opacity: data.data.opacity,
...(data.data.eventData ? { eventData: data.data.eventData } : {}),
};
updateAssetInScene(model, () => {
echo.info(`Renamed asset to: ${model.modelName}`);
});
} else {
echo.error(`Error renaming asset: ${asset.modelName}`);
}
})
.catch(() => {
echo.error(`Error renaming asset: ${asset.modelName}`);
});
} else {
const data = {
organization,
modelUuid: asset.modelUuid,
modelName: newName,
assetId: asset.assetId,
position: asset.position,
rotation: asset.rotation,
scale: asset.scale,
isCollidable: asset.isCollidable,
opacity: asset.opacity,
isLocked: asset.isLocked,
isVisible: asset.isVisible,
socketId: builderSocket?.id,
versionId: selectedVersion?.versionId || "",
projectId,
userId,
};
builderSocket.emit("v1:model-asset:add", data);
}
};
const handleAssetVisibilityUpdate = async (asset: Asset | null) => {
if (!asset) return;
@@ -556,11 +639,6 @@ export const AssetOutline = () => {
if (!scene.current) return;
// Helper to get item ID
const getItemId = (i: AssetGroupChild): string => {
return isGroup(i) ? i.groupUuid : i.modelUuid;
};
const itemId = getItemId(item);
const flattened = getFlattenedHierarchy();
const clickedIndex = flattened.findIndex((flatItem) => getItemId(flatItem) === itemId);
@@ -674,14 +752,57 @@ export const AssetOutline = () => {
]
);
const handleRename = useCallback(
(item: AssetGroupChild, newName: string) => {
if (isGroup(item)) {
console.log("Rename group:", item.groupUuid, "to:", newName);
} else {
const asset = getAssetById(item.modelUuid);
if (!asset) return;
const oldName = asset.modelName;
const undoActions: UndoRedo3DAction[] = [];
const assetsToUpdate: AssetData[] = [];
assetsToUpdate.push({
type: "Asset",
assetData: { ...asset, modelName: oldName },
newData: { ...asset, modelName: newName },
timeStap: new Date().toISOString(),
});
if (assetsToUpdate.length > 0) {
if (assetsToUpdate.length === 1) {
undoActions.push({
module: "builder",
actionType: "Asset-Update",
asset: assetsToUpdate[0],
});
} else {
undoActions.push({
module: "builder",
actionType: "Assets-Update",
assets: assetsToUpdate,
});
}
push3D({
type: "Scene",
actions: undoActions,
});
}
handleAssetRenameUpdate(asset, newName);
}
},
[selectedVersion, builderSocket, projectId, userId, organization]
);
const handleOptionClick = useCallback(
(option: string, item: AssetGroupChild) => {
const getItemId = (i: AssetGroupChild): string => {
return isGroup(i) ? i.groupUuid : i.modelUuid;
};
if (option === "visibility") {
if (isGroup(item)) {
toggleGroupVisibility(item.groupUuid);
} else {
const undoActions: UndoRedo3DAction[] = [];
const assetsToUpdate: AssetData[] = [];
@@ -724,6 +845,7 @@ export const AssetOutline = () => {
}
} else if (option === "lock") {
if (isGroup(item)) {
toggleGroupLock(item.groupUuid);
} else {
const undoActions: UndoRedo3DAction[] = [];
const assetsToUpdate: AssetData[] = [];
@@ -790,7 +912,7 @@ export const AssetOutline = () => {
const handleAddGroup = useCallback(() => {}, [assetGroupStore, clearSelectedGroups, addSelectedGroup]);
const handleExpandAll = useCallback(() => {}, [assetGroupStore, setGroupExpanded]);
const handleCollapseAll = useCallback(() => {}, [assetGroupStore, setGroupExpanded]);
// Selection statistics
const selectionStats = useMemo(() => {
@@ -824,7 +946,7 @@ export const AssetOutline = () => {
<button className="toolbar-button" title="Add Group" onClick={handleAddGroup}>
<AddIcon />
</button>
<button className="toolbar-button" title="Expand All" onClick={handleExpandAll}>
<button className="toolbar-button" title="Collapse All" onClick={handleCollapseAll}>
<CollapseAllIcon />
</button>
<button className="close-button" onClick={() => setIsOpen(!isOpen)}>
@@ -847,6 +969,7 @@ export const AssetOutline = () => {
onClick={handleClick}
onToggleExpand={handleToggleExpand}
onOptionClick={handleOptionClick}
onRename={handleRename}
/>
))}
</div>

View File

@@ -27,8 +27,9 @@ export function useModelEventHandlers({ boundingBox, groupRef, asset }: { boundi
const { selectedEventData, setSelectedEventData } = useSelectedEventData();
const { setSelectedAction, selectedAction } = useSelectedAction();
const { builderSocket, simulationSocket } = useSocketStore();
const { eventStore, productStore, undoRedo3DStore, versionStore, assetStore } = useSceneContext();
const { eventStore, productStore, undoRedo3DStore, versionStore, assetStore, assetGroupStore } = useSceneContext();
const { toggleSelectedAsset, selectedAssets, clearSelectedAssets } = assetStore();
const { isAssetVisible } = assetGroupStore();
const { push3D } = undoRedo3DStore();
const { zoneAssetId, setZoneAssetId } = useZoneAssetId();
const { resourceManagementId, setResourceManagementId } = useResourceManagementId();
@@ -95,7 +96,9 @@ export function useModelEventHandlers({ boundingBox, groupRef, asset }: { boundi
(toolMode === "cursor" || toolMode === "Move-Asset" || toolMode === "Rotate-Asset") &&
boundingBox &&
groupRef.current &&
(activeModule === "builder" || (activeModule === "simulation" && resourceManagementId))
(activeModule === "builder" || (activeModule === "simulation" && resourceManagementId)) &&
asset.isVisible &&
isAssetVisible(asset.modelUuid)
) {
zoomMeshes([asset.modelUuid]);

View File

@@ -13,17 +13,14 @@ import { getAssetFieldApi } from "../../../../../services/builder/asset/floorAss
import { ModelAnimator } from "./animator/modelAnimator";
import { useModelEventHandlers } from "./eventHandlers/useModelEventHandlers";
function Model({
asset,
isRendered,
loader,
}: Readonly<{ asset: Asset; isRendered: boolean; loader: GLTFLoader }>) {
function Model({ asset, isRendered, loader }: Readonly<{ asset: Asset; isRendered: boolean; loader: GLTFLoader }>) {
const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
const savedTheme: string = localStorage.getItem("theme") || "light";
const { toolMode } = useToolMode();
const { toggleView } = useToggleView();
const { activeModule } = useModuleStore();
const { assetStore, layout } = useSceneContext();
const { assetStore, assetGroupStore } = useSceneContext();
const { isAssetVisible } = assetGroupStore();
const { resetAnimation, hasSelectedAsset, updateSelectedAsset, selectedAssets } = assetStore();
const { setDeletableFloorAsset } = useBuilderStore();
const [gltfScene, setGltfScene] = useState<GLTF["scene"] | null>(null);
@@ -56,12 +53,7 @@ function Model({
}, [activeModule, toolMode, selectedAssets]);
useEffect(() => {
if (
groupRef.current &&
selectedAssets.length === 1 &&
selectedAssets[0].userData.modelUuid === asset.modelUuid &&
hasSelectedAsset(asset.modelUuid)
) {
if (groupRef.current && selectedAssets.length === 1 && selectedAssets[0].userData.modelUuid === asset.modelUuid && hasSelectedAsset(asset.modelUuid)) {
updateSelectedAsset(groupRef.current);
}
}, [isRendered, selectedAssets, asset]);
@@ -139,10 +131,7 @@ function Model({
logModelStatus(assetId, "backend-loaded");
})
.catch((error) => {
console.error(
`[Backend] Error storing/loading ${asset.modelName}:`,
error
);
console.error(`[Backend] Error storing/loading ${asset.modelName}:`, error);
});
},
undefined,
@@ -156,8 +145,7 @@ function Model({
});
}, []);
const { handleDblClick, handleClick, handlePointerOver, handlePointerOut, handleContextMenu } =
useModelEventHandlers({ boundingBox, groupRef, asset });
const { handleDblClick, handleClick, handlePointerOver, handlePointerOut, handleContextMenu } = useModelEventHandlers({ boundingBox, groupRef, asset });
return (
<group
@@ -205,30 +193,14 @@ function Model({
<>
{isRendered ? (
<>
<primitive visible={asset.isVisible} object={gltfScene} />
<primitive visible={asset.isVisible && isAssetVisible(asset.modelUuid)} object={gltfScene} />
<ModelAnimator asset={asset} gltfScene={gltfScene} />
</>
) : (
<>
{!hasSelectedAsset(asset.modelUuid) && (
<AssetBoundingBox
name="Asset Fallback"
boundingBox={boundingBox}
color="gray"
lineWidth={2.5}
/>
)}
</>
)}
{hasSelectedAsset(asset.modelUuid) && (
<AssetBoundingBox
name="Asset BBox"
boundingBox={boundingBox}
color={savedTheme === "dark" ? "#c4abf1" : "#6f42c1"}
lineWidth={2.7}
/>
<>{!hasSelectedAsset(asset.modelUuid) && <AssetBoundingBox name="Asset Fallback" boundingBox={boundingBox} color="gray" lineWidth={2.5} />}</>
)}
{hasSelectedAsset(asset.modelUuid) && <AssetBoundingBox name="Asset BBox" boundingBox={boundingBox} color={savedTheme === "dark" ? "#c4abf1" : "#6f42c1"} lineWidth={2.7} />}
</>
)}
</group>

View File

@@ -24,9 +24,10 @@ const SelectionControls3D: React.FC = () => {
const { toolMode } = useToolMode();
const { builderSocket, simulationSocket } = useSocketStore();
const { contextAction, setContextAction } = useContextActionStore();
const { assetStore, productStore, undoRedo3DStore, versionStore } = useSceneContext();
const { assetStore, productStore, undoRedo3DStore, versionStore, assetGroupStore } = useSceneContext();
const { selectedDecal, selectedWall, selectedAisle, selectedFloor, selectedWallAsset } = useBuilderStore();
const { push3D, undoStack, redoStack } = undoRedo3DStore();
const { isAssetVisible } = assetGroupStore();
const { selectedProduct, updateEvent, deleteEvent } = productStore();
const {
assets,
@@ -253,7 +254,13 @@ const SelectionControls3D: React.FC = () => {
let currentObject: THREE.Object3D | null = object;
while (currentObject) {
// if (currentObject.userData.modelUuid && !currentObject.userData.wallAssetType) {
if (currentObject.userData.modelUuid && !currentObject.userData.wallAssetType && currentObject.userData.isVisible && currentObject.visible) {
if (
currentObject.userData.modelUuid &&
!currentObject.userData.wallAssetType &&
currentObject.userData.isVisible &&
currentObject.visible &&
isAssetVisible(currentObject.userData.modelUuid)
) {
Objects.add(currentObject);
break;
}

View File

@@ -1,35 +1,25 @@
import { useEffect, useMemo } from "react";
import { Canvas } from "@react-three/fiber";
import { useParams } from "react-router-dom";
import { KeyboardControls } from "@react-three/drei";
import { useSceneContext } from "./sceneContext";
import useModuleStore from "../../store/ui/useModuleStore";
import { useCreateNewWindow, useLoadingProgress } from "../../store/builder/store";
import { useSocketStore } from "../../store/socket/useSocketStore";
import Builder from "../builder/builder";
import Visualization from "../visualization/visualization";
import Setup from "./setup/setup";
import Simulation from "../simulation/simulation";
import Collaboration from "../collaboration/collaboration";
import useModuleStore from "../../store/ui/useModuleStore";
import { useParams } from "react-router-dom";
import { getUserData } from "../../functions/getUserData";
import {
useCreateNewWindow,
useHeatmapTypeStore,
useIsComparing,
useLoadingProgress,
} from "../../store/builder/store";
import { useSocketStore } from "../../store/socket/useSocketStore";
import { Color, SRGBColorSpace } from "three";
import { compressImage } from "../../utils/compressImage";
import { ALPHA_ORG } from "../../pages/Dashboard";
import HeatmapPreview from "../../components/heatMapGenerator/heatmapPreview";
import { useSimulationManager } from "../../store/rough/useSimulationManagerStore";
import { useSimulationState } from "../../store/simulation/useSimulationStore";
export default function Scene({
layout,
}: {
readonly layout: "Main Layout" | "Comparison Layout";
}) {
export default function Scene({ layout }: { readonly layout: "Main Layout" | "Comparison Layout" }) {
const map = useMemo(
() => [
{ name: "forward", keys: ["ArrowUp", "w", "W"] },
@@ -40,7 +30,7 @@ export default function Scene({
],
[]
);
const { assetStore, layoutType, versionStore } = useSceneContext();
const { assetStore, layoutType } = useSceneContext();
const { assets } = assetStore();
const { userId, organization } = getUserData();
const { projectId } = useParams();
@@ -48,19 +38,11 @@ export default function Scene({
const { activeModule } = useModuleStore();
const { loadingProgress } = useLoadingProgress();
const { setWindowRendered } = useCreateNewWindow();
const { simulationRecords } = useSimulationManager();
const { isComparing } = useIsComparing();
const { mainScene, comparisonScene } = useSimulationState();
const { selectedVersion } = versionStore();
const { selectedTypes } = useHeatmapTypeStore();
useEffect(() => {
if (!projectId || loadingProgress !== 0) return;
const canvas = document.getElementById("sceneCanvas")?.getElementsByTagName("canvas")[0];
if (
!canvas ||
!(layoutType === "default" || (layoutType === "useCase" && organization === ALPHA_ORG))
)
return;
if (!canvas || !(layoutType === "default" || (layoutType === "useCase" && organization === ALPHA_ORG))) return;
compressImage(canvas.toDataURL("image/png")).then((screenshotDataUrl) => {
const updateProjects = {
projectId,
@@ -72,85 +54,8 @@ export default function Scene({
projectSocket.emit("v1:project:update", updateProjects);
}
});
// eslint-disable-next-line
}, [activeModule, assets, loadingProgress, projectId, layoutType]);
// Prepare heatmaps per scene
const mainSceneHeatmaps = useMemo(() => {
const heatmaps: Array<{ image: string | Blob; type: string }> = [];
simulationRecords.forEach((project) =>
project.versions.forEach((version) =>
version.products.forEach((product) =>
product.simulateData.forEach((simulateDataItem) => {
const isMainScene =
product.productId === mainScene.product.productUuid &&
version.versionId === mainScene.version.versionUuid &&
selectedVersion?.versionId;
if (!isMainScene) return;
product.heatMaps?.forEach((heatMap) => {
if (heatMap.type !== simulateDataItem.type) return;
// Filter by selected types
if (
(heatMap.type === "vehicle" && !selectedTypes.vehicle) ||
(heatMap.type === "human" && !selectedTypes.human)
)
return;
const img = heatMap.image;
if (typeof img === "string") {
if (/\.(png|jpg)$/i.test(img)) {
heatmaps.push({ image: img, type: heatMap.type });
}
} else if (img instanceof Blob) {
heatmaps.push({ image: img, type: heatMap.type });
}
});
})
)
)
);
return heatmaps;
}, [simulationRecords, mainScene, selectedVersion, selectedTypes]);
const comparisonSceneHeatmaps = useMemo(() => {
const heatmaps: Array<{ image: string | Blob; type: string }> = [];
simulationRecords.forEach((project) =>
project.versions.forEach((version) =>
version.products.forEach((product) =>
product.simulateData.forEach((simulateDataItem) => {
const isComparisonScene =
product.productId === comparisonScene?.product.productUuid &&
version.versionId === comparisonScene?.version.versionUuid &&
selectedVersion?.versionId;
if (!isComparisonScene) return;
product.heatMaps?.forEach((heatMap) => {
if (heatMap.type !== simulateDataItem.type) return;
// Filter by selected types
if (
(heatMap.type === "vehicle" && !selectedTypes.vehicle) ||
(heatMap.type === "human" && !selectedTypes.human)
)
return;
const img = heatMap.image;
if (typeof img === "string") {
if (/\.(png|jpg)$/i.test(img)) {
heatmaps.push({ image: img, type: heatMap.type });
}
} else if (img instanceof Blob) {
heatmaps.push({ image: img, type: heatMap.type });
}
});
})
)
)
);
return heatmaps;
}, [simulationRecords, comparisonScene, selectedVersion, selectedTypes]);
return (
<KeyboardControls map={map}>
<Canvas
@@ -163,7 +68,7 @@ export default function Scene({
}}
resize={{ polyfill: ResizeObserver }}
style={{ width: "100vw", height: "100vh", background: "#202020" }}
performance={{ min: 0.9, max: 1.0 }}
performance={{ min: 0.9, max: 1 }}
onCreated={(e) => {
e.scene.background = layout === "Main Layout" ? null : new Color(0x19191d);
}}
@@ -180,27 +85,6 @@ export default function Scene({
<Setup />
<Collaboration />
<Builder />
{isComparing && (selectedTypes.vehicle || selectedTypes.human) && (
<>
{layout === "Main Layout" &&
mainSceneHeatmaps.map((heatMap, idx) => (
<HeatmapPreview
key={`main-${idx}`}
image={heatMap.image}
type={heatMap.type}
/>
))}
{layout === "Comparison Layout" &&
comparisonSceneHeatmaps.map((heatMap, idx) => (
<HeatmapPreview
key={`comp-${idx}`}
image={heatMap.image}
type={heatMap.type}
/>
))}
</>
)}
<Simulation />
<Visualization />
</Canvas>

View File

@@ -15,14 +15,12 @@ import useModuleStore from "../../store/ui/useModuleStore";
import SimulationAnalysis from "./analysis/simulationAnalysis";
import { useSceneContext } from "../scene/sceneContext";
import HeatMap from "../../components/heatMapGenerator/heatMap";
import { useIsComparing } from "../../store/builder/store";
function Simulation() {
const { activeModule } = useModuleStore();
const { eventStore, productStore } = useSceneContext();
const { events } = eventStore();
const { products } = productStore();
const { isComparing } = useIsComparing();
useEffect(() => {
// console.log('events: ', events);
@@ -62,7 +60,7 @@ function Simulation() {
<SimulationAnalysis />
{!isComparing && <HeatMap />}
<HeatMap />
</>
)}
</>

View File

@@ -1,6 +1,6 @@
import { WebGLRenderer } from "three";
import { Buffer } from "buffer";
import { exportHeatmapAsPNG } from "../../../../components/heatMapGenerator/functions/exportHeatmapAsPNG";
import { exportHeatmapAsPNG } from "./exportHeatmapAsPNG";
// Types
interface BakedPoint {
@@ -32,6 +32,7 @@ interface GenerateHeatmapOutputParams {
gl: WebGLRenderer;
width: number;
height: number;
version: string;
outputType?: "url" | "file" | "blob";
download?: boolean;
}
@@ -40,6 +41,7 @@ export async function generateHeatmapOutput({
bakedPoints,
gl,
width,
version,
height,
outputType = "file",
download = false,
@@ -48,7 +50,6 @@ export async function generateHeatmapOutput({
const fileResults = await Promise.all(
bakedResult.map(async (item) => {
console.log("item: ", item);
let imageBlob: Blob;
@@ -62,7 +63,7 @@ export async function generateHeatmapOutput({
imageBlob = item.image;
}
const fileName = `${item.type}-heatmap.png`;
const fileName = `${item.type}-${version}-heatmap.png`;
// Convert Blob -> ArrayBuffer -> Buffer
const arrayBuffer = await imageBlob.arrayBuffer();

View File

@@ -90,7 +90,7 @@ const SimulationHandler = () => {
async function handleSaveSimulationData(data: any) {
try {
await saveSimulationDataApi(data);
console.log("Simulation data saved successfully");
// console.log("Simulation data saved successfully");
setIsPlaying(false);
} catch (err) {
console.error("Failed to save simulation data:", err);
@@ -169,8 +169,10 @@ const SimulationHandler = () => {
gl,
width,
height,
version: selectedVersion.versionId,
download: true,
}).then((bakedResult) => {
console.log("bakedResult: ", bakedResult);
heatMapImageApi(
projectId || "",
selectedVersion?.versionId || "",

View File

@@ -20,6 +20,7 @@ function Simulator() {
const { selectedVersion } = versionStore();
const { setIsComparing } = useIsComparing();
const { addSimulationRecords } = useSimulationManager();
useEffect(() => {
if (!isPlaying || isReset || !selectedProduct.productUuid) return;
@@ -74,11 +75,7 @@ function Simulator() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedProduct, projectId]);
return (
<>
<SimulationHandler />
</>
);
return <SimulationHandler />;
}
export default Simulator;

View File

@@ -77,13 +77,10 @@ const Project: React.FC = () => {
}, [projectId]);
useEffect(() => {
const token = localStorage.getItem("token");
const refreshToken = localStorage.getItem("refreshToken");
if (token && refreshToken && projectId) {
if (projectId) {
echo.warn("Validating token");
initializeBuilderSocket(token, refreshToken, projectId);
initializeSimulationSocket(token, refreshToken, projectId);
initializeBuilderSocket(projectId);
initializeSimulationSocket(projectId);
echo.success("Builder socket initialized");
} else {
navigate("/");
@@ -100,11 +97,8 @@ const Project: React.FC = () => {
}, [projectId, builderSocket]);
useEffect(() => {
const token = localStorage.getItem("token");
const refreshToken = localStorage.getItem("refreshToken");
if (token && refreshToken && projectId) {
initializeVisualizationSocket(token, refreshToken, projectId);
if (projectId) {
initializeVisualizationSocket(projectId);
echo.success("Visualization socket initialized");
} else {
navigate("/");
@@ -121,11 +115,8 @@ const Project: React.FC = () => {
}, [projectId, visualizationSocket]);
useEffect(() => {
const token = localStorage.getItem("token");
const refreshToken = localStorage.getItem("refreshToken");
if (token && refreshToken && projectId) {
initializeThreadSocket(token, refreshToken, projectId);
if (projectId) {
initializeThreadSocket(projectId);
echo.success("Thread socket initialized");
} else {
navigate("/");
@@ -142,11 +133,8 @@ const Project: React.FC = () => {
}, [projectId, threadSocket]);
useEffect(() => {
const token = localStorage.getItem("token");
const refreshToken = localStorage.getItem("refreshToken");
if (token && refreshToken && projectId) {
initializeSimulationSocket(token, refreshToken, projectId);
if (projectId) {
initializeSimulationSocket(projectId);
echo.success("Simulation socket initialized");
} else {
navigate("/");

View File

@@ -487,8 +487,8 @@ interface HeatmapTypeState {
}
export const useHeatmapTypeStore = create<HeatmapTypeState>((set) => ({
selectedTypes: {
vehicle: false,
human: false,
vehicle: true,
human: true,
},
toggleType: (type) =>
set((state) => ({

View File

@@ -27,8 +27,9 @@ interface AssetGroupStore {
// Group properties
setGroupName: (groupUuid: string, newName: string) => void;
setGroupVisibility: (groupUuid: string, isVisible: boolean) => void;
setGroupLock: (groupUuid: string, isLocked: boolean) => void;
setGroupVisibility: (groupUuid: string, isVisible: boolean) => void;
toggleGroupLock: (groupUuid: string) => void;
toggleGroupVisibility: (groupUuid: string) => void;
setGroupExpanded: (groupUuid: string, isExpanded: boolean) => void;
@@ -46,7 +47,12 @@ interface AssetGroupStore {
getParentGroup: (item: AssetGroupChild) => string | null;
hasGroup: (groupUuid: string) => boolean;
isGroup: (item: AssetGroupChild) => item is AssetGroupHierarchyNode;
getItemId: (i: AssetGroupChild) => string;
isEmptyGroup: (groupUuid: string) => boolean;
isAssetGroupVisible: (groupUuid: string) => boolean;
isAssetGroupLocked: (groupUuid: string) => boolean;
isAssetVisible: (assetUuid: string) => boolean;
isAssetLocked: (assetUuid: string) => boolean;
}
export const createAssetGroupStore = () => {
@@ -228,6 +234,15 @@ export const createAssetGroupStore = () => {
});
},
setGroupLock: (groupUuid, isLocked) => {
set((state) => {
const group = state.assetGroups.find((g) => g.groupUuid === groupUuid);
if (group) {
group.isLocked = isLocked;
}
});
},
setGroupVisibility: (groupUuid, isVisible) => {
set((state) => {
const group = state.assetGroups.find((g) => g.groupUuid === groupUuid);
@@ -237,11 +252,11 @@ export const createAssetGroupStore = () => {
});
},
setGroupLock: (groupUuid, isLocked) => {
toggleGroupLock: (groupUuid) => {
set((state) => {
const group = state.assetGroups.find((g) => g.groupUuid === groupUuid);
if (group) {
group.isLocked = isLocked;
group.isLocked = !group.isLocked;
}
});
},
@@ -455,10 +470,94 @@ export const createAssetGroupStore = () => {
return "children" in item;
},
getItemId: (i: AssetGroupChild): string => {
return get().isGroup(i) ? i.groupUuid : i.modelUuid;
},
isEmptyGroup: (groupUuid) => {
const group = get().assetGroups.find((g) => g.groupUuid === groupUuid);
return !group || group.children.length === 0;
},
isAssetGroupVisible: (groupUuid: string) => {
const state = get();
const group = state.assetGroups.find((g) => g.groupUuid === groupUuid);
if (!group) return true; // Default to visible if group not found
// If this group is not visible, return false regardless of parents
if (!group.isVisible) return false;
// Check parent groups recursively
const parentGroups = state.getGroupsContainingGroup(groupUuid);
for (const parentGroup of parentGroups) {
if (!state.isAssetGroupVisible(parentGroup.groupUuid)) {
return false;
}
}
return true;
},
isAssetGroupLocked: (groupUuid: string) => {
const state = get();
const group = state.assetGroups.find((g) => g.groupUuid === groupUuid);
if (!group) return false; // Default to unlocked if group not found
// If this group is locked, return true regardless of parents
if (group.isLocked) return true;
// Check parent groups recursively
const parentGroups = state.getGroupsContainingGroup(groupUuid);
for (const parentGroup of parentGroups) {
if (state.isAssetGroupLocked(parentGroup.groupUuid)) {
return true;
}
}
return false;
},
isAssetVisible: (assetUuid: string) => {
const state = get();
// Check if asset is in any groups
const parentGroups = state.getGroupsContainingAsset(assetUuid);
// If asset is not in any groups, it's always visible
if (parentGroups.length === 0) return true;
// Asset is visible only if ALL parent groups are visible
for (const parentGroup of parentGroups) {
if (!state.isAssetGroupVisible(parentGroup.groupUuid)) {
return false;
}
}
return true;
},
isAssetLocked: (assetUuid: string) => {
const state = get();
// Check if asset is in any groups
const parentGroups = state.getGroupsContainingAsset(assetUuid);
// If asset is not in any groups, check its own locked state
if (parentGroups.length === 0) {
// You might need access to the assets store to get the asset's own locked state
// For now, returning false as default
return false;
}
// Asset is locked if ANY parent group is locked
for (const parentGroup of parentGroups) {
if (state.isAssetGroupLocked(parentGroup.groupUuid)) {
return true;
}
}
return false;
},
}))
);
};

View File

@@ -7,12 +7,12 @@ interface SimulationUsageRecord {
type: "roboticArm" | "vehicle" | "transfer" | "storageUnit" | "crane" | "human" | "machine";
assetId: string;
}
type image = string | Blob | null;
// Product → holds multiple usage records
interface ProductSimulation {
productId: string;
simulateData: SimulationUsageRecord[];
heatMaps?: { type: string; image: string | Blob | null }[];
heatMaps?: { type: string; image: image }[];
}
// Version → holds multiple products
@@ -29,7 +29,6 @@ interface ProjectSimulation {
interface SimulationManagerStore {
simulationRecords: ProjectSimulation[];
setSimulationRecords: (simulateData: ProjectSimulation[]) => void;
addSimulationRecord: (
projectId: string | undefined,
@@ -42,7 +41,7 @@ interface SimulationManagerStore {
versionId: string,
productId: string,
records: SimulationUsageRecord[],
heatMaps?: { type: string; image: string | Blob | null }[]
heatMaps?: { type: string; image: image }[]
) => void;
resetProductRecords: (
projectId: string | undefined,
@@ -131,7 +130,7 @@ export const useSimulationManager = create<SimulationManagerStore>((set, get) =>
versionId: string,
productId: string,
records: SimulationUsageRecord[],
heatMaps?: { type: string; image: string | Blob | null }[]
heatMaps?: { type: string; image: image }[]
) =>
set((state) => {
const projects = [...state.simulationRecords];
@@ -199,60 +198,6 @@ export const useSimulationManager = create<SimulationManagerStore>((set, get) =>
return { simulationRecords: projects };
}),
// addSimulationRecords: (projectId, versionId, productId, records) =>
// set((state) => {
// const projects = state.simulationRecords.map((project) => {
// if (project.projectId !== projectId) return project;
// return {
// ...project,
// versions: project.versions.map((version) => {
// if (version.versionId !== versionId) return version;
// return {
// ...version,
// products: version.products.map((product) =>
// product.productId === productId
// ? {
// ...product,
// simulateData: [...product.simulateData, ...records],
// }
// : product
// ),
// };
// }),
// };
// });
// // same creation logic for new project/version/product
// if (!state.simulationRecords.find((p) => p.projectId === projectId)) {
// projects.push({
// projectId,
// versions: [
// {
// versionId,
// products: [{ productId, simulateData: [...records] }],
// },
// ],
// });
// } else {
// const project = projects.find((p) => p.projectId === projectId)!;
// if (!project.versions.find((v) => v.versionId === versionId)) {
// project.versions.push({
// versionId,
// products: [{ productId, simulateData: [...records] }],
// });
// } else {
// const version = project.versions.find((v) => v.versionId === versionId)!;
// if (!version.products.find((p) => p.productId === productId)) {
// version.products.push({ productId, simulateData: [...records] });
// }
// }
// }
// return { simulationRecords: projects };
// }),
resetProductRecords: (projectId, versionId, productId) =>
set((state) => {
const projects = state.simulationRecords.map((project) => {
@@ -277,9 +222,7 @@ export const useSimulationManager = create<SimulationManagerStore>((set, get) =>
return { simulationRecords: projects };
}),
setSimulationRecords: (simulateData) => {
set({ simulationRecords: simulateData });
},
getProjectById: (projectId: string | undefined) => {
return get().simulationRecords.find((p: ProjectSimulation) => p.projectId === projectId);
},

View File

@@ -8,11 +8,11 @@ interface SocketStore {
threadSocket: Socket | null;
simulationSocket: Socket | null;
initializeBuilderSocket: (token: string, refreshToken: string, projectId: string) => void;
initializeVisualizationSocket: (token: string, refreshToken: string, projectId: string) => void;
initializeThreadSocket: (token: string, refreshToken: string, projectId: string) => void;
initializeBuilderSocket: (projectId: string) => void;
initializeVisualizationSocket: (projectId: string) => void;
initializeThreadSocket: (projectId: string) => void;
initializeProjectSocket: (token: string, refreshToken: string) => void;
initializeSimulationSocket: (token: string, refreshToken: string, projectId: string) => void;
initializeSimulationSocket: (projectId: string) => void;
disconnectBuilderSocket: (projectId: string) => void;
disconnectVisualizationSocket: (projectId: string) => void;
@@ -56,36 +56,51 @@ export const useSocketStore = create<SocketStore>((set, get) => ({
threadSocket: null,
simulationSocket: null,
initializeBuilderSocket: (token, refreshToken, projectId) => {
if (get().builderSocket) return;
initializeBuilderSocket: (projectId) => {
const token = localStorage.getItem("token");
const refreshToken = localStorage.getItem("refreshToken");
if (get().builderSocket || !token || !refreshToken) return;
const builderSocket = io(`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/Builder_v1`, socketOptions({ token, refreshToken, projectId }));
attachLogs("Builder", builderSocket);
set({ builderSocket });
},
initializeVisualizationSocket: (token, refreshToken, projectId) => {
if (get().visualizationSocket) return;
initializeVisualizationSocket: (projectId) => {
const token = localStorage.getItem("token");
const refreshToken = localStorage.getItem("refreshToken");
if (get().visualizationSocket || !token || !refreshToken) return;
const visualizationSocket = io(`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/Visualization_v1`, socketOptions({ token, refreshToken, projectId }));
attachLogs("Visualization", visualizationSocket);
set({ visualizationSocket });
},
initializeThreadSocket: (token, refreshToken, projectId) => {
if (get().threadSocket) return;
initializeThreadSocket: (projectId) => {
const token = localStorage.getItem("token");
const refreshToken = localStorage.getItem("refreshToken");
if (get().threadSocket || !token || !refreshToken) return;
const threadSocket = io(`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/thread`, socketOptions({ token, refreshToken, projectId }));
attachLogs("Thread", threadSocket);
set({ threadSocket });
},
initializeProjectSocket: (token, refreshToken) => {
if (get().projectSocket) return;
initializeProjectSocket: () => {
const token = localStorage.getItem("token");
const refreshToken = localStorage.getItem("refreshToken");
if (get().projectSocket || !token || !refreshToken) return;
const projectSocket = io(`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/project`, socketOptions({ token, refreshToken }));
attachLogs("Project", projectSocket);
set({ projectSocket });
},
initializeSimulationSocket: (token, refreshToken, projectId) => {
if (get().simulationSocket) return;
initializeSimulationSocket: (projectId) => {
const token = localStorage.getItem("token");
const refreshToken = localStorage.getItem("refreshToken");
if (get().simulationSocket || !token || !refreshToken) return;
const simulationSocket = io(`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/Simulation_v1`, socketOptions({ token, refreshToken, projectId }));
attachLogs("Simulation", simulationSocket);
set({ simulationSocket });