Merge pull request 'dev-contextMenu' (#1) from dev-contextMenu into main-demo

Reviewed-on: http://185.100.212.76:7778/Dwinzo-Beta/Dwinzo_Demo/pulls/1
This commit was merged in pull request #1.
This commit is contained in:
2025-08-14 04:58:00 +00:00
20 changed files with 803 additions and 153 deletions

View File

@@ -160,3 +160,153 @@ export function RenameIcon() {
</svg>
);
}
export function FocusIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.31999 1.56H9.89999C10.0325 1.56 10.14 1.66745 10.14 1.8V4.14M10.14 7.5V9.9C10.14 10.0325 10.0325 10.14 9.89999 10.14H7.31999M4.55999 10.14H1.91999C1.78744 10.14 1.67999 10.0325 1.67999 9.9V7.5M1.67999 4.14V1.8C1.67999 1.66745 1.78744 1.56 1.91999 1.56H4.55999" stroke="white" stroke-linecap="round" />
<circle cx="6.00005" cy="5.87999" r="1.7" stroke="white" />
</svg>
);
}
export function TransformIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_55_63)">
<path d="M3 0.75C2.40326 0.75 1.83097 0.987053 1.40901 1.40901C0.987053 1.83097 0.75 2.40326 0.75 3C0.75 3.59674 0.987053 4.16903 1.40901 4.59099C1.83097 5.01295 2.40326 5.25 3 5.25C3.24134 5.24937 3.481 5.20991 3.7098 5.13314L3.28805 4.71141L4.79632 3.20316L4.94545 3.05402L5.22342 3.33199C5.24047 3.22214 5.24935 3.11117 5.25 3C5.25 2.40326 5.01295 1.83097 4.59099 1.40901C4.16903 0.987053 3.59674 0.75 3 0.75ZM4.94545 3.65062L3.88467 4.71141L5.92336 6.75L5.37333 7.30001L8.07427 7.84017L7.53403 5.13923L6.98405 5.68922L4.94545 3.65062ZM8.28647 6.75L8.61202 8.37797L6.75 8.00555V11.25H11.25V6.75H8.28645H8.28647Z" fill="#FCFDFD" />
</g>
<defs>
<clipPath id="clip0_55_63">
<rect width="12" height="12" fill="white" />
</clipPath>
</defs>
</svg>
);
}
export function DublicateIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_190)">
<path d="M9 1.5H2C1.72386 1.5 1.5 1.72386 1.5 2V9C1.5 9.27615 1.27614 9.5 1 9.5C0.72386 9.5 0.5 9.27615 0.5 9V2C0.5 1.17158 1.17158 0.5 2 0.5H9C9.27615 0.5 9.5 0.72386 9.5 1C9.5 1.27614 9.27615 1.5 9 1.5Z" fill="white" />
<path d="M6.5 5.5C6.5 5.22385 6.72385 5 7 5C7.27615 5 7.5 5.22385 7.5 5.5V6.5H8.5C8.77615 6.5 9 6.72385 9 7C9 7.27615 8.77615 7.5 8.5 7.5H7.5V8.5C7.5 8.77615 7.27615 9 7 9C6.72385 9 6.5 8.77615 6.5 8.5V7.5H5.5C5.22385 7.5 5 7.27615 5 7C5 6.72385 5.22385 6.5 5.5 6.5H6.5V5.5Z" fill="white" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 2.5C10.8285 2.5 11.5 3.17158 11.5 4V10C11.5 10.8285 10.8285 11.5 10 11.5H4C3.17158 11.5 2.5 10.8285 2.5 10V4C2.5 3.17158 3.17158 2.5 4 2.5H10ZM10 3.5C10.2761 3.5 10.5 3.72386 10.5 4V10C10.5 10.2761 10.2761 10.5 10 10.5H4C3.72386 10.5 3.5 10.2761 3.5 10V4C3.5 3.72386 3.72386 3.5 4 3.5H10Z" fill="white" />
</g>
<defs>
<clipPath id="clip0_1_190">
<rect width="12" height="12" fill="white" />
</clipPath>
</defs>
</svg>
);
}
export function CopyIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_197)">
<path d="M4.375 1.5H8.875C9.22018 1.5 9.5 1.77982 9.5 2.125V5.07422C9.25749 5.02497 9.00651 5 8.75 5C6.67893 5 5 6.67893 5 8.75C5 8.91951 5.01426 9.08616 5.03613 9.25H4.375C4.02982 9.25 3.75 8.97018 3.75 8.625V2.125C3.75 1.77982 4.02982 1.5 4.375 1.5Z" stroke="white" />
<path d="M7.02181 10.8891C5.8404 9.93469 5.6564 8.20324 6.61085 7.02182C7.56529 5.84041 9.29675 5.65641 10.4782 6.61086C11.6596 7.5653 11.8436 9.29676 10.8891 10.4782C9.93468 11.6596 8.20322 11.8436 7.02181 10.8891ZM7.53035 9.63652C7.55951 9.73588 7.64818 9.80729 7.7514 9.81511L7.79716 9.81441L7.84067 9.80562C7.94019 9.77642 8.01223 9.68724 8.01987 9.58381L8.01932 9.53942L7.89568 8.38246L9.76272 9.88956L9.80012 9.91475C9.89114 9.96447 10.0045 9.95243 10.083 9.88469L10.1143 9.8522L10.1395 9.8148C10.1892 9.72378 10.1772 9.61043 10.1094 9.53189L10.0769 9.50062L8.20913 7.99291L9.36823 7.86974L9.41174 7.86095C9.52542 7.82759 9.60327 7.71674 9.59039 7.59475C9.57742 7.47272 9.47859 7.38002 9.36039 7.37128L9.3154 7.37259L7.5357 7.5631L7.50592 7.57044L7.46405 7.58808L7.42779 7.61277L7.40435 7.63401L7.37612 7.66895L7.36028 7.69633L7.35134 7.71672L7.33894 7.75692L7.33456 7.781L7.33441 7.81227L7.52217 9.59225L7.53035 9.63652Z" fill="white" />
</g>
<defs>
<clipPath id="clip0_1_197">
<rect width="12" height="12" fill="white" />
</clipPath>
</defs>
</svg>
);
}
export function PasteIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_207)">
<path d="M4.375 1.5H8.875C9.22018 1.5 9.5 1.77982 9.5 2.125V5.07422C9.25749 5.02497 9.00651 5 8.75 5C6.67893 5 5 6.67893 5 8.75C5 8.91951 5.01426 9.08616 5.03613 9.25H4.375C4.02982 9.25 3.75 8.97018 3.75 8.625V2.125C3.75 1.77982 4.02982 1.5 4.375 1.5Z" stroke="white" />
<path d="M10.4408 6.58164C11.6383 7.51587 11.8516 9.24395 10.9174 10.4414C9.9832 11.6389 8.25512 11.8523 7.05765 10.918C5.86019 9.98382 5.64679 8.25574 6.58102 7.05828C7.51524 5.86081 9.24332 5.64742 10.4408 6.58164ZM9.95361 7.84272C9.92276 7.74387 9.83289 7.67398 9.72956 7.66791L9.68382 7.66939L9.64046 7.67892C9.54146 7.7098 9.47094 7.8002 9.46506 7.90374L9.46636 7.94811L9.60965 9.10281L7.71726 7.62766L7.67944 7.60311C7.58759 7.55494 7.47446 7.56891 7.39709 7.63798L7.36637 7.67099L7.34182 7.70881C7.29366 7.80066 7.30763 7.91379 7.37669 7.99117L7.4097 8.02188L9.30286 9.49763L8.14603 9.64048L8.10268 9.65001C7.98957 9.6853 7.91362 9.79746 7.92858 9.91921C7.94362 10.041 8.044 10.132 8.16233 10.1387L8.2073 10.1367L9.9835 9.91593L10.0131 9.90809L10.0547 9.88974L10.0906 9.86444L10.1136 9.8428L10.1413 9.80739L10.1566 9.77974L10.1652 9.7592L10.1769 9.71879L10.1809 9.69464L10.1805 9.66338L9.96254 7.88684L9.95361 7.84272Z" fill="white" />
</g>
<defs>
<clipPath id="clip0_1_207">
<rect width="12" height="12" fill="white" />
</clipPath>
</defs>
</svg>
);
}
export function ModifiersIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 4.5V3.23607C2 3.08082 2.03615 2.92771 2.10558 2.78886L2.5 2H5L5.5 3H10.5C10.7761 3 11 3.22386 11 3.5V4.5V9C11 9.5523 10.5523 10 10 10H9" stroke="white" stroke-linecap="round" stroke-linejoin="round" />
<path d="M8.61805 4.5H1.15458C0.824894 4.5 0.585449 4.81349 0.672199 5.13155L1.79898 9.2631C1.91764 9.6982 2.3128 10 2.76375 10H9.84535C10.175 10 10.4145 9.6865 10.3277 9.36845L9.10045 4.86844C9.0411 4.65091 8.84355 4.5 8.61805 4.5Z" stroke="white" />
</svg>
);
}
export function DeleteIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_241)">
<path d="M4.70588 5.32353V9.02941M7.17647 5.32353V9.02941M9.64706 2.85294V10.2647C9.64706 10.947 9.09402 11.5 8.41177 11.5H3.47059C2.78835 11.5 2.23529 10.947 2.23529 10.2647V2.85294M1 2.85294H10.8824M7.79412 2.85294V2.23529C7.79412 1.55306 7.24108 1 6.55882 1H5.32353C4.6413 1 4.08824 1.55306 4.08824 2.23529V2.85294" stroke="white" stroke-linecap="round" stroke-linejoin="round" />
</g>
<defs>
<clipPath id="clip0_1_241">
<rect width="12" height="12" fill="white" />
</clipPath>
</defs>
</svg>
);
}
export function MoveIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_241)">
<path d="M4.70588 5.32353V9.02941M7.17647 5.32353V9.02941M9.64706 2.85294V10.2647C9.64706 10.947 9.09402 11.5 8.41177 11.5H3.47059C2.78835 11.5 2.23529 10.947 2.23529 10.2647V2.85294M1 2.85294H10.8824M7.79412 2.85294V2.23529C7.79412 1.55306 7.24108 1 6.55882 1H5.32353C4.6413 1 4.08824 1.55306 4.08824 2.23529V2.85294" stroke="white" stroke-linecap="round" stroke-linejoin="round" />
</g>
<defs>
<clipPath id="clip0_1_241">
<rect width="12" height="12" fill="white" />
</clipPath>
</defs>
</svg>
);
}
export function RotateIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.99998 1.31111H4.7251L3.88539 0.471406L4.3568 0L6.00124 1.64444L4.37502 3.27067L3.90361 2.79927L4.72511 1.97777H3.99998C2.89541 1.97777 1.99998 2.87321 1.99998 3.97777H1.33331C1.33331 2.50501 2.52722 1.31111 3.99998 1.31111ZM3.99998 5.33333C3.99998 4.59696 4.59693 4 5.33331 4H10.6667C11.4031 4 12 4.59696 12 5.33333V10.6667C12 11.4031 11.4031 12 10.6667 12H5.33331C4.59693 12 3.99998 11.4031 3.99998 10.6667V5.33333ZM5.33331 4.66667H10.6667C11.0349 4.66667 11.3333 4.96514 11.3333 5.33333V10.6667C11.3333 11.0349 11.0349 11.3333 10.6667 11.3333H5.33331C4.96513 11.3333 4.66664 11.0349 4.66664 10.6667V5.33333C4.66664 4.96514 4.96513 4.66667 5.33331 4.66667Z" fill="#FCFDFD" />
</svg>
);
}
export function GroupIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.99998 1.31111H4.7251L3.88539 0.471406L4.3568 0L6.00124 1.64444L4.37502 3.27067L3.90361 2.79927L4.72511 1.97777H3.99998C2.89541 1.97777 1.99998 2.87321 1.99998 3.97777H1.33331C1.33331 2.50501 2.52722 1.31111 3.99998 1.31111ZM3.99998 5.33333C3.99998 4.59696 4.59693 4 5.33331 4H10.6667C11.4031 4 12 4.59696 12 5.33333V10.6667C12 11.4031 11.4031 12 10.6667 12H5.33331C4.59693 12 3.99998 11.4031 3.99998 10.6667V5.33333ZM5.33331 4.66667H10.6667C11.0349 4.66667 11.3333 4.96514 11.3333 5.33333V10.6667C11.3333 11.0349 11.0349 11.3333 10.6667 11.3333H5.33331C4.96513 11.3333 4.66664 11.0349 4.66664 10.6667V5.33333C4.66664 4.96514 4.96513 4.66667 5.33331 4.66667Z" fill="#FCFDFD" />
</svg>
);
}
export function ArrayIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1.09998" y="0.5" width="1.66667" height="5.66667" rx="0.5" stroke="white" />
<rect x="5.09998" y="3.16797" width="1.66667" height="5.66667" rx="0.5" stroke="white" />
<rect x="9.09998" y="5.83203" width="1.66667" height="5.66667" rx="0.5" stroke="white" />
</svg>
);
}
export function SubMenuIcon() {
return (
<svg width="4" height="6" viewBox="0 0 4 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.5 6V0L4 3L0.5 6Z" fill="white" />
</svg>
);
}

View File

@@ -3,6 +3,7 @@ import {
useLoadingProgress,
useRenameModeStore,
useSaveVersion,
useSelectedAssets,
useSelectedComment,
useSelectedFloorItem,
useSocketStore,
@@ -58,6 +59,7 @@ function MainScene() {
const { setFloatingWidget } = useFloatingWidget();
const { clearComparisonProduct } = useComparisonProduct();
const { selectedFloorItem, setSelectedFloorItem } = useSelectedFloorItem();
const { selectedAssets,setSelectedAssets } = useSelectedAssets();
const { assetStore, productStore } = useSceneContext();
const { products } = productStore();
const { setName } = assetStore();
@@ -97,18 +99,40 @@ function MainScene() {
const handleObjectRename = async (newName: string) => {
if (!projectId) return
let response = await setAssetsApi({
if (selectedFloorItem) {
console.log('selectedFloorItem.userData.modelUuid: ', selectedFloorItem.userData.modelUuid);
console.log(' newName: ', newName);
console.log('projectId: ', projectId);
setAssetsApi({
modelUuid: selectedFloorItem.userData.modelUuid,
modelName: newName,
projectId
});
projectId,
versionId: selectedVersion?.versionId || ''
}).then(() => {
selectedFloorItem.userData = {
...selectedFloorItem.userData,
modelName: newName
};
setSelectedFloorItem(selectedFloorItem);
setIsRenameMode(false);
setName(selectedFloorItem.userData.modelUuid, response.modelName);
setName(selectedFloorItem.userData.modelUuid, newName);
})
} else if (selectedAssets.length === 1) {
setAssetsApi({
modelUuid: selectedAssets[0].userData.modelUuid,
modelName: newName,
projectId,
versionId: selectedVersion?.versionId || ''
}).then(() => {
selectedAssets[0].userData = {
...selectedAssets[0].userData,
modelName: newName
};
setSelectedAssets(selectedAssets);
setIsRenameMode(false);
setName(selectedAssets[0].userData.modelUuid, newName);
})
}
}
return (
@@ -135,7 +159,7 @@ function MainScene() {
{(isPlaying) &&
activeModule !== "simulation" && <ControlsPlayer />}
{isRenameMode && selectedFloorItem?.userData.modelName && <RenameTooltip name={selectedFloorItem?.userData.modelName} onSubmit={handleObjectRename} />}
{isRenameMode && (selectedFloorItem?.userData.modelName || selectedAssets.length === 1) && <RenameTooltip name={selectedFloorItem?.userData.modelName || selectedAssets[0].userData.modelName} onSubmit={handleObjectRename} />}
{/* remove this later */}
{activeModule === "builder" && !toggleThreeD && <SelectFloorPlan />}
</>

View File

@@ -1,23 +1,62 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import Search from "../../ui/inputs/Search";
import DropDownList from "../../ui/list/DropDownList";
import { useSceneContext } from "../../../modules/scene/sceneContext";
import { isPointInsidePolygon } from "../../../functions/isPointInsidePolygon";
interface ZoneData {
id: string;
name: string;
assets: { id: string; name: string; position?: []; rotation?: {} }[];
}
const Outline: React.FC = () => {
const [searchValue, setSearchValue] = useState<string>("");
const [zoneDataList, setZoneDataList] = useState<ZoneData[]>([]);
const [buildingsList, setBuildingsList] = useState<{ id: string; name: string }[]>([]);
const [isLayersOpen, setIsLayersOpen] = useState(true);
const [isBuildingsOpen, setIsBuildingsOpen] = useState(false);
const [isZonesOpen, setIsZonesOpen] = useState(false);
const { assetStore, zoneStore } = useSceneContext();
const { assets } = assetStore();
const { zones } = zoneStore();
useEffect(() => {
const updatedZoneList: ZoneData[] = zones?.map((zone: any) => {
const polygon2D = zone.points.map((p: any) => [p.position[0], p.position[2]]);
const assetsInZone = assets.filter((item: any) => {
const [x, , z] = item.position;
return isPointInsidePolygon([x, z], polygon2D as [number, number][]);
})
.map((item: any) => ({
id: item.modelUuid,
name: item.modelName,
position: item.position,
rotation: item.rotation,
}));
return {
id: zone.zoneUuid,
name: zone.zoneName,
assets: assetsInZone,
};
});
setZoneDataList(updatedZoneList);
}, [zones, assets]);
const handleSearchChange = (value: string) => {
setSearchValue(value);
// console.log(value); // Log the search value if needed
};
const dropdownItems = [
{ id: "1", name: "Ground Floor", active: true },
// { id: "2", name: "Floor 1" },
]; // Example dropdown items
const dropdownItems = [{ id: "1", name: "Ground Floor" }];
return (
<div className="outline-container">
<Search onChange={handleSearchChange} />
{searchValue ? (
<div className="searched-content">
<p>Results for "{searchValue}"</p>
@@ -28,7 +67,8 @@ const Outline: React.FC = () => {
<DropDownList
value="Layers"
items={dropdownItems}
defaultOpen={true}
isOpen={isLayersOpen}
onToggle={() => setIsLayersOpen((prev) => !prev)}
showKebabMenu={false}
showFocusIcon={true}
remove
@@ -36,10 +76,18 @@ const Outline: React.FC = () => {
</section>
<section className="outline-section overflow">
<DropDownList
value="Scene"
items={dropdownItems}
defaultOpen={true}
listType="outline"
value="Buildings"
items={buildingsList}
isOpen={isBuildingsOpen}
onToggle={() => setIsBuildingsOpen((prev) => !prev)}
showKebabMenu={false}
showAddIcon={false}
/>
<DropDownList
value="Zones"
items={zoneDataList}
isOpen={isZonesOpen}
onToggle={() => setIsZonesOpen((prev) => !prev)}
showKebabMenu={false}
showAddIcon={false}
/>

View File

@@ -42,11 +42,11 @@ const ZoneProperties: React.FC = () => {
let response = await zoneCameraUpdate(zonesdata, organization, projectId, selectedVersion?.versionId || "");
// console.log('response: ', response);
//
if (response.message === "zone updated") {
setEdit(false);
} else {
// console.log(response);
//
}
} catch (error) {
echo.error("Failed to set zone view");
@@ -75,7 +75,7 @@ const ZoneProperties: React.FC = () => {
// )
// );
} else {
// console.log(response?.message);
//
}
}
function handleVectorChange(
@@ -85,7 +85,7 @@ const ZoneProperties: React.FC = () => {
setSelectedZone((prev) => ({ ...prev, [key]: newValue }));
}
const checkZoneNameDuplicate = (name: string) => {
console.log('zones: ', zones);
return zones.some(
(zone: any) =>
zone.zoneName?.trim().toLowerCase() === name?.trim().toLowerCase() &&

View File

@@ -1,30 +1,18 @@
import React, { useEffect, useState } from "react";
import React from "react";
import List from "./List";
import { AddIcon, ArrowIcon, FocusIcon } from "../../icons/ExportCommonIcons";
import KebabMenuListMultiSelect from "./KebebMenuListMultiSelect";
import { useSceneContext } from "../../../modules/scene/sceneContext";
interface DropDownListProps {
value?: string; // Value to display in the DropDownList
items?: { id: string; name: string }[]; // Items to display in the dropdown list
showFocusIcon?: boolean; // Determines if the FocusIcon should be displayed
showAddIcon?: boolean; // Determines if the AddIcon should be displayed
showKebabMenu?: boolean; // Determines if the KebabMenuList should be displayed
kebabMenuItems?: { id: string; name: string }[]; // Items for the KebabMenuList
defaultOpen?: boolean; // Determines if the dropdown list should be open by default
listType?: string; // Type of list to display
value?: string;
items?: { id: string; name: string }[];
showFocusIcon?: boolean;
showAddIcon?: boolean;
showKebabMenu?: boolean;
kebabMenuItems?: { id: string; name: string }[];
remove?: boolean;
}
interface Zone {
zoneUuid: string;
zoneName: string;
points: [number, number, number][]; // polygon vertices
}
interface ZoneData {
id: string;
name: string;
assets: { id: string; name: string; position?: []; rotation?: {} }[];
isOpen: boolean;
onToggle: () => void;
}
const DropDownList: React.FC<DropDownListProps> = ({
@@ -38,76 +26,13 @@ const DropDownList: React.FC<DropDownListProps> = ({
{ id: "Paths", name: "Paths" },
{ id: "Zones", name: "Zones" },
],
defaultOpen = false,
listType = "default",
remove,
isOpen,
onToggle,
}) => {
const [isOpen, setIsOpen] = useState<boolean>(defaultOpen);
const handleToggle = () => {
setIsOpen((prev) => !prev); // Toggle the state
};
const [zoneDataList, setZoneDataList] = useState<ZoneData[]>([]);
const { assetStore, zoneStore } = useSceneContext();
const { assets } = assetStore();
const { zones } = zoneStore()
const isPointInsidePolygon = (
point: [number, number],
polygon: [number, number][]
) => {
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i][0],
zi = polygon[i][1];
const xj = polygon[j][0],
zj = polygon[j][1];
const intersect =
// eslint-disable-next-line no-mixed-operators
zi > point[1] !== zj > point[1] &&
point[0] < ((xj - xi) * (point[1] - zi)) / (zj - zi + 0.000001) + xi;
if (intersect) inside = !inside;
}
return inside;
};
useEffect(() => {
const updatedZoneList: ZoneData[] = zones?.map((zone: any) => {
const polygon2D = zone.points.map((p: [number, number, number]) => [
p[0],
p[2],
]);
const assetsInZone = assets
.filter((item: any) => {
const [x, , z] = item.position;
return isPointInsidePolygon([x, z], polygon2D as [number, number][]);
})
.map((item: any) => ({
id: item.modelUuid,
name: item.modelName,
position: item.position,
rotation: item.rotation,
}));
return {
id: zone.zoneUuid,
name: zone.zoneName,
assets: assetsInZone,
};
});
setZoneDataList(updatedZoneList);
}, [zones, assets]);
return (
<div className="dropdown-list-container">
{/* eslint-disable-next-line */}
<div className="head" onClick={handleToggle}>
<div className="head" onClick={onToggle}>
<div className="value">{value}</div>
<div className="options">
{showFocusIcon && (
@@ -130,31 +55,15 @@ const DropDownList: React.FC<DropDownListProps> = ({
title="collapse-btn"
className="collapse-icon option"
style={{ transform: isOpen ? "rotate(0deg)" : "rotate(-90deg)" }}
// onClick={handleToggle}
>
<ArrowIcon />
</button>
</div>
</div>
{isOpen && (
<div className="lists-container">
{listType === "default" && <List items={items} remove={remove} />}
{listType === "outline" && (
<>
<DropDownList
value="Buildings"
showKebabMenu={false}
showAddIcon={false}
// items={zoneDataList}
/>
<DropDownList
value="Zones"
showKebabMenu={false}
showAddIcon={false}
items={zoneDataList}
/>
</>
)}
<List items={items} remove={remove} />
</div>
)}
</div>

View File

@@ -136,7 +136,6 @@ const List: React.FC<ListProps> = ({ items = [], remove }) => {
}
async function handleZoneAssetName(newName: string) {
if (zoneAssetId?.id) {
let response = await setAssetsApi({
modelUuid: zoneAssetId.id,

View File

@@ -0,0 +1,187 @@
import React from "react";
import { ArrayIcon, CopyIcon, DeleteIcon, DublicateIcon, FlipXAxisIcon, FlipZAxisIcon, FocusIcon, GroupIcon, ModifiersIcon, MoveIcon, PasteIcon, RenameIcon, RotateIcon, SubMenuIcon, TransformIcon } from "../../icons/ContextMenuIcons";
type ContextMenuProps = {
visibility: {
rename: boolean;
focus: boolean;
flipX: boolean;
flipZ: boolean;
move: boolean;
rotate: boolean;
duplicate: boolean;
copy: boolean;
paste: boolean;
modifier: boolean;
group: boolean;
array: boolean;
delete: boolean;
};
onRename: () => void;
onFocus: () => void;
onFlipX: () => void;
onFlipZ: () => void;
onMove: () => void;
onRotate: () => void;
onDuplicate: () => void;
onCopy: () => void;
onPaste: () => void;
onGroup: () => void;
onArray: () => void;
onDelete: () => void;
};
const ContextMenu: React.FC<ContextMenuProps> = ({
visibility,
onRename,
onFocus,
onFlipX,
onFlipZ,
onMove,
onRotate,
onDuplicate,
onCopy,
onPaste,
onGroup,
onArray,
onDelete,
}) => {
return (
<div className="context-menu">
{visibility.rename && (
<div className="menuItem">
<button className="button" onClick={onRename}>
<div className="icon"><RenameIcon /></div>
<span>Rename</span>
</button>
<span className="shortcut">F2</span>
</div>
)}
{visibility.focus && (
<div className="menuItem">
<button className="button" onClick={onFocus}>
<div className="icon"><FocusIcon /></div>
<span>Focus</span>
</button>
<span className="shortcut">F</span>
</div>
)}
{visibility.flipX && (
<div className="menuItem">
<button className="button" onClick={onFlipX}>
<div className="icon"><FlipXAxisIcon /></div>
<span>Flip to X axis</span>
</button>
</div>
)}
{visibility.flipZ && (
<div className="menuItem">
<button className="button" onClick={onFlipZ}>
<div className="icon"><FlipZAxisIcon /></div>
<span>Flip to Z axis</span>
</button>
</div>
)}
{(visibility.move || visibility.rotate) && (
<div className="menuItem">
<button className="button">
<div className="icon"><TransformIcon /></div>
<span>Transform</span>
</button>
<div className="more"><SubMenuIcon /></div>
<div className="submenu">
{visibility.move && (
<div className="menuItem">
<button className="button" onClick={onMove}>
<div className="icon"><MoveIcon /></div>
<span>Move</span>
</button>
<span className="shortcut">G</span>
</div>
)}
{visibility.rotate && (
<div className="menuItem">
<button className="button" onClick={onRotate}>
<div className="icon"><RotateIcon /></div>
<span>Rotate</span>
</button>
<span className="shortcut">R</span>
</div>
)}
</div>
</div>
)}
{visibility.duplicate && (
<div className="menuItem">
<button className="button" onClick={onDuplicate}>
<div className="icon"><DublicateIcon /></div>
<span>Duplicate</span>
</button>
<span className="shortcut">Ctrl + D</span>
</div>
)}
{visibility.copy && (
<div className="menuItem">
<button className="button" onClick={onCopy}>
<div className="icon"><CopyIcon /></div>
<span>Copy Objects</span>
</button>
<span className="shortcut">Ctrl + C</span>
</div>
)}
{visibility.paste && (
<div className="menuItem">
<button className="button" onClick={onPaste}>
<div className="icon"><PasteIcon /></div>
<span>Paste Objects</span>
</button>
<span className="shortcut">Ctrl + V</span>
</div>
)}
{visibility.modifier && (
<div className="menuItem">
<div className="icon"><ModifiersIcon /></div>
<button className="button">Modifiers</button>
</div>
)}
{(visibility.group || visibility.array) && (
<div className="menuItem">
<button className="button">Group / Array</button>
<div className="submenu">
{visibility.group && (
<div className="menuItem">
<button className="button" onClick={onGroup}>
<GroupIcon />
<span>Group</span>
</button>
<span className="shortcut">Ctrl + G</span>
</div>
)}
{visibility.array && (
<div className="menuItem">
<button className="button" onClick={onArray}>
<div className="icon"><ArrayIcon /></div>
<span>Array</span>
</button>
</div>
)}
</div>
</div>
)}
{visibility.delete && (
<div className="menuItem">
<button className="button" onClick={onDelete}>
<div className="icon"><DeleteIcon /></div>
<span>Delete</span>
</button>
<span className="shortcut">X</span>
</div>
)}
</div>
);
};
export default ContextMenu;

View File

@@ -0,0 +1,20 @@
export const isPointInsidePolygon = (
point: [number, number],
polygon: [number, number][]
) => {
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i][0],
zi = polygon[i][1];
const xj = polygon[j][0],
zj = polygon[j][1];
const intersect =
// eslint-disable-next-line no-mixed-operators
zi > point[1] !== zj > point[1] &&
point[0] < ((xj - xi) * (point[1] - zi)) / (zj - zi + 0.000001) + xi;
if (intersect) inside = !inside;
}
return inside;
};

View File

@@ -0,0 +1,194 @@
import { useEffect, useState } from 'react';
import { useThree } from '@react-three/fiber';
import { CameraControls, Html, ScreenSpace } from '@react-three/drei';
import { useContextActionStore, useRenameModeStore, useSelectedAssets } from '../../../../store/builder/store';
import ContextMenu from '../../../../components/ui/menu/contextMenu';
function ContextControls() {
const { gl, controls } = useThree();
const [canRender, setCanRender] = useState(false);
const [visibility, setVisibility] = useState({ rename: true, focus: true, flipX: true, flipZ: true, move: true, rotate: true, duplicate: true, copy: true, paste: true, modifier: false, group: false, array: false, delete: true, });
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
const { selectedAssets } = useSelectedAssets();
const { setContextAction } = useContextActionStore();
const { setIsRenameMode } = useRenameModeStore();
useEffect(() => {
if (selectedAssets.length === 1) {
setVisibility({
rename: true,
focus: true,
flipX: true,
flipZ: true,
move: true,
rotate: true,
duplicate: true,
copy: true,
paste: true,
modifier: false,
group: false,
array: false,
delete: true,
});
} else if (selectedAssets.length > 1) {
setVisibility({
rename: false,
focus: true,
flipX: true,
flipZ: true,
move: true,
rotate: true,
duplicate: true,
copy: true,
paste: true,
modifier: false,
group: true,
array: false,
delete: true,
});
} else {
setVisibility({
rename: false,
focus: false,
flipX: false,
flipZ: false,
move: false,
rotate: false,
duplicate: false,
copy: false,
paste: false,
modifier: false,
group: false,
array: false,
delete: false,
});
}
}, [selectedAssets]);
useEffect(() => {
const canvasElement = gl.domElement;
const handleContextClick = (event: MouseEvent) => {
event.preventDefault();
if (selectedAssets.length > 0) {
setMenuPosition({ x: event.clientX - gl.domElement.width / 2, y: event.clientY - gl.domElement.height / 2 });
setCanRender(true);
if (controls) {
(controls as CameraControls).enabled = false;
}
} else {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
}
};
if (selectedAssets.length > 0) {
canvasElement.addEventListener('contextmenu', handleContextClick)
} else {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
setMenuPosition({ x: 0, y: 0 });
}
return () => {
canvasElement.removeEventListener('contextmenu', handleContextClick);
};
}, [gl, selectedAssets]);
const handleAssetRename = () => {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
setContextAction("renameAsset");
setIsRenameMode(true);
}
const handleAssetFocus = () => {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
setContextAction("focusAsset");
}
const handleAssetMove = () => {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
setContextAction("moveAsset")
}
const handleAssetRotate = () => {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
setContextAction("rotateAsset")
}
const handleAssetCopy = () => {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
setContextAction("copyAsset")
}
const handleAssetPaste = () => {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
setContextAction("pasteAsset")
}
const handleAssetDelete = () => {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
setContextAction("deleteAsset")
}
const handleAssetDuplicate = () => {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
setContextAction("duplicateAsset")
}
return (
<>
{canRender && (
<ScreenSpace depth={1} >
<Html
style={{
position: 'fixed',
top: menuPosition.y,
left: menuPosition.x,
zIndex: 1000
}}
>
<ContextMenu
visibility={visibility}
onRename={() => handleAssetRename()}
onFocus={() => handleAssetFocus()}
onFlipX={() => console.log("Flip to X")}
onFlipZ={() => console.log("Flip to Z")}
onMove={() => handleAssetMove()}
onRotate={() => handleAssetRotate()}
onDuplicate={() => handleAssetDuplicate()}
onCopy={() => handleAssetCopy()}
onPaste={() => handleAssetPaste()}
onGroup={() => console.log("Group")}
onArray={() => console.log("Array")}
onDelete={() => handleAssetDelete()}
/>
</Html>
</ScreenSpace>
)}
</>
);
}
export default ContextControls;

View File

@@ -14,6 +14,7 @@ import TransformControl from "./transformControls/transformControls";
import { useParams } from "react-router-dom";
import { getUserData } from "../../../functions/getUserData";
import ContextControls from "./contextControls/contextControls";
import SelectionControls2D from "./selectionControls/selection2D/selectionControls2D";
import UndoRedo2DControls from "./undoRedoControls/undoRedo2D/undoRedo2DControls";
import UndoRedo3DControls from "./undoRedoControls/undoRedo3D/undoRedo3DControls";
@@ -149,6 +150,8 @@ export default function Controls() {
<TransformControl />
<ContextControls />
</>
);
}

View File

@@ -2,7 +2,7 @@ import * as THREE from "three";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { SkeletonUtils } from "three-stdlib";
import { useSelectedAssets, useSocketStore, useToggleView } from "../../../../../store/builder/store";
import { useContextActionStore, useSelectedAssets, useSocketStore, useToggleView } from "../../../../../store/builder/store";
import * as Types from "../../../../../types/world/worldTypes";
import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys";
import { useParams } from "react-router-dom";
@@ -41,6 +41,7 @@ const CopyPasteControls3D = ({
const [relativePositions, setRelativePositions] = useState<THREE.Vector3[]>([]);
const [centerOffset, setCenterOffset] = useState<THREE.Vector3 | null>(null);
const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ left: false, right: false, });
const { contextAction, setContextAction } = useContextActionStore()
const calculateRelativePositions = useCallback((objects: THREE.Object3D[]) => {
if (objects.length === 0) return { center: new THREE.Vector3(), relatives: [] };
@@ -58,6 +59,16 @@ const CopyPasteControls3D = ({
return { center, relatives };
}, []);
useEffect(() => {
if (contextAction === "copyAsset") {
setContextAction(null);
copySelection()
} else if (contextAction === "pasteAsset") {
setContextAction(null);
pasteCopiedObjects()
}
}, [contextAction])
useEffect(() => {
if (!camera || !scene || toggleView) return;
const canvasElement = gl.domElement;

View File

@@ -2,7 +2,7 @@ import * as THREE from "three";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { SkeletonUtils } from "three-stdlib";
import { useSelectedAssets, useSocketStore, useToggleView } from "../../../../../store/builder/store";
import { useContextActionStore, useSelectedAssets, useSocketStore, useToggleView } from "../../../../../store/builder/store";
import * as Types from "../../../../../types/world/worldTypes";
import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys";
import { useParams } from "react-router-dom";
@@ -39,12 +39,20 @@ const DuplicationControls3D = ({
const [initialPositions, setInitialPositions] = useState<Record<string, THREE.Vector3>>({});
const [isDuplicating, setIsDuplicating] = useState(false);
const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ left: false, right: false, });
const { contextAction, setContextAction } = useContextActionStore()
const calculateDragOffset = useCallback((point: THREE.Object3D, hitPoint: THREE.Vector3) => {
const pointPosition = new THREE.Vector3().copy(point.position);
return new THREE.Vector3().subVectors(pointPosition, hitPoint);
}, []);
useEffect(() => {
if (contextAction === "duplicateAsset") {
setContextAction(null);
duplicateSelection()
}
}, [contextAction])
useEffect(() => {
if (!camera || !scene || toggleView) return;
const canvasElement = gl.domElement;

View File

@@ -1,7 +1,7 @@
import * as THREE from "three";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { useSelectedAssets, useSocketStore, useToggleView, } from "../../../../../store/builder/store";
import { useContextActionStore, useSelectedAssets, useSocketStore, useToggleView, } from "../../../../../store/builder/store";
import * as Types from "../../../../../types/world/worldTypes";
import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys";
import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi";
@@ -48,6 +48,7 @@ function MoveControls3D({
const [initialStates, setInitialStates] = useState<Record<string, { position: THREE.Vector3; rotation?: THREE.Euler; }>>({});
const [isMoving, setIsMoving] = useState(false);
const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ left: false, right: false, });
const { contextAction, setContextAction } = useContextActionStore()
const updateBackend = (
productName: string,
@@ -64,6 +65,13 @@ function MoveControls3D({
});
};
useEffect(() => {
if (contextAction === "moveAsset") {
setContextAction(null);
moveAssets()
}
}, [contextAction])
useEffect(() => {
if (!camera || !scene || toggleView) return;

View File

@@ -1,7 +1,7 @@
import * as THREE from "three";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { useSelectedAssets, useSocketStore, useToggleView } from "../../../../../store/builder/store";
import { useContextActionStore, useSelectedAssets, useSocketStore, useToggleView } from "../../../../../store/builder/store";
import * as Types from "../../../../../types/world/worldTypes";
import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi";
import { useParams } from "react-router-dom";
@@ -43,10 +43,8 @@ function RotateControls3D({
const [isRotating, setIsRotating] = useState(false);
const prevPointerPosition = useRef<THREE.Vector2 | null>(null);
const rotationCenter = useRef<THREE.Vector3 | null>(null);
const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({
left: false,
right: false,
});
const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ left: false, right: false, });
const { contextAction, setContextAction } = useContextActionStore()
const updateBackend = useCallback((
productName: string,
@@ -63,6 +61,13 @@ function RotateControls3D({
});
}, [selectedVersion]);
useEffect(() => {
if (contextAction === "rotateAsset") {
setContextAction(null);
rotateAssets()
}
}, [contextAction])
useEffect(() => {
if (!camera || !scene || toggleView) return;

View File

@@ -10,7 +10,7 @@ import { getUserData } from "../../../../../functions/getUserData";
import { useSceneContext } from "../../../sceneContext";
import { useVersionContext } from "../../../../builder/version/versionContext";
import { useProductContext } from "../../../../simulation/products/productContext";
import { useSelectedAssets, useSocketStore, useToggleView, useToolMode, } from "../../../../../store/builder/store";
import { useContextActionStore, useSelectedAssets, useSocketStore, useToggleView, useToolMode, } from "../../../../../store/builder/store";
import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi";
import DuplicationControls3D from "./duplicationControls3D";
import CopyPasteControls3D from "./copyPasteControls3D";
@@ -31,6 +31,7 @@ const SelectionControls3D: React.FC = () => {
const boundingBoxRef = useRef<THREE.Mesh>();
const { activeModule } = useModuleStore();
const { socket } = useSocketStore();
const { contextAction, setContextAction } = useContextActionStore()
const { assetStore, eventStore, productStore, undoRedo3DStore } = useSceneContext();
const { push3D } = undoRedo3DStore();
const { removeAsset, getAssetById } = assetStore();
@@ -66,6 +67,13 @@ const SelectionControls3D: React.FC = () => {
});
};
useEffect(() => {
if (contextAction === "deleteAsset") {
setContextAction(null);
deleteSelection()
}
}, [contextAction])
useEffect(() => {
if (!camera || !scene || toggleView) return;
@@ -108,7 +116,7 @@ const SelectionControls3D: React.FC = () => {
if (event.button === 2 && !event.ctrlKey && !event.shiftKey) {
isRightClick.current = false;
if (!rightClickMoved.current) {
clearSelection();
// clearSelection();
}
return;
}
@@ -193,7 +201,7 @@ const SelectionControls3D: React.FC = () => {
const onContextMenu = (event: MouseEvent) => {
event.preventDefault();
if (!rightClickMoved.current) {
clearSelection();
// clearSelection();
}
rightClickMoved.current = false;
};

View File

@@ -202,10 +202,10 @@ const UserAuth: React.FC = () => {
</div>
{!isSignIn && (
<div className="policy-checkbox">
<input type="checkbox" name="" id="" required />
<div className="label">
<input type="checkbox" id="tos" required />
<label htmlFor="tos" className="label">
I have read and agree to the terms of service
</div>
</label>
</div>
)}
<button id="form-submit" type="submit" className="continue-button">

View File

@@ -1,3 +1,4 @@
import { Object3D } from "three";
import { create } from "zustand";
import { io } from "socket.io-client";
import * as CONSTANTS from "../../types/world/worldConstants";
@@ -166,9 +167,14 @@ export const useNavMesh = create<any>((set: any) => ({
setNavMesh: (x: any) => set({ navMesh: x }),
}));
export const useSelectedAssets = create<any>((set: any) => ({
type SelectedAssetsState = {
selectedAssets: Object3D[];
setSelectedAssets: (assets: Object3D[]) => void;
};
export const useSelectedAssets = create<SelectedAssetsState>((set) => ({
selectedAssets: [],
setSelectedAssets: (x: any) => set(() => ({ selectedAssets: x })),
setSelectedAssets: (assets) => set({ selectedAssets: assets }),
}));
export const useLayers = create<any>((set: any) => ({
@@ -632,3 +638,7 @@ export const useSelectedPath = create<any>((set: any) => ({
selectedPath: "auto",
setSelectedPath: (x: any) => set({ selectedPath: x }),
}));
export const useContextActionStore = create<any>((set: any) => ({
contextAction: null,
setContextAction: (x: any) => set({ contextAction: x }),
}));

View File

@@ -0,0 +1,62 @@
@use "../../abstracts/variables" as *;
@use "../../abstracts/mixins" as *;
.context-menu {
position: absolute;
top: 0;
left: 0;
background: var(--background-color);
backdrop-filter: blur(50px);
color: var(--text-button-color);
box-shadow: var(--box-shadow-light);
border-radius: 6px;
z-index: 1000;
min-width: 200px;
padding: 4px;
display: flex;
flex-direction: column;
gap: 2px;
.menuItem {
position: relative;
display: flex;
justify-content: space-between;
padding: 6px 8px;
border-radius: 10px;
cursor: pointer;
.submenu {
display: none;
min-width: 178px;
position: absolute;
top: 0;
left: 100%; // place directly beside
background: var(--background-color);
backdrop-filter: blur(50px);
color: var(--text-button-color);
box-shadow: var(--box-shadow-light);
padding: 4px;
border-radius: 6px;
z-index: 1000;
}
.button {
display: flex;
gap: 6px;
}
// Keep submenu open while hovering parent OR submenu
&:hover .submenu,
.submenu:hover {
display: block;
}
&:hover {
background-color: var(--background-color-button);
}
}
}

View File

@@ -27,6 +27,7 @@
@use "components/simulation/analysis";
@use "components/logs/logs";
@use "components/footer/footer.scss";
@use "components/contextMenu/contextMenu";
// layout
@use "layout/loading";

View File

@@ -11,6 +11,7 @@ import useVersionHistoryVisibleStore, {
useDfxUpload,
useRenameModeStore,
useSaveVersion,
useSelectedAssets,
useSelectedComment,
useSelectedFloorItem,
useSelectedWallItem,
@@ -50,6 +51,7 @@ const KeyPressListener: React.FC = () => {
const { setViewSceneLabels } = useViewSceneStore();
const { isRenameMode, setIsRenameMode } = useRenameModeStore();
const { selectedFloorItem } = useSelectedFloorItem();
const { selectedAssets } = useSelectedAssets();
const { setCreateNewVersion } = useVersionHistoryStore();
const { setVersionHistoryVisible } = useVersionHistoryVisibleStore();
const { setSelectedComment } = useSelectedComment();
@@ -251,7 +253,7 @@ const KeyPressListener: React.FC = () => {
setViewSceneLabels((prev) => !prev);
}
if (selectedFloorItem && keyCombination === "F2") {
if ((selectedFloorItem || selectedAssets.length === 1) && keyCombination === "F2") {
setIsRenameMode(true);
}
@@ -278,6 +280,7 @@ const KeyPressListener: React.FC = () => {
hidePlayer,
selectedFloorItem,
isRenameMode,
selectedAssets
]);
return null;