feat: Enhance vehicle simulation with draggable path points and interactive controls

This commit is contained in:
2025-09-06 16:03:45 +05:30
226 changed files with 13644 additions and 17458 deletions

View File

@@ -1,208 +1,220 @@
import { useParams } from "react-router-dom";
import React, { useState, useRef, useEffect, Suspense } from "react";
import {
CompareLayoutIcon,
LayoutIcon,
ResizerIcon,
} from "../../icons/SimulationIcons";
import {
useLoadingProgress,
useSaveVersion,
} from "../../../store/builder/store";
import { CompareLayoutIcon, LayoutIcon, ResizerIcon } from "../../icons/SimulationIcons";
import { useLoadingProgress, useIsComparing } from "../../../store/builder/store";
import { useComparisonProduct } from "../../../store/simulation/useSimulationStore";
import { usePlayButtonStore } from "../../../store/ui/usePlayButtonStore";
import { useSceneContext } from "../../../modules/scene/sceneContext";
import { getAllProductsApi } from "../../../services/simulation/products/getallProductsApi";
import Search from "../inputs/Search";
import OuterClick from "../../../utils/outerClick";
import Scene from "../../../modules/scene/scene";
import { useComparisonProduct } from "../../../store/simulation/useSimulationStore";
import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
import { useVersionHistoryStore } from "../../../store/builder/useVersionHistoryStore";
import { useVersionContext } from "../../../modules/builder/version/versionContext";
import { useSceneContext } from "../../../modules/scene/sceneContext";
import { getAllProductsApi } from "../../../services/simulation/products/getallProductsApi";
import { useParams } from "react-router-dom";
import useRestStates from "../../../hooks/useResetStates";
import { getVersionHistoryApi } from "../../../services/factoryBuilder/versionControl/getVersionHistoryApi";
const CompareLayOut = () => {
const { clearComparisonProduct, comparisonProduct, setComparisonProduct } = useComparisonProduct();
const { productStore } = useSceneContext();
const { products } = productStore();
const { versionHistory } = useVersionHistoryStore();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion, setSelectedVersion, clearSelectedVersion } = selectedVersionStore();
const { setLoadingProgress } = useLoadingProgress();
const [width, setWidth] = useState("50vw");
const [isResizing, setIsResizing] = useState(false);
const [showLayoutDropdown, setShowLayoutDropdown] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
const startWidthRef = useRef<number>(0);
const startXRef = useRef<number>(0);
const { setIsVersionSaved } = useSaveVersion();
const { loadingProgress } = useLoadingProgress();
const { setIsPlaying } = usePlayButtonStore();
const { projectId } = useParams();
const { clearComparisonProduct, comparisonProduct, setComparisonProduct } = useComparisonProduct();
const { versionStore } = useSceneContext();
const { versionHistory, selectedVersion, setSelectedVersion, clearSelectedVersion, setVersions } = versionStore();
const { setLoadingProgress } = useLoadingProgress();
const [width, setWidth] = useState("50vw");
const [isResizing, setIsResizing] = useState(false);
const [showLayoutDropdown, setShowLayoutDropdown] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
const startWidthRef = useRef<number>(0);
const startXRef = useRef<number>(0);
const { setIsComparing } = useIsComparing();
const { loadingProgress } = useLoadingProgress();
const { setIsPlaying } = usePlayButtonStore();
const { projectId } = useParams();
const { resetStates } = useRestStates();
useEffect(() => {
if (!comparisonProduct) {
clearSelectedVersion();
}
}, [comparisonProduct])
useEffect(() => {
return () => {
if (selectedVersion?.versionId) {
resetStates();
}
};
}, [selectedVersion?.versionId]);
OuterClick({
contextClassName: ["displayLayouts-container", "selectLayout"],
setMenuVisible: () => setShowLayoutDropdown(false),
});
useEffect(() => {
if (!projectId) return;
const handleStartResizing = (e: React.MouseEvent) => {
e.preventDefault();
setIsResizing(true);
startXRef.current = e.clientX;
if (wrapperRef.current) {
startWidthRef.current = wrapperRef.current.getBoundingClientRect().width;
}
};
getVersionHistoryApi(projectId)
.then((data) => {
const versions: VersionHistory = [];
data.versions.forEach((version: any) => {
versions.push({
version: version.version,
versionId: version.versionId,
versionName: version.versionName,
versionDescription: version.description,
timeStamp: version.createdAt,
createdBy: version.createdBy.userName,
});
});
setVersions(versions);
})
.catch(() => {
console.error("Error fetching version history");
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId]);
const handleMouseMove = (e: MouseEvent) => {
if (!isResizing || !wrapperRef.current) return;
useEffect(() => {
if (!comparisonProduct) {
clearSelectedVersion();
}
}, [comparisonProduct]);
const dx = startXRef.current - e.clientX;
const newWidthPx = startWidthRef.current + dx;
const viewportWidth = window.innerWidth;
const newWidthVw = (newWidthPx / viewportWidth) * 100;
OuterClick({
contextClassName: ["displayLayouts-container", "selectLayout"],
setMenuVisible: () => setShowLayoutDropdown(false),
});
if (newWidthVw <= 10) {
setWidth("0px");
} else if (newWidthVw <= 90) {
setWidth(`${newWidthPx}px`);
}
};
const handleMouseUp = () => {
if (!isResizing) return;
if (wrapperRef.current) {
const finalWidthPx = wrapperRef.current.getBoundingClientRect().width;
const viewportWidth = window.innerWidth;
const finalWidthVw = (finalWidthPx / viewportWidth) * 100;
if (finalWidthVw <= 10) {
setWidth("0px");
setIsVersionSaved(false);
clearComparisonProduct();
setIsPlaying(false);
} else {
setWidth(`${finalWidthVw}vw`);
}
}
setIsResizing(false);
};
useEffect(() => {
if (isResizing) {
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
document.body.classList.add("resizing-active");
}
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
document.body.classList.remove("resizing-active");
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isResizing]);
// Maintain proportional width on window resize
useEffect(() => {
const handleResize = () => {
if (!wrapperRef.current || isResizing) return;
const currentWidth = wrapperRef.current.style.width;
if (currentWidth === "0px" || currentWidth.endsWith("vw")) return;
const pxWidth = parseFloat(currentWidth);
const vwWidth = (pxWidth / window.innerWidth) * 100;
setWidth(`${vwWidth}vw`);
const handleStartResizing = (e: React.MouseEvent) => {
e.preventDefault();
setIsResizing(true);
startXRef.current = e.clientX;
if (wrapperRef.current) {
startWidthRef.current = wrapperRef.current.getBoundingClientRect().width;
}
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [isResizing]);
const handleMouseMove = (e: MouseEvent) => {
if (!isResizing || !wrapperRef.current) return;
const handleSelectLayout = (version: Version) => {
getAllProductsApi(projectId || '', version.versionId || '').then((data) => {
if (data && data.length > 0) {
setSelectedVersion(version);
setComparisonProduct(data[0].productUuid, data[0].productName);
setLoadingProgress(1);
}
})
};
const dx = startXRef.current - e.clientX;
const newWidthPx = startWidthRef.current + dx;
const viewportWidth = window.innerWidth;
const newWidthVw = (newWidthPx / viewportWidth) * 100;
return (
<div
className={`compareLayOut-wrapper ${width === "0px" ? "closed" : ""}`}
ref={wrapperRef}
style={{ width }}
>
{loadingProgress == 0 && selectedVersion?.versionId && (
<button
title="resize-canvas"
id="compare-resize-slider-btn"
className="resizer"
onMouseDown={handleStartResizing}
>
<ResizerIcon />
</button>
)}
<div className="chooseLayout-container">
{selectedVersion?.versionId && (
<div className="compare-layout-canvas-container">
<Suspense fallback={null}>
<Scene layout="Comparison Layout" />
</Suspense>
</div>
)}
if (newWidthVw <= 10) {
setWidth("0px");
} else if (newWidthVw <= 90) {
setWidth(`${newWidthPx}px`);
}
};
{width !== "0px" &&
!selectedVersion?.versionId && ( // Show only if no layout selected
<div className="chooseLayout-wrapper">
<div className="icon">
<CompareLayoutIcon />
</div>
<div className="value">Choose Version to compare</div>
<button
className="selectLayout"
onClick={() => setShowLayoutDropdown(!showLayoutDropdown)}
>
Select Version
</button>
const handleMouseUp = () => {
if (!isResizing) return;
{showLayoutDropdown && (
<div className="displayLayouts-container">
<div className="header">Versions</div>
<Search onChange={() => { }} />
<div className="layouts-container">
{versionHistory.map((version) => (
<button
key={version.versionId}
className="layout-wrapper"
onClick={() => {
handleSelectLayout(version);
setShowLayoutDropdown(false);
}}
>
<LayoutIcon />
<div className="layout">{version.versionName}</div>
</button>
))}
</div>
</div>
)}
if (wrapperRef.current) {
const finalWidthPx = wrapperRef.current.getBoundingClientRect().width;
const viewportWidth = window.innerWidth;
const finalWidthVw = (finalWidthPx / viewportWidth) * 100;
if (finalWidthVw <= 10) {
setWidth("0px");
setIsComparing(false);
clearComparisonProduct();
setIsPlaying(false);
} else {
setWidth(`${finalWidthVw}vw`);
}
}
setIsResizing(false);
};
useEffect(() => {
if (isResizing) {
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
document.body.classList.add("resizing-active");
}
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
document.body.classList.remove("resizing-active");
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isResizing]);
// Maintain proportional width on window resize
useEffect(() => {
const handleResize = () => {
if (!wrapperRef.current || isResizing) return;
const currentWidth = wrapperRef.current.style.width;
if (currentWidth === "0px" || currentWidth.endsWith("vw")) return;
const pxWidth = parseFloat(currentWidth);
const vwWidth = (pxWidth / window.innerWidth) * 100;
setWidth(`${vwWidth}vw`);
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [isResizing]);
const handleSelectLayout = (version: Version) => {
getAllProductsApi(projectId || "", version.versionId || "").then((data) => {
if (data && data.length > 0) {
setSelectedVersion(version);
setComparisonProduct(data[0].productUuid, data[0].productName);
setLoadingProgress(1);
}
});
};
return (
<div className={`compareLayOut-wrapper ${width === "0px" ? "closed" : ""}`} ref={wrapperRef} style={{ width }}>
{loadingProgress == 0 && selectedVersion?.versionId && (
<button title="resize-canvas" id="compare-resize-slider-btn" className="resizer" onMouseDown={handleStartResizing}>
<ResizerIcon />
</button>
)}
<div className="chooseLayout-container">
{selectedVersion?.versionId && (
<div className="compare-layout-canvas-container">
<Suspense fallback={null}>
<Scene layout="Comparison Layout" />
</Suspense>
</div>
)}
{width !== "0px" &&
!selectedVersion?.versionId && ( // Show only if no layout selected
<div className="chooseLayout-wrapper">
<div className="icon">
<CompareLayoutIcon />
</div>
<div className="value">Choose Version to compare</div>
<button className="selectLayout" onClick={() => setShowLayoutDropdown(!showLayoutDropdown)}>
Select Version
</button>
{showLayoutDropdown && (
<div className="displayLayouts-container">
<div className="header">Versions</div>
<Search onChange={() => {}} />
<div className="layouts-container">
{versionHistory.map((version) => (
<button
key={version.versionId}
className="layout-wrapper"
onClick={() => {
handleSelectLayout(version);
setShowLayoutDropdown(false);
}}
>
<LayoutIcon />
<div className="layout">{version.versionName}</div>
</button>
))}
</div>
</div>
)}
</div>
)}
{/* Always show after layout is selected */}
</div>
)}
{/* Always show after layout is selected */}
</div>
</div>
);
</div>
);
};
export default CompareLayOut;