Compare commits
30 Commits
decal-list
...
feature/pr
| Author | SHA1 | Date | |
|---|---|---|---|
| c60f15db13 | |||
| 6da82895b7 | |||
| b44111a620 | |||
| 9825c3ef12 | |||
| 3bc0e28267 | |||
| f9d314b69f | |||
| b956ed57e8 | |||
| df36ee0366 | |||
| ef9c3a9c63 | |||
| 62ddc1c25f | |||
| 1b161b2176 | |||
| a14f7fcf6a | |||
| b6783f99d3 | |||
| b2311ab186 | |||
| c9536a13e0 | |||
| e23e339ed3 | |||
| 6182862296 | |||
| a0e5115c6c | |||
| 5117e48527 | |||
| 1d2a42b7bd | |||
| 6fa4d5323d | |||
| 6026c3b82b | |||
| 358ce22767 | |||
| 7b5486590a | |||
| fe95ea8d0b | |||
| d090b976b0 | |||
| 7fb83417be | |||
| b623a92b9c | |||
| 3f808f167d | |||
| 5e025224d6 |
@@ -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>
|
||||
|
||||
9
app/src/assets/image/404/404.svg
Normal 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 |
BIN
app/src/assets/image/404/404_bk.png
Normal file
|
After Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 35 KiB |
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
|
||||
177
app/src/components/layout/sidebarLeft/assetList/Assets.tsx
Normal 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;
|
||||
@@ -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 /> },
|
||||
];
|
||||
@@ -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 [];
|
||||
}
|
||||
};
|
||||
@@ -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 [];
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
0
app/src/functions/findShortestPath.ts
Normal 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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]]}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -199,6 +199,7 @@ const VehicleUI = () => {
|
||||
steeringAngle: steeringRotation[1],
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 we’re 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]);
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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]);
|
||||
};
|
||||
428
app/src/modules/simulation/vehicle/pathCreator/pathCreator.tsx
Normal 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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
189
app/src/modules/simulation/vehicle/pathCreator/pathHandler.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
720
app/src/modules/simulation/vehicle/pathCreator/pointHandler.tsx
Normal 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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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]);
|
||||
@@ -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 [];
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export function handleContextMenu(
|
||||
evt: MouseEvent,
|
||||
setCurrentTempPath: (val: any[]) => void
|
||||
) {
|
||||
evt.preventDefault();
|
||||
setCurrentTempPath([]);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export function handleMouseMove(
|
||||
isLeftClickDown: { current: boolean },
|
||||
isDragging: { current: boolean }
|
||||
) {
|
||||
if (isLeftClickDown.current) isDragging.current = true;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export function handleMouseUp(
|
||||
evt: MouseEvent,
|
||||
isLeftClickDown: { current: boolean }
|
||||
) {
|
||||
if (evt.button === 0) isLeftClickDown.current = false;
|
||||
}
|
||||
@@ -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([]);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
57
app/src/pages/PageNotFound.tsx
Normal 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;
|
||||
@@ -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("/");
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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");
|
||||
|
||||
}
|
||||
};
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -45,6 +45,7 @@
|
||||
@use "pages/realTimeViz";
|
||||
@use "pages/userAuth";
|
||||
@use "pages/forgotPassword";
|
||||
@use "pages/pageNotFound.scss";
|
||||
|
||||
//
|
||||
@use "./scene/scene";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||