This commit is contained in:
Vishnu 2025-05-19 10:28:25 +05:30
commit b7219bce19
26 changed files with 1482 additions and 381 deletions

View File

@ -1040,3 +1040,189 @@ export const EyeCloseIcon = () => {
</svg>
);
};
export const SaveIcon = () => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.6942 4.26907C12.9155 3.91031 11.0846 3.91031 9.30594 4.26907C7.25504 4.68274 5.77783 6.50871 5.77783 8.63016V17.8926C5.77783 19.5181 7.51712 20.5298 8.89979 19.7085L11.4179 18.2129C11.7775 17.9993 12.2226 17.9993 12.5822 18.2129L15.1003 19.7085C16.483 20.5298 18.2223 19.5181 18.2223 17.8926V8.63016C18.2223 6.50871 16.745 4.68274 14.6942 4.26907Z"
fill="white"
/>
</svg>
);
};
export const SaveVersionIcon = () => {
return (
<svg
width="183"
height="114"
viewBox="0 0 183 114"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<foreignObject x="-38.7816" y="-60.596" width="261.775" height="235.114">
</foreignObject>
<rect
data-figma-bg-blur-radius="60.596"
x="22.4204"
y="0.60596"
width="139.371"
height="112.709"
rx="10.3013"
fill="url(#paint0_linear_4816_8890)"
stroke="#564B69"
strokeWidth="1.21192"
/>
<circle cx="33.9336" cy="10.9082" r="2.42384" fill="#6F6F7A" />
<circle cx="41.2051" cy="10.9082" r="2.42384" fill="#6F6F7A" />
<circle cx="48.4766" cy="10.9082" r="2.42384" fill="#6F6F7A" />
<rect
x="132.099"
y="8.48438"
width="20.6026"
height="3.63576"
rx="1.81788"
fill="#6F6F7A"
/>
<rect
x="0.60596"
y="21.2075"
width="181.788"
height="27.8742"
rx="13.9371"
fill="url(#paint1_linear_4816_8890)"
stroke="#564B69"
strokeWidth="1.21192"
/>
<g clipPath="url(#clip1_4816_8890)">
<path
d="M14.5432 43.9516C9.69069 43.9516 5.74316 40.0038 5.74316 35.1516C5.74316 30.2993 9.69069 26.3516 14.5432 26.3516C19.3956 26.3516 23.3432 30.2993 23.3432 35.1516C23.3432 40.0038 19.3956 43.9516 14.5432 43.9516Z"
fill="#D7EBFF"
/>
<path
d="M23.343 35.1516C23.343 30.2993 19.3954 26.3516 14.543 26.3516V43.9516C19.3954 43.9516 23.343 40.0038 23.343 35.1516Z"
fill="#C4E2FF"
/>
<path
d="M14.5428 42.8044C10.3233 42.8044 6.89062 39.3715 6.89062 35.1522C6.89062 30.9328 10.3233 27.5 14.5428 27.5C18.7623 27.5 22.195 30.9329 22.195 35.1522C22.195 39.3715 18.7623 42.8044 14.5428 42.8044Z"
fill="#88CC2A"
/>
<g filter="url(#filter1_f_4816_8890)">
<path
d="M14.5428 42.8044C10.3233 42.8044 6.89062 39.3715 6.89062 35.1522C6.89062 30.9328 10.3233 27.5 14.5428 27.5C18.7623 27.5 22.195 30.9329 22.195 35.1522C22.195 39.3715 18.7623 42.8044 14.5428 42.8044Z"
fill="#88CC2A"
/>
</g>
<path
d="M22.1952 35.1522C22.1952 30.9329 18.7625 27.5 14.543 27.5V42.8043C18.7625 42.8044 22.1952 39.3715 22.1952 35.1522Z"
fill="#7FB335"
/>
<path
d="M13.7776 38.5936C13.4839 38.5936 13.1903 38.4815 12.9661 38.2573L10.6009 35.8921C10.1526 35.4439 10.1526 34.7172 10.6009 34.269C11.0493 33.8206 11.7757 33.8206 12.2241 34.269L13.7776 35.8224L17.053 32.5472C17.5013 32.0988 18.2277 32.0988 18.6761 32.5472C19.1245 32.9954 19.1245 33.7221 18.6761 34.1703L14.5892 38.2573C14.365 38.4815 14.0713 38.5936 13.7776 38.5936Z"
fill="white"
/>
<path
d="M17.0531 32.5472L14.543 35.0572V38.295C14.5579 38.2815 14.5749 38.2716 14.5893 38.2572L18.6762 34.1703C19.1246 33.7221 19.1246 32.9954 18.6762 32.5472C18.2278 32.0988 17.5015 32.0988 17.0531 32.5472Z"
fill="#EDF0F2"
/>
</g>
<rect
x="29.0859"
y="31.5078"
width="130.887"
height="6.0596"
rx="3.0298"
fill="#6F6F7A"
/>
<path
d="M168.457 32.1172L173.911 37.5708"
stroke="#F65648"
strokeWidth="1.21192"
strokeLinecap="round"
/>
<path
d="M173.911 32.1172L168.457 37.5708"
stroke="#F65648"
strokeWidth="1.21192"
strokeLinecap="round"
/>
<defs>
<clipPath
id="bgblur_0_4816_8890_clip_path"
transform="translate(38.7816 60.596)"
>
<rect
x="22.4204"
y="0.60596"
width="139.371"
height="112.709"
rx="10.3013"
/>
</clipPath>
<filter
id="filter1_f_4816_8890"
x="1.93981"
y="22.5492"
width="25.2058"
height="25.2063"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="BackgroundImageFix"
result="shape"
/>
<feGaussianBlur
stdDeviation="2.47541"
result="effect1_foregroundBlur_4816_8890"
/>
</filter>
<linearGradient
id="paint0_linear_4816_8890"
x1="24.4428"
y1="-3.9557e-07"
x2="36.8899"
y2="151.75"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#333333" />
<stop offset="1" stopColor="#2D2437" />
</linearGradient>
<linearGradient
id="paint1_linear_4816_8890"
x1="3.42139"
y1="20.6016"
x2="4.04875"
y2="59.5968"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#333333" />
<stop offset="1" stopColor="#2D2437" />
</linearGradient>
<clipPath id="clip1_4816_8890">
<rect
width="24"
height="24"
fill="white"
transform="translate(2.54297 23.1484)"
/>
</clipPath>
</defs>
</svg>
);
};

View File

@ -547,7 +547,7 @@ export function CompareLayoutIcon() {
width="142"
height="114"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
@ -582,7 +582,7 @@ export function CompareLayoutIcon() {
width="9"
height="113.5"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
@ -603,7 +603,7 @@ export function CompareLayoutIcon() {
width="32"
height="32"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
@ -624,7 +624,7 @@ export function CompareLayoutIcon() {
width="17.8184"
height="17.8203"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
@ -645,7 +645,7 @@ export function CompareLayoutIcon() {
width="12.9082"
height="12.9062"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
@ -666,7 +666,7 @@ export function CompareLayoutIcon() {
width="12.9082"
height="12.9062"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
@ -687,7 +687,7 @@ export function CompareLayoutIcon() {
width="29"
height="70"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
@ -722,7 +722,7 @@ export function CompareLayoutIcon() {
width="29"
height="70"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
@ -784,7 +784,7 @@ export const ResizerIcon = () => {
width="26.7441"
height="24.0703"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend

View File

@ -22,12 +22,12 @@ import { upsertProductOrEventApi } from "../../../../services/simulation/UpsertP
import { deleteProductApi } from "../../../../services/simulation/deleteProductApi";
import { renameProductApi } from "../../../../services/simulation/renameProductApi";
import { determineExecutionMachineSequences } from "../../../../modules/simulation/simulator/functions/determineExecutionMachineSequences";
import ComparePopUp from "../../../ui/compare/compare";
import ComparePopUp from "../../../ui/compareVersion/Compare";
import {
useCompareStore,
useSaveVersion,
} from "../../../../store/builder/store";
import CompareLayOut from "../../../ui/compare/CompareLayOut";
import CompareLayOut from "../../../ui/compareVersion/CompareLayOut";
import useToggleStore from "../../../../store/useUIToggleStore";
interface Event {

View File

@ -7,25 +7,21 @@ import {
LocationIcon,
} from "../../../icons/ExportCommonIcons";
import RenameInput from "../../../ui/inputs/RenameInput";
import { useVersionStore } from "../../../../store/builder/store";
const VersionHistory = () => {
const userName = localStorage.getItem("userName") ?? "Anonymous";
const initialVersions = [
{
versionName: "v1.0",
timestamp: "April 09, 2025",
savedBy: userName,
},
];
const [versions, setVersions] = useState(initialVersions);
const [selectedVersion, setSelectedVersion] = useState(initialVersions[0]);
const { versions, addVersion, setVersions } = useVersionStore();
const [selectedVersion, setSelectedVersion] = useState(
versions.length > 0 ? versions[0] : null
);
const addNewVersion = () => {
const newVersionNumber = versions.length + 1;
const newVersion = {
versionName: `v${newVersionNumber}.0`,
id: crypto.randomUUID(),
versionLabel: `v${versions.length + 1}.0`,
versionName: "",
timestamp: new Date().toLocaleDateString("en-US", {
year: "numeric",
month: "long",
@ -34,21 +30,26 @@ const VersionHistory = () => {
savedBy: userName,
};
const updated = [newVersion, ...versions];
setVersions(updated);
const newVersions = [newVersion, ...versions];
addVersion(newVersion);
setSelectedVersion(newVersion);
setVersions(newVersions); // bring new one to top
};
const handleSelectVersion = (version: any) => {
setSelectedVersion(version);
const reordered = [version, ...versions.filter((v) => v !== version)];
// Move selected version to top, keep others in same order
const reordered = [version, ...versions.filter((v) => v.id !== version.id)];
setVersions(reordered);
};
const handleTimestampChange = (newTimestamp: string, index: number) => {
const updatedVersions = [...versions];
updatedVersions[index].timestamp = newTimestamp;
setVersions(updatedVersions);
const updated = [...versions];
updated[index].timestamp = newTimestamp;
console.warn("Timestamp updated locally but not persisted in store.");
setVersions(updated); // Optional: persist timestamp change
};
return (
@ -82,49 +83,55 @@ const VersionHistory = () => {
</div>
{/* Current Version Display */}
<div className="version-history-location">
<div className="location-label">
<LocationIcon />
</div>
<div className="location-details">
<div className="current-version">
Current Version ({selectedVersion.versionName})
{selectedVersion && (
<div className="version-history-location">
<div className="location-label">
<LocationIcon />
</div>
<div className="saved-history-count">
{versions.length} Saved History
</div>
</div>
</div>
{/* Versions List */}
<div className="saved-versions-list">
{versions.map((version, index) => (
<button
key={version.versionName}
className="saved-version"
id={`${version.versionName}-${index}`}
onClick={() => handleSelectVersion(version)}
>
<div className="version-name">{version.versionName}</div>
<div className="version-details">
<div className="details">
<span className="timestamp">
<RenameInput
value={version.timestamp}
onRename={(newTimestamp) =>
handleTimestampChange(newTimestamp, index)
}
/>
</span>
<span className="saved-by">
<div className="user-profile">{version.savedBy[0]}</div>
<div className="user-name">{version.savedBy}</div>
</span>
</div>
<ArrowIcon />
<div className="location-details">
<div className="current-version">
Current Version ({selectedVersion.versionLabel})
</div>
</button>
))}
<div className="saved-history-count">
{versions.length} Saved History
</div>
</div>
</div>
)}
{/* Versions List or No Versions Message */}
<div className="saved-versions-list">
{versions.length === 0 ? (
<div className="no-versions-message">No saved versions</div>
) : (
versions.map((version, index) => (
<button
key={version.id}
className="saved-version"
id={`${version.versionName}-${index}`}
onClick={() => handleSelectVersion(version)}
>
<div className="version-name">{version.versionLabel}</div>
<div className="version-details">
<div className="details">
<span className="timestamp">
<RenameInput
value={version.timestamp}
onRename={(newTimestamp) =>
handleTimestampChange(newTimestamp, index)
}
/>
</span>
<span className="saved-by">
<div className="user-profile">{version.savedBy[0]}</div>
<div className="user-name">{version.savedBy}</div>
</span>
</div>
<ArrowIcon />
</div>
</button>
))
)}
</div>
</div>
);

View File

@ -0,0 +1,186 @@
import React, { useState, useEffect, useRef } from "react";
import { useVersionStore } from "../../../../store/builder/store";
import {
CloseIcon,
SaveIcon,
SaveVersionIcon,
} from "../../../icons/ExportCommonIcons";
import { RenameIcon } from "../../../icons/ContextMenuIcons";
import RenderOverlay from "../../../templates/Overlay";
const VersionSaved = () => {
const { versions, updateVersion } = useVersionStore();
const [isEditing, setIsEditing] = useState(false);
const [shouldDismiss, setShouldDismiss] = useState(false);
const [showNotification, setShowNotification] = useState(false);
const [newName, setNewName] = useState("");
const [description, setDescription] = useState("");
const prevVersionCount = useRef(versions.length);
const dismissTimerRef = useRef<NodeJS.Timeout | null>(null);
const latestVersion = versions?.[0];
// Clear dismiss timer when component unmounts
useEffect(() => {
return () => {
if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
};
}, []);
// Handle new version notification and setup dismiss timer
useEffect(() => {
if (versions.length > prevVersionCount.current) {
setShowNotification(true);
setShouldDismiss(false);
setIsEditing(false);
setNewName(versions[0].versionName ?? "");
setDescription(versions[0]?.description ?? "");
// Only start dismiss timer if not in edit mode
if (!isEditing) {
startDismissTimer();
}
prevVersionCount.current = versions.length;
} else if (versions.length < prevVersionCount.current) {
prevVersionCount.current = versions.length;
}
}, [versions, isEditing]);
// Start or restart the dismiss timer
const startDismissTimer = (delay = 5000) => {
if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
dismissTimerRef.current = setTimeout(() => {
console.log("isEditing: ", isEditing);
setShouldDismiss(true);
}, delay);
};
// Hide notification after dismiss animation delay
useEffect(() => {
if (shouldDismiss) {
const timer = setTimeout(() => setShowNotification(false), 200);
return () => clearTimeout(timer);
}
}, [shouldDismiss]);
const handleEditName = () => {
setIsEditing(true);
setNewName(latestVersion?.versionName ?? "");
setDescription(latestVersion?.description ?? "");
// Clear any existing dismiss timer when editing starts
if (dismissTimerRef.current) {
clearTimeout(dismissTimerRef.current);
dismissTimerRef.current = null;
}
};
const handleFinishEdit = () => {
if (latestVersion) {
updateVersion(latestVersion.id, {
versionName: newName,
description,
});
console.log("saved");
startDismissTimer(); // Restart 5s timer after save
}
startDismissTimer(); // Restart 5s timer after save
setIsEditing(false);
};
const handleCancel = () => {
setIsEditing(false);
startDismissTimer(); // Restart 5s timer after cancel
};
const handleClose = () => {
setShouldDismiss(true);
if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
};
if (!showNotification || !latestVersion) return null;
return (
<div className={`versionSaved ${shouldDismiss ? "dismissing" : ""}`}>
<div className="version-header">
<div className="header-wrapper">
<div className="icon">
<SaveIcon />
</div>
<span>Saved New Version</span>
</div>
<button className="close-btn" onClick={handleClose}>
<CloseIcon />
</button>
</div>
<div className="version-details">
<SaveVersionIcon />
<div className="details">
<div className="details-wrapper">
New Version Created {latestVersion.versionLabel}{" "}
{latestVersion.timestamp.toUpperCase()}
</div>
<button onClick={handleEditName}>Edit name</button>
</div>
</div>
{isEditing && (
<RenderOverlay>
<div className="edit-version-popup-wrapper">
<div className="details-wrapper-popup-container">
<div className="header-wrapper">
<RenameIcon />
<div className="label">Rename Version</div>
</div>
<div className="details-wrapper">
<div className="version-name">
<input
type="text"
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder="Enter new version name"
/>
<div className="label">
by @{latestVersion.savedBy}{" "}
{new Date(latestVersion.timestamp).toLocaleString("en-US", {
month: "short",
day: "numeric",
year: "2-digit",
hour: "numeric",
minute: "2-digit",
})}
</div>
</div>
<div className="version-description">
{/* <input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Add description"
/> */}
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Add description"
/>
</div>
</div>
<div className="btn-wrapper">
<button className="cancel" onClick={handleCancel}>
Cancel
</button>
<button className="save" onClick={handleFinishEdit}>
Save
</button>
</div>
</div>
</div>
</RenderOverlay>
)}
</div>
);
};
export default VersionSaved;

View File

@ -1,9 +1,8 @@
import React, { useState } from "react";
import React from "react";
import { InfoIcon } from "../../icons/ShortcutIcons";
import { SaveDiskIcon } from "../../icons/ExportCommonIcons";
import { useCompareStore } from "../../../store/builder/store";
import OuterClick from "../../../utils/outerClick";
import useToggleStore from "../../../store/useUIToggleStore";
interface ComparePopUpProps {
onClose: () => void;

View File

@ -4,6 +4,7 @@ import { ArrowIcon } from "../../icons/ExportCommonIcons";
import { toggleTheme } from "../../../utils/theme";
import useVersionHistoryStore, {
useShortcutStore,
useVersionStore,
} from "../../../store/builder/store";
import { useSubModuleStore } from "../../../store/useModuleStore";
@ -20,6 +21,8 @@ interface MenuItem {
}
const MenuBar: React.FC<MenuBarProps> = ({ setOpenMenu }) => {
const userName = localStorage.getItem("userName") ?? "Anonymous";
const navigate = useNavigate();
const [activeMenu, setActiveMenu] = useState<string | null>(null);
const [activeSubMenu, setActiveSubMenu] = useState<string | null>(null);
@ -59,7 +62,32 @@ const MenuBar: React.FC<MenuBarProps> = ({ setOpenMenu }) => {
File: [
{ label: "New File", shortcut: "Ctrl + N" },
{ label: "Open Local File", shortcut: "Ctrl + O" },
{ label: "Save Version" },
{
label: "Save Version",
action: () => {
const versionStore = useVersionStore.getState();
const versionCount = versionStore.versions.length;
const newVersion = {
id: crypto.randomUUID(),
versionLabel: `v${versionCount + 1}.0`,
timestamp: `${new Date().toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
})} ${new Date().toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "2-digit",
})}`,
savedBy: userName,
};
console.log("newVersion: ", newVersion);
versionStore.addVersion(newVersion);
},
},
{ label: "Make a Copy" },
{ label: "Share" },
{ label: "Rename" },

View File

@ -48,6 +48,7 @@ import NavMesh from "../simulation/vehicle/navMesh/navMesh";
import CalculateAreaGroup from "./groups/calculateAreaGroup";
import LayoutImage from "./layout/layoutImage";
import AssetsGroup from "./assetGroup/assetsGroup";
import { Bvh } from "@react-three/drei";
export default function Builder() {
const state = useThree<Types.ThreeState>(); // Importing the state from the useThree hook, which contains the scene, camera, and other Three.js elements.
@ -203,29 +204,33 @@ export default function Builder() {
<>
<Ground grid={grid} plane={plane} />
<DistanceText key={toggleView} />
<Bvh firstHitOnly>
<DistanceText key={toggleView} />
</Bvh>
<ReferenceDistanceText
key={refTextupdate}
line={ReferenceLineMesh.current}
/>
<SocketResponses
floorPlanGroup={floorPlanGroup}
lines={lines}
floorGroup={floorGroup}
floorGroupAisle={floorGroupAisle}
scene={scene}
onlyFloorlines={onlyFloorlines}
itemsGroup={itemsGroup}
isTempLoader={isTempLoader}
tempLoader={tempLoader}
currentLayerPoint={currentLayerPoint}
floorPlanGroupPoint={floorPlanGroupPoint}
floorPlanGroupLine={floorPlanGroupLine}
zoneGroup={zoneGroup}
dragPointControls={dragPointControls}
/>
<Bvh firstHitOnly>
<SocketResponses
floorPlanGroup={floorPlanGroup}
lines={lines}
floorGroup={floorGroup}
floorGroupAisle={floorGroupAisle}
scene={scene}
onlyFloorlines={onlyFloorlines}
itemsGroup={itemsGroup}
isTempLoader={isTempLoader}
tempLoader={tempLoader}
currentLayerPoint={currentLayerPoint}
floorPlanGroupPoint={floorPlanGroupPoint}
floorPlanGroupLine={floorPlanGroupLine}
zoneGroup={zoneGroup}
dragPointControls={dragPointControls}
/>
</Bvh>
<WallsAndWallItems
CSGGroup={CSGGroup}
@ -237,78 +242,80 @@ export default function Builder() {
hoveredDeletableWallItem={hoveredDeletableWallItem}
/>
<FloorItemsGroup
itemsGroup={itemsGroup}
hoveredDeletableFloorItem={hoveredDeletableFloorItem}
AttachedObject={AttachedObject}
floorGroup={floorGroup}
tempLoader={tempLoader}
isTempLoader={isTempLoader}
plane={plane}
/>
<Bvh firstHitOnly>
<FloorItemsGroup
itemsGroup={itemsGroup}
hoveredDeletableFloorItem={hoveredDeletableFloorItem}
AttachedObject={AttachedObject}
floorGroup={floorGroup}
tempLoader={tempLoader}
isTempLoader={isTempLoader}
plane={plane}
/>
<FloorGroup
floorGroup={floorGroup}
lines={lines}
referencePole={referencePole}
hoveredDeletablePillar={hoveredDeletablePillar}
/>
<FloorGroup
floorGroup={floorGroup}
lines={lines}
referencePole={referencePole}
hoveredDeletablePillar={hoveredDeletablePillar}
/>
<FloorPlanGroup
floorPlanGroup={floorPlanGroup}
floorPlanGroupLine={floorPlanGroupLine}
floorPlanGroupPoint={floorPlanGroupPoint}
floorGroup={floorGroup}
currentLayerPoint={currentLayerPoint}
dragPointControls={dragPointControls}
hoveredDeletablePoint={hoveredDeletablePoint}
hoveredDeletableLine={hoveredDeletableLine}
plane={plane}
line={line}
lines={lines}
onlyFloorline={onlyFloorline}
onlyFloorlines={onlyFloorlines}
ReferenceLineMesh={ReferenceLineMesh}
LineCreated={LineCreated}
isSnapped={isSnapped}
ispreSnapped={ispreSnapped}
snappedPoint={snappedPoint}
isSnappedUUID={isSnappedUUID}
isAngleSnapped={isAngleSnapped}
anglesnappedPoint={anglesnappedPoint}
/>
<FloorPlanGroup
floorPlanGroup={floorPlanGroup}
floorPlanGroupLine={floorPlanGroupLine}
floorPlanGroupPoint={floorPlanGroupPoint}
floorGroup={floorGroup}
currentLayerPoint={currentLayerPoint}
dragPointControls={dragPointControls}
hoveredDeletablePoint={hoveredDeletablePoint}
hoveredDeletableLine={hoveredDeletableLine}
plane={plane}
line={line}
lines={lines}
onlyFloorline={onlyFloorline}
onlyFloorlines={onlyFloorlines}
ReferenceLineMesh={ReferenceLineMesh}
LineCreated={LineCreated}
isSnapped={isSnapped}
ispreSnapped={ispreSnapped}
snappedPoint={snappedPoint}
isSnappedUUID={isSnappedUUID}
isAngleSnapped={isAngleSnapped}
anglesnappedPoint={anglesnappedPoint}
/>
<ZoneGroup />
<ZoneGroup />
<FloorGroupAilse
floorGroupAisle={floorGroupAisle}
plane={plane}
floorPlanGroupLine={floorPlanGroupLine}
floorPlanGroupPoint={floorPlanGroupPoint}
line={line}
lines={lines}
currentLayerPoint={currentLayerPoint}
dragPointControls={dragPointControls}
floorPlanGroup={floorPlanGroup}
ReferenceLineMesh={ReferenceLineMesh}
LineCreated={LineCreated}
isSnapped={isSnapped}
ispreSnapped={ispreSnapped}
snappedPoint={snappedPoint}
isSnappedUUID={isSnappedUUID}
isAngleSnapped={isAngleSnapped}
anglesnappedPoint={anglesnappedPoint}
/>
<FloorGroupAilse
floorGroupAisle={floorGroupAisle}
plane={plane}
floorPlanGroupLine={floorPlanGroupLine}
floorPlanGroupPoint={floorPlanGroupPoint}
line={line}
lines={lines}
currentLayerPoint={currentLayerPoint}
dragPointControls={dragPointControls}
floorPlanGroup={floorPlanGroup}
ReferenceLineMesh={ReferenceLineMesh}
LineCreated={LineCreated}
isSnapped={isSnapped}
ispreSnapped={ispreSnapped}
snappedPoint={snappedPoint}
isSnappedUUID={isSnappedUUID}
isAngleSnapped={isAngleSnapped}
anglesnappedPoint={anglesnappedPoint}
/>
{/* <AssetsGroup /> */}
{/* <AssetsGroup /> */}
<MeasurementTool />
<MeasurementTool />
<CalculateAreaGroup />
<CalculateAreaGroup />
<NavMesh lines={lines} />
<NavMesh lines={lines} />
<LayoutImage />
<LayoutImage />
</Bvh>
</>
);
}

View File

@ -0,0 +1,90 @@
import * as THREE from 'three';
import { useRef, useState } from 'react';
import { useFrame } from '@react-three/fiber';
import { Clouds, Cloud } from '@react-three/drei';
interface CloudGroupProps {
initialX: number;
initialZ: number;
speed: number;
height: number;
}
function CloudGroup({ initialX, initialZ, speed, height }: CloudGroupProps) {
const group = useRef<THREE.Group>(null);
useFrame((_, delta) => {
if (group.current) {
group.current.position.x += delta * speed;
group.current.position.z += delta * speed * 0.5;
if (group.current.position.x > 500) group.current.position.x = -500;
if (group.current.position.z > 500) group.current.position.z = -500;
}
});
return (
<group ref={group} position={[initialX, height, initialZ]}>
<Clouds material={THREE.MeshBasicMaterial} frustumCulled={false} limit={10000}>
<Cloud
seed={Math.random() * 100}
bounds={[100, 15, 100]}
volume={50}
color="#eeeeee"
scale={4}
fade={10000}
/>
<Cloud
seed={Math.random() * 100}
bounds={[100, 15, 100]}
volume={50}
scale={6}
color="#ffffff"
fade={10000}
/>
<Cloud
seed={Math.random() * 100}
bounds={[100, 15, 100]}
volume={50}
scale={4}
fade={10000}
color="#f0f0f0"
/>
</Clouds>
</group>
);
}
export function MovingClouds() {
const savedTheme: string | null = localStorage.getItem("theme");
const [theme, setTheme] = useState(savedTheme || "light");
const cloudGroups = [
{ initialX: 0, initialZ: 0, speed: 8, height: 300 },
{ initialX: -300, initialZ: 100, speed: 10, height: 300 },
{ initialX: 200, initialZ: -150, speed: 4, height: 300 },
{ initialX: -400, initialZ: -200, speed: 7, height: 300 },
{ initialX: 400, initialZ: 300, speed: 5, height: 300 },
{ initialX: -200, initialZ: -300, speed: 7, height: 300 },
{ initialX: 300, initialZ: 200, speed: 10, height: 300 },
];
return (
<>
{theme === 'light' &&
<>
{cloudGroups.map((group, index) => (
<CloudGroup
key={index}
initialX={group.initialX}
initialZ={group.initialZ}
speed={group.speed}
height={group.height}
/>
))}
</>
}
</>
);
}

View File

@ -2,50 +2,37 @@ import { useTileDistance, useToggleView } from "../../../store/builder/store";
import * as CONSTANTS from "../../../types/world/worldConstants";
const Ground = ({ grid, plane }: any) => {
const { toggleView } = useToggleView();
const { planeValue, gridValue } = useTileDistance();
const { toggleView } = useToggleView();
const { planeValue, gridValue } = useTileDistance();
return (
<mesh name="Ground">
<mesh
ref={grid}
name="Grid"
position={
!toggleView
? CONSTANTS.gridConfig.position3D
: CONSTANTS.gridConfig.position2D
}
>
<gridHelper
args={[
gridValue.size,
gridValue.divisions,
// CONSTANTS.gridConfig.size,
// CONSTANTS.gridConfig.divisions,
CONSTANTS.gridConfig.primaryColor,
CONSTANTS.gridConfig.secondaryColor,
]}
/>
</mesh>
<mesh
ref={plane}
rotation-x={CONSTANTS.planeConfig.rotation}
position={
!toggleView
? CONSTANTS.planeConfig.position3D
: CONSTANTS.planeConfig.position2D
}
name="Plane"
receiveShadow
>
<planeGeometry
args={[planeValue.width, planeValue.height]}
// args={[CONSTANTS.planeConfig.width, CONSTANTS.planeConfig.height]}
/>
<meshBasicMaterial color={CONSTANTS.planeConfig.color} />
</mesh>
</mesh>
);
return (
<mesh name="Ground">
<mesh
ref={grid}
name="Grid"
position={!toggleView ? CONSTANTS.gridConfig.position3D : CONSTANTS.gridConfig.position2D}
>
<gridHelper
args={[
gridValue.size,
gridValue.divisions,
CONSTANTS.gridConfig.primaryColor,
CONSTANTS.gridConfig.secondaryColor,
]}
/>
</mesh>
<mesh
ref={plane}
rotation-x={CONSTANTS.planeConfig.rotation}
position={!toggleView ? CONSTANTS.planeConfig.position3D : CONSTANTS.planeConfig.position2D}
name="Plane"
receiveShadow
>
<planeGeometry args={[planeValue.width, planeValue.height]} />
<meshBasicMaterial color={CONSTANTS.planeConfig.color} />
</mesh>
</mesh>
);
};
export default Ground;

View File

@ -2,111 +2,105 @@ import { useRef, useEffect } from "react";
import { useThree } from "@react-three/fiber";
import * as THREE from "three";
import {
useAzimuth,
useElevation,
useShadows,
useSunPosition,
useFloorItems,
useWallItems,
useTileDistance,
useAzimuth,
useElevation,
useShadows,
useSunPosition,
useFloorItems,
useWallItems,
useTileDistance,
} from "../../../store/builder/store";
import * as CONSTANTS from "../../../types/world/worldConstants";
const shadowWorker = new Worker(
new URL(
"../../../services/factoryBuilder/webWorkers/shadowWorker",
import.meta.url
)
new URL(
"../../../services/factoryBuilder/webWorkers/shadowWorker",
import.meta.url
)
);
export default function Shadows() {
const { shadows, setShadows } = useShadows();
const { sunPosition, setSunPosition } = useSunPosition();
const lightRef = useRef<THREE.DirectionalLight | null>(null);
const targetRef = useRef<THREE.Object3D | null>(null);
const { controls, gl } = useThree();
const { elevation, setElevation } = useElevation();
const { azimuth, setAzimuth } = useAzimuth();
const { floorItems } = useFloorItems();
const { wallItems } = useWallItems();
const { planeValue } = useTileDistance();
const { shadows, setShadows } = useShadows();
const { sunPosition, setSunPosition } = useSunPosition();
const lightRef = useRef<THREE.DirectionalLight | null>(null);
const targetRef = useRef<THREE.Object3D | null>(null);
const { controls, gl } = useThree();
const { elevation, setElevation } = useElevation();
const { azimuth, setAzimuth } = useAzimuth();
const { floorItems } = useFloorItems();
const { wallItems } = useWallItems();
const { planeValue } = useTileDistance();
useEffect(() => {
gl.shadowMap.enabled = true;
gl.shadowMap.type = THREE.PCFShadowMap;
}, [gl, floorItems, wallItems]);
useEffect(() => {
gl.shadowMap.enabled = true;
gl.shadowMap.type = THREE.PCFShadowMap;
}, [gl, floorItems, wallItems]);
useEffect(() => {
if (lightRef.current && targetRef.current) {
lightRef.current.target = targetRef.current;
}
}, []);
useEffect(() => {
if (lightRef.current && targetRef.current) {
lightRef.current.target = targetRef.current;
}
}, []);
useEffect(() => {
shadowWorker.onmessage = (event) => {
const { lightPosition, controlsTarget } = event.data;
if (lightRef.current && targetRef.current && controls) {
lightRef.current.position.copy(lightPosition);
targetRef.current.position.copy(controlsTarget);
}
useEffect(() => {
shadowWorker.onmessage = (event) => {
const { lightPosition, controlsTarget } = event.data;
if (lightRef.current && targetRef.current && controls) {
lightRef.current.position.copy(lightPosition);
targetRef.current.position.copy(controlsTarget);
gl.shadowMap.needsUpdate = true;
}
};
}, [shadowWorker, controls]);
const updateShadows = () => {
if (controls && shadowWorker) {
const offsetDistance = CONSTANTS.shadowConfig.shadowOffset;
const controlsTarget = (controls as any).getTarget();
shadowWorker.postMessage({ controlsTarget, sunPosition, offsetDistance });
}
};
}, [shadowWorker, controls]);
const updateShadows = () => {
if (controls && shadowWorker) {
const offsetDistance = CONSTANTS.shadowConfig.shadowOffset;
const controlsTarget = (controls as any).getTarget();
shadowWorker.postMessage({ controlsTarget, sunPosition, offsetDistance });
}
};
useEffect(() => {
if (controls && shadows) {
updateShadows();
(controls as any).addEventListener("update", updateShadows);
return () => {
(controls as any).removeEventListener("update", updateShadows);
};
}
}, [controls, elevation, azimuth, shadows]);
useEffect(() => {
if (controls && shadows) {
updateShadows();
(controls as any).addEventListener("update", updateShadows);
return () => {
(controls as any).removeEventListener("update", updateShadows);
};
}
}, [controls, elevation, azimuth, shadows]);
return (
<>
{/* {(lightRef.current?.shadow) &&
<cameraHelper visible={shadows} args={[lightRef.current.shadow.camera]} />
} */}
<directionalLight
ref={lightRef}
castShadow={shadows}
shadow-mapSize-width={CONSTANTS.shadowConfig.shadowmapSizewidth}
shadow-mapSize-height={CONSTANTS.shadowConfig.shadowmapSizeheight}
shadow-camera-far={CONSTANTS.shadowConfig.shadowcamerafar}
shadow-camera-near={CONSTANTS.shadowConfig.shadowcameranear}
shadow-camera-top={CONSTANTS.shadowConfig.shadowcameratop}
shadow-camera-bottom={CONSTANTS.shadowConfig.shadowcamerabottom}
shadow-camera-left={CONSTANTS.shadowConfig.shadowcameraleft}
shadow-camera-right={CONSTANTS.shadowConfig.shadowcameraright}
shadow-bias={CONSTANTS.shadowConfig.shadowbias}
shadow-normalBias={CONSTANTS.shadowConfig.shadownormalBias}
/>
<object3D ref={targetRef} />
<mesh
position={CONSTANTS.shadowConfig.shadowMaterialPosition}
rotation={CONSTANTS.shadowConfig.shadowMaterialRotation}
receiveShadow
>
{/* <planeGeometry
args={[CONSTANTS.planeConfig.width, CONSTANTS.planeConfig.height]}
/>
<shadowMaterial
opacity={CONSTANTS.shadowConfig.shadowMaterialOpacity}
transparent
/> */}
<planeGeometry args={[planeValue.width, planeValue.height]} />
<shadowMaterial
opacity={CONSTANTS.shadowConfig.shadowMaterialOpacity}
transparent
/>
</mesh>
</>
);
return (
<>
{/* {(lightRef.current?.shadow) &&
<cameraHelper visible={shadows} args={[lightRef.current.shadow.camera]} />
} */}
<directionalLight
ref={lightRef}
castShadow={shadows}
shadow-mapSize-width={CONSTANTS.shadowConfig.shadowmapSizewidth}
shadow-mapSize-height={CONSTANTS.shadowConfig.shadowmapSizeheight}
shadow-camera-far={CONSTANTS.shadowConfig.shadowcamerafar}
shadow-camera-near={CONSTANTS.shadowConfig.shadowcameranear}
shadow-camera-top={CONSTANTS.shadowConfig.shadowcameratop}
shadow-camera-bottom={CONSTANTS.shadowConfig.shadowcamerabottom}
shadow-camera-left={CONSTANTS.shadowConfig.shadowcameraleft}
shadow-camera-right={CONSTANTS.shadowConfig.shadowcameraright}
shadow-bias={CONSTANTS.shadowConfig.shadowbias}
shadow-normalBias={CONSTANTS.shadowConfig.shadownormalBias}
/>
<object3D ref={targetRef} />
<mesh
position={CONSTANTS.shadowConfig.shadowMaterialPosition}
rotation={CONSTANTS.shadowConfig.shadowMaterialRotation}
receiveShadow
>
<planeGeometry args={[planeValue.width, planeValue.height]} />
<shadowMaterial
opacity={CONSTANTS.shadowConfig.shadowMaterialOpacity}
transparent
/>
</mesh>
</>
);
}

View File

@ -9,35 +9,32 @@ import Simulation from "../simulation/simulation";
import Collaboration from "../collaboration/collaboration";
export default function Scene() {
const map = useMemo(
() => [
{ name: "forward", keys: ["ArrowUp", "w", "W"] },
{ name: "backward", keys: ["ArrowDown", "s", "S"] },
{ name: "left", keys: ["ArrowLeft", "a", "A"] },
{ name: "right", keys: ["ArrowRight", "d", "D"] },
],
[]
);
const map = useMemo(() => [
{ name: "forward", keys: ["ArrowUp", "w", "W"] },
{ name: "backward", keys: ["ArrowDown", "s", "S"] },
{ name: "left", keys: ["ArrowLeft", "a", "A"] },
{ name: "right", keys: ["ArrowRight", "d", "D"] },
], []);
return (
<KeyboardControls map={map}>
<Canvas
eventPrefix="client"
gl={{ powerPreference: "high-performance", antialias: true }}
onContextMenu={(e) => {
e.preventDefault();
}}
>
<Setup />
return (
<KeyboardControls map={map}>
<Canvas
eventPrefix="client"
gl={{ powerPreference: "high-performance", antialias: true }}
onContextMenu={(e) => {
e.preventDefault();
}}
>
<Setup />
<Collaboration />
<Collaboration />
<Builder />
<Builder />
<Simulation />
<Simulation />
<Visualization />
</Canvas>
</KeyboardControls>
);
<Visualization />
</Canvas>
</KeyboardControls>
);
}

View File

@ -5,6 +5,7 @@ import Controls from '../controls/controls';
import { Environment } from '@react-three/drei'
import background from "../../../assets/textures/hdr/mudroadpuresky2k.hdr";
import { MovingClouds } from '../clouds/clouds';
function Setup() {
return (
@ -17,6 +18,8 @@ function Setup() {
<PostProcessing />
<MovingClouds />
<Environment files={background} environmentIntensity={1.5} />
</>
)

View File

@ -2,36 +2,43 @@ import React, { useEffect } from 'react'
import { useMaterialStore } from '../../../../../store/simulation/useMaterialStore';
import { useConveyorStore } from '../../../../../store/simulation/useConveyorStore';
import { useResetButtonStore } from '../../../../../store/usePlayButtonStore';
import { findConveyorInSequences } from '../../../simulator/functions/findConveyorInSequences';
import { useSelectedProduct } from '../../../../../store/simulation/useSimulationStore';
import { useProductStore } from '../../../../../store/simulation/useProductStore';
function ConveyorInstance({ conveyor }: { conveyor: ConveyorStatus }) {
const { getProductById } = useProductStore();
const { selectedProduct } = useSelectedProduct();
const { materials, getMaterialsByCurrentModelUuid } = useMaterialStore();
const { isReset } = useResetButtonStore();
const { setConveyorPaused } = useConveyorStore();
useEffect(() => {
const product = getProductById(selectedProduct.productId);
if (!product) return;
const sequenceInfo = findConveyorInSequences(product, conveyor.modelUuid);
if (!sequenceInfo) return;
const { currentSubSequence } = sequenceInfo;
const conveyorMaterials = getMaterialsByCurrentModelUuid(conveyor.modelUuid);
if (conveyorMaterials && conveyorMaterials?.length > 0) {
const hasPausedMaterials = conveyorMaterials.some(material => material.isPaused);
if (conveyorMaterials && conveyorMaterials.length > 0) {
const shouldPauseSubsequence = currentSubSequence.some(subConveyor => {
if (subConveyor.type !== 'transfer') return false;
const subMaterials = getMaterialsByCurrentModelUuid(subConveyor.modelUuid);
return subMaterials?.some(m => m.isPaused) ?? false;
});
if (hasPausedMaterials) {
setConveyorPaused(conveyor.modelUuid, true);
} else {
setConveyorPaused(conveyor.modelUuid, false);
}
currentSubSequence.forEach(subConveyor => {
if (subConveyor.type === 'transfer') {
setConveyorPaused(subConveyor.modelUuid, shouldPauseSubsequence);
}
});
}
}, [materials, conveyor.modelUuid, getMaterialsByCurrentModelUuid, setConveyorPaused, isReset, selectedProduct.productId, getProductById]);
}, [materials, conveyor.modelUuid, getMaterialsByCurrentModelUuid, setConveyorPaused, isReset]);
useEffect(() => {
// console.log('conveyor: ', conveyor);
}, [conveyor])
return (
<>
</>
)
return null;
}
export default ConveyorInstance
export default React.memo(ConveyorInstance);

View File

@ -52,7 +52,7 @@ function Products() {
});
}
}
}, [selectedProduct, products, isReset]);
}, [selectedProduct, products, isReset, isPlaying]);
useEffect(() => {
if (selectedProduct.productId) {
@ -66,7 +66,7 @@ function Products() {
});
}
}
}, [selectedProduct, products, isReset]);
}, [selectedProduct, products, isReset, isPlaying]);
useEffect(() => {
if (selectedProduct.productId) {
@ -80,7 +80,7 @@ function Products() {
});
}
}
}, [selectedProduct, products, isReset]);
}, [selectedProduct, products, isReset, isPlaying]);
useEffect(() => {
if (selectedProduct.productId) {
@ -94,7 +94,7 @@ function Products() {
});
}
}
}, [selectedProduct, products, isReset]);
}, [selectedProduct, products, isReset, isPlaying]);
useEffect(() => {
if (selectedProduct.productId) {

View File

@ -13,6 +13,7 @@ import { useVehicleStore } from '../../../../../store/simulation/useVehicleStore
import { useStorageUnitStore } from '../../../../../store/simulation/useStorageUnitStore';
import { useSelectedProduct } from '../../../../../store/simulation/useSimulationStore';
import { useTriggerHandler } from '../../../triggers/triggerHandler/useTriggerHandler';
import { useCheckActiveRoboticArmsInSubsequence } from '../../../simulator/functions/checkActiveRoboticArmsInSubsequence';
function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
@ -30,13 +31,16 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
const { setArmBotActive, setArmBotState, removeCurrentAction } = useArmBotStore();
const { decrementVehicleLoad, removeLastMaterial } = useVehicleStore();
const { removeLastMaterial: removeLastStorageMaterial, updateCurrentLoad } = useStorageUnitStore();
const { setIsVisible, getMaterialById } = useMaterialStore();
const { setIsVisible, setIsPaused, getMaterialById } = useMaterialStore();
const { selectedProduct } = useSelectedProduct();
const { getActionByUuid, getEventByActionUuid, getEventByModelUuid } = useProductStore();
const { getActionByUuid, getEventByActionUuid, getEventByModelUuid, getProductById } = useProductStore();
const { triggerPointActions } = useTriggerHandler();
const { isPlaying } = usePlayButtonStore();
const { isReset } = useResetButtonStore();
const { isPaused } = usePauseButtonStore();
const checkActiveRoboticArms = useCheckActiveRoboticArmsInSubsequence();
const lastRemoved = useRef<{ type: string, materialId: string } | null>(null);
function firstFrame() {
startTime = performance.now();
@ -63,6 +67,7 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
removeLastStorageMaterial(previousModel.modelUuid);
updateCurrentLoad(previousModel.modelUuid, -1)
}
lastRemoved.current = { type: previousModel.type, materialId: armBot.currentAction.materialId };
} else {
setIsVisible(armBot.currentAction.materialId, false);
}
@ -76,11 +81,20 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
if (armBot.currentAction) {
const action = getActionByUuid(selectedProduct.productId, armBot.currentAction.actionUuid);
const model = getEventByModelUuid(selectedProduct.productId, action?.triggers[0].triggeredAsset?.triggeredModel.modelUuid || '');
if (action && action.triggers[0].triggeredAsset?.triggeredModel.modelUuid) {
const model = getEventByModelUuid(selectedProduct.productId, action?.triggers[0].triggeredAsset?.triggeredModel.modelUuid);
if (!model) return;
if (model.type === 'transfer') {
setIsVisible(armBot.currentAction.materialId || '', true);
const product = getProductById(selectedProduct.productId);
if (product) {
const result = checkActiveRoboticArms(product, armBot.modelUuid);
// console.log('result: ', result);
// if (result?.hasActiveRoboticArm) {
// lastRemoved.current = null;
// }
}
} else if (model.type === 'machine') {
//
} else if (model.type === 'vehicle') {
@ -287,6 +301,14 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
setArmBotState(armBot.modelUuid, "idle")
setCurrentPhase("rest");
setPath([])
if (lastRemoved.current) {
if (lastRemoved.current.type === 'transfer') {
setIsPaused(lastRemoved.current.materialId, true)
} else {
setIsPaused(lastRemoved.current.materialId, false)
}
}
}
}
const logStatus = (id: string, status: string) => {

View File

@ -0,0 +1,188 @@
import { extractTriggersFromPoint } from "./extractTriggersFromPoint";
import { useArmBotStore } from "../../../../store/simulation/useArmBotStore";
export function getRoboticArmSequencesInProduct(
product: {
productName: string;
productId: string;
eventDatas: EventsSchema[];
}
): EventsSchema[][][] {
// Get all machine sequences for this product
const machineSequences = determineExecutionMachineSequences([product]);
const allRoboticArmSequences: EventsSchema[][][] = [];
// Process each machine sequence separately
for (const machineSequence of machineSequences) {
const roboticArmSequencesForThisMachineSequence: EventsSchema[][] = [];
let currentRoboticArmSequence: EventsSchema[] = [];
for (const event of machineSequence) {
if (event.type === 'roboticArm') {
// Add robotic arm to current sequence
currentRoboticArmSequence.push(event);
} else if (event.type === 'vehicle') {
// Vehicle encountered - split the sequence
if (currentRoboticArmSequence.length > 0) {
roboticArmSequencesForThisMachineSequence.push([...currentRoboticArmSequence]);
currentRoboticArmSequence = [];
}
}
// Other machine types continue the current sequence
}
// Add any remaining robotic arms in the current sequence
if (currentRoboticArmSequence.length > 0) {
roboticArmSequencesForThisMachineSequence.push([...currentRoboticArmSequence]);
}
if (roboticArmSequencesForThisMachineSequence.length > 0) {
allRoboticArmSequences.push(roboticArmSequencesForThisMachineSequence);
}
}
return allRoboticArmSequences;
}
export function findRoboticArmSubsequence(
product: {
productName: string;
productId: string;
eventDatas: EventsSchema[];
},
roboticArmModelUuid: string
): {
allSequences: EventsSchema[][][];
parentSequence: EventsSchema[][];
currentSubSequence: EventsSchema[];
} | null {
const allSequences = getRoboticArmSequencesInProduct(product);
for (const parentSequence of allSequences) {
for (const currentSubSequence of parentSequence) {
const hasTargetRoboticArm = currentSubSequence.some(
event => event.type === 'roboticArm' && event.modelUuid === roboticArmModelUuid
);
if (hasTargetRoboticArm) {
return {
allSequences,
parentSequence,
currentSubSequence
};
}
}
}
return null;
}
// React component/hook that uses the pure functions
export function useCheckActiveRoboticArmsInSubsequence() {
const { getArmBotById } = useArmBotStore();
return function (product: {
productName: string;
productId: string;
eventDatas: EventsSchema[];
}, roboticArmModelUuid: string) {
const result = findRoboticArmSubsequence(product, roboticArmModelUuid);
if (!result) return null;
const hasActiveRoboticArm = result.currentSubSequence.some(event => {
if (event.type === 'roboticArm' && event.modelUuid !== roboticArmModelUuid) {
const armBot = getArmBotById(event.modelUuid);
return armBot?.isActive;
}
return false;
});
return {
...result,
hasActiveRoboticArm
};
};
}
// Helper function to get machine sequences (simplified from your example)
function determineExecutionMachineSequences(products: productsSchema): EventsSchema[][] {
const pointToEventMap = new Map<string, EventsSchema>();
const allPoints: PointsScheme[] = [];
// First pass: map points to their corresponding events
products.forEach(product => {
product.eventDatas.forEach(event => {
if (event.type === 'transfer') {
event.points.forEach(point => {
pointToEventMap.set(point.uuid, event);
allPoints.push(point);
});
} else if (
event.type === 'vehicle' ||
event.type === 'machine' ||
event.type === 'storageUnit' ||
event.type === 'roboticArm'
) {
pointToEventMap.set(event.point.uuid, event);
allPoints.push(event.point);
}
});
});
// Build dependency graph
const dependencyGraph = new Map<string, string[]>();
const triggeredPoints = new Set<string>();
allPoints.forEach(point => {
const triggers = extractTriggersFromPoint(point);
const dependencies: string[] = [];
triggers.forEach(trigger => {
const targetUuid = trigger.triggeredAsset?.triggeredPoint?.pointUuid;
if (targetUuid && pointToEventMap.has(targetUuid)) {
dependencies.push(targetUuid);
triggeredPoints.add(targetUuid);
}
});
dependencyGraph.set(point.uuid, dependencies);
});
// Find root points (points that aren't triggered by others)
const rootPoints = allPoints.filter(point =>
!triggeredPoints.has(point.uuid) &&
dependencyGraph.get(point.uuid)?.length
);
const executionSequences: EventsSchema[][] = [];
function buildSequence(startUuid: string): EventsSchema[] {
const sequence: EventsSchema[] = [];
const visited = new Set<string>();
function traverse(uuid: string) {
if (visited.has(uuid)) return;
visited.add(uuid);
const event = pointToEventMap.get(uuid);
if (event && !sequence.includes(event)) {
sequence.push(event);
}
const nextPoints = dependencyGraph.get(uuid) || [];
nextPoints.forEach(nextUuid => traverse(nextUuid));
}
traverse(startUuid);
return sequence;
}
// Build sequences from root points
rootPoints.forEach(root => {
executionSequences.push(buildSequence(root.uuid));
});
return executionSequences;
}

View File

@ -0,0 +1,49 @@
import { getConveyorSequencesInProduct } from "./getConveyorSequencesForProduct";
export function findConveyorInSequences(
product: {
productName: string;
productId: string;
eventDatas: EventsSchema[];
},
conveyorUuid: string
): {
allSequences: EventsSchema[][][];
parentSequence: EventsSchema[][];
currentSubSequence: EventsSchema[];
} | null {
// Get all conveyor sequences
const allSequences = getConveyorSequencesInProduct(product);
// Search through all sequences
for (const parentSequence of allSequences) {
for (const currentSubSequence of parentSequence) {
for (const conveyor of currentSubSequence) {
// Check if this is the conveyor we're looking for
if (conveyor.modelUuid === conveyorUuid) {
return {
allSequences,
parentSequence,
currentSubSequence
};
}
// Also check points in case the UUID matches a point's conveyor
if (conveyor.type === 'transfer') {
for (const point of conveyor.points) {
if (point.uuid === conveyorUuid) {
return {
allSequences,
parentSequence,
currentSubSequence
};
}
}
}
}
}
}
// Conveyor not found
return null;
}

View File

@ -0,0 +1,126 @@
import { extractTriggersFromPoint } from "./extractTriggersFromPoint";
export function getConveyorSequencesInProduct(
product: {
productName: string;
productId: string;
eventDatas: EventsSchema[];
}
): EventsSchema[][][] { // Now returns array of array of arrays
// Get all machine sequences for this product
const machineSequences = determineExecutionMachineSequences([product]);
const allConveyorSequences: EventsSchema[][][] = [];
// Process each machine sequence separately
for (const machineSequence of machineSequences) {
const conveyorSequencesForThisMachineSequence: EventsSchema[][] = [];
let currentConveyorSequence: EventsSchema[] = [];
for (const event of machineSequence) {
if (event.type === 'transfer') {
// Add conveyor to current sequence
currentConveyorSequence.push(event);
} else if (event.type === 'vehicle') {
// Vehicle encountered - split the sequence
if (currentConveyorSequence.length > 0) {
conveyorSequencesForThisMachineSequence.push([...currentConveyorSequence]);
currentConveyorSequence = [];
}
}
// Other machine types don't affect the conveyor sequence
}
// Add any remaining conveyors in the current sequence
if (currentConveyorSequence.length > 0) {
conveyorSequencesForThisMachineSequence.push([...currentConveyorSequence]);
}
if (conveyorSequencesForThisMachineSequence.length > 0) {
allConveyorSequences.push(conveyorSequencesForThisMachineSequence);
}
}
return allConveyorSequences;
}
// Helper function to get machine sequences (simplified from your example)
function determineExecutionMachineSequences(products: productsSchema): EventsSchema[][] {
const pointToEventMap = new Map<string, EventsSchema>();
const allPoints: PointsScheme[] = [];
// First pass: map points to their corresponding events
products.forEach(product => {
product.eventDatas.forEach(event => {
if (event.type === 'transfer') {
event.points.forEach(point => {
pointToEventMap.set(point.uuid, event);
allPoints.push(point);
});
} else if (
event.type === 'vehicle' ||
event.type === 'machine' ||
event.type === 'storageUnit' ||
event.type === 'roboticArm'
) {
pointToEventMap.set(event.point.uuid, event);
allPoints.push(event.point);
}
});
});
// Build dependency graph
const dependencyGraph = new Map<string, string[]>();
const triggeredPoints = new Set<string>();
allPoints.forEach(point => {
const triggers = extractTriggersFromPoint(point);
const dependencies: string[] = [];
triggers.forEach(trigger => {
const targetUuid = trigger.triggeredAsset?.triggeredPoint?.pointUuid;
if (targetUuid && pointToEventMap.has(targetUuid)) {
dependencies.push(targetUuid);
triggeredPoints.add(targetUuid);
}
});
dependencyGraph.set(point.uuid, dependencies);
});
// Find root points (points that aren't triggered by others)
const rootPoints = allPoints.filter(point =>
!triggeredPoints.has(point.uuid) &&
dependencyGraph.get(point.uuid)?.length
);
const executionSequences: EventsSchema[][] = [];
function buildSequence(startUuid: string): EventsSchema[] {
const sequence: EventsSchema[] = [];
const visited = new Set<string>();
function traverse(uuid: string) {
if (visited.has(uuid)) return;
visited.add(uuid);
const event = pointToEventMap.get(uuid);
if (event && !sequence.includes(event)) {
sequence.push(event);
}
const nextPoints = dependencyGraph.get(uuid) || [];
nextPoints.forEach(nextUuid => traverse(nextUuid));
}
traverse(startUuid);
return sequence;
}
// Build sequences from root points
rootPoints.forEach(root => {
executionSequences.push(buildSequence(root.uuid));
});
return executionSequences;
}

View File

@ -138,6 +138,7 @@ export function useTriggerHandler() {
if (armBot.isActive === false && armBot.state === 'idle') {
// Handle current action from arm bot
setIsPaused(materialId, true);
handleAction(action, materialId);
} else {
@ -291,6 +292,9 @@ export function useTriggerHandler() {
// Machine to Robotic Arm
if (materialId && trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint && trigger.triggeredAsset.triggeredAction) {
const material = getMaterialById(materialId);
setIsPaused(materialId, true);
if (material) {
const action = getActionByUuid(selectedProduct.productId, trigger.triggeredAsset.triggeredAction.actionUuid);
const armBot = getArmBotById(trigger.triggeredAsset?.triggeredModel.modelUuid);
@ -302,7 +306,7 @@ export function useTriggerHandler() {
})
setCurrentLocation(material.materialId, {
modelUuid: trigger.triggeredAsset.triggeredModel.modelUuid,
modelUuid: material.current.modelUuid,
pointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
});
@ -338,7 +342,7 @@ export function useTriggerHandler() {
const material = getMaterialById(materialId);
if (material) {
setIsPaused(material.materialId, false);
// setIsPaused(material.materialId, false);
setPreviousLocation(material.materialId, {
modelUuid: material.current.modelUuid,
@ -477,7 +481,7 @@ export function useTriggerHandler() {
const material = getMaterialById(materialId);
if (material) {
setIsPaused(material.materialId, false);
// setIsPaused(material.materialId, false);
const action = getActionByUuid(selectedProduct.productId, trigger.triggeredAsset.triggeredAction.actionUuid);
const machine = getMachineById(trigger.triggeredAsset?.triggeredModel.modelUuid);
@ -498,7 +502,7 @@ export function useTriggerHandler() {
})
setCurrentLocation(material.materialId, {
modelUuid: trigger.triggeredAsset.triggeredModel.modelUuid,
modelUuid: material.current.modelUuid,
pointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
});

View File

@ -280,26 +280,34 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>)
unLoadDuration: number,
action: VehicleAction
) {
startTime = performance.now();
const fixedInterval = unLoadDuration * (1000 / speed);
let lastIncrementTime = performance.now();
let pauseStartTime: number | null = null;
let totalPausedDuration = 0;
const fixedInterval = (unLoadDuration * 1000) / speed;
const dropLoop = () => {
if (isPausedRef.current) {
pauseTimeRef.current ??= performance.now();
const dropLoop = (currentTime: number) => {
const conveyor = getConveyorById(conveyorId);
if (isPausedRef.current || (conveyor && conveyor.isPaused)) {
if (pauseStartTime === null) {
pauseStartTime = currentTime;
}
requestAnimationFrame(dropLoop);
return;
}
if (pauseTimeRef.current) {
const pauseDuration = performance.now() - pauseTimeRef.current;
startTime += pauseDuration;
pauseTimeRef.current = null;
// If we were paused but now resumed
if (pauseStartTime !== null) {
totalPausedDuration += currentTime - pauseStartTime;
pauseStartTime = null;
}
const elapsedTime = performance.now() - startTime;
const conveyor = getConveyorById(conveyorId);
if (elapsedTime >= fixedInterval) {
if (conveyor && !conveyor.isPaused && vehicleCurrentLoad > 0) {
// Adjust for paused time
const adjustedCurrentTime = currentTime - totalPausedDuration;
const elapsedSinceLastIncrement = adjustedCurrentTime - lastIncrementTime;
if (elapsedSinceLastIncrement >= fixedInterval) {
if (conveyor && vehicleCurrentLoad > 0) {
decrementVehicleLoad(vehicleId, 1);
vehicleCurrentLoad -= 1;
@ -308,18 +316,18 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>)
triggerPointActions(action, material.materialId);
}
if (vehicleCurrentLoad > 0) {
startTime = performance.now();
requestAnimationFrame(dropLoop);
}
} else if (!conveyor?.isActive) {
requestAnimationFrame(dropLoop);
// Update the last increment time (using adjusted time)
lastIncrementTime = adjustedCurrentTime;
}
} else {
}
// Continue the loop if there's more load to drop
if (vehicleCurrentLoad > 0) {
requestAnimationFrame(dropLoop);
}
};
dropLoop();
requestAnimationFrame(dropLoop);
}
function handleMaterialDropToArmBot(model: RoboticArmEventSchema) {

View File

@ -244,7 +244,7 @@ const AddButtons: React.FC<ButtonsProps> = ({
{/* "+" Button */}
<button
id="panel-add-button"
id={`${side}-add-button`}
className={`side-button ${side}${
selectedZone.activeSides.includes(side) ? " active" : ""
}`}
@ -280,7 +280,7 @@ const AddButtons: React.FC<ButtonsProps> = ({
? "active"
: ""
}`}
id="hide-panel-visulization"
id={`${side}-hide-panel-visulization`}
title={
hiddenPanels[selectedZone.zoneId]?.includes(side)
? "Show Panel"
@ -301,7 +301,7 @@ const AddButtons: React.FC<ButtonsProps> = ({
<button
className="icon"
title="Clean Panel"
id="clean-panel-visulization"
id={`${side}-clean-panel-visulization`}
onClick={() => cleanPanel(side)}
style={{
cursor:
@ -319,7 +319,7 @@ const AddButtons: React.FC<ButtonsProps> = ({
className={`icon ${
selectedZone.lockedPanels.includes(side) ? "active" : ""
}`}
id="lock-panel-visulization"
id={`${side}-lock-panel-visulization`}
title={
selectedZone.lockedPanels.includes(side)
? "Unlock Panel"

View File

@ -20,7 +20,6 @@ import { useNavigate } from "react-router-dom";
import { usePlayButtonStore } from "../store/usePlayButtonStore";
import MarketPlace from "../modules/market/MarketPlace";
import LoadingPage from "../components/templates/LoadingPage";
import SimulationPlayer from "../components/ui/simulation/simulationPlayer";
import KeyPressListener from "../utils/shortcutkeys/handleShortcutKeys";
import { useSelectedUserStore } from "../store/useCollabStore";
import FollowPerson from "../components/templates/FollowPerson";
@ -34,9 +33,11 @@ import LogList from "../components/ui/log/LogList";
import Footer from "../components/footer/Footer";
import SelectFloorPlan from "../components/temporary/SelectFloorPlan";
import ControlsPlayer from "../components/layout/controls/ControlsPlayer";
import CompareLayOut from "../components/ui/compare/CompareLayOut";
import CompareLayOut from "../components/ui/compareVersion/CompareLayOut";
import useToggleStore from "../store/useUIToggleStore";
import RegularDropDown from "../components/ui/inputs/RegularDropDown";
import VersionSaved from "../components/layout/sidebarRight/versionHisory/VersionSaved";
import SimulationPlayer from "../components/ui/simulation/simulationPlayer";
const Project: React.FC = () => {
let navigate = useNavigate();
@ -178,6 +179,7 @@ const Project: React.FC = () => {
<CompareLayOut dummyLayouts={dummyLayouts} />
</>
)}
<VersionSaved />
</div>
);
};

View File

@ -477,15 +477,52 @@ export const useCompareStore = create<CompareStore>((set) => ({
toggleComparePopUp: () =>
set((state) => ({ comparePopUp: !state.comparePopUp })),
}));
// Define the types for the store state
interface VersionStore {
// Save state store
interface SaveVersionStore {
isVersionSaved: boolean;
setIsVersionSaved: (value: boolean) => void;
}
// Create the Zustand store
export const useSaveVersion = create<VersionStore>((set) => ({
isVersionSaved: false, // Default state
setIsVersionSaved: (value: boolean) => set({ isVersionSaved: value }), // Function to update the state
export const useSaveVersion = create<SaveVersionStore>((set) => ({
isVersionSaved: false,
setIsVersionSaved: (value: boolean) => set({ isVersionSaved: value }),
}));
// Version object type
export interface Version {
id: string;
versionLabel: string;
versionName?: string;
timestamp: string;
savedBy: string;
description?: string;
}
// Version list store
interface VersionListStore {
versions: Version[];
addVersion: (version: Version) => void;
clearVersions: () => void;
setVersions: (newVersions: Version[]) => void;
updateVersion: (id: string, data: Partial<Version>) => void; // ✅ Added
}
export const useVersionStore = create<VersionListStore>((set) => ({
versions: [],
addVersion: (newVersion) =>
set((state) => ({
versions: [newVersion, ...state.versions],
})),
clearVersions: () => set({ versions: [] }),
setVersions: (newVersions) => set({ versions: newVersions }),
updateVersion: (id, data) =>
set((state) => ({
versions: state.versions.map((version) =>
version.id === id ? { ...version, ...data } : version
),
})),
}));

View File

@ -1738,4 +1738,178 @@
to {
height: 60%;
}
}
.versionSaved {
min-width: 449px;
position: fixed;
bottom: 45px;
right: 10px;
z-index: 10;
background: var(--background-color);
backdrop-filter: blur(20px);
border-radius: 20px;
padding: 8px 10px;
display: flex;
flex-direction: column;
gap: 18px;
.version-header {
display: flex;
justify-content: space-between;
align-items: center;
.header-wrapper {
display: flex;
align-items: center;
gap: 6px;
}
}
.version-details {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 18px;
.details {
width: 100%;
background: var(--background-color);
backdrop-filter: blur(20px);
border-radius: 20px;
outline: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
padding: 7px 12px;
.details-wrapper {
font-size: var(--font-size-small);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
button {
font-size: var(--font-size-small);
display: flex;
justify-content: center;
align-items: center;
background: var(--background-color-button);
color: var(--text-button-color);
border-radius: 12px;
padding: 5px 6px;
cursor: pointer;
}
}
}
}
.dismissing {
display: none;
}
.edit-version-popup-wrapper {
height: 100vh;
width: 100vw;
background: var(--background-color-secondary);
@include flex-center;
.details-wrapper-popup-container {
min-width: 535px;
width: 520px;
background: var(--background-color);
border-radius: #{$border-radius-large};
backdrop-filter: blur(15px);
outline: 1px solid var(--border-color);
padding: 6px;
display: flex;
flex-direction: column;
gap: 30px;
padding: 20px;
.header-wrapper {
display: flex;
gap: 6px;
}
.details-wrapper {
display: flex;
flex-direction: column;
gap: 10px;
.version-name,
.version-description {
background: var(--background-color);
backdrop-filter: blur(20px);
border-radius: 20px;
outline: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 12px;
position: relative;
.label {
position: absolute;
right: 8px;
font-size: var(--font-size-tiny);
color: var(--text-disabled);
}
input {
border: none;
background: none;
outline: none;
color: var(--text-color);
}
}
.version-description {
textarea {
padding: 4px 8px;
width: 100%;
min-height: 101px;
background: transparent;
border: none;
outline: none;
}
}
}
.btn-wrapper {
display: flex;
justify-content: flex-end;
gap: 20px;
.save {
display: flex;
justify-content: center;
align-items: center;
background: var(--background-color-button);
color: var(--text-button-color);
border-radius: 12px;
padding: 5px 12px;
cursor: pointer;
}
}
}
}