Compare commits

...

30 Commits

Author SHA1 Message Date
c60f15db13 added vehicle conflict path structure 2025-09-02 09:13:06 +05:30
6da82895b7 Merge remote-tracking branch 'origin/main-demo' into feature/predefined-path 2025-09-01 11:35:34 +05:30
b44111a620 solved a bug based on generating point based on zustand 2025-09-01 11:24:18 +05:30
9825c3ef12 worker img update 2025-08-29 17:58:37 +05:30
3bc0e28267 - online off line status updated
- echo added to rename
2025-08-29 17:47:29 +05:30
f9d314b69f decal default scale scale update 2025-08-29 17:16:50 +05:30
b956ed57e8 human resource management profile ui fix 2025-08-29 17:07:00 +05:30
df36ee0366 Merge remote-tracking branch 'origin/dev-resourceManagement' into main-demo 2025-08-29 16:58:27 +05:30
ef9c3a9c63 dashboard sidebar updated 2025-08-29 16:39:14 +05:30
62ddc1c25f styles file name updated 2025-08-29 16:26:49 +05:30
1b161b2176 - 404-page added and fallback for project not found
- page nav handling for project-not-found updated
2025-08-29 16:24:03 +05:30
a14f7fcf6a dashboard kebab bug fix 2025-08-29 15:14:24 +05:30
b6783f99d3 refactor: reorganize asset management components and enhance search functionality 2025-08-29 13:47:07 +05:30
b2311ab186 Merge remote-tracking branch 'origin/decal-list' into main-demo 2025-08-29 12:52:50 +05:30
c9536a13e0 updated vehicle 2025-08-29 10:27:44 +05:30
e23e339ed3 decal scale fix 2025-08-29 10:10:54 +05:30
6182862296 added temp vehicle schema that holds the path details and vehicle details 2025-08-28 17:58:38 +05:30
a0e5115c6c added available paths 2025-08-28 09:43:42 +05:30
5117e48527 finding shortest path 2025-08-26 14:06:25 +05:30
1d2a42b7bd Merge branch 'main-demo' into dev-resourceManagement 2025-08-26 09:36:42 +05:30
6fa4d5323d feat: add asset thumbnail fetching and update worker data display in Hrm component 2025-08-26 09:36:09 +05:30
6026c3b82b refactor: remove unnecessary console logs in asset management and thumbnail fetching 2025-08-25 18:16:02 +05:30
358ce22767 feat: implement asset thumbnail fetching and update Hrm and AssetManagement components to use asset data 2025-08-25 18:13:57 +05:30
7b5486590a created points using pen icon 2025-08-25 16:21:54 +05:30
fe95ea8d0b Update Path Drawing Component to Match Wall Drawing Component Behavior 2025-08-25 15:25:48 +05:30
d090b976b0 updated vehicle path based on wall data 2025-08-23 10:12:37 +05:30
7fb83417be Merge remote-tracking branch 'origin/main-demo' into feature/predefined-path 2025-08-22 09:12:02 +05:30
b623a92b9c added path curves 2025-08-22 09:10:37 +05:30
3f808f167d added predined path 2025-08-20 10:09:34 +05:30
5e025224d6 added pre defined paths in rough 2025-08-13 15:24:32 +05:30
108 changed files with 7494 additions and 2119 deletions

View File

@@ -7,6 +7,7 @@ import UserAuth from "./pages/UserAuth";
import "./styles/main.scss";
import { LoggerProvider } from "./components/ui/log/LoggerContext";
import ForgotPassword from "./pages/ForgotPassword";
import PageNotFound from "./pages/PageNotFound";
const App: React.FC = () => {
@@ -23,6 +24,7 @@ const App: React.FC = () => {
<Route path="/forgot" element={<ForgotPassword />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/projects/:projectId" element={<Project />} />
<Route path="*" element={<PageNotFound />} />
</Routes>
</Router>
</LoggerProvider>

View File

@@ -0,0 +1,9 @@
<svg width="435" height="192" viewBox="0 0 435 192" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M434.294 35.8327L419.597 87.2712C406.577 88.0129 399.354 88.9272 386.529 90.946L393.877 59.3474L332.885 112.991L377.711 126.218L379.915 115.93C393.611 111.893 401.218 109.947 414.453 107.847L407.84 134.301L427.681 140.18L419.597 167.37L401.226 162.226L392.407 191.62L362.279 183.537L369.627 151.938L297.612 132.097L304.961 104.173L398.286 24.8093L434.294 35.8327Z" fill="#0F0F0F"/>
<path d="M107.287 110.787L127.128 106.378L132.271 133.567L113.9 137.976L119.779 168.84L86.7109 176.923L80.833 144.59L63.9316 148.999C72.6686 121.327 81.38 109.098 102.144 91.6804L107.287 110.787Z" fill="#0F0F0F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M204.287 0.55928C212.617 -0.162914 217.275 -0.209574 225.598 0.55928C254.396 3.22002 271.652 31.4235 274.832 50.529C276.587 61.0745 277.015 67.0381 277.037 77.7185C265.905 97.6708 263.444 104.779 252.787 126.218C243.954 141.438 233.146 152.429 218.249 162.226C186.397 160.022 167.896 140.12 157.991 109.317C154.89 92.6487 155.445 73.5003 156.521 59.3474C158.678 31.0003 174.892 6.89136 204.287 0.55928ZM217.147 23.9997C197.265 23.9997 181.147 48.624 181.147 78.9997C181.147 109.375 197.265 134 217.147 134C237.029 133.999 253.147 109.375 253.147 78.9997C253.147 48.6243 237.029 24.0001 217.147 23.9997Z" fill="#0F0F0F"/>
<path d="M99.2041 71.1052L66.1357 81.3923L60.2568 50.529L29.3936 126.953L66.1357 120.34C58.6892 132.967 56.9985 139.297 54.3779 150.468L7.34766 160.757L0 131.362L49.2344 17.4606L85.2422 10.113L99.2041 71.1052Z" fill="#0F0F0F"/>
<path d="M277.037 96.8249C275.203 113.08 274.273 122.314 268.953 134.301C257.417 152.202 250.176 154.67 237.354 160.021C254.909 142.239 264.111 127.901 277.037 96.8249Z" fill="#0F0F0F"/>
<path d="M99.2041 81.3923C89.3926 88.8475 83.8084 94.1174 73.4844 109.317L68.3398 91.6804L99.2041 81.3923Z" fill="#0F0F0F"/>
<path d="M383.59 100.499C397.029 98.1517 404.407 97.8166 417.393 98.2946C404.464 99.701 397.094 100.893 383.59 104.173V100.499Z" fill="#0F0F0F"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -1,4 +1,5 @@
import React, { useState, useRef, useEffect, act } from "react";
import React, { useState, useRef } from "react";
import { createPortal } from "react-dom";
import img from "../../assets/image/image.png";
import { useNavigate } from "react-router-dom";
import { getUserData } from "../../functions/getUserData";
@@ -7,11 +8,11 @@ import {
useProjectName,
useSocketStore,
} from "../../store/builder/store";
import { viewProject } from "../../services/dashboard/viewProject";
import OuterClick from "../../utils/outerClick";
import { KebabIcon } from "../icons/ExportCommonIcons";
import { getAllProjects } from "../../services/dashboard/getAllProjects";
import { updateProject } from "../../services/dashboard/updateProject";
// import { viewProject } from "../../services/dashboard/viewProject";
// import { updateProject } from "../../services/dashboard/updateProject";
interface DashBoardCardProps {
projectName: string;
@@ -69,9 +70,9 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
const kebabRef = useRef<HTMLDivElement>(null);
const navigateToProject = async (e: any) => {
if (active && active == "trash") return;
if (active && active === "trash") return;
try {
const viewProjects = await viewProject(organization, projectId, userId);
// const viewProjects = await viewProject(organization, projectId, userId);
setLoadingProgress(1);
setProjectName(projectName);
@@ -96,13 +97,13 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
case "open in new tab":
try {
if (active === "shared" && createdBy) {
const newTab = await viewProject(
organization,
projectId,
createdBy?._id
);
// const newTab = await viewProject(
// organization,
// projectId,
// createdBy?._id
// );
} else {
const newTab = await viewProject(organization, projectId, userId);
// const newTab = await viewProject(organization, projectId, userId);
setProjectName(projectName);
setIsKebabOpen(false);
@@ -226,7 +227,7 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
className="dashboard-card-container"
onClick={navigateToProject}
title={projectName}
onMouseLeave={() => setIsKebabOpen(false)}
// onMouseLeave={() => setIsKebabOpen(false)}
>
<div className="dashboard-card-wrapper">
<div className="preview-container">
@@ -267,7 +268,7 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
)}
{createdAt && (
<div className="project-data">
{active && active == "trash" ? `Trashed by you` : `Edited `}{" "}
{active && active === "trash" ? `Trashed by you` : `Edited `}{" "}
{getRelativeTime(createdAt)}
</div>
)}
@@ -292,22 +293,37 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
</div>
</div>
</div>
{isKebabOpen && (
<div className="kebab-options-wrapper">
{getOptions().map((option) => (
<button
key={option}
className="option"
onClick={(e) => {
e.stopPropagation();
handleOptionClick(option);
}}
>
{option}
</button>
))}
</div>
)}
{isKebabOpen &&
createPortal(
<div
className="kebab-options-wrapper"
style={{
position: "fixed",
zIndex: 9999,
top: kebabRef.current
? kebabRef.current.getBoundingClientRect().bottom +
window.scrollY
: 0,
left: kebabRef.current
? kebabRef.current.getBoundingClientRect().left + window.scrollX
: 0,
}}
>
{getOptions().map((option) => (
<button
key={option}
className="option"
onClick={(e) => {
e.stopPropagation();
handleOptionClick(option);
}}
>
{option}
</button>
))}
</div>,
document.body
)}
</button>
);
};

View File

@@ -64,7 +64,7 @@ const SidePannel: React.FC<SidePannelProps> = ({ setActiveTab, activeTab }) => {
if (projectSocket) {
const handleResponse = (data: any) => {
if (data.message === "Project created successfully") {
setLoadingProgress(1)
setLoadingProgress(1);
navigate(`/projects/${data.data.projectId}`);
}
projectSocket.off("v1-project:response:add", handleResponse); // Clean up
@@ -141,7 +141,10 @@ const SidePannel: React.FC<SidePannelProps> = ({ setActiveTab, activeTab }) => {
activeTab === "Tutorials" ? "option-list active" : "option-list"
}
title="coming soon"
onClick={() => setActiveTab("Tutorials")}
onClick={() => {
// setActiveTab("Tutorials");
console.warn("Tutorials comming soon");
}}
>
<TutorialsIcon />
Tutorials
@@ -153,14 +156,17 @@ const SidePannel: React.FC<SidePannelProps> = ({ setActiveTab, activeTab }) => {
: "option-list"
}
title="coming soon"
onClick={() => setActiveTab("Documentation")}
onClick={() => {
// setActiveTab("Documentation");
console.warn("Documentation comming soon");
}}
>
<DocumentationIcon />
Documentation
</div>
</div>
<div className="side-bar-options-container" title="coming soon">
<div className="option-list">
<div className="side-bar-options-container">
<div className="option-list" title="coming soon">
<SettingsIcon />
Settings
</div>
@@ -175,7 +181,7 @@ const SidePannel: React.FC<SidePannelProps> = ({ setActiveTab, activeTab }) => {
<LogoutIcon />
Log out
</div>
<div className="option-list">
<div className="option-list" title="coming soon">
<HelpIcon />
Help & Feedback
</div>

View File

@@ -1,5 +1,5 @@
import React, { useEffect } from "react";
import { HelpIcon } from "../icons/DashboardIcon";
import React, { useEffect, useState } from "react";
import { HelpIcon, WifiIcon } from "../icons/DashboardIcon";
import { useLogger } from "../ui/log/LoggerContext";
import { GetLogIcon } from "./getLogIcons";
import {
@@ -31,6 +31,27 @@ const Footer: React.FC = () => {
const { Leftnote, Middlenote, Rightnote } = useMouseNoteStore();
const [isOnline, setIsOnline] = useState<boolean>(navigator.onLine);
useEffect(() => {
const handleOnline = () => {
echo.success('You are back Online');
setIsOnline(true);
};
const handleOffline = () => {
echo.warn('Changes made now might not be saved');
echo.error('You are now Offline.');
setIsOnline(false);
};
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
const mouseButtons = [
{
icon: <CurserLeftIcon />,
@@ -98,6 +119,16 @@ const Footer: React.FC = () => {
<HelpIcon />
</div>
</div>
<div
className={`wifi-connection ${
isOnline ? "connected" : "disconnected"
}`}
>
<div className="icon">
<WifiIcon />
</div>
<div className="tooltip">{isOnline ? "Online" : "Offline"}</div>
</div>
</div>
</div>

View File

@@ -336,18 +336,35 @@ export const MachineIcon = () => {
);
};
export const CraneIcon = () => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18.5 5.7502L12.875 1.3752C12.6875 1.1877 12.375 1.1877 12.125 1.3127L5.4375 5.6252H1.875C1.5 5.6252 1.25 5.8752 1.25 6.2502C1.25 6.6252 1.5 6.8752 1.875 6.8752H4.375V10.0002H1.875C1.5 10.0002 1.25 10.2502 1.25 10.6252V13.7502C1.25 14.1252 1.5 14.3752 1.875 14.3752H8.125C8.5 14.3752 8.75 14.1252 8.75 13.7502V10.6252C8.75 10.2502 8.5 10.0002 8.125 10.0002H5.625V6.8752H10.625V15.9377L8.375 17.6252C8.1875 17.8127 8.0625 18.0627 8.1875 18.3127C8.25 18.5627 8.5 18.7502 8.75 18.7502H16.25C16.5 18.7502 16.75 18.5627 16.8125 18.3127C16.875 18.0627 16.8125 17.7502 16.625 17.6252L14.375 15.9377V6.8752H18.125C18.375 6.8752 18.625 6.6877 18.6875 6.4377C18.8125 6.1877 18.75 5.9377 18.5 5.7502ZM3.6875 11.2502L2.5 12.7502V11.2502H3.6875ZM5 11.6252L6.1875 13.1252H3.8125L5 11.6252ZM7.5 12.7502L6.3125 11.2502H7.5V12.7502ZM11.875 3.0002V5.6252H11.25H7.75L11.875 3.0002ZM13.75 5.6252H13.125V3.1252L16.3125 5.6252H13.75Z"
fill="white"
/>
</svg>
);
};
type TypeBasedAssetIconsProps = {
assetType: string;
};
export function TypeBasedAssetIcons({ assetType }: TypeBasedAssetIconsProps) {
console.log("assetType: ", assetType);
return (
<div>
{assetType === "machine" && <MachineIcon />}
{assetType === "vehicle" && <ForkLiftIcon />}
{assetType === "transfer" && <ConveyorIcon />}
{assetType === "roboticArm" && <RoboticArmIcon />}
{assetType === "StaticMachine" && <MachineIcon />}
{assetType === "Vehicle" && <ForkLiftIcon />}
{assetType === "Conveyor" && <ConveyorIcon />}
{assetType === "Crane" && <CraneIcon />}
{assetType === "ArmBot" && <RoboticArmIcon />}
</div>
);
}

View File

@@ -253,3 +253,20 @@ export function LogoutIcon() {
</svg>
);
}
export function WifiIcon() {
return (
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.78458 7.91952C6.35145 7.4864 5.64945 7.4864 5.21633 7.91952C4.7832 8.35265 4.7832 9.05465 5.21633 9.48777C5.64945 9.9209 6.35145 9.9209 6.78458 9.48777C7.2177 9.05465 7.2177 8.35265 6.78458 7.91952ZM9.7902 6.48215L8.67945 7.5929C7.20008 6.11352 4.8012 6.11352 3.32183 7.5929L2.21108 6.48215C4.30395 4.38927 7.69733 4.38927 9.7902 6.48215ZM10.574 5.69802C8.04795 3.17202 3.95258 3.17202 1.42658 5.69802L0.283203 4.55465C3.4407 1.39715 8.55983 1.39715 11.7173 4.55465L10.574 5.69802Z"
fill="white"
/>
</svg>
);
}

View File

@@ -1,474 +0,0 @@
import React, { useEffect, useState } from "react";
import Search from "../../ui/inputs/Search";
import { getCategoryAsset } from "../../../services/factoryBuilder/asset/assets/getCategoryAsset";
import { fetchAssets } from "../../../services/marketplace/fetchAssets";
import {
useDecalStore,
useDroppedDecal,
useSelectedItem,
} from "../../../store/builder/store";
// images -------------------
import vehicle from "../../../assets/image/categories/vehicles.png";
import workStation from "../../../assets/image/categories/workStation.png";
import machines from "../../../assets/image/categories/machines.png";
import worker from "../../../assets/image/categories/worker.png";
import storage from "../../../assets/image/categories/storage.png";
import office from "../../../assets/image/categories/office.png";
import safety from "../../../assets/image/categories/safety.png";
import feneration from "../../../assets/image/categories/feneration.png";
import decal from "../../../assets/image/categories/decal.png";
import SkeletonUI from "../../templates/SkeletonUI";
import {
AlertIcon,
ArrowIcon,
DecalInfoIcon,
HangTagIcon,
NavigationIcon,
} from "../../icons/ExportCommonIcons";
import { getCategoryDecals } from "../../../services/factoryBuilder/asset/decals/getCategoryDecals";
// -------------------------------------
interface AssetProp {
filename: string;
thumbnail?: string;
category: string;
description?: string;
tags: string;
url?: string;
uploadDate?: number;
isArchieve?: boolean;
animated?: boolean;
price?: number;
CreatedBy?: string;
}
interface CategoryListProp {
assetImage?: string;
assetName?: string;
categoryImage: string;
category: string;
}
const Assets: React.FC = () => {
const { setSelectedItem } = useSelectedItem();
const { setDroppedDecal } = useDroppedDecal();
const [searchValue, setSearchValue] = useState<string>("");
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [categoryAssets, setCategoryAssets] = useState<AssetProp[]>([]);
const [decalAsset, setDecalAsset] = useState<any>();
const [filtereredAssets, setFiltereredAssets] = useState<AssetProp[] | []>(
[]
);
const [categoryList, setCategoryList] = useState<CategoryListProp[]>([]);
const [isLoading, setisLoading] = useState<boolean>(false); // Loading state for assets
const { selectedSubCategory, setSelectedSubCategory } = useDecalStore();
const handleSearchChange = (value: string) => {
const searchTerm = searchValue
? searchValue.toLowerCase()
: value.toLowerCase();
setSearchValue(value);
if (searchTerm.trim() === "" && !selectedCategory) {
setCategoryAssets([]);
return;
}
if (selectedCategory === "Decals" || selectedSubCategory) {
const filteredModels = decalAsset?.filter((model: any) =>
model.decalName?.toLowerCase().includes(searchTerm.toLowerCase())
);
setCategoryAssets(filteredModels);
} else {
const filteredModels = filtereredAssets?.filter((model) => {
if (!model?.tags || !model?.filename || !model?.category) return false;
if (searchTerm.startsWith(":") && searchTerm.length > 1) {
const tagSearchTerm = searchTerm.slice(1);
return model.tags.toLowerCase().includes(tagSearchTerm);
} else if (selectedCategory) {
return (
model.category
.toLowerCase()
.includes(selectedCategory.toLowerCase()) &&
model.filename.toLowerCase().includes(searchTerm)
);
} else {
return model.filename.toLowerCase().includes(searchTerm);
}
});
setCategoryAssets(filteredModels);
}
};
useEffect(() => {
if (selectedCategory === "Decals") return;
const filteredAssets = async () => {
try {
const filt = await fetchAssets();
setFiltereredAssets(filt);
} catch {
echo.error("Filter asset not found");
}
};
filteredAssets();
}, [categoryAssets, selectedCategory]);
useEffect(() => {
if (
(searchValue.trim() === "" && selectedCategory === "Decals") ||
selectedSubCategory
) {
const filteredModels = decalAsset?.filter((model: any) =>
model.decalName?.toLowerCase().includes(searchValue.toLowerCase())
);
setCategoryAssets(filteredModels);
}
}, [selectedSubCategory, decalAsset, searchValue]);
useEffect(() => {
setCategoryList([
{ category: "Fenestration", categoryImage: feneration },
{ category: "Decals", categoryImage: decal },
{ category: "Vehicles", categoryImage: vehicle },
{ category: "Workstation", categoryImage: workStation },
{ category: "Machines", categoryImage: machines },
{ category: "Workers", categoryImage: worker },
{ category: "Storage", categoryImage: storage },
{ category: "Safety", categoryImage: safety },
{ category: "Office", categoryImage: office },
]);
}, []);
const fetchCategoryAssets = async (asset: any) => {
setisLoading(true);
setSelectedCategory(asset);
try {
const res = await getCategoryAsset(asset);
setCategoryAssets(res);
setFiltereredAssets(res);
setisLoading(false); // End loading
// eslint-disable-next-line
} catch (error) {
echo.error("failed to fetch assets");
setisLoading(false);
}
if (asset === "Decals") {
fetchCategoryDecals("Safety");
}
};
const fetchCategoryDecals = async (asset: any) => {
setisLoading(true);
// setSelectedCategory(asset);
try {
const res = await getCategoryDecals(asset);
setCategoryAssets(res);
setFiltereredAssets(res);
setDecalAsset(res);
setisLoading(false); // End loading
// eslint-disable-next-line
} catch (error) {
echo.error("failed to fetch assets");
setisLoading(false);
}
};
const activeSubcategories = [
{ name: "Safety", icon: <AlertIcon /> },
{ name: "Navigation", icon: <NavigationIcon /> },
{ name: "Branding", icon: <HangTagIcon /> },
{ name: "Informational", icon: <DecalInfoIcon /> },
];
return (
<div className="assets-container-main">
<Search onChange={handleSearchChange} value={searchValue} />
<div className="assets-list-section">
<section>
{(() => {
if (isLoading) {
return <SkeletonUI type="asset" />; // Show skeleton when loading
}
if (searchValue) {
return (
<div className="assets-result">
<div className="assets-wrapper">
<div className="searched-content">
<p>
Results for{" "}
<span className="search-for">'{searchValue}'</span>
</p>
</div>
<div className="assets-container">
{selectedCategory == "Decals" ? (
<>
<div className="catogory-asset-filter">
{activeSubcategories.map((cat, index) => (
<div
key={index}
className={`catogory-asset-filter-wrapper ${
selectedSubCategory === cat.name
? "active"
: ""
}`}
onClick={() => {
fetchCategoryDecals(cat.name);
setSelectedSubCategory(cat.name);
}}
>
<div className="sub-catagory">{cat.icon}</div>
<div className="sub-catagory">{cat.name}</div>
</div>
))}
</div>
{categoryAssets?.map((asset: any, index: number) => (
<div
key={`${index}-${asset}`}
className="assets"
id={asset.decalName}
title={asset.decalName}
>
<img
src={asset?.decalImage}
alt={asset.decalName}
className="asset-image"
onPointerDown={() => {
setSelectedItem({
name: asset.decalName,
id: asset.id,
type:
asset.type === "undefined"
? undefined
: asset.type,
category: asset.category,
// subType: asset.subType,
});
}}
/>
<div className="asset-name">
{asset.decalName
.split("_")
.map(
(word: any) =>
word.charAt(0).toUpperCase() +
word.slice(1)
)
.join(" ")}
</div>
</div>
))}
</>
) : (
categoryAssets?.map((asset: any, index: number) => (
<div
key={`${index}-${asset.filename}`}
className="assets"
id={asset.filename}
title={asset.filename}
>
<img
src={asset?.thumbnail}
alt={asset.filename}
className="asset-image"
onPointerDown={() => {
setSelectedItem({
name: asset.filename,
id: asset.AssetID,
type:
asset.type === "undefined"
? undefined
: asset.type,
});
}}
/>
<div className="asset-name">
{asset.filename
.split("_")
.map(
(word: any) =>
word.charAt(0).toUpperCase() + word.slice(1)
)
.join(" ")}
</div>
</div>
))
)}
</div>
</div>
</div>
);
}
if (selectedCategory) {
return (
<div className="assets-wrapper">
<h2 className="header">
{selectedCategory}
<button
className="back-button"
id="asset-backButtom"
onClick={() => {
setSelectedCategory(null);
setSelectedSubCategory(null);
setCategoryAssets([]);
}}
>
<div className="back-arrow">
<ArrowIcon />
</div>
Back
</button>
</h2>
{selectedCategory === "Decals" && (
<>
<div className="catogory-asset-filter">
{activeSubcategories.map((cat, index) => (
<div
key={index}
className={`catogory-asset-filter-wrapper ${
selectedSubCategory === cat.name ? "active" : ""
}`}
onClick={() => {
fetchCategoryDecals(cat.name);
setSelectedSubCategory(cat.name);
}}
>
<div className="sub-catagory">{cat.icon}</div>
<div className="sub-catagory">{cat.name}</div>
</div>
))}
</div>
</>
)}
{selectedCategory !== "Decals" && !selectedSubCategory ? (
<div className="assets-container">
{categoryAssets?.map((asset: any, index: number) => (
<div
key={`${index}-${asset}`}
className="assets"
id={asset.filename}
title={asset.filename}
>
<img
src={asset?.thumbnail}
alt={asset.filename}
className="asset-image"
onPointerDown={() => {
setSelectedItem({
name: asset.filename,
id: asset.AssetID,
type:
asset.type === "undefined"
? undefined
: asset.type,
category: asset.category,
subType: asset.subType,
});
}}
/>
<div className="asset-name">
{asset.filename
.split("_")
.map(
(word: any) =>
word.charAt(0).toUpperCase() + word.slice(1)
)
.join(" ")}
</div>
</div>
))}
{categoryAssets.length === 0 && (
<div className="no-asset">
🚧 The asset shelf is empty. We're working on filling
it up!
</div>
)}
</div>
) : (
<div className="assets-container">
{categoryAssets?.map((asset: any, index: number) => (
<div
key={`${index}-${asset}`}
className="assets"
id={asset.decalName}
title={asset.decalName}
>
<img
src={asset?.decalImage}
alt={asset.decalName}
className="asset-image"
onPointerDown={() => {
setDroppedDecal({
category: asset.category,
decalName: asset.decalName,
decalImage: asset.decalImage,
decalId: asset.id,
});
}}
/>
<div className="asset-name">
{asset.decalName
.split("_")
.map(
(word: any) =>
word.charAt(0).toUpperCase() + word.slice(1)
)
.join(" ")}
</div>
</div>
))}
{categoryAssets.length === 0 && (
<div className="no-asset">
🚧 The asset shelf is empty. We're working on filling
it up!
</div>
)}
</div>
)}
</div>
);
}
return (
<div className="assets-wrapper">
<h2 className="categories-header">Categories</h2>
<div className="categories-container">
{Array.from(
new Set(categoryList.map((asset) => asset.category))
).map((category, index) => {
const categoryInfo = categoryList.find(
(asset) => asset.category === category
);
return (
<div
key={`${index}-${category}`}
className="category"
id={category}
onClick={() => {
fetchCategoryAssets(category);
}}
>
<img
src={categoryInfo?.categoryImage ?? ""}
alt={category}
className="category-image"
draggable={false}
/>
<div className="category-name">{category}</div>
</div>
);
})}
</div>
</div>
);
})()}
</section>
</div>
</div>
);
};
export default Assets;

View File

@@ -3,7 +3,7 @@ import ToggleHeader from "../../ui/inputs/ToggleHeader";
import Outline from "./Outline";
import Header from "./Header";
import { useToggleStore } from "../../../store/useUIToggleStore";
import Assets from "./Assets";
import Assets from "./assetList/Assets";
import useModuleStore from "../../../store/useModuleStore";
import Widgets from "./visualization/widgets/Widgets";
import Templates from "../../../modules/visualization/template/Templates";

View File

@@ -0,0 +1,177 @@
import React, { useState, useEffect, useMemo, useCallback } from "react";
import { useDecalStore } from "../../../../store/builder/store";
import { getFilteredAssets } from "./assetsHelpers/filteredAssetsHelper";
import { fetchCategoryDecals } from "./assetsHelpers/fetchDecalsHelper";
import {
fetchAllAssets,
fetchCategoryAssets,
} from "./assetsHelpers/fetchAssetsHelper";
import Search from "../../../ui/inputs/Search";
import SkeletonUI from "../../../templates/SkeletonUI";
import { RenderAsset } from "./assetsHelpers/renderAssetHelper";
import {
ACTIVE_DECAL_SUBCATEGORIES,
CATEGORY_LIST,
} from "./assetsHelpers/constants";
import { ArrowIcon } from "../../../icons/ExportCommonIcons";
const Assets: React.FC = () => {
const { selectedSubCategory, setSelectedSubCategory } = useDecalStore();
const [searchValue, setSearchValue] = useState<string | null>(null);
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [assets, setAssets] = useState<AssetProp[] | DecalProp[]>([]);
const [globalResults, setGlobalResults] = useState<(AssetProp | DecalProp)[]>(
[]
);
const [isLoading, setIsLoading] = useState(false);
const filteredAssets = useMemo(
() =>
getFilteredAssets({
assets,
searchValue,
selectedCategory,
selectedSubCategory,
}),
[assets, searchValue, selectedCategory, selectedSubCategory]
);
const handleFetchCategory = useCallback(
async (category: string) => {
setIsLoading(true);
setSelectedCategory(category);
if (category === "Decals") {
const res = await fetchCategoryDecals("Safety");
setAssets(res);
setSelectedSubCategory("Safety");
} else {
const res = await fetchCategoryAssets(category);
setAssets(res);
}
setIsLoading(false);
},
[setSelectedSubCategory]
);
const fetchGlobalSearch = useCallback(async (term: string) => {
setIsLoading(true);
const allAssets = await fetchAllAssets();
const lowerTerm = term.toLowerCase();
const matches = allAssets.filter(
(a) =>
a.filename.toLowerCase().includes(lowerTerm) ||
a.tags?.toLowerCase().includes(lowerTerm) ||
a.category?.toLowerCase().includes(lowerTerm)
);
setGlobalResults(matches);
setIsLoading(false);
}, []);
useEffect(() => {
if (!selectedCategory && searchValue?.trim())
fetchGlobalSearch(searchValue);
else setGlobalResults([]);
}, [searchValue, selectedCategory, fetchGlobalSearch]);
return (
<div className="assets-container-main">
<Search onChange={setSearchValue} value={searchValue} />
<div className="assets-list-section">
<section>
{isLoading ? (
<SkeletonUI type="asset" />
) : searchValue || selectedCategory ? (
<div className="assets-wrapper">
{selectedCategory ? (
<>
<h2 className="header">
{selectedCategory}
<button
className="back-button"
onClick={() => {
setSelectedCategory(null);
setSelectedSubCategory(null);
setAssets([]);
setSearchValue(null);
}}
>
<div className="back-arrow">
<ArrowIcon />
</div>{" "}
Back
</button>
</h2>
{selectedCategory === "Decals" && (
<div className="catogory-asset-filter">
{ACTIVE_DECAL_SUBCATEGORIES.map((cat) => (
<div
key={cat.name}
className={`catogory-asset-filter-wrapper ${
selectedSubCategory === cat.name ? "active" : ""
}`}
onClick={async () => {
setIsLoading(true);
const res = await fetchCategoryDecals(cat.name);
setAssets(res);
setSelectedSubCategory(cat.name);
setIsLoading(false);
}}
>
<div className="sub-catagory">{cat.icon}</div>
<div className="sub-catagory">{cat.name}</div>
</div>
))}
</div>
)}
<div className="assets-container">
{filteredAssets.map((a, i) => (
<RenderAsset key={i} asset={a} index={i} />
))}
{filteredAssets.length === 0 && (
<div className="no-asset">🚧 No assets found</div>
)}
</div>
</>
) : (
<>
<h2 className="header">Global Search Results</h2>
<div className="assets-container">
{globalResults.map((a, i) => (
<RenderAsset key={i} asset={a} index={i} />
))}
{globalResults.length === 0 && (
<div className="no-asset">🔎 No matches found</div>
)}
</div>
</>
)}
</div>
) : (
<div className="assets-wrapper">
<h2 className="categories-header">Categories</h2>
<div className="categories-container">
{CATEGORY_LIST.map((cat) => (
<div
key={cat.category}
className="category"
onClick={() => handleFetchCategory(cat.category)}
>
<img
src={cat.categoryImage}
alt={cat.category}
className="category-image"
draggable={false}
/>
<div className="category-name">{cat.category}</div>
</div>
))}
</div>
</div>
)}
</section>
</div>
</div>
);
};
export default Assets;

View File

@@ -0,0 +1,34 @@
import vehicle from "../../../../../assets/image/categories/vehicles.png";
import workStation from "../../../../../assets/image/categories/workStation.png";
import machines from "../../../../../assets/image/categories/machines.png";
import worker from "../../../../../assets/image/categories/worker.png";
import storage from "../../../../../assets/image/categories/storage.png";
import office from "../../../../../assets/image/categories/office.png";
import safety from "../../../../../assets/image/categories/safety.png";
import feneration from "../../../../../assets/image/categories/feneration.png";
import decal from "../../../../../assets/image/categories/decal.png";
import {
AlertIcon,
DecalInfoIcon,
HangTagIcon,
NavigationIcon,
} from "../../../../icons/ExportCommonIcons";
export const CATEGORY_LIST: CategoryListProp[] = [
{ category: "Fenestration", categoryImage: feneration },
{ category: "Decals", categoryImage: decal },
{ category: "Vehicles", categoryImage: vehicle },
{ category: "Workstation", categoryImage: workStation },
{ category: "Machines", categoryImage: machines },
{ category: "Workers", categoryImage: worker },
{ category: "Storage", categoryImage: storage },
{ category: "Safety", categoryImage: safety },
{ category: "Office", categoryImage: office },
];
export const ACTIVE_DECAL_SUBCATEGORIES = [
{ name: "Safety", icon: <AlertIcon /> },
{ name: "Navigation", icon: <NavigationIcon /> },
{ name: "Branding", icon: <HangTagIcon /> },
{ name: "Informational", icon: <DecalInfoIcon /> },
];

View File

@@ -0,0 +1,22 @@
import { getCategoryAsset } from "../../../../../services/factoryBuilder/asset/assets/getCategoryAsset";
import { fetchAssets } from "../../../../../services/marketplace/fetchAssets";
export const fetchCategoryAssets = async (category: string): Promise<AssetProp[]> => {
if (category === "Decals") return []; // handled separately
try {
const res = await getCategoryAsset(category);
return res;
} catch (err) {
console.error("Failed to fetch category assets", err);
return [];
}
};
export const fetchAllAssets = async (): Promise<AssetProp[]> => {
try {
return await fetchAssets();
} catch (err) {
console.error("Failed to fetch all assets", err);
return [];
}
};

View File

@@ -0,0 +1,11 @@
import { getCategoryDecals } from "../../../../../services/factoryBuilder/asset/decals/getCategoryDecals";
export const fetchCategoryDecals = async (subcategory: string): Promise<DecalProp[]> => {
try {
const res = await getCategoryDecals(subcategory);
return res;
} catch (err) {
console.error("Failed to fetch decals", err);
return [];
}
};

View File

@@ -0,0 +1,34 @@
interface FilterProps {
assets: AssetProp[] | DecalProp[];
searchValue: string | null;
selectedCategory: string | null;
selectedSubCategory: string | null;
}
export const getFilteredAssets = ({
assets,
searchValue,
selectedCategory,
selectedSubCategory,
}: FilterProps) => {
const term = searchValue?.trim().toLowerCase();
if (!term) return assets;
if (selectedCategory === "Decals" || selectedSubCategory) {
return (assets as DecalProp[]).filter((a) =>
a.decalName?.toLowerCase().includes(term)
);
}
return (assets as AssetProp[]).filter((a) => {
const tags = a.tags?.toLowerCase() ?? "";
const filename = a.filename?.toLowerCase() ?? "";
const category = a.category?.toLowerCase() ?? "";
if (term.startsWith(":")) return tags.includes(term.slice(1));
if (selectedCategory)
return category.includes(selectedCategory.toLowerCase()) && filename.includes(term);
return filename.includes(term);
});
};

View File

@@ -0,0 +1,58 @@
import React from "react";
import { useDroppedDecal, useSelectedItem } from "../../../../../store/builder/store";
export const RenderAsset: React.FC<{ asset: AssetProp | DecalProp; index: number }> = ({ asset, index }) => {
const { setSelectedItem } = useSelectedItem();
const { setDroppedDecal } = useDroppedDecal();
if ("decalName" in asset) {
return (
<div key={`${index}-${asset.decalName}`} className="assets" id={asset.decalName} title={asset.decalName}>
<img
src={asset.decalImage}
alt={asset.decalName}
className="asset-image"
onPointerDown={() =>
setDroppedDecal({
category: asset.category,
decalName: asset.decalName,
decalImage: asset.decalImage,
decalId: asset.id,
})
}
/>
<div className="asset-name">
{asset.decalName
.split("_")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" ")}
</div>
</div>
);
}
return (
<div key={`${index}-${asset.filename}`} className="assets" id={asset.filename} title={asset.filename}>
<img
src={asset.thumbnail}
alt={asset.filename}
className="asset-image"
onPointerDown={() =>
setSelectedItem({
name: asset.filename,
id: asset.AssetID,
type: asset.type === "undefined" ? undefined : asset.type,
category: asset.category,
subType: asset.subType,
})
}
/>
<div className="asset-name">
{asset.filename
.split("_")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" ")}
</div>
</div>
);
};

View File

@@ -146,7 +146,7 @@ const SelectedDecalProperties = () => {
label="Scale"
value={selectedDecal.decalData.decalScale || 1}
min={0.1}
max={5}
max={2}
step={0.1}
onChange={(value: number) => handleScaleChange(value)}
/>

View File

@@ -1,199 +1,225 @@
import { useEffect, useState } from 'react'
import { ClockThreeIcon, LocationPinIcon, TargetIcon } from '../../../../icons/ExportCommonIcons'
import { useSceneContext } from '../../../../../modules/scene/sceneContext';
import { useProductContext } from '../../../../../modules/simulation/products/productContext';
import RenameInput from '../../../../ui/inputs/RenameInput';
import { useResourceManagementId } from '../../../../../store/builder/store';
import { set } from 'immer/dist/internal';
import { useEffect, useState } from "react";
import {
ClockThreeIcon,
LocationPinIcon,
TargetIcon,
} from "../../../../icons/ExportCommonIcons";
import { useSceneContext } from "../../../../../modules/scene/sceneContext";
import RenameInput from "../../../../ui/inputs/RenameInput";
import { useResourceManagementId } from "../../../../../store/builder/store";
import { getAssetThumbnail } from "../../../../../services/factoryBuilder/asset/assets/getAssetThumbnail";
// import NavigateCatagory from '../NavigateCatagory'
const Hrm = () => {
const [selectedCard, setSelectedCard] = useState(0);
const [workers, setWorkers] = useState<any[]>([]);
const { productStore } = useSceneContext();
const { products, getProductById } = productStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { setResourceManagementId } = useResourceManagementId();
useEffect(() => {
if (selectedProduct) {
const productDetails = getProductById(selectedProduct.productUuid);
const workerDetails = productDetails?.eventDatas || [];
const formattedWorkers = workerDetails
.filter((worker: any) => worker.type === "human")
.map((worker: any, index: number) => ({
employee: {
image: "",
name: worker.modelName,
modelId: worker.modelUuid,
employee_id: `HR-${204 + index}`,
status: "Active",
},
task: {
status: "Ongoing",
title: worker.taskTitle || "No Task Assigned",
location: {
floor: worker.floor || 0,
zone: worker.zone || "N/A"
},
planned_time_hours: worker.plannedTime || 0,
time_spent_hours: worker.timeSpent || 0,
total_tasks: worker.totalTasks || 0,
completed_tasks: worker.completedTasks || 0
},
actions: [
"Assign Task",
"Reassign Task",
"Pause",
"Emergency Stop"
],
location: `Floor ${worker.floor || "-"} . Zone ${worker.zone || "-"}`
}));
setWorkers(formattedWorkers);
}
}, [selectedProduct, getProductById]);
useEffect(() => {
//
}, [workers]);
// const employee_details = [
// {
// "employee": {
// image: "",
// "name": "John Doe",
// "employee_id": "HR-204",
// "status": "Active",
// },
// "task": {
// "status": "Ongoing",
// "title": "Inspecting Machine X",
// "location": {
// "floor": 4,
// "zone": "B"
// },
// "planned_time_hours": 6,
// "time_spent_hours": 2,
// "total_tasks": 12,
// "completed_tasks": 3
// },
// "actions": [
// "Assign Task",
// "Reassign Task",
// "Pause",
// "Emergency Stop"
// ],
// "location": "Floor 4 . Zone B"
// },
// {
// "employee": {
// image: "",
// "name": "Alice Smith",
// "employee_id": "HR-205",
// "status": "Active",
// },
// "task": {
// "status": "Ongoing",
// "title": "Calibrating Sensor Y",
// "location": {
// "floor": 2,
// "zone": "A"
// },
// "planned_time_hours": 4,
// "time_spent_hours": 1.5,
// "total_tasks": 10,
// "completed_tasks": 2
// },
// "actions": [
// "Assign Task",
// "Reassign Task",
// "Pause",
// "Emergency Stop"
// ],
// "location": "Floor 4 . Zone B"
// },
// {
// "employee": {
// image: "",
// "name": "Michael Lee",
// "employee_id": "HR-206",
// "status": "Active",
// },
// "task": {
// "status": "Ongoing",
// "title": "Testing Conveyor Belt Z",
// "location": {
// "floor": 5,
// "zone": "C"
// },
// "planned_time_hours": 5,
// "time_spent_hours": 3,
// "total_tasks": 8,
// "completed_tasks": 5
// },
// "actions": [
// "Assign Task",
// "Reassign Task",
// "Pause",
// "Emergency Stop"
// ],
// "location": "Floor 4 . Zone B"
// },
// ]
function handleRenameWorker(newName: string) {
//
const [selectedCard, setSelectedCard] = useState(0);
const [workers, setWorkers] = useState<any[]>([]);
const { setResourceManagementId } = useResourceManagementId();
const { assetStore } = useSceneContext();
const { assets: allAssets } = assetStore();
async function getAsset(assetId: string) {
let thumbnail = await getAssetThumbnail(assetId);
if (thumbnail.thumbnail) {
let assetImage = thumbnail.thumbnail;
return assetImage;
}
function handleHumanClick(employee: any) {
if (employee.modelId) {
setResourceManagementId(employee.modelId);
}
}
useEffect(() => {
if (allAssets.length > 0) {
const fetchWorkers = async () => {
const humans = allAssets.filter(
(worker: any) => worker.eventData.type === "Human"
);
const formattedWorkers = await Promise.all(
humans.map(async (worker: any, index: number) => {
const assetImage = await getAsset(worker.assetId);
return {
employee: {
image: assetImage,
name: worker.modelName,
modelId: worker.modelUuid,
employee_id: `HR-${204 + index}`,
status: "Active",
},
task: {
status: "Ongoing",
title: worker.taskTitle ?? "No Task Assigned",
location: {
floor: worker.floor ?? 0,
zone: worker.zone ?? "N/A",
},
planned_time_hours: worker.plannedTime ?? 0,
time_spent_hours: worker.timeSpent ?? 0,
total_tasks: worker.totalTasks ?? 0,
completed_tasks: worker.completedTasks ?? 0,
},
actions: [
"Assign Task",
"Reassign Task",
"Pause",
"Emergency Stop",
],
location: `Floor ${worker.floor || "-"} . Zone ${
worker.zone || "-"
}`,
};
})
);
setWorkers(formattedWorkers);
};
fetchWorkers();
}
}, [allAssets]);
// const employee_details = [
// {
// "employee": {
// image: "",
// "name": "John Doe",
// "employee_id": "HR-204",
// "status": "Active",
return (
<>
{/* <NavigateCatagory
// },
// "task": {
// "status": "Ongoing",
// "title": "Inspecting Machine X",
// "location": {
// "floor": 4,
// "zone": "B"
// },
// "planned_time_hours": 6,
// "time_spent_hours": 2,
// "total_tasks": 12,
// "completed_tasks": 3
// },
// "actions": [
// "Assign Task",
// "Reassign Task",
// "Pause",
// "Emergency Stop"
// ],
// "location": "Floor 4 . Zone B"
// },
// {
// "employee": {
// image: "",
// "name": "Alice Smith",
// "employee_id": "HR-205",
// "status": "Active",
// },
// "task": {
// "status": "Ongoing",
// "title": "Calibrating Sensor Y",
// "location": {
// "floor": 2,
// "zone": "A"
// },
// "planned_time_hours": 4,
// "time_spent_hours": 1.5,
// "total_tasks": 10,
// "completed_tasks": 2
// },
// "actions": [
// "Assign Task",
// "Reassign Task",
// "Pause",
// "Emergency Stop"
// ],
// "location": "Floor 4 . Zone B"
// },
// {
// "employee": {
// image: "",
// "name": "Michael Lee",
// "employee_id": "HR-206",
// "status": "Active",
// },
// "task": {
// "status": "Ongoing",
// "title": "Testing Conveyor Belt Z",
// "location": {
// "floor": 5,
// "zone": "C"
// },
// "planned_time_hours": 5,
// "time_spent_hours": 3,
// "total_tasks": 8,
// "completed_tasks": 5
// },
// "actions": [
// "Assign Task",
// "Reassign Task",
// "Pause",
// "Emergency Stop"
// ],
// "location": "Floor 4 . Zone B"
// },
// ]
function handleRenameWorker(newName: string) {}
function handleHumanClick(employee: any) {
if (employee.modelId) {
setResourceManagementId(employee.modelId);
}
}
return (
<>
{/* <NavigateCatagory
category={["All People", "Technician", "Operator", "Supervisor", "Safety Officer"]}
selectedCategory={selectedCategory}
setSelectedCategory={setSelectedCategory}
/> */}
<div className='hrm-container assetManagement-wrapper'>
{workers.map((employee, index) => (
<div
className={`analysis-wrapper ${selectedCard === index ? "active" : ""}`}
onClick={() => setSelectedCard(index)}
key={index}
>
<header>
<div className="user-details">
<div className="user-image-wrapper">
<img className='user-image' src={employee.employee.image} alt="" />
<div className={`status ${employee.employee.status}`}></div>
</div>
<div className="details" >
{/* <div className="employee-name">{employee.employee.name}</div> */}
<RenameInput value={employee.employee.name} onRename={handleRenameWorker} />
<div className="employee-id">{employee.employee.employee_id}</div>
</div>
</div>
<div className="hrm-container assetManagement-wrapper">
{workers.map((employee, index) => (
<div
className={`analysis-wrapper ${
selectedCard === index ? "active" : ""
}`}
onClick={() => setSelectedCard(index)}
key={index}
>
<header>
<div className="user-details">
<div className="user-image-wrapper">
<img
className="user-image"
src={employee.employee.image}
alt=""
/>
<div className={`status ${employee.employee.status}`}></div>
</div>
<div className="details">
{/* <div className="employee-name">{employee.employee.name}</div> */}
<RenameInput
value={employee.employee.name}
onRename={handleRenameWorker}
/>
<div className="employee-id">
{employee.employee.employee_id}
</div>
</div>
</div>
<div className="see-more" onClick={() => { handleHumanClick(employee.employee) }}>View in Scene</div>
</header>
<div
className="see-more"
onClick={() => {
handleHumanClick(employee.employee);
}}
>
View in Scene
</div>
</header>
<div className="content">
{/* <div className="task-info">
<div className="content">
{/* <div className="task-info">
<div className="task-wrapper">
<div className="task-label">
<span className='label-icon'><ListTaskIcon /></span>
@@ -214,17 +240,20 @@ const Hrm = () => {
</div>
</div> */}
<div className="task-stats">
<div className="stat-item">
<div className="task-stats">
<div className="stat-item">
<div className="stat-wrapper">
<span className="stat-icon">
<ClockThreeIcon />
</span>
<span>Planned time:</span>
</div>
<div className="stat-wrapper">
<span className="stat-icon"><ClockThreeIcon /></span>
<span>Planned time:</span>
</div>
<span className='stat-value'>{employee.task.planned_time_hours} hr</span>
</div>
{/* <div className="stat-item">
<span className="stat-value">
{employee.task.planned_time_hours} hr
</span>
</div>
{/* <div className="stat-item">
<div className="stat-wrapper">
<span className="stat-icon"><SlectedTickIcon /></span>
@@ -242,39 +271,41 @@ const Hrm = () => {
<span className='stat-value'>{employee.task.time_spent_hours} hr</span>
</div> */}
<div className="stat-item">
<div className="stat-item">
<div className="stat-wrapper">
<span className="stat-icon">
<TargetIcon />
</span>
<span>Cost per hr:</span>
</div>
<div className="stat-wrapper">
<span className="stat-icon"><TargetIcon /></span>
<span>Cost per hr:</span>
</div>
<span className="stat-value">
{employee.task.completed_tasks}
</span>
</div>
</div>
<span className='stat-value'>{employee.task.completed_tasks}</span>
</div>
</div>
<div className="location-wrapper">
<div className="location-header">
<div className="icon">
<LocationPinIcon />
</div>
<div className="header">Location:</div>
</div>
<div className="location-value">{employee.location}</div>
</div>
<div className="task-actions">
{/* <button className="btn btn-default">Assign Task</button>
<button className="btn btn-default">Reassign Task</button> */}
<button className="btn btn-default">Pause</button>
<button className="btn btn-danger">Emergency Stop</button>
</div>
</div>
</div>
))}
<div className="location-wrapper">
<div className="location-header">
<div className="icon">
<LocationPinIcon />
</div>
<div className="header">Location:</div>
</div>
<div className="location-value">{employee.location}</div>
</div>
{/* <div className="task-actions">
<button className="btn btn-default">Assign Task</button>
<button className="btn btn-default">Reassign Task</button>
<button className="btn btn-default">Pause</button>
<button className="btn btn-danger">Emergency Stop</button>
</div> */}
</div>
</>
)
}
</div>
))}
</div>
</>
);
};
export default Hrm
export default Hrm;

View File

@@ -1,65 +1,70 @@
import { useEffect, useState } from 'react'
// import NavigateCatagory from '../../NavigateCatagory'
import { EyeIcon, KebabIcon, LocationPinIcon, RightHalfFillCircleIcon } from '../../../../../icons/ExportCommonIcons';
import assetImage from "../../../../../../assets/image/asset-image.png"
import assetImageFallback from "../../../../../../assets/image/asset-image.png"
import { useSceneContext } from '../../../../../../modules/scene/sceneContext';
import { useProductContext } from '../../../../../../modules/simulation/products/productContext';
import RenameInput from '../../../../../ui/inputs/RenameInput';
import { useResourceManagementId } from '../../../../../../store/builder/store';
import { getAssetThumbnail } from '../../../../../../services/factoryBuilder/asset/assets/getAssetThumbnail';
import { TypeBasedAssetIcons } from '../../../../../icons/AssetTypeIcons';
const AssetManagement = () => {
// const [selectedCategory, setSelectedCategory] = useState("All Assets");
const [expandedAssetId, setExpandedAssetId] = useState<string | null>(null);
const [assets, setAssets] = useState<any[]>([]);
const { productStore } = useSceneContext();
const { getProductById } = productStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { setResourceManagementId } = useResourceManagementId();
const { assetStore } = useSceneContext();
const { assets: allAssets } = assetStore();
async function getAsset(assetId: string) {
let thumbnail = await getAssetThumbnail(assetId)
if (thumbnail.thumbnail) {
let assetImage = thumbnail.thumbnail ?? assetImageFallback;
return assetImage;
}
}
useEffect(() => {
if (selectedProduct) {
const productDetails = getProductById(selectedProduct.productUuid);
const productAssets = productDetails?.eventDatas || [];
const grouped: Record<string, any> = {};
productAssets.forEach((asset: any) => {
if (asset.type === "storageUnit" || asset.type === "human") return;
if (!grouped[asset.modelName]) {
grouped[asset.modelName] = {
id: asset.modelUuid,
name: asset.modelName,
type: asset.type,
model: asset.modelCode || "N/A",
status: asset.status || "Online",
usageRate: asset.usageRate || 15,
level: asset.level || "Level 1",
image: assetImage,
description: asset.description || "No description",
cost: asset.cost || 0,
count: 1,
};
} else {
grouped[asset.modelName].count += 1;
}
});
if (allAssets.length > 0) {
const fetchAssets = async () => {
const grouped: Record<string, any> = {};
setAssets(Object.values(grouped));
// Use Promise.all to handle all async operations
await Promise.all(allAssets.map(async (asset: any) => {
if (asset.eventData.type === "Storage" || asset.eventData.type === "Human") return;
const assetImage = await getAsset(asset.assetId);
if (!grouped[asset.assetId]) {
//
grouped[asset.assetId] = {
id: asset.modelUuid,
assetId: asset.assetId,
name: asset.modelName,
type: asset.eventData.type,
model: asset.modelCode ?? "N/A",
status: asset.status ?? "Online",
usageRate: asset.usageRate ?? 15,
level: asset.level ?? "Level 1",
image: assetImage,
description: asset.description ?? "No description",
cost: asset.cost ?? 0,
count: 1,
};
} else {
grouped[asset.assetId].count += 1;
}
}));
setAssets(Object.values(grouped));
}
fetchAssets();
}
// eslint-disable-next-line
}, [selectedProduct]);
}, [allAssets]);
function handleRenameAsset(newName: string) {
//
// if (expandedAssetId) {
// setAssets(prevAssets =>
// prevAssets.map(asset =>
// asset.id === expandedAssetId ? { ...asset, name: newName } : asset
// )
// );
// }
}
useEffect(() => {
@@ -68,8 +73,6 @@ const AssetManagement = () => {
}, [assets]);
function handleAssetClick(id: string) {
setResourceManagementId(id);
}
@@ -126,7 +129,6 @@ const AssetManagement = () => {
<div className={`assetManagement-card-wrapper ${expandedAssetId === asset.id ? "openViewMore" : ""}`} key={index}>
<header>
<div className="header-wrapper">
{expandedAssetId === asset.id ?
<>
<div className="drop-icon" onClick={() => setExpandedAssetId(null)}></div>
@@ -173,9 +175,9 @@ const AssetManagement = () => {
</div>
</div>
<div className="asset-estimate__view-button">
<div className="asset-estimate__view-button" onClick={() => handleAssetClick(asset.id)}>
<EyeIcon isClosed={false} />
<div className="asset-estimate__view-text" onClick={() => handleAssetClick(asset.id)}>View in Scene</div>
<div className="asset-estimate__view-text">View in Scene</div>
</div>
</div>

View File

@@ -4,7 +4,6 @@ import {
useLeftData,
useTopData,
} from "../../../store/visualization/useZone3DWidgetStore";
import { useRenameModeStore } from "../../../store/builder/store";
type RenameTooltipProps = {
name: string;
@@ -20,6 +19,7 @@ const RenameTooltip: React.FC<RenameTooltipProps> = ({ name, onSubmit }) => {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmit(value.trim());
echo.info(`Selected Object has been renamed to ${value.trim()}`);
setTop(0);
setLeft(0);
};

View File

@@ -12,7 +12,12 @@ interface RenameInputProps {
canEdit?: boolean;
}
const RenameInput: React.FC<RenameInputProps> = ({ value, onRename, checkDuplicate, canEdit = true }) => {
const RenameInput: React.FC<RenameInputProps> = ({
value,
onRename,
checkDuplicate,
canEdit = true,
}) => {
const [isEditing, setIsEditing] = useState(false);
const [text, setText] = useState(value);
const [isDuplicate, setIsDuplicate] = useState(false);
@@ -36,10 +41,10 @@ const RenameInput: React.FC<RenameInputProps> = ({ value, onRename, checkDuplica
};
const handleBlur = () => {
if (isDuplicate) return
if (isDuplicate) return;
setIsEditing(false);
if (onRename && !isDuplicate) {
echo.info(`Selected Object has been renamed to ${text}`)
onRename(text);
}
};
@@ -52,6 +57,7 @@ const RenameInput: React.FC<RenameInputProps> = ({ value, onRename, checkDuplica
if (e.key === "Enter" && !isDuplicate) {
setIsEditing(false);
if (onRename) {
echo.info(`Selected Object has been renamed to ${text}`)
onRename(text);
}
}
@@ -80,4 +86,4 @@ const RenameInput: React.FC<RenameInputProps> = ({ value, onRename, checkDuplica
</>
);
};
export default RenameInput
export default RenameInput;

View File

@@ -1,8 +1,8 @@
import React, { ChangeEvent, useState } from "react";
import React, { ChangeEvent, useEffect, useState } from "react";
import { CloseIcon, SearchIcon } from "../../icons/ExportCommonIcons";
interface SearchProps {
value?: string; // The current value of the search input
value?: string | null; // The current value of the search input
placeholder?: string; // Placeholder text for the input
onChange: (value: string) => void; // Callback function to handle input changes
}
@@ -22,7 +22,15 @@ const Search: React.FC<SearchProps> = ({
onChange(newValue); // Call the onChange prop with the new value
};
useEffect(() => {
if (value === null) {
setInputValue("");
handleBlur();
}
}, [value]);
const handleClear = () => {
echo.warn("Search field cleared.");
setInputValue("");
onChange(""); // Clear the input value
};
@@ -48,7 +56,7 @@ const Search: React.FC<SearchProps> = ({
<input
type="text"
className="search-input"
value={inputValue}
value={inputValue ?? ""}
placeholder={placeholder}
onChange={handleInputChange}
onFocus={handleFocus}

View File

View File

@@ -55,7 +55,7 @@ function DecalCreator() {
decalPosition: [point.x, point.y, (wall.wallThickness / 2 + 0.001) * (wallIntersect.normal?.z || 1)],
decalRotation: 0,
decalOpacity: 1,
decalScale: 1,
decalScale: 0.5,
}
addDecalOnWall(wallIntersect.object.userData.wallUuid, decal);
@@ -99,7 +99,7 @@ function DecalCreator() {
decalPosition: [point.x, point.y, -0.001],
decalRotation: 0,
decalOpacity: 1,
decalScale: 1,
decalScale: 0.5,
}
addDecalOnFloor(floorIntersect.object.userData.floorUuid, decal);

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,11 @@ import type { CameraControls } from "@react-three/drei";
const CameraShortcutsControls = () => {
const { camera, controls } = useThree();
const isTextInput = (element: Element | null): boolean =>
element instanceof HTMLInputElement ||
element instanceof HTMLTextAreaElement ||
element?.getAttribute("contenteditable") === "true";
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -21,6 +26,8 @@ const CameraShortcutsControls = () => {
const dir = new THREE.Vector3().subVectors(camera.position, target).normalize();
if (isTextInput(document.activeElement)) return;
switch (e.key) {
case "1": // Front
pos = new THREE.Vector3(0, 0, distance).add(target);

View File

@@ -88,9 +88,9 @@ const BoundingBox = ({ boundingBoxRef, isPerAsset = true }: BoundingBoxProps) =>
>
<Line
name="SelectionGroupBoundingBox"
depthWrite={false}
points={box.points}
color={savedTheme === "dark" ? "#c4abf1" : "#6f42c1"}
depthWrite={false}
lineWidth={2.7}
segments
position={[box.position[0], box.position[1], box.position[2]]}

View File

@@ -180,11 +180,11 @@ function PointsCreator() {
drag = false;
};
const onMouseUp = () => {
const onMouseUp = (e : MouseEvent) => {
if (selectedEventSphere && !drag) {
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true).filter((intersect) => intersect.object.name === "Event-Sphere");
if (intersects.length === 0) {
if (intersects.length === 0 && e.button === 0) {
clearSelectedEventSphere();
setTransformMode(null);
}

View File

@@ -199,6 +199,7 @@ const VehicleUI = () => {
steeringAngle: steeringRotation[1],
},
},
}
);

View File

@@ -1,368 +1,409 @@
import { useEffect, useRef, useState } from 'react'
import { useFrame, useThree, ThreeEvent } from '@react-three/fiber';
import * as THREE from 'three';
import { Line } from '@react-three/drei';
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore';
import { useSceneContext } from '../../../../scene/sceneContext';
import { useActiveTool, useSelectedPath } from '../../../../../store/builder/store';
import { useEffect, useRef, useState } from "react";
import { useFrame, useThree, ThreeEvent } from "@react-three/fiber";
import * as THREE from "three";
import { Line } from "@react-three/drei";
import {
useAnimationPlaySpeed,
usePauseButtonStore,
usePlayButtonStore,
useResetButtonStore,
} from "../../../../../store/usePlayButtonStore";
import { useSceneContext } from "../../../../scene/sceneContext";
import {
useActiveTool,
useSelectedPath,
} from "../../../../../store/builder/store";
interface VehicleAnimatorProps {
path: [number, number, number][];
handleCallBack: () => void;
reset: () => void;
startUnloadingProcess: () => void;
currentPhase: string;
agvUuid: string;
agvDetail: VehicleStatus;
path: [number, number, number][];
handleCallBack: () => void;
reset: () => void;
startUnloadingProcess: () => void;
currentPhase: string;
agvUuid: string;
agvDetail: VehicleStatus;
}
function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetail, reset, startUnloadingProcess }: Readonly<VehicleAnimatorProps>) {
const { vehicleStore } = useSceneContext();
const { getVehicleById } = vehicleStore();
const { isPaused } = usePauseButtonStore();
const { isPlaying } = usePlayButtonStore();
const { speed } = useAnimationPlaySpeed();
const { isReset, setReset } = useResetButtonStore();
const progressRef = useRef<number>(0);
const movingForward = useRef<boolean>(true);
const completedRef = useRef<boolean>(false);
const [objectRotation, setObjectRotation] = useState<{ x: number; y: number; z: number } | undefined>(agvDetail.point?.action?.pickUpPoint?.rotation || { x: 0, y: 0, z: 0 })
const [restRotation, setRestingRotation] = useState<boolean>(true);
const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]);
const { scene, controls } = useThree();
const { selectedPath } = useSelectedPath();
const [isAnyDragging, setIsAnyDragging] = useState<string>("");
function VehicleAnimator({
path,
handleCallBack,
currentPhase,
agvUuid,
agvDetail,
reset,
startUnloadingProcess,
}: Readonly<VehicleAnimatorProps>) {
const { vehicleStore } = useSceneContext();
const { getVehicleById } = vehicleStore();
const { isPaused } = usePauseButtonStore();
const { isPlaying } = usePlayButtonStore();
const { speed } = useAnimationPlaySpeed();
const { isReset, setReset } = useResetButtonStore();
const progressRef = useRef<number>(0);
const movingForward = useRef<boolean>(true);
const completedRef = useRef<boolean>(false);
const [objectRotation, setObjectRotation] = useState<
{ x: number; y: number; z: number } | undefined
>(agvDetail.point?.action?.pickUpPoint?.rotation || { x: 0, y: 0, z: 0 });
const [restRotation, setRestingRotation] = useState<boolean>(true);
const [currentPath, setCurrentPath] = useState<[number, number, number][]>(
[]
);
const { scene, controls } = useThree();
const { selectedPath } = useSelectedPath();
const [isAnyDragging, setIsAnyDragging] = useState<string>("");
useEffect(() => {
if (currentPhase === "stationed-pickup" && path.length > 0) {
setCurrentPath(path);
setObjectRotation(agvDetail.point.action?.pickUpPoint?.rotation);
} else if (currentPhase === "pickup-drop" && path.length > 0) {
setObjectRotation(agvDetail.point.action?.unLoadPoint?.rotation);
setCurrentPath(path);
} else if (currentPhase === "drop-pickup" && path.length > 0) {
setObjectRotation(agvDetail.point.action?.pickUpPoint?.rotation);
setCurrentPath(path);
}
}, [currentPhase, path, objectRotation, selectedPath]);
useEffect(() => {
if (currentPhase === 'stationed-pickup' && path.length > 0 && selectedPath === "auto") {
setCurrentPath(path);
setObjectRotation(agvDetail.point.action?.pickUpPoint?.rotation)
} else if (currentPhase === 'pickup-drop' && path.length > 0) {
setObjectRotation(agvDetail.point.action?.unLoadPoint?.rotation)
setCurrentPath(path);
} else if (currentPhase === 'drop-pickup' && path.length > 0) {
setObjectRotation(agvDetail.point.action?.pickUpPoint?.rotation)
setCurrentPath(path);
useEffect(() => {
completedRef.current = false;
}, [currentPath]);
useEffect(() => {
if (isReset || !isPlaying) {
reset();
setCurrentPath([]);
completedRef.current = false;
movingForward.current = true;
progressRef.current = 0;
setReset(false);
setRestingRotation(true);
const object = scene.getObjectByProperty("uuid", agvUuid);
const vehicle = getVehicleById(agvDetail.modelUuid);
if (object && vehicle) {
object.position.set(
vehicle.position[0],
vehicle.position[1],
vehicle.position[2]
);
object.rotation.set(
vehicle.rotation[0],
vehicle.rotation[1],
vehicle.rotation[2]
);
}
}
}, [isReset, isPlaying]);
const lastTimeRef = useRef(performance.now());
useFrame(() => {
if (!isPlaying) return;
const now = performance.now();
const delta = (now - lastTimeRef.current) / 1000;
lastTimeRef.current = now;
const object = scene.getObjectByProperty("uuid", agvUuid);
if (!object || currentPath.length < 2) return;
if (isPaused) return;
let totalDistance = 0;
const distances = [];
let accumulatedDistance = 0;
let index = 0;
const rotationSpeed = 0.75;
for (let i = 0; i < currentPath.length - 1; i++) {
const start = new THREE.Vector3(...currentPath[i]);
const end = new THREE.Vector3(...currentPath[i + 1]);
const segmentDistance = start.distanceTo(end);
distances.push(segmentDistance);
totalDistance += segmentDistance;
}
while (
index < distances.length &&
progressRef.current > accumulatedDistance + distances[index]
) {
accumulatedDistance += distances[index];
index++;
}
if (index < distances.length) {
const start = new THREE.Vector3(...currentPath[index]);
const end = new THREE.Vector3(...currentPath[index + 1]);
const segmentDistance = distances[index];
const targetQuaternion = new THREE.Quaternion().setFromRotationMatrix(
new THREE.Matrix4().lookAt(start, end, new THREE.Vector3(0, 1, 0))
);
const y180 = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
Math.PI
);
targetQuaternion.multiply(y180);
const angle = object.quaternion.angleTo(targetQuaternion);
if (angle < 0.01) {
object.quaternion.copy(targetQuaternion);
} else {
const step = rotationSpeed * delta * speed * agvDetail.speed;
const angle = object.quaternion.angleTo(targetQuaternion);
if (angle < step) {
object.quaternion.copy(targetQuaternion);
} else {
object.quaternion.rotateTowards(targetQuaternion, step);
}
}, [currentPhase, path, objectRotation, selectedPath]);
}
useEffect(() => {
completedRef.current = false;
}, [currentPath]);
const isAligned = angle < 0.01;
useEffect(() => {
if (isReset || !isPlaying) {
reset();
setCurrentPath([]);
completedRef.current = false;
movingForward.current = true;
progressRef.current = 0;
setReset(false);
setRestingRotation(true);
const object = scene.getObjectByProperty('uuid', agvUuid);
const vehicle = getVehicleById(agvDetail.modelUuid);
if (object && vehicle) {
object.position.set(vehicle.position[0], vehicle.position[1], vehicle.position[2]);
object.rotation.set(vehicle.rotation[0], vehicle.rotation[1], vehicle.rotation[2]);
if (isAligned) {
progressRef.current += delta * (speed * agvDetail.speed);
const t = (progressRef.current - accumulatedDistance) / segmentDistance;
const position = start.clone().lerp(end, t);
object.position.copy(position);
}
}
if (progressRef.current >= totalDistance) {
if (restRotation && objectRotation) {
const targetEuler = new THREE.Euler(
0,
objectRotation.y - agvDetail.point.action.steeringAngle,
0
);
const baseQuaternion = new THREE.Quaternion().setFromEuler(targetEuler);
const y180 = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
Math.PI
);
const targetQuaternion = baseQuaternion.multiply(y180);
const angle = object.quaternion.angleTo(targetQuaternion);
if (angle < 0.01) {
object.quaternion.copy(targetQuaternion);
setRestingRotation(false);
} else {
const step = rotationSpeed * delta * speed * agvDetail.speed;
const angle = object.quaternion.angleTo(targetQuaternion);
if (angle < step) {
object.quaternion.copy(targetQuaternion);
} else {
object.quaternion.rotateTowards(targetQuaternion, step);
}
}
return;
}
}
if (progressRef.current >= totalDistance) {
setRestingRotation(true);
progressRef.current = 0;
movingForward.current = !movingForward.current;
setCurrentPath([]);
handleCallBack();
if (currentPhase === "pickup-drop") {
requestAnimationFrame(startUnloadingProcess);
}
}
});
const updatePoint = (index: number, pos: THREE.Vector3) => {
const updated = [...currentPath];
updated[index] = pos.toArray() as [number, number, number];
setCurrentPath(updated);
};
return (
<>
{selectedPath === "auto" && (
<group visible={false}>
{currentPath.map((pos, i) => {
if (i < currentPath.length - 1) {
return (
<DraggableLineSegment
key={i}
index={i}
start={new THREE.Vector3(...currentPath[i])}
end={new THREE.Vector3(...currentPath[i + 1])}
updatePoints={(i0, p0, i1, p1) => {
const updated = [...currentPath];
updated[i0] = p0.toArray() as [number, number, number];
updated[i1] = p1.toArray() as [number, number, number];
setCurrentPath(updated);
}}
isAnyDragging={isAnyDragging}
setIsAnyDragging={setIsAnyDragging}
/>
);
}
}
}, [isReset, isPlaying])
const lastTimeRef = useRef(performance.now());
useFrame(() => {
if (!isPlaying) return
const now = performance.now();
const delta = (now - lastTimeRef.current) / 1000;
lastTimeRef.current = now;
const object = scene.getObjectByProperty('uuid', agvUuid);
if (!object || currentPath.length < 2) return;
if (isPaused) return;
let totalDistance = 0;
const distances = [];
let accumulatedDistance = 0;
let index = 0;
const rotationSpeed = 0.75;
for (let i = 0; i < currentPath.length - 1; i++) {
const start = new THREE.Vector3(...currentPath[i]);
const end = new THREE.Vector3(...currentPath[i + 1]);
const segmentDistance = start.distanceTo(end);
distances.push(segmentDistance);
totalDistance += segmentDistance;
}
while (index < distances.length && progressRef.current > accumulatedDistance + distances[index]) {
accumulatedDistance += distances[index];
index++;
}
if (index < distances.length) {
const start = new THREE.Vector3(...currentPath[index]);
const end = new THREE.Vector3(...currentPath[index + 1]);
const segmentDistance = distances[index];
const targetQuaternion = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().lookAt(start, end, new THREE.Vector3(0, 1, 0)));
const y180 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
targetQuaternion.multiply(y180);
const angle = object.quaternion.angleTo(targetQuaternion);
if (angle < 0.01) {
object.quaternion.copy(targetQuaternion);
} else {
const step = rotationSpeed * delta * speed * agvDetail.speed;
const angle = object.quaternion.angleTo(targetQuaternion);
if (angle < step) {
object.quaternion.copy(targetQuaternion);
} else {
object.quaternion.rotateTowards(targetQuaternion, step);
}
}
const isAligned = angle < 0.01;
if (isAligned) {
progressRef.current += delta * (speed * agvDetail.speed);
const t = (progressRef.current - accumulatedDistance) / segmentDistance;
const position = start.clone().lerp(end, t);
object.position.copy(position);
}
}
if (progressRef.current >= totalDistance) {
if (restRotation && objectRotation) {
const targetEuler = new THREE.Euler(0, objectRotation.y - agvDetail.point.action.steeringAngle, 0);
const baseQuaternion = new THREE.Quaternion().setFromEuler(targetEuler);
const y180 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
const targetQuaternion = baseQuaternion.multiply(y180);
const angle = object.quaternion.angleTo(targetQuaternion);
if (angle < 0.01) {
object.quaternion.copy(targetQuaternion);
setRestingRotation(false);
setTimeout(() => {
setRestingRotation(true);
progressRef.current = 0;
movingForward.current = !movingForward.current;
setCurrentPath([]);
handleCallBack();
if (currentPhase === 'pickup-drop') {
requestAnimationFrame(startUnloadingProcess);
}
}, 0)
} else {
const step = rotationSpeed * delta * speed * agvDetail.speed;
const angle = object.quaternion.angleTo(targetQuaternion);
if (angle < step) {
object.quaternion.copy(targetQuaternion);
} else {
object.quaternion.rotateTowards(targetQuaternion, step);
}
}
return;
}
}
if (progressRef.current >= totalDistance) {
setRestingRotation(true);
progressRef.current = 0;
movingForward.current = !movingForward.current;
setCurrentPath([]);
handleCallBack();
if (currentPhase === 'pickup-drop') {
requestAnimationFrame(startUnloadingProcess);
}
}
});
const updatePoint = (index: number, pos: THREE.Vector3) => {
const updated = [...currentPath];
updated[index] = pos.toArray() as [number, number, number];
setCurrentPath(updated);
};
return (
<>
{selectedPath === "auto" &&
<group visible={false}>
{currentPath.map((pos, i) => {
if (i < currentPath.length - 1) {
return (
<DraggableLineSegment
key={i}
index={i}
start={new THREE.Vector3(...currentPath[i])}
end={new THREE.Vector3(...currentPath[i + 1])}
updatePoints={(i0, p0, i1, p1) => {
const updated = [...currentPath];
updated[i0] = p0.toArray() as [number, number, number];
updated[i1] = p1.toArray() as [number, number, number];
setCurrentPath(updated);
}}
isAnyDragging={isAnyDragging}
setIsAnyDragging={setIsAnyDragging}
/>
);
}
return null;
})}
{currentPath.length > 0 && (
<group onPointerMissed={() => { if (controls) (controls as any).enabled = true; }}>
{currentPath.map((pos, i) =>
(
<DraggableSphere
key={i}
index={i}
position={new THREE.Vector3(...pos)}
onMove={updatePoint}
isAnyDragging={isAnyDragging}
setIsAnyDragging={setIsAnyDragging}
/>)
)}
</group >
)}
</group >
}
</>
);
return null;
})}
{currentPath.length > 0 && (
<group
onPointerMissed={() => {
if (controls) (controls as any).enabled = true;
}}
>
{currentPath.map((pos, i) => (
<DraggableSphere
key={i}
index={i}
position={new THREE.Vector3(...pos)}
onMove={updatePoint}
isAnyDragging={isAnyDragging}
setIsAnyDragging={setIsAnyDragging}
/>
))}
</group>
)}
</group>
)}
</>
);
}
export default VehicleAnimator;
function DraggableSphere({
index,
position,
onMove,
isAnyDragging,
setIsAnyDragging,
index,
position,
onMove,
isAnyDragging,
setIsAnyDragging,
}: {
index: number;
position: THREE.Vector3;
onMove: (index: number, pos: THREE.Vector3) => void;
isAnyDragging: string;
setIsAnyDragging: (val: string) => void;
index: number;
position: THREE.Vector3;
onMove: (index: number, pos: THREE.Vector3) => void;
isAnyDragging: string;
setIsAnyDragging: (val: string) => void;
}) {
const meshRef = useRef<THREE.Mesh>(null);
const { gl, controls, raycaster } = useThree();
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const { activeTool } = useActiveTool();
const meshRef = useRef<THREE.Mesh>(null);
const { gl, controls, raycaster } = useThree();
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const { activeTool } = useActiveTool();
const onPointerDown = (e: ThreeEvent<PointerEvent>) => {
e.stopPropagation()
if (activeTool !== 'pen') return;
setIsAnyDragging("point");
gl.domElement.style.cursor = 'grabbing';
if (controls) (controls as any).enabled = false;
const onPointerDown = (e: ThreeEvent<PointerEvent>) => {
e.stopPropagation();
if (activeTool !== "pen") return;
setIsAnyDragging("point");
gl.domElement.style.cursor = "grabbing";
if (controls) (controls as any).enabled = false;
};
const onPointerMove = (e: ThreeEvent<PointerEvent>) => {
if (isAnyDragging !== "point" || activeTool !== "pen") return;
const intersect = new THREE.Vector3();
if (raycaster.ray.intersectPlane(plane, intersect)) {
meshRef.current!.position.copy(intersect);
onMove(index, intersect);
}
};
const onPointerUp = () => {
if (activeTool !== "pen") return;
setIsAnyDragging("");
gl.domElement.style.cursor = "default";
if (controls) (controls as any).enabled = true;
};
useEffect(() => {
gl.domElement.addEventListener("pointerup", onPointerUp);
return () => {
gl.domElement.removeEventListener("pointerup", onPointerUp);
};
}, [activeTool]);
const onPointerMove = (e: ThreeEvent<PointerEvent>) => {
if (isAnyDragging !== "point" || activeTool !== 'pen') return;
const intersect = new THREE.Vector3();
if (raycaster.ray.intersectPlane(plane, intersect)) {
meshRef.current!.position.copy(intersect);
onMove(index, intersect);
}
};
const onPointerUp = () => {
if (activeTool !== 'pen') return;
setIsAnyDragging("");
gl.domElement.style.cursor = 'default';
if (controls) (controls as any).enabled = true;
};
useEffect(() => {
gl.domElement.addEventListener("pointerup", onPointerUp);
return (() => {
gl.domElement.removeEventListener("pointerup", onPointerUp);
})
}, [activeTool])
return (
<mesh
ref={meshRef}
position={position}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerMissed={onPointerUp}
>
<sphereGeometry args={[0.2, 16, 16]} />
<meshStandardMaterial color="red" />
</mesh>
);
return (
<mesh
ref={meshRef}
position={position}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerMissed={onPointerUp}
>
<sphereGeometry args={[0.2, 16, 16]} />
<meshStandardMaterial color="red" />
</mesh>
);
}
function DraggableLineSegment({
index,
start,
end,
updatePoints,
isAnyDragging,
setIsAnyDragging,
index,
start,
end,
updatePoints,
isAnyDragging,
setIsAnyDragging,
}: {
index: number;
start: THREE.Vector3;
end: THREE.Vector3;
updatePoints: (i0: number, p0: THREE.Vector3, i1: number, p1: THREE.Vector3) => void;
isAnyDragging: string;
setIsAnyDragging: (val: string) => void;
index: number;
start: THREE.Vector3;
end: THREE.Vector3;
updatePoints: (
i0: number,
p0: THREE.Vector3,
i1: number,
p1: THREE.Vector3
) => void;
isAnyDragging: string;
setIsAnyDragging: (val: string) => void;
}) {
const { gl, raycaster, controls } = useThree();
const { activeTool } = useActiveTool();
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const dragStart = useRef<THREE.Vector3 | null>(null);
const { gl, raycaster, controls } = useThree();
const { activeTool } = useActiveTool();
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const dragStart = useRef<THREE.Vector3 | null>(null);
const onPointerDown = () => {
if (activeTool !== 'pen' || isAnyDragging) return;
setIsAnyDragging("line");
gl.domElement.style.cursor = 'grabbing';
if (controls) (controls as any).enabled = false;
const onPointerDown = () => {
if (activeTool !== "pen" || isAnyDragging) return;
setIsAnyDragging("line");
gl.domElement.style.cursor = "grabbing";
if (controls) (controls as any).enabled = false;
};
const onPointerMove = (e: ThreeEvent<PointerEvent>) => {
if (isAnyDragging !== "line" || activeTool !== "pen") return;
const intersect = new THREE.Vector3();
if (raycaster.ray.intersectPlane(plane, intersect)) {
if (!dragStart.current) dragStart.current = intersect.clone();
const offset = new THREE.Vector3().subVectors(
intersect,
dragStart.current
);
const newStart = start.clone().add(offset);
const newEnd = end.clone().add(offset);
updatePoints(index, newStart, index + 1, newEnd);
}
};
const onPointerUp = () => {
if (activeTool !== "pen") return;
setIsAnyDragging("");
dragStart.current = null;
gl.domElement.style.cursor = "default";
if (controls) (controls as any).enabled = true;
};
useEffect(() => {
gl.domElement.addEventListener("pointerup", onPointerUp);
return () => {
gl.domElement.removeEventListener("pointerup", onPointerUp);
};
const onPointerMove = (e: ThreeEvent<PointerEvent>) => {
if (isAnyDragging !== "line" || activeTool !== 'pen') return;
const intersect = new THREE.Vector3();
if (raycaster.ray.intersectPlane(plane, intersect)) {
if (!dragStart.current) dragStart.current = intersect.clone();
const offset = new THREE.Vector3().subVectors(intersect, dragStart.current);
const newStart = start.clone().add(offset);
const newEnd = end.clone().add(offset);
updatePoints(index, newStart, index + 1, newEnd);
}
};
const onPointerUp = () => {
if (activeTool !== 'pen') return;
setIsAnyDragging("");
dragStart.current = null;
gl.domElement.style.cursor = 'default';
if (controls) (controls as any).enabled = true;
};
useEffect(() => {
gl.domElement.addEventListener("pointerup", onPointerUp);
return (() => {
gl.domElement.removeEventListener("pointerup", onPointerUp);
})
}, [activeTool])
return (
<Line
points={[start, end]}
color="blue"
lineWidth={5}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerMissed={onPointerUp}
/>
);
}, [activeTool]);
return (
<Line
points={[start, end]}
color="blue"
lineWidth={5}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerMissed={onPointerUp}
/>
);
}

View File

@@ -0,0 +1,297 @@
import { useFrame, useThree } from "@react-three/fiber";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Quaternion, Vector3 } from "three";
import {
useAnimationPlaySpeed,
usePlayButtonStore,
} from "../../../../../store/usePlayButtonStore";
import { usePathManager } from "../../pathCreator/function/usePathManager";
import { useCreatedPaths } from "../../../../../store/builder/store";
interface VehicleAnimatorProps {
vehiclesData: VehicleStructure[];
}
type ManagerData = {
pathId: string;
vehicleId: string;
};
export default function VehicleAnimator2({
vehiclesData,
}: VehicleAnimatorProps) {
const [managerData, setManagerData] = useState<ManagerData>();
const { scene } = useThree();
const { speed } = useAnimationPlaySpeed();
const { isPlaying } = usePlayButtonStore();
const { paths, allPaths, setAllPaths } = useCreatedPaths();
const vehicleMovementState = useRef<
Record<
string,
{
index: number;
progress: number;
hasStarted: boolean;
pathIndex: number;
pointIndex: number;
}
>
>({});
const managerRef = useRef<ManagerData>();
// Initialize all paths into allPaths store
useEffect(() => {
if (!paths || paths.length === 0) return;
const newPaths = useCreatedPaths.getState().paths.map((val: any) => ({
pathId: val.pathId,
isAvailable: true,
vehicleId: null,
}));
const merged = [...useCreatedPaths.getState().allPaths];
newPaths.forEach((p: any) => {
if (!merged.find((m) => m.pathId === p.pathId)) {
merged.push(p);
}
});
if (merged.length !== useCreatedPaths.getState().allPaths.length) {
setAllPaths(merged);
}
}, [paths, allPaths, setAllPaths]);
useFrame((_, delta) => {
if (!isPlaying) return;
vehiclesData.forEach((vehicle) => {
const { vehicleId, route } = vehicle;
if (!route || route.length === 0) return;
const mesh = scene.getObjectByProperty("uuid", vehicleId);
if (!mesh) return;
const uuid = vehicleId;
// ✅ Initialize state if not already
if (!vehicleMovementState.current[uuid]) {
vehicleMovementState.current[uuid] = {
index: 0,
pointIndex: 0,
pathIndex: 0,
progress: 0,
hasStarted: false,
};
}
const state = vehicleMovementState.current[uuid];
let currentPath = route[state.pathIndex];
if (
!currentPath ||
!currentPath.pathPoints ||
currentPath.pathPoints.length < 2
)
return;
let pathPoints = currentPath.pathPoints;
const pathStart = new Vector3(...pathPoints[0].position);
/**
* 🟢 STEP 1: Move vehicle to the starting point of its first path
*/
if (!state.hasStarted) {
const distanceToStart = mesh.position.distanceTo(pathStart);
const step = speed * delta;
if (distanceToStart <= step) {
mesh.position.copy(pathStart);
mesh.quaternion.identity();
state.hasStarted = true;
return;
}
const direction = pathStart.clone().sub(mesh.position).normalize();
mesh.position.add(direction.clone().multiplyScalar(step));
const forward = new Vector3(0, 0, 1);
const targetQuat = new Quaternion().setFromUnitVectors(
forward,
direction
);
mesh.quaternion.slerp(targetQuat, 0.1);
return;
}
/**
* 🟢 STEP 2: Move along path once at start
*/
const currentPoint = new Vector3(
...pathPoints[state.pointIndex].position
);
const nextPoint = new Vector3(
...pathPoints[state.pointIndex + 1].position
);
const segmentVector = new Vector3().subVectors(nextPoint, currentPoint);
const segmentLength = segmentVector.length();
const direction = segmentVector.clone().normalize();
const moveDistance = speed * delta;
state.progress += moveDistance / segmentLength;
if (state.progress >= 1) {
state.pointIndex++;
state.progress = 0;
if (state.pointIndex >= pathPoints.length - 1) {
state.pathIndex++;
state.pointIndex = 0;
if (state.pathIndex >= route.length) {
return;
}
currentPath = route[state.pathIndex];
pathPoints = currentPath.pathPoints;
}
}
// Interpolate position
const newPos = new Vector3().lerpVectors(
new Vector3(...pathPoints[state.pointIndex].position),
new Vector3(...pathPoints[state.pointIndex + 1].position),
state.progress
);
mesh.position.copy(newPos);
// Smooth rotation
const forward = new Vector3(0, 0, 1);
const targetQuat = new Quaternion().setFromUnitVectors(
forward,
direction
);
mesh.quaternion.slerp(targetQuat, 0.1);
const pathCheck = handlePathCheck(currentPath.pathId, vehicleId);
console.log("pathCheck: ", pathCheck);
//
// 🟢 Log current pathId while moving
// console.log(
// `🚗 Vehicle ${uuid} moving on pathId: ${currentPath.pathId}`,
// "→",
// newPos.toArray()
// );
});
});
// const handlePathCheck = (pathId: string, vehicleId: string) => {
// const { paths, allPaths, setAllPaths } = useCreatedPaths.getState();
// const normalize = (v: any) => String(v ?? "").trim();
// // Find path
// const path = paths.find(
// (p: any) => normalize(p.pathId) === normalize(pathId)
// );
// if (!path) {
//
// return false;
// }
// // If path already reserved → always reject
// if (!path.isAvailable) {
// console.log(
// `🚨 Path ${pathId} is already reserved by vehicle ${vehicleId}`
// );
// return false;
// }
//
// // Reserve the path for this vehicle
// const updated = allPaths.map((p: any) =>
// normalize(p.pathId) === normalize(pathId)
// ? { ...p, vehicleId, isAvailable: false }
// : p
// );
// setAllPaths(updated);
//
// return true;
// };
const handlePathCheck = (pathId: string, vehicleId: string) => {
const normalize = (v: any) => String(v ?? "").trim();
// Find path
const path = useCreatedPaths
.getState()
.allPaths.find((p: any) => normalize(p.pathId) === normalize(pathId));
if (!path) {
return false;
}
// If path already reserved → reject always
if (!path.isAvailable) {
// console.warn(
// `🚨 Path ${pathId} is already reserved by vehicle ${vehicleId}`
// );
echo.warn(`Path ${pathId}`);
return false;
} else {
console.log("path is reserved");
}
// Reserve the path properly with vehicleId
const updated = allPaths.map((p: any) =>
normalize(p.pathId) === normalize(pathId)
? { ...p, vehicleId, isAvailable: false }
: p
);
console.log("updated: ", updated);
setAllPaths(updated);
return true;
};
// const manager = usePathManager(
// managerRef.current?.pathId,
// managerRef.current?.vehicleId
// );
return null;
}
// const handlePathCheck = (pathId: string, vehicleId: string) => {
// const allPaths = useCreatedPaths.getState().allPaths;
// // find the path were checking
// const path = allPaths.find((p: any) => p.pathId === pathId);
// if (!path) {
//
// return false;
// }
// // if path is available, update it with vehicleId and mark unavailable
// if (path.isAvailable) {
// const updated = allPaths.map((p: any) =>
// p.pathId === pathId ? { ...p, vehicleId, isAvailable: false } : p
// );
//
// //
// // setAllPaths(updated); // uncomment if you want to persist update
// return true; // path was available
// }
//
// return false; // path not available
// };
// useEffect(() => {
//
// if (managerRef.current) {
//
// }
// }, [manager, managerRef.current]);

View File

@@ -0,0 +1,271 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useCreatedPaths } from "../../../../../store/builder/store";
import { useThree } from "@react-three/fiber";
import { Vector3 } from "three";
import { useSceneContext } from "../../../../scene/sceneContext";
import { useProductContext } from "../../../products/productContext";
import { useSelectedEventSphere } from "../../../../../store/simulation/useSimulationStore";
import VehicleAnimator2 from "../animator/vehicleAnimator2";
function dist(a: PointData, b: PointData): number {
return Math.sqrt(
(a.position[0] - b.position[0]) ** 2 +
(a.position[1] - b.position[1]) ** 2 +
(a.position[2] - b.position[2]) ** 2
);
}
type SegmentPoint = {
position: Vector3;
originalPoint?: PointData;
pathId?: string;
startId?: string;
endId?: string;
};
/** --- A* Algorithm --- */
type AStarResult = {
pointIds: string[];
distance: number;
};
function aStarShortestPath(
startId: string,
goalId: string,
points: PointData[],
paths: PathData
): AStarResult | null {
const pointById = new Map(points.map((p) => [p.pointId, p]));
const start = pointById.get(startId);
const goal = pointById.get(goalId);
if (!start || !goal) return null;
const openSet = new Set<string>([startId]);
const cameFrom: Record<string, string | null> = {};
const gScore: Record<string, number> = {};
const fScore: Record<string, number> = {};
for (const p of points) {
cameFrom[p.pointId] = null;
gScore[p.pointId] = Infinity;
fScore[p.pointId] = Infinity;
}
gScore[startId] = 0;
fScore[startId] = dist(start, goal);
const neighborsOf = (id: string): { id: string; cost: number }[] => {
const me = pointById.get(id)!;
const out: { id: string; cost: number }[] = [];
for (const edge of paths) {
const [a, b] = edge.pathPoints;
if (a.pointId === id) out.push({ id: b.pointId, cost: dist(me, b) });
else if (b.pointId === id) out.push({ id: a.pointId, cost: dist(me, a) });
}
return out;
};
while (openSet.size > 0) {
let current: string = [...openSet].reduce((a, b) =>
fScore[a] < fScore[b] ? a : b
);
if (current === goalId) {
const ids: string[] = [];
let node: string | null = current;
while (node) {
ids.unshift(node);
node = cameFrom[node];
}
return { pointIds: ids, distance: gScore[goalId] };
}
openSet.delete(current);
for (const nb of neighborsOf(current)) {
const tentativeG = gScore[current] + nb.cost;
if (tentativeG < gScore[nb.id]) {
cameFrom[nb.id] = current;
gScore[nb.id] = tentativeG;
fScore[nb.id] = tentativeG + dist(pointById.get(nb.id)!, goal);
openSet.add(nb.id);
}
}
}
return null;
}
/** --- Convert node path to edges --- */
function nodePathToEdges(
pointIds: string[],
points: PointData[],
paths: PathData
): PathData {
const byId = new Map(points.map((p) => [p.pointId, p]));
const edges: PathData = [];
for (let i = 0; i < pointIds.length - 1; i++) {
const a = pointIds[i];
const b = pointIds[i + 1];
const edge = paths.find(
(p) =>
(p.pathPoints[0].pointId === a && p.pathPoints[1].pointId === b) ||
(p.pathPoints[0].pointId === b && p.pathPoints[1].pointId === a)
);
if (edge) {
const [p1, p2] = edge.pathPoints;
edges.push({
pathId: edge.pathId,
pathPoints:
p1.pointId === a
? ([p1, p2] as [PointData, PointData])
: ([p2, p1] as [PointData, PointData]),
});
} else {
const pa = byId.get(a)!;
const pb = byId.get(b)!;
edges.push({
pathId: `synthetic-${a}-${b}`,
pathPoints: [pa, pb],
});
}
}
return edges;
}
interface VehicleInstanceProps {
vehicleData: VehicleStructure;
vehiclesData: VehicleStructure[];
setVehiclesData: React.Dispatch<React.SetStateAction<VehicleStructure[]>>;
}
export default function VehicleInstance2({
vehicleData,
vehiclesData,
setVehiclesData,
}: VehicleInstanceProps) {
const { paths, setPaths } = useCreatedPaths();
const { vehicleStore, productStore } = useSceneContext();
const { vehicles, getVehicleById } = vehicleStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { updateEvent, updateAction } = productStore();
const { selectedEventSphere } = useSelectedEventSphere();
const { scene, gl, raycaster } = useThree();
const [selected, setSelected] = useState<any>([]);
const allPoints = useMemo(() => {
const points: PointData[] = [];
const seen = new Set<string>();
useCreatedPaths.getState().paths?.forEach((path: PathDataInterface) => {
path.pathPoints.forEach((p) => {
if (!seen.has(p.pointId)) {
seen.add(p.pointId);
points.push(p);
}
});
});
return points;
}, [paths]);
const vehiclesDataRef = useRef(vehiclesData);
const selectedEventSphereRef = useRef(selectedEventSphere);
useEffect(() => {
vehiclesDataRef.current = vehiclesData;
}, [vehiclesData]);
useEffect(() => {
selectedEventSphereRef.current = selectedEventSphere;
}, [selectedEventSphere]);
const handleContextMenu = (e: any) => {
const intersectObject = raycaster.intersectObjects(scene.children);
if (intersectObject.length > 0) {
const pathPoint = intersectObject[0].object;
if (pathPoint.name === "Path-Point") {
const point: any = pathPoint.userData;
const pointIndex = allPoints.findIndex(
(p) => p.pointId === point.pointId
);
if (pointIndex === -1) return;
setSelected((prev: any) => {
if (prev.length === 0) {
return [pointIndex];
}
if (prev.length === 1) {
const prevPoint = allPoints[prev[0]];
const newPoint = allPoints[pointIndex];
//
//
if (prevPoint.pointId === newPoint.pointId) return prev;
const result = aStarShortestPath(
prevPoint.pointId,
newPoint.pointId,
allPoints,
paths
);
if (result) {
const edges = nodePathToEdges(result.pointIds, allPoints, paths);
setTimeout(() => {
const modelUuid = selectedEventSphere?.userData?.modelUuid;
// const index = vehiclesData.findIndex(
// (v) => v.vehicleId === modelUuid
// );
// if (index !== -1) {
// const updatedVehicles = [...vehiclesData];
// updatedVehicles[index] = {
// ...updatedVehicles[index],
// startPoint: prevPoint.position,
// endPoint: newPoint.position,
// route: edges,
// };
//
// setVehiclesData(updatedVehicles);
// }
// }, 0);
const index = vehiclesDataRef.current.findIndex(
(v) => v.vehicleId === modelUuid
);
if (index !== -1) {
const updatedVehicles = [...vehiclesDataRef.current];
updatedVehicles[index] = {
...vehiclesDataRef.current[index],
startPoint: prevPoint.position,
endPoint: newPoint.position,
route: edges,
};
setVehiclesData(updatedVehicles);
}
}, 0);
}
return [prev[0], pointIndex];
}
return [pointIndex];
});
}
}
};
useEffect(() => {
const canvasElement = gl.domElement;
canvasElement.addEventListener("contextmenu", handleContextMenu);
console.log("vehiclesDataRef.current: ", vehiclesDataRef.current);
return () => {
canvasElement.removeEventListener("contextmenu", handleContextMenu);
};
}, [raycaster, setVehiclesData, vehiclesData, selectedEventSphere]);
return <VehicleAnimator2 vehiclesData={vehiclesDataRef.current} />;
}

View File

@@ -1,21 +1,44 @@
import React from "react";
import React, { useEffect, useState } from "react";
import VehicleInstance from "./instance/vehicleInstance";
import VehicleContentUi from "../../ui3d/VehicleContentUi";
import { useSceneContext } from "../../../scene/sceneContext";
import { useViewSceneStore } from "../../../../store/builder/store";
import PathCreator from "../pathCreator/pathCreator";
import VehicleInstance2 from "./instance/vehicleInstance2";
function VehicleInstances() {
const { vehicleStore } = useSceneContext();
const { vehicles } = vehicleStore();
const { viewSceneLabels } = useViewSceneStore();
const [vehiclesData, setVehiclesData] = useState<VehicleStructure[]>([]);
useEffect(() => {
const updatedVehicles = vehicles.map((val) => ({
vehicleId: val.modelUuid,
position: val.position,
rotation: val.rotation,
startPoint: null,
endPoint: null,
selectedPointId: val.point.uuid,
}));
setVehiclesData(updatedVehicles);
}, []);
return (
<>
{vehicles.map((vehicle: VehicleStatus) => (
{/* {vehicles.map((vehicle: VehicleStatus) => (
<React.Fragment key={vehicle.modelUuid}>
<VehicleInstance agvDetail={vehicle} />
{viewSceneLabels && <VehicleContentUi vehicle={vehicle} />}
</React.Fragment>
))} */}
{vehiclesData.map((vehicle: VehicleStructure) => (
<React.Fragment key={vehicle.vehicleId}>
<VehicleInstance2
vehicleData={vehicle}
vehiclesData={vehiclesData}
setVehiclesData={setVehiclesData}
/>
</React.Fragment>
))}
</>
);

View File

@@ -0,0 +1,13 @@
export const getPathPointByPoints = (point: any, paths: any) => {
for (const path of paths) {
if (
(path.pathPoints[0].pointId === point[0].pointId ||
path.pathPoints[1].pointId === point[0].pointId) &&
(path.pathPoints[0].pointId === point[1].pointId ||
path.pathPoints[1].pointId === point[1].pointId)
) {
return path;
}
}
return undefined;
};

View File

@@ -0,0 +1,48 @@
import { useCallback } from "react";
type PointData = {
pointId: string;
position: [number, number, number];
isCurved?: boolean;
handleA?: [number, number, number] | null;
handleB?: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
isActive?: boolean;
isCurved?: boolean;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
type setPathPositionProps = (
pointUuid: string,
position: [number, number, number],
setPaths: React.Dispatch<React.SetStateAction<PathData>>
) => void;
export const getPathsByPointId = (pointId: any, paths: PathData) => {
return paths.filter((a) => a.pathPoints.some((p) => p.pointId === pointId));
};
export const setPathPosition = (
pointUuid: string,
position: [number, number, number],
setPaths: React.Dispatch<React.SetStateAction<PathData>>,
paths: PathData
) => {
const newPaths = paths.map((path: any) => {
if (path?.pathPoints.some((p: any) => p.pointId === pointUuid)) {
return {
...path,
pathPoints: path.pathPoints.map((p: any) =>
p.pointId === pointUuid ? { ...p, position } : p
) as [PointData, PointData], // 👈 force back to tuple
};
}
return path;
});
setPaths(newPaths);
};

View File

@@ -0,0 +1,49 @@
import { useEffect, useMemo } from "react";
import { useCreatedPaths } from "../../../../../store/builder/store";
export const usePathManager = (pathId?: string, vehicleId?: string) => {
const { paths, allPaths, setAllPaths } = useCreatedPaths();
// Initialize all paths into allPaths store
useEffect(() => {
if (!paths || paths.length === 0) return;
const newPaths = paths.map((val: any) => ({
pathId: val.pathId,
isAvailable: true,
vehicleId: null,
}));
const merged = [...allPaths];
newPaths.forEach((p: any) => {
if (!merged.find((m) => m.pathId === p.pathId)) {
merged.push(p);
}
});
if (merged.length !== allPaths.length) {
setAllPaths(merged);
}
}, [paths, allPaths, setAllPaths]);
// Assign vehicle to a path
useEffect(() => {
if (!pathId || !vehicleId) return;
const updated = allPaths.map((p: any) =>
p.pathId === pathId ? { ...p, vehicleId, isAvailable: false } : p
);
const hasChanged = JSON.stringify(updated) !== JSON.stringify(allPaths);
if (hasChanged) {
setAllPaths(updated);
}
}, [pathId, vehicleId, allPaths, setAllPaths]);
// ✅ return true if path exists & isAvailable, else false
return useMemo(() => {
if (!pathId) return false;
const path = allPaths.find((p: any) => p.pathId === pathId);
return path ? path.isAvailable : false;
}, [pathId, allPaths]);
};

View File

@@ -0,0 +1,428 @@
import { DragControls, Line } from "@react-three/drei";
import { useFrame, useThree } from "@react-three/fiber";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { LineCurve3, MathUtils, Plane, Vector3 } from "three";
import {
useActiveTool,
useCreatedPaths,
useToolMode,
} from "../../../../store/builder/store";
import PointHandler from "./pointHandler";
import { getPathPointByPoints } from "./function/getPathPointByPoints";
import PathHandler from "./pathHandler";
type PointData = {
pointId: string;
position: [number, number, number];
isCurved?: boolean;
handleA?: [number, number, number] | null;
handleB?: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
isActive?: boolean;
isCurved?: boolean;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
export default function PathCreator() {
const { paths, setPaths } = useCreatedPaths();
const { activeTool } = useActiveTool();
const { toolMode } = useToolMode();
const [draftPoints, setDraftPoints] = useState<PointData[]>([]);
const [mousePos, setMousePos] = useState<[number, number, number] | null>(
null
);
const [snappedPosition, setSnappedPosition] = useState<
[number, number, number] | null
>(null);
const [snappedPoint, setSnappedPoint] = useState<PointData | null>(null);
const finalPosition = useRef<[number, number, number] | null>(null);
const [hoveredLine, setHoveredLine] = useState<PathDataInterface | null>(
null
);
const [hoveredPoint, setHoveredPoint] = useState<PointData | null>(null);
const [pathPointsList, setPathPointsList] = useState<PointData[]>([]);
const [selectedPointIndices, setSelectedPointIndices] = useState<number[]>(
[]
);
const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []);
const { scene, raycaster, gl } = useThree();
const POINT_SNAP_THRESHOLD = 0.5;
const CAN_POINT_SNAP = true;
const getAllOtherPathPoints = useCallback((): PointData[] => {
if (draftPoints.length === 0) return [];
return (
paths?.flatMap((path: any) =>
path.pathPoints.filter(
(pt: PointData) => pt.pointId !== draftPoints[0].pointId
)
) ?? []
);
}, [paths, draftPoints]);
useEffect(() => {
const stored = localStorage.getItem("paths");
setPaths(stored ? JSON.parse(stored) : []);
}, []);
const snapPathPoint = useCallback(
(position: [number, number, number]) => {
if (draftPoints.length === 0 || !CAN_POINT_SNAP) {
return {
position,
isSnapped: false,
snappedPoint: null as PointData | null,
};
}
const otherPoints = getAllOtherPathPoints();
const currentVec = new Vector3(...position);
for (const point of otherPoints) {
const pointVec = new Vector3(...point.position);
const distance = currentVec.distanceTo(pointVec);
if (distance <= POINT_SNAP_THRESHOLD) {
return {
position: point.position,
isSnapped: true,
snappedPoint: point,
};
}
}
return { position, isSnapped: false, snappedPoint: null };
},
[draftPoints, getAllOtherPathPoints]
);
// ---- RAYCAST ----
useFrame(() => {
const intersectionPoint = new Vector3();
raycaster.ray.intersectPlane(plane, intersectionPoint);
if (!intersectionPoint) return;
const snapped = snapPathPoint([
intersectionPoint.x,
intersectionPoint.y,
intersectionPoint.z,
]);
if (snapped.isSnapped && snapped.snappedPoint) {
finalPosition.current = snapped.position;
setSnappedPosition(snapped.position);
setSnappedPoint(snapped.snappedPoint);
} else {
finalPosition.current = [
intersectionPoint.x,
intersectionPoint.y,
intersectionPoint.z,
];
setSnappedPosition(null);
setSnappedPoint(null);
}
if (!finalPosition.current) return;
const paths: [PointData, PointData] = [
draftPoints[0],
{ pointId: "temp-point", position: finalPosition.current },
];
});
const getPathPointById = (uuid: any) => {
for (const path of paths) {
const point = path.pathPoints.find((p: PointData) => p.pointId === uuid);
if (point) return point;
}
return undefined;
};
const handleClick = (e: any) => {
if (activeTool !== "pen") return;
if (toolMode === "3D-Delete") return;
if (e.ctrlKey) return;
const intersectionPoint = new Vector3();
const pos = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (!pos) return;
const pointIntersect = raycaster
.intersectObjects(scene.children)
.find((intersect) => intersect.object.name === "Path-Point");
const pathIntersect = raycaster
.intersectObjects(scene.children)
.find((intersect) => intersect.object.name === "Path-Line");
// --- Case 1: Split path ---
if (!pointIntersect && pathIntersect) {
const hitLine = pathIntersect.object;
const clickedPath = getPathPointByPoints(
hitLine.userData.pathPoints,
paths
);
if (clickedPath) {
const hitPath = paths.find(
(p: PathDataInterface) => p.pathId === clickedPath.pathId
);
if (!hitPath) return;
const [p1, p2] = clickedPath.pathPoints;
const point1Vec = new Vector3(...p1.position);
const point2Vec = new Vector3(...p2.position);
// Project clicked point onto line
const lineDir = new Vector3()
.subVectors(point2Vec, point1Vec)
.normalize();
const point1ToClick = new Vector3().subVectors(
pathIntersect.point,
point1Vec
);
const dot = point1ToClick.dot(lineDir);
const projection = new Vector3()
.copy(lineDir)
.multiplyScalar(dot)
.add(point1Vec);
const lineLength = point1Vec.distanceTo(point2Vec);
let t = point1Vec.distanceTo(projection) / lineLength;
t = Math.max(0, Math.min(1, t));
const closestPoint = new Vector3().lerpVectors(point1Vec, point2Vec, t);
// const filteredPath = paths.filter(
// (p: PathDataInterface) =>
// String(p.pathId).trim() !== String(clickedPath.pathId).trim()
// );
// setPaths(filteredPath);
const filteredPath = useCreatedPaths
.getState()
.paths.filter(
(p: PathDataInterface) =>
String(p.pathId).trim() !== String(clickedPath.pathId).trim()
);
setPaths(filteredPath);
const point1: PointData = {
pointId: clickedPath?.pathPoints[0].pointId,
position: clickedPath?.pathPoints[0].position,
};
const point2: PointData = {
pointId: clickedPath?.pathPoints[1].pointId,
position: clickedPath?.pathPoints[1].position,
};
const splitPoint: PointData = {
pointId: MathUtils.generateUUID(),
position: closestPoint.toArray() as [number, number, number],
};
if (draftPoints.length === 0) {
const path1: PathDataInterface = {
pathId: MathUtils.generateUUID(),
pathPoints: [point1, splitPoint],
};
const path2: PathDataInterface = {
pathId: MathUtils.generateUUID(),
pathPoints: [point2, splitPoint],
};
setDraftPoints([splitPoint]);
// Instead of relying on "paths" from the component:
setPaths([
...useCreatedPaths.getState().paths, // 👈 always current from store
path1,
path2,
]);
// setPaths([...paths, path1, path2]);
} else {
const newPath: PathDataInterface = {
pathId: MathUtils.generateUUID(),
pathPoints: [draftPoints[0], splitPoint],
};
const firstPath: PathDataInterface = {
pathId: MathUtils.generateUUID(),
pathPoints: [point1, splitPoint],
};
const secondPath: PathDataInterface = {
pathId: MathUtils.generateUUID(),
pathPoints: [point2, splitPoint],
};
setPaths([
...useCreatedPaths.getState().paths,
newPath,
firstPath,
secondPath,
]);
setDraftPoints([splitPoint]);
}
return;
}
}
const newPoint: PointData = {
pointId: MathUtils.generateUUID(),
position: [pos.x, pos.y, pos.z],
};
// --- Case 2: Normal path creation ---
let clickedPoint: PointData | null = null;
for (const pt of allPoints) {
if (new Vector3(...pt.position).distanceTo(pos) <= POINT_SNAP_THRESHOLD) {
clickedPoint = pt;
break;
}
}
if (snappedPosition && snappedPoint) {
newPoint.pointId = snappedPoint.pointId;
newPoint.position = snappedPosition;
}
if (snappedPoint && snappedPoint.pointId == draftPoints[0]?.pointId) {
return;
}
if (snappedPosition && !snappedPoint) {
newPoint.position = snappedPosition;
}
if (pointIntersect && !snappedPoint) {
const point = getPathPointById(pointIntersect.object.userData.pointId);
if (point) {
newPoint.pointId = point.pointId;
newPoint.position = point.position;
}
}
setPathPointsList((prev) => {
if (!prev.find((p) => p.pointId === newPoint.pointId))
return [...prev, newPoint];
return prev;
});
if (draftPoints.length === 0) {
setDraftPoints([newPoint]);
} else {
const newPath: PathDataInterface = {
pathId: MathUtils.generateUUID(),
pathPoints: [draftPoints[0], newPoint],
};
setPaths([...useCreatedPaths.getState().paths, newPath]);
setDraftPoints([newPoint]);
}
};
const handleContextMenu = (event: any) => {
event.preventDefault();
setDraftPoints([]);
};
const handleMouseMove = () => {
const intersectionPoint = new Vector3();
const pos = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (pos) {
setMousePos([pos.x, pos.y, pos.z]);
}
};
useEffect(() => {
const canvasElement = gl.domElement;
canvasElement.addEventListener("click", handleClick);
canvasElement.addEventListener("mousemove", handleMouseMove);
canvasElement.addEventListener("contextmenu", handleContextMenu);
return () => {
canvasElement.removeEventListener("click", handleClick);
canvasElement.removeEventListener("mousemove", handleMouseMove);
canvasElement.removeEventListener("contextmenu", handleContextMenu);
};
}, [gl, draftPoints, paths, toolMode]);
const allPoints = useMemo(() => {
const points: PointData[] = [];
const seen = new Set<string>();
paths?.forEach((path: PathDataInterface) => {
path.pathPoints.forEach((p) => {
if (!seen.has(p.pointId)) {
seen.add(p.pointId);
points.push(p);
}
});
});
return points;
}, [paths]);
useEffect(() => {
localStorage.setItem("paths", JSON.stringify(paths));
console.log("paths: ", paths);
}, [paths]);
return (
<>
{/* Draft points (red) */}
{draftPoints.map((point) => (
<mesh key={point.pointId} position={point.position}>
<sphereGeometry args={[0.2, 16, 16]} />
<meshBasicMaterial color="red" />
</mesh>
))}
{/* Saved points */}
{allPoints.map((point: PointData, i: any) => (
<PointHandler
points={allPoints}
pointIndex={i}
key={point.pointId}
point={point}
setPaths={setPaths}
paths={paths}
setHoveredPoint={setHoveredPoint}
hoveredLine={hoveredLine}
hoveredPoint={hoveredPoint}
selected={selectedPointIndices}
setSelected={setSelectedPointIndices}
/>
))}
{/* Preview line */}
{draftPoints.length > 0 && mousePos && (
<Line
points={[draftPoints[0].position, mousePos]}
color="orange"
lineWidth={2}
dashed
/>
)}
{/* Permanent paths */}
{paths.map((path: PathDataInterface) => (
<PathHandler
key={path.pathId}
selectedPath={path}
setPaths={setPaths}
paths={paths}
points={path.pathPoints}
setHoveredLine={setHoveredLine}
hoveredLine={hoveredLine}
hoveredPoint={hoveredPoint}
/>
))}
</>
);
}

View File

@@ -0,0 +1,189 @@
import { DragControls, Line } from "@react-three/drei";
import React, { useMemo, useState } from "react";
import { useActiveTool, useToolMode } from "../../../../store/builder/store";
import { useThree } from "@react-three/fiber";
import { LineCurve3, Plane, Vector3 } from "three";
import { getPathsByPointId, setPathPosition } from "./function/getPaths";
import { handleCanvasCursors } from "../../../../utils/mouseUtils/handleCanvasCursors";
type PointData = {
pointId: string;
position: [number, number, number];
isCurved?: boolean;
handleA?: [number, number, number] | null;
handleB?: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
isActive?: boolean;
isCurved?: boolean;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
type PathHandlerProps = {
selectedPath: PathDataInterface;
points: [PointData, PointData];
paths: PathData;
setPaths: React.Dispatch<React.SetStateAction<PathData>>;
setHoveredLine: React.Dispatch<
React.SetStateAction<PathDataInterface | null>
>;
hoveredLine: PathDataInterface | null;
hoveredPoint: PointData | null;
};
export default function PathHandler({
selectedPath,
setPaths,
points,
paths,
setHoveredLine,
hoveredLine,
hoveredPoint,
}: PathHandlerProps) {
const { toolMode } = useToolMode();
const { scene, raycaster } = useThree();
const [dragOffset, setDragOffset] = useState<Vector3 | null>(null);
const [initialPositions, setInitialPositions] = useState<{
paths?: any;
}>({});
const [isHovered, setIsHovered] = useState(false);
const { activeTool } = useActiveTool();
const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []);
const path = useMemo(() => {
const [start, end] = points.map((p) => new Vector3(...p.position));
return new LineCurve3(start, end);
}, [points]);
const removePath = (pathId: string) => {
setPaths((prevPaths) => prevPaths.filter((p) => p.pathId !== pathId));
};
const handlePathClick = (pointId: string) => {
if (toolMode === "3D-Delete") {
removePath(pointId);
}
};
const handleDragStart = (points: [PointData, PointData]) => {
if (activeTool !== "cursor") return;
const intersectionPoint = new Vector3();
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (hit) {
const start = new Vector3(...points[0].position);
const end = new Vector3(...points[1].position);
const midPoint = new Vector3().addVectors(start, end).multiplyScalar(0.5);
const offset = new Vector3().subVectors(midPoint, hit);
setDragOffset(offset);
const pathSet = getPathsByPointId(points[0].pointId, paths);
setInitialPositions({ paths: pathSet });
}
};
// const handleDrag = (points: [PointData, PointData]) => {
// if (isHovered && dragOffset) {
// const intersectionPoint = new Vector3();
// const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
// if (hit) {
// handleCanvasCursors("grabbing");
// const positionWithOffset = new Vector3().addVectors(hit, dragOffset);
// const start = new Vector3(...points[0].position);
// const end = new Vector3(...points[1].position);
// const midPoint = new Vector3()
// .addVectors(start, end)
// .multiplyScalar(0.5);
// const delta = new Vector3().subVectors(positionWithOffset, midPoint);
// const newStart = new Vector3().addVectors(start, delta);
// const newEnd = new Vector3().addVectors(end, delta);
// setPathPosition(
// points[0].pointId,
// [newStart.x, newStart.y, newStart.z],
// setPaths
// );
// setPathPosition(
// points[1].pointId,
// [newEnd.x, newEnd.y, newEnd.z],
// setPaths
// );
// }
// }
// };
const handleDrag = (points: [PointData, PointData]) => {
if (isHovered && dragOffset) {
const intersectionPoint = new Vector3();
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (hit) {
handleCanvasCursors("grabbing");
const positionWithOffset = new Vector3().addVectors(hit, dragOffset);
const start = new Vector3(...points[0].position);
const end = new Vector3(...points[1].position);
const midPoint = new Vector3()
.addVectors(start, end)
.multiplyScalar(0.5);
const delta = new Vector3().subVectors(positionWithOffset, midPoint);
const newStart: [number, number, number] = [
start.x + delta.x,
start.y + delta.y,
start.z + delta.z,
];
const newEnd: [number, number, number] = [
end.x + delta.x,
end.y + delta.y,
end.z + delta.z,
];
}
}
};
const handleDragEnd = (points: [PointData, PointData]) => {};
return (
<>
<DragControls
axisLock="y"
autoTransform={false}
onDragStart={() => handleDragStart(points)}
onDrag={() => handleDrag(points)}
onDragEnd={() => handleDragEnd(points)}
>
<Line
name="Path-Line"
key={selectedPath.pathId}
points={[points[0].position, points[1].position]}
color="purple"
lineWidth={5}
userData={selectedPath}
onClick={(e) => {
e.stopPropagation();
handlePathClick(selectedPath.pathId);
}}
onPointerOver={(e) => {
if (e.buttons === 0 && !e.ctrlKey) {
setHoveredLine(selectedPath);
setIsHovered(true);
if (!hoveredPoint) {
// handleCanvasCursors("grab");
}
}
}}
onPointerOut={() => {
if (isHovered && hoveredLine) {
setHoveredLine(null);
if (!hoveredPoint) {
// handleCanvasCursors("default");
}
}
setIsHovered(false);
}}
/>
</DragControls>
</>
);
}

View File

@@ -0,0 +1,720 @@
import { DragControls, Line } from "@react-three/drei";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
useActiveTool,
useCreatedPaths,
useToolMode,
} from "../../../../store/builder/store";
import { CubicBezierCurve3, Plane, Quaternion, Vector3 } from "three";
import { useFrame, useThree } from "@react-three/fiber";
import { handleCanvasCursors } from "../../../../utils/mouseUtils/handleCanvasCursors";
import { getPathsByPointId, setPathPosition } from "./function/getPaths";
import { aStar } from "../structuredPath/functions/aStar";
import {
useAnimationPlaySpeed,
usePlayButtonStore,
} from "../../../../store/usePlayButtonStore";
import { useSceneContext } from "../../../scene/sceneContext";
import { usePathManager } from "./function/usePathManager";
import { useProductContext } from "../../products/productContext";
import { useSelectedEventSphere } from "../../../../store/simulation/useSimulationStore";
type PointData = {
pointId: string;
position: [number, number, number];
isCurved?: boolean;
handleA?: [number, number, number] | null;
handleB?: [number, number, number] | null;
neighbors?: string[];
};
interface PathDataInterface {
pathId: string;
isActive?: boolean;
isCurved?: boolean;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
type PointHandlerProps = {
point: PointData;
hoveredPoint: PointData | null;
setPaths: React.Dispatch<React.SetStateAction<PathData>>;
paths: PathDataInterface[];
setHoveredPoint: React.Dispatch<React.SetStateAction<PointData | null>>;
hoveredLine: PathDataInterface | null;
pointIndex: any;
points: PointData[];
selected: number[];
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
};
function dist(a: PointData, b: PointData): number {
return Math.sqrt(
(a.position[0] - b.position[0]) ** 2 +
(a.position[1] - b.position[1]) ** 2 +
(a.position[2] - b.position[2]) ** 2
);
}
type SegmentPoint = {
position: Vector3;
originalPoint?: PointData;
pathId?: string;
startId?: string;
endId?: string;
};
/** --- A* Algorithm --- */
type AStarResult = {
pointIds: string[];
distance: number;
};
function aStarShortestPath(
startId: string,
goalId: string,
points: PointData[],
paths: PathData
): AStarResult | null {
const pointById = new Map(points.map((p) => [p.pointId, p]));
const start = pointById.get(startId);
const goal = pointById.get(goalId);
if (!start || !goal) return null;
const openSet = new Set<string>([startId]);
const cameFrom: Record<string, string | null> = {};
const gScore: Record<string, number> = {};
const fScore: Record<string, number> = {};
for (const p of points) {
cameFrom[p.pointId] = null;
gScore[p.pointId] = Infinity;
fScore[p.pointId] = Infinity;
}
gScore[startId] = 0;
fScore[startId] = dist(start, goal);
const neighborsOf = (id: string): { id: string; cost: number }[] => {
const me = pointById.get(id)!;
const out: { id: string; cost: number }[] = [];
for (const edge of paths) {
const [a, b] = edge.pathPoints;
if (a.pointId === id) out.push({ id: b.pointId, cost: dist(me, b) });
else if (b.pointId === id) out.push({ id: a.pointId, cost: dist(me, a) });
}
return out;
};
while (openSet.size > 0) {
let current: string = [...openSet].reduce((a, b) =>
fScore[a] < fScore[b] ? a : b
);
if (current === goalId) {
const ids: string[] = [];
let node: string | null = current;
while (node) {
ids.unshift(node);
node = cameFrom[node];
}
return { pointIds: ids, distance: gScore[goalId] };
}
openSet.delete(current);
for (const nb of neighborsOf(current)) {
const tentativeG = gScore[current] + nb.cost;
if (tentativeG < gScore[nb.id]) {
cameFrom[nb.id] = current;
gScore[nb.id] = tentativeG;
fScore[nb.id] = tentativeG + dist(pointById.get(nb.id)!, goal);
openSet.add(nb.id);
}
}
}
return null;
}
/** --- Convert node path to edges --- */
function nodePathToEdges(
pointIds: string[],
points: PointData[],
paths: PathData
): PathData {
const byId = new Map(points.map((p) => [p.pointId, p]));
const edges: PathData = [];
for (let i = 0; i < pointIds.length - 1; i++) {
const a = pointIds[i];
const b = pointIds[i + 1];
const edge = paths.find(
(p) =>
(p.pathPoints[0].pointId === a && p.pathPoints[1].pointId === b) ||
(p.pathPoints[0].pointId === b && p.pathPoints[1].pointId === a)
);
if (edge) {
const [p1, p2] = edge.pathPoints;
edges.push({
pathId: edge.pathId,
pathPoints:
p1.pointId === a
? ([p1, p2] as [PointData, PointData])
: ([p2, p1] as [PointData, PointData]),
});
} else {
const pa = byId.get(a)!;
const pb = byId.get(b)!;
edges.push({
pathId: `synthetic-${a}-${b}`,
pathPoints: [pa, pb],
});
}
}
return edges;
}
type VehicleDetails = {
vehicleId: string;
vehiclePosition: [number, number, number];
};
type Manager = {
pathId: string;
vehicleId: string;
};
export default function PointHandler({
point,
// setPaths,
// paths,
setHoveredPoint,
hoveredLine,
hoveredPoint,
pointIndex,
points,
setSelected,
selected,
}: PointHandlerProps) {
const { isPlaying } = usePlayButtonStore();
const [multiPaths, setMultiPaths] = useState<
{ id: number; path: PathData }[]
>([]);
const { vehicleStore, productStore } = useSceneContext();
const { vehicles, getVehicleById } = vehicleStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { updateEvent, updateAction } = productStore();
const { selectedEventSphere } = useSelectedEventSphere();
const pathIdRef = useRef(1); // To ensure unique incremental IDs
const { toolMode } = useToolMode();
const { activeTool } = useActiveTool();
const { scene, raycaster } = useThree();
const [isHovered, setIsHovered] = useState(false);
const [dragOffset, setDragOffset] = useState<Vector3 | null>(null);
const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []);
const [initialPositions, setInitialPositions] = useState<{
paths?: any;
}>({});
const [shortestPaths, setShortestPaths] = useState<PathData>([]);
const POINT_SNAP_THRESHOLD = 0.5; // Distance threshold for snapping in meters
const [vehicleUuids, setVehicleUuids] = useState<any>();
const CAN_POINT_SNAP = true;
const CAN_ANGLE_SNAP = true;
const ANGLE_SNAP_DISTANCE_THRESHOLD = 0.5;
const [selectedPointIndices, setSelectedPointIndices] = useState<number[]>(
[]
);
const [shortestEdges, setShortestEdges] = useState<PathData>([]);
const { speed } = useAnimationPlaySpeed();
const { assetStore } = useSceneContext();
const { assets } = assetStore();
const vehicleMovementState = useRef<any>({});
const [activeVehicleIndex, setActiveVehicleIndex] = useState(0);
const [vehicleData, setVehicleData] = useState<VehicleDetails[]>([]);
const { paths, setPaths } = useCreatedPaths();
const [managerData, setManagerData] = useState<Manager>();
useEffect(() => {
const findVehicle = assets
.filter((val) => val.eventData?.type === "Vehicle")
?.map((val) => val.modelUuid);
const findVehicleDatas = assets
.filter((val) => val.eventData?.type === "Vehicle")
?.map((val) => val);
findVehicleDatas.forEach((val) => {
const vehicledId = val.modelUuid;
const vehiclePosition: [number, number, number] = val.position;
setVehicleData((prev) => [
...prev,
{ vehicleId: vehicledId, vehiclePosition },
]);
});
setVehicleUuids(findVehicle);
setActiveVehicleIndex(0); // Reset to first vehicle
vehicleMovementState.current = {};
findVehicle.forEach((uuid) => {
vehicleMovementState.current[uuid] = {
index: 0,
progress: 0,
hasStarted: false,
};
});
}, [assets]);
const removePathByPoint = (pointId: string): PathDataInterface[] => {
const removedPaths: PathDataInterface[] = [];
const newPaths = paths.filter((path: PathDataInterface) => {
const hasPoint = path.pathPoints.some(
(p: PointData) => p.pointId === pointId
);
if (hasPoint) {
removedPaths.push(JSON.parse(JSON.stringify(path))); // keep a copy
return false; // remove this path
}
return true; // keep this path
});
setPaths(newPaths);
return removedPaths;
};
const getConnectedPoints = (uuid: string): PointData[] => {
const connected: PointData[] = [];
for (const path of paths) {
for (const point of path.pathPoints) {
if (point.pointId === uuid) {
connected.push(
...path.pathPoints.filter((p: PointData) => p.pointId !== uuid)
);
}
}
}
return connected;
};
const snapPathAngle = useCallback(
(
newPosition: [number, number, number],
pointId: string
): {
position: [number, number, number];
isSnapped: boolean;
snapSources: Vector3[];
} => {
if (!pointId || !CAN_ANGLE_SNAP) {
return { position: newPosition, isSnapped: false, snapSources: [] };
}
const connectedPoints: PointData[] = getConnectedPoints(pointId) || [];
if (connectedPoints.length === 0) {
return {
position: newPosition,
isSnapped: false,
snapSources: [],
};
}
const newPos = new Vector3(...newPosition);
let closestX: { pos: Vector3; dist: number } | null = null;
let closestZ: { pos: Vector3; dist: number } | null = null;
for (const connectedPoint of connectedPoints) {
const cPos = new Vector3(...connectedPoint.position);
const xDist = Math.abs(newPos.x - cPos.x);
const zDist = Math.abs(newPos.z - cPos.z);
if (xDist < ANGLE_SNAP_DISTANCE_THRESHOLD) {
if (!closestX || xDist < closestX.dist) {
closestX = { pos: cPos, dist: xDist };
}
}
if (zDist < ANGLE_SNAP_DISTANCE_THRESHOLD) {
if (!closestZ || zDist < closestZ.dist) {
closestZ = { pos: cPos, dist: zDist };
}
}
}
const snappedPos = newPos.clone();
const snapSources: Vector3[] = [];
if (closestX) {
snappedPos.x = closestX.pos.x;
snapSources.push(closestX.pos.clone());
}
if (closestZ) {
snappedPos.z = closestZ.pos.z;
snapSources.push(closestZ.pos.clone());
}
const isSnapped = snapSources.length > 0;
return {
position: [snappedPos.x, snappedPos.y, snappedPos.z],
isSnapped,
snapSources,
};
},
[]
);
const getAllOtherPathPoints = useCallback((): PointData[] => {
return (
paths?.flatMap((path: PathDataInterface) =>
path.pathPoints.filter((pt: PointData) => pt.pointId !== point.pointId)
) ?? []
);
}, [paths]);
const snapPathPoint = useCallback(
(position: [number, number, number], pointId?: string) => {
if (!CAN_POINT_SNAP)
return { position: position, isSnapped: false, snappedPoint: null };
const otherPoints = getAllOtherPathPoints();
const currentVec = new Vector3(...position);
for (const point of otherPoints) {
const pointVec = new Vector3(...point.position);
const distance = currentVec.distanceTo(pointVec);
if (distance <= POINT_SNAP_THRESHOLD) {
return {
position: point.position,
isSnapped: true,
snappedPoint: point,
};
}
}
return { position: position, isSnapped: false, snappedPoint: null };
},
[getAllOtherPathPoints]
);
const handlePointClick = (e: any, point: PointData) => {
e.stopPropagation();
if (toolMode === "3D-Delete") {
removePathByPoint(point.pointId);
return;
}
};
let selectedVehiclePaths: Array<
Array<{
vehicleId: string;
pathId: string;
isActive?: boolean;
isCurved?: boolean;
pathPoints: [PointData, PointData];
}>
> = [];
function assignPathToSelectedVehicle(
selectedVehicleId: string,
currentPath: PathData
) {
const vehiclePathSegments = currentPath.map((path) => ({
vehicleId: selectedVehicleId,
...path,
}));
return selectedVehiclePaths.push(vehiclePathSegments);
}
const handleContextMenu = (e: any, point: PointData) => {
// if (e.shiftKey && e.button === 2) {
const pointIndex = points.findIndex((p) => p.pointId === point.pointId);
if (pointIndex === -1) {
return;
}
setSelected((prev) => {
if (prev.length === 0) {
return [pointIndex];
}
// if (prev.length === 1) {
// setTimeout(() => {
//
// const prevPoint = points[prev[0]];
//
// const newPoint = points[pointIndex];
//
// const result = aStarShortestPath(
// prevPoint.pointId,
// newPoint.pointId,
// points,
// paths
// );
// if (result) {
// const edges = nodePathToEdges(result.pointIds, points, paths);
//
// setShortestPaths(edges);
// setShortestEdges(edges);
// } else {
// setShortestPaths([]);
// setShortestEdges([]);
// }
// if (prevPoint.pointId === newPoint.pointId) {
// return prev;
// }
// }, 0);
// return [prev[0], pointIndex];
// }
// More than two points — reset
if (prev.length === 1) {
setTimeout(() => {
const prevPoint = points[prev[0]];
const newPoint = points[pointIndex];
console.log(
"selectedEventSphere?.userData.modelUuid: ",
selectedEventSphere?.userData.modelUuid
);
if (selectedEventSphere?.userData.modelUuid) {
const updatedVehicle = getVehicleById(
selectedEventSphere.userData.modelUuid
);
const startPoint = new Vector3(...prevPoint.position);
const endPoint = new Vector3(...newPoint.position);
if (updatedVehicle && startPoint && endPoint) {
if (updatedVehicle.type === "vehicle") {
const event = updateAction(
selectedProduct.productUuid,
updatedVehicle.point?.action.actionUuid,
{
pickUpPoint: {
position: {
x: startPoint.x,
y: 0,
z: startPoint.z,
},
rotation: {
x:
updatedVehicle.point.action.pickUpPoint?.rotation.x ??
0,
y:
updatedVehicle.point.action.pickUpPoint?.rotation.y ??
0,
z:
updatedVehicle.point.action.pickUpPoint?.rotation.z ??
0,
},
},
unLoadPoint: {
position: {
x: endPoint.x,
y: endPoint.y,
z: endPoint.z,
},
rotation: {
x:
updatedVehicle.point.action.unLoadPoint?.rotation.x ??
0,
y:
updatedVehicle.point.action.unLoadPoint?.rotation.y ??
0,
z:
updatedVehicle.point.action.unLoadPoint?.rotation.z ??
0,
},
},
}
);
if (prevPoint.pointId === newPoint.pointId) return;
const result = aStarShortestPath(
prevPoint.pointId,
newPoint.pointId,
points,
paths
);
if (result) {
const edges = nodePathToEdges(result.pointIds, points, paths);
// Create a new path object/
const newPathObj = {
id: pathIdRef.current++,
path: edges,
};
const shortPath = assignPathToSelectedVehicle(
updatedVehicle?.modelUuid,
edges
);
console.log("shortPath: ", shortPath);
setShortestPaths(edges);
setShortestEdges(edges);
// Append it to the list of paths
setMultiPaths((prevPaths) => [...prevPaths, newPathObj]);
}
}
}
}
// Reset selection to allow new pair selection
}, 0);
return [prev[0], pointIndex];
}
setShortestPaths([]);
return [pointIndex];
});
// }
};
const handleDragStart = (point: PointData) => {
if (activeTool !== "cursor") return;
const intersectionPoint = new Vector3();
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (hit) {
const currentPosition = new Vector3(...point.position);
const offset = new Vector3().subVectors(currentPosition, hit);
setDragOffset(offset);
const pathIntersection = getPathsByPointId(point.pointId, paths);
setInitialPositions({ paths: pathIntersection });
}
};
const handleDrag = (point: PointData) => {
if (isHovered && dragOffset) {
const intersectionPoint = new Vector3();
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (hit) {
// handleCanvasCursors("grabbing");
const positionWithOffset = new Vector3().addVectors(hit, dragOffset);
const newPosition: [number, number, number] = [
positionWithOffset.x,
positionWithOffset.y,
positionWithOffset.z,
];
// ✅ Pass newPosition and pointId
const pathSnapped = snapPathAngle(newPosition, point.pointId);
const finalSnapped = snapPathPoint(pathSnapped.position);
setPathPosition(point.pointId, finalSnapped.position, setPaths, paths);
}
}
};
const handleDragEnd = (point: PointData) => {
const pathIntersection = getPathsByPointId(point.pointId, paths);
if (pathIntersection && pathIntersection.length > 0) {
pathIntersection.forEach((update) => {});
}
};
const pathSegments = useMemo(() => {
if (!shortestPaths || shortestPaths.length === 0) return [];
const segments: SegmentPoint[] = [];
shortestPaths.forEach((path) => {
const [start, end] = path.pathPoints;
const startPos = new Vector3(...start.position);
const endPos = new Vector3(...end.position);
segments.push(
{ position: startPos, originalPoint: start, startId: start.pointId },
{ position: endPos, originalPoint: end, endId: end.pointId }
);
});
return segments.filter(
(v, i, arr) => i === 0 || !v.position.equals(arr[i - 1].position)
);
}, [shortestPaths]);
function getPathIdByPoints(
startId: string | undefined,
endId: string | undefined,
shortestPaths: any[]
) {
for (const path of shortestPaths) {
for (let i = 0; i < path.pathPoints.length - 1; i++) {
const s = path.pathPoints[i];
const e = path.pathPoints[i + 1];
if (
(s.pointId === startId && e.pointId === endId) ||
(s.pointId === endId && e.pointId === startId) // handle both directions
) {
return path.pathId;
}
}
}
return null; // not found
}
return (
<>
<DragControls
axisLock="y"
autoTransform={false}
onDragStart={() => handleDragStart(point)}
onDrag={() => handleDrag(point)}
onDragEnd={() => handleDragEnd(point)}
>
<mesh
key={point.pointId}
position={point.position}
name="Path-Point"
userData={point}
onClick={(e) => {
handlePointClick(e, point);
}}
// onContextMenu={(e) => handleContextMenu(e, point)}
onPointerOver={(e) => {
if (!hoveredPoint && e.buttons === 0 && !e.ctrlKey) {
setHoveredPoint(point);
setIsHovered(true);
// handleCanvasCursors("default");
}
}}
onPointerOut={() => {
if (hoveredPoint) {
setHoveredPoint(null);
if (!hoveredLine) {
// handleCanvasCursors("default");
}
}
setIsHovered(false);
}}
>
<sphereGeometry args={[0.3, 16, 16]} />
<meshBasicMaterial color="pink" />
</mesh>
</DragControls>
{shortestEdges.map((edge) => (
<Line
key={`sp-${edge.pathId}`}
points={edge.pathPoints.map((p) => p.position)}
color="yellow"
lineWidth={3}
/>
))}
</>
);
}

View File

@@ -0,0 +1,360 @@
import React, { useRef, useState, useEffect } from "react";
import { useThree, useFrame } from "@react-three/fiber";
import * as THREE from "three";
import { Line } from "@react-three/drei";
type PointData = {
pointId: string;
position: [number, number, number];
isCurved: boolean;
handleA: [number, number, number] | null;
handleB: [number, number, number] | null;
};
interface PointProps {
point: any;
pointIndex: number;
groupIndex: number;
selected: number[];
mainShapeOnly?: PointData[];
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
pointsGroups: any[][];
setPointsGroups: React.Dispatch<React.SetStateAction<any[][]>>;
shortestPath: number[]; // <- add this
setShortestPath: React.Dispatch<React.SetStateAction<number[]>>; // <- add this
setShortestDistance?: React.Dispatch<React.SetStateAction<number>>; // optional
}
export default function PointHandle({
point,
pointIndex,
groupIndex,
selected,
setSelected,
pointsGroups,
setPointsGroups,
setShortestDistance,
shortestPath,
setShortestPath,
}: PointProps) {
const meshRef = useRef<THREE.Mesh>(null);
const handleARef = useRef<THREE.Mesh>(null);
const handleBRef = useRef<THREE.Mesh>(null);
const lineRef = useRef<THREE.Line>(null!);
// const pathLineRef = useRef<THREE.Line>(null!);
const { camera, gl, controls } = useThree();
const [dragging, setDragging] = useState<
null | "main" | "handleA" | "handleB"
>(null);
const dragOffset = useRef(new THREE.Vector3());
// const [shortestPath, setShortestPath] = useState<number[]>([]);
/** Shift-click or ctrl-click handling */
const onPointClick = (e: any) => {
e.stopPropagation();
if (e.ctrlKey) {
// Toggle handles
setPointsGroups((prev) => {
const newGroups = [...prev];
const group = [...newGroups[groupIndex]];
const idx = group.findIndex((p) => p.pointId === point.pointId);
const updated = { ...group[idx] };
if (!updated.handleA && !updated.handleB) {
updated.handleA = [
updated.position[0] + 1,
updated.position[1],
updated.position[2],
];
updated.handleB = [
updated.position[0] - 1,
updated.position[1],
updated.position[2],
];
updated.isCurved = true;
} else {
updated.handleA = null;
updated.handleB = null;
updated.isCurved = false;
}
group[idx] = updated;
newGroups[groupIndex] = group;
return newGroups;
});
} else if (e.shiftKey) {
// Shift-click for multi-select
setSelected((prev) => {
if (prev.includes(pointIndex)) return prev; // keep selection
const newSelection = [...prev, pointIndex];
return newSelection.slice(-2); // keep only 2 points
});
} else {
// Single selection
setSelected([pointIndex]);
}
};
/** Dragging logic */
const startDrag = (target: "main" | "handleA" | "handleB", e: any) => {
e.stopPropagation();
setDragging(target);
const targetRef =
target === "main"
? meshRef.current
: target === "handleA"
? handleARef.current
: handleBRef.current;
if (targetRef) dragOffset.current.copy(targetRef.position).sub(e.point);
if (controls) (controls as any).enabled = false;
gl.domElement.style.cursor = "grabbing";
};
const stopDrag = () => {
setDragging(null);
gl.domElement.style.cursor = "auto";
if (controls) (controls as any).enabled = true;
};
useFrame(({ raycaster, mouse }) => {
if (!dragging) return;
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
raycaster.setFromCamera(mouse, camera);
const intersection = new THREE.Vector3();
if (raycaster.ray.intersectPlane(plane, intersection)) {
const newPos = intersection.add(dragOffset.current);
setPointsGroups((prev) => {
const newGroups = [...prev];
const group = [...newGroups[groupIndex]];
const idx = group.findIndex((p) => p.pointId === point.pointId);
const updated = { ...group[idx] };
if (dragging === "main") {
const delta = new THREE.Vector3()
.fromArray(newPos.toArray())
.sub(new THREE.Vector3().fromArray(updated.position));
updated.position = newPos.toArray() as [number, number, number];
if (updated.handleA) {
updated.handleA = new THREE.Vector3()
.fromArray(updated.handleA)
.add(delta)
.toArray() as [number, number, number];
}
if (updated.handleB) {
updated.handleB = new THREE.Vector3()
.fromArray(updated.handleB)
.add(delta)
.toArray() as [number, number, number];
}
} else {
updated[dragging] = newPos.toArray() as [number, number, number];
if (updated.isCurved) {
const mainPos = new THREE.Vector3().fromArray(updated.position);
const thisHandle = new THREE.Vector3().fromArray(
updated[dragging]!
);
const mirrorHandle = mainPos
.clone()
.sub(thisHandle.clone().sub(mainPos));
if (dragging === "handleA")
updated.handleB = mirrorHandle.toArray() as [
number,
number,
number
];
if (dragging === "handleB")
updated.handleA = mirrorHandle.toArray() as [
number,
number,
number
];
}
}
group[idx] = updated;
newGroups[groupIndex] = group;
return newGroups;
});
}
});
/** Update handle lines */
useFrame(() => {
if (lineRef.current && point.handleA && point.handleB) {
const positions = lineRef.current.geometry.attributes.position
.array as Float32Array;
positions[0] = point.handleA[0];
positions[1] = point.handleA[1];
positions[2] = point.handleA[2];
positions[3] = point.handleB[0];
positions[4] = point.handleB[1];
positions[5] = point.handleB[2];
lineRef.current.geometry.attributes.position.needsUpdate = true;
}
});
useEffect(() => {
if (selected.length === 2) {
const groupPoints = pointsGroups[groupIndex];
if (!groupPoints) return;
const pathPoints = selected
.map((i) => groupPoints[i])
.filter((p) => p !== undefined)
.map((p) => p.position);
setShortestPath(pathPoints);
// compute distance
let totalDistance = 0;
for (let i = 0; i < pathPoints.length - 1; i++) {
const p1 = new THREE.Vector3().fromArray(pathPoints[i]);
const p2 = new THREE.Vector3().fromArray(pathPoints[i + 1]);
totalDistance += p1.distanceTo(p2);
}
setShortestDistance?.(totalDistance);
} else {
setShortestPath([]);
setShortestDistance?.(0);
}
}, [selected, pointsGroups]);
return (
<>
{/* Main point */}
<mesh
ref={meshRef}
position={point.position}
onClick={onPointClick}
onPointerDown={(e) => startDrag("main", e)}
onPointerUp={stopDrag}
>
<sphereGeometry args={[0.3, 16, 16]} />
<meshStandardMaterial
color={selected.includes(pointIndex) ? "red" : "pink"}
/>
</mesh>
{/* Handles + line */}
{point.isCurved && point.handleA && point.handleB && (
<>
<Line
points={[point.handleA, point.handleB]}
color="gray"
lineWidth={1}
/>
<mesh
ref={handleARef}
position={point.handleA}
onPointerDown={(e) => startDrag("handleA", e)}
onPointerUp={stopDrag}
>
<sphereGeometry args={[0.15, 8, 8]} />
<meshStandardMaterial color="orange" />
</mesh>
<mesh
ref={handleBRef}
position={point.handleB}
onPointerDown={(e) => startDrag("handleB", e)}
onPointerUp={stopDrag}
>
<sphereGeometry args={[0.15, 8, 8]} />
<meshStandardMaterial color="green" />
</mesh>
</>
)}
{/* Highlight shortest path */}
{shortestPath.length > 1 && (
<Line
points={shortestPath} // <- just use the positions array
color="blue"
lineWidth={2}
/>
)}
</>
);
}
/** Build adjacency list for shortest path */
// const buildGraph = (points: any[]) => {
// const graph: Record<number, { neighbor: number; distance: number }[]> = {};
// points.forEach((p, idx) => {
// graph[idx] = [];
// points.forEach((q, j) => {
// if (idx !== j) {
// const d = new THREE.Vector3()
// .fromArray(p.position)
// .distanceTo(new THREE.Vector3().fromArray(q.position));
// graph[idx].push({ neighbor: j, distance: d });
// }
// });
// });
// return graph;
// };
// /** Dijkstra shortest path */
// const findShortestPath = (graph: any, startIdx: number, endIdx: number) => {
// const distances: number[] = Array(Object.keys(graph).length).fill(Infinity);
// const previous: (number | null)[] = Array(distances.length).fill(null);
// distances[startIdx] = 0;
// const queue = new Set(Object.keys(graph).map(Number));
// while (queue.size) {
// let current = [...queue].reduce((a, b) =>
// distances[a] < distances[b] ? a : b
// );
// if (current === endIdx) break;
// queue.delete(current);
// for (const { neighbor, distance } of graph[current]) {
// const alt = distances[current] + distance;
// if (alt < distances[neighbor]) {
// distances[neighbor] = alt;
// previous[neighbor] = current;
// }
// }
// }
// const path: number[] = [];
// let u: number | null = endIdx;
// while (u !== null) {
// path.unshift(u);
// u = previous[u];
// }
//
// return path;
// };
// /** Calculate shortest path when 2 points are selected */
// useEffect(() => {
// if (selected.length === 2) {
// const groupPoints = pointsGroups[groupIndex];
// const graph = buildGraph(groupPoints);
// const path = findShortestPath(graph, selected[0], selected[1]);
// setShortestPath(path);
// // Calculate distance
// if (setShortestDistance) {
// let totalDistance = 0;
// for (let i = 0; i < path.length - 1; i++) {
// const p1 = new THREE.Vector3().fromArray(
// groupPoints[path[i]].position
// );
// const p2 = new THREE.Vector3().fromArray(
// groupPoints[path[i + 1]].position
// );
// totalDistance += p1.distanceTo(p2);
// }
// setShortestDistance?.(totalDistance);
// }
// } else {
// setShortestPath([]);
// if (setShortestDistance) setShortestDistance(0);
// }
// }, [selected, pointsGroups]);

View File

@@ -0,0 +1,22 @@
export function findShortestPath(
startIndex: number,
endIndex: number,
adjacency: number[][]
) {
const queue = [[startIndex]];
const visited = new Set<number>();
while (queue.length > 0) {
const path = queue.shift()!;
const node = path[path.length - 1];
if (node === endIndex) return path;
if (!visited.has(node)) {
visited.add(node);
for (const neighbor of adjacency[node]) {
queue.push([...path, neighbor]);
}
}
}
return [];
}

View File

@@ -0,0 +1,185 @@
// import { Line } from "@react-three/drei";
// import { useThree } from "@react-three/fiber";
// import { useEffect, useMemo, useRef } from "react";
// import { CubicBezierCurve3, LineCurve3, Plane, Vector3 } from "three";
// export default function LineSegment({
// index,
// createdPoints,
// updatePoints,
// insertPoint,
// }: {
// index: number;
// createdPoints: any[]; // Array of points with position, isCurved, handleA, handleB
// updatePoints: (i0: number, p0: Vector3, i1: number, p1: Vector3) => void;
// insertPoint?: (index: number, point: Vector3) => void;
// }) {
// const { gl, raycaster, camera, controls } = useThree();
// const plane = new Plane(new Vector3(0, 1, 0), 0);
// const dragStart = useRef<Vector3 | null>(null);
// // ======== Curve or Line Points ========
// const curvePoints = useMemo(() => {
// if (!createdPoints || index + 1 >= createdPoints.length) return [];
// const current = createdPoints[index];
// const next = createdPoints[index + 1];
// const starts = new Vector3(...current.position);
// const ends = new Vector3(...next.position);
// const useCurve =
// (current.isCurved && current.handleB) || (next.isCurved && next.handleA);
// const hB = current.handleB ? new Vector3(...current.handleB) : starts;
// const hA = next.handleA ? new Vector3(...next.handleA) : ends;
// const curve = useCurve
// ? new CubicBezierCurve3(starts, hB, hA, ends)
// : new LineCurve3(starts, ends);
// return curve.getPoints(useCurve ? 100 : 2);
// }, [createdPoints, index]);
// // ======== Events ========
// const onPointerUp = () => {
// dragStart.current = null;
// gl.domElement.style.cursor = "default";
// if (controls) (controls as any).enabled = true;
// };
// const onClickLine = () => {
// const intersection = new Vector3();
// if (raycaster.ray.intersectPlane(plane, intersection)) {
// const start = new Vector3(...createdPoints[index].position);
// const end = new Vector3(...createdPoints[index + 1].position);
// const segLen = start.distanceTo(end);
// const distToStart = start.distanceTo(intersection);
// const distToEnd = end.distanceTo(intersection);
// if (
// distToStart > 0.01 &&
// distToEnd > 0.01 &&
// distToStart + distToEnd <= segLen + 0.01
// ) {
// insertPoint?.(index + 1, intersection);
// }
// }
// };
// useEffect(() => {
// gl.domElement.addEventListener("pointerup", onPointerUp);
// return () => {
// gl.domElement.removeEventListener("pointerup", onPointerUp);
// };
// }, []);
// // ======== Render ========
// return (
// <Line
// points={curvePoints}
// color="purple"
// lineWidth={2}
// onPointerDown={onClickLine}
// onPointerUp={onPointerUp}
// />
// );
// }
import { Line } from "@react-three/drei";
import { useThree } from "@react-three/fiber";
import { useEffect, useMemo, useRef } from "react";
import { CubicBezierCurve3, LineCurve3, Plane, Vector3 } from "three";
export default function LineSegment({
index,
createdPoints,
updatePoints,
insertPoint,
}: {
index: number;
createdPoints: any[];
updatePoints: (i0: number, p0: Vector3, i1: number, p1: Vector3) => void;
insertPoint?: (index: number, point: Vector3) => void;
}) {
const { gl, raycaster, camera, controls } = useThree();
const plane = new Plane(new Vector3(0, 1, 0), 0);
const dragStart = useRef<Vector3 | null>(null);
const curvePoints = useMemo(() => {
if (!createdPoints || index + 1 >= createdPoints.length) return [];
const current = createdPoints[index];
const next = createdPoints[index + 1];
// Force y = 0
const starts = new Vector3(current.position[0], 0, current.position[2]);
const ends = new Vector3(next.position[0], 0, next.position[2]);
const useCurve =
(current.isCurved && current.handleB) || (next.isCurved && next.handleA);
const hB = current.handleB
? new Vector3(current.handleB[0], 0, current.handleB[2])
: starts;
const hA = next.handleA
? new Vector3(next.handleA[0], 0, next.handleA[2])
: ends;
const curve = useCurve
? new CubicBezierCurve3(starts, hB, hA, ends)
: new LineCurve3(starts, ends);
return curve.getPoints(useCurve ? 100 : 2);
}, [createdPoints, index]);
const onPointerUp = () => {
dragStart.current = null;
gl.domElement.style.cursor = "default";
if (controls) (controls as any).enabled = true;
};
const onClickLine = () => {
const intersection = new Vector3();
if (raycaster.ray.intersectPlane(plane, intersection)) {
const start = new Vector3(
createdPoints[index].position[0],
0,
createdPoints[index].position[2]
);
const end = new Vector3(
createdPoints[index + 1].position[0],
0,
createdPoints[index + 1].position[2]
);
const segLen = start.distanceTo(end);
const distToStart = start.distanceTo(intersection);
const distToEnd = end.distanceTo(intersection);
if (
distToStart > 0.01 &&
distToEnd > 0.01 &&
distToStart + distToEnd <= segLen + 0.01
) {
insertPoint?.(index + 1, intersection);
}
}
};
useEffect(() => {
gl.domElement.addEventListener("pointerup", onPointerUp);
return () => {
gl.domElement.removeEventListener("pointerup", onPointerUp);
};
}, []);
return (
<Line
points={curvePoints}
color="purple"
lineWidth={2}
onPointerDown={onClickLine}
onPointerUp={onPointerUp}
/>
);
}

View File

@@ -0,0 +1,298 @@
import { Line, Plane } from "@react-three/drei";
import { ThreeEvent, useFrame, useThree } from "@react-three/fiber";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Matrix4, Mesh, Quaternion, Vector3 } from "three";
import {
useAnimationPlaySpeed,
usePlayButtonStore,
} from "../../../../store/usePlayButtonStore";
import { useSceneContext } from "../../../scene/sceneContext";
import { findShortestPath } from "./functions/findShortestPath";
import * as THREE from "three";
import PointHandle from "./PointHandle";
import LineSegment from "./lineSegment";
type PointData = {
pointId: string;
position: [number, number, number];
isCurved: boolean;
handleA: [number, number, number] | null;
handleB: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
export default function PreDefinedPath() {
const { gl, raycaster } = useThree();
const plane = useRef(new THREE.Plane(new THREE.Vector3(0, 1, 0), 0));
const [mainShapeOnly, setMainShapeOnly] = useState<PointData[][]>([]);
const [pointsGroups, setPointsGroups] = useState<PointData[][]>([[]]);
const [definedPath, setDefinedPath] = useState<PointData[][] | PointData[]>(
[]
);
const [selected, setSelected] = useState<number[]>([]);
const downPosition = useRef<{ x: number; y: number } | null>(null);
const hasClicked = useRef(false);
const handleMouseDown = useCallback((e: any) => {
hasClicked.current = false;
downPosition.current = { x: e.clientX, y: e.clientY };
}, []);
const SNAP_DISTANCE = 0.5;
const handleClick = useCallback(
(e: any) => {
console.log("e.ctrlKey: ", e.ctrlKey);
if (e.ctrlKey) return;
if (e.button === 2) {
setPointsGroups((prev) => [...prev, []]);
setSelected([]);
return;
}
if (e.button !== 0) return;
if (hasClicked.current) return;
hasClicked.current = true;
if (
!downPosition.current ||
Math.abs(downPosition.current.x - e.clientX) > 2 ||
Math.abs(downPosition.current.y - e.clientY) > 2
)
return;
const intersection = new THREE.Vector3();
if (raycaster.ray.intersectPlane(plane.current, intersection)) {
const pointArray = intersection.toArray() as [number, number, number];
setPointsGroups((prev) => {
const newGroups = [...prev];
const currentGroup = [...newGroups[newGroups.length - 1]];
// 1⃣ Find nearest existing point
let nearestPos: [number, number, number] | null = null;
newGroups.forEach((group) => {
group.forEach((p) => {
const dist = Math.sqrt(
Math.pow(p.position[0] - pointArray[0], 2) +
Math.pow(p.position[1] - pointArray[1], 2) +
Math.pow(p.position[2] - pointArray[2], 2)
);
if (dist <= SNAP_DISTANCE && !nearestPos) {
nearestPos = p.position; // take only the position
}
});
});
if (nearestPos) {
// 2⃣ Reuse the position, but create NEW pointId
const snapPoint: PointData = {
pointId: crypto.randomUUID(),
position: nearestPos,
isCurved: false,
handleA: null,
handleB: null,
};
currentGroup.push(snapPoint);
newGroups[newGroups.length - 1] = currentGroup;
return newGroups;
}
// 3⃣ Otherwise, create brand new point
const newPoint: PointData = {
pointId: crypto.randomUUID(),
position: pointArray,
isCurved: false,
handleA: null,
handleB: null,
};
currentGroup.push(newPoint);
newGroups[newGroups.length - 1] = currentGroup;
return newGroups;
});
}
},
[raycaster]
);
function findConnectedComponents(groups: PointData[][]) {
const visited = new Set<string>();
const components: PointData[][] = [];
const arePointsEqual = (p1: PointData, p2: PointData) =>
Math.abs(p1.position[0] - p2.position[0]) < 0.001 &&
Math.abs(p1.position[1] - p2.position[1]) < 0.001 &&
Math.abs(p1.position[2] - p2.position[2]) < 0.001;
const dfs = (point: PointData, component: PointData[][]) => {
if (visited.has(point.pointId)) return;
visited.add(point.pointId);
for (const group of groups) {
if (group.some((gp) => arePointsEqual(gp, point))) {
if (!component.includes(group)) {
component.push(group);
for (const gp of group) dfs(gp, component);
}
}
}
};
for (const group of groups) {
for (const point of group) {
if (!visited.has(point.pointId)) {
const newComponent: PointData[][] = [];
dfs(point, newComponent);
if (newComponent.length > 0) components.push(newComponent.flat());
}
}
}
return components;
}
useEffect(() => {
const newDefinedPath = pointsGroups.filter((g) => g.length > 0);
setDefinedPath(newDefinedPath);
const connected = findConnectedComponents(newDefinedPath);
if (connected.length > 0) {
let mainShape = [...connected[0]];
const isolatedPoints = connected
.slice(1)
.filter((arr) => arr.length === 1);
const updatedMainShapeOnly = [mainShape, ...isolatedPoints];
setMainShapeOnly(updatedMainShapeOnly);
} else {
setMainShapeOnly([]);
}
}, [pointsGroups]);
useEffect(() => {
setDefinedPath(() => {
if (pointsGroups.length === 1) {
return [...pointsGroups[0]];
} else {
return pointsGroups.filter((group) => group.length > 0);
}
});
}, [pointsGroups]);
const [shortestPath, setShortestPath] = useState<number[]>([]);
const [shortestDistance, setShortestDistance] = useState<number>(0);
useEffect(() => {
const domElement = gl.domElement;
domElement.addEventListener("contextmenu", (e) => e.preventDefault());
domElement.addEventListener("mousedown", handleMouseDown);
domElement.addEventListener("mouseup", handleClick);
return () => {
domElement.removeEventListener("mousedown", handleMouseDown);
domElement.removeEventListener("mouseup", handleClick);
};
}, [handleClick, handleMouseDown]);
return (
<>
{pointsGroups.map((group, gIdx) => (
<React.Fragment key={gIdx}>
{group.map((point, idx) => (
<PointHandle
key={point.pointId}
point={point}
groupIndex={gIdx}
pointIndex={idx}
// mainShapeOnly={mainShapeOnly}
setPointsGroups={setPointsGroups}
pointsGroups={pointsGroups} // <-- pass the full groups
selected={selected}
setSelected={setSelected} // <-- pass setter for multi-selection
shortestPath={shortestPath}
setShortestPath={setShortestPath}
setShortestDistance={setShortestDistance}
/>
))}
{group.map((point, i) => {
if (i < group.length - 1) {
return (
<LineSegment
key={i}
index={i}
createdPoints={group} // pass the whole group here
updatePoints={(i0, p0, i1, p1) => {
setPointsGroups((prev) => {
const newGroups = [...prev];
const newGroup = [...newGroups[gIdx]];
newGroup[i0] = {
...newGroup[i0],
position: p0.toArray() as [number, number, number],
};
newGroup[i1] = {
...newGroup[i1],
position: p1.toArray() as [number, number, number],
};
newGroups[gIdx] = newGroup;
return newGroups;
});
}}
insertPoint={(index, pointVec) => {
setPointsGroups((prev) => {
const newGroups = [...prev];
const groupToSplit = newGroups[gIdx];
// Create the new point
const newPoint = {
pointId: crypto.randomUUID(),
position: pointVec.toArray() as [
number,
number,
number
],
isCurved: false,
handleA: null,
handleB: null,
};
// First half: everything from start to clicked segment
const firstHalf = [
...groupToSplit.slice(0, index),
newPoint,
];
// Second half: new point + everything after clicked segment
const secondHalf = [
newPoint,
...groupToSplit.slice(index),
];
// Replace the original group with the first half
newGroups[gIdx] = firstHalf;
// Insert the second half as a new group right after
newGroups.splice(gIdx + 1, 0, secondHalf);
return newGroups;
});
}}
/>
);
}
return null;
})}
</React.Fragment>
))}
</>
);
}

View File

@@ -0,0 +1,213 @@
type PointData = {
pointId: string;
position: [number, number, number];
isCurved?: boolean;
handleA?: [number, number, number] | null;
handleB?: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
isActive?: boolean;
isCurved?: boolean;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
function dist(a: PointData, b: PointData): number {
return Math.sqrt(
(a.position[0] - b.position[0]) ** 2 +
(a.position[1] - b.position[1]) ** 2 +
(a.position[2] - b.position[2]) ** 2
);
}
/** --- A* Algorithm --- */
type AStarResult = {
pointIds: string[];
distance: number;
};
// function aStarShortestPath(
// startId: string,
// goalId: string,
// points: PointData[],
// paths: PathData
// ): AStarResult | null {
// const pointById = new Map(points.map((p) => [p.pointId, p]));
// const start = pointById.get(startId);
// const goal = pointById.get(goalId);
// if (!start || !goal) return null;
// const openSet = new Set<string>([startId]);
// const cameFrom: Record<string, string | null> = {};
// const gScore: Record<string, number> = {};
// const fScore: Record<string, number> = {};
// for (const p of points) {
// cameFrom[p.pointId] = null;
// gScore[p.pointId] = Infinity;
// fScore[p.pointId] = Infinity;
// }
// gScore[startId] = 0;
// fScore[startId] = dist(start, goal);
// const neighborsOf = (id: string): { id: string; cost: number }[] => {
// const me = pointById.get(id)!;
// const out: { id: string; cost: number }[] = [];
// for (const edge of paths) {
// const [a, b] = edge.pathPoints;
// if (a.pointId === id) out.push({ id: b.pointId, cost: dist(me, b) });
// else if (b.pointId === id) out.push({ id: a.pointId, cost: dist(me, a) });
// }
// return out;
// };
// while (openSet.size > 0) {
// let current: string = [...openSet].reduce((a, b) =>
// fScore[a] < fScore[b] ? a : b
// );
// if (current === goalId) {
// const ids: string[] = [];
// let node: string | null = current;
// while (node) {
// ids.unshift(node);
// node = cameFrom[node];
// }
// return { pointIds: ids, distance: gScore[goalId] };
// }
// openSet.delete(current);
// for (const nb of neighborsOf(current)) {
// const tentativeG = gScore[current] + nb.cost;
// if (tentativeG < gScore[nb.id]) {
// cameFrom[nb.id] = current;
// gScore[nb.id] = tentativeG;
// fScore[nb.id] = tentativeG + dist(pointById.get(nb.id)!, goal);
// openSet.add(nb.id);
// }
// }
// }
// return null;
// }
/** --- Convert node path to edges --- */
function nodePathToEdges(
pointIds: string[],
points: PointData[],
paths: PathData
): PathData {
const byId = new Map(points.map((p) => [p.pointId, p]));
const edges: PathData = [];
for (let i = 0; i < pointIds.length - 1; i++) {
const a = pointIds[i];
const b = pointIds[i + 1];
const edge = paths.find(
(p) =>
(p.pathPoints[0].pointId === a && p.pathPoints[1].pointId === b) ||
(p.pathPoints[0].pointId === b && p.pathPoints[1].pointId === a)
);
if (edge) {
const [p1, p2] = edge.pathPoints;
edges.push({
pathId: edge.pathId,
pathPoints:
p1.pointId === a
? ([p1, p2] as [PointData, PointData])
: ([p2, p1] as [PointData, PointData]),
});
} else {
const pa = byId.get(a)!;
const pb = byId.get(b)!;
edges.push({
pathId: `synthetic-${a}-${b}`,
pathPoints: [pa, pb],
});
}
}
return edges;
}
export function aStar(
start: string,
end: string,
points: PointData[],
paths: PathData
) {
// Map points by id for quick access
const pointMap = new Map(points.map((p) => [p.pointId, p]));
// Build adjacency list from paths
const graph = new Map<string, string[]>();
paths.forEach((path) => {
const pathPoints = path.pathPoints;
for (let i = 0; i < pathPoints.length - 1; i++) {
const a = pathPoints[i].pointId;
const b = pathPoints[i + 1].pointId;
if (!graph.has(a)) graph.set(a, []);
if (!graph.has(b)) graph.set(b, []);
graph.get(a)!.push(b);
graph.get(b)!.push(a);
}
});
// Manhattan distance heuristic (you can use Euclidean instead)
const heuristic = (a: string, b: string) => {
const pa = pointMap.get(a)!.position;
const pb = pointMap.get(b)!.position;
return (
Math.abs(pa[0] - pb[0]) +
Math.abs(pa[1] - pb[1]) +
Math.abs(pa[2] - pb[2])
);
};
const openSet = new Set([start]);
const cameFrom = new Map<string, string>();
const gScore = new Map(points.map((p) => [p.pointId, Infinity]));
const fScore = new Map(points.map((p) => [p.pointId, Infinity]));
gScore.set(start, 0);
fScore.set(start, heuristic(start, end));
while (openSet.size > 0) {
// get node in openSet with lowest fScore
let current = [...openSet].reduce((a, b) =>
fScore.get(a)! < fScore.get(b)! ? a : b
);
if (current === end) {
// reconstruct path
const path: string[] = [];
while (cameFrom.has(current)) {
path.unshift(current);
current = cameFrom.get(current)!;
}
path.unshift(start);
return path;
}
openSet.delete(current);
for (const neighbor of graph.get(current) || []) {
const tentativeG = gScore.get(current)! + heuristic(current, neighbor);
if (tentativeG < gScore.get(neighbor)!) {
cameFrom.set(neighbor, current);
gScore.set(neighbor, tentativeG);
fScore.set(neighbor, tentativeG + heuristic(neighbor, end));
if (!openSet.has(neighbor)) openSet.add(neighbor);
}
}
}
return null; // no path
}

View File

@@ -0,0 +1,95 @@
type PointData = {
pointId: string;
position: [number, number, number];
isCurved: boolean;
handleA: [number, number, number] | null;
handleB: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
function distance(a: PointData, b: PointData): number {
const dx = a.position[0] - b.position[0];
const dy = a.position[1] - b.position[1];
const dz = a.position[2] - b.position[2];
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
type AStarResult = {
path: PointData[]; // ordered list of points along the path
distance: number; // total distance
};
export function aStarShortestPath(
startId: string,
goalId: string,
points: PointData[],
paths: PathData
): AStarResult | null {
const openSet = new Set<string>([startId]);
const cameFrom: Record<string, string | null> = {};
const gScore: Record<string, number> = {};
const fScore: Record<string, number> = {};
points.forEach((p) => {
gScore[p.pointId] = Infinity;
fScore[p.pointId] = Infinity;
cameFrom[p.pointId] = null;
});
gScore[startId] = 0;
fScore[startId] = 0;
while (openSet.size > 0) {
// Pick node with lowest fScore
let current = [...openSet].reduce((a, b) =>
fScore[a] < fScore[b] ? a : b
);
if (current === goalId) {
// ✅ Reconstruct path
const path: PointData[] = [];
let node: string | null = current;
while (node) {
const pt = points.find((p) => p.pointId === node);
if (pt) path.unshift(pt);
node = cameFrom[node];
}
return {
path,
distance: gScore[goalId],
};
}
openSet.delete(current);
// Find neighbors from paths
const neighbors = paths.filter((p) =>
p.pathPoints.some((pt) => pt.pointId === current)
);
for (let n of neighbors) {
const [p1, p2] = n.pathPoints;
const neighbor = p1.pointId === current ? p2 : p1;
const tentativeG =
gScore[current] +
distance(points.find((pt) => pt.pointId === current)!, neighbor);
if (tentativeG < gScore[neighbor.pointId]) {
cameFrom[neighbor.pointId] = current;
gScore[neighbor.pointId] = tentativeG;
fScore[neighbor.pointId] = tentativeG; // no heuristic for now
openSet.add(neighbor.pointId);
}
}
}
return null; // no path found
}

View File

@@ -0,0 +1,7 @@
export function handleContextMenu(
evt: MouseEvent,
setCurrentTempPath: (val: any[]) => void
) {
evt.preventDefault();
setCurrentTempPath([]);
}

View File

@@ -0,0 +1,130 @@
import * as THREE from "three";
type PointData = {
pointId: string;
position: [number, number, number];
isCurved: boolean;
handleA: [number, number, number] | null;
handleB: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
pathPoints: [PointData, PointData]; // always two points
}
type PathData = PathDataInterface[];
export const POLYGON_CLOSE_THRESHOLD = 0.3;
export const SNAP_POINT_THRESHOLD = 0.2;
export const SNAP_LINE_THRESHOLD = 0.2;
export function handleMouseClick({
evt,
isDragging,
raycaster,
plane,
pointer,
currentTempPath,
setCurrentTempPath,
pathPointsList,
allPaths,
setAllPaths,
addPointToCurrentTemp,
}: {
evt: MouseEvent;
isDragging: { current: boolean };
raycaster: THREE.Raycaster;
plane: THREE.Plane;
pointer: { x: number; y: number };
currentTempPath: any[];
setCurrentTempPath: (val: any[]) => void;
pathPointsList: any[];
allPaths: any[];
setAllPaths: React.Dispatch<React.SetStateAction<PathData>>;
addPointToCurrentTemp: (point: any) => void;
}) {
if (isDragging.current) return;
if (evt.ctrlKey || evt.shiftKey) return;
const intersectPoint = new THREE.Vector3();
const pos = raycaster.ray.intersectPlane(plane, intersectPoint);
if (!pos) return;
let clickedPoint = new THREE.Vector3(pos.x, pos.y, pos.z);
let snapPoint: any = null;
for (let p of pathPointsList) {
const pVec = new THREE.Vector3(...p.position);
if (pVec.distanceTo(clickedPoint) < SNAP_POINT_THRESHOLD) {
snapPoint = p;
clickedPoint = pVec;
break;
}
}
let newPoint = snapPoint ?? {
pointId: THREE.MathUtils.generateUUID(),
position: [clickedPoint.x, 0, clickedPoint.z],
isCurved: false,
handleA: null,
handleB: null,
};
if (currentTempPath.length > 2) {
const firstVec = new THREE.Vector3(...currentTempPath[0].position);
if (firstVec.distanceTo(clickedPoint) < POLYGON_CLOSE_THRESHOLD) {
const closingPoint = { ...currentTempPath[0] };
console.log("closingPoint: ", closingPoint);
addPointToCurrentTemp(closingPoint);
setCurrentTempPath([]);
return;
}
}
const getNearestPointOnLine = (
a: THREE.Vector3,
b: THREE.Vector3,
p: THREE.Vector3
) => {
const ab = new THREE.Vector3().subVectors(b, a);
const t = Math.max(
0,
Math.min(1, p.clone().sub(a).dot(ab) / ab.lengthSq())
);
return a.clone().add(ab.multiplyScalar(t));
};
for (let path of allPaths) {
const a = new THREE.Vector3(...path.pathPoints[0].position);
const b = new THREE.Vector3(...path.pathPoints[1].position);
const closest = getNearestPointOnLine(a, b, clickedPoint);
if (closest.distanceTo(clickedPoint) < SNAP_LINE_THRESHOLD) {
const splitPoint = {
pointId: THREE.MathUtils.generateUUID(),
position: closest.toArray() as [number, number, number],
isCurved: false,
handleA: null,
handleB: null,
};
setAllPaths((prev: any) =>
prev
.filter((pa: any) => pa.pathId !== path.pathId)
.concat([
{
pathId: THREE.MathUtils.generateUUID(),
pathPoints: [path.pathPoints[0], splitPoint],
},
{
pathId: THREE.MathUtils.generateUUID(),
pathPoints: [splitPoint, path.pathPoints[1]],
},
])
);
addPointToCurrentTemp(splitPoint);
return;
}
}
addPointToCurrentTemp(newPoint);
}

View File

@@ -0,0 +1,13 @@
export function handleMouseDown(
evt: MouseEvent,
isLeftClickDown: { current: boolean },
isDragging: { current: boolean }
) {
if (evt.button === 0) {
if (evt.ctrlKey || evt.shiftKey) return;
isLeftClickDown.current = true;
isDragging.current = false;
}
}

View File

@@ -0,0 +1,6 @@
export function handleMouseMove(
isLeftClickDown: { current: boolean },
isDragging: { current: boolean }
) {
if (isLeftClickDown.current) isDragging.current = true;
}

View File

@@ -0,0 +1,6 @@
export function handleMouseUp(
evt: MouseEvent,
isLeftClickDown: { current: boolean }
) {
if (evt.button === 0) isLeftClickDown.current = false;
}

View File

@@ -0,0 +1,209 @@
import * as THREE from "three";
export const POLYGON_CLOSE_THRESHOLD = 0.1;
export const SNAP_POINT_THRESHOLD = 0.2;
export const SNAP_LINE_THRESHOLD = 0.2;
type PointData = {
pointId: string;
position: [number, number, number];
isCurved: boolean;
handleA: [number, number, number] | null;
handleB: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
pathPoints: [PointData, PointData]; // always two points
}
type PathData = PathDataInterface[];
export function handleMouseDown(
evt: MouseEvent,
isLeftClickDown: React.MutableRefObject<boolean>,
isDragging: React.MutableRefObject<boolean>
) {
if (evt.button === 0) {
if (evt.ctrlKey || evt.shiftKey) return;
isLeftClickDown.current = true;
isDragging.current = false;
}
}
export function handleMouseUp(
evt: MouseEvent,
isLeftClickDown: React.MutableRefObject<boolean>
) {
if (evt.button === 0) isLeftClickDown.current = false;
}
export function handleMouseMove(
isLeftClickDown: React.MutableRefObject<boolean>,
isDragging: React.MutableRefObject<boolean>
) {
if (isLeftClickDown.current) isDragging.current = true;
}
export function handleMouseClick({
evt,
isDragging,
raycaster,
plane,
pointer,
currentTempPath,
setCurrentTempPath,
pathPointsList,
allPaths,
setAllPaths,
addPointToCurrentTemp,
}: {
evt: MouseEvent;
isDragging: React.MutableRefObject<boolean>;
raycaster: THREE.Raycaster;
plane: THREE.Plane;
pointer: { x: number; y: number };
currentTempPath: any[];
setCurrentTempPath: (val: any[]) => void;
pathPointsList: any[];
allPaths: PathData;
setAllPaths: React.Dispatch<React.SetStateAction<PathData>>;
addPointToCurrentTemp: (point: any) => void;
}) {
if (isDragging.current) return;
if (evt.ctrlKey || evt.shiftKey) return;
const intersectPoint = new THREE.Vector3();
const pos = raycaster.ray.intersectPlane(plane, intersectPoint);
if (!pos) return;
let clickedPoint = new THREE.Vector3(pos.x, 0, pos.z); // force y = 0
let snapPoint: any = null;
for (let p of pathPointsList) {
const pVec = new THREE.Vector3(p.position[0], 0, p.position[2]); // force y = 0
if (pVec.distanceTo(clickedPoint) < SNAP_POINT_THRESHOLD) {
snapPoint = {
...p,
position: [p.position[0], 0, p.position[2]], // force y = 0
};
clickedPoint = pVec;
break;
}
}
let newPoint = snapPoint ?? {
pointId: THREE.MathUtils.generateUUID(),
position: [clickedPoint.x, 0, clickedPoint.z], // y = 0
isCurved: false,
handleA: null,
handleB: null,
};
if (currentTempPath.length > 2) {
const firstVec = new THREE.Vector3(
currentTempPath[0].position[0],
0,
currentTempPath[0].position[2]
); // y = 0
if (firstVec.distanceTo(clickedPoint) < POLYGON_CLOSE_THRESHOLD) {
const closingPoint = {
...currentTempPath[0],
position: [
currentTempPath[0].position[0],
0,
currentTempPath[0].position[2],
], // y = 0
};
addPointToCurrentTemp(closingPoint);
setCurrentTempPath([]);
return;
}
}
const getNearestPointOnLine = (
a: THREE.Vector3,
b: THREE.Vector3,
p: THREE.Vector3
) => {
const ab = new THREE.Vector3().subVectors(b, a);
const t = Math.max(
0,
Math.min(1, p.clone().sub(a).dot(ab) / ab.lengthSq())
);
return a.clone().add(ab.multiplyScalar(t));
};
for (let path of allPaths) {
const a = new THREE.Vector3(
path.pathPoints[0].position[0],
0,
path.pathPoints[0].position[2]
);
const b = new THREE.Vector3(
path.pathPoints[1].position[0],
0,
path.pathPoints[1].position[2]
);
const closest = getNearestPointOnLine(a, b, clickedPoint);
closest.y = 0; // force y = 0
if (closest.distanceTo(clickedPoint) < SNAP_LINE_THRESHOLD) {
const splitPoint = {
pointId: THREE.MathUtils.generateUUID(),
position: [closest.x, 0, closest.z], // y = 0
isCurved: false,
handleA: null,
handleB: null,
};
setAllPaths((prev) =>
prev
.filter((pa) => pa.pathId !== path.pathId)
.concat([
{
pathId: THREE.MathUtils.generateUUID(),
pathPoints: [
{
...path.pathPoints[0],
position: [
path.pathPoints[0].position[0],
0,
path.pathPoints[0].position[2],
] as [number, number, number],
},
splitPoint,
] as [PointData, PointData],
},
{
pathId: THREE.MathUtils.generateUUID(),
pathPoints: [
splitPoint,
{
...path.pathPoints[1],
position: [
path.pathPoints[1].position[0],
0,
path.pathPoints[1].position[2],
] as [number, number, number],
},
] as [PointData, PointData],
},
])
);
console.log("path.pathPoints[1]: ", path.pathPoints);
addPointToCurrentTemp(splitPoint);
return;
}
}
addPointToCurrentTemp(newPoint);
}
export function handleContextMenu(
evt: MouseEvent,
setCurrentTempPath: (val: any[]) => void
) {
evt.preventDefault();
setCurrentTempPath([]);
}

View File

@@ -0,0 +1,256 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useThree } from "@react-three/fiber";
import * as THREE from "three";
import { Line } from "@react-three/drei";
type PointData = {
pointId: string;
position: [number, number, number];
isCurved: boolean;
handleA: [number, number, number] | null;
handleB: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
interface LineSegmentProps {
index: number;
paths: PathDataInterface[];
pathIndex: number;
setPaths: React.Dispatch<React.SetStateAction<PathData>>;
insertPoint?: (pathIndex: number, point: THREE.Vector3) => void;
}
export default function LineSegment({
index,
paths,
setPaths,
insertPoint,
pathIndex,
}: LineSegmentProps) {
const { gl, raycaster, camera, controls } = useThree();
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const [curve, setCurve] = useState<any>();
const [curveState, setCurveState] = useState<string>("");
const curvePoints = useMemo(() => {
if (!paths || index >= paths.length) return [];
const path = paths[index];
const [current, next] = path.pathPoints;
const start = new THREE.Vector3(...current.position);
const end = new THREE.Vector3(...next.position);
// 1⃣ Case 1: use predefined handles
const useCurve =
(current.isCurved && current.handleB) || (next.isCurved && next.handleA);
if (useCurve) {
const hB = current.handleB
? new THREE.Vector3(...current.handleB)
: start;
const hA = next.handleA ? new THREE.Vector3(...next.handleA) : end;
const curve = new THREE.CubicBezierCurve3(start, hB, hA, end);
return curve.getPoints(100);
}
// 2⃣ Case 2: use curveState-generated curve
if (curveState) {
const direction = new THREE.Vector3().subVectors(end, start).normalize();
const up = new THREE.Vector3(0, 1, 0);
const perpendicular = new THREE.Vector3()
.crossVectors(direction, up)
.normalize();
const distance = start.distanceTo(end);
const controlDistance = distance / 6;
let controlPoint1, controlPoint2;
// if (curveState === "arc") {
// const direction = new THREE.Vector3()
// .subVectors(end, start)
// .normalize();
// const perpendicular = new THREE.Vector3(
// -direction.z,
// 0,
// direction.x
// ).normalize();
// controlPoint1 = new THREE.Vector3().addVectors(
// start,
// perpendicular.clone().multiplyScalar(-controlDistance) // negative fixes to "C"
// );
// controlPoint2 = new THREE.Vector3().addVectors(
// end,
// perpendicular.clone().multiplyScalar(-controlDistance)
// );
// }
if (curveState === "arc") {
const direction = new THREE.Vector3()
.subVectors(end, start)
.normalize();
// Perpendicular direction in XZ plane
const perpendicular = new THREE.Vector3(-direction.z, 0, direction.x);
const distance = start.distanceTo(end);
const controlDistance = distance / 4;
const controlPoint1 = new THREE.Vector3()
.addVectors(start, direction.clone().multiplyScalar(distance / 3))
.add(perpendicular.clone().multiplyScalar(-controlDistance)); // ← flipped
const controlPoint2 = new THREE.Vector3()
.addVectors(end, direction.clone().multiplyScalar(-distance / 3))
.add(perpendicular.clone().multiplyScalar(-controlDistance)); // ← flipped
const curve = new THREE.CubicBezierCurve3(
start,
controlPoint1,
controlPoint2,
end
);
return curve.getPoints(64);
}
// if (curveState === "arc") {
// const direction = new THREE.Vector3()
// .subVectors(end, start)
// .normalize();
// // XZ-plane perpendicular
// const perpendicular = new THREE.Vector3(-direction.z, 0, direction.x);
// const distance = start.distanceTo(end);
// const controlDistance = distance / 6; // ← increase this for more curvature
// const controlPoint1 = new THREE.Vector3().addVectors(
// start,
// perpendicular.clone().multiplyScalar(-controlDistance)
// );
// const controlPoint2 = new THREE.Vector3().addVectors(
// end,
// perpendicular.clone().multiplyScalar(-controlDistance)
// );
// const curve = new THREE.CubicBezierCurve3(
// start,
// controlPoint1,
// controlPoint2,
// end
// );
// return curve.getPoints(64);
// }
const curve = new THREE.CubicBezierCurve3(
start,
controlPoint1,
controlPoint2,
end
);
return curve.getPoints(32);
}
// 3⃣ Case 3: fallback straight line
const line = new THREE.LineCurve3(start, end);
return line.getPoints(2);
}, [paths, index, curveState]);
// const curvePoints = useMemo(() => {
// if (!paths || index >= paths.length) return [];
// const path = paths[index];
// const [current, next] = path.pathPoints;
// const start = new THREE.Vector3(...current.position);
// const end = new THREE.Vector3(...next.position);
// // 1⃣ Case 1: Use predefined curve handles if present
// const useCurve =
// (current.isCurved && current.handleB) || (next.isCurved && next.handleA);
// if (useCurve) {
// const handleB = current.handleB
// ? new THREE.Vector3(...current.handleB)
// : start;
// const handleA = next.handleA ? new THREE.Vector3(...next.handleA) : end;
// const curve = new THREE.CubicBezierCurve3(start, handleB, handleA, end);
// return curve.getPoints(100);
// }
// // 2⃣ Case 2: Use curveState-generated arc (gentle C-shaped)
// // if (curveState === "arc") {
// // const direction = new THREE.Vector3().subVectors(end, start).normalize();
// // const distance = start.distanceTo(end);
// // // Get perpendicular in XZ plane
// // const perpendicular = new THREE.Vector3(-direction.z, 0, direction.x);
// // const controlOffset = perpendicular.multiplyScalar(distance / 8);
// // // Create gentle symmetric control points for a "C" arc
// // const controlPoint1 = start.clone().add(controlOffset.clone().negate());
// // const controlPoint2 = end.clone().add(controlOffset.clone().negate());
// // const curve = new THREE.CubicBezierCurve3(
// // start,
// // controlPoint1,
// // controlPoint2,
// // end
// // );
// // return curve.getPoints(64); // 64 for smoother shape
// // }
// if (curveState === "arc") {
// const direction = new THREE.Vector3().subVectors(end, start).normalize();
// const distance = start.distanceTo(end);
// // const curveHeight = distance * 0.25; // 25% of distance
// // 🔺 Control height: Raise control points on Y-axis
// const curveHeight = distance / 4; // adjust 4 → higher = taller arc
// // Control points directly above the midpoint
// const mid = start.clone().add(end).multiplyScalar(0.5);
// const controlPoint = mid
// .clone()
// .add(new THREE.Vector3(0, curveHeight, 0));
// // Use Quadratic Bezier for simple arc
// const curve = new THREE.QuadraticBezierCurve3(start, controlPoint, end);
// return curve.getPoints(64);
// }
// // 3⃣ Case 3: Fallback to straight line
// const line = new THREE.LineCurve3(start, end);
// return line.getPoints(2);
// }, [paths, index, curveState]);
const handleClick = (evt: any) => {
if (evt.ctrlKey) {
setCurveState("arc");
}
};
// const bendFactor = 1 / 10; // tweak this dynamically
// const controlOffset = perpendicular.multiplyScalar(distance * bendFactor);
return (
<Line
points={curvePoints}
color="purple"
lineWidth={3.5}
onClick={handleClick}
/>
);
}

View File

@@ -0,0 +1,831 @@
// import { Line } from "@react-three/drei";
// import { useFrame, useThree } from "@react-three/fiber";
// import React, { useRef, useState } from "react";
// import * as THREE from "three";
// /** --- Types --- */
// type PointData = {
// pointId: string;
// position: [number, number, number];
// isCurved: boolean;
// handleA: [number, number, number] | null;
// handleB: [number, number, number] | null;
// };
// interface PathDataInterface {
// pathId: string;
// pathPoints: [PointData, PointData]; // always two points
// }
// type PathData = PathDataInterface[];
// interface PointHandleProps {
// point: PointData;
// pointIndex: number;
// points: PointData[];
// setPoints: React.Dispatch<React.SetStateAction<PointData[]>>;
// setPaths: React.Dispatch<React.SetStateAction<PathData>>;
// paths: PathData;
// selected: number[];
// setSelected: React.Dispatch<React.SetStateAction<number[]>>;
// setShortestPath: React.Dispatch<React.SetStateAction<PathData>>;
// }
// /** --- Math helpers --- */
// function dist(a: PointData, b: PointData): number {
// return Math.sqrt(
// (a.position[0] - b.position[0]) ** 2 +
// (a.position[1] - b.position[1]) ** 2 +
// (a.position[2] - b.position[2]) ** 2
// );
// }
// /** --- A* Algorithm --- */
// type AStarResult = {
// pointIds: string[];
// distance: number;
// };
// function aStarShortestPath(
// startId: string,
// goalId: string,
// points: PointData[],
// paths: PathData
// ): AStarResult | null {
// const pointById = new Map(points.map((p) => [p.pointId, p]));
// const start = pointById.get(startId);
// const goal = pointById.get(goalId);
// if (!start || !goal) return null;
// const openSet = new Set<string>([startId]);
// const cameFrom: Record<string, string | null> = {};
// const gScore: Record<string, number> = {};
// const fScore: Record<string, number> = {};
// for (const p of points) {
// cameFrom[p.pointId] = null;
// gScore[p.pointId] = Infinity;
// fScore[p.pointId] = Infinity;
// }
// gScore[startId] = 0;
// fScore[startId] = dist(start, goal);
// const neighborsOf = (id: string): { id: string; cost: number }[] => {
// const me = pointById.get(id)!;
// const out: { id: string; cost: number }[] = [];
// for (const edge of paths) {
// const [a, b] = edge.pathPoints;
// if (a.pointId === id) out.push({ id: b.pointId, cost: dist(me, b) });
// else if (b.pointId === id) out.push({ id: a.pointId, cost: dist(me, a) });
// }
// return out;
// };
// while (openSet.size > 0) {
// let current: string = [...openSet].reduce((a, b) =>
// fScore[a] < fScore[b] ? a : b
// );
// if (current === goalId) {
// const ids: string[] = [];
// let node: string | null = current;
// while (node) {
// ids.unshift(node);
// node = cameFrom[node];
// }
// return { pointIds: ids, distance: gScore[goalId] };
// }
// openSet.delete(current);
// for (const nb of neighborsOf(current)) {
// const tentativeG = gScore[current] + nb.cost;
// if (tentativeG < gScore[nb.id]) {
// cameFrom[nb.id] = current;
// gScore[nb.id] = tentativeG;
// fScore[nb.id] = tentativeG + dist(pointById.get(nb.id)!, goal);
// openSet.add(nb.id);
// }
// }
// }
// return null;
// }
// /** --- Convert node path to edges --- */
// // function nodePathToEdges(
// // pointIds: string[],
// // points: PointData[],
// // paths: PathData
// // ): PathData {
// // const byId = new Map(points.map((p) => [p.pointId, p]));
// // const edges: PathData = [];
// // for (let i = 0; i < pointIds.length - 1; i++) {
// // const a = pointIds[i];
// // const b = pointIds[i + 1];
// //
// //
// // const edge = paths.find(
// // (p) =>
// // (p.pathPoints[0].pointId === a && p.pathPoints[1].pointId === b) ||
// // (p.pathPoints[0].pointId === b && p.pathPoints[1].pointId === a)
// // );
// // if (edge) edges.push(edge);
// // else {
// // const pa = byId.get(a)!;
// // const pb = byId.get(b)!;
// // edges.push({
// // pathId: `synthetic-${a}-${b}`,
// // pathPoints: [pa, pb],
// // });
// // }
// // }
// //
// // return edges;
// // }
// function nodePathToEdges(
// pointIds: string[],
// points: PointData[],
// paths: PathData
// ): PathData {
// const byId = new Map(points.map((p) => [p.pointId, p]));
// const edges: PathData = [];
// for (let i = 0; i < pointIds.length - 1; i++) {
// const a = pointIds[i];
// const b = pointIds[i + 1];
// const edge = paths.find(
// (p) =>
// (p.pathPoints[0].pointId === a && p.pathPoints[1].pointId === b) ||
// (p.pathPoints[0].pointId === b && p.pathPoints[1].pointId === a)
// );
// if (edge) {
// // Ensure correct order in edge
// const [p1, p2] = edge.pathPoints;
// edges.push({
// pathId: edge.pathId,
// pathPoints:
// p1.pointId === a
// ? ([p1, p2] as [PointData, PointData])
// : ([p2, p1] as [PointData, PointData]),
// });
// } else {
// const pa = byId.get(a)!;
// const pb = byId.get(b)!;
// edges.push({
// pathId: `synthetic-${a}-${b}`,
// pathPoints: [pa, pb],
// });
// }
// }
// return edges;
// }
// /** --- React Component --- */
// export default function PointHandlers({
// point,
// pointIndex,
// points,
// setPoints,
// setPaths,
// paths,
// selected,
// setSelected,
// setShortestPath,
// }: PointHandleProps) {
// const meshRef = useRef<THREE.Mesh>(null);
// const handleARef = useRef<THREE.Mesh>(null);
// const handleBRef = useRef<THREE.Mesh>(null);
// const lineRef = useRef<any>(null!);
// const { camera, gl, controls } = useThree();
// const [dragging, setDragging] = useState<
// null | "main" | "handleA" | "handleB"
// >(null);
// const dragOffset = useRef(new THREE.Vector3());
// const [shortestEdges, setShortestEdges] = useState<PathData>([]);
// /** Click handling */
// const onPointClick = (e: any) => {
// e.stopPropagation();
// if (e.shiftKey) {
// setSelected((prev) => {
// if (prev.length === 0) return [pointIndex];
// else if (prev.length === 1) {
// const p1 = points[prev[0]];
// const p2 = points[pointIndex];
// const result = aStarShortestPath(
// p1.pointId,
// p2.pointId,
// points,
// paths
// );
// if (result) {
// const edges = nodePathToEdges(result.pointIds, points, paths);
// setShortestEdges(edges);
// setShortestPath(edges);
// } else {
// setShortestEdges([]);
// }
// return [prev[0], pointIndex];
// } else {
// setShortestEdges([]);
// return [pointIndex];
// }
// });
// } else if (e.ctrlKey) {
// setPoints((prev) => {
// const updated = [...prev];
// const p = { ...updated[pointIndex] };
// if (!p.handleA && !p.handleB) {
// p.handleA = [p.position[0] + 1, p.position[1], p.position[2]];
// p.handleB = [p.position[0] - 1, p.position[1], p.position[2]];
// p.isCurved = true;
// } else {
// p.handleA = null;
// p.handleB = null;
// p.isCurved = false;
// }
// updated[pointIndex] = p;
// return updated;
// });
// }
// };
// /** Dragging logic */
// const startDrag = (target: "main" | "handleA" | "handleB", e: any) => {
// e.stopPropagation();
// setDragging(target);
// const targetRef =
// target === "main"
// ? meshRef.current
// : target === "handleA"
// ? handleARef.current
// : handleBRef.current;
// if (targetRef) dragOffset.current.copy(targetRef.position).sub(e.point);
// if (controls) (controls as any).enabled = false;
// gl.domElement.style.cursor = "grabbing";
// };
// const stopDrag = () => {
// setDragging(null);
// gl.domElement.style.cursor = "auto";
// if (controls) (controls as any).enabled = true;
// };
// useFrame(({ raycaster, mouse }) => {
// if (!dragging) return;
// const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
// raycaster.setFromCamera(mouse, camera);
// const intersection = new THREE.Vector3();
// if (!raycaster.ray.intersectPlane(plane, intersection)) return;
// const newPos = intersection.add(dragOffset.current);
// setPoints((prevPoints) => {
// const updatedPoints = [...prevPoints];
// const point = { ...updatedPoints[pointIndex] };
// if (dragging === "main") {
// // Calculate delta movement
// const delta = newPos
// .clone()
// .sub(new THREE.Vector3().fromArray(point.position));
// // Move main point
// point.position = newPos.toArray() as [number, number, number];
// // Move handles with main point
// if (point.handleA)
// point.handleA = new THREE.Vector3()
// .fromArray(point.handleA)
// .add(delta)
// .toArray() as [number, number, number];
// if (point.handleB)
// point.handleB = new THREE.Vector3()
// .fromArray(point.handleB)
// .add(delta)
// .toArray() as [number, number, number];
// } else {
// // Dragging a handle
// point[dragging] = newPos.toArray() as [number, number, number];
// if (point.isCurved) {
// // Mirror the opposite handle
// const mainPos = new THREE.Vector3().fromArray(point.position);
// const thisHandle = new THREE.Vector3().fromArray(point[dragging]!);
// const mirrorHandle = mainPos
// .clone()
// .sub(thisHandle.clone().sub(mainPos));
// if (dragging === "handleA")
// point.handleB = mirrorHandle.toArray() as [number, number, number];
// if (dragging === "handleB")
// point.handleA = mirrorHandle.toArray() as [number, number, number];
// }
// }
// updatedPoints[pointIndex] = point;
// // Update all paths that include this point
// setPaths((prevPaths: any) =>
// prevPaths.map((path: any) => {
// const updatedPathPoints = path.pathPoints.map((p: any) =>
// p.pointId === point.pointId ? point : p
// );
// return { ...path, pathPoints: updatedPathPoints };
// })
// );
// return updatedPoints;
// });
// });
// /** Update line between handles */
// useFrame(() => {
// if (lineRef.current && point.handleA && point.handleB) {
// const positions = lineRef.current.geometry.attributes.position
// .array as Float32Array;
// positions[0] = point.handleA[0];
// positions[1] = point.handleA[1];
// positions[2] = point.handleA[2];
// positions[3] = point.handleB[0];
// positions[4] = point.handleB[1];
// positions[5] = point.handleB[2];
// lineRef.current.geometry.attributes.position.needsUpdate = true;
// }
// });
// return (
// <>
// {/* Main point */}
// <mesh
// ref={meshRef}
// position={point.position}
// onClick={onPointClick}
// onPointerDown={(e) => startDrag("main", e)}
// onPointerUp={stopDrag}
// >
// <sphereGeometry args={[0.3, 16, 16]} />
// <meshStandardMaterial
// color={selected.includes(pointIndex) ? "red" : "pink"}
// />
// </mesh>
// {/* Curve handles */}
// {point.isCurved && point.handleA && point.handleB && (
// <>
// <Line
// ref={lineRef}
// points={[point.handleA, point.handleB]}
// color="gray"
// lineWidth={1}
// />
// <mesh
// ref={handleARef}
// position={point.handleA}
// onPointerDown={(e) => startDrag("handleA", e)}
// onPointerUp={stopDrag}
// >
// <sphereGeometry args={[0.15, 8, 8]} />
// <meshStandardMaterial color="orange" />
// </mesh>
// <mesh
// ref={handleBRef}
// position={point.handleB}
// onPointerDown={(e) => startDrag("handleB", e)}
// onPointerUp={stopDrag}
// >
// <sphereGeometry args={[0.15, 8, 8]} />
// <meshStandardMaterial color="green" />
// </mesh>
// </>
// )}
// {/* Draw connected paths */}
// {/* Highlight shortest path */}
// {shortestEdges.map((edge) => (
// <Line
// key={`sp-${edge.pathId}`}
// points={edge.pathPoints.map((p) => p.position)}
// color="yellow"
// lineWidth={3}
// />
// ))}
// </>
// );
// }
/** --- Types --- */
import { Line } from "@react-three/drei";
import { useFrame, useThree } from "@react-three/fiber";
import React, { useRef, useState } from "react";
import * as THREE from "three";
/** --- Types --- */
type PointData = {
pointId: string;
position: [number, number, number];
isCurved: boolean;
handleA: [number, number, number] | null;
handleB: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
interface PointHandleProps {
point: PointData;
pointIndex: number;
points: PointData[];
setPoints: React.Dispatch<React.SetStateAction<PointData[]>>;
setPaths: React.Dispatch<React.SetStateAction<PathData>>;
paths: PathData;
selected: number[];
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
setShortestPath: React.Dispatch<React.SetStateAction<PathData>>;
}
/** --- Math helpers --- */
function dist(a: PointData, b: PointData): number {
return Math.sqrt(
(a.position[0] - b.position[0]) ** 2 +
(a.position[1] - b.position[1]) ** 2 +
(a.position[2] - b.position[2]) ** 2
);
}
/** --- A* Algorithm --- */
type AStarResult = {
pointIds: string[];
distance: number;
};
function aStarShortestPath(
startId: string,
goalId: string,
points: PointData[],
paths: PathData
): AStarResult | null {
const pointById = new Map(points.map((p) => [p.pointId, p]));
const start = pointById.get(startId);
const goal = pointById.get(goalId);
if (!start || !goal) return null;
const openSet = new Set<string>([startId]);
const cameFrom: Record<string, string | null> = {};
const gScore: Record<string, number> = {};
const fScore: Record<string, number> = {};
for (const p of points) {
cameFrom[p.pointId] = null;
gScore[p.pointId] = Infinity;
fScore[p.pointId] = Infinity;
}
gScore[startId] = 0;
fScore[startId] = dist(start, goal);
const neighborsOf = (id: string): { id: string; cost: number }[] => {
const me = pointById.get(id)!;
const out: { id: string; cost: number }[] = [];
for (const edge of paths) {
const [a, b] = edge.pathPoints;
if (a.pointId === id) out.push({ id: b.pointId, cost: dist(me, b) });
else if (b.pointId === id) out.push({ id: a.pointId, cost: dist(me, a) });
}
return out;
};
while (openSet.size > 0) {
let current: string = [...openSet].reduce((a, b) =>
fScore[a] < fScore[b] ? a : b
);
if (current === goalId) {
const ids: string[] = [];
let node: string | null = current;
while (node) {
ids.unshift(node);
node = cameFrom[node];
}
return { pointIds: ids, distance: gScore[goalId] };
}
openSet.delete(current);
for (const nb of neighborsOf(current)) {
const tentativeG = gScore[current] + nb.cost;
if (tentativeG < gScore[nb.id]) {
cameFrom[nb.id] = current;
gScore[nb.id] = tentativeG;
fScore[nb.id] = tentativeG + dist(pointById.get(nb.id)!, goal);
openSet.add(nb.id);
}
}
}
return null;
}
/** --- Convert node path to edges --- */
function nodePathToEdges(
pointIds: string[],
points: PointData[],
paths: PathData
): PathData {
const byId = new Map(points.map((p) => [p.pointId, p]));
const edges: PathData = [];
for (let i = 0; i < pointIds.length - 1; i++) {
const a = pointIds[i];
const b = pointIds[i + 1];
const edge = paths.find(
(p) =>
(p.pathPoints[0].pointId === a && p.pathPoints[1].pointId === b) ||
(p.pathPoints[0].pointId === b && p.pathPoints[1].pointId === a)
);
if (edge) {
const [p1, p2] = edge.pathPoints;
edges.push({
pathId: edge.pathId,
pathPoints:
p1.pointId === a
? ([p1, p2] as [PointData, PointData])
: ([p2, p1] as [PointData, PointData]),
});
} else {
const pa = byId.get(a)!;
const pb = byId.get(b)!;
edges.push({
pathId: `synthetic-${a}-${b}`,
pathPoints: [pa, pb],
});
}
}
return edges;
}
/** --- React Component --- */
export default function PointHandlers({
point,
pointIndex,
points,
setPoints,
setPaths,
paths,
selected,
setSelected,
setShortestPath,
}: PointHandleProps) {
const meshRef = useRef<THREE.Mesh>(null);
const handleARef = useRef<THREE.Mesh>(null);
const handleBRef = useRef<THREE.Mesh>(null);
const lineRef = useRef<any>(null);
const { gl, controls, raycaster } = useThree();
const [dragging, setDragging] = useState<
null | "main" | "handleA" | "handleB"
>(null);
const dragOffset = useRef(new THREE.Vector3());
const [shortestEdges, setShortestEdges] = useState<PathData>([]);
/** Click handling */
const onPointClick = (e: any) => {
e.stopPropagation();
if (e.shiftKey) {
setSelected((prev) => {
console.log("prev: ", prev);
console.log("pointIndex: ", pointIndex);
if (prev.length === 0) return [pointIndex];
if (prev.length === 1) {
// defer shortest path calculation
setTimeout(() => {
console.log("points: ", points);
const p1 = points[prev[0]];
const p2 = points[pointIndex];
const result = aStarShortestPath(
p1.pointId,
p2.pointId,
points,
paths
);
if (result) {
const edges = nodePathToEdges(result.pointIds, points, paths);
console.log("edges: ", edges);
setShortestEdges(edges);
setShortestPath(edges);
} else {
setShortestEdges([]);
}
}, 0);
return [prev[0], pointIndex];
}
return [pointIndex];
});
} else if (e.ctrlKey) {
setPoints((prev) => {
const updated = [...prev];
const p = { ...updated[pointIndex] };
if (!p.handleA && !p.handleB) {
p.handleA = [p.position[0] + 1, 0, p.position[2]];
p.handleB = [p.position[0] - 1, 0, p.position[2]];
p.isCurved = true;
} else {
p.handleA = null;
p.handleB = null;
p.isCurved = false;
}
updated[pointIndex] = p;
return updated;
});
}
};
/** Dragging logic */
const startDrag = (target: "main" | "handleA" | "handleB", e: any) => {
e.stopPropagation();
setDragging(target);
const targetRef =
target === "main"
? meshRef.current
: target === "handleA"
? handleARef.current
: handleBRef.current;
if (targetRef && targetRef.position) {
dragOffset.current
.copy(new THREE.Vector3(targetRef.position.x, 0, targetRef.position.z))
.sub(e.point);
}
if (controls) (controls as any).enabled = false;
gl.domElement.style.cursor = "grabbing";
};
const stopDrag = () => {
setDragging(null);
gl.domElement.style.cursor = "auto";
if (controls) (controls as any).enabled = true;
};
/** Update position in useFrame */
useFrame(({ mouse }) => {
if (!dragging) return;
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const intersection = new THREE.Vector3();
if (!raycaster.ray.intersectPlane(plane, intersection)) return;
const newPos = intersection.add(dragOffset.current);
setPoints((prevPoints) => {
const updatedPoints = [...prevPoints];
const p = { ...updatedPoints[pointIndex] };
if (dragging === "main") {
const delta = newPos
.clone()
.sub(new THREE.Vector3().fromArray(p.position));
p.position = [newPos.x, 0, newPos.z];
if (p.handleA) {
p.handleA = new THREE.Vector3()
.fromArray(p.handleA)
.add(new THREE.Vector3(delta.x, 0, delta.z))
.toArray() as [number, number, number];
}
if (p.handleB) {
p.handleB = new THREE.Vector3()
.fromArray(p.handleB)
.add(new THREE.Vector3(delta.x, 0, delta.z))
.toArray() as [number, number, number];
}
} else {
p[dragging] = [newPos.x, 0, newPos.z];
if (p.isCurved) {
const mainPos = new THREE.Vector3().fromArray(p.position);
const thisHandle = new THREE.Vector3().fromArray(p[dragging]!);
const mirrorHandle = mainPos
.clone()
.sub(thisHandle.clone().sub(mainPos));
console.log("mirrorHandle: ", mirrorHandle);
if (dragging === "handleA")
p.handleB = mirrorHandle.toArray() as [number, number, number];
if (dragging === "handleB")
p.handleA = mirrorHandle.toArray() as [number, number, number];
}
}
updatedPoints[pointIndex] = p;
setPaths((prevPaths: any) =>
prevPaths.map((path: any) => ({
...path,
pathPoints: path.pathPoints.map((pp: any) =>
pp.pointId === p.pointId ? p : pp
),
}))
);
return updatedPoints;
});
});
/** Update line between handles */
useFrame(() => {
if (lineRef.current && point.handleA && point.handleB) {
const positions = lineRef.current.geometry.attributes.position
.array as Float32Array;
positions[0] = point.handleA[0];
positions[1] = point.handleA[1];
positions[2] = point.handleA[2];
positions[3] = point.handleB[0];
positions[4] = point.handleB[1];
positions[5] = point.handleB[2];
lineRef.current.geometry.attributes.position.needsUpdate = true;
}
});
return (
<>
{/* Main point */}
<mesh
ref={meshRef}
position={point.position}
onClick={onPointClick}
onPointerDown={(e) => startDrag("main", e)}
onPointerUp={stopDrag}
>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial
color={selected.includes(pointIndex) ? "red" : "pink"}
/>
</mesh>
{/* Curve handles */}
{point.isCurved && point.handleA && point.handleB && (
<>
<Line
ref={lineRef}
points={[point.handleA, point.handleB]}
color="gray"
lineWidth={1}
/>
<mesh
ref={handleARef}
position={point.handleA}
onPointerDown={(e) => startDrag("handleA", e)}
onPointerUp={stopDrag}
>
<sphereGeometry args={[0.15, 8, 8]} />
<meshStandardMaterial color="orange" />
</mesh>
<mesh
ref={handleBRef}
position={point.handleB}
onPointerDown={(e) => startDrag("handleB", e)}
onPointerUp={stopDrag}
>
<sphereGeometry args={[0.15, 8, 8]} />
<meshStandardMaterial color="green" />
</mesh>
</>
)}
{/* Highlight shortest path */}
{shortestEdges.map((edge) => (
<Line
key={`sp-${edge.pathId}`}
points={edge.pathPoints.map((p) => p.position)}
color="yellow"
lineWidth={3}
/>
))}
</>
);
}

View File

@@ -0,0 +1,318 @@
import * as THREE from "three";
import { useRef, useState, useMemo, useEffect } from "react";
import { useThree, useFrame } from "@react-three/fiber";
import { Line } from "@react-three/drei";
import { useSceneContext } from "../../../scene/sceneContext";
import {
useAnimationPlaySpeed,
usePlayButtonStore,
} from "../../../../store/usePlayButtonStore";
import PointHandles from "./pointHandlers";
import LineSegment from "./lineSegment";
import {
handleContextMenu,
handleMouseClick,
handleMouseDown,
handleMouseMove,
handleMouseUp,
} from "./functions/pathMouseHandler";
type PointData = {
pointId: string;
position: [number, number, number];
isCurved: boolean;
handleA: [number, number, number] | null;
handleB: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
isActive?: boolean;
isCurved?: boolean;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
type SegmentPoint = {
position: THREE.Vector3;
originalPoint?: PointData;
};
export default function StructuredPath() {
const { scene, camera, raycaster, gl, pointer } = useThree();
const plane = useMemo(
() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0),
[]
);
const { speed } = useAnimationPlaySpeed();
const { assetStore } = useSceneContext();
const { assets } = assetStore();
// --- State Variables ---
const [pathPointsList, setPathPointsList] = useState<PointData[]>([]);
const [allPaths, setAllPaths] = useState<PathData>([]);
const [computedShortestPath, setComputedShortestPath] = useState<PathData>(
[]
);
const [currentTempPath, setCurrentTempPath] = useState<PointData[]>([]);
const [currentMousePos, setCurrentMousePos] = useState<
[number, number, number] | null
>(null);
const [selectedPointIndices, setSelectedPointIndices] = useState<number[]>(
[]
);
const [vehicleUuids, setVehicleUuids] = useState<any>();
// --- Constants & Refs ---
const isLeftClickDown = useRef<boolean>(false);
const isDragging = useRef<boolean>(false);
const vehicleMovementState = useRef<any>({});
const activeVehicleIndexRef = useRef(0);
const { isPlaying } = usePlayButtonStore();
// --- Computed Path Segments ---
const pathSegments = useMemo(() => {
if (!computedShortestPath || computedShortestPath.length === 0) return [];
const segments: SegmentPoint[] = [];
computedShortestPath.forEach((path) => {
const [start, end] = path.pathPoints;
const startPos = new THREE.Vector3(...start.position);
const endPos = new THREE.Vector3(...end.position);
// Start point has curve handles
if (start.isCurved && start.handleA && start.handleB) {
const handleA = new THREE.Vector3(...start.handleA);
const handleB = new THREE.Vector3(...start.handleB);
const curve = new THREE.CubicBezierCurve3(
startPos,
handleA,
handleB,
endPos
);
const points = curve.getPoints(20).map((pos) => ({
position: pos,
originalPoint: start,
}));
segments.push(...points);
}
// End point has curve handles
else if (end.isCurved && end.handleA && end.handleB) {
const handleA = new THREE.Vector3(...end.handleA);
const handleB = new THREE.Vector3(...end.handleB);
const curve = new THREE.CubicBezierCurve3(
startPos,
handleA,
handleB,
endPos
);
const points = curve.getPoints(20).map((pos) => ({
position: pos,
originalPoint: end,
}));
segments.push(...points);
}
// No curves — just straight line
else {
segments.push(
{ position: startPos, originalPoint: start },
{ position: endPos, originalPoint: end }
);
}
});
// Filter out duplicate consecutive points
return segments.filter(
(v, i, arr) => i === 0 || !v.position.equals(arr[i - 1].position)
);
}, [computedShortestPath]);
// --- Initialize Vehicles ---
useEffect(() => {
const findVehicle = assets
.filter((val) => val.eventData?.type === "Vehicle")
?.map((val) => val.modelUuid);
setVehicleUuids(findVehicle);
vehicleMovementState.current = {};
findVehicle.forEach((uuid) => {
vehicleMovementState.current[uuid] = { index: 0, progress: 0 };
});
}, [assets]);
// --- Vehicle Movement ---
useFrame((_, delta) => {
if (!isPlaying || pathSegments.length < 2) return;
const object = scene.getObjectByProperty(
"uuid",
vehicleUuids[activeVehicleIndexRef.current]
);
if (!object) return;
const state =
vehicleMovementState.current[vehicleUuids[activeVehicleIndexRef.current]];
if (!state) return;
const startSeg = pathSegments[state.index];
const endSeg = pathSegments[state.index + 1];
const segmentDistance = startSeg.position.distanceTo(endSeg.position);
state.progress += (speed * delta) / segmentDistance;
if (state.progress >= 1) {
state.progress = 0;
state.index++;
if (state.index >= pathSegments.length - 1) {
state.index = 0;
activeVehicleIndexRef.current =
(activeVehicleIndexRef.current + 1) % vehicleUuids.length;
}
}
const newPos = startSeg.position
.clone()
.lerp(endSeg.position, state.progress);
object.position.copy(newPos);
const direction = endSeg.position
.clone()
.sub(startSeg.position)
.normalize();
const forward = new THREE.Vector3(0, 0, 1);
object.quaternion.setFromUnitVectors(forward, direction);
});
// --- Update Mouse Position ---
useFrame(() => {
if (currentTempPath.length === 0) return;
raycaster.setFromCamera(pointer, camera);
const intersect = new THREE.Vector3();
if (raycaster.ray.intersectPlane(plane, intersect)) {
setCurrentMousePos([intersect.x, intersect.y, intersect.z]);
}
});
const addPointToCurrentTemp = (newPoint: PointData) => {
setCurrentTempPath((prev) => {
const updated = [...prev, newPoint];
if (prev.length > 0) {
const lastPoint = prev[prev.length - 1];
const newPath: PathDataInterface = {
pathId: THREE.MathUtils.generateUUID(),
pathPoints: [lastPoint, newPoint],
};
setAllPaths((prevPaths) => [...prevPaths, newPath]);
}
return updated;
});
setPathPointsList((prev) => {
if (!prev.find((p) => p.pointId === newPoint.pointId))
return [...prev, newPoint];
return prev;
});
};
useEffect(() => {
const canvas = gl.domElement;
const onMouseDown = (evt: MouseEvent) =>
handleMouseDown(evt, isLeftClickDown, isDragging);
const onMouseUp = (evt: MouseEvent) => handleMouseUp(evt, isLeftClickDown);
const onMouseMove = () => handleMouseMove(isLeftClickDown, isDragging);
const onClick = (evt: MouseEvent) =>
handleMouseClick({
evt,
isDragging,
raycaster,
plane,
pointer,
currentTempPath,
setCurrentTempPath,
pathPointsList,
allPaths,
setAllPaths,
addPointToCurrentTemp,
});
const onContextMenu = (evt: MouseEvent) =>
handleContextMenu(evt, setCurrentTempPath);
canvas.addEventListener("mousedown", onMouseDown);
canvas.addEventListener("mouseup", onMouseUp);
canvas.addEventListener("mousemove", onMouseMove);
canvas.addEventListener("click", onClick);
canvas.addEventListener("contextmenu", onContextMenu);
return () => {
canvas.removeEventListener("mousedown", onMouseDown);
canvas.removeEventListener("mouseup", onMouseUp);
canvas.removeEventListener("mousemove", onMouseMove);
canvas.removeEventListener("click", onClick);
canvas.removeEventListener("contextmenu", onContextMenu);
};
}, [
gl,
camera,
raycaster,
pointer,
plane,
currentTempPath,
pathPointsList,
allPaths,
]);
// --- Render ---
return (
<>
{allPaths.map((path, pathIndex) => (
<LineSegment
key={path.pathId}
index={pathIndex}
pathIndex={pathIndex}
paths={allPaths}
setPaths={setAllPaths}
/>
))}
{pathPointsList.map((point, index) => (
<PointHandles
key={point.pointId}
point={point}
pointIndex={index}
points={pathPointsList}
setPoints={setPathPointsList}
paths={allPaths}
setPaths={setAllPaths}
setShortestPath={setComputedShortestPath}
selected={selectedPointIndices}
setSelected={setSelectedPointIndices}
/>
))}
{currentTempPath.length > 0 && currentMousePos && (
<Line
points={[
new THREE.Vector3(
...currentTempPath[currentTempPath.length - 1].position
),
new THREE.Vector3(...currentMousePos),
]}
color="orange"
lineWidth={2}
dashed
/>
)}
</>
);
}

View File

@@ -4,36 +4,39 @@ import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
import VehicleInstances from "./instances/vehicleInstances";
import VehicleUI from "../spatialUI/vehicle/vehicleUI";
import { useSceneContext } from "../../scene/sceneContext";
import PreDefinedPath from "./preDefinedPath/preDefinedPath";
import StructuredPath from "./structuredPath/structuredPath";
import PathCreator from "./pathCreator/pathCreator";
function Vehicles() {
const { vehicleStore } = useSceneContext();
const { getVehicleById } = vehicleStore();
const { selectedEventSphere } = useSelectedEventSphere();
const { isPlaying } = usePlayButtonStore();
const [isVehicleSelected, setIsVehicleSelected] = useState(false);
const { vehicleStore } = useSceneContext();
const { getVehicleById } = vehicleStore();
const { selectedEventSphere } = useSelectedEventSphere();
const { isPlaying } = usePlayButtonStore();
const [isVehicleSelected, setIsVehicleSelected] = useState(false);
useEffect(() => {
if (selectedEventSphere) {
const selectedVehicle = getVehicleById(selectedEventSphere.userData.modelUuid);
if (selectedVehicle) {
setIsVehicleSelected(true);
} else {
setIsVehicleSelected(false);
}
}
}, [getVehicleById, selectedEventSphere])
useEffect(() => {
if (selectedEventSphere) {
const selectedVehicle = getVehicleById(
selectedEventSphere.userData.modelUuid
);
if (selectedVehicle) {
setIsVehicleSelected(true);
} else {
setIsVehicleSelected(false);
}
}
}, [getVehicleById, selectedEventSphere]);
return (
<>
<VehicleInstances />
{isVehicleSelected && selectedEventSphere && !isPlaying &&
<VehicleUI />
}
</>
);
return (
<>
<PathCreator />
{/* <StructuredPath /> */}
{/* <PreDefinedPath /> */}
<VehicleInstances />
{isVehicleSelected && selectedEventSphere && !isPlaying && <VehicleUI />}
</>
);
}
export default Vehicles;

View File

@@ -0,0 +1,57 @@
import { useLocation, useNavigate } from "react-router-dom";
import text404 from "../assets/image/404/404.svg";
import hero from "../assets/image/404/404_bk.png";
const PageNotFound = () => {
const savedTheme = localStorage.getItem("theme");
const isLogedIn = localStorage.getItem("userId");
const navigate = useNavigate();
const { hash } = useLocation();
function getErrorContext() {
const contexts = hash.split("#");
const context = contexts[1];
const info = contexts.length > 1 ? contexts[2] : "";
switch (context) {
case "project_not_found":
return `Project Not found - The project ${
info !== "" && `with ID (${info})`
} was not found.`;
default:
return "Page not Found - looks like we have hit a roadblock";
}
}
return (
<div className="page-not-found-wrapper">
<div className="page-not-found-container">
<div className="text-404">
<img
src={text404}
alt=""
style={savedTheme === "dark" ? { filter: "invert(1)" } : {}}
/>
</div>
<div className="hero-container">
<img src={hero} alt="" />
</div>
<div className="context">{getErrorContext()}</div>
<button
className="back-to-home"
id="go-back-home-404-btn"
onClick={() => {
if (isLogedIn) {
navigate("/Dashboard");
} else {
navigate("/");
}
}}
>
Go Back to Home
</button>
</div>
</div>
);
};
export default PageNotFound;

View File

@@ -42,6 +42,7 @@ const Project: React.FC = () => {
useEffect(() => {
if (!email || !userId) {
console.error("User data not found in localStorage");
navigate("/page-not-found");
return;
}
@@ -60,6 +61,7 @@ const Project: React.FC = () => {
await viewProject(organization, matchedProject._id, userId);
} else {
console.warn("Project not found with given ID:", projectId);
navigate(`/not_found#project_not_found#${projectId}`);
}
} catch (error) {
console.error("Error fetching projects:", error);
@@ -72,6 +74,7 @@ const Project: React.FC = () => {
useEffect(() => {
if (!projectId) return;
getVersionHistoryApi(projectId)
.then((data) => {
const versions: VersionHistory = [];
@@ -98,6 +101,7 @@ const Project: React.FC = () => {
if (email) {
const token = localStorage.getItem("token");
const refreshToken = localStorage.getItem("refreshToken");
echo.warn('Validating token');
if (token) {
useSocketStore
.getState()
@@ -107,7 +111,7 @@ const Project: React.FC = () => {
setOrganization(organization);
setUserName(userName);
}
echo.success("Log in successful");
echo.success("Project initialized and loaded successfully");
} else {
navigate("/");
}

View File

@@ -36,8 +36,6 @@ const UserAuth: React.FC = () => {
initializeFingerprint();
}, []);
const { userId, organization } = getUserData();
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const organization = email.split("@")[1].split(".")[0];

View File

@@ -0,0 +1,33 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
export const getAssetThumbnail = async (assetId: String) => {
try {
const response = await fetch(
`${url_Backend_dwinzo}/api/v2/getAssetThumbnail/${assetId}`,
{
method: "GET",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
}
);
const newAccessToken = response.headers.get("x-access-token");
if (newAccessToken) {
localStorage.setItem("token", newAccessToken);
}
if (!response.ok) {
throw new Error("Failed to fetch assets");
}
//
return await response.json();
} catch (error: any) {
echo.error("Failed to get asset image");
}
};

View File

@@ -25,6 +25,7 @@ export const renameProductApi = async (body: {
if (!response.ok) {
console.error("Failed to rename product");
echo.error("Failed to rename product");
}
const result = await response.json();
@@ -32,11 +33,11 @@ export const renameProductApi = async (body: {
return result;
} catch (error) {
echo.error("Failed to rename product Api");
if (error instanceof Error) {
console.log(error.message);
} else {
console.log("An unknown error occurred");
echo.log("An unknown error occurred");
}
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -63,7 +63,7 @@
}
.bg-dummy.right-bottom {
right: 68px;
right: 84px;
bottom: 0;
width: 20%;
height: 100%;
@@ -79,7 +79,8 @@
}
.logs-detail,
.version {
.version,
.wifi-connection {
@include flex-center;
border-radius: #{$border-radius-extra-large};
padding: 3px 6px;
@@ -107,7 +108,28 @@
}
}
.version {
.wifi-connection {
.tooltip {
transform: translateX(-16px);
&::after {
left: 76%;
}
}
&:hover {
.tooltip {
opacity: 1;
}
}
&.connected svg path {
fill: #1ec018;
}
&.disconnected svg path {
fill: #e44405;
}
}
.version,
.wifi-connection {
background: var(--background-color);
font-size: var(--font-size-tiny);
@@ -179,7 +201,7 @@
background: var(--background-color);
border-radius: #{$border-radius-medium};
outline: 1px solid var(--border-color);
&:hover{
&:hover {
background: var(--background-color-solid);
}
}
@@ -288,7 +310,7 @@
font-family: monospace;
font-size: var(--font-size-tiny);
color: var(--icon-default-color-active);
&:last-child{
&:last-child {
background: var(--background-color-button);
}
}

View File

@@ -114,29 +114,34 @@
.user-image-wrapper {
width: 28px;
height: 28px;
border-radius: 50%;
background-color: #fff;
position: relative;
.user-image {
height: 100%;
width: 100%;
border-radius: 50%;
background-color: #fff;
overflow: hidden;
// transform: translate(-26px, -12px);
}
.status {
border-radius: 50%;
width: 6px;
height: 6px;
outline: 1px solid #2f2c32;
outline: 2px solid var(--background-color-solid);
position: absolute;
bottom: 0;
right: 0;
&.Active {
background-color: #44e5c6;
background-color: #1d9419;
}
}
}
.details {
max-width: 144px;
.input-value{
.input-value {
max-width: 120px;
}
.employee-id {

View File

@@ -45,6 +45,7 @@
@use "pages/realTimeViz";
@use "pages/userAuth";
@use "pages/forgotPassword";
@use "pages/pageNotFound.scss";
//
@use "./scene/scene";

View File

@@ -239,38 +239,6 @@
}
}
.kebab-options-wrapper {
position: absolute;
bottom: 40px;
right: 40px;
background: var(--background-color);
border: 1px solid var(--border-color);
border-radius: 8px;
z-index: 100;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(20px);
flex-direction: column;
transform: translate(100%, 100%);
overflow: hidden;
display: none;
.option {
padding: 8px 12px;
font-size: 14px;
text-align: left;
background: transparent;
border: none;
color: var(--text-color);
cursor: pointer;
transition: background 0.2s ease;
text-transform: capitalize;
&:hover {
background-color: var(--background-color-selected);
}
}
}
&:hover {
overflow: visible;
@@ -337,4 +305,37 @@
font-family: #{$font-roboto};
cursor: pointer;
}
}
}
.kebab-options-wrapper {
// position: absolute;
// bottom: 40px;
// right: 40px;
// z-index: 100;
background: var(--background-color);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(20px);
flex-direction: column;
// transform: translate(100%, 100%);
overflow: hidden;
display: flex;
flex-direction: column;
transform: translate(0%, 0%);
.option {
padding: 8px 12px;
font-size: 14px;
text-align: left;
background: transparent;
border: none;
color: var(--text-color);
cursor: pointer;
transition: background 0.2s ease;
text-transform: capitalize;
&:hover {
background-color: var(--background-color-selected);
}
}
}

Some files were not shown because too many files have changed in this diff Show More