first commit
This commit is contained in:
30
app/src/modules/builder/IntialLoad/loadInitialLine.ts
Normal file
30
app/src/modules/builder/IntialLoad/loadInitialLine.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import addLineToScene from '../../builder/geomentries/lines/addLineToScene';
|
||||
import * as CONSTANTS from '../../../types/world/worldConstants';
|
||||
import * as Types from "../../../types/world/worldTypes";
|
||||
|
||||
function loadInitialLine(
|
||||
floorPlanGroupLine: Types.RefGroup,
|
||||
lines: Types.RefLines
|
||||
): void {
|
||||
|
||||
if (!floorPlanGroupLine.current) return
|
||||
|
||||
////////// Load the Lines initially if there are any //////////
|
||||
|
||||
floorPlanGroupLine.current.children = [];
|
||||
lines.current.forEach((line) => {
|
||||
let colour;
|
||||
if (line[0][3] && line[1][3] === CONSTANTS.lineConfig.wallName) {
|
||||
colour = CONSTANTS.lineConfig.wallColor;
|
||||
} else if (line[0][3] && line[1][3] === CONSTANTS.lineConfig.floorName) {
|
||||
colour = CONSTANTS.lineConfig.floorColor;
|
||||
} else if (line[0][3] && line[1][3] === CONSTANTS.lineConfig.aisleName) {
|
||||
colour = CONSTANTS.lineConfig.aisleColor;
|
||||
}
|
||||
if (colour) {
|
||||
addLineToScene(line[0][0], line[1][0], colour, line, floorPlanGroupLine);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default loadInitialLine;
|
||||
87
app/src/modules/builder/IntialLoad/loadInitialPoint.ts
Normal file
87
app/src/modules/builder/IntialLoad/loadInitialPoint.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
import * as CONSTANTS from '../../../types/world/worldConstants';
|
||||
import * as Types from "../../../types/world/worldTypes";
|
||||
|
||||
////////// Load the Boxes initially if there are any //////////
|
||||
|
||||
function loadInitialPoint(
|
||||
lines: Types.RefLines,
|
||||
floorPlanGroupPoint: Types.RefGroup,
|
||||
currentLayerPoint: Types.RefMeshArray,
|
||||
dragPointControls: Types.RefDragControl
|
||||
): void {
|
||||
|
||||
if (!floorPlanGroupPoint.current) return
|
||||
|
||||
floorPlanGroupPoint.current.children = [];
|
||||
currentLayerPoint.current = [];
|
||||
lines.current.forEach((line) => {
|
||||
const colour = getPointColor(line[0][3]);
|
||||
line.forEach((pointData) => {
|
||||
const [point, id] = pointData;
|
||||
|
||||
/////////// Check if a box with this id already exists //////////
|
||||
|
||||
const existingBox = floorPlanGroupPoint.current?.getObjectByProperty('uuid', id);
|
||||
if (existingBox) {
|
||||
return;
|
||||
}
|
||||
|
||||
const geometry = new THREE.BoxGeometry(...CONSTANTS.pointConfig.boxScale);
|
||||
const material = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
uOuterColor: { value: new THREE.Color(colour) }, // Blue color for the border
|
||||
uInnerColor: { value: new THREE.Color(CONSTANTS.pointConfig.defaultInnerColor) }, // White color for the inner square
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
varying vec2 vUv;
|
||||
uniform vec3 uOuterColor;
|
||||
uniform vec3 uInnerColor;
|
||||
|
||||
void main() {
|
||||
// Define the size of the white square as a proportion of the face
|
||||
float borderThickness = 0.2; // Adjust this value for border thickness
|
||||
if (vUv.x > borderThickness && vUv.x < 1.0 - borderThickness &&
|
||||
vUv.y > borderThickness && vUv.y < 1.0 - borderThickness) {
|
||||
gl_FragColor = vec4(uInnerColor, 1.0); // White inner square
|
||||
} else {
|
||||
gl_FragColor = vec4(uOuterColor, 1.0); // Blue border
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
const box = new THREE.Mesh(geometry, material);
|
||||
box.name = "point";
|
||||
box.uuid = id;
|
||||
box.userData = { type: line[0][3], color: colour };
|
||||
box.position.set(point.x, point.y, point.z);
|
||||
currentLayerPoint.current.push(box);
|
||||
|
||||
floorPlanGroupPoint.current?.add(box);
|
||||
});
|
||||
});
|
||||
|
||||
function getPointColor(lineType: string | undefined): string {
|
||||
switch (lineType) {
|
||||
case CONSTANTS.lineConfig.wallName: return CONSTANTS.pointConfig.wallOuterColor;
|
||||
case CONSTANTS.lineConfig.floorName: return CONSTANTS.pointConfig.floorOuterColor;
|
||||
case CONSTANTS.lineConfig.aisleName: return CONSTANTS.pointConfig.aisleOuterColor;
|
||||
default: return CONSTANTS.pointConfig.defaultOuterColor;
|
||||
}
|
||||
}
|
||||
|
||||
if (dragPointControls.current) {
|
||||
dragPointControls.current!.objects = currentLayerPoint.current;
|
||||
}
|
||||
}
|
||||
|
||||
export default loadInitialPoint;
|
||||
103
app/src/modules/builder/IntialLoad/loadInitialWallItems.ts
Normal file
103
app/src/modules/builder/IntialLoad/loadInitialWallItems.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
|
||||
import * as THREE from 'three';
|
||||
import * as Types from "../../../types/world/worldTypes";
|
||||
import { getWallItems } from '../../../services/factoryBuilder/assest/wallAsset/getWallItemsApi';
|
||||
import { retrieveGLTF, storeGLTF } from '../../../utils/indexDB/idbUtils';
|
||||
|
||||
async function loadInitialWallItems(
|
||||
setWallItems: Types.setWallItemSetState,
|
||||
projectId?:string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const email = localStorage.getItem('email');
|
||||
if (!email) {
|
||||
throw new Error('No email found in localStorage');
|
||||
}
|
||||
|
||||
const organization = email.split("@")[1].split(".")[0];
|
||||
const items = await getWallItems(organization,projectId);
|
||||
|
||||
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
localStorage.removeItem("WallItems");
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem("WallItems", JSON.stringify(items));
|
||||
|
||||
const loader = new GLTFLoader();
|
||||
const dracoLoader = new DRACOLoader();
|
||||
dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/');
|
||||
loader.setDRACOLoader(dracoLoader);
|
||||
|
||||
const loadedWallItems = await Promise.all(items.map(async (item: Types.WallItem) => {
|
||||
// Check THREE.js cache first
|
||||
const cachedModel = THREE.Cache.get(item.modelfileID!);
|
||||
if (cachedModel) {
|
||||
return processModel(cachedModel, item);
|
||||
}
|
||||
|
||||
// Check IndexedDB cache
|
||||
const cachedModelBlob = await retrieveGLTF(item.modelfileID!);
|
||||
if (cachedModelBlob) {
|
||||
const blobUrl = URL.createObjectURL(cachedModelBlob);
|
||||
return new Promise<Types.WallItem>((resolve) => {
|
||||
loader.load(blobUrl, (gltf) => {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
THREE.Cache.add(item.modelfileID!, gltf);
|
||||
resolve(processModel(gltf, item));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Load from original URL if not cached
|
||||
const modelUrl = `${url_Backend_dwinzo}/api/v2/AssetFile/${item.modelfileID!}`;
|
||||
return new Promise<Types.WallItem>((resolve) => {
|
||||
loader.load(modelUrl, async (gltf) => {
|
||||
try {
|
||||
// Cache the model
|
||||
const modelBlob = await fetch(modelUrl).then((res) => res.blob());
|
||||
await storeGLTF(item.modelfileID!, modelBlob);
|
||||
THREE.Cache.add(item.modelfileID!, gltf);
|
||||
resolve(processModel(gltf, item));
|
||||
} catch (error) {
|
||||
console.error('Failed to cache model:', error);
|
||||
resolve(processModel(gltf, item));
|
||||
}
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
setWallItems(loadedWallItems);
|
||||
} catch (error) {
|
||||
console.error('Failed to load wall items:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function processModel(gltf: GLTF, item: Types.WallItem): Types.WallItem {
|
||||
const model = gltf.scene.clone();
|
||||
model.uuid = item.modelUuid!;
|
||||
|
||||
model.children[0]?.children?.forEach((child: THREE.Object3D) => {
|
||||
if (child.name !== "CSG_REF") {
|
||||
child.castShadow = true;
|
||||
child.receiveShadow = true;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
type: item.type,
|
||||
model: model,
|
||||
modelName: item.modelName,
|
||||
modelfileID: item.modelfileID,
|
||||
scale: item.scale,
|
||||
csgscale: item.csgscale,
|
||||
csgposition: item.csgposition,
|
||||
position: item.position,
|
||||
quaternion: item.quaternion,
|
||||
};
|
||||
}
|
||||
|
||||
export default loadInitialWallItems;
|
||||
81
app/src/modules/builder/aisle/Instances/aisleInstances.tsx
Normal file
81
app/src/modules/builder/aisle/Instances/aisleInstances.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useAisleStore } from '../../../../store/builder/useAisleStore';
|
||||
import { useToggleView } from '../../../../store/builder/store';
|
||||
import AisleInstance from './instance/aisleInstance';
|
||||
import Point from '../../point/point';
|
||||
import { Html } from '@react-three/drei';
|
||||
import { Vector3 } from 'three';
|
||||
|
||||
function AisleInstances() {
|
||||
const { aisles } = useAisleStore();
|
||||
const { toggleView } = useToggleView();
|
||||
|
||||
const allPoints = useMemo(() => {
|
||||
const points: Point[] = [];
|
||||
const seenUuids = new Set<string>();
|
||||
|
||||
aisles.forEach(aisle => {
|
||||
aisle.points.forEach(point => {
|
||||
if (!seenUuids.has(point.pointUuid)) {
|
||||
seenUuids.add(point.pointUuid);
|
||||
points.push(point);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return points;
|
||||
}, [aisles]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{toggleView &&
|
||||
<group name='Aisle-Points-Group'>
|
||||
{allPoints.map((point) => (
|
||||
<Point key={point.pointUuid} point={point} />
|
||||
))}
|
||||
</group>
|
||||
}
|
||||
|
||||
<group name='Aisles-Group'>
|
||||
{aisles.map((aisle) => {
|
||||
const textPosition = new Vector3().addVectors(new Vector3(...aisle.points[0].position), new Vector3(...aisle.points[1].position)).divideScalar(2);
|
||||
const distance = new Vector3(...aisle.points[0].position).distanceTo(new Vector3(...aisle.points[1].position));
|
||||
|
||||
return (
|
||||
< React.Fragment key={aisle.aisleUuid}>
|
||||
<AisleInstance aisle={aisle} key={aisle.aisleUuid} />
|
||||
|
||||
{toggleView &&
|
||||
<Html
|
||||
// data
|
||||
key={`${aisle.points[0].pointUuid}_${aisle.points[1].pointUuid}`}
|
||||
userData={aisle}
|
||||
position={[textPosition.x, 1, textPosition.z]}
|
||||
// class
|
||||
wrapperClass="distance-text-wrapper"
|
||||
className="distance-text"
|
||||
// other
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
sprite
|
||||
>
|
||||
<div
|
||||
key={aisle.aisleUuid}
|
||||
className={`distance ${aisle.aisleUuid}`}
|
||||
>
|
||||
{distance.toFixed(2)} m
|
||||
</div>
|
||||
</Html>
|
||||
}
|
||||
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</group>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AisleInstances
|
||||
@@ -0,0 +1,49 @@
|
||||
import ArcAisle from './aisleTypes/arcAisle';
|
||||
import ArrowAisle from './aisleTypes/arrowAisle';
|
||||
import ArrowsAisle from './aisleTypes/arrowsAisle';
|
||||
import CircleAisle from './aisleTypes/circleAisle';
|
||||
import DashedAisle from './aisleTypes/dashedAisle';
|
||||
import DottedAisle from './aisleTypes/dottedAisle';
|
||||
import JunctionAisle from './aisleTypes/junctionAisle';
|
||||
import SolidAisle from './aisleTypes/solidAisle';
|
||||
|
||||
function AisleInstance({ aisle }: { readonly aisle: Aisle }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{aisle.type.aisleType === 'solid-aisle' && (
|
||||
<SolidAisle aisle={aisle} />
|
||||
)}
|
||||
|
||||
{aisle.type.aisleType === 'dashed-aisle' && (
|
||||
<DashedAisle aisle={aisle} />
|
||||
)}
|
||||
|
||||
{aisle.type.aisleType === 'dotted-aisle' && (
|
||||
<DottedAisle aisle={aisle} />
|
||||
)}
|
||||
|
||||
{aisle.type.aisleType === 'arrows-aisle' && (
|
||||
<ArrowsAisle aisle={aisle} />
|
||||
)}
|
||||
|
||||
{aisle.type.aisleType === 'arrow-aisle' && (
|
||||
<ArrowAisle aisle={aisle} />
|
||||
)}
|
||||
|
||||
{aisle.type.aisleType === 'circle-aisle' && (
|
||||
<CircleAisle aisle={aisle} />
|
||||
)}
|
||||
|
||||
{aisle.type.aisleType === 'junction-aisle' && (
|
||||
<JunctionAisle aisle={aisle} />
|
||||
)}
|
||||
|
||||
{aisle.type.aisleType === 'arc-aisle' && (
|
||||
<ArcAisle aisle={aisle} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AisleInstance;
|
||||
@@ -0,0 +1,104 @@
|
||||
import * as THREE from 'three';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { Extrude } from '@react-three/drei';
|
||||
import * as Constants from '../../../../../../types/world/worldConstants';
|
||||
import { useToolMode } from '../../../../../../store/builder/store';
|
||||
import { useBuilderStore } from '../../../../../../store/builder/useBuilderStore';
|
||||
|
||||
function ArcAisle({ aisle }: { readonly aisle: Aisle }) {
|
||||
const aisleRef = useRef<THREE.Group>(null);
|
||||
const { toolMode } = useToolMode();
|
||||
const { setSelectedAisle, hoveredPoint } = useBuilderStore();
|
||||
|
||||
const arc = useMemo(() => {
|
||||
if (aisle.points.length < 2 || aisle.type.aisleType !== 'arc-aisle') return null;
|
||||
|
||||
const start = new THREE.Vector3(...aisle.points[0].position);
|
||||
const end = new THREE.Vector3(...aisle.points[1].position);
|
||||
const width = aisle.type.aisleWidth || 0.5;
|
||||
const isFlipped = aisle.type.isFlipped || false;
|
||||
|
||||
const direction = new THREE.Vector3().subVectors(end, start);
|
||||
const length = direction.length();
|
||||
direction.normalize();
|
||||
|
||||
const perpendicular = new THREE.Vector3(-direction.z, 0, direction.x).normalize();
|
||||
if (!isFlipped) perpendicular.negate();
|
||||
|
||||
const arcHeight = length * 0.25;
|
||||
const midPoint = new THREE.Vector3().lerpVectors(start, end, 0.5);
|
||||
const controlPoint = new THREE.Vector3().copy(midPoint).addScaledVector(perpendicular, arcHeight);
|
||||
|
||||
const widthOffset = perpendicular.clone().multiplyScalar(width / 2);
|
||||
|
||||
const p1 = new THREE.Vector3().copy(start).add(widthOffset);
|
||||
const p2 = new THREE.Vector3().copy(end).add(widthOffset);
|
||||
const p3 = new THREE.Vector3().copy(end).sub(widthOffset);
|
||||
const p4 = new THREE.Vector3().copy(start).sub(widthOffset);
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
shape.moveTo(p1.x, p1.z);
|
||||
|
||||
shape.quadraticCurveTo(
|
||||
controlPoint.x + widthOffset.x,
|
||||
controlPoint.z + widthOffset.z,
|
||||
p2.x, p2.z
|
||||
);
|
||||
|
||||
shape.lineTo(p3.x, p3.z);
|
||||
|
||||
shape.quadraticCurveTo(
|
||||
controlPoint.x - widthOffset.x,
|
||||
controlPoint.z - widthOffset.z,
|
||||
p4.x, p4.z
|
||||
);
|
||||
|
||||
shape.lineTo(p1.x, p1.z);
|
||||
|
||||
return {
|
||||
shape,
|
||||
position: new THREE.Vector3(0, 0, 0),
|
||||
rotationY: 0
|
||||
};
|
||||
}, [aisle]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (toolMode === 'move' && !hoveredPoint) {
|
||||
setSelectedAisle(aisleRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
if (!arc) return null;
|
||||
|
||||
return (
|
||||
<group
|
||||
name='Arc-Aisle'
|
||||
uuid={aisle.aisleUuid}
|
||||
ref={aisleRef}
|
||||
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
userData={aisle}
|
||||
onClick={handleClick}
|
||||
onPointerMissed={() => {
|
||||
setSelectedAisle(null);
|
||||
}}
|
||||
>
|
||||
<Extrude
|
||||
args={[arc.shape, {
|
||||
depth: 0.01,
|
||||
bevelEnabled: false,
|
||||
curveSegments: 32
|
||||
}]}
|
||||
receiveShadow
|
||||
castShadow
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={aisle.type.aisleColor || '#ffffff'}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</Extrude>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArcAisle;
|
||||
@@ -0,0 +1,89 @@
|
||||
import * as THREE from 'three';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { Extrude } from '@react-three/drei';
|
||||
import * as Constants from '../../../../../../types/world/worldConstants';
|
||||
import { useToolMode } from '../../../../../../store/builder/store';
|
||||
import { useBuilderStore } from '../../../../../../store/builder/useBuilderStore';
|
||||
|
||||
function ArrowAisle({ aisle }: { readonly aisle: Aisle }) {
|
||||
const aisleRef = useRef<THREE.Group>(null);
|
||||
const { toolMode } = useToolMode();
|
||||
const { setSelectedAisle, hoveredPoint } = useBuilderStore();
|
||||
|
||||
const arrow = useMemo(() => {
|
||||
if (aisle.points.length < 2 || aisle.type.aisleType !== 'arrow-aisle') return null;
|
||||
|
||||
const start = new THREE.Vector3(...aisle.points[0].position);
|
||||
const end = new THREE.Vector3(...aisle.points[1].position);
|
||||
const width = aisle.type.aisleWidth || 0.1;
|
||||
|
||||
const direction = new THREE.Vector3().subVectors(end, start);
|
||||
const length = direction.length();
|
||||
direction.normalize();
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
const arrowHeadLength = width * 2;
|
||||
const shaftLength = length - arrowHeadLength;
|
||||
|
||||
if (shaftLength > 0) {
|
||||
shape.moveTo(0, 0);
|
||||
shape.lineTo(width, -arrowHeadLength);
|
||||
shape.lineTo(width / 2, -arrowHeadLength);
|
||||
shape.lineTo(width / 2, -length);
|
||||
shape.lineTo(-width / 2, -length);
|
||||
shape.lineTo(-width / 2, -arrowHeadLength);
|
||||
shape.lineTo(-width, -arrowHeadLength);
|
||||
shape.lineTo(0, 0);
|
||||
} else {
|
||||
shape.moveTo(0, 0);
|
||||
shape.lineTo(width, -length);
|
||||
shape.lineTo(-width, -length);
|
||||
shape.lineTo(0, 0);
|
||||
}
|
||||
|
||||
shape.closePath();
|
||||
|
||||
const position = end;
|
||||
const angle = Math.atan2(direction.x, direction.z);
|
||||
|
||||
return { shape, position, rotationY: angle };
|
||||
}, [aisle]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (toolMode === 'move' && !hoveredPoint) {
|
||||
setSelectedAisle(aisleRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
if (!arrow) return null;
|
||||
|
||||
return (
|
||||
<group
|
||||
name='Arrow-Aisle'
|
||||
uuid={aisle.aisleUuid}
|
||||
ref={aisleRef}
|
||||
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
userData={aisle}
|
||||
onClick={handleClick}
|
||||
onPointerMissed={() => {
|
||||
setSelectedAisle(null);
|
||||
}}
|
||||
>
|
||||
<group position={[arrow.position.x, arrow.position.z, 0]} rotation={[0, 0, -arrow.rotationY]}>
|
||||
<Extrude
|
||||
args={[arrow.shape, { depth: 0.01, bevelEnabled: false }]}
|
||||
receiveShadow
|
||||
castShadow
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={aisle.type.aisleColor || '#ffffff'}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</Extrude>
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArrowAisle;
|
||||
@@ -0,0 +1,94 @@
|
||||
import * as THREE from 'three';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { Extrude } from '@react-three/drei';
|
||||
import * as Constants from '../../../../../../types/world/worldConstants';
|
||||
import { useToolMode } from '../../../../../../store/builder/store';
|
||||
import { useBuilderStore } from '../../../../../../store/builder/useBuilderStore';
|
||||
|
||||
function ArrowsAisle({ aisle }: { readonly aisle: Aisle }) {
|
||||
const aisleRef = useRef<THREE.Group>(null);
|
||||
const { toolMode } = useToolMode();
|
||||
const { setSelectedAisle, hoveredPoint } = useBuilderStore();
|
||||
|
||||
const arrows = useMemo(() => {
|
||||
if (aisle.points.length < 2 || aisle.type.aisleType !== 'arrows-aisle') return [];
|
||||
|
||||
const start = new THREE.Vector3(...aisle.points[0].position);
|
||||
const end = new THREE.Vector3(...aisle.points[1].position);
|
||||
const width = aisle.type.aisleWidth || 0.1;
|
||||
const arrowLength = aisle.type.aisleLength || 0.6;
|
||||
const spacing = aisle.type.gapLength || 0.6;
|
||||
|
||||
const direction = new THREE.Vector3().subVectors(end, start);
|
||||
const length = direction.length();
|
||||
direction.normalize();
|
||||
|
||||
const count = Math.floor((length + spacing) / (arrowLength + spacing));
|
||||
|
||||
const arrowShapes: { shape: THREE.Shape; position: THREE.Vector3; rotationY: number }[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const initialOffset = arrowLength;
|
||||
const center = new THREE.Vector3().copy(start).addScaledVector(direction, initialOffset + i * (arrowLength + spacing));
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
const w = width * 0.8;
|
||||
const h = arrowLength;
|
||||
|
||||
shape.moveTo(0, 0);
|
||||
shape.lineTo(w, h * 0.6);
|
||||
shape.lineTo(w * 0.4, h * 0.6);
|
||||
shape.lineTo(w * 0.4, h);
|
||||
shape.lineTo(-w * 0.4, h);
|
||||
shape.lineTo(-w * 0.4, h * 0.6);
|
||||
shape.lineTo(-w, h * 0.6);
|
||||
shape.lineTo(0, 0);
|
||||
|
||||
const angle = Math.atan2(direction.x, direction.z) + Math.PI;
|
||||
|
||||
arrowShapes.push({ shape, position: center, rotationY: angle });
|
||||
}
|
||||
|
||||
return arrowShapes;
|
||||
}, [aisle]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (toolMode === 'move' && !hoveredPoint) {
|
||||
setSelectedAisle(aisleRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
if (arrows.length === 0) return null;
|
||||
|
||||
return (
|
||||
<group
|
||||
name='Arrows-Aisle'
|
||||
uuid={aisle.aisleUuid}
|
||||
ref={aisleRef}
|
||||
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
userData={aisle}
|
||||
onClick={handleClick}
|
||||
onPointerMissed={() => {
|
||||
setSelectedAisle(null);
|
||||
}}
|
||||
>
|
||||
{arrows.map(({ shape, position, rotationY }, index) => (
|
||||
<group key={index} position={[position.x, position.z, 0]} rotation={[0, 0, -rotationY]}>
|
||||
<Extrude
|
||||
args={[shape, { depth: 0.01, bevelEnabled: false }]}
|
||||
receiveShadow
|
||||
castShadow
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={aisle.type.aisleColor || '#ffffff'}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</Extrude>
|
||||
</group>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArrowsAisle;
|
||||
@@ -0,0 +1,80 @@
|
||||
import * as THREE from 'three';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { Extrude } from '@react-three/drei';
|
||||
import * as Constants from '../../../../../../types/world/worldConstants';
|
||||
import { useToolMode } from '../../../../../../store/builder/store';
|
||||
import { useBuilderStore } from '../../../../../../store/builder/useBuilderStore';
|
||||
|
||||
function CircleAisle({ aisle }: { readonly aisle: Aisle }) {
|
||||
const aisleRef = useRef<THREE.Group>(null);
|
||||
const { toolMode } = useToolMode();
|
||||
const { setSelectedAisle, hoveredPoint } = useBuilderStore();
|
||||
|
||||
const circle = useMemo(() => {
|
||||
if (aisle.points.length < 2 || aisle.type.aisleType !== 'circle-aisle') return null;
|
||||
|
||||
const center = new THREE.Vector3(...aisle.points[0].position);
|
||||
const widthCenter = new THREE.Vector3(...aisle.points[1].position);
|
||||
const width = aisle.type.aisleWidth || 0.1;
|
||||
|
||||
const middleRadius = center.distanceTo(widthCenter);
|
||||
|
||||
const innerRadius = Math.max(0, middleRadius - width / 2);
|
||||
const outerRadius = middleRadius + width / 2;
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
shape.absarc(center.x, center.z, outerRadius, 0, Math.PI * 2, false);
|
||||
|
||||
if (innerRadius > 0) {
|
||||
const hole = new THREE.Path();
|
||||
hole.absarc(center.x, center.z, innerRadius, 0, Math.PI * 2, true);
|
||||
shape.holes.push(hole);
|
||||
}
|
||||
|
||||
return {
|
||||
shape,
|
||||
rotationY: 0
|
||||
};
|
||||
}, [aisle]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (toolMode === 'move' && !hoveredPoint) {
|
||||
setSelectedAisle(aisleRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
if (!circle) return null;
|
||||
|
||||
return (
|
||||
<group
|
||||
name='Circle-Aisle'
|
||||
uuid={aisle.aisleUuid}
|
||||
ref={aisleRef}
|
||||
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
userData={aisle}
|
||||
onClick={handleClick}
|
||||
onPointerMissed={() => {
|
||||
setSelectedAisle(null);
|
||||
}}
|
||||
>
|
||||
<Extrude
|
||||
args={[circle.shape, {
|
||||
depth: 0.01,
|
||||
bevelEnabled: false,
|
||||
steps: 1,
|
||||
curveSegments: 64
|
||||
}]}
|
||||
receiveShadow
|
||||
castShadow
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={aisle.type.aisleColor || '#ffffff'}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</Extrude>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default CircleAisle;
|
||||
@@ -0,0 +1,91 @@
|
||||
import * as THREE from 'three';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { Extrude } from '@react-three/drei';
|
||||
import * as Constants from '../../../../../../types/world/worldConstants';
|
||||
import { useToolMode } from '../../../../../../store/builder/store';
|
||||
import { useBuilderStore } from '../../../../../../store/builder/useBuilderStore';
|
||||
|
||||
function DashedAisle({ aisle }: { readonly aisle: Aisle }) {
|
||||
const aisleRef = useRef<THREE.Group>(null);
|
||||
const { toolMode } = useToolMode();
|
||||
const { setSelectedAisle, hoveredPoint } = useBuilderStore();
|
||||
|
||||
const shapes = useMemo(() => {
|
||||
if (aisle.points.length < 2 || aisle.type.aisleType !== 'dashed-aisle') return [];
|
||||
|
||||
const start = new THREE.Vector3(...aisle.points[0].position);
|
||||
const end = new THREE.Vector3(...aisle.points[1].position);
|
||||
const width = aisle.type.aisleWidth || 0.1;
|
||||
const dashLength = aisle.type.dashLength || 0.5;
|
||||
const gapLength = aisle.type.gapLength || 0.3;
|
||||
|
||||
const direction = new THREE.Vector3().subVectors(end, start).normalize();
|
||||
const perp = new THREE.Vector3(-direction.z, 0, direction.x).normalize();
|
||||
|
||||
const totalLength = new THREE.Vector3().subVectors(end, start).length();
|
||||
const segmentCount = Math.floor((totalLength + gapLength) / (dashLength + gapLength));
|
||||
|
||||
const shapes = [];
|
||||
const directionNormalized = new THREE.Vector3().subVectors(end, start).normalize();
|
||||
|
||||
for (let i = 0; i < segmentCount; i++) {
|
||||
const segmentStart = new THREE.Vector3().copy(start).addScaledVector(directionNormalized, i * (dashLength + gapLength));
|
||||
const segmentEnd = new THREE.Vector3().copy(segmentStart).addScaledVector(directionNormalized, dashLength);
|
||||
|
||||
const leftStart = new THREE.Vector3().copy(segmentStart).addScaledVector(perp, width / 2);
|
||||
const rightStart = new THREE.Vector3().copy(segmentStart).addScaledVector(perp, -width / 2);
|
||||
const leftEnd = new THREE.Vector3().copy(segmentEnd).addScaledVector(perp, width / 2);
|
||||
const rightEnd = new THREE.Vector3().copy(segmentEnd).addScaledVector(perp, -width / 2);
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
shape.moveTo(leftStart.x, leftStart.z);
|
||||
shape.lineTo(leftEnd.x, leftEnd.z);
|
||||
shape.lineTo(rightEnd.x, rightEnd.z);
|
||||
shape.lineTo(rightStart.x, rightStart.z);
|
||||
shape.closePath();
|
||||
|
||||
shapes.push(shape);
|
||||
}
|
||||
|
||||
return shapes;
|
||||
}, [aisle]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (toolMode === 'move' && !hoveredPoint) {
|
||||
setSelectedAisle(aisleRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
if (shapes.length === 0) return null;
|
||||
|
||||
return (
|
||||
<group
|
||||
name='Dashed-Aisle'
|
||||
uuid={aisle.aisleUuid}
|
||||
ref={aisleRef}
|
||||
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
userData={aisle}
|
||||
onClick={handleClick}
|
||||
onPointerMissed={() => {
|
||||
setSelectedAisle(null);
|
||||
}}
|
||||
>
|
||||
{shapes.map((shape, index) => (
|
||||
<Extrude
|
||||
key={index}
|
||||
args={[shape, { depth: 0.01, bevelEnabled: false }]}
|
||||
receiveShadow
|
||||
castShadow
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={aisle.type.aisleColor || '#ffffff'}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</Extrude>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashedAisle;
|
||||
@@ -0,0 +1,78 @@
|
||||
import * as THREE from 'three';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { Extrude } from '@react-three/drei';
|
||||
import * as Constants from '../../../../../../types/world/worldConstants';
|
||||
import { useToolMode } from '../../../../../../store/builder/store';
|
||||
import { useBuilderStore } from '../../../../../../store/builder/useBuilderStore';
|
||||
|
||||
function DottedAisle({ aisle }: { readonly aisle: Aisle }) {
|
||||
const aisleRef = useRef<THREE.Group>(null);
|
||||
const { toolMode } = useToolMode();
|
||||
const { setSelectedAisle, hoveredPoint } = useBuilderStore();
|
||||
|
||||
const shapes = useMemo(() => {
|
||||
if (aisle.points.length < 2 || aisle.type.aisleType !== 'dotted-aisle') return [];
|
||||
|
||||
const start = new THREE.Vector3(...aisle.points[0].position);
|
||||
const end = new THREE.Vector3(...aisle.points[1].position);
|
||||
const width = aisle.type.dotRadius || 0.1;
|
||||
const dotSpacing = aisle.type.gapLength || 0.5;
|
||||
const dotRadius = width * 0.6;
|
||||
|
||||
const totalLength = new THREE.Vector3().subVectors(end, start).length();
|
||||
const dotCount = Math.floor((totalLength + (dotSpacing / 2)) / dotSpacing);
|
||||
|
||||
const shapes = [];
|
||||
const directionNormalized = new THREE.Vector3().subVectors(end, start).normalize();
|
||||
|
||||
for (let i = 0; i < dotCount; i++) {
|
||||
const dotCenter = new THREE.Vector3().copy(start).addScaledVector(directionNormalized, i * dotSpacing + dotSpacing / 2);
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
shape.absarc(dotCenter.x, dotCenter.z, dotRadius, 0, Math.PI * 2, false);
|
||||
|
||||
shapes.push(shape);
|
||||
}
|
||||
|
||||
return shapes;
|
||||
}, [aisle]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (toolMode === 'move' && !hoveredPoint) {
|
||||
setSelectedAisle(aisleRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
if (shapes.length === 0) return null;
|
||||
|
||||
return (
|
||||
<group
|
||||
name='Dotted-Aisle'
|
||||
uuid={aisle.aisleUuid}
|
||||
ref={aisleRef}
|
||||
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
userData={aisle}
|
||||
onClick={handleClick}
|
||||
onPointerMissed={() => {
|
||||
setSelectedAisle(null);
|
||||
}}
|
||||
>
|
||||
{shapes.map((shape, index) => (
|
||||
<Extrude
|
||||
key={index}
|
||||
args={[shape, { depth: 0.01, bevelEnabled: false }]}
|
||||
receiveShadow
|
||||
castShadow
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={aisle.type.aisleColor || '#ffffff'}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</Extrude>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default DottedAisle;
|
||||
@@ -0,0 +1,126 @@
|
||||
import * as THREE from 'three';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { Extrude } from '@react-three/drei';
|
||||
import * as Constants from '../../../../../../types/world/worldConstants';
|
||||
import { useToolMode } from '../../../../../../store/builder/store';
|
||||
import { useBuilderStore } from '../../../../../../store/builder/useBuilderStore';
|
||||
|
||||
function JunctionAisle({ aisle }: { readonly aisle: Aisle }) {
|
||||
const aisleRef = useRef<THREE.Group>(null);
|
||||
const { toolMode } = useToolMode();
|
||||
const { setSelectedAisle, hoveredPoint } = useBuilderStore();
|
||||
|
||||
const arrows = useMemo(() => {
|
||||
if (aisle.points.length < 2 || aisle.type.aisleType !== 'junction-aisle') return null;
|
||||
|
||||
const start = new THREE.Vector3(...aisle.points[0].position);
|
||||
const end = new THREE.Vector3(...aisle.points[1].position);
|
||||
const width = aisle.type.aisleWidth || 0.1;
|
||||
const isFlipped = aisle.type.isFlipped || false;
|
||||
|
||||
const direction = new THREE.Vector3().subVectors(end, start);
|
||||
const length = direction.length();
|
||||
direction.normalize();
|
||||
|
||||
const mainShape = new THREE.Shape();
|
||||
const arrowHeadLength = width * 2;
|
||||
const shaftLength = length - arrowHeadLength;
|
||||
|
||||
if (shaftLength > 0) {
|
||||
mainShape.moveTo(0, 0);
|
||||
mainShape.lineTo(width, -arrowHeadLength);
|
||||
mainShape.lineTo(width / 2, -arrowHeadLength);
|
||||
mainShape.lineTo(width / 2, -length);
|
||||
mainShape.lineTo(-width / 2, -length);
|
||||
mainShape.lineTo(-width / 2, -arrowHeadLength);
|
||||
mainShape.lineTo(-width, -arrowHeadLength);
|
||||
mainShape.lineTo(0, 0);
|
||||
} else {
|
||||
mainShape.moveTo(0, 0);
|
||||
mainShape.lineTo(width, -length);
|
||||
mainShape.lineTo(-width, -length);
|
||||
mainShape.lineTo(0, 0);
|
||||
}
|
||||
mainShape.closePath();
|
||||
|
||||
const secondaryLength = length / 4;
|
||||
const secondaryShape = new THREE.Shape();
|
||||
const secondaryHeadLength = width * 2;
|
||||
const secondaryShaftLength = secondaryLength - secondaryHeadLength;
|
||||
|
||||
if (secondaryShaftLength > 0) {
|
||||
secondaryShape.moveTo(0, 0);
|
||||
secondaryShape.lineTo(width / 2, 0);
|
||||
secondaryShape.lineTo(width / 2, secondaryShaftLength);
|
||||
secondaryShape.lineTo(width, secondaryShaftLength);
|
||||
secondaryShape.lineTo(0, secondaryLength);
|
||||
secondaryShape.lineTo(-width, secondaryShaftLength);
|
||||
secondaryShape.lineTo(-width / 2, secondaryShaftLength);
|
||||
secondaryShape.lineTo(-width / 2, 0);
|
||||
secondaryShape.lineTo(0, 0);
|
||||
} else {
|
||||
secondaryShape.moveTo(0, 0);
|
||||
secondaryShape.lineTo(width, 0);
|
||||
secondaryShape.lineTo(0, secondaryLength);
|
||||
secondaryShape.lineTo(-width, 0);
|
||||
secondaryShape.lineTo(0, 0);
|
||||
}
|
||||
secondaryShape.closePath();
|
||||
|
||||
const mainPosition = end;
|
||||
const mainAngle = Math.atan2(direction.x, direction.z);
|
||||
|
||||
const perpendicularDirection = isFlipped
|
||||
? new THREE.Vector3(direction.z, 0, -direction.x).normalize()
|
||||
: new THREE.Vector3(-direction.z, 0, direction.x).normalize();
|
||||
|
||||
const secondaryAngle = Math.atan2(perpendicularDirection.x, perpendicularDirection.z);
|
||||
|
||||
const secondaryPosition = new THREE.Vector3().lerpVectors(start, end, 0.75);
|
||||
|
||||
return [
|
||||
{ shape: mainShape, position: mainPosition, rotationY: mainAngle },
|
||||
{ shape: secondaryShape, position: secondaryPosition, rotationY: secondaryAngle + Math.PI }
|
||||
];
|
||||
}, [aisle]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (toolMode === 'move' && !hoveredPoint) {
|
||||
setSelectedAisle(aisleRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
if (!arrows) return null;
|
||||
|
||||
return (
|
||||
<group
|
||||
name='Junction-Aisle'
|
||||
uuid={aisle.aisleUuid}
|
||||
ref={aisleRef}
|
||||
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
userData={aisle}
|
||||
onClick={handleClick}
|
||||
onPointerMissed={() => {
|
||||
setSelectedAisle(null);
|
||||
}}
|
||||
>
|
||||
{arrows.map((arrow, index) => (
|
||||
<group key={index} position={[arrow.position.x, arrow.position.z, 0]} rotation={[0, 0, -arrow.rotationY]}>
|
||||
<Extrude
|
||||
args={[arrow.shape, { depth: 0.01, bevelEnabled: false }]}
|
||||
receiveShadow
|
||||
castShadow
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={aisle.type.aisleColor || '#ffffff'}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</Extrude>
|
||||
</group>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default JunctionAisle;
|
||||
@@ -0,0 +1,72 @@
|
||||
import * as THREE from 'three';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { Extrude } from '@react-three/drei';
|
||||
import * as Constants from '../../../../../../types/world/worldConstants';
|
||||
import { useToolMode } from '../../../../../../store/builder/store';
|
||||
import { useBuilderStore } from '../../../../../../store/builder/useBuilderStore';
|
||||
|
||||
function SolidAisle({ aisle }: { readonly aisle: Aisle }) {
|
||||
const aisleRef = useRef<THREE.Group>(null);
|
||||
const { toolMode } = useToolMode();
|
||||
const { setSelectedAisle, hoveredPoint } = useBuilderStore();
|
||||
const shape = useMemo(() => {
|
||||
if (aisle.points.length < 2 || aisle.type.aisleType !== 'solid-aisle') return null;
|
||||
|
||||
const start = new THREE.Vector3(...aisle.points[0].position);
|
||||
const end = new THREE.Vector3(...aisle.points[1].position);
|
||||
const width = aisle.type.aisleWidth || 0.1;
|
||||
|
||||
const direction = new THREE.Vector3().subVectors(end, start).normalize();
|
||||
const perp = new THREE.Vector3(-direction.z, 0, direction.x).normalize();
|
||||
|
||||
const leftStart = new THREE.Vector3().copy(start).addScaledVector(perp, width / 2);
|
||||
const rightStart = new THREE.Vector3().copy(start).addScaledVector(perp, -width / 2);
|
||||
const leftEnd = new THREE.Vector3().copy(end).addScaledVector(perp, width / 2);
|
||||
const rightEnd = new THREE.Vector3().copy(end).addScaledVector(perp, -width / 2);
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
shape.moveTo(leftStart.x, leftStart.z);
|
||||
shape.lineTo(leftEnd.x, leftEnd.z);
|
||||
shape.lineTo(rightEnd.x, rightEnd.z);
|
||||
shape.lineTo(rightStart.x, rightStart.z);
|
||||
shape.closePath();
|
||||
|
||||
return shape;
|
||||
}, [aisle]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (toolMode === 'move' && !hoveredPoint) {
|
||||
setSelectedAisle(aisleRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
if (!shape) return null;
|
||||
|
||||
return (
|
||||
<group
|
||||
name='Solid-Aisle'
|
||||
uuid={aisle.aisleUuid}
|
||||
ref={aisleRef}
|
||||
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
userData={aisle}
|
||||
onClick={handleClick}
|
||||
onPointerMissed={() => {
|
||||
setSelectedAisle(null);
|
||||
}}
|
||||
>
|
||||
<Extrude
|
||||
args={[shape, { depth: 0.01, bevelEnabled: false }]}
|
||||
receiveShadow
|
||||
castShadow
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={aisle.type.aisleColor || '#ffffff'}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</Extrude>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default SolidAisle;
|
||||
318
app/src/modules/builder/aisle/aisleCreator/aisleCreator.tsx
Normal file
318
app/src/modules/builder/aisle/aisleCreator/aisleCreator.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
import * as THREE from 'three'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useThree } from '@react-three/fiber';
|
||||
import { useActiveLayer, useSocketStore, useToggleView, useToolMode } from '../../../../store/builder/store';
|
||||
import { useAisleStore } from '../../../../store/builder/useAisleStore';
|
||||
import ReferenceAisle from './referenceAisle';
|
||||
import { useBuilderStore } from '../../../../store/builder/useBuilderStore';
|
||||
import ReferencePoint from '../../point/reference/referencePoint';
|
||||
import { createAisleApi } from '../../../../services/factoryBuilder/aisle/createAisleApi';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
function AisleCreator() {
|
||||
const { scene, camera, raycaster, gl, pointer } = useThree();
|
||||
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []);
|
||||
const { toggleView } = useToggleView();
|
||||
const { toolMode } = useToolMode();
|
||||
const { activeLayer } = useActiveLayer();
|
||||
const { socket } = useSocketStore();
|
||||
const { addAisle, getAislePointById } = useAisleStore();
|
||||
const drag = useRef(false);
|
||||
const isLeftMouseDown = useRef(false);
|
||||
const { projectId } = useParams();
|
||||
|
||||
const [tempPoints, setTempPoints] = useState<Point[]>([]);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const { aisleType, aisleWidth, aisleColor, dashLength, gapLength, dotRadius, aisleLength, isFlipped, snappedPosition, snappedPoint } = useBuilderStore();
|
||||
|
||||
useEffect(() => {
|
||||
const canvasElement = gl.domElement;
|
||||
|
||||
const onMouseDown = (evt: any) => {
|
||||
if (evt.button === 0) {
|
||||
isLeftMouseDown.current = true;
|
||||
drag.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUp = (evt: any) => {
|
||||
if (evt.button === 0) {
|
||||
isLeftMouseDown.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseMove = () => {
|
||||
if (isLeftMouseDown) {
|
||||
drag.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseClick = () => {
|
||||
if (drag.current || !toggleView) return;
|
||||
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
let position = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
if (!position) return;
|
||||
|
||||
const intersects = raycaster.intersectObjects(scene.children).find((intersect) => intersect.object.name === 'Aisle-Point');
|
||||
|
||||
const newPoint: Point = {
|
||||
pointUuid: THREE.MathUtils.generateUUID(),
|
||||
pointType: 'Aisle',
|
||||
position: [position.x, position.y, position.z],
|
||||
layer: activeLayer
|
||||
};
|
||||
|
||||
if (snappedPosition && snappedPoint) {
|
||||
newPoint.pointUuid = snappedPoint.pointUuid;
|
||||
newPoint.position = snappedPosition;
|
||||
newPoint.layer = snappedPoint.layer;
|
||||
}
|
||||
|
||||
if (snappedPosition && !snappedPoint) {
|
||||
newPoint.position = snappedPosition;
|
||||
}
|
||||
|
||||
if (intersects && !snappedPoint) {
|
||||
const point = getAislePointById(intersects.object.uuid);
|
||||
if (point) {
|
||||
newPoint.pointUuid = point.pointUuid;
|
||||
newPoint.position = point.position;
|
||||
newPoint.layer = point.layer;
|
||||
}
|
||||
}
|
||||
|
||||
if (aisleType === 'solid-aisle') {
|
||||
|
||||
if (tempPoints.length === 0) {
|
||||
setTempPoints([newPoint]);
|
||||
setIsCreating(true);
|
||||
} else {
|
||||
const aisle: Aisle = {
|
||||
aisleUuid: THREE.MathUtils.generateUUID(),
|
||||
points: [tempPoints[0], newPoint],
|
||||
type: {
|
||||
aisleType: 'solid-aisle',
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth
|
||||
}
|
||||
};
|
||||
addAisle(aisle);
|
||||
if (projectId) {
|
||||
createAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId)
|
||||
}
|
||||
setTempPoints([newPoint]);
|
||||
}
|
||||
} else if (aisleType === 'dashed-aisle') {
|
||||
|
||||
if (tempPoints.length === 0) {
|
||||
setTempPoints([newPoint]);
|
||||
setIsCreating(true);
|
||||
} else {
|
||||
const aisle: Aisle = {
|
||||
aisleUuid: THREE.MathUtils.generateUUID(),
|
||||
points: [tempPoints[0], newPoint],
|
||||
type: {
|
||||
aisleType: 'dashed-aisle',
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth,
|
||||
dashLength: dashLength,
|
||||
gapLength: gapLength
|
||||
}
|
||||
};
|
||||
addAisle(aisle);
|
||||
if (projectId) {
|
||||
createAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId)
|
||||
}
|
||||
setTempPoints([newPoint]);
|
||||
}
|
||||
} else if (aisleType === 'dotted-aisle') {
|
||||
|
||||
if (tempPoints.length === 0) {
|
||||
setTempPoints([newPoint]);
|
||||
setIsCreating(true);
|
||||
} else {
|
||||
const aisle: Aisle = {
|
||||
aisleUuid: THREE.MathUtils.generateUUID(),
|
||||
points: [tempPoints[0], newPoint],
|
||||
type: {
|
||||
aisleType: 'dotted-aisle',
|
||||
aisleColor: aisleColor,
|
||||
dotRadius: dotRadius,
|
||||
gapLength: gapLength
|
||||
}
|
||||
};
|
||||
addAisle(aisle);
|
||||
if (projectId) {
|
||||
createAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId)
|
||||
}
|
||||
setTempPoints([newPoint]);
|
||||
}
|
||||
} else if (aisleType === 'arrow-aisle') {
|
||||
|
||||
if (tempPoints.length === 0) {
|
||||
setTempPoints([newPoint]);
|
||||
setIsCreating(true);
|
||||
} else {
|
||||
const aisle: Aisle = {
|
||||
aisleUuid: THREE.MathUtils.generateUUID(),
|
||||
points: [tempPoints[0], newPoint],
|
||||
type: {
|
||||
aisleType: 'arrow-aisle',
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth
|
||||
}
|
||||
};
|
||||
addAisle(aisle);
|
||||
if (projectId) {
|
||||
createAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId)
|
||||
}
|
||||
setTempPoints([newPoint]);
|
||||
}
|
||||
} else if (aisleType === 'arrows-aisle') {
|
||||
|
||||
if (tempPoints.length === 0) {
|
||||
setTempPoints([newPoint]);
|
||||
setIsCreating(true);
|
||||
} else {
|
||||
const aisle: Aisle = {
|
||||
aisleUuid: THREE.MathUtils.generateUUID(),
|
||||
points: [tempPoints[0], newPoint],
|
||||
type: {
|
||||
aisleType: 'arrows-aisle',
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth,
|
||||
aisleLength: aisleLength,
|
||||
gapLength: gapLength
|
||||
}
|
||||
};
|
||||
addAisle(aisle);
|
||||
if (projectId) {
|
||||
createAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId)
|
||||
}
|
||||
setTempPoints([newPoint]);
|
||||
}
|
||||
} else if (aisleType === 'arc-aisle') {
|
||||
|
||||
if (tempPoints.length === 0) {
|
||||
setTempPoints([newPoint]);
|
||||
setIsCreating(true);
|
||||
} else {
|
||||
const aisle: Aisle = {
|
||||
aisleUuid: THREE.MathUtils.generateUUID(),
|
||||
points: [tempPoints[0], newPoint],
|
||||
type: {
|
||||
aisleType: 'arc-aisle',
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth,
|
||||
isFlipped: isFlipped
|
||||
}
|
||||
};
|
||||
addAisle(aisle);
|
||||
if (projectId) {
|
||||
createAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId)
|
||||
}
|
||||
setTempPoints([newPoint]);
|
||||
}
|
||||
} else if (aisleType === 'circle-aisle') {
|
||||
|
||||
if (tempPoints.length === 0) {
|
||||
setTempPoints([newPoint]);
|
||||
setIsCreating(true);
|
||||
} else {
|
||||
const aisle: Aisle = {
|
||||
aisleUuid: THREE.MathUtils.generateUUID(),
|
||||
points: [tempPoints[0], newPoint],
|
||||
type: {
|
||||
aisleType: 'circle-aisle',
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth
|
||||
}
|
||||
};
|
||||
addAisle(aisle);
|
||||
if (projectId) {
|
||||
createAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId)
|
||||
}
|
||||
setTempPoints([newPoint]);
|
||||
}
|
||||
} else if (aisleType === 'junction-aisle') {
|
||||
|
||||
if (tempPoints.length === 0) {
|
||||
setTempPoints([newPoint]);
|
||||
setIsCreating(true);
|
||||
} else {
|
||||
const aisle: Aisle = {
|
||||
aisleUuid: THREE.MathUtils.generateUUID(),
|
||||
points: [tempPoints[0], newPoint],
|
||||
type: {
|
||||
aisleType: 'junction-aisle',
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth,
|
||||
isFlipped: isFlipped
|
||||
}
|
||||
};
|
||||
addAisle(aisle);
|
||||
if (projectId) {
|
||||
createAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId)
|
||||
}
|
||||
setTempPoints([newPoint]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onContext = (event: any) => {
|
||||
event.preventDefault();
|
||||
if (isCreating) {
|
||||
setTempPoints([]);
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (toolMode === "Aisle" && toggleView) {
|
||||
canvasElement.addEventListener("mousedown", onMouseDown);
|
||||
canvasElement.addEventListener("mouseup", onMouseUp);
|
||||
canvasElement.addEventListener("mousemove", onMouseMove);
|
||||
canvasElement.addEventListener("click", onMouseClick);
|
||||
canvasElement.addEventListener("contextmenu", onContext);
|
||||
} else {
|
||||
if (tempPoints.length > 0 || isCreating) {
|
||||
setTempPoints([]);
|
||||
setIsCreating(false);
|
||||
}
|
||||
canvasElement.removeEventListener("mousedown", onMouseDown);
|
||||
canvasElement.removeEventListener("mouseup", onMouseUp);
|
||||
canvasElement.removeEventListener("mousemove", onMouseMove);
|
||||
canvasElement.removeEventListener("click", onMouseClick);
|
||||
canvasElement.removeEventListener("contextmenu", onContext);
|
||||
}
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("mousedown", onMouseDown);
|
||||
canvasElement.removeEventListener("mouseup", onMouseUp);
|
||||
canvasElement.removeEventListener("mousemove", onMouseMove);
|
||||
canvasElement.removeEventListener("click", onMouseClick);
|
||||
canvasElement.removeEventListener("contextmenu", onContext);
|
||||
};
|
||||
}, [gl, camera, scene, raycaster, pointer, plane, toggleView, toolMode, activeLayer, socket, tempPoints, isCreating, addAisle, getAislePointById, aisleType, aisleWidth, aisleColor, dashLength, gapLength, dotRadius, aisleLength, snappedPosition, snappedPoint]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{toggleView &&
|
||||
<>
|
||||
<group name='Aisle-Reference-Points-Group'>
|
||||
{tempPoints.map((point) => (
|
||||
<ReferencePoint key={point.pointUuid} point={point} />
|
||||
))}
|
||||
</group>
|
||||
|
||||
{tempPoints.length > 0 &&
|
||||
<ReferenceAisle tempPoints={tempPoints} />
|
||||
}
|
||||
</>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AisleCreator;
|
||||
774
app/src/modules/builder/aisle/aisleCreator/referenceAisle.tsx
Normal file
774
app/src/modules/builder/aisle/aisleCreator/referenceAisle.tsx
Normal file
@@ -0,0 +1,774 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { useFrame, useThree } from '@react-three/fiber';
|
||||
import { useActiveLayer, useToolMode, useToggleView } from '../../../../store/builder/store';
|
||||
import * as Constants from '../../../../types/world/worldConstants';
|
||||
import { Extrude, Html } from '@react-three/drei';
|
||||
import { useBuilderStore } from '../../../../store/builder/useBuilderStore';
|
||||
import { useDirectionalSnapping } from '../../point/helpers/useDirectionalSnapping';
|
||||
import { usePointSnapping } from '../../point/helpers/usePointSnapping';
|
||||
|
||||
interface ReferenceAisleProps {
|
||||
tempPoints: Point[];
|
||||
}
|
||||
|
||||
function ReferenceAisle({ tempPoints }: Readonly<ReferenceAisleProps>) {
|
||||
const { aisleType, aisleWidth, aisleColor, dashLength, gapLength, dotRadius, aisleLength, isFlipped, setSnappedPosition, setSnappedPoint } = useBuilderStore();
|
||||
const { pointer, raycaster, camera } = useThree();
|
||||
const { toolMode } = useToolMode();
|
||||
const { toggleView } = useToggleView();
|
||||
const { activeLayer } = useActiveLayer();
|
||||
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
||||
const finalPosition = useRef<[number, number, number] | null>(null);
|
||||
|
||||
const [tempAisle, setTempAisle] = useState<Aisle | null>(null);
|
||||
const [currentPosition, setCurrentPosition] = useState<[number, number, number]>(tempPoints[0]?.position);
|
||||
|
||||
const directionalSnap = useDirectionalSnapping(currentPosition, tempPoints[0]?.position || null);
|
||||
const { checkSnapForAisle } = usePointSnapping({ uuid: 'temp-aisle', pointType: 'Aisle', position: directionalSnap.position || [0, 0, 0] });
|
||||
|
||||
useFrame(() => {
|
||||
if (toolMode === "Aisle" && toggleView && tempPoints.length === 1) {
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
|
||||
setCurrentPosition([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]);
|
||||
|
||||
if (intersectionPoint) {
|
||||
const snapped = checkSnapForAisle([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]);
|
||||
|
||||
if (snapped.isSnapped && snapped.snappedPoint) {
|
||||
finalPosition.current = snapped.position;
|
||||
setSnappedPosition(snapped.position);
|
||||
setSnappedPoint(snapped.snappedPoint);
|
||||
} else if (directionalSnap.isSnapped) {
|
||||
finalPosition.current = directionalSnap.position;
|
||||
setSnappedPosition(directionalSnap.position);
|
||||
setSnappedPoint(null);
|
||||
} else {
|
||||
finalPosition.current = [intersectionPoint.x, intersectionPoint.y, intersectionPoint.z];
|
||||
setSnappedPosition(null);
|
||||
setSnappedPoint(null);
|
||||
}
|
||||
|
||||
if (!finalPosition.current) return;
|
||||
|
||||
if (aisleType === 'solid-aisle') {
|
||||
setTempAisle({
|
||||
aisleUuid: 'temp-aisle',
|
||||
points: [
|
||||
tempPoints[0],
|
||||
{
|
||||
pointUuid: 'temp-point',
|
||||
pointType: 'Aisle',
|
||||
position: finalPosition.current,
|
||||
layer: activeLayer
|
||||
}
|
||||
],
|
||||
type: {
|
||||
aisleType: aisleType,
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth
|
||||
}
|
||||
});
|
||||
} else if (aisleType === 'dashed-aisle') {
|
||||
setTempAisle({
|
||||
aisleUuid: 'temp-aisle',
|
||||
points: [
|
||||
tempPoints[0],
|
||||
{
|
||||
pointUuid: 'temp-point',
|
||||
pointType: 'Aisle',
|
||||
position: finalPosition.current,
|
||||
layer: activeLayer
|
||||
}
|
||||
],
|
||||
type: {
|
||||
aisleType: aisleType,
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth,
|
||||
dashLength: dashLength,
|
||||
gapLength: gapLength
|
||||
}
|
||||
});
|
||||
} else if (aisleType === 'dotted-aisle') {
|
||||
setTempAisle({
|
||||
aisleUuid: 'temp-aisle',
|
||||
points: [
|
||||
tempPoints[0],
|
||||
{
|
||||
pointUuid: 'temp-point',
|
||||
pointType: 'Aisle',
|
||||
position: finalPosition.current,
|
||||
layer: activeLayer
|
||||
}
|
||||
],
|
||||
type: {
|
||||
aisleType: aisleType,
|
||||
aisleColor: aisleColor,
|
||||
dotRadius: dotRadius,
|
||||
gapLength: gapLength
|
||||
}
|
||||
});
|
||||
} else if (aisleType === 'arrow-aisle' || aisleType === 'circle-aisle') {
|
||||
setTempAisle({
|
||||
aisleUuid: 'temp-aisle',
|
||||
points: [
|
||||
tempPoints[0],
|
||||
{
|
||||
pointUuid: 'temp-point',
|
||||
pointType: 'Aisle',
|
||||
position: finalPosition.current,
|
||||
layer: activeLayer
|
||||
}
|
||||
],
|
||||
type: {
|
||||
aisleType: aisleType,
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth,
|
||||
}
|
||||
});
|
||||
} else if (aisleType === 'arrows-aisle') {
|
||||
setTempAisle({
|
||||
aisleUuid: 'temp-aisle',
|
||||
points: [
|
||||
tempPoints[0],
|
||||
{
|
||||
pointUuid: 'temp-point',
|
||||
pointType: 'Aisle',
|
||||
position: finalPosition.current,
|
||||
layer: activeLayer
|
||||
}
|
||||
],
|
||||
type: {
|
||||
aisleType: aisleType,
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth,
|
||||
aisleLength: aisleLength,
|
||||
gapLength: gapLength
|
||||
}
|
||||
});
|
||||
} else if (aisleType === 'junction-aisle' || aisleType === 'arc-aisle') {
|
||||
setTempAisle({
|
||||
aisleUuid: 'temp-aisle',
|
||||
points: [
|
||||
tempPoints[0],
|
||||
{
|
||||
pointUuid: 'temp-point',
|
||||
pointType: 'Aisle',
|
||||
position: finalPosition.current,
|
||||
layer: activeLayer
|
||||
}
|
||||
],
|
||||
type: {
|
||||
aisleType: aisleType,
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth,
|
||||
isFlipped: isFlipped,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (tempAisle !== null) {
|
||||
setTempAisle(null);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setTempAisle(null);
|
||||
}, [toolMode, toggleView, tempPoints.length, aisleType, aisleWidth, aisleColor]);
|
||||
|
||||
if (!tempAisle) return null;
|
||||
|
||||
const renderAisle = () => {
|
||||
switch (aisleType) {
|
||||
case 'solid-aisle':
|
||||
return <SolidAisle aisle={tempAisle} />;
|
||||
case 'dashed-aisle':
|
||||
return <DashedAisle aisle={tempAisle} />;
|
||||
case 'dotted-aisle':
|
||||
return <DottedAisle aisle={tempAisle} />;
|
||||
case 'arrow-aisle':
|
||||
return <ArrowAisle aisle={tempAisle} />;
|
||||
case 'arrows-aisle':
|
||||
return <ArrowsAisle aisle={tempAisle} />;
|
||||
case 'circle-aisle':
|
||||
return <CircleAisle aisle={tempAisle} />;
|
||||
case 'junction-aisle':
|
||||
return <JunctionAisle aisle={tempAisle} />;
|
||||
case 'arc-aisle':
|
||||
return <ArcAisle aisle={tempAisle} />
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const textPosition = new THREE.Vector3().addVectors(new THREE.Vector3(...tempAisle.points[0].position), new THREE.Vector3(...tempAisle.points[1].position)).divideScalar(2);
|
||||
const distance = new THREE.Vector3(...tempAisle.points[0].position).distanceTo(new THREE.Vector3(...tempAisle.points[1].position));
|
||||
|
||||
const rendertext = () => {
|
||||
return (
|
||||
<>
|
||||
{toggleView &&
|
||||
<Html
|
||||
key={tempAisle.aisleUuid}
|
||||
userData={tempAisle}
|
||||
position={[textPosition.x, 1, textPosition.z]}
|
||||
wrapperClass="distance-text-wrapper"
|
||||
className="distance-text"
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
sprite
|
||||
>
|
||||
<div
|
||||
key={tempAisle.aisleUuid}
|
||||
className={`distance ${tempAisle.aisleUuid}`}
|
||||
>
|
||||
{distance.toFixed(2)} m
|
||||
</div>
|
||||
</Html>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<group name='Aisle-Reference-Group'>
|
||||
{renderAisle()}
|
||||
{rendertext()}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReferenceAisle;
|
||||
|
||||
function SolidAisle({ aisle }: { readonly aisle: Aisle }) {
|
||||
|
||||
const shape = useMemo(() => {
|
||||
if (aisle.points.length < 2 || aisle.type.aisleType !== 'solid-aisle') return null;
|
||||
|
||||
const start = new THREE.Vector3(...aisle.points[0].position);
|
||||
const end = new THREE.Vector3(...aisle.points[1].position);
|
||||
const width = aisle.type.aisleWidth || 0.1;
|
||||
|
||||
const direction = new THREE.Vector3().subVectors(end, start).normalize();
|
||||
const perp = new THREE.Vector3(-direction.z, 0, direction.x).normalize();
|
||||
|
||||
const leftStart = new THREE.Vector3().copy(start).addScaledVector(perp, width / 2);
|
||||
const rightStart = new THREE.Vector3().copy(start).addScaledVector(perp, -width / 2);
|
||||
const leftEnd = new THREE.Vector3().copy(end).addScaledVector(perp, width / 2);
|
||||
const rightEnd = new THREE.Vector3().copy(end).addScaledVector(perp, -width / 2);
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
shape.moveTo(leftStart.x, leftStart.z);
|
||||
shape.lineTo(leftEnd.x, leftEnd.z);
|
||||
shape.lineTo(rightEnd.x, rightEnd.z);
|
||||
shape.lineTo(rightStart.x, rightStart.z);
|
||||
shape.closePath();
|
||||
|
||||
return shape;
|
||||
}, [aisle]);
|
||||
|
||||
if (!shape) return null;
|
||||
|
||||
return (
|
||||
<group
|
||||
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
>
|
||||
<Extrude
|
||||
args={[shape, { depth: 0.01, bevelEnabled: false }]}
|
||||
receiveShadow
|
||||
castShadow
|
||||
>
|
||||
<meshStandardMaterial color={aisle.type.aisleColor || '#ffffff'} side={THREE.DoubleSide} />
|
||||
</Extrude>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
function DashedAisle({ aisle }: { readonly aisle: Aisle }) {
|
||||
const shapes = useMemo(() => {
|
||||
if (aisle.points.length < 2 || aisle.type.aisleType !== 'dashed-aisle') return [];
|
||||
|
||||
const start = new THREE.Vector3(...aisle.points[0].position);
|
||||
const end = new THREE.Vector3(...aisle.points[1].position);
|
||||
const width = aisle.type.aisleWidth || 0.1;
|
||||
const dashLength = aisle.type.dashLength || 0.5;
|
||||
const gapLength = aisle.type.gapLength || 0.3;
|
||||
|
||||
const direction = new THREE.Vector3().subVectors(end, start).normalize();
|
||||
const perp = new THREE.Vector3(-direction.z, 0, direction.x).normalize();
|
||||
|
||||
const totalLength = new THREE.Vector3().subVectors(end, start).length();
|
||||
const segmentCount = Math.floor((totalLength + gapLength) / (dashLength + gapLength));
|
||||
|
||||
const shapes = [];
|
||||
const directionNormalized = new THREE.Vector3().subVectors(end, start).normalize();
|
||||
|
||||
for (let i = 0; i < segmentCount; i++) {
|
||||
const segmentStart = new THREE.Vector3().copy(start).addScaledVector(directionNormalized, i * (dashLength + gapLength));
|
||||
const segmentEnd = new THREE.Vector3().copy(segmentStart).addScaledVector(directionNormalized, dashLength);
|
||||
|
||||
const leftStart = new THREE.Vector3().copy(segmentStart).addScaledVector(perp, width / 2);
|
||||
const rightStart = new THREE.Vector3().copy(segmentStart).addScaledVector(perp, -width / 2);
|
||||
const leftEnd = new THREE.Vector3().copy(segmentEnd).addScaledVector(perp, width / 2);
|
||||
const rightEnd = new THREE.Vector3().copy(segmentEnd).addScaledVector(perp, -width / 2);
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
shape.moveTo(leftStart.x, leftStart.z);
|
||||
shape.lineTo(leftEnd.x, leftEnd.z);
|
||||
shape.lineTo(rightEnd.x, rightEnd.z);
|
||||
shape.lineTo(rightStart.x, rightStart.z);
|
||||
shape.closePath();
|
||||
|
||||
shapes.push(shape);
|
||||
}
|
||||
|
||||
return shapes;
|
||||
}, [aisle]);
|
||||
|
||||
if (shapes.length === 0) return null;
|
||||
|
||||
return (
|
||||
<group
|
||||
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
>
|
||||
{shapes.map((shape, index) => (
|
||||
<Extrude
|
||||
key={index}
|
||||
args={[shape, { depth: 0.01, bevelEnabled: false }]}
|
||||
receiveShadow
|
||||
castShadow
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={aisle.type.aisleColor || '#ffffff'}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</Extrude>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
function DottedAisle({ aisle }: { readonly aisle: Aisle }) {
|
||||
const shapes = useMemo(() => {
|
||||
if (aisle.points.length < 2 || aisle.type.aisleType !== 'dotted-aisle') return [];
|
||||
|
||||
const start = new THREE.Vector3(...aisle.points[0].position);
|
||||
const end = new THREE.Vector3(...aisle.points[1].position);
|
||||
const width = aisle.type.dotRadius || 0.1;
|
||||
const dotSpacing = aisle.type.gapLength || 0.5;
|
||||
const dotRadius = width * 0.6;
|
||||
|
||||
const totalLength = new THREE.Vector3().subVectors(end, start).length();
|
||||
const dotCount = Math.floor((totalLength + (dotSpacing / 2)) / dotSpacing);
|
||||
|
||||
const shapes = [];
|
||||
const directionNormalized = new THREE.Vector3().subVectors(end, start).normalize();
|
||||
|
||||
for (let i = 0; i < dotCount; i++) {
|
||||
const dotCenter = new THREE.Vector3().copy(start).addScaledVector(directionNormalized, i * dotSpacing + dotSpacing / 2);
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
shape.absarc(dotCenter.x, dotCenter.z, dotRadius, 0, Math.PI * 2, false);
|
||||
|
||||
shapes.push(shape);
|
||||
}
|
||||
|
||||
return shapes;
|
||||
}, [aisle]);
|
||||
|
||||
if (shapes.length === 0) return null;
|
||||
|
||||
return (
|
||||
<group
|
||||
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
>
|
||||
{shapes.map((shape, index) => (
|
||||
<Extrude
|
||||
key={index}
|
||||
args={[shape, { depth: 0.01, bevelEnabled: false }]}
|
||||
receiveShadow
|
||||
castShadow
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={aisle.type.aisleColor || '#ffffff'}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</Extrude>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
function ArrowsAisle({ aisle }: { readonly aisle: Aisle }) {
|
||||
const arrows = useMemo(() => {
|
||||
if (aisle.points.length < 2 || aisle.type.aisleType !== 'arrows-aisle') return [];
|
||||
|
||||
const start = new THREE.Vector3(...aisle.points[0].position);
|
||||
const end = new THREE.Vector3(...aisle.points[1].position);
|
||||
const width = aisle.type.aisleWidth || 0.1;
|
||||
const arrowLength = aisle.type.aisleLength || 0.6;
|
||||
const spacing = aisle.type.gapLength || 0.6;
|
||||
|
||||
const direction = new THREE.Vector3().subVectors(end, start);
|
||||
const length = direction.length();
|
||||
direction.normalize();
|
||||
|
||||
const count = Math.floor((length + spacing) / (arrowLength + spacing));
|
||||
|
||||
const arrowShapes: { shape: THREE.Shape; position: THREE.Vector3; rotationY: number }[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const initialOffset = arrowLength;
|
||||
const center = new THREE.Vector3().copy(start).addScaledVector(direction, initialOffset + i * (arrowLength + spacing));
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
const w = width * 0.8;
|
||||
const h = arrowLength;
|
||||
|
||||
shape.moveTo(0, 0);
|
||||
shape.lineTo(w, h * 0.6);
|
||||
shape.lineTo(w * 0.4, h * 0.6);
|
||||
shape.lineTo(w * 0.4, h);
|
||||
shape.lineTo(-w * 0.4, h);
|
||||
shape.lineTo(-w * 0.4, h * 0.6);
|
||||
shape.lineTo(-w, h * 0.6);
|
||||
shape.lineTo(0, 0);
|
||||
|
||||
const angle = Math.atan2(direction.x, direction.z) + Math.PI;
|
||||
|
||||
arrowShapes.push({ shape, position: center, rotationY: angle });
|
||||
}
|
||||
|
||||
return arrowShapes;
|
||||
}, [aisle]);
|
||||
|
||||
if (arrows.length === 0) return null;
|
||||
|
||||
return (
|
||||
<group
|
||||
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
>
|
||||
{arrows.map(({ shape, position, rotationY }, index) => (
|
||||
<group key={index} position={[position.x, position.z, 0]} rotation={[0, 0, -rotationY]}>
|
||||
<Extrude
|
||||
args={[shape, { depth: 0.01, bevelEnabled: false }]}
|
||||
receiveShadow
|
||||
castShadow
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={aisle.type.aisleColor || '#ffffff'}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</Extrude>
|
||||
</group>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
function ArrowAisle({ aisle }: { readonly aisle: Aisle }) {
|
||||
const arrow = useMemo(() => {
|
||||
if (aisle.points.length < 2 || aisle.type.aisleType !== 'arrow-aisle') return null;
|
||||
|
||||
const start = new THREE.Vector3(...aisle.points[0].position);
|
||||
const end = new THREE.Vector3(...aisle.points[1].position);
|
||||
const width = aisle.type.aisleWidth || 0.1;
|
||||
|
||||
const direction = new THREE.Vector3().subVectors(end, start);
|
||||
const length = direction.length();
|
||||
direction.normalize();
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
const arrowHeadLength = width * 2;
|
||||
const shaftLength = length - arrowHeadLength;
|
||||
|
||||
if (shaftLength > 0) {
|
||||
shape.moveTo(0, 0);
|
||||
shape.lineTo(width, -arrowHeadLength);
|
||||
shape.lineTo(width / 2, -arrowHeadLength);
|
||||
shape.lineTo(width / 2, -length);
|
||||
shape.lineTo(-width / 2, -length);
|
||||
shape.lineTo(-width / 2, -arrowHeadLength);
|
||||
shape.lineTo(-width, -arrowHeadLength);
|
||||
shape.lineTo(0, 0);
|
||||
} else {
|
||||
shape.moveTo(0, 0);
|
||||
shape.lineTo(width, -length);
|
||||
shape.lineTo(-width, -length);
|
||||
shape.lineTo(0, 0);
|
||||
}
|
||||
|
||||
shape.closePath();
|
||||
|
||||
const position = end;
|
||||
const angle = Math.atan2(direction.x, direction.z);
|
||||
|
||||
return { shape, position, rotationY: angle };
|
||||
}, [aisle]);
|
||||
|
||||
if (!arrow) return null;
|
||||
|
||||
return (
|
||||
<group
|
||||
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
>
|
||||
<group position={[arrow.position.x, arrow.position.z, 0]} rotation={[0, 0, -arrow.rotationY]}>
|
||||
<Extrude
|
||||
args={[arrow.shape, { depth: 0.01, bevelEnabled: false }]}
|
||||
receiveShadow
|
||||
castShadow
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={aisle.type.aisleColor || '#ffffff'}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</Extrude>
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
function CircleAisle({ aisle }: { readonly aisle: Aisle }) {
|
||||
const circle = useMemo(() => {
|
||||
if (aisle.points.length < 2 || aisle.type.aisleType !== 'circle-aisle') return null;
|
||||
|
||||
const center = new THREE.Vector3(...aisle.points[0].position);
|
||||
const widthCenter = new THREE.Vector3(...aisle.points[1].position);
|
||||
const width = aisle.type.aisleWidth || 0.1;
|
||||
|
||||
const middleRadius = center.distanceTo(widthCenter);
|
||||
|
||||
const innerRadius = Math.max(0, middleRadius - width / 2);
|
||||
const outerRadius = middleRadius + width / 2;
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
shape.absarc(center.x, center.z, outerRadius, 0, Math.PI * 2, false);
|
||||
|
||||
if (innerRadius > 0) {
|
||||
const hole = new THREE.Path();
|
||||
hole.absarc(center.x, center.z, innerRadius, 0, Math.PI * 2, true);
|
||||
shape.holes.push(hole);
|
||||
}
|
||||
|
||||
return {
|
||||
shape,
|
||||
rotationY: 0
|
||||
};
|
||||
}, [aisle]);
|
||||
|
||||
if (!circle) return null;
|
||||
|
||||
return (
|
||||
<group
|
||||
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
>
|
||||
<Extrude
|
||||
args={[circle.shape, {
|
||||
depth: 0.01,
|
||||
bevelEnabled: false,
|
||||
steps: 1,
|
||||
curveSegments: 64
|
||||
}]}
|
||||
receiveShadow
|
||||
castShadow
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={aisle.type.aisleColor || '#ffffff'}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</Extrude>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
function JunctionAisle({ aisle }: { readonly aisle: Aisle }) {
|
||||
const arrows = useMemo(() => {
|
||||
if (aisle.points.length < 2 || aisle.type.aisleType !== 'junction-aisle') return null;
|
||||
|
||||
const start = new THREE.Vector3(...aisle.points[0].position);
|
||||
const end = new THREE.Vector3(...aisle.points[1].position);
|
||||
const width = aisle.type.aisleWidth || 0.1;
|
||||
const isFlipped = aisle.type.isFlipped || false;
|
||||
|
||||
const direction = new THREE.Vector3().subVectors(end, start);
|
||||
const length = direction.length();
|
||||
direction.normalize();
|
||||
|
||||
const mainShape = new THREE.Shape();
|
||||
const arrowHeadLength = width * 2;
|
||||
const shaftLength = length - arrowHeadLength;
|
||||
|
||||
if (shaftLength > 0) {
|
||||
mainShape.moveTo(0, 0);
|
||||
mainShape.lineTo(width, -arrowHeadLength);
|
||||
mainShape.lineTo(width / 2, -arrowHeadLength);
|
||||
mainShape.lineTo(width / 2, -length);
|
||||
mainShape.lineTo(-width / 2, -length);
|
||||
mainShape.lineTo(-width / 2, -arrowHeadLength);
|
||||
mainShape.lineTo(-width, -arrowHeadLength);
|
||||
mainShape.lineTo(0, 0);
|
||||
} else {
|
||||
mainShape.moveTo(0, 0);
|
||||
mainShape.lineTo(width, -length);
|
||||
mainShape.lineTo(-width, -length);
|
||||
mainShape.lineTo(0, 0);
|
||||
}
|
||||
mainShape.closePath();
|
||||
|
||||
const secondaryLength = length / 4;
|
||||
const secondaryShape = new THREE.Shape();
|
||||
const secondaryHeadLength = width * 2;
|
||||
const secondaryShaftLength = secondaryLength - secondaryHeadLength;
|
||||
|
||||
if (secondaryShaftLength > 0) {
|
||||
secondaryShape.moveTo(0, 0);
|
||||
secondaryShape.lineTo(width / 2, 0);
|
||||
secondaryShape.lineTo(width / 2, secondaryShaftLength);
|
||||
secondaryShape.lineTo(width, secondaryShaftLength);
|
||||
secondaryShape.lineTo(0, secondaryLength);
|
||||
secondaryShape.lineTo(-width, secondaryShaftLength);
|
||||
secondaryShape.lineTo(-width / 2, secondaryShaftLength);
|
||||
secondaryShape.lineTo(-width / 2, 0);
|
||||
secondaryShape.lineTo(0, 0);
|
||||
} else {
|
||||
secondaryShape.moveTo(0, 0);
|
||||
secondaryShape.lineTo(width, 0);
|
||||
secondaryShape.lineTo(0, secondaryLength);
|
||||
secondaryShape.lineTo(-width, 0);
|
||||
secondaryShape.lineTo(0, 0);
|
||||
}
|
||||
secondaryShape.closePath();
|
||||
|
||||
const mainPosition = end;
|
||||
const mainAngle = Math.atan2(direction.x, direction.z);
|
||||
|
||||
const perpendicularDirection = isFlipped
|
||||
? new THREE.Vector3(direction.z, 0, -direction.x).normalize()
|
||||
: new THREE.Vector3(-direction.z, 0, direction.x).normalize();
|
||||
|
||||
const secondaryAngle = Math.atan2(perpendicularDirection.x, perpendicularDirection.z);
|
||||
|
||||
const secondaryPosition = new THREE.Vector3().lerpVectors(start, end, 0.75);
|
||||
|
||||
return [
|
||||
{ shape: mainShape, position: mainPosition, rotationY: mainAngle },
|
||||
{
|
||||
shape: secondaryShape,
|
||||
position: secondaryPosition,
|
||||
rotationY: secondaryAngle + Math.PI
|
||||
}
|
||||
];
|
||||
}, [aisle]);
|
||||
|
||||
if (!arrows) return null;
|
||||
|
||||
return (
|
||||
<group
|
||||
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
>
|
||||
{arrows.map((arrow, index) => (
|
||||
<group key={index} position={[arrow.position.x, arrow.position.z, 0]} rotation={[0, 0, -arrow.rotationY]}>
|
||||
<Extrude
|
||||
args={[arrow.shape, { depth: 0.01, bevelEnabled: false }]}
|
||||
receiveShadow
|
||||
castShadow
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={aisle.type.aisleColor || '#ffffff'}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</Extrude>
|
||||
</group>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
function ArcAisle({ aisle }: { readonly aisle: Aisle }) {
|
||||
const arc = useMemo(() => {
|
||||
if (aisle.points.length < 2 || aisle.type.aisleType !== 'arc-aisle') return null;
|
||||
|
||||
const start = new THREE.Vector3(...aisle.points[0].position);
|
||||
const end = new THREE.Vector3(...aisle.points[1].position);
|
||||
const width = aisle.type.aisleWidth || 0.5;
|
||||
const isFlipped = aisle.type.isFlipped || false;
|
||||
|
||||
const direction = new THREE.Vector3().subVectors(end, start);
|
||||
const length = direction.length();
|
||||
direction.normalize();
|
||||
|
||||
const perpendicular = new THREE.Vector3(-direction.z, 0, direction.x).normalize();
|
||||
if (!isFlipped) perpendicular.negate();
|
||||
|
||||
const arcHeight = length * 0.25;
|
||||
const midPoint = new THREE.Vector3().lerpVectors(start, end, 0.5);
|
||||
const controlPoint = new THREE.Vector3().copy(midPoint).addScaledVector(perpendicular, arcHeight);
|
||||
|
||||
const widthOffset = perpendicular.clone().multiplyScalar(width / 2);
|
||||
|
||||
const p1 = new THREE.Vector3().copy(start).add(widthOffset);
|
||||
const p2 = new THREE.Vector3().copy(end).add(widthOffset);
|
||||
const p3 = new THREE.Vector3().copy(end).sub(widthOffset);
|
||||
const p4 = new THREE.Vector3().copy(start).sub(widthOffset);
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
shape.moveTo(p1.x, p1.z);
|
||||
|
||||
shape.quadraticCurveTo(
|
||||
controlPoint.x + widthOffset.x,
|
||||
controlPoint.z + widthOffset.z,
|
||||
p2.x, p2.z
|
||||
);
|
||||
|
||||
shape.lineTo(p3.x, p3.z);
|
||||
|
||||
shape.quadraticCurveTo(
|
||||
controlPoint.x - widthOffset.x,
|
||||
controlPoint.z - widthOffset.z,
|
||||
p4.x, p4.z
|
||||
);
|
||||
|
||||
shape.lineTo(p1.x, p1.z);
|
||||
|
||||
return {
|
||||
shape,
|
||||
position: new THREE.Vector3(0, 0, 0),
|
||||
rotationY: 0
|
||||
};
|
||||
}, [aisle]);
|
||||
|
||||
|
||||
if (!arc) return null;
|
||||
|
||||
return (
|
||||
<group
|
||||
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
>
|
||||
<Extrude
|
||||
args={[arc.shape, {
|
||||
depth: 0.01,
|
||||
bevelEnabled: false,
|
||||
curveSegments: 32
|
||||
}]}
|
||||
receiveShadow
|
||||
castShadow
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={aisle.type.aisleColor || '#ffffff'}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</Extrude>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
35
app/src/modules/builder/aisle/aislesGroup.tsx
Normal file
35
app/src/modules/builder/aisle/aislesGroup.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useEffect } from 'react'
|
||||
import AisleCreator from './aisleCreator/aisleCreator'
|
||||
import AisleInstances from './Instances/aisleInstances'
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { getAisleApi } from '../../../services/factoryBuilder/aisle/getAisleApi';
|
||||
import { useAisleStore } from '../../../store/builder/useAisleStore';
|
||||
|
||||
function AislesGroup() {
|
||||
const { projectId } = useParams();
|
||||
const { setAisles } = useAisleStore();
|
||||
useEffect(() => {
|
||||
const fetchAisle = async () => {
|
||||
if (projectId) {
|
||||
const aisles = await getAisleApi(projectId);
|
||||
setAisles(aisles);
|
||||
}
|
||||
}
|
||||
fetchAisle()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
||||
<>
|
||||
|
||||
<AisleCreator />
|
||||
|
||||
|
||||
<AisleInstances />
|
||||
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default AislesGroup
|
||||
299
app/src/modules/builder/asset/assetsGroup.tsx
Normal file
299
app/src/modules/builder/asset/assetsGroup.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
import * as THREE from "three"
|
||||
import { useEffect } from 'react'
|
||||
import { getFloorAssets } from '../../../services/factoryBuilder/assest/floorAsset/getFloorItemsApi';
|
||||
import { useLoadingProgress, useSelectedFloorItem, useSelectedItem, useSocketStore } from '../../../store/builder/store';
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
|
||||
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
|
||||
import { FloorItems, RefGroup, RefMesh } from "../../../types/world/worldTypes";
|
||||
import { useAssetsStore } from "../../../store/builder/useAssetStore";
|
||||
import { useEventsStore } from "../../../store/simulation/useEventsStore";
|
||||
import Models from "./models/models";
|
||||
import useModuleStore from "../../../store/useModuleStore";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import { CameraControls } from "@react-three/drei";
|
||||
import addAssetModel from "./functions/addAssetModel";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
const gltfLoaderWorker = new Worker(
|
||||
new URL(
|
||||
"../../../services/factoryBuilder/webWorkers/gltfLoaderWorker.js",
|
||||
import.meta.url
|
||||
)
|
||||
);
|
||||
|
||||
function AssetsGroup({ floorGroup, plane }: { readonly floorGroup: RefGroup, readonly plane: RefMesh }) {
|
||||
const { activeModule } = useModuleStore();
|
||||
const { socket } = useSocketStore();
|
||||
const { controls, gl, pointer, camera, raycaster } = useThree();
|
||||
const { setLoadingProgress } = useLoadingProgress();
|
||||
const { setAssets, addAsset } = useAssetsStore();
|
||||
const { addEvent } = useEventsStore();
|
||||
const { setSelectedFloorItem } = useSelectedFloorItem();
|
||||
const { selectedItem, setSelectedItem } = useSelectedItem();
|
||||
const { projectId } = useParams();
|
||||
const email = localStorage.getItem("email");
|
||||
const organization = email!.split("@")[1].split(".")[0];
|
||||
const userId = localStorage.getItem("userId")!;
|
||||
|
||||
const loader = new GLTFLoader();
|
||||
const dracoLoader = new DRACOLoader();
|
||||
|
||||
dracoLoader.setDecoderPath(
|
||||
"https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/"
|
||||
);
|
||||
loader.setDRACOLoader(dracoLoader);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
let totalAssets = 0;
|
||||
let loadedAssets = 0;
|
||||
|
||||
const updateLoadingProgress = (progress: number) => {
|
||||
if (progress < 100) {
|
||||
setLoadingProgress(progress);
|
||||
} else if (progress === 100) {
|
||||
setTimeout(() => {
|
||||
setLoadingProgress(100);
|
||||
setTimeout(() => {
|
||||
setLoadingProgress(0);
|
||||
}, 1500);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
getFloorAssets(organization, projectId).then((data) => {
|
||||
if (data.length > 0) {
|
||||
const uniqueItems = (data as FloorItems).filter((item, index, self) => index === self.findIndex((t) => t.modelfileID === item.modelfileID));
|
||||
totalAssets = uniqueItems.length;
|
||||
if (totalAssets === 0) {
|
||||
updateLoadingProgress(100);
|
||||
return;
|
||||
}
|
||||
gltfLoaderWorker.postMessage({ floorItems: uniqueItems });
|
||||
} else {
|
||||
gltfLoaderWorker.postMessage({ floorItems: [] });
|
||||
updateLoadingProgress(100);
|
||||
}
|
||||
});
|
||||
|
||||
gltfLoaderWorker.onmessage = async (event) => {
|
||||
if (event.data.message === "gltfLoaded" && event.data.modelBlob) {
|
||||
const blobUrl = URL.createObjectURL(event.data.modelBlob);
|
||||
loader.load(blobUrl, (gltf) => {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
THREE.Cache.remove(blobUrl);
|
||||
THREE.Cache.add(event.data.modelID, gltf);
|
||||
|
||||
loadedAssets++;
|
||||
const progress = Math.round((loadedAssets / totalAssets) * 100);
|
||||
updateLoadingProgress(progress);
|
||||
|
||||
if (loadedAssets === totalAssets) {
|
||||
const assets: Asset[] = [];
|
||||
getFloorAssets(organization, projectId).then((data: FloorItems) => {
|
||||
data.forEach((item) => {
|
||||
if (item.eventData) {
|
||||
assets.push({
|
||||
modelUuid: item.modelUuid,
|
||||
modelName: item.modelName,
|
||||
assetId: item.modelfileID,
|
||||
position: item.position,
|
||||
rotation: [item.rotation.x, item.rotation.y, item.rotation.z],
|
||||
isLocked: item.isLocked,
|
||||
isCollidable: false,
|
||||
isVisible: item.isVisible,
|
||||
opacity: 1,
|
||||
eventData: item.eventData
|
||||
})
|
||||
|
||||
if (item.eventData.type === "Vehicle") {
|
||||
const vehicleEvent: VehicleEventSchema = {
|
||||
modelUuid: item.modelUuid,
|
||||
modelName: item.modelName,
|
||||
position: item.position,
|
||||
rotation: [item.rotation.x, item.rotation.y, item.rotation.z],
|
||||
state: "idle",
|
||||
type: "vehicle",
|
||||
speed: 1,
|
||||
point: {
|
||||
uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(),
|
||||
position: [item.eventData.point?.position[0] || 0, item.eventData.point?.position[1] || 0, item.eventData.point?.position[2] || 0],
|
||||
rotation: [item.eventData.point?.rotation[0] || 0, item.eventData.point?.rotation[1] || 0, item.eventData.point?.rotation[2] || 0],
|
||||
action: {
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: "Action 1",
|
||||
actionType: "travel",
|
||||
unLoadDuration: 5,
|
||||
loadCapacity: 1,
|
||||
steeringAngle: 0,
|
||||
pickUpPoint: null,
|
||||
unLoadPoint: null,
|
||||
triggers: []
|
||||
}
|
||||
}
|
||||
};
|
||||
addEvent(vehicleEvent);
|
||||
} else if (item.eventData.type === "Conveyor") {
|
||||
const ConveyorEvent: ConveyorEventSchema = {
|
||||
modelUuid: item.modelUuid,
|
||||
modelName: item.modelName,
|
||||
position: item.position,
|
||||
rotation: [item.rotation.x, item.rotation.y, item.rotation.z],
|
||||
state: "idle",
|
||||
type: "transfer",
|
||||
speed: 1,
|
||||
points: item.eventData.points?.map((point: any, index: number) => ({
|
||||
uuid: point.uuid || THREE.MathUtils.generateUUID(),
|
||||
position: [point.position[0], point.position[1], point.position[2]],
|
||||
rotation: [point.rotation[0], point.rotation[1], point.rotation[2]],
|
||||
action: {
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: `Action 1`,
|
||||
actionType: 'default',
|
||||
material: 'Default material',
|
||||
delay: 0,
|
||||
spawnInterval: 5,
|
||||
spawnCount: 1,
|
||||
triggers: []
|
||||
}
|
||||
})) || [],
|
||||
};
|
||||
addEvent(ConveyorEvent);
|
||||
} else if (item.eventData.type === "StaticMachine") {
|
||||
const machineEvent: MachineEventSchema = {
|
||||
modelUuid: item.modelUuid,
|
||||
modelName: item.modelName,
|
||||
position: item.position,
|
||||
rotation: [item.rotation.x, item.rotation.y, item.rotation.z],
|
||||
state: "idle",
|
||||
type: "machine",
|
||||
point: {
|
||||
uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(),
|
||||
position: [item.eventData.point?.position[0] || 0, item.eventData.point?.position[1] || 0, item.eventData.point?.position[2] || 0],
|
||||
rotation: [item.eventData.point?.rotation[0] || 0, item.eventData.point?.rotation[1] || 0, item.eventData.point?.rotation[2] || 0],
|
||||
action: {
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: "Action 1",
|
||||
actionType: "process",
|
||||
processTime: 10,
|
||||
swapMaterial: "Default Material",
|
||||
triggers: []
|
||||
}
|
||||
}
|
||||
};
|
||||
addEvent(machineEvent);
|
||||
} else if (item.eventData.type === "ArmBot") {
|
||||
const roboticArmEvent: RoboticArmEventSchema = {
|
||||
modelUuid: item.modelUuid,
|
||||
modelName: item.modelName,
|
||||
position: item.position,
|
||||
rotation: [item.rotation.x, item.rotation.y, item.rotation.z],
|
||||
state: "idle",
|
||||
type: "roboticArm",
|
||||
speed: 1,
|
||||
point: {
|
||||
uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(),
|
||||
position: [item.eventData.point?.position[0] || 0, item.eventData.point?.position[1] || 0, item.eventData.point?.position[2] || 0],
|
||||
rotation: [item.eventData.point?.rotation[0] || 0, item.eventData.point?.rotation[1] || 0, item.eventData.point?.rotation[2] || 0],
|
||||
actions: [
|
||||
{
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: "Action 1",
|
||||
actionType: "pickAndPlace",
|
||||
process: {
|
||||
startPoint: null,
|
||||
endPoint: null
|
||||
},
|
||||
triggers: []
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
addEvent(roboticArmEvent);
|
||||
} else if (item.eventData.type === 'Storage') {
|
||||
const storageEvent: StorageEventSchema = {
|
||||
modelUuid: item.modelUuid,
|
||||
modelName: item.modelName,
|
||||
position: item.position,
|
||||
rotation: [item.rotation.x, item.rotation.y, item.rotation.z], state: "idle",
|
||||
type: "storageUnit",
|
||||
point: {
|
||||
uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(),
|
||||
position: [item.eventData.point?.position[0] || 0, item.eventData.point?.position[1] || 0, item.eventData.point?.position[2] || 0],
|
||||
rotation: [item.eventData.point?.rotation[0] || 0, item.eventData.point?.rotation[1] || 0, item.eventData.point?.rotation[2] || 0],
|
||||
action: {
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: "Action 1",
|
||||
actionType: "store",
|
||||
storageCapacity: 10,
|
||||
triggers: []
|
||||
}
|
||||
}
|
||||
};
|
||||
addEvent(storageEvent);
|
||||
}
|
||||
} else {
|
||||
assets.push({
|
||||
modelUuid: item.modelUuid,
|
||||
modelName: item.modelName,
|
||||
assetId: item.modelfileID,
|
||||
position: item.position,
|
||||
rotation: [item.rotation.x, item.rotation.y, item.rotation.z],
|
||||
isLocked: item.isLocked,
|
||||
isCollidable: false,
|
||||
isVisible: item.isVisible,
|
||||
opacity: 1,
|
||||
})
|
||||
}
|
||||
})
|
||||
setAssets(assets);
|
||||
})
|
||||
updateLoadingProgress(100);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const canvasElement = gl.domElement;
|
||||
const onDrop = (event: any) => {
|
||||
if (!event.dataTransfer?.files[0]) return;
|
||||
|
||||
if (selectedItem.id !== "" && event.dataTransfer?.files[0] && selectedItem.category !== 'Fenestration') {
|
||||
|
||||
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
|
||||
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
||||
|
||||
addAssetModel(raycaster, camera, pointer, floorGroup, socket, selectedItem, setSelectedItem, addEvent, addAsset, plane, projectId, userId);
|
||||
}
|
||||
};
|
||||
|
||||
const onDragOver = (event: any) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
|
||||
if (activeModule === "builder") {
|
||||
canvasElement.addEventListener("drop", onDrop);
|
||||
canvasElement.addEventListener("dragover", onDragOver);
|
||||
} else {
|
||||
if ((controls as CameraControls)) {
|
||||
const target = (controls as CameraControls).getTarget(new THREE.Vector3());
|
||||
(controls as CameraControls).setTarget(target.x, 0, target.z, true);
|
||||
setSelectedFloorItem(null);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("drop", onDrop);
|
||||
canvasElement.removeEventListener("dragover", onDragOver);
|
||||
};
|
||||
}, [selectedItem, camera, pointer, activeModule, controls]);
|
||||
|
||||
return (
|
||||
<Models />
|
||||
)
|
||||
}
|
||||
|
||||
export default AssetsGroup;
|
||||
427
app/src/modules/builder/asset/functions/addAssetModel.ts
Normal file
427
app/src/modules/builder/asset/functions/addAssetModel.ts
Normal file
@@ -0,0 +1,427 @@
|
||||
import * as THREE from "three";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
|
||||
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
import { retrieveGLTF, storeGLTF } from "../../../../utils/indexDB/idbUtils";
|
||||
// import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi';
|
||||
import { Socket } from "socket.io-client";
|
||||
import * as CONSTANTS from "../../../../types/world/worldConstants";
|
||||
import PointsCalculator from "../../../simulation/events/points/functions/pointsCalculator";
|
||||
|
||||
async function addAssetModel(
|
||||
raycaster: THREE.Raycaster,
|
||||
camera: THREE.Camera,
|
||||
pointer: THREE.Vector2,
|
||||
floorGroup: Types.RefGroup,
|
||||
socket: Socket<any>,
|
||||
selectedItem: any,
|
||||
setSelectedItem: any,
|
||||
addEvent: (event: EventsSchema) => void,
|
||||
addAsset: (asset: Asset) => void,
|
||||
plane: Types.RefMesh,
|
||||
projectId?: string,
|
||||
userId?: string
|
||||
): Promise<void> {
|
||||
////////// Load Floor GLtf's and set the positions, rotation, type etc. in state and store in localstorage //////////
|
||||
|
||||
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
|
||||
|
||||
try {
|
||||
const loader = new GLTFLoader();
|
||||
const dracoLoader = new DRACOLoader();
|
||||
|
||||
dracoLoader.setDecoderPath("https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/");
|
||||
loader.setDRACOLoader(dracoLoader);
|
||||
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const floorIntersections = raycaster.intersectObjects(floorGroup.current.children, true);
|
||||
const intersectedFloor = floorIntersections.find((intersect) => intersect.object.name.includes("Floor"));
|
||||
|
||||
const planeIntersections = raycaster.intersectObject(plane.current!, true);
|
||||
const intersectedPlane = planeIntersections[0];
|
||||
|
||||
let intersectPoint: THREE.Vector3 | null = null;
|
||||
|
||||
if (intersectedFloor && intersectedPlane) {
|
||||
intersectPoint =
|
||||
intersectedFloor.distance < intersectedPlane.distance
|
||||
? new THREE.Vector3(intersectedFloor.point.x, Math.round(intersectedFloor.point.y), intersectedFloor.point.z)
|
||||
: new THREE.Vector3(intersectedPlane.point.x, 0, intersectedPlane.point.z);
|
||||
} else if (intersectedFloor) {
|
||||
intersectPoint = new THREE.Vector3(intersectedFloor.point.x, Math.round(intersectedFloor.point.y), intersectedFloor.point.z);
|
||||
} else if (intersectedPlane) {
|
||||
intersectPoint = new THREE.Vector3(intersectedPlane.point.x, 0, intersectedPlane.point.z);
|
||||
}
|
||||
|
||||
if (intersectPoint) {
|
||||
if (intersectPoint.y < 0) {
|
||||
intersectPoint = new THREE.Vector3(intersectPoint.x, 0, intersectPoint.z);
|
||||
}
|
||||
const cachedModel = THREE.Cache.get(selectedItem.id);
|
||||
if (cachedModel) {
|
||||
handleModelLoad(cachedModel, intersectPoint!, selectedItem, addEvent, addAsset, socket, projectId, userId);
|
||||
return;
|
||||
} else {
|
||||
const cachedModelBlob = await retrieveGLTF(selectedItem.id);
|
||||
if (cachedModelBlob) {
|
||||
const blobUrl = URL.createObjectURL(cachedModelBlob);
|
||||
loader.load(blobUrl, (gltf) => {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
THREE.Cache.remove(blobUrl);
|
||||
THREE.Cache.add(selectedItem.id, gltf);
|
||||
handleModelLoad(gltf, intersectPoint!, selectedItem, addEvent, addAsset, socket, projectId, userId);
|
||||
});
|
||||
} else {
|
||||
loader.load(`${url_Backend_dwinzo}/api/v2/AssetFile/${selectedItem.id}`, async (gltf) => {
|
||||
const modelBlob = await fetch(`${url_Backend_dwinzo}/api/v2/AssetFile/${selectedItem.id}`).then((res) => res.blob());
|
||||
await storeGLTF(selectedItem.id, modelBlob);
|
||||
THREE.Cache.add(selectedItem.id, gltf);
|
||||
await handleModelLoad(gltf, intersectPoint!, selectedItem, addEvent, addAsset, socket, projectId, userId);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
echo.error("Failed to add asset");
|
||||
} finally {
|
||||
setSelectedItem({});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleModelLoad(
|
||||
gltf: any,
|
||||
intersectPoint: THREE.Vector3,
|
||||
selectedItem: any,
|
||||
addEvent: (event: EventsSchema) => void,
|
||||
addAsset: (asset: Asset) => void,
|
||||
socket: Socket<any>,
|
||||
projectId?: string,
|
||||
userId?: string
|
||||
) {
|
||||
const model = gltf.scene.clone();
|
||||
model.userData = {
|
||||
name: selectedItem.name,
|
||||
modelId: selectedItem.id,
|
||||
modelUuid: model.uuid,
|
||||
};
|
||||
model.position.set(intersectPoint!.x, intersectPoint!.y, intersectPoint!.z);
|
||||
model.scale.set(...CONSTANTS.assetConfig.defaultScaleAfterGsap);
|
||||
|
||||
model.traverse((child: any) => {
|
||||
if (child) {
|
||||
child.castShadow = true;
|
||||
child.receiveShadow = true;
|
||||
}
|
||||
});
|
||||
|
||||
const newFloorItem: Asset = {
|
||||
modelUuid: model.uuid,
|
||||
modelName: selectedItem.name,
|
||||
assetId: selectedItem.id,
|
||||
position: [intersectPoint!.x, intersectPoint!.y, intersectPoint!.z],
|
||||
rotation: [0, 0, 0],
|
||||
isLocked: false,
|
||||
isVisible: true,
|
||||
isCollidable: false,
|
||||
opacity: 1,
|
||||
};
|
||||
|
||||
const email = localStorage.getItem("email");
|
||||
const organization = email ? email.split("@")[1].split(".")[0] : "";
|
||||
|
||||
// API
|
||||
|
||||
// await setFloorItemApi(
|
||||
// organization,
|
||||
// newFloorItem.modelUuid,
|
||||
// newFloorItem.modelName,
|
||||
// newFloorItem.modelfileID,
|
||||
// newFloorItem.position,
|
||||
// { x: 0, y: 0, z: 0 },
|
||||
// false,
|
||||
// true,
|
||||
// );
|
||||
|
||||
// SOCKET
|
||||
|
||||
if (selectedItem.type) {
|
||||
const data = PointsCalculator(
|
||||
selectedItem.type,
|
||||
gltf.scene.clone(),
|
||||
new THREE.Vector3(...model.rotation)
|
||||
);
|
||||
|
||||
if (!data || !data.points) return;
|
||||
|
||||
const eventData: any = {
|
||||
type: selectedItem.type,
|
||||
};
|
||||
|
||||
if (selectedItem.type === "Conveyor") {
|
||||
const ConveyorEvent: ConveyorEventSchema = {
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
position: newFloorItem.position,
|
||||
rotation: newFloorItem.rotation,
|
||||
state: "idle",
|
||||
type: "transfer",
|
||||
speed: 1,
|
||||
points: data.points.map((point: THREE.Vector3, index: number) => {
|
||||
const triggers: TriggerSchema[] = [];
|
||||
|
||||
if (data.points && index < data.points.length - 1) {
|
||||
triggers.push({
|
||||
triggerUuid: THREE.MathUtils.generateUUID(),
|
||||
triggerName: `Trigger 1`,
|
||||
triggerType: "onComplete",
|
||||
delay: 0,
|
||||
triggeredAsset: {
|
||||
triggeredModel: {
|
||||
modelName: newFloorItem.modelName,
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
},
|
||||
triggeredPoint: {
|
||||
pointName: `Point`,
|
||||
pointUuid: "",
|
||||
},
|
||||
triggeredAction: {
|
||||
actionName: `Action 1`,
|
||||
actionUuid: "",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
uuid: THREE.MathUtils.generateUUID(),
|
||||
position: [point.x, point.y, point.z],
|
||||
rotation: [0, 0, 0],
|
||||
action: {
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: `Action 1`,
|
||||
actionType: "default",
|
||||
material: "Default Material",
|
||||
delay: 0,
|
||||
spawnInterval: 5,
|
||||
spawnCount: 1,
|
||||
triggers: triggers,
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
for (let i = 0; i < ConveyorEvent.points.length - 1; i++) {
|
||||
const currentPoint = ConveyorEvent.points[i];
|
||||
const nextPoint = ConveyorEvent.points[i + 1];
|
||||
|
||||
if (currentPoint.action.triggers.length > 0) {
|
||||
currentPoint.action.triggers[0].triggeredAsset!.triggeredPoint!.pointUuid = nextPoint.uuid;
|
||||
currentPoint.action.triggers[0].triggeredAsset!.triggeredAction!.actionUuid = nextPoint.action.actionUuid;
|
||||
}
|
||||
}
|
||||
addEvent(ConveyorEvent);
|
||||
eventData.points = ConveyorEvent.points.map((point) => ({
|
||||
uuid: point.uuid,
|
||||
position: point.position,
|
||||
rotation: point.rotation,
|
||||
}));
|
||||
} else if (selectedItem.type === "Vehicle") {
|
||||
const vehicleEvent: VehicleEventSchema = {
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
position: newFloorItem.position,
|
||||
rotation: newFloorItem.rotation,
|
||||
state: "idle",
|
||||
type: "vehicle",
|
||||
speed: 1,
|
||||
point: {
|
||||
uuid: THREE.MathUtils.generateUUID(),
|
||||
position: [data.points[0].x, data.points[0].y, data.points[0].z],
|
||||
rotation: [0, 0, 0],
|
||||
action: {
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: "Action 1",
|
||||
actionType: "travel",
|
||||
unLoadDuration: 5,
|
||||
loadCapacity: 1,
|
||||
steeringAngle: 0,
|
||||
pickUpPoint: null,
|
||||
unLoadPoint: null,
|
||||
triggers: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
addEvent(vehicleEvent);
|
||||
eventData.point = {
|
||||
uuid: vehicleEvent.point.uuid,
|
||||
position: vehicleEvent.point.position,
|
||||
rotation: vehicleEvent.point.rotation,
|
||||
};
|
||||
} else if (selectedItem.type === "ArmBot") {
|
||||
const roboticArmEvent: RoboticArmEventSchema = {
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
position: newFloorItem.position,
|
||||
rotation: newFloorItem.rotation,
|
||||
state: "idle",
|
||||
type: "roboticArm",
|
||||
speed: 1,
|
||||
point: {
|
||||
uuid: THREE.MathUtils.generateUUID(),
|
||||
position: [data.points[0].x, data.points[0].y, data.points[0].z],
|
||||
rotation: [0, 0, 0],
|
||||
actions: [
|
||||
{
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: "Action 1",
|
||||
actionType: "pickAndPlace",
|
||||
process: {
|
||||
startPoint: null,
|
||||
endPoint: null,
|
||||
},
|
||||
triggers: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
addEvent(roboticArmEvent);
|
||||
eventData.point = {
|
||||
uuid: roboticArmEvent.point.uuid,
|
||||
position: roboticArmEvent.point.position,
|
||||
rotation: roboticArmEvent.point.rotation,
|
||||
};
|
||||
} else if (selectedItem.type === "StaticMachine") {
|
||||
const machineEvent: MachineEventSchema = {
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
position: newFloorItem.position,
|
||||
rotation: newFloorItem.rotation,
|
||||
state: "idle",
|
||||
type: "machine",
|
||||
point: {
|
||||
uuid: THREE.MathUtils.generateUUID(),
|
||||
position: [data.points[0].x, data.points[0].y, data.points[0].z],
|
||||
rotation: [0, 0, 0],
|
||||
action: {
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: "Action 1",
|
||||
actionType: "process",
|
||||
processTime: 10,
|
||||
swapMaterial: "Default Material",
|
||||
triggers: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
addEvent(machineEvent);
|
||||
eventData.point = {
|
||||
uuid: machineEvent.point.uuid,
|
||||
position: machineEvent.point.position,
|
||||
rotation: machineEvent.point.rotation,
|
||||
};
|
||||
} else if (selectedItem.type === "Storage") {
|
||||
const storageEvent: StorageEventSchema = {
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
position: newFloorItem.position,
|
||||
rotation: newFloorItem.rotation,
|
||||
state: "idle",
|
||||
type: "storageUnit",
|
||||
point: {
|
||||
uuid: THREE.MathUtils.generateUUID(),
|
||||
position: [data.points[0].x, data.points[0].y, data.points[0].z],
|
||||
rotation: [0, 0, 0],
|
||||
action: {
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: "Action 1",
|
||||
actionType: "store",
|
||||
storageCapacity: 10,
|
||||
triggers: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
addEvent(storageEvent);
|
||||
eventData.point = {
|
||||
uuid: storageEvent.point.uuid,
|
||||
position: storageEvent.point.position,
|
||||
rotation: storageEvent.point.rotation,
|
||||
};
|
||||
}
|
||||
|
||||
const completeData = {
|
||||
organization,
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
modelfileID: newFloorItem.assetId,
|
||||
position: newFloorItem.position,
|
||||
rotation: {
|
||||
x: model.rotation.x,
|
||||
y: model.rotation.y,
|
||||
z: model.rotation.z,
|
||||
},
|
||||
isLocked: false,
|
||||
isVisible: true,
|
||||
socketId: socket.id,
|
||||
eventData: eventData,
|
||||
projectId: projectId,
|
||||
userId: userId,
|
||||
};
|
||||
|
||||
socket.emit("v1:model-asset:add", completeData);
|
||||
|
||||
const asset: Asset = {
|
||||
modelUuid: completeData.modelUuid,
|
||||
modelName: completeData.modelName,
|
||||
assetId: completeData.modelfileID,
|
||||
position: completeData.position,
|
||||
rotation: [
|
||||
completeData.rotation.x,
|
||||
completeData.rotation.y,
|
||||
completeData.rotation.z,
|
||||
] as [number, number, number],
|
||||
isLocked: completeData.isLocked,
|
||||
isCollidable: false,
|
||||
isVisible: completeData.isVisible,
|
||||
opacity: 1,
|
||||
eventData: completeData.eventData,
|
||||
};
|
||||
|
||||
addAsset(asset);
|
||||
} else {
|
||||
const data = {
|
||||
organization,
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
modelfileID: newFloorItem.assetId,
|
||||
position: newFloorItem.position,
|
||||
rotation: {
|
||||
x: model.rotation.x,
|
||||
y: model.rotation.y,
|
||||
z: model.rotation.z,
|
||||
},
|
||||
isLocked: false,
|
||||
isVisible: true,
|
||||
socketId: socket.id,
|
||||
projectId: projectId,
|
||||
userId: userId,
|
||||
};
|
||||
|
||||
socket.emit("v1:model-asset:add", data);
|
||||
|
||||
const asset = {
|
||||
modelUuid: data.modelUuid,
|
||||
modelName: data.modelName,
|
||||
assetId: data.modelfileID,
|
||||
position: data.position,
|
||||
rotation: [data.rotation.x, data.rotation.y, data.rotation.z] as [number, number, number],
|
||||
isLocked: data.isLocked,
|
||||
isCollidable: false,
|
||||
isVisible: data.isVisible,
|
||||
opacity: 1,
|
||||
};
|
||||
|
||||
addAsset(asset);
|
||||
}
|
||||
}
|
||||
|
||||
export default addAssetModel;
|
||||
20
app/src/modules/builder/asset/functions/assetBoundingBox.tsx
Normal file
20
app/src/modules/builder/asset/functions/assetBoundingBox.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Box3, BoxGeometry, EdgesGeometry, Vector3 } from "three";
|
||||
|
||||
export const AssetBoundingBox = ({ boundingBox }: { boundingBox: Box3 | null }) => {
|
||||
if (!boundingBox) return null;
|
||||
|
||||
const size = boundingBox.getSize(new Vector3());
|
||||
const center = boundingBox.getCenter(new Vector3());
|
||||
|
||||
const boxGeometry = new BoxGeometry(size.x, size.y, size.z);
|
||||
const edges = new EdgesGeometry(boxGeometry);
|
||||
|
||||
return (
|
||||
<group name='Asset FallBack'>
|
||||
<lineSegments position={center}>
|
||||
<bufferGeometry attach="geometry" {...edges} />
|
||||
<lineBasicMaterial depthWrite={false} attach="material" color="gray" linewidth={1} />
|
||||
</lineSegments>
|
||||
</group>
|
||||
);
|
||||
};
|
||||
297
app/src/modules/builder/asset/models/model/model.tsx
Normal file
297
app/src/modules/builder/asset/models/model/model.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import * as THREE from 'three';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { retrieveGLTF, storeGLTF } from '../../../../../utils/indexDB/idbUtils';
|
||||
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
|
||||
import { ThreeEvent, useFrame, useThree } from '@react-three/fiber';
|
||||
import { useActiveTool, useDeletableFloorItem, useRenderDistance, useSelectedFloorItem, useSocketStore, useToggleView } from '../../../../../store/builder/store';
|
||||
import { AssetBoundingBox } from '../../functions/assetBoundingBox';
|
||||
import { CameraControls } from '@react-three/drei';
|
||||
import { useAssetsStore } from '../../../../../store/builder/useAssetStore';
|
||||
import { useEventsStore } from "../../../../../store/simulation/useEventsStore";
|
||||
import { useProductStore } from "../../../../../store/simulation/useProductStore";
|
||||
import useModuleStore, { useSubModuleStore } from '../../../../../store/useModuleStore';
|
||||
import { useLeftData, useTopData } from '../../../../../store/visualization/useZone3DWidgetStore';
|
||||
import { useSelectedAsset } from '../../../../../store/simulation/useSimulationStore';
|
||||
import { useProductContext } from '../../../../simulation/products/productContext';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
function Model({ asset }: { readonly asset: Asset }) {
|
||||
const { camera, controls, gl } = useThree();
|
||||
const { activeTool } = useActiveTool();
|
||||
const { toggleView } = useToggleView();
|
||||
const { subModule } = useSubModuleStore();
|
||||
const { activeModule } = useModuleStore();
|
||||
const { removeAsset } = useAssetsStore();
|
||||
const { setTop } = useTopData();
|
||||
const { setLeft } = useLeftData();
|
||||
const { getIsEventInProduct } = useProductStore();
|
||||
const { getEventByModelUuid } = useEventsStore();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { setSelectedAsset, clearSelectedAsset } = useSelectedAsset();
|
||||
const { socket } = useSocketStore();
|
||||
const { deletableFloorItem, setDeletableFloorItem } = useDeletableFloorItem();
|
||||
const { setSelectedFloorItem } = useSelectedFloorItem();
|
||||
const { renderDistance } = useRenderDistance();
|
||||
const [isRendered, setIsRendered] = useState(false);
|
||||
const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
|
||||
const [gltfScene, setGltfScene] = useState<GLTF["scene"] | null>(null);
|
||||
const [boundingBox, setBoundingBox] = useState<THREE.Box3 | null>(null);
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const { projectId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
const loader = new GLTFLoader();
|
||||
const dracoLoader = new DRACOLoader();
|
||||
|
||||
dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/');
|
||||
loader.setDRACOLoader(dracoLoader);
|
||||
const loadModel = async () => {
|
||||
try {
|
||||
// Check Cache
|
||||
const assetId = asset.assetId;
|
||||
const cachedModel = THREE.Cache.get(assetId);
|
||||
if (cachedModel) {
|
||||
setGltfScene(cachedModel.scene.clone());
|
||||
calculateBoundingBox(cachedModel.scene);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check IndexedDB
|
||||
const indexedDBModel = await retrieveGLTF(assetId);
|
||||
if (indexedDBModel) {
|
||||
const blobUrl = URL.createObjectURL(indexedDBModel);
|
||||
loader.load(blobUrl, (gltf) => {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
THREE.Cache.remove(blobUrl);
|
||||
THREE.Cache.add(assetId, gltf);
|
||||
setGltfScene(gltf.scene.clone());
|
||||
calculateBoundingBox(gltf.scene);
|
||||
},
|
||||
undefined,
|
||||
(error) => {
|
||||
echo.error(`[IndexedDB] Error loading ${asset.modelName}:`);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch from Backend
|
||||
const modelUrl = `${url_Backend_dwinzo}/api/v2/AssetFile/${assetId}`;
|
||||
const handleBackendLoad = async (gltf: GLTF) => {
|
||||
try {
|
||||
const response = await fetch(modelUrl);
|
||||
const modelBlob = await response.blob();
|
||||
await storeGLTF(assetId, modelBlob);
|
||||
THREE.Cache.add(assetId, gltf);
|
||||
setGltfScene(gltf.scene.clone());
|
||||
calculateBoundingBox(gltf.scene);
|
||||
} catch (error) {
|
||||
console.error(`[Backend] Error storing/loading ${asset.modelName}:`, error);
|
||||
}
|
||||
};
|
||||
loader.load(
|
||||
modelUrl,
|
||||
handleBackendLoad,
|
||||
undefined,
|
||||
(error) => {
|
||||
echo.error(`[Backend] Error loading ${asset.modelName}:`);
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Failed to load model:", asset.assetId, err);
|
||||
}
|
||||
};
|
||||
|
||||
const calculateBoundingBox = (scene: THREE.Object3D) => {
|
||||
const box = new THREE.Box3().setFromObject(scene);
|
||||
setBoundingBox(box);
|
||||
};
|
||||
|
||||
loadModel();
|
||||
|
||||
}, []);
|
||||
|
||||
useFrame(() => {
|
||||
const assetPosition = new THREE.Vector3(...asset.position);
|
||||
if (!isRendered && assetPosition.distanceTo(camera.position) <= renderDistance) {
|
||||
setIsRendered(true);
|
||||
} else if (isRendered && assetPosition.distanceTo(camera.position) > renderDistance) {
|
||||
setIsRendered(false);
|
||||
}
|
||||
})
|
||||
|
||||
const handleDblClick = (asset: Asset) => {
|
||||
if (asset) {
|
||||
if (activeTool === "cursor" && boundingBox && groupRef.current && activeModule === 'builder') {
|
||||
const size = boundingBox.getSize(new THREE.Vector3());
|
||||
const center = boundingBox.getCenter(new THREE.Vector3());
|
||||
|
||||
const front = new THREE.Vector3(0, 0, 1);
|
||||
groupRef.current.localToWorld(front);
|
||||
front.sub(groupRef.current.position).normalize();
|
||||
|
||||
const distance = Math.max(size.x, size.y, size.z) * 2;
|
||||
const newPosition = center.clone().addScaledVector(front, distance);
|
||||
|
||||
(controls as CameraControls).setPosition(
|
||||
newPosition.x,
|
||||
newPosition.y,
|
||||
newPosition.z,
|
||||
true
|
||||
);
|
||||
(controls as CameraControls).setTarget(center.x, center.y, center.z, true);
|
||||
(controls as CameraControls).fitToBox(groupRef.current, true, {
|
||||
cover: true,
|
||||
paddingTop: 5,
|
||||
paddingLeft: 5,
|
||||
paddingBottom: 5,
|
||||
paddingRight: 5,
|
||||
});
|
||||
setSelectedFloorItem(groupRef.current);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (asset: Asset) => {
|
||||
if (activeTool === 'delete' && deletableFloorItem && deletableFloorItem.uuid === asset.modelUuid) {
|
||||
const email = localStorage.getItem('email')
|
||||
const organization = (email!.split("@")[1]).split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
//REST
|
||||
|
||||
// const response = await deleteFloorItem(organization, asset.modelUuid, asset.modelName);
|
||||
|
||||
//SOCKET
|
||||
|
||||
const data = {
|
||||
organization: organization,
|
||||
modelUuid: asset.modelUuid,
|
||||
modelName: asset.modelName,
|
||||
socketId: socket.id,
|
||||
userId,
|
||||
projectId
|
||||
}
|
||||
|
||||
const response = socket.emit('v1:model-asset:delete', data)
|
||||
|
||||
useEventsStore.getState().removeEvent(asset.modelUuid);
|
||||
useProductStore.getState().deleteEvent(asset.modelUuid);
|
||||
|
||||
if (response) {
|
||||
|
||||
removeAsset(asset.modelUuid);
|
||||
|
||||
echo.success("Model Removed!");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const handlePointerOver = (asset: Asset) => {
|
||||
if (activeTool === "delete") {
|
||||
if (deletableFloorItem && deletableFloorItem.uuid === asset.modelUuid) {
|
||||
return;
|
||||
} else {
|
||||
setDeletableFloorItem(groupRef.current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handlePointerOut = (asset: Asset) => {
|
||||
if (activeTool === "delete" && deletableFloorItem && deletableFloorItem.uuid === asset.modelUuid) {
|
||||
setDeletableFloorItem(null);
|
||||
}
|
||||
}
|
||||
|
||||
const handleContextMenu = (asset: Asset, evt: ThreeEvent<MouseEvent>) => {
|
||||
if (activeTool === "cursor" && subModule === 'simulations') {
|
||||
if (asset.modelUuid) {
|
||||
const canvasElement = gl.domElement;
|
||||
const isInProduct = getIsEventInProduct(selectedProduct.productUuid, asset.modelUuid);
|
||||
if (isInProduct) {
|
||||
const event = getEventByModelUuid(asset.modelUuid);
|
||||
if (event) {
|
||||
setSelectedAsset(event);
|
||||
const canvasRect = canvasElement.getBoundingClientRect();
|
||||
const relativeX = evt.clientX - canvasRect.left;
|
||||
const relativeY = evt.clientY - canvasRect.top;
|
||||
setTop(relativeY);
|
||||
setLeft(relativeX);
|
||||
} else {
|
||||
clearSelectedAsset();
|
||||
}
|
||||
} else {
|
||||
const event = getEventByModelUuid(asset.modelUuid);
|
||||
if (event) {
|
||||
setSelectedAsset(event)
|
||||
const canvasRect = canvasElement.getBoundingClientRect();
|
||||
const relativeX = evt.clientX - canvasRect.left;
|
||||
const relativeY = evt.clientY - canvasRect.top;
|
||||
setTop(relativeY);
|
||||
setLeft(relativeX);
|
||||
} else {
|
||||
clearSelectedAsset()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clearSelectedAsset()
|
||||
}
|
||||
} else {
|
||||
clearSelectedAsset()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<group
|
||||
name='Asset Model'
|
||||
ref={groupRef}
|
||||
uuid={asset.modelUuid}
|
||||
position={asset.position}
|
||||
rotation={asset.rotation}
|
||||
visible={asset.isVisible}
|
||||
userData={asset}
|
||||
onDoubleClick={(e) => {
|
||||
if (!toggleView) {
|
||||
e.stopPropagation();
|
||||
handleDblClick(asset);
|
||||
}
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (!toggleView) {
|
||||
e.stopPropagation();
|
||||
handleClick(asset);
|
||||
}
|
||||
}}
|
||||
onPointerOver={(e) => {
|
||||
if (!toggleView) {
|
||||
e.stopPropagation();
|
||||
handlePointerOver(asset);
|
||||
}
|
||||
}}
|
||||
onPointerOut={(e) => {
|
||||
if (!toggleView) {
|
||||
e.stopPropagation();
|
||||
handlePointerOut(asset);
|
||||
}
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.stopPropagation();
|
||||
handleContextMenu(asset, e);
|
||||
}}
|
||||
>
|
||||
{gltfScene && (
|
||||
isRendered ? (
|
||||
<primitive object={gltfScene} />
|
||||
) : (
|
||||
<AssetBoundingBox boundingBox={boundingBox} />
|
||||
)
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default Model;
|
||||
37
app/src/modules/builder/asset/models/models.tsx
Normal file
37
app/src/modules/builder/asset/models/models.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useAssetsStore } from '../../../../store/builder/useAssetStore';
|
||||
import Model from './model/model';
|
||||
import { useThree } from '@react-three/fiber';
|
||||
import { CameraControls } from '@react-three/drei';
|
||||
import { Vector3 } from 'three';
|
||||
import { useSelectedFloorItem } from '../../../../store/builder/store';
|
||||
import { useSelectedAsset } from '../../../../store/simulation/useSimulationStore';
|
||||
|
||||
function Models() {
|
||||
const { controls } = useThree();
|
||||
const { assets } = useAssetsStore();
|
||||
const { selectedFloorItem, setSelectedFloorItem } = useSelectedFloorItem();
|
||||
const { selectedAsset, clearSelectedAsset } = useSelectedAsset();
|
||||
|
||||
return (
|
||||
<group
|
||||
name='Asset Group'
|
||||
onPointerMissed={(e) => {
|
||||
e.stopPropagation();
|
||||
if (selectedFloorItem) {
|
||||
const target = (controls as CameraControls).getTarget(new Vector3());
|
||||
(controls as CameraControls).setTarget(target.x, 0, target.z, true);
|
||||
setSelectedFloorItem(null);
|
||||
}
|
||||
if (selectedAsset) {
|
||||
clearSelectedAsset();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{assets.map((asset) =>
|
||||
<Model key={asset.modelUuid} asset={asset} />
|
||||
)}
|
||||
</group>
|
||||
)
|
||||
}
|
||||
|
||||
export default Models
|
||||
307
app/src/modules/builder/builder.tsx
Normal file
307
app/src/modules/builder/builder.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
////////// Three and React Three Fiber Imports //////////
|
||||
|
||||
import * as THREE from "three";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useThree, useFrame } from "@react-three/fiber";
|
||||
|
||||
////////// Component Imports //////////
|
||||
|
||||
import DistanceText from "./geomentries/lines/distanceText/distanceText";
|
||||
import ReferenceDistanceText from "./geomentries/lines/distanceText/referenceDistanceText";
|
||||
|
||||
////////// Zustand State Imports //////////
|
||||
|
||||
import {
|
||||
useToggleView,
|
||||
useDeletePointOrLine,
|
||||
useActiveLayer,
|
||||
useWallVisibility,
|
||||
useRoofVisibility,
|
||||
useShadows,
|
||||
useUpdateScene,
|
||||
useWalls,
|
||||
useToolMode,
|
||||
useRefTextUpdate,
|
||||
useRenderDistance,
|
||||
useLimitDistance,
|
||||
} from "../../store/builder/store";
|
||||
|
||||
////////// 3D Function Imports //////////
|
||||
|
||||
import loadWalls from "./geomentries/walls/loadWalls";
|
||||
|
||||
import * as Types from "../../types/world/worldTypes";
|
||||
|
||||
import SocketResponses from "../collaboration/socket/socketResponses.dev";
|
||||
import FloorPlanGroup from "./groups/floorPlanGroup";
|
||||
import FloorGroup from "./groups/floorGroup";
|
||||
import Draw from "./functions/draw";
|
||||
import WallsAndWallItems from "./groups/wallsAndWallItems";
|
||||
import Ground from "../scene/environment/ground";
|
||||
import { findEnvironment } from "../../services/factoryBuilder/environment/findEnvironment";
|
||||
import Layer2DVisibility from "./geomentries/layers/layer2DVisibility";
|
||||
import ZoneGroup from "./groups/zoneGroup";
|
||||
import MeasurementTool from "../scene/tools/measurementTool";
|
||||
import NavMesh from "../simulation/vehicle/navMesh/navMesh";
|
||||
import CalculateAreaGroup from "./groups/calculateAreaGroup";
|
||||
import LayoutImage from "./layout/layoutImage";
|
||||
import AssetsGroup from "./asset/assetsGroup";
|
||||
import { Bvh } from "@react-three/drei";
|
||||
import DxfFile from "./dfx/LoadBlueprint";
|
||||
import { useParams } from "react-router-dom";
|
||||
import AislesGroup from "./aisle/aislesGroup";
|
||||
import WallGroup from "./wall/wallGroup";
|
||||
import { useBuilderStore } from "../../store/builder/useBuilderStore";
|
||||
|
||||
export default function Builder() {
|
||||
const state = useThree<Types.ThreeState>(); // Importing the state from the useThree hook, which contains the scene, camera, and other Three.js elements.
|
||||
const csg = useRef(); // Reference for CSG object, used for 3D modeling.
|
||||
const CSGGroup = useRef() as Types.RefMesh; // Reference to a group of CSG objects.
|
||||
const scene = useRef() as Types.RefScene; // Reference to the scene.
|
||||
const camera = useRef() as Types.RefCamera; // Reference to the camera object.
|
||||
const controls = useRef<any>(); // Reference to the controls object.
|
||||
const raycaster = useRef() as Types.RefRaycaster; // Reference for raycaster used for detecting objects being pointed at in the scene.
|
||||
const dragPointControls = useRef() as Types.RefDragControl; // Reference for drag point controls, an array for drag control.
|
||||
|
||||
// Assigning the scene and camera from the Three.js state to the references.
|
||||
|
||||
scene.current = state.scene;
|
||||
camera.current = state.camera;
|
||||
controls.current = state.controls;
|
||||
raycaster.current = state.raycaster;
|
||||
|
||||
const plane = useRef<THREE.Mesh>(null); // Reference for a plane object for raycaster reference.
|
||||
const grid = useRef() as any; // Reference for a grid object for raycaster reference.
|
||||
const snappedPoint = useRef() as Types.RefVector3; // Reference for storing a snapped point at the (end = isSnapped) and (start = ispreSnapped) of the line.
|
||||
const isSnapped = useRef(false) as Types.RefBoolean; // Boolean reference to indicate if an object is snapped at the (end).
|
||||
const anglesnappedPoint = useRef() as Types.RefVector3; // Reference for storing an angle-snapped point when the line is in 90 degree etc...
|
||||
const isAngleSnapped = useRef(false) as Types.RefBoolean; // Boolean to indicate if angle snapping is active.
|
||||
const isSnappedUUID = useRef() as Types.RefString; // UUID reference to identify the snapped point.
|
||||
const ispreSnapped = useRef(false) as Types.RefBoolean; // Boolean reference to indicate if an object is snapped at the (start).
|
||||
const tempLoader = useRef() as Types.RefMesh; // Reference for a temporary loader for the floor items.
|
||||
const isTempLoader = useRef() as Types.RefBoolean; // Reference to check if a temporary loader is active.
|
||||
const Tube = useRef() as Types.RefTubeGeometry; // Reference for tubes used for reference line creation and updation.
|
||||
const line = useRef([]) as Types.RefLine; // Reference for line which stores the current line that is being drawn.
|
||||
const lines = useRef([]) as Types.RefLines; // Reference for lines which stores all the lines that are ever drawn.
|
||||
const onlyFloorline = useRef<Types.OnlyFloorLine>([]); // Reference for floor lines which does not have walls or roof and have only floor used to store the current line that is being drawn.
|
||||
const onlyFloorlines = useRef<Types.OnlyFloorLines>([]); // Reference for all the floor lines that are ever drawn.
|
||||
const ReferenceLineMesh = useRef() as Types.RefMesh; // Reference for storing the mesh of the reference line for moving it during draw.
|
||||
const LineCreated = useRef(false) as Types.RefBoolean; // Boolean to track whether the reference line is created or not.
|
||||
const referencePole = useRef() as Types.RefMesh; // Reference for a pole that is used as the reference for the user to show where it is placed.
|
||||
const itemsGroup = useRef() as Types.RefGroup; // Reference to the THREE.Group that has the floor items (Gltf).
|
||||
const floorGroup = useRef() as Types.RefGroup; // Reference to the THREE.Group that has the roofs and the floors.
|
||||
const floorPlanGroup = useRef() as Types.RefGroup; // Reference for a THREE.Group that has the lines group and the points group.
|
||||
const floorPlanGroupLine = useRef() as Types.RefGroup; // Reference for a THREE.Group that has the lines that are drawn.
|
||||
const floorPlanGroupPoint = useRef() as Types.RefGroup; // Reference for a THREE.Group that has the points that are created.
|
||||
const floorGroupAisle = useRef() as Types.RefGroup;
|
||||
const zoneGroup = useRef() as Types.RefGroup;
|
||||
const currentLayerPoint = useRef([]) as Types.RefMeshArray; // Reference for points that re in the current layer used to update the points in drag controls.
|
||||
const hoveredDeletablePoint = useRef() as Types.RefMesh; // Reference for the currently hovered point that can be deleted.
|
||||
const hoveredDeletableLine = useRef() as Types.RefMesh; // Reference for the currently hovered line that can be deleted.
|
||||
const hoveredDeletableWallItem = useRef() as Types.RefMesh; // Reference for the currently hovered wall item that can be deleted.
|
||||
const hoveredDeletablePillar = useRef() as Types.RefMesh; // Reference for the currently hovered pillar that can be deleted.
|
||||
const currentWallItem = useRef() as Types.RefMesh; // Reference for the currently selected wall item that can be scaled, dragged etc...
|
||||
|
||||
const cursorPosition = new THREE.Vector3(); // 3D vector for storing the cursor position.
|
||||
|
||||
const [selectedItemsIndex, setSelectedItemsIndex] = useState<Types.Number | null>(null); // State for tracking the index of the selected item.
|
||||
const { activeLayer } = useActiveLayer(); // State that changes based on which layer the user chooses in Layers.jsx.
|
||||
const { toggleView } = useToggleView(); // State for toggling between 2D and 3D.
|
||||
const { toolMode, setToolMode } = useToolMode();
|
||||
const { setDeletePointOrLine } = useDeletePointOrLine();
|
||||
const { setRoofVisibility } = useRoofVisibility();
|
||||
const { setWallVisibility } = useWallVisibility();
|
||||
const { setShadows } = useShadows();
|
||||
const { setRenderDistance } = useRenderDistance();
|
||||
const { setLimitDistance } = useLimitDistance();
|
||||
const { setUpdateScene } = useUpdateScene();
|
||||
const { setWalls } = useWalls();
|
||||
const { refTextupdate, setRefTextUpdate } = useRefTextUpdate();
|
||||
const { projectId } = useParams();
|
||||
const { setHoveredPoint } = useBuilderStore();
|
||||
|
||||
// const loader = new GLTFLoader();
|
||||
// const dracoLoader = new DRACOLoader();
|
||||
|
||||
// dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/');
|
||||
// loader.setDRACOLoader(dracoLoader);
|
||||
|
||||
////////// All Toggle's //////////
|
||||
|
||||
useEffect(() => {
|
||||
setRefTextUpdate((prevUpdate: number) => prevUpdate - 1);
|
||||
if (toggleView) {
|
||||
Layer2DVisibility(
|
||||
activeLayer,
|
||||
floorPlanGroup,
|
||||
floorPlanGroupLine,
|
||||
floorPlanGroupPoint,
|
||||
currentLayerPoint,
|
||||
dragPointControls
|
||||
);
|
||||
} else {
|
||||
setHoveredPoint(null);
|
||||
setToolMode(null);
|
||||
setDeletePointOrLine(false);
|
||||
loadWalls(lines, setWalls);
|
||||
setUpdateScene(true);
|
||||
line.current = [];
|
||||
}
|
||||
}, [toggleView]);
|
||||
|
||||
useEffect(() => {
|
||||
THREE.Cache.clear();
|
||||
THREE.Cache.enabled = true;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const email = localStorage.getItem("email");
|
||||
const organization = email!.split("@")[1].split(".")[0];
|
||||
|
||||
async function fetchVisibility() {
|
||||
const data = await findEnvironment(organization, localStorage.getItem("userId")!, projectId);
|
||||
if (data) {
|
||||
setRoofVisibility(data.roofVisibility);
|
||||
setWallVisibility(data.wallVisibility);
|
||||
setShadows(data.shadowVisibility);
|
||||
setRenderDistance(data.renderDistance);
|
||||
setLimitDistance(data.limitDistance);
|
||||
}
|
||||
}
|
||||
fetchVisibility();
|
||||
}, []);
|
||||
|
||||
////////// UseFrame is Here //////////
|
||||
|
||||
useFrame(() => {
|
||||
if (toolMode) {
|
||||
Draw(
|
||||
state,
|
||||
plane,
|
||||
cursorPosition,
|
||||
floorPlanGroupPoint,
|
||||
floorPlanGroupLine,
|
||||
snappedPoint,
|
||||
isSnapped,
|
||||
isSnappedUUID,
|
||||
line,
|
||||
lines,
|
||||
ispreSnapped,
|
||||
floorPlanGroup,
|
||||
ReferenceLineMesh,
|
||||
LineCreated,
|
||||
setRefTextUpdate,
|
||||
Tube,
|
||||
anglesnappedPoint,
|
||||
isAngleSnapped,
|
||||
toolMode
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
////////// Return //////////
|
||||
|
||||
return (
|
||||
<>
|
||||
<Ground grid={grid} plane={plane} />
|
||||
|
||||
<Bvh firstHitOnly>
|
||||
<DistanceText key={toggleView} />
|
||||
</Bvh>
|
||||
|
||||
<ReferenceDistanceText
|
||||
key={refTextupdate}
|
||||
line={ReferenceLineMesh.current}
|
||||
/>
|
||||
|
||||
<Bvh firstHitOnly>
|
||||
<SocketResponses
|
||||
floorPlanGroup={floorPlanGroup}
|
||||
lines={lines}
|
||||
floorGroup={floorGroup}
|
||||
floorGroupAisle={floorGroupAisle}
|
||||
scene={scene}
|
||||
onlyFloorlines={onlyFloorlines}
|
||||
itemsGroup={itemsGroup}
|
||||
isTempLoader={isTempLoader}
|
||||
tempLoader={tempLoader}
|
||||
currentLayerPoint={currentLayerPoint}
|
||||
floorPlanGroupPoint={floorPlanGroupPoint}
|
||||
floorPlanGroupLine={floorPlanGroupLine}
|
||||
zoneGroup={zoneGroup}
|
||||
dragPointControls={dragPointControls}
|
||||
/>
|
||||
</Bvh>
|
||||
|
||||
<WallsAndWallItems
|
||||
CSGGroup={CSGGroup}
|
||||
setSelectedItemsIndex={setSelectedItemsIndex}
|
||||
selectedItemsIndex={selectedItemsIndex}
|
||||
currentWallItem={currentWallItem}
|
||||
csg={csg}
|
||||
lines={lines}
|
||||
hoveredDeletableWallItem={hoveredDeletableWallItem}
|
||||
/>
|
||||
|
||||
<Bvh firstHitOnly>
|
||||
|
||||
<FloorGroup
|
||||
floorGroup={floorGroup}
|
||||
lines={lines}
|
||||
referencePole={referencePole}
|
||||
hoveredDeletablePillar={hoveredDeletablePillar}
|
||||
/>
|
||||
|
||||
<FloorPlanGroup
|
||||
floorPlanGroup={floorPlanGroup}
|
||||
floorPlanGroupLine={floorPlanGroupLine}
|
||||
floorPlanGroupPoint={floorPlanGroupPoint}
|
||||
floorGroup={floorGroup}
|
||||
currentLayerPoint={currentLayerPoint}
|
||||
dragPointControls={dragPointControls}
|
||||
hoveredDeletablePoint={hoveredDeletablePoint}
|
||||
hoveredDeletableLine={hoveredDeletableLine}
|
||||
plane={plane}
|
||||
line={line}
|
||||
lines={lines}
|
||||
onlyFloorline={onlyFloorline}
|
||||
onlyFloorlines={onlyFloorlines}
|
||||
ReferenceLineMesh={ReferenceLineMesh}
|
||||
LineCreated={LineCreated}
|
||||
isSnapped={isSnapped}
|
||||
ispreSnapped={ispreSnapped}
|
||||
snappedPoint={snappedPoint}
|
||||
isSnappedUUID={isSnappedUUID}
|
||||
isAngleSnapped={isAngleSnapped}
|
||||
anglesnappedPoint={anglesnappedPoint}
|
||||
/>
|
||||
|
||||
<ZoneGroup />
|
||||
|
||||
<AssetsGroup
|
||||
floorGroup={floorGroup}
|
||||
plane={plane}
|
||||
/>
|
||||
|
||||
{/* <WallGroup /> */}
|
||||
|
||||
<AislesGroup />
|
||||
|
||||
<MeasurementTool />
|
||||
|
||||
<CalculateAreaGroup />
|
||||
<NavMesh lines={lines} />
|
||||
|
||||
<DxfFile
|
||||
lines={lines}
|
||||
dragPointControls={dragPointControls}
|
||||
currentLayerPoint={currentLayerPoint}
|
||||
floorPlanGroupLine={floorPlanGroupLine}
|
||||
floorPlanGroupPoint={floorPlanGroupPoint}
|
||||
/>
|
||||
|
||||
<LayoutImage />
|
||||
</Bvh>
|
||||
</>
|
||||
);
|
||||
}
|
||||
63
app/src/modules/builder/csg/csg.tsx
Normal file
63
app/src/modules/builder/csg/csg.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import * as THREE from "three";
|
||||
import { Geometry, Base, Subtraction } from "@react-three/csg";
|
||||
import { useDeleteTool } from "../../../store/builder/store";
|
||||
import { useRef } from "react";
|
||||
|
||||
export interface CsgProps {
|
||||
position: THREE.Vector3 | [number, number, number];
|
||||
scale: THREE.Vector3 | [number, number, number];
|
||||
model: THREE.Object3D;
|
||||
hoveredDeletableWallItem: { current: THREE.Mesh | null };
|
||||
}
|
||||
|
||||
export const Csg: React.FC<CsgProps> = (props) => {
|
||||
const { deleteTool } = useDeleteTool();
|
||||
const modelRef = useRef<THREE.Object3D>();
|
||||
const originalMaterials = useRef<Map<THREE.Mesh, THREE.Material>>(new Map());
|
||||
|
||||
const handleHover = (hovered: boolean, object: THREE.Mesh | null) => {
|
||||
if (modelRef.current && deleteTool) {
|
||||
modelRef.current.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
if (!originalMaterials.current.has(child)) {
|
||||
originalMaterials.current.set(child, child.material);
|
||||
}
|
||||
child.material = child.material.clone();
|
||||
child.material.color.set(hovered && deleteTool ? 0xff0000 : (originalMaterials.current.get(child) as any).color);
|
||||
}
|
||||
});
|
||||
}
|
||||
let currentObject = object;
|
||||
while (currentObject) {
|
||||
if (currentObject.name === "Scene") {
|
||||
break;
|
||||
}
|
||||
currentObject = currentObject.parent as THREE.Mesh;
|
||||
}
|
||||
if (currentObject) {
|
||||
props.hoveredDeletableWallItem.current = hovered ? currentObject : null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Geometry>
|
||||
<Subtraction {...props}>
|
||||
<Geometry>
|
||||
<Base geometry={new THREE.BoxGeometry()} />
|
||||
</Geometry>
|
||||
</Subtraction>
|
||||
<primitive
|
||||
object={props.model}
|
||||
ref={modelRef}
|
||||
onPointerOver={(e: any) => {
|
||||
e.stopPropagation();
|
||||
handleHover(true, e.object.parent);
|
||||
}}
|
||||
onPointerOut={(e: any) => {
|
||||
e.stopPropagation();
|
||||
handleHover(false, null);
|
||||
}}
|
||||
/>
|
||||
</Geometry>
|
||||
);
|
||||
};
|
||||
151
app/src/modules/builder/dfx/LoadBlueprint.tsx
Normal file
151
app/src/modules/builder/dfx/LoadBlueprint.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useDfxUpload, useSocketStore, useToggleView, useUpdateScene } from '../../../store/builder/store';
|
||||
import { LineBasicMaterial, Line } from 'three';
|
||||
import loadInitialPoint from '../IntialLoad/loadInitialPoint';
|
||||
import loadInitialLine from '../IntialLoad/loadInitialLine';
|
||||
import { TransformControls } from '@react-three/drei';
|
||||
import { getWallPointsFromBlueprint } from './functions/getWallPointsFromBlueprint';
|
||||
import * as Types from '../../../types/world/worldTypes';
|
||||
import arrayLineToObject from '../geomentries/lines/lineConvertions/arrayLineToObject';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
// Interface defining the props for the DxfFile component
|
||||
interface DxfFileProps {
|
||||
lines: Types.RefLines; // Reference to lines in the DXF file
|
||||
floorPlanGroupPoint: Types.RefGroup; // Reference to floor plan points group
|
||||
dragPointControls: Types.RefDragControl; // Reference to drag controls
|
||||
floorPlanGroupLine: Types.RefGroup; // Reference to floor plan lines group
|
||||
currentLayerPoint: Types.RefMeshArray; // Reference to current layer points
|
||||
}
|
||||
|
||||
/**
|
||||
* DxfFile component handles the rendering and manipulation of DXf file data in a 3D scene.
|
||||
* It processes the DXF data to create points and lines representing walls and allows
|
||||
* transformation controls for interactive editing.
|
||||
*/
|
||||
const DxfFile = ({
|
||||
floorPlanGroupPoint,
|
||||
currentLayerPoint,
|
||||
dragPointControls,
|
||||
floorPlanGroupLine,
|
||||
lines,
|
||||
}: DxfFileProps) => {
|
||||
// State management hooks
|
||||
const { dfxuploaded, dfxWallGenerate, setObjValue, objValue } = useDfxUpload();
|
||||
const { setUpdateScene } = useUpdateScene();
|
||||
const { toggleView } = useToggleView();
|
||||
const { socket } = useSocketStore();
|
||||
const { projectId } = useParams();
|
||||
|
||||
// Refs for storing line objects
|
||||
const lineRefs = useRef<Line[]>([]);
|
||||
|
||||
/**
|
||||
* Effect hook that runs when DXF wall generation is triggered.
|
||||
* Loads initial points and lines from the DXF data and updates the scene.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (
|
||||
dfxWallGenerate &&
|
||||
dragPointControls &&
|
||||
floorPlanGroupPoint &&
|
||||
currentLayerPoint &&
|
||||
floorPlanGroupLine
|
||||
) {
|
||||
// Store generated lines in ref
|
||||
lines.current.push(...dfxWallGenerate);
|
||||
dfxWallGenerate.map((line: any) => {
|
||||
const lineData = arrayLineToObject(line as Types.Line);
|
||||
const email = localStorage.getItem('email')
|
||||
const organization = (email!.split("@")[1]).split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
//REST
|
||||
|
||||
// setLine(organization, lineData.layer!, lineData.line!, lineData.type!);
|
||||
|
||||
//SOCKET
|
||||
|
||||
const input = {
|
||||
organization: organization,
|
||||
layer: lineData.layer,
|
||||
line: lineData.line,
|
||||
type: lineData.type,
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
}
|
||||
|
||||
socket.emit('v1:Line:create', input);
|
||||
|
||||
})
|
||||
|
||||
// Load initial points and lines from DXF data
|
||||
loadInitialPoint(lines, floorPlanGroupPoint, currentLayerPoint, dragPointControls);
|
||||
loadInitialLine(floorPlanGroupLine, lines);
|
||||
|
||||
// Trigger scene update
|
||||
setUpdateScene(true);
|
||||
}
|
||||
}, [
|
||||
lines,
|
||||
floorPlanGroupLine,
|
||||
floorPlanGroupPoint,
|
||||
currentLayerPoint,
|
||||
dragPointControls,
|
||||
dfxWallGenerate,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Handles transformation changes for individual lines.
|
||||
* Updates the object value state with new position and recalculates wall points.
|
||||
* @param index - Index of the line being transformed
|
||||
*/
|
||||
const handleTransformChange = (index: number) => {
|
||||
const line = lineRefs.current[index];
|
||||
if (!line) return;
|
||||
|
||||
// Get current position of the line
|
||||
const position = line.position;
|
||||
|
||||
// Update state with new position
|
||||
setObjValue({ x: position.x, y: position.y, z: position.z });
|
||||
|
||||
// Recalculate wall points based on new position
|
||||
getWallPointsFromBlueprint({
|
||||
objValue: { x: position.x, y: position.y, z: position.z },
|
||||
setDfxGenerate: () => { },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Render DXF lines with transform controls when DXF data is available and view is toggled */}
|
||||
{dfxuploaded &&
|
||||
dfxuploaded.length > 0 &&
|
||||
toggleView &&
|
||||
dfxuploaded?.map((geometry: any, index: number) => {
|
||||
// Create a new line object for each geometry in the DXF data
|
||||
const line = new Line(geometry, new LineBasicMaterial({ color: 'red' }));
|
||||
line.rotation.set(-Math.PI / 2, 0, 0);
|
||||
|
||||
// Store line reference
|
||||
lineRefs.current[index] = line;
|
||||
|
||||
return (
|
||||
<TransformControls
|
||||
key={index}
|
||||
object={line}
|
||||
showY={false}
|
||||
onMouseUp={() => handleTransformChange(index)}
|
||||
>
|
||||
{/* Render the line with current position from state */}
|
||||
<primitive object={line} position={[objValue.x, objValue.y, objValue.z]} />
|
||||
</TransformControls>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DxfFile;
|
||||
65
app/src/modules/builder/dfx/functions/convertDxfToThree.ts
Normal file
65
app/src/modules/builder/dfx/functions/convertDxfToThree.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { BufferGeometry, Vector3 } from "three";
|
||||
type DXFData = any;
|
||||
export const convertDXFToThree = (dxfData: DXFData): BufferGeometry[] => {
|
||||
const geometries: BufferGeometry[] = [];
|
||||
const UNIT = 1000;
|
||||
if (dxfData.entities) {
|
||||
dxfData.entities.forEach((entity: any) => {
|
||||
// LINE
|
||||
if (entity.type === "LINE" && entity.vertices) {
|
||||
const points = [
|
||||
new Vector3(entity.vertices[0].x, entity.vertices[0].y, 0),
|
||||
new Vector3(entity.vertices[1].x, entity.vertices[1].y, 0),
|
||||
];
|
||||
const geometry = new BufferGeometry().setFromPoints(points);
|
||||
geometry.scale(1 / UNIT, 1 / UNIT, 1 / UNIT);
|
||||
geometries.push(geometry);
|
||||
}
|
||||
|
||||
// LWPOLYLINE
|
||||
else if (entity.type === "LWPOLYLINE" && entity.vertices) {
|
||||
const points: Vector3[] = entity.vertices.map(
|
||||
(v: any) => new Vector3(v.x, v.y, 0)
|
||||
);
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const segment = [points[i], points[i + 1]];
|
||||
const geometry = new BufferGeometry().setFromPoints(segment);
|
||||
geometry.scale(1 / UNIT, 1 / UNIT, 1 / UNIT);
|
||||
geometries.push(geometry);
|
||||
}
|
||||
}
|
||||
|
||||
// ARC
|
||||
else if (
|
||||
entity.type === "ARC" &&
|
||||
entity.center &&
|
||||
entity.radius !== undefined
|
||||
) {
|
||||
const { center, radius, startAngle, endAngle } = entity;
|
||||
if (
|
||||
center === undefined ||
|
||||
radius === undefined ||
|
||||
startAngle === undefined ||
|
||||
endAngle === undefined
|
||||
)
|
||||
return;
|
||||
const numSegments = 32;
|
||||
const points: Vector3[] = [];
|
||||
|
||||
for (let i = 0; i <= numSegments; i++) {
|
||||
const t = i / numSegments;
|
||||
const angle = startAngle + t * (endAngle - startAngle); // linear interpolation
|
||||
const x = center.x + radius * Math.cos(angle);
|
||||
const y = center.y + radius * Math.sin(angle);
|
||||
points.push(new Vector3(x, y, 0));
|
||||
}
|
||||
|
||||
const geometry = new BufferGeometry().setFromPoints(points);
|
||||
geometry.scale(1 / UNIT, 1 / UNIT, 1 / UNIT);
|
||||
geometries.push(geometry);
|
||||
}
|
||||
});
|
||||
}
|
||||
return geometries;
|
||||
};
|
||||
@@ -0,0 +1,187 @@
|
||||
import { MathUtils, Vector3, BufferGeometry } from "three";
|
||||
type DXFData = any; // Replace with actual DXF data type
|
||||
type DXFEntity = any; // Replace with actual DXF entity type
|
||||
type WallLineVertex = [Vector3, string, number, string]; // Represents a wall segment with start point, ID, weight, and type
|
||||
interface Props {
|
||||
parsedData?: DXFData; // Parsed DXF file data
|
||||
setDfxGenerate?: (walls: WallLineVertex[][]) => void; // Callback to set generated walls
|
||||
objValue: any; // Object position values for offset calculation
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes DXF entities to generate wall points for a blueprint.
|
||||
* Handles LINE, LWPOLYLINE, and ARC entity types, converting them to wall segments.
|
||||
* Applies unit conversion and positional offsets to all points.
|
||||
*
|
||||
* @param {Props} params - Configuration parameters
|
||||
* @param {DXFData} params.parsedData - Parsed DXF file data
|
||||
* @param {Function} params.setDfxGenerate - Callback to store generated walls
|
||||
* @param {Object} params.objValue - Contains x,y,z offsets for position adjustment
|
||||
*/
|
||||
export function getWallPointsFromBlueprint({
|
||||
parsedData,
|
||||
setDfxGenerate,
|
||||
objValue,
|
||||
}: Props) {
|
||||
// Early return if no data is provided
|
||||
if (!parsedData) return;
|
||||
if (!parsedData.entities) return;
|
||||
|
||||
const unit = 1000; // Conversion factor from millimeters to meters
|
||||
const wallVertex: WallLineVertex[][] = []; // Stores all generated wall segments
|
||||
|
||||
// Process each entity in the DXF file
|
||||
parsedData.entities.forEach((entity: DXFEntity) => {
|
||||
// Handle LINE entities
|
||||
if (entity.type === "LINE" && entity.vertices) {
|
||||
// Create start and end vectors with unit conversion and position offset
|
||||
const startVec = new Vector3(
|
||||
entity.vertices[0].x / unit,
|
||||
0.01, // Slightly above ground to avoid z-fighting
|
||||
-entity.vertices[0].y / unit // Invert Y-axis to match Three.js coordinate system
|
||||
).add(new Vector3(objValue.x, 0, objValue.z));
|
||||
|
||||
const endVec = new Vector3(
|
||||
entity.vertices[1].x / unit,
|
||||
0.01,
|
||||
-entity.vertices[1].y / unit
|
||||
).add(new Vector3(objValue.x, 0, objValue.z));
|
||||
|
||||
// Check if points already exist to avoid duplicates
|
||||
const existingStart = wallVertex
|
||||
.flat()
|
||||
.find((v) => v[0].equals(startVec));
|
||||
const startPoint: WallLineVertex = existingStart || [
|
||||
startVec,
|
||||
MathUtils.generateUUID(), // Generate unique ID for new points
|
||||
1, // Default weight
|
||||
"WallLine", // Type identifier
|
||||
];
|
||||
|
||||
const existingEnd = wallVertex.flat().find((v) => v[0].equals(endVec));
|
||||
const endPoint: WallLineVertex = existingEnd || [
|
||||
endVec,
|
||||
MathUtils.generateUUID(),
|
||||
1,
|
||||
"WallLine",
|
||||
];
|
||||
|
||||
// Add the line segment to our collection
|
||||
wallVertex.push([startPoint, endPoint]);
|
||||
}
|
||||
// Handle LWPOLYLINE entities (connected line segments)
|
||||
else if (entity.type === "LWPOLYLINE" && entity.vertices) {
|
||||
let firstPoint: WallLineVertex | undefined; // Store first point for closing the polyline
|
||||
|
||||
// Process each vertex pair in the polyline
|
||||
for (let i = 0; i < entity.vertices.length - 1; i++) {
|
||||
// Convert vertices to Three.js vectors with offset
|
||||
const startVec = new Vector3(
|
||||
entity.vertices[i].x / unit,
|
||||
0.01,
|
||||
-entity.vertices[i].y / unit
|
||||
).add(new Vector3(objValue.x, 0, objValue.z));
|
||||
|
||||
const endVec = new Vector3(
|
||||
entity.vertices[i + 1].x / unit,
|
||||
0.01,
|
||||
-entity.vertices[i + 1].y / unit
|
||||
).add(new Vector3(objValue.x, 0, objValue.z));
|
||||
|
||||
// Check for existing points
|
||||
const existingStart = wallVertex
|
||||
.flat()
|
||||
.find((v) => v[0].equals(startVec));
|
||||
const startPoint: WallLineVertex = existingStart || [
|
||||
startVec,
|
||||
MathUtils.generateUUID(),
|
||||
1,
|
||||
"WallLine",
|
||||
];
|
||||
|
||||
const existingEnd = wallVertex.flat().find((v) => v[0].equals(endVec));
|
||||
const endPoint: WallLineVertex = existingEnd || [
|
||||
endVec,
|
||||
MathUtils.generateUUID(),
|
||||
1,
|
||||
"WallLine",
|
||||
];
|
||||
|
||||
wallVertex.push([startPoint, endPoint]);
|
||||
|
||||
// Store first point and create closing segment if this is the last vertex
|
||||
if (i === 0) firstPoint = startPoint;
|
||||
if (i === entity.vertices.length - 2 && firstPoint) {
|
||||
wallVertex.push([endPoint, firstPoint]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle ARC entities
|
||||
else if (entity.type === "ARC") {
|
||||
const { center, radius, startAngle, endAngle } = entity;
|
||||
// Validate required ARC properties
|
||||
if (
|
||||
!center ||
|
||||
radius === undefined ||
|
||||
startAngle === undefined ||
|
||||
endAngle === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert ARC to series of line segments
|
||||
const numSegments = 16; // Number of segments to approximate the arc
|
||||
const angleStep = (endAngle - startAngle) / numSegments;
|
||||
const arcPoints: Vector3[] = []; // Stores points along the arc
|
||||
|
||||
// Generate points along the arc
|
||||
for (let i = 0; i <= numSegments; i++) {
|
||||
const angle = startAngle + i * angleStep;
|
||||
// Calculate arc point in DXF coordinate system
|
||||
const x = center.x + radius * Math.cos(angle);
|
||||
const y = -center.y + radius * Math.sin(angle); // Invert Y-axis
|
||||
|
||||
// Convert to Three.js vector with offset
|
||||
const vec = new Vector3(x / unit, 0.01, y / unit).add(
|
||||
new Vector3(objValue.x, 0, objValue.z)
|
||||
);
|
||||
|
||||
arcPoints.push(vec);
|
||||
}
|
||||
|
||||
// Create line segments between arc points
|
||||
for (let i = 0; i < arcPoints.length - 1; i++) {
|
||||
const startVec = arcPoints[i];
|
||||
const endVec = arcPoints[i + 1];
|
||||
|
||||
// Check for existing points
|
||||
const existingStart = wallVertex
|
||||
.flat()
|
||||
.find((v) => v[0].equals(startVec));
|
||||
const startPoint: WallLineVertex = existingStart || [
|
||||
startVec,
|
||||
MathUtils.generateUUID(),
|
||||
1,
|
||||
"WallLine",
|
||||
];
|
||||
|
||||
const existingEnd = wallVertex.flat().find((v) => v[0].equals(endVec));
|
||||
const endPoint: WallLineVertex = existingEnd || [
|
||||
endVec,
|
||||
MathUtils.generateUUID(),
|
||||
1,
|
||||
"WallLine",
|
||||
];
|
||||
|
||||
wallVertex.push([startPoint, endPoint]);
|
||||
}
|
||||
}
|
||||
// Log unsupported entity types
|
||||
else {
|
||||
console.error("Unsupported entity type:", entity.type);
|
||||
}
|
||||
});
|
||||
|
||||
// Return the generated walls through callback if provided
|
||||
setDfxGenerate && setDfxGenerate(wallVertex);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import * as THREE from 'three';
|
||||
import { DragControls } from 'three/examples/jsm/controls/DragControls';
|
||||
import * as CONSTANTS from '../../../types/world/worldConstants';
|
||||
import DragPoint from '../geomentries/points/dragPoint';
|
||||
|
||||
import * as Types from "../../../types/world/worldTypes";
|
||||
// import { updatePoint } from '../../../services/factoryBuilder/lines/updatePointApi';
|
||||
import { Socket } from 'socket.io-client';
|
||||
|
||||
export default async function addDragControl(
|
||||
dragPointControls: Types.RefDragControl,
|
||||
currentLayerPoint: Types.RefMeshArray,
|
||||
state: Types.ThreeState,
|
||||
floorPlanGroupPoint: Types.RefGroup,
|
||||
floorPlanGroupLine: Types.RefGroup,
|
||||
lines: Types.RefLines,
|
||||
onlyFloorlines: Types.RefOnlyFloorLines,
|
||||
socket: Socket<any>,
|
||||
projectId?:string
|
||||
) {
|
||||
|
||||
////////// Dragging Point and also change the size to indicate during hover //////////
|
||||
|
||||
dragPointControls.current = new DragControls(currentLayerPoint.current, state.camera, state.gl.domElement);
|
||||
dragPointControls.current.enabled = false;
|
||||
|
||||
dragPointControls.current.addEventListener('drag', function (event) {
|
||||
const object = event.object;
|
||||
if (object.visible) {
|
||||
(state.controls as any).enabled = false;
|
||||
DragPoint(event as any, floorPlanGroupPoint, floorPlanGroupLine, state.scene, lines, onlyFloorlines)
|
||||
} else {
|
||||
(state.controls as any).enabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
dragPointControls.current.addEventListener('dragstart', function (event) {
|
||||
});
|
||||
|
||||
dragPointControls.current.addEventListener('dragend', async function (event) {
|
||||
if (!dragPointControls.current) return;
|
||||
const email = localStorage.getItem('email')
|
||||
const organization = (email!.split("@")[1]).split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
//REST
|
||||
|
||||
// await updatePoint(
|
||||
// organization,
|
||||
// { "x": event.object.position.x, "y": 0.01, "z": event.object.position.z },
|
||||
// event.object.uuid,
|
||||
// )
|
||||
|
||||
//SOCKET
|
||||
|
||||
const data = {
|
||||
organization: organization,
|
||||
position: { "x": event.object.position.x, "y": 0.01, "z": event.object.position.z },
|
||||
uuid: event.object.uuid,
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
}
|
||||
|
||||
socket.emit('v1:Line:update', data);
|
||||
|
||||
if (state.controls) {
|
||||
(state.controls as any).enabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
dragPointControls.current.addEventListener('hoveron', function (event: any) {
|
||||
if ((event.object as Types.Mesh).name === "point") {
|
||||
event.object.material.uniforms.uInnerColor.value.set(event.object.userData.color)
|
||||
}
|
||||
});
|
||||
|
||||
dragPointControls.current.addEventListener('hoveroff', function (event: any) {
|
||||
if ((event.object as Types.Mesh).name === "point") {
|
||||
event.object.material.uniforms.uInnerColor.value.set(new THREE.Color(CONSTANTS.pointConfig.defaultInnerColor))
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import * as Types from "../../../types/world/worldTypes";
|
||||
|
||||
export default function handleContextMenu(
|
||||
menuVisible: Types.Boolean,
|
||||
setMenuVisible: Types.BooleanState
|
||||
): void {
|
||||
// setMenuVisible(true)
|
||||
}
|
||||
61
app/src/modules/builder/eventFunctions/handleMeshDown.ts
Normal file
61
app/src/modules/builder/eventFunctions/handleMeshDown.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
import * as Types from "../../../types/world/worldTypes";
|
||||
|
||||
function handleMeshDown(
|
||||
event: Types.MeshEvent,
|
||||
currentWallItem: Types.RefMesh,
|
||||
setSelectedWallItem: Types.setSelectedWallItemSetState,
|
||||
setSelectedItemsIndex: Types.setSelectedItemsIndexSetState,
|
||||
wallItems: Types.wallItems,
|
||||
toggleView: Types.Boolean
|
||||
): void {
|
||||
|
||||
////////// To select which of the Wall item and CSG is selected to be dragged //////////
|
||||
|
||||
if (!toggleView) {
|
||||
if (currentWallItem.current) {
|
||||
currentWallItem.current.children.forEach((child) => {
|
||||
if ((child as THREE.Mesh).isMesh && child.name !== "CSG_REF") {
|
||||
const material = (child as THREE.Mesh).material;
|
||||
if (Array.isArray(material)) {
|
||||
material.forEach(mat => {
|
||||
if (mat instanceof THREE.MeshStandardMaterial) {
|
||||
mat.emissive = new THREE.Color("black");
|
||||
}
|
||||
});
|
||||
} else if (material instanceof THREE.MeshStandardMaterial) {
|
||||
material.emissive = new THREE.Color("black");
|
||||
}
|
||||
}
|
||||
});
|
||||
currentWallItem.current = null;
|
||||
setSelectedWallItem(null);
|
||||
setSelectedItemsIndex(null);
|
||||
}
|
||||
|
||||
if (event.intersections.length > 0) {
|
||||
if (event.object) {
|
||||
const wallItemModel = event.object;
|
||||
let currentObject = wallItemModel as THREE.Object3D;
|
||||
while (currentObject) {
|
||||
if (currentObject.name === "Scene") {
|
||||
break;
|
||||
}
|
||||
currentObject = currentObject.parent as THREE.Object3D;
|
||||
}
|
||||
if (!currentObject) return;
|
||||
const clickedIndex = wallItems.findIndex((item) => item.model?.uuid === currentObject.uuid);
|
||||
if (clickedIndex !== -1) {
|
||||
setSelectedItemsIndex(clickedIndex);
|
||||
const wallItemModel = wallItems[clickedIndex]?.model;
|
||||
if (wallItemModel) {
|
||||
setSelectedWallItem(wallItemModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
export default handleMeshDown;
|
||||
34
app/src/modules/builder/eventFunctions/handleMeshMissed.ts
Normal file
34
app/src/modules/builder/eventFunctions/handleMeshMissed.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as THREE from 'three';
|
||||
import * as Types from "../../../types/world/worldTypes";
|
||||
|
||||
function handleMeshMissed(
|
||||
currentWallItem: Types.RefMesh,
|
||||
setSelectedWallItem: Types.setSelectedWallItemSetState,
|
||||
setSelectedItemsIndex: Types.setSelectedItemsIndexSetState
|
||||
): void {
|
||||
|
||||
////////// If an item is selected and then clicked outside other than the selected object, this runs and removes the color of the selected object and sets setSelectedWallItem and setSelectedItemsIndex as null //////////
|
||||
|
||||
if (currentWallItem.current) {
|
||||
currentWallItem.current.children.forEach((child) => {
|
||||
if ((child as THREE.Mesh).isMesh && child.name !== "CSG_REF") {
|
||||
const material = (child as THREE.Mesh).material;
|
||||
|
||||
if (Array.isArray(material)) {
|
||||
material.forEach(mat => {
|
||||
if (mat instanceof THREE.MeshStandardMaterial) {
|
||||
mat.emissive = new THREE.Color("black");
|
||||
}
|
||||
});
|
||||
} else if (material instanceof THREE.MeshStandardMaterial) {
|
||||
material.emissive = new THREE.Color("black");
|
||||
}
|
||||
}
|
||||
});
|
||||
currentWallItem.current = null;
|
||||
setSelectedWallItem(null);
|
||||
setSelectedItemsIndex(null);
|
||||
}
|
||||
}
|
||||
|
||||
export default handleMeshMissed;
|
||||
29
app/src/modules/builder/functions/computeArea.ts
Normal file
29
app/src/modules/builder/functions/computeArea.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Vector2 } from "three";
|
||||
|
||||
export function computeArea(data: any, type: "zone" | "rooms" | "aisle"): any {
|
||||
if (type === "zone") {
|
||||
const points3D = data.map((p: any) => new Vector2(p[0], p[2]));
|
||||
let area = 0;
|
||||
for (let i = 0; i < points3D.length - 1; i++) {
|
||||
const current = points3D[i];
|
||||
const next = points3D[i + 1];
|
||||
area += current.x * next.y - next.x * current.y;
|
||||
}
|
||||
|
||||
return Math.abs(area) / 2;
|
||||
}
|
||||
|
||||
if (type === "rooms") {
|
||||
const points2D = data.coordinates.map(
|
||||
(coordinate: any) =>
|
||||
new Vector2(coordinate.position.x, coordinate.position.z)
|
||||
);
|
||||
let area = 0;
|
||||
for (let i = 0; i < points2D.length - 1; i++) {
|
||||
const current = points2D[i];
|
||||
const next = points2D[i + 1];
|
||||
area += current.x * next.y - next.x * current.y;
|
||||
}
|
||||
return Math.abs(area) / 2;
|
||||
}
|
||||
}
|
||||
87
app/src/modules/builder/functions/deletableLineOrPoint.ts
Normal file
87
app/src/modules/builder/functions/deletableLineOrPoint.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import * as THREE from 'three';
|
||||
import * as CONSTANTS from '../../../types/world/worldConstants';
|
||||
import * as Types from "../../../types/world/worldTypes";
|
||||
|
||||
function DeletableLineorPoint(
|
||||
state: Types.ThreeState,
|
||||
plane: Types.RefMesh,
|
||||
floorPlanGroupLine: Types.RefGroup,
|
||||
floorPlanGroupPoint: Types.RefGroup,
|
||||
hoveredDeletableLine: Types.RefMesh,
|
||||
hoveredDeletablePoint: Types.RefMesh
|
||||
): void {
|
||||
|
||||
////////// Altering the color of the hovered line or point during the deletion time //////////
|
||||
|
||||
if (!plane.current) return;
|
||||
let intersects = state.raycaster.intersectObject(plane.current, true);
|
||||
|
||||
let visibleIntersectLines;
|
||||
if (floorPlanGroupLine.current) { visibleIntersectLines = state.raycaster?.intersectObjects(floorPlanGroupLine.current.children, true); }
|
||||
const visibleIntersectLine = visibleIntersectLines?.find(intersect => intersect.object.visible) as THREE.Line | undefined || null;
|
||||
|
||||
let visibleIntersectPoints;
|
||||
if (floorPlanGroupPoint.current) {
|
||||
visibleIntersectPoints = state.raycaster?.intersectObjects(floorPlanGroupPoint.current.children, true);
|
||||
}
|
||||
const visibleIntersectPoint = visibleIntersectPoints?.find(intersect => intersect.object.visible) as THREE.Mesh | undefined;
|
||||
|
||||
function getLineColor(lineType: string | undefined): string {
|
||||
switch (lineType) {
|
||||
case CONSTANTS.lineConfig.wallName: return CONSTANTS.lineConfig.wallColor;
|
||||
case CONSTANTS.lineConfig.floorName: return CONSTANTS.lineConfig.floorColor;
|
||||
case CONSTANTS.lineConfig.aisleName: return CONSTANTS.lineConfig.aisleColor;
|
||||
default: return CONSTANTS.lineConfig.defaultColor;
|
||||
}
|
||||
}
|
||||
|
||||
if (intersects.length > 0) {
|
||||
if (visibleIntersectPoint) {
|
||||
if (hoveredDeletableLine.current) {
|
||||
const lineType = hoveredDeletableLine.current.userData.linePoints[1]?.[3];
|
||||
const color = getLineColor(lineType);
|
||||
(hoveredDeletableLine.current.material as THREE.MeshBasicMaterial).color = new THREE.Color(color);
|
||||
hoveredDeletableLine.current = null;
|
||||
}
|
||||
|
||||
hoveredDeletablePoint.current = (visibleIntersectPoint as any).object;
|
||||
(hoveredDeletablePoint.current as any).material.uniforms.uInnerColor.value.set(new THREE.Color("red"));
|
||||
(hoveredDeletablePoint.current as any).material.uniforms.uOuterColor.value.set(new THREE.Color("red"));
|
||||
// (hoveredDeletablePoint.current as THREE.Mesh).scale.set(1.5, 1.5, 1.5);
|
||||
} else if (hoveredDeletablePoint.current) {
|
||||
(hoveredDeletablePoint.current as any).material.uniforms.uInnerColor.value.set(CONSTANTS.pointConfig.defaultInnerColor);
|
||||
(hoveredDeletablePoint.current as any).material.uniforms.uOuterColor.value.set((hoveredDeletablePoint.current as any).userData.color);
|
||||
// hoveredDeletablePoint.current.scale.set(1, 1, 1);
|
||||
hoveredDeletablePoint.current = null;
|
||||
}
|
||||
|
||||
if (visibleIntersectLine && !visibleIntersectPoint) {
|
||||
if (hoveredDeletableLine.current) {
|
||||
const lineType = hoveredDeletableLine.current.userData.linePoints[1]?.[3];
|
||||
const color = getLineColor(lineType);
|
||||
(hoveredDeletableLine.current.material as THREE.MeshBasicMaterial).color = new THREE.Color(color);
|
||||
hoveredDeletableLine.current = null;
|
||||
}
|
||||
|
||||
if (hoveredDeletablePoint.current) {
|
||||
(hoveredDeletablePoint.current as any).material.uniforms.uInnerColor.value.set(CONSTANTS.pointConfig.defaultInnerColor);
|
||||
(hoveredDeletablePoint.current as any).material.uniforms.uOuterColor.value.set((hoveredDeletablePoint.current as any).userData.color);
|
||||
// hoveredDeletablePoint.current.scale.set(1, 1, 1);
|
||||
hoveredDeletablePoint.current = null;
|
||||
}
|
||||
|
||||
hoveredDeletableLine.current = (visibleIntersectLine as any).object;
|
||||
if (hoveredDeletableLine.current) {
|
||||
(hoveredDeletableLine.current.material as THREE.MeshBasicMaterial).color = new THREE.Color("red");
|
||||
}
|
||||
} else if (hoveredDeletableLine.current) {
|
||||
const lineType = hoveredDeletableLine.current.userData.linePoints[1]?.[3];
|
||||
const color = getLineColor(lineType);
|
||||
(hoveredDeletableLine.current.material as THREE.MeshBasicMaterial).color = new THREE.Color(color);
|
||||
hoveredDeletableLine.current = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default DeletableLineorPoint;
|
||||
97
app/src/modules/builder/functions/draw.ts
Normal file
97
app/src/modules/builder/functions/draw.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import * as Types from "../../../types/world/worldTypes";
|
||||
import * as CONSTANTS from '../../../types/world/worldConstants';
|
||||
import createAndMoveReferenceLine from "../geomentries/lines/createAndMoveReferenceLine";
|
||||
|
||||
async function Draw(
|
||||
state: Types.ThreeState,
|
||||
plane: Types.RefMesh,
|
||||
cursorPosition: Types.Vector3,
|
||||
floorPlanGroupPoint: Types.RefGroup,
|
||||
floorPlanGroupLine: Types.RefGroup,
|
||||
snappedPoint: Types.RefVector3,
|
||||
isSnapped: Types.RefBoolean,
|
||||
isSnappedUUID: Types.RefString,
|
||||
line: Types.RefLine,
|
||||
lines: Types.RefLines,
|
||||
ispreSnapped: Types.RefBoolean,
|
||||
floorPlanGroup: Types.RefGroup,
|
||||
ReferenceLineMesh: Types.RefMesh,
|
||||
LineCreated: Types.RefBoolean,
|
||||
setRefTextUpdate: any,
|
||||
Tube: Types.RefTubeGeometry,
|
||||
anglesnappedPoint: Types.RefVector3,
|
||||
isAngleSnapped: Types.RefBoolean,
|
||||
toolMode: Types.String,
|
||||
): Promise<void> {
|
||||
|
||||
////////// Snapping the cursor during the drawing time and also changing the color of the intersected lines //////////
|
||||
|
||||
if (!plane.current) return;
|
||||
const intersects = state.raycaster.intersectObject(plane.current, true);
|
||||
|
||||
if (intersects.length > 0 && (toolMode === "Wall" || toolMode === "Aisle" || toolMode === "Floor")) {
|
||||
const intersectionPoint = intersects[0].point;
|
||||
cursorPosition.copy(intersectionPoint);
|
||||
const snapThreshold = 1;
|
||||
|
||||
if (line.current.length === 0) {
|
||||
for (const point of floorPlanGroupPoint.current.children) {
|
||||
const pointType = point.userData.type;
|
||||
|
||||
const canSnap =
|
||||
((toolMode === "Wall") && (pointType === CONSTANTS.lineConfig.wallName || pointType === CONSTANTS.lineConfig.floorName)) ||
|
||||
((toolMode === "Floor") && (pointType === CONSTANTS.lineConfig.wallName || pointType === CONSTANTS.lineConfig.floorName)) ||
|
||||
((toolMode === "Aisle") && pointType === CONSTANTS.lineConfig.aisleName);;
|
||||
|
||||
if (canSnap && cursorPosition.distanceTo(point.position) < snapThreshold + 0.5 && point.visible) {
|
||||
cursorPosition.copy(point.position);
|
||||
snappedPoint.current = point.position;
|
||||
ispreSnapped.current = true;
|
||||
isSnapped.current = false;
|
||||
isSnappedUUID.current = point.uuid;
|
||||
break;
|
||||
} else {
|
||||
ispreSnapped.current = false;
|
||||
}
|
||||
}
|
||||
} else if (line.current.length > 0 && line.current[0]) {
|
||||
for (const point of floorPlanGroupPoint.current.children) {
|
||||
const pointType = point.userData.type;
|
||||
|
||||
let canSnap =
|
||||
((toolMode === "Wall") && (pointType === CONSTANTS.lineConfig.wallName || pointType === CONSTANTS.lineConfig.floorName)) ||
|
||||
((toolMode === "Floor") && (pointType === CONSTANTS.lineConfig.wallName || pointType === CONSTANTS.lineConfig.floorName)) ||
|
||||
((toolMode === "Aisle") && pointType === CONSTANTS.lineConfig.aisleName);
|
||||
|
||||
if (canSnap && cursorPosition.distanceTo(point.position) < snapThreshold && point.visible) {
|
||||
cursorPosition.copy(point.position);
|
||||
snappedPoint.current = point.position;
|
||||
isSnapped.current = true;
|
||||
ispreSnapped.current = false;
|
||||
isSnappedUUID.current = point.uuid;
|
||||
break;
|
||||
} else {
|
||||
isSnapped.current = false;
|
||||
}
|
||||
}
|
||||
|
||||
createAndMoveReferenceLine(
|
||||
line.current[0][0],
|
||||
cursorPosition,
|
||||
isSnapped,
|
||||
ispreSnapped,
|
||||
line,
|
||||
setRefTextUpdate,
|
||||
floorPlanGroup,
|
||||
ReferenceLineMesh,
|
||||
LineCreated,
|
||||
Tube,
|
||||
anglesnappedPoint,
|
||||
isAngleSnapped
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Draw;
|
||||
@@ -0,0 +1,62 @@
|
||||
import * as THREE from 'three';
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
import * as CONSTANTS from "../../../../types/world/worldConstants";
|
||||
|
||||
import texturePath from "../../../../assets/textures/floor/white.png";
|
||||
import texturePathDark from "../../../../assets/textures/floor/black.png";
|
||||
|
||||
// Cache for materials
|
||||
const materialCache = new Map<string, THREE.Material>();
|
||||
|
||||
export default function addFloorToScene(
|
||||
shape: THREE.Shape,
|
||||
layer: number,
|
||||
floorGroup: Types.RefGroup,
|
||||
userData: any,
|
||||
) {
|
||||
const savedTheme: string | null = localStorage.getItem('theme');
|
||||
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
|
||||
const textureScale = CONSTANTS.floorConfig.textureScale;
|
||||
|
||||
const materialKey = `floorMaterial_${textureScale}`;
|
||||
|
||||
let material: THREE.Material;
|
||||
|
||||
if (materialCache.has(materialKey)) {
|
||||
material = materialCache.get(materialKey) as THREE.Material;
|
||||
} else {
|
||||
const floorTexture = textureLoader.load(savedTheme === "dark" ? texturePathDark : texturePath);
|
||||
// const floorTexture = textureLoader.load(texturePath);
|
||||
|
||||
floorTexture.wrapS = floorTexture.wrapT = THREE.RepeatWrapping;
|
||||
floorTexture.repeat.set(textureScale, textureScale);
|
||||
floorTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
|
||||
material = new THREE.MeshStandardMaterial({
|
||||
map: floorTexture,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
|
||||
materialCache.set(materialKey, material);
|
||||
}
|
||||
|
||||
const extrudeSettings = {
|
||||
depth: CONSTANTS.floorConfig.height,
|
||||
bevelEnabled: false,
|
||||
};
|
||||
|
||||
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
mesh.receiveShadow = true;
|
||||
mesh.position.y = (layer) * CONSTANTS.wallConfig.height;
|
||||
mesh.rotateX(Math.PI / 2);
|
||||
mesh.name = `Floor_Layer_${layer}`;
|
||||
|
||||
// Store UUIDs for debugging or future processing
|
||||
mesh.userData.uuids = userData;
|
||||
|
||||
floorGroup.current.add(mesh);
|
||||
}
|
||||
186
app/src/modules/builder/geomentries/floors/drawOnlyFloor.ts
Normal file
186
app/src/modules/builder/geomentries/floors/drawOnlyFloor.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
import * as CONSTANTS from '../../../../types/world/worldConstants';
|
||||
|
||||
import addPointToScene from '../points/addPointToScene';
|
||||
import addLineToScene from '../lines/addLineToScene';
|
||||
import splitLine from '../lines/splitLine';
|
||||
import removeReferenceLine from '../lines/removeReferenceLine';
|
||||
import getClosestIntersection from '../lines/getClosestIntersection';
|
||||
import arrayLineToObject from '../lines/lineConvertions/arrayLineToObject';
|
||||
// import { setLine } from '../../../../services/factoryBuilder/lines/setLineApi';
|
||||
import { Socket } from 'socket.io-client';
|
||||
|
||||
async function drawOnlyFloor(
|
||||
raycaster: THREE.Raycaster,
|
||||
state: Types.ThreeState,
|
||||
camera: THREE.Camera,
|
||||
plane: Types.RefMesh,
|
||||
floorPlanGroupPoint: Types.RefGroup,
|
||||
snappedPoint: Types.RefVector3,
|
||||
isSnapped: Types.RefBoolean,
|
||||
isSnappedUUID: Types.RefString,
|
||||
line: Types.RefLine,
|
||||
ispreSnapped: Types.RefBoolean,
|
||||
anglesnappedPoint: Types.RefVector3,
|
||||
isAngleSnapped: Types.RefBoolean,
|
||||
onlyFloorline: Types.RefOnlyFloorLine,
|
||||
onlyFloorlines: Types.RefOnlyFloorLines,
|
||||
lines: Types.RefLines,
|
||||
floorPlanGroupLine: Types.RefGroup,
|
||||
floorPlanGroup: Types.RefGroup,
|
||||
ReferenceLineMesh: Types.RefMesh,
|
||||
LineCreated: Types.RefBoolean,
|
||||
currentLayerPoint: Types.RefMeshArray,
|
||||
dragPointControls: Types.RefDragControl,
|
||||
setNewLines: any,
|
||||
setDeletedLines: any,
|
||||
activeLayer: Types.Number,
|
||||
socket: Socket<any>,
|
||||
projectId?:string
|
||||
): Promise<void> {
|
||||
|
||||
////////// Creating lines Based on the positions clicked //////////
|
||||
|
||||
if (!plane.current) return
|
||||
const intersects = raycaster.intersectObject(plane.current, true);
|
||||
const intersectsLines = raycaster.intersectObjects(floorPlanGroupLine.current.children, true);
|
||||
const intersectsPoint = raycaster.intersectObjects(floorPlanGroupPoint.current.children, true);
|
||||
const VisibleintersectsPoint = intersectsPoint.find(intersect => intersect.object.visible);
|
||||
const visibleIntersect = intersectsLines.find(intersect => intersect.object.visible && intersect.object.name !== CONSTANTS.lineConfig.referenceName);
|
||||
|
||||
if ((intersectsPoint.length === 0 || VisibleintersectsPoint === undefined) && intersectsLines.length > 0 && !isSnapped.current && !ispreSnapped.current) {
|
||||
|
||||
////////// Clicked on a preexisting Line //////////
|
||||
|
||||
if (visibleIntersect && (intersectsLines[0].object.userData.linePoints[0][3] === CONSTANTS.lineConfig.floorName || intersectsLines[0].object.userData.linePoints[0][3] === CONSTANTS.lineConfig.wallName)) {
|
||||
let pointColor, lineColor;
|
||||
if (intersectsLines[0].object.userData.linePoints[0][3] === CONSTANTS.lineConfig.wallName) {
|
||||
pointColor = CONSTANTS.pointConfig.wallOuterColor;
|
||||
lineColor = CONSTANTS.lineConfig.wallColor;
|
||||
} else {
|
||||
pointColor = CONSTANTS.pointConfig.floorOuterColor;
|
||||
lineColor = CONSTANTS.lineConfig.floorColor;
|
||||
}
|
||||
let IntersectsPoint = new THREE.Vector3(intersects[0].point.x, 0.01, intersects[0].point.z);
|
||||
if (isAngleSnapped.current && line.current.length > 0 && anglesnappedPoint.current) {
|
||||
IntersectsPoint = anglesnappedPoint.current;
|
||||
}
|
||||
if (visibleIntersect.object instanceof THREE.Mesh) {
|
||||
const ThroughPoint = (visibleIntersect.object.geometry.parameters.path).getPoints(CONSTANTS.lineConfig.lineIntersectionPoints);
|
||||
let intersectionPoint = getClosestIntersection(ThroughPoint, IntersectsPoint);
|
||||
|
||||
if (intersectionPoint) {
|
||||
|
||||
const newLines = splitLine(visibleIntersect, intersectionPoint, currentLayerPoint, floorPlanGroupPoint, dragPointControls, isSnappedUUID, lines, setDeletedLines, floorPlanGroupLine, socket, pointColor, lineColor, intersectsLines[0].object.userData.linePoints[0][3],projectId);
|
||||
setNewLines([newLines[0], newLines[1]]);
|
||||
|
||||
(line.current as Types.Line).push([new THREE.Vector3(intersectionPoint.x, 0.01, intersectionPoint.z), isSnappedUUID.current!, activeLayer, CONSTANTS.lineConfig.floorName]);
|
||||
|
||||
if (line.current.length >= 2 && line.current[0] && line.current[1]) {
|
||||
lines.current.push(line.current as Types.Line);
|
||||
const data = arrayLineToObject(line.current as Types.Line);
|
||||
|
||||
const email = localStorage.getItem('email')
|
||||
const organization = (email!.split("@")[1]).split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
//REST
|
||||
|
||||
// setLine(organization, data.layer!, data.line!, data.type!);
|
||||
|
||||
//SOCKET
|
||||
|
||||
const input = {
|
||||
organization: organization,
|
||||
layer: data.layer,
|
||||
line: data.line,
|
||||
type: data.type,
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
}
|
||||
|
||||
socket.emit('v1:Line:create', input);
|
||||
|
||||
setNewLines([newLines[0], newLines[1], line.current]);
|
||||
onlyFloorline.current.push(line.current as Types.Line);
|
||||
onlyFloorlines.current.push(onlyFloorline.current);
|
||||
onlyFloorline.current = [];
|
||||
|
||||
addLineToScene(line.current[0][0], line.current[1][0], CONSTANTS.lineConfig.floorColor, line.current, floorPlanGroupLine);
|
||||
|
||||
removeReferenceLine(floorPlanGroup, ReferenceLineMesh, LineCreated, line);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (intersects.length > 0 && intersectsLines.length === 0) {
|
||||
|
||||
////////// Clicked on an empty place or a point //////////
|
||||
|
||||
let intersectionPoint = intersects[0].point;
|
||||
|
||||
if (isAngleSnapped.current && line.current.length > 0 && anglesnappedPoint.current) {
|
||||
intersectionPoint = anglesnappedPoint.current;
|
||||
}
|
||||
if (isSnapped.current && line.current.length > 0 && snappedPoint.current) {
|
||||
intersectionPoint = snappedPoint.current;
|
||||
}
|
||||
if (ispreSnapped.current && snappedPoint.current) {
|
||||
intersectionPoint = snappedPoint.current;
|
||||
}
|
||||
|
||||
if (!isSnapped.current && !ispreSnapped.current) {
|
||||
addPointToScene(intersectionPoint, CONSTANTS.pointConfig.floorOuterColor, currentLayerPoint, floorPlanGroupPoint, dragPointControls, isSnappedUUID, CONSTANTS.lineConfig.floorName);
|
||||
} else {
|
||||
ispreSnapped.current = false;
|
||||
isSnapped.current = false;
|
||||
}
|
||||
|
||||
(line.current as Types.Line).push([new THREE.Vector3(intersectionPoint.x, 0.01, intersectionPoint.z), isSnappedUUID.current!, activeLayer, CONSTANTS.lineConfig.floorName]);
|
||||
|
||||
if (line.current.length >= 2 && line.current[0] && line.current[1]) {
|
||||
onlyFloorline.current.push(line.current as Types.Line);
|
||||
lines.current.push(line.current as Types.Line);
|
||||
const data = arrayLineToObject(line.current as Types.Line);
|
||||
|
||||
const email = localStorage.getItem('email')
|
||||
const organization = (email!.split("@")[1]).split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
//REST
|
||||
|
||||
// setLine(organization, data.layer!, data.line!, data.type!);
|
||||
|
||||
//SOCKET
|
||||
|
||||
const input = {
|
||||
organization: organization,
|
||||
layer: data.layer,
|
||||
line: data.line,
|
||||
type: data.type,
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
}
|
||||
|
||||
socket.emit('v1:Line:create', input);
|
||||
|
||||
setNewLines([line.current]);
|
||||
addLineToScene(line.current[0][0], line.current[1][0], CONSTANTS.lineConfig.floorColor, line.current, floorPlanGroupLine);
|
||||
const lastPoint = line.current[line.current.length - 1];
|
||||
line.current = [lastPoint];
|
||||
}
|
||||
if (isSnapped.current) { ////////// Add this to stop the drawing mode after snapping //////////
|
||||
removeReferenceLine(floorPlanGroup, ReferenceLineMesh, LineCreated, line);
|
||||
onlyFloorlines.current.push(onlyFloorline.current);
|
||||
onlyFloorline.current = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default drawOnlyFloor;
|
||||
50
app/src/modules/builder/geomentries/floors/loadFloor.ts
Normal file
50
app/src/modules/builder/geomentries/floors/loadFloor.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as THREE from 'three';
|
||||
import * as CONSTANTS from '../../../../types/world/worldConstants';
|
||||
import addRoofToScene from '../roofs/addRoofToScene';
|
||||
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
import loadOnlyFloors from './loadOnlyFloors';
|
||||
import addFloorToScene from './addFloorToScene';
|
||||
import getRoomsFromLines from '../lines/getRoomsFromLines';
|
||||
|
||||
async function loadFloor(
|
||||
lines: Types.RefLines,
|
||||
floorGroup: Types.RefGroup,
|
||||
): Promise<void> {
|
||||
|
||||
if (!floorGroup.current) return;
|
||||
|
||||
floorGroup.current.children = [];
|
||||
|
||||
if (lines.current.length > 2) {
|
||||
const linesByLayer = lines.current.reduce((acc: { [key: number]: any[] }, pair) => {
|
||||
const layer = pair[0][2];
|
||||
if (!acc[layer]) acc[layer] = [];
|
||||
acc[layer].push(pair);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
for (const layer in linesByLayer) {
|
||||
// Only Floor Polygons
|
||||
loadOnlyFloors(floorGroup, linesByLayer, layer);
|
||||
|
||||
const rooms: Types.Rooms = await getRoomsFromLines({ current: linesByLayer[layer] });
|
||||
|
||||
rooms.forEach(({ coordinates: room, layer }) => {
|
||||
const userData = room.map(point => point.uuid);
|
||||
const shape = new THREE.Shape();
|
||||
shape.moveTo(room[0].position.x, room[0].position.z);
|
||||
room.forEach(point => shape.lineTo(point.position.x, point.position.z));
|
||||
shape.closePath();
|
||||
|
||||
// Floor Polygons
|
||||
addFloorToScene(shape, (layer - 1) * CONSTANTS.wallConfig.height, floorGroup, userData);
|
||||
|
||||
// Roof Polygons
|
||||
addRoofToScene(shape, (layer - 1) * CONSTANTS.wallConfig.height, userData, floorGroup);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default loadFloor;
|
||||
183
app/src/modules/builder/geomentries/floors/loadOnlyFloors.ts
Normal file
183
app/src/modules/builder/geomentries/floors/loadOnlyFloors.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import * as THREE from 'three';
|
||||
import * as turf from '@turf/turf';
|
||||
import * as CONSTANTS from '../../../../types/world/worldConstants';
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function loadOnlyFloors(
|
||||
floorGroup: Types.RefGroup,
|
||||
linesByLayer: any,
|
||||
layer: any,
|
||||
): void {
|
||||
|
||||
////////// Creating polygon floor based on the onlyFloorlines.current which does not add roof to it, The lines are still stored in Lines.current as well //////////
|
||||
|
||||
let floorsInLayer = linesByLayer[layer];
|
||||
floorsInLayer = floorsInLayer.filter((line: any) => line[0][3] && line[1][3] === CONSTANTS.lineConfig.floorName);
|
||||
const floorResult = floorsInLayer.map((pair: [THREE.Vector3, string, number, string][]) =>
|
||||
pair.map((point) => ({
|
||||
position: [point[0].x, point[0].z],
|
||||
uuid: point[1]
|
||||
}))
|
||||
);
|
||||
const FloorLineFeatures = floorResult.map((line: any) => turf.lineString(line.map((p: any) => p.position)));
|
||||
|
||||
function identifyPolygonsAndConnectedLines(FloorLineFeatures: any) {
|
||||
const floorpolygons = [];
|
||||
const connectedLines = [];
|
||||
const unprocessedLines = [...FloorLineFeatures]; // Copy the features
|
||||
|
||||
while (unprocessedLines.length > 0) {
|
||||
const currentLine = unprocessedLines.pop();
|
||||
const coordinates = currentLine.geometry.coordinates;
|
||||
|
||||
// Check if the line is closed (forms a polygon)
|
||||
if (
|
||||
coordinates[0][0] === coordinates[coordinates.length - 1][0] &&
|
||||
coordinates[0][1] === coordinates[coordinates.length - 1][1]
|
||||
) {
|
||||
floorpolygons.push(turf.polygon([coordinates])); // Add as a polygon
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the line connects to another line
|
||||
let connected = false;
|
||||
for (let i = unprocessedLines.length - 1; i >= 0; i--) {
|
||||
const otherCoordinates = unprocessedLines[i].geometry.coordinates;
|
||||
|
||||
// Check if lines share a start or end point
|
||||
if (
|
||||
coordinates[0][0] === otherCoordinates[otherCoordinates.length - 1][0] &&
|
||||
coordinates[0][1] === otherCoordinates[otherCoordinates.length - 1][1]
|
||||
) {
|
||||
// Merge lines
|
||||
const mergedCoordinates = [...otherCoordinates, ...coordinates.slice(1)];
|
||||
unprocessedLines[i] = turf.lineString(mergedCoordinates);
|
||||
connected = true;
|
||||
break;
|
||||
} else if (
|
||||
coordinates[coordinates.length - 1][0] === otherCoordinates[0][0] &&
|
||||
coordinates[coordinates.length - 1][1] === otherCoordinates[0][1]
|
||||
) {
|
||||
// Merge lines
|
||||
const mergedCoordinates = [...coordinates, ...otherCoordinates.slice(1)];
|
||||
unprocessedLines[i] = turf.lineString(mergedCoordinates);
|
||||
connected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
connectedLines.push(currentLine); // Add unconnected line as-is
|
||||
}
|
||||
}
|
||||
|
||||
return { floorpolygons, connectedLines };
|
||||
}
|
||||
|
||||
const { floorpolygons, connectedLines } = identifyPolygonsAndConnectedLines(FloorLineFeatures);
|
||||
|
||||
function convertConnectedLinesToPolygons(connectedLines: any) {
|
||||
return connectedLines.map((line: any) => {
|
||||
const coordinates = line.geometry.coordinates;
|
||||
|
||||
// If the line has more than two points, close the polygon
|
||||
if (coordinates.length > 2) {
|
||||
const firstPoint = coordinates[0];
|
||||
const lastPoint = coordinates[coordinates.length - 1];
|
||||
|
||||
// Check if already closed; if not, close it
|
||||
if (firstPoint[0] !== lastPoint[0] || firstPoint[1] !== lastPoint[1]) {
|
||||
coordinates.push(firstPoint);
|
||||
}
|
||||
|
||||
// Convert the closed line into a polygon
|
||||
return turf.polygon([coordinates]);
|
||||
}
|
||||
|
||||
// If not enough points for a polygon, return the line unchanged
|
||||
return line;
|
||||
});
|
||||
}
|
||||
|
||||
const convertedConnectedPolygons = convertConnectedLinesToPolygons(connectedLines);
|
||||
|
||||
if (convertedConnectedPolygons.length > 0) {
|
||||
const validPolygons = convertedConnectedPolygons.filter(
|
||||
(polygon: any) => polygon.geometry?.type === "Polygon"
|
||||
);
|
||||
|
||||
if (validPolygons.length > 0) {
|
||||
floorpolygons.push(...validPolygons);
|
||||
}
|
||||
}
|
||||
|
||||
function convertPolygonsToOriginalFormat(floorpolygons: any, originalLines: [THREE.Vector3, string, number, string][][]) {
|
||||
return floorpolygons.map((polygon: any) => {
|
||||
const coordinates = polygon.geometry.coordinates[0]; // Extract the coordinates array (assume it's a single polygon)
|
||||
|
||||
// Map each coordinate back to its original structure
|
||||
const mappedPoints = coordinates.map((coord: [number, number]) => {
|
||||
const [x, z] = coord;
|
||||
|
||||
// Find the original point matching this coordinate
|
||||
const originalPoint = originalLines.flat().find(([point]) => point.x === x && point.z === z);
|
||||
|
||||
if (!originalPoint) {
|
||||
throw new Error(`Original point for coordinate [${x}, ${z}] not found.`);
|
||||
}
|
||||
|
||||
return originalPoint;
|
||||
});
|
||||
|
||||
// Create pairs of consecutive points
|
||||
const pairs: typeof originalLines = [];
|
||||
for (let i = 0; i < mappedPoints.length - 1; i++) {
|
||||
pairs.push([mappedPoints[i], mappedPoints[i + 1]]);
|
||||
}
|
||||
|
||||
return pairs;
|
||||
});
|
||||
}
|
||||
|
||||
const convertedFloorPolygons: Types.OnlyFloorLines = convertPolygonsToOriginalFormat(floorpolygons, floorsInLayer);
|
||||
|
||||
convertedFloorPolygons.forEach((floor) => {
|
||||
const points: THREE.Vector3[] = [];
|
||||
|
||||
floor.forEach((lineSegment) => {
|
||||
const startPoint = lineSegment[0][0];
|
||||
points.push(new THREE.Vector3(startPoint.x, startPoint.y, startPoint.z));
|
||||
});
|
||||
|
||||
const lastLine = floor[floor.length - 1];
|
||||
const endPoint = lastLine[1][0];
|
||||
points.push(new THREE.Vector3(endPoint.x, endPoint.y, endPoint.z));
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
shape.moveTo(points[0].x, points[0].z);
|
||||
|
||||
points.forEach(point => shape.lineTo(point.x, point.z));
|
||||
shape.closePath();
|
||||
|
||||
const extrudeSettings = {
|
||||
depth: CONSTANTS.floorConfig.height,
|
||||
bevelEnabled: false
|
||||
};
|
||||
|
||||
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
|
||||
const material = new THREE.MeshStandardMaterial({ color: CONSTANTS.floorConfig.defaultColor, side: THREE.DoubleSide });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
|
||||
mesh.position.y = (floor[0][0][2] - 1) * CONSTANTS.wallConfig.height;
|
||||
mesh.rotateX(Math.PI / 2);
|
||||
mesh.name = `Only_Floor_Line_${floor[0][0][2]}`;
|
||||
|
||||
mesh.userData = floor;
|
||||
floorGroup?.current?.add(mesh);
|
||||
});
|
||||
}
|
||||
|
||||
export default loadOnlyFloors;
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function updateFloorLines(
|
||||
onlyFloorlines: Types.RefOnlyFloorLines,
|
||||
DragedPoint: Types.Mesh | { uuid: string, position: Types.Vector3 }
|
||||
): void {
|
||||
|
||||
////////// Update onlyFloorlines.current if it contains the dragged point //////////
|
||||
|
||||
onlyFloorlines.current.forEach((floorline) => {
|
||||
floorline.forEach((line) => {
|
||||
line.forEach((point) => {
|
||||
const [position, uuid] = point;
|
||||
if (uuid === DragedPoint.uuid) {
|
||||
position.x = DragedPoint.position.x;
|
||||
position.y = 0.01;
|
||||
position.z = DragedPoint.position.z;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default updateFloorLines;
|
||||
93
app/src/modules/builder/geomentries/layers/deleteLayer.ts
Normal file
93
app/src/modules/builder/geomentries/layers/deleteLayer.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { toast } from 'react-toastify';
|
||||
import RemoveConnectedLines from '../lines/removeConnectedLines';
|
||||
|
||||
import * as Types from '../../../../types/world/worldTypes';
|
||||
import { Socket } from 'socket.io-client';
|
||||
// import { deleteLayer } from '../../../../services/factoryBuilder/lines/deleteLayerApi';
|
||||
|
||||
async function DeleteLayer(
|
||||
removedLayer: Types.Number,
|
||||
lines: Types.RefLines,
|
||||
floorPlanGroupLine: Types.RefGroup,
|
||||
floorPlanGroupPoint: Types.RefGroup,
|
||||
onlyFloorlines: Types.RefOnlyFloorLines,
|
||||
floorGroup: Types.RefGroup,
|
||||
setDeletedLines: any,
|
||||
setRemovedLayer: Types.setRemoveLayerSetState,
|
||||
socket: Socket<any>,
|
||||
projectId?:string
|
||||
): Promise<void> {
|
||||
|
||||
////////// Remove the Lines from the lines.current based on the removed layer and rearrange the layer number that are higher than the removed layer //////////
|
||||
|
||||
const removedLines: Types.Lines = lines.current.filter(line => line[0][2] === removedLayer);
|
||||
|
||||
const email = localStorage.getItem('email')
|
||||
const organization = (email!.split("@")[1]).split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
//REST
|
||||
|
||||
// await deleteLayer(organization, removedLayer);
|
||||
|
||||
//SOCKET
|
||||
|
||||
const data = {
|
||||
organization: organization,
|
||||
layer: removedLayer,
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
}
|
||||
|
||||
socket.emit('v1:Line:delete:layer', data);
|
||||
|
||||
////////// Remove Points and lines from the removed layer //////////
|
||||
|
||||
removedLines.forEach((line) => {
|
||||
line.forEach((removedPoint) => {
|
||||
RemoveConnectedLines(removedPoint[1], floorPlanGroupLine, floorPlanGroupPoint, setDeletedLines, lines);
|
||||
});
|
||||
});
|
||||
|
||||
////////// Update the remaining lines layer values in the userData and in lines.current //////////
|
||||
|
||||
let remaining = lines.current.filter(line => line[0][2] !== removedLayer);
|
||||
let updatedLines: Types.Lines = [];
|
||||
remaining.forEach(line => {
|
||||
let newLines: Types.Line = [...line];
|
||||
if (newLines[0][2] > removedLayer) {
|
||||
newLines[0][2] -= 1;
|
||||
newLines[1][2] -= 1;
|
||||
}
|
||||
|
||||
const matchingLine = floorPlanGroupLine.current.children.find(l => l.userData.linePoints[0][1] === line[0][1] && l.userData.linePoints[1][1] === line[1][1]);
|
||||
if (matchingLine) {
|
||||
const updatedUserData = matchingLine.userData;
|
||||
updatedUserData.linePoints[0][2] = newLines[0][2];
|
||||
updatedUserData.linePoints[1][2] = newLines[1][2];
|
||||
}
|
||||
updatedLines.push(newLines);
|
||||
});
|
||||
|
||||
lines.current = updatedLines;
|
||||
localStorage.setItem("Lines", JSON.stringify(lines.current));
|
||||
|
||||
////////// Also remove OnlyFloorLines and update it in localstorage //////////
|
||||
|
||||
onlyFloorlines.current = onlyFloorlines.current.filter((floor) => {
|
||||
return floor[0][0][2] !== removedLayer;
|
||||
});
|
||||
const meshToRemove: any = floorGroup.current?.children.find((mesh) =>
|
||||
mesh.name === `Only_Floor_Line_${removedLayer}`
|
||||
);
|
||||
if (meshToRemove) {
|
||||
(<any>meshToRemove.material).dispose();
|
||||
(<any>meshToRemove.geometry).dispose();
|
||||
floorGroup.current?.remove(meshToRemove);
|
||||
}
|
||||
|
||||
echo.success("Layer Removed!");
|
||||
setRemovedLayer(null);
|
||||
}
|
||||
export default DeleteLayer;
|
||||
@@ -0,0 +1,35 @@
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function Layer2DVisibility(
|
||||
activeLayer: Types.Number,
|
||||
floorPlanGroup: Types.RefGroup,
|
||||
floorPlanGroupLine: Types.RefGroup,
|
||||
floorPlanGroupPoint: Types.RefGroup,
|
||||
currentLayerPoint: Types.RefMeshArray,
|
||||
dragPointControls: Types.RefDragControl
|
||||
): void {
|
||||
|
||||
if (floorPlanGroup.current && dragPointControls.current) {
|
||||
currentLayerPoint.current = [];
|
||||
floorPlanGroupLine.current.children.forEach((line) => {
|
||||
const linePoints = line.userData.linePoints;
|
||||
|
||||
const point1 = floorPlanGroupPoint.current.getObjectByProperty('uuid', linePoints[0][1]) as Types.Mesh;
|
||||
const point2 = floorPlanGroupPoint.current.getObjectByProperty('uuid', linePoints[1][1]) as Types.Mesh;
|
||||
|
||||
if (linePoints[0][2] !== activeLayer && linePoints[1][2] !== activeLayer) {
|
||||
point1.visible = false;
|
||||
point2.visible = false;
|
||||
line.visible = false;
|
||||
} else {
|
||||
point1.visible = true;
|
||||
point2.visible = true;
|
||||
line.visible = true;
|
||||
currentLayerPoint.current.push(point1, point2);
|
||||
}
|
||||
});
|
||||
dragPointControls.current!.objects = currentLayerPoint.current;
|
||||
}
|
||||
}
|
||||
|
||||
export default Layer2DVisibility;
|
||||
24
app/src/modules/builder/geomentries/lines/addLineToScene.ts
Normal file
24
app/src/modules/builder/geomentries/lines/addLineToScene.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as THREE from "three";
|
||||
import * as CONSTANTS from '../../../../types/world/worldConstants';
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function addLineToScene(
|
||||
start: Types.Vector3,
|
||||
end: Types.Vector3,
|
||||
colour: Types.Color,
|
||||
userData: Types.UserData,
|
||||
floorPlanGroupLine: Types.RefGroup
|
||||
): void {
|
||||
|
||||
////////// A function that creates and adds lines based on the start, end, and colour from the params, Also adds the userData in the mesh userData //////////
|
||||
|
||||
const path = new THREE.CatmullRomCurve3([start, end]);
|
||||
const geometry = new THREE.TubeGeometry(path, CONSTANTS.lineConfig.tubularSegments, CONSTANTS.lineConfig.radius, CONSTANTS.lineConfig.radialSegments, false);
|
||||
const material = new THREE.MeshBasicMaterial({ color: colour });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
floorPlanGroupLine.current.add(mesh);
|
||||
|
||||
mesh.userData.linePoints = userData;
|
||||
}
|
||||
|
||||
export default addLineToScene;
|
||||
@@ -0,0 +1,98 @@
|
||||
import * as THREE from "three";
|
||||
import * as CONSTANTS from '../../../../types/world/worldConstants';
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function createAndMoveReferenceLine(
|
||||
point: Types.Vector3,
|
||||
cursorPosition: Types.Vector3,
|
||||
isSnapped: Types.RefBoolean,
|
||||
ispreSnapped: Types.RefBoolean,
|
||||
line: Types.RefLine,
|
||||
setRefTextUpdate: Types.NumberIncrementState,
|
||||
floorPlanGroup: Types.RefGroup,
|
||||
ReferenceLineMesh: Types.RefMesh,
|
||||
LineCreated: Types.RefBoolean,
|
||||
Tube: Types.RefTubeGeometry,
|
||||
anglesnappedPoint: Types.RefVector3,
|
||||
isAngleSnapped: Types.RefBoolean
|
||||
): void {
|
||||
|
||||
////////// Creating new and maintaining the old reference line and also snap the reference line based on its angle //////////
|
||||
|
||||
const startPoint = point;
|
||||
|
||||
const dx = cursorPosition.x - startPoint.x;
|
||||
const dz = cursorPosition.z - startPoint.z;
|
||||
let angle = Math.atan2(dz, dx);
|
||||
|
||||
angle = (angle * 180) / Math.PI;
|
||||
angle = (angle + 360) % 360;
|
||||
|
||||
const snapAngles = [0, 90, 180, 270, 360];
|
||||
const snapThreshold = 2.5;
|
||||
|
||||
const closestSnapAngle = snapAngles.reduce((prev, curr) =>
|
||||
Math.abs(curr - angle) < Math.abs(prev - angle) ? curr : prev
|
||||
);
|
||||
|
||||
if (!isSnapped.current && !ispreSnapped.current && line.current.length > 0) {
|
||||
if (Math.abs(closestSnapAngle - angle) <= snapThreshold) {
|
||||
const snappedAngleRad = (closestSnapAngle * Math.PI) / 180;
|
||||
const distance = Math.sqrt(dx * dx + dz * dz);
|
||||
const snappedX = startPoint.x + distance * Math.cos(snappedAngleRad);
|
||||
const snappedZ = startPoint.z + distance * Math.sin(snappedAngleRad);
|
||||
|
||||
if (
|
||||
cursorPosition.distanceTo(
|
||||
new THREE.Vector3(snappedX, 0.01, snappedZ)
|
||||
) < 2
|
||||
) {
|
||||
cursorPosition.set(snappedX, 0.01, snappedZ);
|
||||
isAngleSnapped.current = true;
|
||||
anglesnappedPoint.current = new THREE.Vector3(
|
||||
snappedX,
|
||||
0.01,
|
||||
snappedZ
|
||||
);
|
||||
} else {
|
||||
isAngleSnapped.current = false;
|
||||
anglesnappedPoint.current = null;
|
||||
}
|
||||
} else {
|
||||
isAngleSnapped.current = false;
|
||||
anglesnappedPoint.current = null;
|
||||
}
|
||||
} else {
|
||||
isAngleSnapped.current = false;
|
||||
anglesnappedPoint.current = null;
|
||||
}
|
||||
|
||||
if (!LineCreated.current) {
|
||||
setRefTextUpdate((prevUpdate) => prevUpdate - 1);
|
||||
const path = new THREE.LineCurve3(startPoint, cursorPosition);
|
||||
Tube.current = new THREE.TubeGeometry(path, CONSTANTS.lineConfig.tubularSegments, CONSTANTS.lineConfig.radius, CONSTANTS.lineConfig.radialSegments, false);
|
||||
const material = new THREE.MeshBasicMaterial({ color: CONSTANTS.lineConfig.helperColor });
|
||||
ReferenceLineMesh.current = new THREE.Mesh(Tube.current, material);
|
||||
ReferenceLineMesh.current.name = CONSTANTS.lineConfig.referenceName;
|
||||
ReferenceLineMesh.current.userData = {
|
||||
linePoints: { startPoint, cursorPosition },
|
||||
};
|
||||
floorPlanGroup.current?.add(ReferenceLineMesh.current);
|
||||
LineCreated.current = true;
|
||||
} else {
|
||||
if (ReferenceLineMesh.current) {
|
||||
const path = new THREE.LineCurve3(startPoint, new THREE.Vector3(cursorPosition.x, 0.01, cursorPosition.z));
|
||||
Tube.current = new THREE.TubeGeometry(path, CONSTANTS.lineConfig.tubularSegments, CONSTANTS.lineConfig.radius, CONSTANTS.lineConfig.radialSegments, false);
|
||||
|
||||
if (ReferenceLineMesh.current) {
|
||||
ReferenceLineMesh.current.userData = {
|
||||
linePoints: { startPoint, cursorPosition },
|
||||
};
|
||||
ReferenceLineMesh.current.geometry.dispose();
|
||||
ReferenceLineMesh.current.geometry = Tube.current;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default createAndMoveReferenceLine;
|
||||
92
app/src/modules/builder/geomentries/lines/deleteLine.ts
Normal file
92
app/src/modules/builder/geomentries/lines/deleteLine.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Socket } from "socket.io-client";
|
||||
// import { deleteLineApi } from "../../../../services/factoryBuilder/lines/deleteLineApi";
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
function deleteLine(
|
||||
hoveredDeletableLine: Types.RefMesh,
|
||||
onlyFloorlines: Types.RefOnlyFloorLines,
|
||||
lines: Types.RefLines,
|
||||
floorPlanGroupLine: Types.RefGroup,
|
||||
floorPlanGroupPoint: Types.RefGroup,
|
||||
setDeletedLines: any,
|
||||
socket: Socket<any>,
|
||||
projectId?: string
|
||||
): void {
|
||||
|
||||
////////// Deleting a line and the points if they are not connected to any other line //////////
|
||||
|
||||
if (!hoveredDeletableLine.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const linePoints = hoveredDeletableLine.current.userData.linePoints;
|
||||
const connectedpoints = [linePoints[0][1], linePoints[1][1]];
|
||||
|
||||
const email = localStorage.getItem('email')
|
||||
const organization = (email!.split("@")[1]).split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
//REST
|
||||
|
||||
// deleteLineApi(
|
||||
// organization,
|
||||
// [
|
||||
// { "uuid": linePoints[0][1] },
|
||||
// { "uuid": linePoints[1][1] }
|
||||
// ]
|
||||
// )
|
||||
|
||||
//SOCKET
|
||||
|
||||
const data = {
|
||||
organization: organization,
|
||||
line: [
|
||||
{ "uuid": linePoints[0][1] },
|
||||
{ "uuid": linePoints[1][1] }
|
||||
],
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
}
|
||||
|
||||
socket.emit('v1:Line:delete', data);
|
||||
|
||||
|
||||
onlyFloorlines.current = onlyFloorlines.current.map(floorline =>
|
||||
floorline.filter(line => line[0][1] !== connectedpoints[0] && line[1][1] !== connectedpoints[1])
|
||||
).filter(floorline => floorline.length > 0);
|
||||
|
||||
lines.current = lines.current.filter(item => item !== linePoints);
|
||||
(<any>hoveredDeletableLine.current.material).dispose();
|
||||
(<any>hoveredDeletableLine.current.geometry).dispose();
|
||||
floorPlanGroupLine.current.remove(hoveredDeletableLine.current);
|
||||
setDeletedLines([linePoints]);
|
||||
|
||||
connectedpoints.forEach((pointUUID) => {
|
||||
let isConnected = false;
|
||||
floorPlanGroupLine.current.children.forEach((line) => {
|
||||
const linePoints = line.userData.linePoints;
|
||||
const uuid1 = linePoints[0][1];
|
||||
const uuid2 = linePoints[1][1];
|
||||
if (uuid1 === pointUUID || uuid2 === pointUUID) {
|
||||
isConnected = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!isConnected) {
|
||||
floorPlanGroupPoint.current.children.forEach((point: any) => {
|
||||
if (point.uuid === pointUUID) {
|
||||
(<any>point.material).dispose();
|
||||
(<any>point.geometry).dispose();
|
||||
floorPlanGroupPoint.current.remove(point);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
echo.success("Line Removed!");
|
||||
}
|
||||
|
||||
export default deleteLine;
|
||||
@@ -0,0 +1,223 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { getLines } from "../../../../../services/factoryBuilder/lines/getLinesApi";
|
||||
import * as THREE from "three";
|
||||
import {
|
||||
useActiveLayer,
|
||||
useDeletedLines,
|
||||
useNewLines,
|
||||
useRoomsState,
|
||||
useToggleView,
|
||||
} from "../../../../../store/builder/store";
|
||||
import objectLinesToArray from "../lineConvertions/objectLinesToArray";
|
||||
import { Html } from "@react-three/drei";
|
||||
import { Vector2 } from "three";
|
||||
import * as Types from "../../../../../types/world/worldTypes";
|
||||
import getRoomsFromLines from "../getRoomsFromLines";
|
||||
import * as turf from '@turf/turf';
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
const DistanceText = () => {
|
||||
const [lines, setLines] = useState<
|
||||
{
|
||||
distance: string;
|
||||
position: THREE.Vector3;
|
||||
userData: Types.Line;
|
||||
layer: string;
|
||||
}[]
|
||||
>([]);
|
||||
const { activeLayer } = useActiveLayer();
|
||||
const { toggleView } = useToggleView();
|
||||
const { newLines, setNewLines } = useNewLines();
|
||||
const { deletedLines, setDeletedLines } = useDeletedLines();
|
||||
const [linesState, setLinesState] = useState<Types.Lines>([]);
|
||||
const { roomsState, setRoomsState } = useRoomsState();
|
||||
const { projectId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (linesState.length === 0) return;
|
||||
const getLines = async () => {
|
||||
const points3D = linesState.map(line => {
|
||||
const startPoint = line[0][0]; // First point of each wall line
|
||||
return [startPoint.x, 0, startPoint.z];
|
||||
});
|
||||
|
||||
// Ensure the polygon is closed
|
||||
if (
|
||||
points3D[0][0] !== points3D[points3D.length - 1][0] ||
|
||||
points3D[0][1] !== points3D[points3D.length - 1][1]
|
||||
) {
|
||||
points3D.push(points3D[0]);
|
||||
}
|
||||
|
||||
// Convert to 2D for turf (x, z)
|
||||
const coords2D = points3D.map(p => [p[0], p[1]]);
|
||||
|
||||
const projected = points3D.map((p: any) => new Vector2(p[0], p[1]));
|
||||
|
||||
// Shoelace formula for 2D polygon
|
||||
let area = 0;
|
||||
const n = projected.length;
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
const curr = projected[i];
|
||||
const next = projected[i + 1];
|
||||
area += curr.x * next.y - next.x * curr.y;
|
||||
|
||||
}
|
||||
|
||||
// return Math.abs(area) / 2;
|
||||
|
||||
// Build polygon and compute area
|
||||
// const polygon = turf.polygon([coords2D]);
|
||||
// const area = turf.area(polygon);
|
||||
// const area = computeAreaFrom3DPoints(coords2D)
|
||||
|
||||
//
|
||||
if (lines.length > 2) {
|
||||
const linesByLayer = linesState.reduce((acc: { [key: number]: any[] }, pair) => {
|
||||
const layer = pair[0][2];
|
||||
if (!acc[layer]) acc[layer] = [];
|
||||
acc[layer].push(pair);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
|
||||
for (const layer in linesByLayer) {
|
||||
const rooms: Types.Rooms = await getRoomsFromLines({ current: linesByLayer[layer] });
|
||||
setRoomsState(rooms)
|
||||
}
|
||||
}
|
||||
}
|
||||
getLines();
|
||||
}, [linesState, roomsState])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const email = localStorage.getItem("email");
|
||||
if (!email) return;
|
||||
const organization = email.split("@")[1].split(".")[0];
|
||||
|
||||
getLines(organization,projectId).then((data) => {
|
||||
data = objectLinesToArray(data);
|
||||
setLinesState(data);
|
||||
|
||||
const lines = data
|
||||
.filter((line: Types.Line) => line[0][2] === activeLayer)
|
||||
.map((line: Types.Line) => {
|
||||
const point1 = new THREE.Vector3(
|
||||
line[0][0].x,
|
||||
line[0][0].y,
|
||||
line[0][0].z
|
||||
);
|
||||
const point2 = new THREE.Vector3(
|
||||
line[1][0].x,
|
||||
line[1][0].y,
|
||||
line[1][0].z
|
||||
);
|
||||
const distance = point1.distanceTo(point2);
|
||||
const midpoint = new THREE.Vector3()
|
||||
.addVectors(point1, point2)
|
||||
.divideScalar(2);
|
||||
return {
|
||||
distance: distance.toFixed(1),
|
||||
position: midpoint,
|
||||
userData: line,
|
||||
layer: activeLayer,
|
||||
};
|
||||
});
|
||||
setLines(lines);
|
||||
});
|
||||
}, [activeLayer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (newLines.length > 0) {
|
||||
if (newLines[0][0][2] !== activeLayer) return;
|
||||
const newLinesData = newLines.map((line: Types.Line) => {
|
||||
const point1 = new THREE.Vector3(
|
||||
line[0][0].x,
|
||||
line[0][0].y,
|
||||
line[0][0].z
|
||||
);
|
||||
const point2 = new THREE.Vector3(
|
||||
line[1][0].x,
|
||||
line[1][0].y,
|
||||
line[1][0].z
|
||||
);
|
||||
const distance = point1.distanceTo(point2);
|
||||
const midpoint = new THREE.Vector3()
|
||||
.addVectors(point1, point2)
|
||||
.divideScalar(2);
|
||||
|
||||
return {
|
||||
distance: distance.toFixed(1),
|
||||
position: midpoint,
|
||||
userData: line,
|
||||
layer: activeLayer,
|
||||
};
|
||||
});
|
||||
setLines((prevLines) => [...prevLines, ...newLinesData]);
|
||||
setLinesState((prevLines) => [...prevLines, ...newLines]);
|
||||
setNewLines([]);
|
||||
}
|
||||
}, [newLines, activeLayer]);
|
||||
|
||||
useEffect(() => {
|
||||
if ((deletedLines as Types.Lines).length > 0) {
|
||||
setLines((prevLines) =>
|
||||
prevLines.filter(
|
||||
(line) =>
|
||||
!deletedLines.some(
|
||||
(deletedLine: any) =>
|
||||
deletedLine[0][1] === line.userData[0][1] &&
|
||||
deletedLine[1][1] === line.userData[1][1]
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
setLinesState(prev =>
|
||||
prev.filter(line =>
|
||||
!(deletedLines as Types.Lines).some(
|
||||
deleted =>
|
||||
line[0][1] === deleted[0][1] && line[1][1] === deleted[1][1]
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
setDeletedLines([]);
|
||||
setRoomsState([])
|
||||
}
|
||||
}, [deletedLines]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{toggleView && (
|
||||
<group name="Distance_Text">
|
||||
{lines.map((text) => (
|
||||
<Html
|
||||
// data
|
||||
key={`${text.userData[0][1]}_${text.userData[1][1]}`}
|
||||
userData={text.userData}
|
||||
position={[text.position.x, 1, text.position.z]}
|
||||
// class
|
||||
wrapperClass="distance-text-wrapper"
|
||||
className="distance-text"
|
||||
// other
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
sprite
|
||||
>
|
||||
<div
|
||||
key={`${text.userData[0][1]}_${text.userData[1][1]}`}
|
||||
className={`distance line-${text.userData[0][1]}_${text.userData[1][1]}_${text.layer}`}
|
||||
>
|
||||
{text.distance} m
|
||||
</div>
|
||||
</Html>
|
||||
))}
|
||||
</group>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DistanceText;
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as THREE from "three";
|
||||
import { Html } from "@react-three/drei";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useActiveLayer } from "../../../../../store/builder/store";
|
||||
|
||||
const ReferenceDistanceText = ({ line }: { line: any }) => {
|
||||
interface TextState {
|
||||
distance: string;
|
||||
position: THREE.Vector3;
|
||||
userData: any;
|
||||
layer: any;
|
||||
}
|
||||
|
||||
const [text, setTexts] = useState<TextState | null>(null);
|
||||
const { activeLayer } = useActiveLayer();
|
||||
|
||||
useEffect(() => {
|
||||
if (line) {
|
||||
if (line.parent === null) {
|
||||
setTexts(null);
|
||||
return;
|
||||
}
|
||||
const distance = line.userData.linePoints.cursorPosition.distanceTo(
|
||||
line.userData.linePoints.startPoint
|
||||
);
|
||||
const midpoint = new THREE.Vector3()
|
||||
.addVectors(
|
||||
line.userData.linePoints.cursorPosition,
|
||||
line.userData.linePoints.startPoint
|
||||
)
|
||||
.divideScalar(2);
|
||||
const newTexts = {
|
||||
distance: distance.toFixed(1),
|
||||
position: midpoint,
|
||||
userData: line,
|
||||
layer: activeLayer,
|
||||
};
|
||||
setTexts(newTexts);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<group name="Reference_Distance_Text">
|
||||
<mesh>
|
||||
{text !== null && (
|
||||
<Html
|
||||
// data
|
||||
key={text.distance}
|
||||
userData={text.userData}
|
||||
position={[text.position.x, 1, text.position.z]}
|
||||
// class
|
||||
wrapperClass="distance-text-wrapper"
|
||||
className="distance-text"
|
||||
// other
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
sprite
|
||||
>
|
||||
<div
|
||||
className={`Reference_Distance line-${text.userData.userData}`}
|
||||
>
|
||||
{text.distance} m
|
||||
</div>
|
||||
</Html>
|
||||
)}
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReferenceDistanceText;
|
||||
176
app/src/modules/builder/geomentries/lines/drawWall.ts
Normal file
176
app/src/modules/builder/geomentries/lines/drawWall.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import * as THREE from 'three';
|
||||
import * as CONSTANTS from '../../../../types/world/worldConstants';
|
||||
|
||||
import addPointToScene from '../points/addPointToScene';
|
||||
import addLineToScene from './addLineToScene';
|
||||
import splitLine from './splitLine';
|
||||
import removeReferenceLine from './removeReferenceLine';
|
||||
import getClosestIntersection from './getClosestIntersection';
|
||||
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
import arrayLineToObject from './lineConvertions/arrayLineToObject';
|
||||
// import { setLine } from '../../../../services/factoryBuilder/lines/setLineApi';
|
||||
import { Socket } from 'socket.io-client';
|
||||
|
||||
async function drawWall(
|
||||
raycaster: THREE.Raycaster,
|
||||
plane: Types.RefMesh,
|
||||
floorPlanGroupPoint: Types.RefGroup,
|
||||
snappedPoint: Types.RefVector3,
|
||||
isSnapped: Types.RefBoolean,
|
||||
isSnappedUUID: Types.RefString,
|
||||
line: Types.RefLine,
|
||||
ispreSnapped: Types.RefBoolean,
|
||||
anglesnappedPoint: Types.RefVector3,
|
||||
isAngleSnapped: Types.RefBoolean,
|
||||
lines: Types.RefLines,
|
||||
floorPlanGroupLine: Types.RefGroup,
|
||||
floorPlanGroup: Types.RefGroup,
|
||||
ReferenceLineMesh: Types.RefMesh,
|
||||
LineCreated: Types.RefBoolean,
|
||||
currentLayerPoint: Types.RefMeshArray,
|
||||
dragPointControls: Types.RefDragControl,
|
||||
setNewLines: any,
|
||||
setDeletedLines: any,
|
||||
activeLayer: Types.Number,
|
||||
socket: Socket<any>,
|
||||
projectId?: string
|
||||
): Promise<void> {
|
||||
|
||||
////////// Creating lines Based on the positions clicked //////////
|
||||
|
||||
////////// Allows the user lines that represents walls and roof, floor if forms a polygon //////////
|
||||
|
||||
|
||||
if (!plane.current) return
|
||||
let intersects = raycaster.intersectObject(plane.current, true);
|
||||
|
||||
let intersectsLines = raycaster.intersectObjects(floorPlanGroupLine.current.children, true);
|
||||
let intersectsPoint = raycaster.intersectObjects(floorPlanGroupPoint.current.children, true);
|
||||
|
||||
const VisibleintersectsPoint = intersectsPoint.find(intersect => intersect.object.visible);
|
||||
const visibleIntersect = intersectsLines.find(intersect => intersect.object.visible && intersect.object.name !== CONSTANTS.lineConfig.referenceName && intersect.object.userData.linePoints[0][3] === CONSTANTS.lineConfig.wallName);
|
||||
|
||||
if ((intersectsPoint.length === 0 || VisibleintersectsPoint === undefined) && intersectsLines.length > 0 && !isSnapped.current && !ispreSnapped.current) {
|
||||
|
||||
////////// Clicked on a preexisting Line //////////
|
||||
|
||||
if (visibleIntersect && intersects) {
|
||||
let IntersectsPoint = new THREE.Vector3(intersects[0].point.x, 0.01, intersects[0].point.z);
|
||||
|
||||
if (isAngleSnapped.current && anglesnappedPoint.current) {
|
||||
IntersectsPoint = anglesnappedPoint.current;
|
||||
}
|
||||
if (visibleIntersect.object instanceof THREE.Mesh) {
|
||||
const ThroughPoint = (visibleIntersect.object.geometry.parameters.path).getPoints(CONSTANTS.lineConfig.lineIntersectionPoints);
|
||||
let intersectionPoint = getClosestIntersection(ThroughPoint, IntersectsPoint);
|
||||
|
||||
if (intersectionPoint) {
|
||||
|
||||
const newLines = splitLine(visibleIntersect, intersectionPoint, currentLayerPoint, floorPlanGroupPoint, dragPointControls, isSnappedUUID, lines, setDeletedLines, floorPlanGroupLine, socket, CONSTANTS.pointConfig.wallOuterColor, CONSTANTS.lineConfig.wallColor, CONSTANTS.lineConfig.wallName,projectId);
|
||||
setNewLines([newLines[0], newLines[1]]);
|
||||
|
||||
(line.current as Types.Line).push([new THREE.Vector3(intersectionPoint.x, 0.01, intersectionPoint.z), isSnappedUUID.current!, activeLayer, CONSTANTS.lineConfig.wallName,]);
|
||||
|
||||
if (line.current.length >= 2 && line.current[0] && line.current[1]) {
|
||||
const data = arrayLineToObject(line.current as Types.Line);
|
||||
|
||||
const email = localStorage.getItem('email')
|
||||
const organization = (email!.split("@")[1]).split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
//REST
|
||||
|
||||
// setLine(organization, data.layer!, data.line!, data.type!);
|
||||
|
||||
//SOCKET
|
||||
|
||||
const input = {
|
||||
organization: organization,
|
||||
layer: data.layer,
|
||||
line: data.line,
|
||||
type: data.type,
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
}
|
||||
|
||||
console.log('input: ', input);
|
||||
socket.emit('v1:Line:create', input);
|
||||
|
||||
setNewLines([newLines[0], newLines[1], line.current]);
|
||||
lines.current.push(line.current as Types.Line);
|
||||
addLineToScene(line.current[0][0], line.current[1][0], CONSTANTS.lineConfig.wallColor, line.current, floorPlanGroupLine);
|
||||
let lastPoint = line.current[line.current.length - 1];
|
||||
line.current = [lastPoint];
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (intersects && intersects.length > 0) {
|
||||
|
||||
////////// Clicked on a emply place or a point //////////
|
||||
|
||||
let intersectionPoint = intersects[0].point;
|
||||
|
||||
if (isAngleSnapped.current && line.current.length > 0 && anglesnappedPoint.current) {
|
||||
intersectionPoint = anglesnappedPoint.current;
|
||||
}
|
||||
if (isSnapped.current && line.current.length > 0 && snappedPoint.current) {
|
||||
intersectionPoint = snappedPoint.current;
|
||||
}
|
||||
if (ispreSnapped.current && snappedPoint.current) {
|
||||
intersectionPoint = snappedPoint.current;
|
||||
}
|
||||
|
||||
if (!isSnapped.current && !ispreSnapped.current) {
|
||||
addPointToScene(intersectionPoint, CONSTANTS.pointConfig.wallOuterColor, currentLayerPoint, floorPlanGroupPoint, dragPointControls, isSnappedUUID, CONSTANTS.lineConfig.wallName);
|
||||
} else {
|
||||
ispreSnapped.current = false;
|
||||
isSnapped.current = false;
|
||||
}
|
||||
|
||||
(line.current as Types.Line).push([new THREE.Vector3(intersectionPoint.x, 0.01, intersectionPoint.z), isSnappedUUID.current!, activeLayer, CONSTANTS.lineConfig.wallName,]);
|
||||
|
||||
if (line.current.length >= 2 && line.current[0] && line.current[1]) {
|
||||
const data = arrayLineToObject(line.current as Types.Line);
|
||||
|
||||
const email = localStorage.getItem('email')
|
||||
const organization = (email!.split("@")[1]).split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
//REST
|
||||
|
||||
// setLine(organization, data.layer!, data.line!, data.type!);
|
||||
|
||||
//SOCKET
|
||||
|
||||
const input = {
|
||||
organization: organization,
|
||||
layer: data.layer,
|
||||
line: data.line,
|
||||
type: data.type,
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
}
|
||||
|
||||
console.log('input: ', input);
|
||||
socket.emit('v1:Line:create', input);
|
||||
|
||||
setNewLines([line.current])
|
||||
lines.current.push(line.current as Types.Line);
|
||||
addLineToScene(line.current[0][0], line.current[1][0], CONSTANTS.lineConfig.wallColor, line.current, floorPlanGroupLine);
|
||||
let lastPoint = line.current[line.current.length - 1];
|
||||
line.current = [lastPoint];
|
||||
}
|
||||
if (isSnapped.current) {
|
||||
removeReferenceLine(floorPlanGroup, ReferenceLineMesh, LineCreated, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default drawWall;
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function getClosestIntersection(
|
||||
intersects: Types.Vector3Array,
|
||||
point: Types.Vector3
|
||||
): Types.Vector3 | null {
|
||||
|
||||
////////// A function that finds which point is closest from the intersects points that is given, Used in finding which point in a line is closest when clicked on a line during drawing //////////
|
||||
|
||||
let closestNewPoint: THREE.Vector3 | null = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const intersect of intersects) {
|
||||
const distance = point.distanceTo(intersect);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestNewPoint = intersect;
|
||||
}
|
||||
}
|
||||
|
||||
return closestNewPoint;
|
||||
}
|
||||
|
||||
export default getClosestIntersection;
|
||||
@@ -0,0 +1,86 @@
|
||||
import * as THREE from 'three';
|
||||
import * as turf from '@turf/turf';
|
||||
import * as CONSTANTS from '../../../../types/world/worldConstants';
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
async function getRoomsFromLines(lines: Types.RefLines) {
|
||||
const rooms: Types.Rooms = [];
|
||||
|
||||
if (lines.current.length > 2) {
|
||||
const linesByLayer = lines.current.reduce((acc: { [key: number]: any[] }, pair) => {
|
||||
const layer = pair[0][2];
|
||||
if (!acc[layer]) acc[layer] = [];
|
||||
acc[layer].push(pair);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
////////// Use turf.polygonize to create polygons from the line points //////////
|
||||
|
||||
for (const layer in linesByLayer) {
|
||||
|
||||
let linesInLayer = linesByLayer[layer];
|
||||
linesInLayer = linesInLayer.filter(line => line[0][3] && line[1][3] === CONSTANTS.lineConfig.wallName);
|
||||
const result = linesInLayer.map((pair: [THREE.Vector3, string, number, string][]) =>
|
||||
pair.map((point) => ({
|
||||
position: [point[0].x, point[0].z],
|
||||
uuid: point[1]
|
||||
}))
|
||||
);
|
||||
const lineFeatures = result.map(line => turf.lineString(line.map(p => p.position)));
|
||||
const polygons = turf.polygonize(turf.featureCollection(lineFeatures));
|
||||
|
||||
let union: any[] = [];
|
||||
|
||||
polygons.features.forEach((feature) => {
|
||||
union.push(feature);
|
||||
});
|
||||
|
||||
if (union.length > 1) {
|
||||
const unionResult = turf.union(turf.featureCollection(union));
|
||||
if (unionResult?.geometry.type === "MultiPolygon") {
|
||||
unionResult?.geometry.coordinates.forEach((poly) => {
|
||||
const Coordinates = poly[0].map(([x, z]) => {
|
||||
const matchingPoint = result.flat().find(r =>
|
||||
r.position[0].toFixed(10) === x.toFixed(10) &&
|
||||
r.position[1].toFixed(10) === z.toFixed(10)
|
||||
);
|
||||
return {
|
||||
position: new THREE.Vector3(x, 0, z),
|
||||
uuid: matchingPoint ? matchingPoint.uuid : ''
|
||||
};
|
||||
});
|
||||
rooms.push({ coordinates: Coordinates.reverse(), layer: parseInt(layer) });
|
||||
});
|
||||
} else if (unionResult?.geometry.type === "Polygon") {
|
||||
const Coordinates = unionResult?.geometry.coordinates[0].map(([x, z]) => {
|
||||
const matchingPoint = result.flat().find(r =>
|
||||
r.position[0].toFixed(10) === x.toFixed(10) &&
|
||||
r.position[1].toFixed(10) === z.toFixed(10)
|
||||
);
|
||||
return {
|
||||
position: new THREE.Vector3(x, 0, z),
|
||||
uuid: matchingPoint ? matchingPoint.uuid : ''
|
||||
};
|
||||
});
|
||||
rooms.push({ coordinates: Coordinates.reverse(), layer: parseInt(layer) });
|
||||
}
|
||||
} else if (union.length === 1) {
|
||||
const Coordinates = union[0].geometry.coordinates[0].map(([x, z]: [number, number]) => {
|
||||
const matchingPoint = result.flat().find(r =>
|
||||
r.position[0].toFixed(10) === x.toFixed(10) &&
|
||||
r.position[1].toFixed(10) === z.toFixed(10)
|
||||
);
|
||||
return {
|
||||
position: new THREE.Vector3(x, 0, z),
|
||||
uuid: matchingPoint ? matchingPoint.uuid : ''
|
||||
};
|
||||
});
|
||||
rooms.push({ coordinates: Coordinates, layer: parseInt(layer) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
export default getRoomsFromLines;
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as Types from "../../../../../types/world/worldTypes";
|
||||
|
||||
export default function arrayLineToObject(array: Types.Line) {
|
||||
if (!Array.isArray(array)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Extract common properties from the first point
|
||||
const commonLayer = array[0][2];
|
||||
const commonType = array[0][3];
|
||||
|
||||
// Map points into a structured format
|
||||
const line = array.map(([position, uuid]) => ({
|
||||
position,
|
||||
uuid,
|
||||
}));
|
||||
|
||||
// Create the final structured object
|
||||
return {
|
||||
layer: commonLayer,
|
||||
type: commonType,
|
||||
line,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as Types from "../../../../../types/world/worldTypes";
|
||||
|
||||
export default function arrayLinesToObject(array: Array<Types.Line>) {
|
||||
if (!Array.isArray(array)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array.map((lineArray) => {
|
||||
if (!Array.isArray(lineArray)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract common properties from the first point
|
||||
const commonLayer = lineArray[0][2];
|
||||
const commonType = lineArray[0][3];
|
||||
|
||||
// Map points into a structured format
|
||||
const line = lineArray.map(([position, uuid]) => ({
|
||||
position,
|
||||
uuid,
|
||||
}));
|
||||
|
||||
// Create the final structured object
|
||||
return {
|
||||
layer: commonLayer,
|
||||
type: commonType,
|
||||
line,
|
||||
};
|
||||
}).filter((item) => item !== null); // Filter out invalid entries
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
export default function objectLineToArray(structuredObject: any) {
|
||||
if (!structuredObject || !structuredObject.line) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Destructure common properties
|
||||
const { layer, type, line } = structuredObject;
|
||||
|
||||
// Map points back to the original array format
|
||||
return line.map(({ position, uuid }: any) => [new THREE.Vector3(position.x, position.y, position.z), uuid, layer, type]);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
export default function objectLinesToArray(structuredObjects: any): any {
|
||||
if (!Array.isArray(structuredObjects)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return structuredObjects.map((structuredObject) => {
|
||||
if (!structuredObject || !structuredObject.line) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { layer, type, line } = structuredObject;
|
||||
|
||||
return line.map(({ position, uuid }: any) => {
|
||||
const vector = new THREE.Vector3(position.x, position.y, position.z);
|
||||
return [vector, uuid, layer, type];
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function RemoveConnectedLines(
|
||||
DeletedPointUUID: Types.String,
|
||||
floorPlanGroupLine: Types.RefGroup,
|
||||
floorPlanGroupPoint: Types.RefGroup,
|
||||
setDeletedLines: any,
|
||||
lines: Types.RefLines,
|
||||
): void {
|
||||
|
||||
////////// Check if any and how many lines are connected to the deleted point //////////
|
||||
|
||||
const removableLines: THREE.Mesh[] = [];
|
||||
const connectedpoints: string[] = [];
|
||||
|
||||
const removedLinePoints: [number, string, number][][] = []; // Array to hold linePoints of removed lines
|
||||
|
||||
floorPlanGroupLine.current.children.forEach((line) => {
|
||||
const linePoints = line.userData.linePoints as [number, string, number][];
|
||||
const uuid1 = linePoints[0][1];
|
||||
const uuid2 = linePoints[1][1];
|
||||
|
||||
if (uuid1 === DeletedPointUUID || uuid2 === DeletedPointUUID) {
|
||||
connectedpoints.push(uuid1 === DeletedPointUUID ? uuid2 : uuid1);
|
||||
removableLines.push(line as THREE.Mesh);
|
||||
removedLinePoints.push(linePoints);
|
||||
}
|
||||
});
|
||||
|
||||
if (removableLines.length > 0) {
|
||||
removableLines.forEach((line) => {
|
||||
lines.current = lines.current.filter(item => item !== line.userData.linePoints);
|
||||
(<any>line.material).dispose();
|
||||
(<any>line.geometry).dispose();
|
||||
floorPlanGroupLine.current.remove(line);
|
||||
});
|
||||
}
|
||||
setDeletedLines(removedLinePoints)
|
||||
|
||||
////////// Check and Remove point that are no longer connected to any lines //////////
|
||||
|
||||
connectedpoints.forEach((pointUUID) => {
|
||||
let isConnected = false;
|
||||
floorPlanGroupLine.current.children.forEach((line) => {
|
||||
const linePoints = line.userData.linePoints as [number, string, number][];
|
||||
const uuid1 = linePoints[0][1];
|
||||
const uuid2 = linePoints[1][1];
|
||||
if (uuid1 === pointUUID || uuid2 === pointUUID) {
|
||||
isConnected = true;
|
||||
}
|
||||
});
|
||||
if (!isConnected) {
|
||||
floorPlanGroupPoint.current.children.forEach((point: any) => {
|
||||
if (point.uuid === pointUUID) {
|
||||
(<any>point.material).dispose();
|
||||
(<any>point.geometry).dispose();
|
||||
floorPlanGroupPoint.current.remove(point);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default RemoveConnectedLines;
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function removeReferenceLine(
|
||||
floorPlanGroup: Types.RefGroup,
|
||||
ReferenceLineMesh: Types.RefMesh,
|
||||
LineCreated: Types.RefBoolean,
|
||||
line: Types.RefLine
|
||||
): void {
|
||||
|
||||
////////// Removes Dangling reference line if the draw mode is ended or any other case //////////
|
||||
|
||||
line.current = [];
|
||||
if (ReferenceLineMesh.current) {
|
||||
(<any>ReferenceLineMesh.current.material).dispose();
|
||||
(<any>ReferenceLineMesh.current.geometry).dispose();
|
||||
floorPlanGroup.current.remove(ReferenceLineMesh.current);
|
||||
LineCreated.current = false;
|
||||
ReferenceLineMesh.current = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export default removeReferenceLine;
|
||||
132
app/src/modules/builder/geomentries/lines/splitLine.ts
Normal file
132
app/src/modules/builder/geomentries/lines/splitLine.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
import addLineToScene from './addLineToScene';
|
||||
import addPointToScene from '../points/addPointToScene';
|
||||
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
import arrayLineToObject from '../lines/lineConvertions/arrayLineToObject';
|
||||
import { Socket } from 'socket.io-client';
|
||||
// import { deleteLineApi } from '../../../../services/factoryBuilder/lines/deleteLineApi';
|
||||
// import { setLine } from '../../../../services/factoryBuilder/lines/setLineApi';
|
||||
|
||||
function splitLine(
|
||||
visibleIntersect: Types.IntersectionEvent,
|
||||
intersectionPoint: Types.Vector3,
|
||||
currentLayerPoint: Types.RefMeshArray,
|
||||
floorPlanGroupPoint: Types.RefGroup,
|
||||
dragPointControls: Types.RefDragControl,
|
||||
isSnappedUUID: Types.RefString,
|
||||
lines: Types.RefLines,
|
||||
setDeletedLines: any,
|
||||
floorPlanGroupLine: { current: THREE.Group },
|
||||
socket: Socket<any>,
|
||||
pointColor: Types.String,
|
||||
lineColor: Types.String,
|
||||
lineType: Types.String,
|
||||
projectId?: string
|
||||
): [Types.Line, Types.Line] {
|
||||
|
||||
////////// Removing the clicked line and splitting it with the clicked position adding a new point and two new lines //////////
|
||||
|
||||
|
||||
((visibleIntersect.object as any).material).dispose();
|
||||
((visibleIntersect.object as any).geometry).dispose();
|
||||
floorPlanGroupLine.current.remove(visibleIntersect.object);
|
||||
setDeletedLines([visibleIntersect.object.userData.linePoints]);
|
||||
|
||||
const email = localStorage.getItem('email')
|
||||
const organization = (email!.split("@")[1]).split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
//REST
|
||||
|
||||
// deleteLineApi(
|
||||
// organization,
|
||||
// [
|
||||
// { "uuid": visibleIntersect.object.userData.linePoints[0][1] },
|
||||
// { "uuid": visibleIntersect.object.userData.linePoints[1][1] }
|
||||
// ]
|
||||
// )
|
||||
|
||||
//SOCKET
|
||||
|
||||
|
||||
const data = {
|
||||
organization: organization,
|
||||
line: [
|
||||
{ "uuid": visibleIntersect.object.userData.linePoints[0][1] },
|
||||
{ "uuid": visibleIntersect.object.userData.linePoints[1][1] }
|
||||
],
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
}
|
||||
|
||||
socket.emit('v1:Line:delete', data);
|
||||
|
||||
const point = addPointToScene(intersectionPoint, pointColor, currentLayerPoint, floorPlanGroupPoint, dragPointControls, isSnappedUUID, lineType);
|
||||
|
||||
const oldLinePoints = visibleIntersect.object.userData.linePoints;
|
||||
lines.current = lines.current.filter(item => item !== oldLinePoints);
|
||||
|
||||
const clickedPoint: Types.Point = [
|
||||
new THREE.Vector3(intersectionPoint.x, 0.01, intersectionPoint.z),
|
||||
point.uuid,
|
||||
oldLinePoints[0][2],
|
||||
lineType
|
||||
];
|
||||
|
||||
const start = oldLinePoints[0];
|
||||
const end = oldLinePoints[1];
|
||||
|
||||
const newLine1: Types.Line = [start, clickedPoint];
|
||||
const newLine2: Types.Line = [clickedPoint, end];
|
||||
|
||||
const line1 = arrayLineToObject(newLine1);
|
||||
const line2 = arrayLineToObject(newLine2);
|
||||
|
||||
//REST
|
||||
|
||||
// setLine(organization, line1.layer!, line1.line!, line1.type!);
|
||||
|
||||
//SOCKET
|
||||
|
||||
const input1 = {
|
||||
organization: organization,
|
||||
layer: line1.layer,
|
||||
line: line1.line,
|
||||
type: line1.type,
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
}
|
||||
|
||||
socket.emit('v1:Line:create', input1);
|
||||
|
||||
//REST
|
||||
|
||||
// setLine(organization, line2.layer!, line2.line!, line2.type!);
|
||||
|
||||
//SOCKET
|
||||
|
||||
const input2 = {
|
||||
organization: organization,
|
||||
layer: line2.layer,
|
||||
line: line2.line,
|
||||
type: line2.type,
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
}
|
||||
|
||||
socket.emit('v1:Line:create', input2);
|
||||
|
||||
lines.current.push(newLine1, newLine2);
|
||||
|
||||
addLineToScene(newLine1[0][0], newLine1[1][0], lineColor, newLine1, floorPlanGroupLine);
|
||||
addLineToScene(newLine2[0][0], newLine2[1][0], lineColor, newLine2, floorPlanGroupLine);
|
||||
|
||||
return [newLine1, newLine2];
|
||||
}
|
||||
|
||||
export default splitLine;
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function updateDistanceText(
|
||||
scene: THREE.Scene,
|
||||
floorPlanGroupLine: Types.RefGroup,
|
||||
affectedLines: Types.NumberArray
|
||||
): void {
|
||||
|
||||
////////// Updating the Distance Texts of the lines that are affected during drag //////////
|
||||
|
||||
const DistanceGroup = scene.getObjectByName('Distance_Text') as THREE.Group;
|
||||
|
||||
affectedLines.forEach((lineIndex) => {
|
||||
const mesh = floorPlanGroupLine.current.children[lineIndex] as THREE.Mesh;
|
||||
const linePoints = mesh.userData.linePoints;
|
||||
|
||||
if (linePoints) {
|
||||
const distance = linePoints[0][0].distanceTo(linePoints[1][0]).toFixed(1);
|
||||
const position = new THREE.Vector3().addVectors(linePoints[0][0], linePoints[1][0]).divideScalar(2);
|
||||
|
||||
if (!DistanceGroup || !linePoints) {
|
||||
return
|
||||
}
|
||||
|
||||
DistanceGroup.children.forEach((text) => {
|
||||
const textMesh = text as THREE.Mesh;
|
||||
if (textMesh.userData[0][1] === linePoints[0][1] && textMesh.userData[1][1] === linePoints[1][1]) {
|
||||
textMesh.position.set(position.x, 1, position.z);
|
||||
const className = `distance line-${textMesh.userData[0][1]}_${textMesh.userData[1][1]}_${linePoints[0][2]}`;
|
||||
const element = document.getElementsByClassName(className)[0] as HTMLElement;
|
||||
if (element) {
|
||||
element.innerHTML = `${distance} m`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default updateDistanceText;
|
||||
24
app/src/modules/builder/geomentries/lines/updateLines.ts
Normal file
24
app/src/modules/builder/geomentries/lines/updateLines.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as THREE from 'three';
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
import * as CONSTANTS from '../../../../types/world/worldConstants';
|
||||
|
||||
function updateLines(
|
||||
floorPlanGroupLine: Types.RefGroup,
|
||||
affectedLines: Types.NumberArray
|
||||
): void {
|
||||
|
||||
////////// Updating the positions for the affected lines only based on the updated positions //////////
|
||||
|
||||
affectedLines.forEach((lineIndex) => {
|
||||
const mesh = floorPlanGroupLine.current.children[lineIndex] as Types.Mesh;
|
||||
const linePoints = mesh.userData.linePoints as Types.Line;
|
||||
if (linePoints) {
|
||||
const newPositions = linePoints.map(([pos]) => pos);
|
||||
const newPath = new THREE.CatmullRomCurve3(newPositions);
|
||||
mesh.geometry.dispose();
|
||||
mesh.geometry = new THREE.TubeGeometry(newPath, CONSTANTS.lineConfig.tubularSegments, CONSTANTS.lineConfig.radius, CONSTANTS.lineConfig.radialSegments, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default updateLines;
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function updateLinesPositions(
|
||||
DragedPoint: Types.Mesh | { uuid: string, position: Types.Vector3 },
|
||||
lines: Types.RefLines
|
||||
): Types.NumberArray {
|
||||
|
||||
////////// Updating the lines position based on the dragged point's position //////////
|
||||
|
||||
const objectUUID = DragedPoint.uuid;
|
||||
const affectedLines: Types.NumberArray = [];
|
||||
|
||||
lines.current.forEach((line, index) => {
|
||||
let lineUpdated = false;
|
||||
line.forEach((point) => {
|
||||
const [position, uuid] = point;
|
||||
if (uuid === objectUUID) {
|
||||
position.x = DragedPoint.position.x;
|
||||
position.y = 0.01;
|
||||
position.z = DragedPoint.position.z;
|
||||
lineUpdated = true;
|
||||
}
|
||||
});
|
||||
if (lineUpdated) {
|
||||
affectedLines.push(index);
|
||||
}
|
||||
});
|
||||
|
||||
return affectedLines;
|
||||
}
|
||||
|
||||
export default updateLinesPositions;
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function vectorizeLinesCurrent(
|
||||
lines: Types.Lines
|
||||
): Types.Lines {
|
||||
|
||||
////////// Storing a vector3 array in localstorage makes the prototype functions go puff. This function brings back the prototype functions by creating it again //////////
|
||||
|
||||
return lines.map((line) => {
|
||||
const p1: Types.Point = [new THREE.Vector3(line[0][0].x, line[0][0].y, line[0][0].z), line[0][1], line[0][2], line[0][3],];
|
||||
const p2: Types.Point = [new THREE.Vector3(line[1][0].x, line[1][0].y, line[1][0].z), line[1][1], line[0][2], line[1][3],];
|
||||
return [p1, p2];
|
||||
});
|
||||
}
|
||||
|
||||
export default vectorizeLinesCurrent;
|
||||
@@ -0,0 +1,54 @@
|
||||
import * as THREE from 'three';
|
||||
import updateReferencePolesheight from './updateReferencePolesheight';
|
||||
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function addAndUpdateReferencePillar(
|
||||
raycaster: THREE.Raycaster,
|
||||
floorGroup: Types.RefGroup,
|
||||
referencePole: Types.RefMesh
|
||||
): void {
|
||||
|
||||
////////// Find Pillars position and scale based on the pointer interaction //////////
|
||||
|
||||
let Roofs = raycaster.intersectObjects(floorGroup.current.children, true);
|
||||
const intersected = Roofs.find(intersect => intersect.object.name.includes("Roof") || intersect.object.name.includes("Floor"));
|
||||
|
||||
if (intersected) {
|
||||
const intersectionPoint = intersected.point;
|
||||
raycaster.ray.origin.copy(intersectionPoint);
|
||||
raycaster.ray.direction.set(0, -1, 0);
|
||||
const belowIntersections = raycaster.intersectObjects(floorGroup.current.children, true);
|
||||
const validIntersections = belowIntersections.filter(intersect => intersect.object.name.includes("Floor"));
|
||||
|
||||
let distance: Types.Number;
|
||||
|
||||
if (validIntersections.length > 1) {
|
||||
let valid = validIntersections.find(intersectedBelow => intersected.point.distanceTo(intersectedBelow.point) > 3);
|
||||
if (valid) {
|
||||
updateReferencePolesheight(intersectionPoint, valid.distance, referencePole, floorGroup);
|
||||
} else {
|
||||
const belowPoint = new THREE.Vector3(intersectionPoint.x, 0, intersectionPoint.z);
|
||||
distance = intersected.point.distanceTo(belowPoint);
|
||||
if (distance > 3) {
|
||||
updateReferencePolesheight(intersectionPoint, distance, referencePole, floorGroup);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const belowPoint = new THREE.Vector3(intersectionPoint.x, 0, intersectionPoint.z);
|
||||
distance = intersected.point.distanceTo(belowPoint);
|
||||
if (distance > 3) {
|
||||
updateReferencePolesheight(intersectionPoint, distance, referencePole, floorGroup);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (referencePole.current) {
|
||||
(<any>referencePole.current.material).dispose();
|
||||
(<any>referencePole.current.geometry).dispose();
|
||||
floorGroup.current.remove(referencePole.current);
|
||||
referencePole.current = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default addAndUpdateReferencePillar;
|
||||
24
app/src/modules/builder/geomentries/pillars/addPillar.ts
Normal file
24
app/src/modules/builder/geomentries/pillars/addPillar.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as THREE from 'three';
|
||||
import * as CONSTANTS from '../../../../types/world/worldConstants';
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function addPillar(
|
||||
referencePole: Types.RefMesh,
|
||||
floorGroup: Types.RefGroup
|
||||
): void {
|
||||
|
||||
////////// Add Pillars to the scene based on the reference. current poles position and scale //////////
|
||||
|
||||
if (referencePole.current) {
|
||||
let pole: THREE.Mesh;
|
||||
const geometry = referencePole.current.userData.geometry.clone();
|
||||
const material = new THREE.MeshStandardMaterial({ color: CONSTANTS.columnConfig.defaultColor });
|
||||
pole = new THREE.Mesh(geometry, material);
|
||||
pole.rotateX(Math.PI / 2);
|
||||
pole.name = "Pole";
|
||||
pole.position.set(referencePole.current.userData.position.x, referencePole.current.userData.position.y, referencePole.current.userData.position.z);
|
||||
floorGroup.current.add(pole);
|
||||
}
|
||||
}
|
||||
|
||||
export default addPillar;
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function DeletableHoveredPillar(
|
||||
state: Types.ThreeState,
|
||||
floorGroup: Types.RefGroup,
|
||||
hoveredDeletablePillar: Types.RefMesh
|
||||
): void {
|
||||
|
||||
////////// Altering the color of the hovered Pillar during the Deletion time //////////
|
||||
|
||||
const intersects = state.raycaster.intersectObjects(floorGroup.current.children, true);
|
||||
const poleIntersect = intersects.find(intersect => intersect.object.name === "Pole");
|
||||
|
||||
if (poleIntersect) {
|
||||
if (poleIntersect.object.name !== "Pole") {
|
||||
return;
|
||||
}
|
||||
if (hoveredDeletablePillar.current) {
|
||||
(hoveredDeletablePillar.current.material as THREE.MeshStandardMaterial).emissive = new THREE.Color("black");
|
||||
hoveredDeletablePillar.current = undefined;
|
||||
}
|
||||
hoveredDeletablePillar.current = poleIntersect.object as THREE.Mesh; // Type assertion
|
||||
(hoveredDeletablePillar.current.material as THREE.MeshStandardMaterial).emissive = new THREE.Color("red");
|
||||
} else {
|
||||
if (hoveredDeletablePillar.current) {
|
||||
(hoveredDeletablePillar.current.material as THREE.MeshStandardMaterial).emissive = new THREE.Color("black");
|
||||
hoveredDeletablePillar.current = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DeletableHoveredPillar;
|
||||
21
app/src/modules/builder/geomentries/pillars/deletePillar.ts
Normal file
21
app/src/modules/builder/geomentries/pillars/deletePillar.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function DeletePillar(
|
||||
hoveredDeletablePillar: Types.RefMesh,
|
||||
floorGroup: Types.RefGroup
|
||||
): void {
|
||||
|
||||
////////// Deleting the hovered Pillar from the itemsGroup //////////
|
||||
|
||||
if (hoveredDeletablePillar.current) {
|
||||
(<any>hoveredDeletablePillar.current.material).dispose();
|
||||
(<any>hoveredDeletablePillar.current.geometry).dispose();
|
||||
floorGroup.current.remove(hoveredDeletablePillar.current);
|
||||
echo.success("Pillar Removed!");
|
||||
hoveredDeletablePillar.current = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export default DeletePillar;
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function updateReferencePolesheight(
|
||||
intersectionPoint: Types.Vector3,
|
||||
distance: Types.Number,
|
||||
referencePole: Types.RefMesh,
|
||||
floorGroup: Types.RefGroup
|
||||
): void {
|
||||
|
||||
////////// Add a Reference Pillar and update its position and scale based on the pointer interaction //////////
|
||||
|
||||
if (referencePole.current) {
|
||||
(<any>referencePole.current.material).dispose();
|
||||
(<any>referencePole.current.geometry).dispose();
|
||||
floorGroup.current.remove(referencePole.current);
|
||||
referencePole.current.geometry.dispose();
|
||||
}
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
shape.moveTo(0.5, 0);
|
||||
shape.absarc(0, 0, 0.5, 0, 2 * Math.PI, false);
|
||||
|
||||
const extrudeSettings = {
|
||||
depth: distance,
|
||||
bevelEnabled: false,
|
||||
};
|
||||
|
||||
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
|
||||
const material = new THREE.MeshBasicMaterial({ color: "green", transparent: true, opacity: 0.5 });
|
||||
referencePole.current = new THREE.Mesh(geometry, material);
|
||||
referencePole.current.rotateX(Math.PI / 2);
|
||||
referencePole.current.position.set(intersectionPoint.x, intersectionPoint.y - 0.01, intersectionPoint.z);
|
||||
referencePole.current.userData = { geometry: geometry, distance: distance, position: { x: intersectionPoint.x, y: intersectionPoint.y - 0.01, z: intersectionPoint.z } };
|
||||
|
||||
floorGroup.current.add(referencePole.current);
|
||||
}
|
||||
|
||||
export default updateReferencePolesheight;
|
||||
@@ -0,0 +1,65 @@
|
||||
import * as THREE from 'three';
|
||||
import * as CONSTANTS from '../../../../types/world/worldConstants';
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function addPointToScene(
|
||||
position: Types.Vector3,
|
||||
colour: Types.Color,
|
||||
currentLayerPoint: Types.RefMeshArray,
|
||||
floorPlanGroupPoint: Types.RefGroup,
|
||||
dragPointControls: Types.RefDragControl | undefined,
|
||||
uuid: Types.RefString | undefined,
|
||||
Type: Types.String
|
||||
): Types.Mesh {
|
||||
|
||||
////////// A function that creates and adds a cube (point) with an outline based on the position and colour given as params, It also updates the drag controls objects and sets the box uuid in uuid.current //////////
|
||||
|
||||
const geometry = new THREE.BoxGeometry(...CONSTANTS.pointConfig.boxScale);
|
||||
const material = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
uOuterColor: { value: new THREE.Color(colour) }, // Blue color for the border
|
||||
uInnerColor: { value: new THREE.Color(CONSTANTS.pointConfig.defaultInnerColor) }, // White color for the inner square
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
varying vec2 vUv;
|
||||
uniform vec3 uOuterColor;
|
||||
uniform vec3 uInnerColor;
|
||||
|
||||
void main() {
|
||||
// Define the size of the white square as a proportion of the face
|
||||
float borderThickness = 0.2; // Adjust this value for border thickness
|
||||
if (vUv.x > borderThickness && vUv.x < 1.0 - borderThickness &&
|
||||
vUv.y > borderThickness && vUv.y < 1.0 - borderThickness) {
|
||||
gl_FragColor = vec4(uInnerColor, 1.0); // White inner square
|
||||
} else {
|
||||
gl_FragColor = vec4(uOuterColor, 1.0); // Blue border
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
const point = new THREE.Mesh(geometry, material);
|
||||
point.name = "point";
|
||||
point.userData = { type: Type, color: colour };
|
||||
point.position.set(position.x, 0.01, position.z);
|
||||
|
||||
currentLayerPoint.current.push(point);
|
||||
floorPlanGroupPoint.current.add(point);
|
||||
if (uuid) {
|
||||
uuid.current = point.uuid;
|
||||
}
|
||||
if (dragPointControls) {
|
||||
dragPointControls.current!.objects = currentLayerPoint.current;
|
||||
}
|
||||
|
||||
return point;
|
||||
}
|
||||
|
||||
export default addPointToScene;
|
||||
62
app/src/modules/builder/geomentries/points/deletePoint.ts
Normal file
62
app/src/modules/builder/geomentries/points/deletePoint.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import RemoveConnectedLines from "../lines/removeConnectedLines";
|
||||
// import { deletePointApi } from "../../../../services/factoryBuilder/lines/deletePointApi";
|
||||
import { Socket } from "socket.io-client";
|
||||
|
||||
function deletePoint(
|
||||
hoveredDeletablePoint: Types.RefMesh,
|
||||
onlyFloorlines: Types.RefOnlyFloorLines,
|
||||
floorPlanGroupPoint: Types.RefGroup,
|
||||
floorPlanGroupLine: Types.RefGroup,
|
||||
lines: Types.RefLines,
|
||||
setDeletedLines: any,
|
||||
socket: Socket<any>,
|
||||
projectId?:string
|
||||
): void {
|
||||
////////// Deleting a Point and the lines that are connected to it //////////
|
||||
|
||||
if (!hoveredDeletablePoint.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
(<any>hoveredDeletablePoint.current.material).dispose();
|
||||
(<any>hoveredDeletablePoint.current.geometry).dispose();
|
||||
floorPlanGroupPoint.current.remove(hoveredDeletablePoint.current);
|
||||
const DeletedPointUUID = hoveredDeletablePoint.current.uuid;
|
||||
|
||||
const email = localStorage.getItem('email')
|
||||
const organization = (email!.split("@")[1]).split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
//REST
|
||||
|
||||
// deletePointApi(organization, DeletedPointUUID);
|
||||
|
||||
//SOCKET
|
||||
|
||||
const data = {
|
||||
organization: organization,
|
||||
uuid: DeletedPointUUID,
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
}
|
||||
|
||||
// console.log('data: ', data);
|
||||
socket.emit('v1:Line:delete:point', data);
|
||||
|
||||
////////// Update onlyFloorlines.current to remove references to the deleted point //////////
|
||||
|
||||
onlyFloorlines.current = onlyFloorlines.current.map(floorline =>
|
||||
floorline.filter(line => line[0][1] !== DeletedPointUUID && line[1][1] !== DeletedPointUUID)
|
||||
).filter(floorline => floorline.length > 0);
|
||||
|
||||
RemoveConnectedLines(DeletedPointUUID, floorPlanGroupLine, floorPlanGroupPoint, setDeletedLines, lines);
|
||||
|
||||
echo.success("Point Removed!");
|
||||
}
|
||||
|
||||
export default deletePoint;
|
||||
44
app/src/modules/builder/geomentries/points/dragPoint.ts
Normal file
44
app/src/modules/builder/geomentries/points/dragPoint.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as THREE from "three";
|
||||
import * as Types from "../../../../types/world/worldTypes"
|
||||
import * as CONSTANTS from '../../../../types/world/worldConstants';
|
||||
|
||||
import updateLinesPositions from "../lines/updateLinesPositions";
|
||||
import updateLines from "../lines/updateLines";
|
||||
import updateDistanceText from "../lines/updateDistanceText";
|
||||
import updateFloorLines from "../floors/updateFloorLines";
|
||||
|
||||
function DragPoint(
|
||||
event: Types.IntersectionEvent,
|
||||
floorPlanGroupPoint: Types.RefGroup,
|
||||
floorPlanGroupLine: Types.RefGroup,
|
||||
scene: THREE.Scene,
|
||||
lines: Types.RefLines,
|
||||
onlyFloorlines: Types.RefOnlyFloorLines
|
||||
): void {
|
||||
|
||||
////////// Calling the line updation of the affected lines and Snapping of the point during the drag //////////
|
||||
|
||||
const snapThreshold = CONSTANTS.pointConfig.snappingThreshold;
|
||||
const DragedPoint = event.object as Types.Mesh;
|
||||
|
||||
floorPlanGroupPoint.current.children.forEach((point) => {
|
||||
let canSnap =
|
||||
((DragedPoint.userData.type === CONSTANTS.lineConfig.wallName) && (point.userData.type === CONSTANTS.lineConfig.wallName || point.userData.type === CONSTANTS.lineConfig.floorName)) ||
|
||||
((DragedPoint.userData.type === CONSTANTS.lineConfig.floorName) && (point.userData.type === CONSTANTS.lineConfig.wallName || point.userData.type === CONSTANTS.lineConfig.floorName)) ||
|
||||
((DragedPoint.userData.type === CONSTANTS.lineConfig.aisleName) && point.userData.type === CONSTANTS.lineConfig.aisleName);
|
||||
if (canSnap && point.uuid !== DragedPoint.uuid && point.visible) {
|
||||
const distance = DragedPoint.position.distanceTo(point.position);
|
||||
if (distance < snapThreshold) {
|
||||
DragedPoint.position.copy(point.position);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const affectedLines = updateLinesPositions(DragedPoint, lines);
|
||||
|
||||
updateLines(floorPlanGroupLine, affectedLines);
|
||||
updateDistanceText(scene, floorPlanGroupLine, affectedLines);
|
||||
updateFloorLines(onlyFloorlines, DragedPoint);
|
||||
}
|
||||
|
||||
export default DragPoint;
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function removeSoloPoint(
|
||||
line: Types.RefLine,
|
||||
floorPlanGroupLine: Types.RefGroup,
|
||||
floorPlanGroupPoint: Types.RefGroup
|
||||
): void {
|
||||
|
||||
////////// Remove the point if there is only one point and if it is not connected to any other line and also the reference line //////////
|
||||
|
||||
if (line.current[0]) {
|
||||
const pointUUID = line.current[0][1];
|
||||
let isConnected = false;
|
||||
|
||||
floorPlanGroupLine.current.children.forEach((line) => {
|
||||
const linePoints = line.userData.linePoints;
|
||||
const uuid1 = linePoints[0][1];
|
||||
const uuid2 = linePoints[1][1];
|
||||
if (uuid1 === pointUUID || uuid2 === pointUUID) {
|
||||
isConnected = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!isConnected) {
|
||||
floorPlanGroupPoint.current.children.forEach((point: any) => {
|
||||
if (point.uuid === pointUUID) {
|
||||
(<any>point.material).dispose();
|
||||
(<any>point.geometry).dispose();
|
||||
floorPlanGroupPoint.current.remove(point);
|
||||
}
|
||||
});
|
||||
}
|
||||
line.current = [];
|
||||
}
|
||||
}
|
||||
|
||||
export default removeSoloPoint;
|
||||
32
app/src/modules/builder/geomentries/roofs/addRoofToScene.ts
Normal file
32
app/src/modules/builder/geomentries/roofs/addRoofToScene.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import * as THREE from 'three';
|
||||
import * as CONSTANTS from '../../../../types/world/worldConstants';
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function addRoofToScene(
|
||||
shape: Types.Shape,
|
||||
floor: Types.Number,
|
||||
userData: Types.UserData,
|
||||
floorGroup: Types.RefGroup
|
||||
): void {
|
||||
|
||||
////////// Creating a Polygon roof from the shape of the Polygon floor //////////
|
||||
|
||||
const extrudeSettings: THREE.ExtrudeGeometryOptions = {
|
||||
depth: CONSTANTS.roofConfig.height,
|
||||
bevelEnabled: false
|
||||
};
|
||||
|
||||
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
|
||||
const material = new THREE.MeshStandardMaterial({ color: CONSTANTS.roofConfig.defaultColor, side: THREE.DoubleSide });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.position.y = CONSTANTS.wallConfig.height + floor;
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
mesh.rotateX(Math.PI / 2);
|
||||
mesh.userData.uuids = userData;
|
||||
mesh.name = `Roof_Layer_${(floor / CONSTANTS.wallConfig.height) + 1}`;
|
||||
|
||||
floorGroup.current.add(mesh);
|
||||
}
|
||||
|
||||
export default addRoofToScene;
|
||||
47
app/src/modules/builder/geomentries/roofs/hideRoof.ts
Normal file
47
app/src/modules/builder/geomentries/roofs/hideRoof.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function hideRoof(
|
||||
visibility: Types.Boolean,
|
||||
floorGroup: Types.RefGroup,
|
||||
camera: THREE.Camera
|
||||
): void {
|
||||
|
||||
////////// Toggles the visibility of the roof based on the camera position and the Roof visibility button on UI //////////
|
||||
|
||||
const v = new THREE.Vector3();
|
||||
const u = new THREE.Vector3();
|
||||
|
||||
if (visibility === true && floorGroup.current) {
|
||||
for (const child of floorGroup.current.children) {
|
||||
if (child.name.includes("Roof")) {
|
||||
const roofChild = child as Types.Mesh;
|
||||
roofChild.getWorldDirection(v);
|
||||
camera?.getWorldDirection(u);
|
||||
if (roofChild.material) {
|
||||
const materials = Array.isArray(roofChild.material) ? roofChild.material : [roofChild.material];
|
||||
materials.forEach(material => {
|
||||
material.visible = v.dot(u) < 0.25;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (floorGroup.current) {
|
||||
for (const child of floorGroup.current.children) {
|
||||
if (child.name.includes("Roof")) {
|
||||
const roofChild = child as Types.Mesh;
|
||||
if (roofChild.material) {
|
||||
const materials = Array.isArray(roofChild.material) ? roofChild.material : [roofChild.material];
|
||||
materials.forEach(material => {
|
||||
material.visible = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default hideRoof;
|
||||
143
app/src/modules/builder/geomentries/walls/addWallItems.ts
Normal file
143
app/src/modules/builder/geomentries/walls/addWallItems.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
|
||||
import { toast } from 'react-toastify';
|
||||
import * as THREE from 'three';
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
import * as CONSTANTS from '../../../../types/world/worldConstants';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { retrieveGLTF, storeGLTF } from '../../../../utils/indexDB/idbUtils';
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
|
||||
|
||||
async function AddWallItems(
|
||||
selected: any,
|
||||
raycaster: THREE.Raycaster,
|
||||
CSGGroup: Types.RefMesh,
|
||||
setWallItems: Types.setWallItemSetState,
|
||||
socket: Socket<any>,
|
||||
projectId?: string
|
||||
): Promise<void> {
|
||||
let intersects = raycaster?.intersectObject(CSGGroup.current!, true);
|
||||
const wallRaycastIntersection = intersects?.find((child) => child.object.name.includes("WallRaycastReference"));
|
||||
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
|
||||
|
||||
if (!wallRaycastIntersection) return;
|
||||
|
||||
const intersectionPoint = wallRaycastIntersection;
|
||||
const loader = new GLTFLoader();
|
||||
const dracoLoader = new DRACOLoader();
|
||||
|
||||
dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/');
|
||||
loader.setDRACOLoader(dracoLoader);
|
||||
|
||||
// Check THREE.js cache first
|
||||
const cachedModel = THREE.Cache.get(selected.id);
|
||||
if (cachedModel) {
|
||||
handleModelLoad(cachedModel);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check IndexedDB cache
|
||||
const cachedModelBlob = await retrieveGLTF(selected.id);
|
||||
if (cachedModelBlob) {
|
||||
const blobUrl = URL.createObjectURL(cachedModelBlob);
|
||||
loader.load(blobUrl, (gltf) => {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
THREE.Cache.remove(blobUrl);
|
||||
THREE.Cache.add(selected.id, gltf);
|
||||
handleModelLoad(gltf);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Load from backend if not in any cache
|
||||
loader.load(`${url_Backend_dwinzo}/api/v2/AssetFile/${selected.id}`, async (gltf) => {
|
||||
try {
|
||||
const modelBlob = await fetch(`${url_Backend_dwinzo}/api/v2/AssetFile/${selected.id}`).then((res) => res.blob());
|
||||
await storeGLTF(selected.id, modelBlob);
|
||||
THREE.Cache.add(selected.id, gltf);
|
||||
await handleModelLoad(gltf);
|
||||
} catch (error) {
|
||||
console.error('Failed to cache model:', error);
|
||||
handleModelLoad(gltf);
|
||||
}
|
||||
});
|
||||
|
||||
async function handleModelLoad(gltf: GLTF) {
|
||||
const model = gltf.scene.clone();
|
||||
model.userData = { wall: intersectionPoint.object.parent };
|
||||
|
||||
model.children[0].children.forEach((child) => {
|
||||
if (child.name !== "CSG_REF") {
|
||||
child.castShadow = true;
|
||||
child.receiveShadow = true;
|
||||
}
|
||||
});
|
||||
|
||||
const boundingBox = new THREE.Box3().setFromObject(model);
|
||||
const size = new THREE.Vector3();
|
||||
boundingBox.getSize(size);
|
||||
|
||||
const csgscale = [size.x, size.y, size.z] as [number, number, number];
|
||||
|
||||
const center = new THREE.Vector3();
|
||||
boundingBox.getCenter(center);
|
||||
const csgposition = [center.x, center.y, center.z] as [number, number, number];
|
||||
|
||||
let positionY = selected.subCategory === 'fixed-move' ? 0 : intersectionPoint.point.y;
|
||||
if (positionY === 0) {
|
||||
positionY = Math.floor(intersectionPoint.point.y / CONSTANTS.wallConfig.height) * CONSTANTS.wallConfig.height;
|
||||
}
|
||||
|
||||
const newWallItem = {
|
||||
type: selected.subCategory,
|
||||
model: model,
|
||||
modelName: selected.name,
|
||||
modelfileID: selected.id,
|
||||
scale: [1, 1, 1] as [number, number, number],
|
||||
csgscale: csgscale,
|
||||
csgposition: csgposition,
|
||||
position: [intersectionPoint.point.x, positionY, intersectionPoint.point.z] as [number, number, number],
|
||||
quaternion: intersectionPoint.object.quaternion.clone() as Types.QuaternionType
|
||||
};
|
||||
|
||||
const email = localStorage.getItem('email');
|
||||
const organization = email ? (email.split("@")[1]).split(".")[0] : 'default';
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
const data = {
|
||||
organization: organization,
|
||||
modelUuid: model.uuid,
|
||||
modelName: newWallItem.modelName,
|
||||
modelfileID: selected.id,
|
||||
type: selected.subCategory,
|
||||
csgposition: newWallItem.csgposition,
|
||||
csgscale: newWallItem.csgscale,
|
||||
position: newWallItem.position,
|
||||
quaternion: newWallItem.quaternion,
|
||||
scale: newWallItem.scale,
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
};
|
||||
|
||||
// console.log('data: ', data);
|
||||
socket.emit('v1:wallItems:set', data);
|
||||
|
||||
setWallItems((prevItems) => {
|
||||
const updatedItems = [...prevItems, newWallItem];
|
||||
|
||||
const WallItemsForStorage = updatedItems.map(item => {
|
||||
const { model, ...rest } = item;
|
||||
return {
|
||||
...rest,
|
||||
modelUuid: model?.uuid,
|
||||
};
|
||||
});
|
||||
|
||||
localStorage.setItem("WallItems", JSON.stringify(WallItemsForStorage));
|
||||
echo.success("Model Added!");
|
||||
return updatedItems;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AddWallItems;
|
||||
60
app/src/modules/builder/geomentries/walls/deleteWallItems.ts
Normal file
60
app/src/modules/builder/geomentries/walls/deleteWallItems.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
// import { deleteWallItem } from '../../../../services/factoryBuilder/assest/wallAsset/deleteWallItemApi';
|
||||
import { Socket } from 'socket.io-client';
|
||||
|
||||
function DeleteWallItems(
|
||||
hoveredDeletableWallItem: Types.RefMesh,
|
||||
setWallItems: Types.setWallItemSetState,
|
||||
wallItems: Types.wallItems,
|
||||
socket: Socket<any>,
|
||||
projectId?: string
|
||||
): void {
|
||||
|
||||
////////// Deleting the hovered Wall GLTF from thewallItems and also update it in the localstorage //////////
|
||||
|
||||
if (hoveredDeletableWallItem.current && hoveredDeletableWallItem.current) {
|
||||
setWallItems([]);
|
||||
let WallItemsRef = wallItems;
|
||||
const removedItem = WallItemsRef.find((item) => item.model?.uuid === hoveredDeletableWallItem.current?.uuid);
|
||||
const Items = WallItemsRef.filter((item) => item.model?.uuid !== hoveredDeletableWallItem.current?.uuid);
|
||||
|
||||
setTimeout(async () => {
|
||||
WallItemsRef = Items;
|
||||
setWallItems(WallItemsRef);
|
||||
|
||||
const email = localStorage.getItem('email')
|
||||
const organization = (email!.split("@")[1]).split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
//REST
|
||||
|
||||
// await deleteWallItem(organization, removedItem?.model?.uuid!, removedItem?.modelName!)
|
||||
|
||||
//SOCKET
|
||||
|
||||
const data = {
|
||||
organization: organization,
|
||||
modelUuid: removedItem?.model?.uuid!,
|
||||
modelName: removedItem?.modelName!,
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
}
|
||||
|
||||
socket.emit('v1:wallItems:delete', data);
|
||||
|
||||
const WallItemsForStorage = WallItemsRef.map(item => {
|
||||
const { model, ...rest } = item;
|
||||
return {
|
||||
...rest,
|
||||
modelUuid: model?.uuid,
|
||||
};
|
||||
});
|
||||
|
||||
localStorage.setItem("WallItems", JSON.stringify(WallItemsForStorage));
|
||||
hoveredDeletableWallItem.current = null;
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
export default DeleteWallItems;
|
||||
45
app/src/modules/builder/geomentries/walls/hideWalls.ts
Normal file
45
app/src/modules/builder/geomentries/walls/hideWalls.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
function hideWalls(
|
||||
visibility: Types.Boolean,
|
||||
scene: THREE.Scene,
|
||||
camera: THREE.Camera
|
||||
): void {
|
||||
|
||||
////////// Altering the visibility of the Walls when the world direction of the wall is facing the camera //////////
|
||||
|
||||
const v = new THREE.Vector3();
|
||||
const u = new THREE.Vector3();
|
||||
|
||||
if (visibility === true) {
|
||||
for (const children of scene.children) {
|
||||
if (children.name === "Walls" && children.children[0]?.children.length > 0) {
|
||||
children.children[0].children.forEach((child: any) => {
|
||||
if (child.children[0]?.userData.WallType === "RoomWall") {
|
||||
child.children[0].getWorldDirection(v);
|
||||
camera.getWorldDirection(u);
|
||||
if (child.children[0].material) {
|
||||
child.children[0].material.visible = (2 * v.dot(u)) >= -0.5;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const children of scene.children) {
|
||||
if (children.name === "Walls" && children.children[0]?.children.length > 0) {
|
||||
children.children[0].children.forEach((child: any) => {
|
||||
if (child.children[0]?.userData.WallType === "RoomWall") {
|
||||
if (child.children[0].material) {
|
||||
child.children[0].material.visible = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default hideWalls;
|
||||
140
app/src/modules/builder/geomentries/walls/loadWalls.ts
Normal file
140
app/src/modules/builder/geomentries/walls/loadWalls.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import * as THREE from 'three';
|
||||
import * as turf from '@turf/turf';
|
||||
import * as CONSTANTS from '../../../../types/world/worldConstants';
|
||||
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
import getRoomsFromLines from '../lines/getRoomsFromLines';
|
||||
|
||||
async function loadWalls(
|
||||
lines: Types.RefLines,
|
||||
setWalls: any,
|
||||
): Promise<void> {
|
||||
////////// Removes the old walls if any, Checks if there is any overlapping in lines if any updates it , starts function that creates floor and roof //////////
|
||||
|
||||
const Walls: Types.Walls = [];
|
||||
const Rooms: Types.Rooms = [];
|
||||
|
||||
localStorage.setItem("Lines", JSON.stringify(lines.current));
|
||||
|
||||
if (lines.current.length > 1) {
|
||||
|
||||
////////// Add Walls that are forming a room //////////
|
||||
|
||||
const wallSet = new Set<string>();
|
||||
|
||||
const rooms: Types.Rooms = await getRoomsFromLines(lines);
|
||||
Rooms.push(...rooms);
|
||||
|
||||
Rooms.forEach(({ coordinates: room, layer }) => {
|
||||
for (let i = 0; i < room.length - 1; i++) {
|
||||
const uuid1 = room[i].uuid;
|
||||
const uuid2 = room[(i + 1) % room.length].uuid;
|
||||
const wallId = `${uuid1}_${uuid2}`;
|
||||
|
||||
if (!wallSet.has(wallId)) {
|
||||
const p1 = room[i].position;
|
||||
const p2 = room[(i + 1) % room.length].position;
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
shape.moveTo(0, 0);
|
||||
shape.lineTo(0, CONSTANTS.wallConfig.height);
|
||||
shape.lineTo(p2.distanceTo(p1), CONSTANTS.wallConfig.height);
|
||||
shape.lineTo(p2.distanceTo(p1), 0);
|
||||
shape.lineTo(0, 0);
|
||||
|
||||
const extrudeSettings = {
|
||||
depth: CONSTANTS.wallConfig.width,
|
||||
bevelEnabled: false
|
||||
};
|
||||
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
|
||||
const angle = Math.atan2(p2.z - p1.z, p2.x - p1.x);
|
||||
Walls.push([geometry, [0, -angle, 0], [p1.x, (layer - 1) * CONSTANTS.wallConfig.height, p1.z], "RoomWall", layer]);
|
||||
|
||||
wallSet.add(wallId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
////////// Add Walls that are not forming any room //////////
|
||||
|
||||
lines.current.forEach(line => {
|
||||
if (line[0][3] && line[1][3] !== CONSTANTS.lineConfig.wallName) {
|
||||
return;
|
||||
}
|
||||
const [uuid1, uuid2] = line.map(point => point[1]);
|
||||
let isInRoom = false;
|
||||
const lineLayer = line[0][2];
|
||||
|
||||
for (let room of Rooms) {
|
||||
const roomLayer = room.layer;
|
||||
if (roomLayer !== lineLayer) continue;
|
||||
for (let i = 0; i < room.coordinates.length - 1; i++) {
|
||||
const roomUuid1 = room.coordinates[i].uuid;
|
||||
const roomUuid2 = room.coordinates[(i + 1) % room.coordinates.length].uuid;
|
||||
if (
|
||||
(uuid1 === roomUuid1 && uuid2 === roomUuid2) ||
|
||||
(uuid1 === roomUuid2 && uuid2 === roomUuid1)
|
||||
) {
|
||||
isInRoom = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isInRoom) break;
|
||||
}
|
||||
|
||||
if (!isInRoom) {
|
||||
const p1 = new THREE.Vector3(line[0][0].x, 0, line[0][0].z);
|
||||
const p2 = new THREE.Vector3(line[1][0].x, 0, line[1][0].z);
|
||||
|
||||
let isCollinear = false;
|
||||
for (let room of Rooms) {
|
||||
if (room.layer !== lineLayer) continue;
|
||||
for (let i = 0; i < room.coordinates.length - 1; i++) {
|
||||
const roomP1 = room.coordinates[i].position;
|
||||
const roomP2 = room.coordinates[(i + 1) % room.coordinates.length].position;
|
||||
const lineFeature = turf.lineString([[p1.x, p1.z], [p2.x, p2.z]]);
|
||||
const roomFeature = turf.lineString([[roomP1.x, roomP1.z], [roomP2.x, roomP2.z]]);
|
||||
if (turf.booleanOverlap(lineFeature, roomFeature)) {
|
||||
isCollinear = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isCollinear) break;
|
||||
}
|
||||
|
||||
if (!isCollinear) {
|
||||
const shape = new THREE.Shape();
|
||||
shape.moveTo(0, 0);
|
||||
shape.lineTo(0, CONSTANTS.wallConfig.height);
|
||||
shape.lineTo(p2.distanceTo(p1), CONSTANTS.wallConfig.height);
|
||||
shape.lineTo(p2.distanceTo(p1), 0);
|
||||
shape.lineTo(0, 0);
|
||||
|
||||
const extrudeSettings = {
|
||||
depth: CONSTANTS.wallConfig.width,
|
||||
bevelEnabled: false
|
||||
};
|
||||
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
|
||||
const angle = Math.atan2(p2.z - p1.z, p2.x - p1.x);
|
||||
Walls.push([geometry, [0, -angle, 0], [p1.x, (lineLayer - 1) * CONSTANTS.wallConfig.height, p1.z], "SegmentWall", lineLayer]);
|
||||
}
|
||||
}
|
||||
});
|
||||
setWalls(Walls);
|
||||
} else {
|
||||
setWalls([]);
|
||||
}
|
||||
}
|
||||
|
||||
export default loadWalls;
|
||||
|
||||
|
||||
// A----- B----- C
|
||||
// | | |
|
||||
// | | |
|
||||
// | | |
|
||||
// F----- E----- D
|
||||
|
||||
// 1. A -> B, B -> C, C -> D, D -> E, E -> F, F -> A, B -> E
|
||||
|
||||
// 2. E -> F, F -> A, A -> B, B -> E, E -> D, D -> C, C -> B
|
||||
50
app/src/modules/builder/geomentries/zones/addZonesToScene.ts
Normal file
50
app/src/modules/builder/geomentries/zones/addZonesToScene.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as THREE from 'three';
|
||||
import * as Types from '../../../../types/world/worldTypes';
|
||||
import * as CONSTANTS from '../../../../types/world/worldConstants';
|
||||
|
||||
const baseMaterial = new THREE.ShaderMaterial({
|
||||
side: THREE.DoubleSide,
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
void main(){
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
vUv = uv;
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
varying vec2 vUv;
|
||||
uniform vec3 uOuterColor;
|
||||
void main(){
|
||||
float alpha = 1.0 - vUv.y;
|
||||
gl_FragColor = vec4(uOuterColor, alpha);
|
||||
}
|
||||
`,
|
||||
uniforms: {
|
||||
uOuterColor: { value: new THREE.Color(CONSTANTS.zoneConfig.defaultColor) },
|
||||
},
|
||||
transparent: true,
|
||||
});
|
||||
|
||||
export default function addZonesToScene(
|
||||
line: Types.Line,
|
||||
floorGroupZone: Types.RefGroup,
|
||||
color: THREE.Color
|
||||
) {
|
||||
const point1 = line[0][0];
|
||||
const point2 = line[1][0];
|
||||
|
||||
const length = (new THREE.Vector3(point2.x, point2.y, point2.z)).distanceTo(new THREE.Vector3(point1.x, point1.y, point1.z));
|
||||
const angle = Math.atan2(point2.z - point1.z, point2.x - point1.x);
|
||||
|
||||
const geometry = new THREE.PlaneGeometry(length, 10);
|
||||
|
||||
const material = baseMaterial.clone();
|
||||
material.uniforms.uOuterColor.value.set(color.r, color.g, color.b);
|
||||
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
mesh.position.set((point1.x + point2.x) / 2, ((line[0][2] - 1) * CONSTANTS.wallConfig.height) + 5, (point1.z + point2.z) / 2);
|
||||
mesh.rotation.y = -angle;
|
||||
|
||||
floorGroupZone.current.add(mesh);
|
||||
}
|
||||
19
app/src/modules/builder/geomentries/zones/loadZones.ts
Normal file
19
app/src/modules/builder/geomentries/zones/loadZones.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as Types from '../../../../types/world/worldTypes';
|
||||
import * as THREE from 'three';
|
||||
import * as CONSTANTS from '../../../../types/world/worldConstants';
|
||||
import addZonesToScene from './addZonesToScene';
|
||||
|
||||
export default function loadZones(
|
||||
lines: Types.RefLines,
|
||||
floorGroupZone: Types.RefGroup
|
||||
) {
|
||||
if (!floorGroupZone.current) return
|
||||
floorGroupZone.current.children = [];
|
||||
const zones = lines.current.filter((line) => line[0][3] && line[1][3] === CONSTANTS.lineConfig.zoneName);
|
||||
|
||||
if (zones.length > 0) {
|
||||
zones.forEach((zone: Types.Line) => {
|
||||
addZonesToScene(zone, floorGroupZone, new THREE.Color(CONSTANTS.zoneConfig.color))
|
||||
})
|
||||
}
|
||||
}
|
||||
114
app/src/modules/builder/groups/calculateAreaGroup.tsx
Normal file
114
app/src/modules/builder/groups/calculateAreaGroup.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { useRoomsState, useToggleView } from "../../../store/builder/store";
|
||||
import { computeArea } from "../functions/computeArea";
|
||||
import { Html } from "@react-three/drei";
|
||||
import * as CONSTANTS from "../../../types/world/worldConstants";
|
||||
import * as turf from "@turf/turf";
|
||||
import * as THREE from "three";
|
||||
|
||||
const CalculateAreaGroup = () => {
|
||||
const { roomsState } = useRoomsState();
|
||||
const { toggleView } = useToggleView();
|
||||
const savedTheme: string | null = localStorage.getItem("theme");
|
||||
|
||||
return (
|
||||
<group name="roomArea" visible={toggleView}>
|
||||
<group name="roomFills" visible={toggleView}>
|
||||
{roomsState.length > 0 &&
|
||||
roomsState.flat().map((room: any, index: number) => {
|
||||
const coordinates = room.coordinates;
|
||||
if (!coordinates || coordinates.length < 3) return null;
|
||||
|
||||
const coords2D = coordinates.map(
|
||||
(p: any) => new THREE.Vector2(p.position.x, p.position.z)
|
||||
);
|
||||
|
||||
if (!coords2D[0].equals(coords2D[coords2D.length - 1])) {
|
||||
coords2D.push(coords2D[0]);
|
||||
}
|
||||
|
||||
const shape = new THREE.Shape(coords2D);
|
||||
const extrudeSettings = {
|
||||
depth: 0.01,
|
||||
bevelEnabled: false,
|
||||
};
|
||||
|
||||
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
|
||||
geometry.rotateX(Math.PI / 2);
|
||||
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: savedTheme === "dark" ? "#d2baff" : "#6f42c1",
|
||||
side: THREE.DoubleSide,
|
||||
transparent: true,
|
||||
opacity: 0.4,
|
||||
depthWrite: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<group key={`roomFill-${index}`}>
|
||||
<mesh
|
||||
geometry={geometry}
|
||||
material={material}
|
||||
position={[0, 0.12, 0]}
|
||||
/>
|
||||
</group>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
{roomsState.length > 0 &&
|
||||
roomsState.flat().map((room: any, index: number) => {
|
||||
if (!toggleView) return null;
|
||||
const coordinates = room.coordinates;
|
||||
|
||||
if (!coordinates || coordinates.length < 3) return null;
|
||||
|
||||
let coords2D = coordinates.map((p: any) => [
|
||||
p.position.x,
|
||||
p.position.z,
|
||||
]);
|
||||
|
||||
const first = coords2D[0];
|
||||
const last = coords2D[coords2D.length - 1];
|
||||
if (first[0] !== last[0] || first[1] !== last[1]) {
|
||||
coords2D.push(first);
|
||||
}
|
||||
|
||||
const polygon = turf.polygon([coords2D]);
|
||||
const center2D = turf.center(polygon).geometry.coordinates;
|
||||
|
||||
const sumY = coordinates.reduce(
|
||||
(sum: number, p: any) => sum + p.position.y,
|
||||
0
|
||||
);
|
||||
const avgY = sumY / coordinates.length;
|
||||
|
||||
const area = computeArea(room, "rooms");
|
||||
const formattedArea = `${area.toFixed(2)} m²`;
|
||||
|
||||
const htmlPosition: [number, number, number] = [
|
||||
center2D[0],
|
||||
avgY + CONSTANTS.zoneConfig.height,
|
||||
center2D[1],
|
||||
];
|
||||
|
||||
return (
|
||||
<Html
|
||||
key={`${index}-${room.layer || index}`}
|
||||
position={htmlPosition}
|
||||
wrapperClass="distance-text-wrapper"
|
||||
className="distance-text"
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
center
|
||||
sprite
|
||||
>
|
||||
<div className={`distance area line-${room.layer || index}`}>
|
||||
Room ({formattedArea})
|
||||
</div>
|
||||
</Html>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
export default CalculateAreaGroup;
|
||||
127
app/src/modules/builder/groups/floorGroup.tsx
Normal file
127
app/src/modules/builder/groups/floorGroup.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import {
|
||||
useAddAction,
|
||||
useDeleteTool,
|
||||
useRoofVisibility,
|
||||
useToggleView,
|
||||
useWallVisibility,
|
||||
useUpdateScene,
|
||||
useRenameModeStore,
|
||||
} from "../../../store/builder/store";
|
||||
import hideRoof from "../geomentries/roofs/hideRoof";
|
||||
import hideWalls from "../geomentries/walls/hideWalls";
|
||||
import addAndUpdateReferencePillar from "../geomentries/pillars/addAndUpdateReferencePillar";
|
||||
import { useEffect } from "react";
|
||||
import addPillar from "../geomentries/pillars/addPillar";
|
||||
import DeletePillar from "../geomentries/pillars/deletePillar";
|
||||
import DeletableHoveredPillar from "../geomentries/pillars/deletableHoveredPillar";
|
||||
import loadFloor from "../geomentries/floors/loadFloor";
|
||||
import { useLeftData, useTopData } from "../../../store/visualization/useZone3DWidgetStore";
|
||||
|
||||
const FloorGroup = ({
|
||||
floorGroup,
|
||||
lines,
|
||||
referencePole,
|
||||
hoveredDeletablePillar,
|
||||
}: any) => {
|
||||
const state = useThree();
|
||||
const { roofVisibility } = useRoofVisibility();
|
||||
const { wallVisibility } = useWallVisibility();
|
||||
const { toggleView } = useToggleView();
|
||||
const { scene, camera, raycaster, gl } = useThree();
|
||||
const { addAction } = useAddAction();
|
||||
const { deleteTool } = useDeleteTool();
|
||||
const { updateScene, setUpdateScene } = useUpdateScene();
|
||||
const { setTop } = useTopData();
|
||||
const { setLeft } = useLeftData();
|
||||
const { isRenameMode, setIsRenameMode } = useRenameModeStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (updateScene) {
|
||||
loadFloor(lines, floorGroup);
|
||||
setUpdateScene(false);
|
||||
}
|
||||
}, [updateScene]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!addAction) {
|
||||
if (referencePole.current) {
|
||||
(referencePole.current as any).material.dispose();
|
||||
(referencePole.current.geometry as any).dispose();
|
||||
floorGroup.current.remove(referencePole.current);
|
||||
referencePole.current = undefined;
|
||||
}
|
||||
}
|
||||
}, [addAction]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvasElement = gl.domElement;
|
||||
let drag = false;
|
||||
let isLeftMouseDown = false;
|
||||
|
||||
const onMouseDown = (evt: any) => {
|
||||
|
||||
if (evt.button === 0) {
|
||||
isLeftMouseDown = true;
|
||||
drag = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUp = (evt: any) => {
|
||||
setIsRenameMode(false);
|
||||
if (evt.button === 0) {
|
||||
isLeftMouseDown = false;
|
||||
if (!drag) {
|
||||
if (addAction === "pillar") {
|
||||
addPillar(referencePole, floorGroup);
|
||||
}
|
||||
if (deleteTool) {
|
||||
DeletePillar(hoveredDeletablePillar, floorGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseMove = (evt: any) => {
|
||||
if (!canvasElement) return;
|
||||
const canvasRect = canvasElement.getBoundingClientRect();
|
||||
const relativeX = evt.clientX - canvasRect.left;
|
||||
const relativeY = evt.clientY - canvasRect.top;
|
||||
if (!isRenameMode) {
|
||||
setTop(relativeY);
|
||||
setLeft(relativeX);
|
||||
}
|
||||
if (isLeftMouseDown) {
|
||||
drag = true;
|
||||
}
|
||||
};
|
||||
|
||||
canvasElement.addEventListener("mousedown", onMouseDown);
|
||||
canvasElement.addEventListener("mouseup", onMouseUp);
|
||||
canvasElement.addEventListener("mousemove", onMouseMove);
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("mousedown", onMouseDown);
|
||||
canvasElement.removeEventListener("mouseup", onMouseUp);
|
||||
canvasElement.removeEventListener("mousemove", onMouseMove);
|
||||
};
|
||||
}, [deleteTool, addAction, isRenameMode]);
|
||||
|
||||
useFrame(() => {
|
||||
hideRoof(roofVisibility, floorGroup, camera);
|
||||
hideWalls(wallVisibility, scene, camera);
|
||||
|
||||
if (addAction === "pillar") {
|
||||
addAndUpdateReferencePillar(raycaster, floorGroup, referencePole);
|
||||
}
|
||||
if (deleteTool) {
|
||||
DeletableHoveredPillar(state, floorGroup, hoveredDeletablePillar);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<group ref={floorGroup} visible={!toggleView} name="floorGroup"></group>
|
||||
);
|
||||
};
|
||||
|
||||
export default FloorGroup;
|
||||
204
app/src/modules/builder/groups/floorPlanGroup.tsx
Normal file
204
app/src/modules/builder/groups/floorPlanGroup.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import { useEffect } from "react";
|
||||
import * as Types from '../../../types/world/worldTypes';
|
||||
import { useActiveLayer, useDeletedLines, useDeletePointOrLine, useToolMode, useNewLines, useRemovedLayer, useSocketStore, useToggleView, useUpdateScene } from "../../../store/builder/store";
|
||||
import Layer2DVisibility from "../geomentries/layers/layer2DVisibility";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import DeletableLineorPoint from "../functions/deletableLineOrPoint";
|
||||
import removeSoloPoint from "../geomentries/points/removeSoloPoint";
|
||||
import removeReferenceLine from "../geomentries/lines/removeReferenceLine";
|
||||
import DeleteLayer from "../geomentries/layers/deleteLayer";
|
||||
import { getLines } from "../../../services/factoryBuilder/lines/getLinesApi";
|
||||
import objectLinesToArray from "../geomentries/lines/lineConvertions/objectLinesToArray";
|
||||
import loadInitialPoint from "../IntialLoad/loadInitialPoint";
|
||||
import loadInitialLine from "../IntialLoad/loadInitialLine";
|
||||
import deletePoint from "../geomentries/points/deletePoint";
|
||||
import deleteLine from "../geomentries/lines/deleteLine";
|
||||
import drawWall from "../geomentries/lines/drawWall";
|
||||
import drawOnlyFloor from "../geomentries/floors/drawOnlyFloor";
|
||||
import addDragControl from "../eventDeclaration/dragControlDeclaration";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
const FloorPlanGroup = ({ floorPlanGroup, floorPlanGroupLine, floorPlanGroupPoint, floorGroup, currentLayerPoint, dragPointControls, hoveredDeletablePoint, hoveredDeletableLine, plane, line, lines, onlyFloorline, onlyFloorlines, ReferenceLineMesh, LineCreated, isSnapped, ispreSnapped, snappedPoint, isSnappedUUID, isAngleSnapped, anglesnappedPoint }: any) => {
|
||||
const state = useThree();
|
||||
const { camera, gl, raycaster, controls } = state;
|
||||
const { activeLayer } = useActiveLayer();
|
||||
const { toggleView } = useToggleView();
|
||||
const { deletePointOrLine, setDeletePointOrLine } = useDeletePointOrLine();
|
||||
const { toolMode, setToolMode } = useToolMode();
|
||||
const { removedLayer, setRemovedLayer } = useRemovedLayer();
|
||||
const { setUpdateScene } = useUpdateScene();
|
||||
const { setNewLines } = useNewLines();
|
||||
const { setDeletedLines } = useDeletedLines();
|
||||
const { socket } = useSocketStore();
|
||||
const { projectId } = useParams();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (toolMode === 'move') {
|
||||
addDragControl(dragPointControls, currentLayerPoint, state, floorPlanGroupPoint, floorPlanGroupLine, lines, onlyFloorlines, socket, projectId);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (dragPointControls.current) {
|
||||
dragPointControls.current.enabled = false;
|
||||
}
|
||||
};
|
||||
}, [toolMode, state]);
|
||||
|
||||
useEffect(() => {
|
||||
const email = localStorage.getItem('email')
|
||||
const organization = (email!.split("@")[1]).split(".")[0];
|
||||
|
||||
// Load data from localStorage if available
|
||||
getLines(organization, projectId).then((data) => {
|
||||
// console.log('data: ', data);
|
||||
|
||||
const Lines: Types.Lines = objectLinesToArray(data);
|
||||
|
||||
// const data = localStorage.getItem("Lines");
|
||||
|
||||
if (Lines) {
|
||||
lines.current = Lines;
|
||||
loadInitialPoint(lines, floorPlanGroupPoint, currentLayerPoint, dragPointControls);
|
||||
loadInitialLine(floorPlanGroupLine, lines);
|
||||
setUpdateScene(true);
|
||||
}
|
||||
})
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!toggleView) {
|
||||
removeSoloPoint(line, floorPlanGroupLine, floorPlanGroupPoint);
|
||||
removeReferenceLine(floorPlanGroup, ReferenceLineMesh, LineCreated, line);
|
||||
}
|
||||
}, [toggleView]);
|
||||
|
||||
useEffect(() => {
|
||||
if (toolMode === "Wall" || toolMode === "Floor") {
|
||||
setDeletePointOrLine(false);
|
||||
} else {
|
||||
removeSoloPoint(line, floorPlanGroupLine, floorPlanGroupPoint);
|
||||
removeReferenceLine(floorPlanGroup, ReferenceLineMesh, LineCreated, line);
|
||||
}
|
||||
}, [toolMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (toolMode === 'move' && toggleView) {
|
||||
setDeletePointOrLine(false);
|
||||
if (dragPointControls.current) {
|
||||
dragPointControls.current.enabled = true;
|
||||
}
|
||||
} else {
|
||||
if (dragPointControls.current) {
|
||||
dragPointControls.current.enabled = false;
|
||||
}
|
||||
}
|
||||
}, [toolMode, toggleView, state]);
|
||||
|
||||
useEffect(() => {
|
||||
if (deletePointOrLine) {
|
||||
setToolMode(null);
|
||||
}
|
||||
}, [deletePointOrLine]);
|
||||
|
||||
useEffect(() => {
|
||||
Layer2DVisibility(activeLayer, floorPlanGroup, floorPlanGroupLine, floorPlanGroupPoint, currentLayerPoint, dragPointControls);
|
||||
}, [activeLayer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (removedLayer !== null) {
|
||||
DeleteLayer(removedLayer, lines, floorPlanGroupLine, floorPlanGroupPoint, onlyFloorlines, floorGroup, setDeletedLines, setRemovedLayer, socket, projectId);
|
||||
}
|
||||
}, [removedLayer]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const canvasElement = gl.domElement;
|
||||
|
||||
let drag = false;
|
||||
let isLeftMouseDown = false;
|
||||
|
||||
const onMouseDown = (evt: any) => {
|
||||
if (evt.button === 0) {
|
||||
isLeftMouseDown = true;
|
||||
drag = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUp = (evt: any) => {
|
||||
if (evt.button === 0) {
|
||||
isLeftMouseDown = false;
|
||||
}
|
||||
if (controls) {
|
||||
(controls as any).enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseMove = () => {
|
||||
if (isLeftMouseDown) {
|
||||
drag = true;
|
||||
}
|
||||
};
|
||||
|
||||
const onContextMenu = (e: any) => {
|
||||
e.preventDefault();
|
||||
if (toolMode === "Wall" || toolMode === "Floor") {
|
||||
removeSoloPoint(line, floorPlanGroupLine, floorPlanGroupPoint);
|
||||
removeReferenceLine(floorPlanGroup, ReferenceLineMesh, LineCreated, line);
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseClick = (evt: any) => {
|
||||
if (!plane.current || drag) return;
|
||||
|
||||
if (deletePointOrLine) {
|
||||
if (hoveredDeletablePoint.current !== null) {
|
||||
deletePoint(hoveredDeletablePoint, onlyFloorlines, floorPlanGroupPoint, floorPlanGroupLine, lines, setDeletedLines, socket, projectId);
|
||||
}
|
||||
if (hoveredDeletableLine.current !== null) {
|
||||
deleteLine(hoveredDeletableLine, onlyFloorlines, lines, floorPlanGroupLine, floorPlanGroupPoint, setDeletedLines, socket, projectId);
|
||||
}
|
||||
}
|
||||
|
||||
if (toolMode === "Wall") {
|
||||
drawWall(raycaster, plane, floorPlanGroupPoint, snappedPoint, isSnapped, isSnappedUUID, line, ispreSnapped, anglesnappedPoint, isAngleSnapped, lines, floorPlanGroupLine, floorPlanGroup, ReferenceLineMesh, LineCreated, currentLayerPoint, dragPointControls, setNewLines, setDeletedLines, activeLayer, socket, projectId);
|
||||
}
|
||||
|
||||
if (toolMode === "Floor") {
|
||||
drawOnlyFloor(raycaster, state, camera, plane, floorPlanGroupPoint, snappedPoint, isSnapped, isSnappedUUID, line, ispreSnapped, anglesnappedPoint, isAngleSnapped, onlyFloorline, onlyFloorlines, lines, floorPlanGroupLine, floorPlanGroup, ReferenceLineMesh, LineCreated, currentLayerPoint, dragPointControls, setNewLines, setDeletedLines, activeLayer, socket, projectId);
|
||||
}
|
||||
}
|
||||
|
||||
if (deletePointOrLine || toolMode === "Wall" || toolMode === "Floor") {
|
||||
canvasElement.addEventListener("mousedown", onMouseDown);
|
||||
canvasElement.addEventListener("mouseup", onMouseUp);
|
||||
canvasElement.addEventListener("mousemove", onMouseMove);
|
||||
canvasElement.addEventListener("click", onMouseClick);
|
||||
canvasElement.addEventListener("contextmenu", onContextMenu);
|
||||
}
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("mousedown", onMouseDown);
|
||||
canvasElement.removeEventListener("mouseup", onMouseUp);
|
||||
canvasElement.removeEventListener("mousemove", onMouseMove);
|
||||
canvasElement.removeEventListener("click", onMouseClick);
|
||||
canvasElement.removeEventListener("contextmenu", onContextMenu);
|
||||
};
|
||||
}, [deletePointOrLine, toolMode, activeLayer])
|
||||
|
||||
|
||||
useFrame(() => {
|
||||
if (deletePointOrLine) {
|
||||
DeletableLineorPoint(state, plane, floorPlanGroupLine, floorPlanGroupPoint, hoveredDeletableLine, hoveredDeletablePoint);
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<group ref={floorPlanGroup} visible={toggleView} name="floorPlanGroup">
|
||||
<group ref={floorPlanGroupLine} name="floorPlanGroupLine"></group>
|
||||
<group ref={floorPlanGroupPoint} name="floorPlanGroupPoint"></group>
|
||||
</group>
|
||||
)
|
||||
}
|
||||
|
||||
export default FloorPlanGroup;
|
||||
286
app/src/modules/builder/groups/wallItemsGroup.tsx
Normal file
286
app/src/modules/builder/groups/wallItemsGroup.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
useDeleteTool,
|
||||
useDeletePointOrLine,
|
||||
useObjectPosition,
|
||||
useObjectRotation,
|
||||
useSelectedWallItem,
|
||||
useSocketStore,
|
||||
useWallItems,
|
||||
useSelectedItem,
|
||||
} from "../../../store/builder/store";
|
||||
import { Csg } from "../csg/csg";
|
||||
import * as Types from "../../../types/world/worldTypes";
|
||||
import * as CONSTANTS from "../../../types/world/worldConstants";
|
||||
import * as THREE from "three";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import handleMeshMissed from "../eventFunctions/handleMeshMissed";
|
||||
import DeleteWallItems from "../geomentries/walls/deleteWallItems";
|
||||
import loadInitialWallItems from "../IntialLoad/loadInitialWallItems";
|
||||
import AddWallItems from "../geomentries/walls/addWallItems";
|
||||
import useModuleStore from "../../../store/useModuleStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
const WallItemsGroup = ({
|
||||
currentWallItem,
|
||||
hoveredDeletableWallItem,
|
||||
selectedItemsIndex,
|
||||
setSelectedItemsIndex,
|
||||
CSGGroup,
|
||||
}: any) => {
|
||||
const state = useThree();
|
||||
const { socket } = useSocketStore();
|
||||
const { pointer, camera, raycaster } = state;
|
||||
const { deleteTool } = useDeleteTool();
|
||||
const { wallItems, setWallItems } = useWallItems();
|
||||
const { setObjectPosition } = useObjectPosition();
|
||||
const { setObjectRotation } = useObjectRotation();
|
||||
const { deletePointOrLine } = useDeletePointOrLine();
|
||||
const { setSelectedWallItem } = useSelectedWallItem();
|
||||
const { activeModule } = useModuleStore();
|
||||
const { selectedItem } = useSelectedItem();
|
||||
const { projectId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
// Load Wall Items from the backend
|
||||
loadInitialWallItems(setWallItems,projectId);
|
||||
}, []);
|
||||
|
||||
////////// Update the Position value changes in the selected item //////////
|
||||
|
||||
useEffect(() => {
|
||||
const canvasElement = state.gl.domElement;
|
||||
function handlePointerMove(e: any) {
|
||||
if (selectedItemsIndex !== null && !deletePointOrLine && e.buttons === 1) {
|
||||
const Raycaster = state.raycaster;
|
||||
const intersects = Raycaster.intersectObjects(CSGGroup.current?.children[0].children!, true);
|
||||
const Object = intersects.find((child) => child.object.name.includes("WallRaycastReference"));
|
||||
|
||||
if (Object) {
|
||||
(state.controls as any)!.enabled = false;
|
||||
setWallItems((prevItems: any) => {
|
||||
const updatedItems = [...prevItems];
|
||||
let position: [number, number, number] = [0, 0, 0];
|
||||
|
||||
if (updatedItems[selectedItemsIndex].type === "fixed-move") {
|
||||
position = [
|
||||
Object!.point.x,
|
||||
Math.floor(Object!.point.y / CONSTANTS.wallConfig.height) *
|
||||
CONSTANTS.wallConfig.height,
|
||||
Object!.point.z,
|
||||
];
|
||||
} else if (updatedItems[selectedItemsIndex].type === "free-move") {
|
||||
position = [Object!.point.x, Object!.point.y, Object!.point.z];
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
setObjectPosition(new THREE.Vector3(...position));
|
||||
setObjectRotation({
|
||||
x: THREE.MathUtils.radToDeg(Object!.object.rotation.x),
|
||||
y: THREE.MathUtils.radToDeg(Object!.object.rotation.y),
|
||||
z: THREE.MathUtils.radToDeg(Object!.object.rotation.z),
|
||||
});
|
||||
});
|
||||
|
||||
updatedItems[selectedItemsIndex] = {
|
||||
...updatedItems[selectedItemsIndex],
|
||||
position: position,
|
||||
quaternion: Object!.object.quaternion.clone() as Types.QuaternionType,
|
||||
};
|
||||
|
||||
return updatedItems;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePointerUp() {
|
||||
const Raycaster = state.raycaster;
|
||||
const intersects = Raycaster.intersectObjects(
|
||||
CSGGroup.current?.children[0].children!,
|
||||
true
|
||||
);
|
||||
const Object = intersects.find((child) =>
|
||||
child.object.name.includes("WallRaycastReference")
|
||||
);
|
||||
if (Object) {
|
||||
if (selectedItemsIndex !== null) {
|
||||
let currentItem: any = null;
|
||||
setWallItems((prevItems: any) => {
|
||||
const updatedItems = [...prevItems];
|
||||
const WallItemsForStorage = updatedItems.map((item) => {
|
||||
const { model, ...rest } = item;
|
||||
return {
|
||||
...rest,
|
||||
modelUuid: model?.uuid,
|
||||
};
|
||||
});
|
||||
|
||||
currentItem = updatedItems[selectedItemsIndex];
|
||||
localStorage.setItem("WallItems", JSON.stringify(WallItemsForStorage));
|
||||
return updatedItems;
|
||||
});
|
||||
|
||||
setTimeout(async () => {
|
||||
const email = localStorage.getItem("email");
|
||||
const organization = email!.split("@")[1].split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
//REST
|
||||
|
||||
// await setWallItem(
|
||||
// organization,
|
||||
// currentItem?.model?.uuid,
|
||||
// currentItem.modelName,
|
||||
// currentItem.modelfileID,
|
||||
// currentItem.type!,
|
||||
// currentItem.csgposition!,
|
||||
// currentItem.csgscale!,
|
||||
// currentItem.position,
|
||||
// currentItem.quaternion,
|
||||
// currentItem.scale!,
|
||||
// )
|
||||
|
||||
//SOCKET
|
||||
|
||||
const data = {
|
||||
organization: organization,
|
||||
modelUuid: currentItem.model?.uuid!,
|
||||
modelfileID: currentItem.modelfileID,
|
||||
modelName: currentItem.modelName!,
|
||||
type: currentItem.type!,
|
||||
csgposition: currentItem.csgposition!,
|
||||
csgscale: currentItem.csgscale!,
|
||||
position: currentItem.position!,
|
||||
quaternion: currentItem.quaternion,
|
||||
scale: currentItem.scale!,
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
};
|
||||
|
||||
// console.log('data: ', data);
|
||||
socket.emit("v1:wallItems:set", data);
|
||||
}, 0);
|
||||
(state.controls as any)!.enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
canvasElement.addEventListener("pointermove", handlePointerMove);
|
||||
canvasElement.addEventListener("pointerup", handlePointerUp);
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("pointermove", handlePointerMove);
|
||||
canvasElement.removeEventListener("pointerup", handlePointerUp);
|
||||
};
|
||||
}, [selectedItemsIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvasElement = state.gl.domElement;
|
||||
let drag = false;
|
||||
let isLeftMouseDown = false;
|
||||
|
||||
const onMouseDown = (evt: any) => {
|
||||
if (evt.button === 0) {
|
||||
isLeftMouseDown = true;
|
||||
drag = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUp = (evt: any) => {
|
||||
if (evt.button === 0) {
|
||||
isLeftMouseDown = false;
|
||||
if (!drag && deleteTool && activeModule === "builder") {
|
||||
DeleteWallItems(
|
||||
hoveredDeletableWallItem,
|
||||
setWallItems,
|
||||
wallItems,
|
||||
socket,projectId
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseMove = () => {
|
||||
if (isLeftMouseDown) {
|
||||
drag = true;
|
||||
}
|
||||
};
|
||||
|
||||
const onDrop = (event: any) => {
|
||||
if (selectedItem.category !== 'Fenestration') return;
|
||||
|
||||
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
|
||||
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
||||
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
|
||||
if (selectedItem.id) {
|
||||
if (selectedItem.subCategory) {
|
||||
AddWallItems(
|
||||
selectedItem,
|
||||
raycaster,
|
||||
CSGGroup,
|
||||
setWallItems,
|
||||
socket,
|
||||
projectId
|
||||
);
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const onDragOver = (event: any) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
canvasElement.addEventListener("mousedown", onMouseDown);
|
||||
canvasElement.addEventListener("mouseup", onMouseUp);
|
||||
canvasElement.addEventListener("mousemove", onMouseMove);
|
||||
canvasElement.addEventListener("drop", onDrop);
|
||||
canvasElement.addEventListener("dragover", onDragOver);
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("mousedown", onMouseDown);
|
||||
canvasElement.removeEventListener("mouseup", onMouseUp);
|
||||
canvasElement.removeEventListener("mousemove", onMouseMove);
|
||||
canvasElement.removeEventListener("drop", onDrop);
|
||||
canvasElement.removeEventListener("dragover", onDragOver);
|
||||
};
|
||||
}, [deleteTool, wallItems, selectedItem, camera]);
|
||||
|
||||
useEffect(() => {
|
||||
if (deleteTool && activeModule === "builder") {
|
||||
handleMeshMissed(
|
||||
currentWallItem,
|
||||
setSelectedWallItem,
|
||||
setSelectedItemsIndex
|
||||
);
|
||||
setSelectedWallItem(null);
|
||||
setSelectedItemsIndex(null);
|
||||
}
|
||||
}, [deleteTool]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{wallItems.map((item: Types.WallItem, index: number) => (
|
||||
<group
|
||||
key={index}
|
||||
position={item.position}
|
||||
quaternion={item.quaternion}
|
||||
scale={item.scale}
|
||||
>
|
||||
<Csg
|
||||
position={item.csgposition!}
|
||||
scale={item.csgscale!}
|
||||
model={item.model!}
|
||||
hoveredDeletableWallItem={hoveredDeletableWallItem}
|
||||
/>
|
||||
</group>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WallItemsGroup;
|
||||
74
app/src/modules/builder/groups/wallsAndWallItems.tsx
Normal file
74
app/src/modules/builder/groups/wallsAndWallItems.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Geometry } from "@react-three/csg";
|
||||
import {
|
||||
useDeleteTool,
|
||||
useSelectedWallItem,
|
||||
useToggleView,
|
||||
useWallItems,
|
||||
useWalls,
|
||||
} from "../../../store/builder/store";
|
||||
import handleMeshDown from "../eventFunctions/handleMeshDown";
|
||||
import handleMeshMissed from "../eventFunctions/handleMeshMissed";
|
||||
import WallsMesh from "./wallsMesh";
|
||||
import WallItemsGroup from "./wallItemsGroup";
|
||||
|
||||
const WallsAndWallItems = ({
|
||||
CSGGroup,
|
||||
setSelectedItemsIndex,
|
||||
selectedItemsIndex,
|
||||
currentWallItem,
|
||||
csg,
|
||||
lines,
|
||||
hoveredDeletableWallItem,
|
||||
}: any) => {
|
||||
const { walls } = useWalls();
|
||||
const { wallItems } = useWallItems();
|
||||
const { toggleView } = useToggleView();
|
||||
const { deleteTool } = useDeleteTool();
|
||||
const { setSelectedWallItem } = useSelectedWallItem();
|
||||
|
||||
return (
|
||||
<mesh
|
||||
ref={CSGGroup as any}
|
||||
name="Walls"
|
||||
key={walls.length}
|
||||
receiveShadow
|
||||
visible={!toggleView}
|
||||
onClick={(event) => {
|
||||
if (!deleteTool) {
|
||||
handleMeshDown(
|
||||
event,
|
||||
currentWallItem,
|
||||
setSelectedWallItem,
|
||||
setSelectedItemsIndex,
|
||||
wallItems,
|
||||
toggleView
|
||||
);
|
||||
}
|
||||
}}
|
||||
onPointerMissed={() => {
|
||||
if (!deleteTool) {
|
||||
handleMeshMissed(
|
||||
currentWallItem,
|
||||
setSelectedWallItem,
|
||||
setSelectedItemsIndex
|
||||
);
|
||||
setSelectedWallItem(null);
|
||||
setSelectedItemsIndex(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Geometry ref={csg as any} computeVertexNormals useGroups>
|
||||
<WallsMesh lines={lines} />
|
||||
<WallItemsGroup
|
||||
currentWallItem={currentWallItem}
|
||||
hoveredDeletableWallItem={hoveredDeletableWallItem}
|
||||
selectedItemsIndex={selectedItemsIndex}
|
||||
setSelectedItemsIndex={setSelectedItemsIndex}
|
||||
CSGGroup={CSGGroup}
|
||||
/>
|
||||
</Geometry>
|
||||
</mesh>
|
||||
);
|
||||
};
|
||||
|
||||
export default WallsAndWallItems;
|
||||
76
app/src/modules/builder/groups/wallsMesh.tsx
Normal file
76
app/src/modules/builder/groups/wallsMesh.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import * as THREE from "three";
|
||||
import * as Types from "../../../types/world/worldTypes";
|
||||
import * as CONSTANTS from "../../../types/world/worldConstants";
|
||||
import { Base } from "@react-three/csg";
|
||||
import { MeshDiscardMaterial } from "@react-three/drei";
|
||||
import { useUpdateScene, useWalls } from "../../../store/builder/store";
|
||||
import React, { useEffect } from "react";
|
||||
import { getLines } from "../../../services/factoryBuilder/lines/getLinesApi";
|
||||
import objectLinesToArray from "../geomentries/lines/lineConvertions/objectLinesToArray";
|
||||
import loadWalls from "../geomentries/walls/loadWalls";
|
||||
import texturePath from "../../../assets/textures/floor/wall-tex.png";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
const WallsMeshComponent = ({ lines }: any) => {
|
||||
const { walls, setWalls } = useWalls();
|
||||
const { updateScene, setUpdateScene } = useUpdateScene();
|
||||
const { projectId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (updateScene) {
|
||||
const email = localStorage.getItem("email");
|
||||
const organization = email!.split("@")[1].split(".")[0];
|
||||
|
||||
getLines(organization,projectId).then((data) => {
|
||||
const Lines: Types.Lines = objectLinesToArray(data);
|
||||
localStorage.setItem("Lines", JSON.stringify(Lines));
|
||||
|
||||
if (Lines) {
|
||||
loadWalls(lines, setWalls);
|
||||
}
|
||||
});
|
||||
setUpdateScene(false);
|
||||
}
|
||||
}, [updateScene]);
|
||||
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
const wallTexture = textureLoader.load(texturePath);
|
||||
|
||||
wallTexture.wrapS = wallTexture.wrapT = THREE.RepeatWrapping;
|
||||
wallTexture.repeat.set(0.1, 0.1);
|
||||
wallTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
|
||||
return (
|
||||
<>
|
||||
{walls.map((wall: Types.Wall, index: number) => (
|
||||
<mesh key={index} renderOrder={1}>
|
||||
<Base
|
||||
name={`Wall${index + 1}`}
|
||||
geometry={wall[0]}
|
||||
rotation={wall[1]}
|
||||
position={wall[2]}
|
||||
userData={{ WallType: wall[3], Layer: wall[4] }}
|
||||
>
|
||||
<meshStandardMaterial
|
||||
side={THREE.DoubleSide}
|
||||
color={CONSTANTS.wallConfig.defaultColor}
|
||||
map={wallTexture}
|
||||
/>
|
||||
</Base>
|
||||
<mesh
|
||||
castShadow
|
||||
geometry={wall[0]}
|
||||
rotation={wall[1]}
|
||||
position={wall[2]}
|
||||
name={`WallRaycastReference_${index + 1}`}
|
||||
>
|
||||
<MeshDiscardMaterial />
|
||||
</mesh>
|
||||
</mesh>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const WallsMesh = React.memo(WallsMeshComponent);
|
||||
export default WallsMesh;
|
||||
742
app/src/modules/builder/groups/zoneGroup.tsx
Normal file
742
app/src/modules/builder/groups/zoneGroup.tsx
Normal file
@@ -0,0 +1,742 @@
|
||||
import React, { useState, useEffect, useMemo, useRef } from "react";
|
||||
import { Html, Line, Sphere } from "@react-three/drei";
|
||||
import { useThree, useFrame } from "@react-three/fiber";
|
||||
import * as THREE from "three";
|
||||
import {
|
||||
useActiveLayer,
|
||||
useDeleteTool,
|
||||
useDeletePointOrLine,
|
||||
useSocketStore,
|
||||
useToggleView,
|
||||
useToolMode,
|
||||
useRemovedLayer,
|
||||
useZones,
|
||||
useZonePoints,
|
||||
} from "../../../store/builder/store";
|
||||
import { getZonesApi } from "../../../services/factoryBuilder/zones/getZonesApi";
|
||||
|
||||
import * as CONSTANTS from "../../../types/world/worldConstants";
|
||||
import * as turf from "@turf/turf";
|
||||
import { computeArea } from "../functions/computeArea";
|
||||
import { useSelectedZoneStore } from "../../../store/visualization/useZoneStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
const ZoneGroup: React.FC = () => {
|
||||
const { camera, pointer, gl, raycaster, scene, controls } = useThree();
|
||||
const [startPoint, setStartPoint] = useState<THREE.Vector3 | null>(null);
|
||||
const [endPoint, setEndPoint] = useState<THREE.Vector3 | null>(null);
|
||||
const { zones, setZones } = useZones();
|
||||
const { zonePoints, setZonePoints } = useZonePoints();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const { selectedZone } = useSelectedZoneStore();
|
||||
const [draggedSphere, setDraggedSphere] = useState<THREE.Vector3 | null>(
|
||||
null
|
||||
);
|
||||
const plane = useMemo(
|
||||
() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0),
|
||||
[]
|
||||
);
|
||||
const { toggleView } = useToggleView();
|
||||
const { deletePointOrLine, setDeletePointOrLine } = useDeletePointOrLine();
|
||||
const { removedLayer, setRemovedLayer } = useRemovedLayer();
|
||||
const { toolMode } = useToolMode();
|
||||
const { setDeleteTool } = useDeleteTool();
|
||||
const { activeLayer } = useActiveLayer();
|
||||
const { socket } = useSocketStore();
|
||||
const { projectId } = useParams();
|
||||
|
||||
const groupsRef = useRef<any>();
|
||||
|
||||
const zoneMaterial = useMemo(
|
||||
() =>
|
||||
new THREE.ShaderMaterial({
|
||||
side: THREE.DoubleSide,
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
void main(){
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
vUv = uv;
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
varying vec2 vUv;
|
||||
uniform vec3 uOuterColor;
|
||||
void main(){
|
||||
float alpha = 1.0 - vUv.y;
|
||||
gl_FragColor = vec4(uOuterColor, alpha);
|
||||
}
|
||||
`,
|
||||
uniforms: {
|
||||
uOuterColor: { value: new THREE.Color(CONSTANTS.zoneConfig.color) },
|
||||
},
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchZones = async () => {
|
||||
const email = localStorage.getItem("email");
|
||||
if (!email) return;
|
||||
|
||||
const organization = email.split("@")[1].split(".")[0];
|
||||
const data = await getZonesApi(organization, projectId);
|
||||
// console.log('data: ', data);
|
||||
|
||||
if (data.length > 0) {
|
||||
const fetchedZones = data.map((zone: any) => ({
|
||||
zoneUuid: zone.zoneUuid,
|
||||
zoneName: zone.zoneName,
|
||||
points: zone.points,
|
||||
viewPortCenter: zone.viewPortCenter,
|
||||
viewPortposition: zone.viewPortposition,
|
||||
layer: zone.layer,
|
||||
}));
|
||||
|
||||
setZones(fetchedZones);
|
||||
|
||||
const fetchedPoints = data.flatMap((zone: any) =>
|
||||
zone.points
|
||||
.slice(0, 4)
|
||||
.map(
|
||||
(point: [number, number, number]) => new THREE.Vector3(...point)
|
||||
)
|
||||
);
|
||||
|
||||
setZonePoints(fetchedPoints);
|
||||
}
|
||||
};
|
||||
|
||||
fetchZones();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("zones", JSON.stringify(zones));
|
||||
}, [zones]);
|
||||
|
||||
useEffect(() => {
|
||||
if (removedLayer) {
|
||||
const updatedZones = zones.filter(
|
||||
(zone: any) => zone.layer !== removedLayer
|
||||
);
|
||||
setZones(updatedZones);
|
||||
|
||||
const updatedzonePoints = zonePoints.filter((_: any, index: any) => {
|
||||
const zoneIndex = Math.floor(index / 4);
|
||||
return zones[zoneIndex]?.layer !== removedLayer;
|
||||
});
|
||||
setZonePoints(updatedzonePoints);
|
||||
|
||||
zones
|
||||
.filter((zone: any) => zone.layer === removedLayer)
|
||||
.forEach((zone: any) => {
|
||||
deleteZoneFromBackend(zone.zoneUuid);
|
||||
});
|
||||
|
||||
setRemovedLayer(null);
|
||||
}
|
||||
}, [removedLayer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (toolMode !== "Zone") {
|
||||
setStartPoint(null);
|
||||
setEndPoint(null);
|
||||
} else {
|
||||
setDeletePointOrLine(false);
|
||||
setDeleteTool(false);
|
||||
}
|
||||
if (!toggleView) {
|
||||
setStartPoint(null);
|
||||
setEndPoint(null);
|
||||
}
|
||||
}, [toolMode, toggleView]);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const addZoneToBackend = async (zone: {
|
||||
zoneUuid: string;
|
||||
zoneName: string;
|
||||
points: [number, number, number][];
|
||||
layer: string;
|
||||
}) => {
|
||||
console.log('zoneUuid: ', zone);
|
||||
const email = localStorage.getItem("email");
|
||||
const userId = localStorage.getItem("userId");
|
||||
const organization = email!.split("@")[1].split(".")[0];
|
||||
|
||||
const calculateCenter = (points: number[][]) => {
|
||||
if (!points || points.length === 0) return null;
|
||||
|
||||
let sumX = 0,
|
||||
sumY = 0,
|
||||
sumZ = 0;
|
||||
const numPoints = points.length;
|
||||
|
||||
points.forEach(([x, y, z]) => {
|
||||
sumX += x;
|
||||
sumY += y;
|
||||
sumZ += z;
|
||||
});
|
||||
|
||||
return [sumX / numPoints, sumY / numPoints, sumZ / numPoints] as [
|
||||
number,
|
||||
number,
|
||||
number
|
||||
];
|
||||
};
|
||||
|
||||
const target: [number, number, number] | null = calculateCenter(
|
||||
zone.points
|
||||
);
|
||||
if (!target || zone.points.length < 4) return;
|
||||
const position = [target[0], 10, target[2]];
|
||||
|
||||
const input = {
|
||||
userId: userId,
|
||||
projectId,
|
||||
organization: organization,
|
||||
zoneData: {
|
||||
zoneName: zone.zoneName,
|
||||
zoneUuid: zone.zoneUuid,
|
||||
points: zone.points,
|
||||
viewPortCenter: target,
|
||||
viewPortposition: position,
|
||||
layer: zone.layer,
|
||||
},
|
||||
};
|
||||
|
||||
socket.emit("v1:zone:set", input);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const updateZoneToBackend = async (zone: {
|
||||
zoneUuid: string;
|
||||
zoneName: string;
|
||||
points: [number, number, number][];
|
||||
layer: string;
|
||||
}) => {
|
||||
const email = localStorage.getItem("email");
|
||||
const userId = localStorage.getItem("userId");
|
||||
const organization = email!.split("@")[1].split(".")[0];
|
||||
|
||||
const calculateCenter = (points: number[][]) => {
|
||||
if (!points || points.length === 0) return null;
|
||||
|
||||
let sumX = 0,
|
||||
sumY = 0,
|
||||
sumZ = 0;
|
||||
const numPoints = points.length;
|
||||
|
||||
points.forEach(([x, y, z]) => {
|
||||
sumX += x;
|
||||
sumY += y;
|
||||
sumZ += z;
|
||||
});
|
||||
|
||||
return [sumX / numPoints, sumY / numPoints, sumZ / numPoints] as [
|
||||
number,
|
||||
number,
|
||||
number
|
||||
];
|
||||
};
|
||||
|
||||
const target: [number, number, number] | null = calculateCenter(
|
||||
zone.points
|
||||
);
|
||||
if (!target || zone.points.length < 4) return;
|
||||
const position = [target[0], 10, target[2]];
|
||||
|
||||
const input = {
|
||||
userId: userId,
|
||||
projectId,
|
||||
organization: organization,
|
||||
zoneData: {
|
||||
zoneName: zone.zoneName,
|
||||
zoneUuid: zone.zoneUuid,
|
||||
points: zone.points,
|
||||
viewPortCenter: target,
|
||||
viewPortposition: position,
|
||||
layer: zone.layer,
|
||||
},
|
||||
};
|
||||
|
||||
socket.emit("v1:zone:set", input);
|
||||
};
|
||||
|
||||
const deleteZoneFromBackend = async (zoneUuid: string) => {
|
||||
const email = localStorage.getItem("email");
|
||||
const userId = localStorage.getItem("userId");
|
||||
const organization = email!.split("@")[1].split(".")[0];
|
||||
|
||||
const input = {
|
||||
userId: userId,
|
||||
projectId,
|
||||
organization: organization,
|
||||
zoneUuid: zoneUuid,
|
||||
};
|
||||
|
||||
socket.emit("v1:zone:delete", input);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const handleDeleteZone = (zoneUuid: string) => {
|
||||
const updatedZones = zones.filter((zone: any) => zone.zoneUuid !== zoneUuid);
|
||||
setZones(updatedZones);
|
||||
|
||||
const zoneIndex = zones.findIndex((zone: any) => zone.zoneUuid === zoneUuid);
|
||||
if (zoneIndex !== -1) {
|
||||
const zonePointsToRemove = zonePoints.slice(
|
||||
zoneIndex * 4,
|
||||
zoneIndex * 4 + 4
|
||||
);
|
||||
zonePointsToRemove.forEach((point: any) =>
|
||||
groupsRef.current.remove(point)
|
||||
);
|
||||
const updatedzonePoints = zonePoints.filter(
|
||||
(_: any, index: any) =>
|
||||
index < zoneIndex * 4 || index >= zoneIndex * 4 + 4
|
||||
);
|
||||
setZonePoints(updatedzonePoints);
|
||||
}
|
||||
deleteZoneFromBackend(zoneUuid);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!camera || !toggleView) return;
|
||||
const canvasElement = gl.domElement;
|
||||
|
||||
let drag = false;
|
||||
let isLeftMouseDown = false;
|
||||
|
||||
const onMouseDown = (evt: any) => {
|
||||
if (evt.button === 0) {
|
||||
isLeftMouseDown = true;
|
||||
drag = false;
|
||||
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersects = raycaster.intersectObjects(
|
||||
groupsRef.current.children,
|
||||
true
|
||||
);
|
||||
|
||||
if (intersects.length > 0 && toolMode === "move") {
|
||||
const clickedObject = intersects[0].object;
|
||||
const sphereIndex = zonePoints.findIndex((point: any) =>
|
||||
point.equals(clickedObject.position)
|
||||
);
|
||||
if (sphereIndex !== -1) {
|
||||
(controls as any).enabled = false;
|
||||
setDraggedSphere(zonePoints[sphereIndex]);
|
||||
setIsDragging(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUp = (evt: any) => {
|
||||
if (evt.button === 0 && !drag && !isDragging && !deletePointOrLine) {
|
||||
isLeftMouseDown = false;
|
||||
|
||||
if (!startPoint && toolMode !== "move") {
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
const point = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
if (point) {
|
||||
setStartPoint(point);
|
||||
setEndPoint(null);
|
||||
}
|
||||
} else if (startPoint && toolMode !== "move") {
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
const point = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
if (!point) return;
|
||||
|
||||
const points = [
|
||||
[startPoint.x, 0.15, startPoint.z],
|
||||
[point.x, 0.15, startPoint.z],
|
||||
[point.x, 0.15, point.z],
|
||||
[startPoint.x, 0.15, point.z],
|
||||
[startPoint.x, 0.15, startPoint.z],
|
||||
] as [number, number, number][];
|
||||
|
||||
const zoneName = `Zone ${zones.length + 1}`;
|
||||
const zoneUuid = THREE.MathUtils.generateUUID();
|
||||
const newZone = {
|
||||
zoneUuid,
|
||||
zoneName,
|
||||
points: points,
|
||||
layer: activeLayer,
|
||||
};
|
||||
|
||||
const newZones = [...zones, newZone];
|
||||
|
||||
setZones(newZones);
|
||||
|
||||
const newzonePoints = [
|
||||
new THREE.Vector3(startPoint.x, 0.15, startPoint.z),
|
||||
new THREE.Vector3(point.x, 0.15, startPoint.z),
|
||||
new THREE.Vector3(point.x, 0.15, point.z),
|
||||
new THREE.Vector3(startPoint.x, 0.15, point.z),
|
||||
];
|
||||
|
||||
const updatedZonePoints = [...zonePoints, ...newzonePoints];
|
||||
setZonePoints(updatedZonePoints);
|
||||
|
||||
addZoneToBackend(newZone);
|
||||
setStartPoint(null);
|
||||
setEndPoint(null);
|
||||
}
|
||||
} else if (
|
||||
evt.button === 0 &&
|
||||
!drag &&
|
||||
!isDragging &&
|
||||
deletePointOrLine
|
||||
) {
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersects = raycaster.intersectObjects(
|
||||
groupsRef.current.children,
|
||||
true
|
||||
);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const clickedObject = intersects[0].object;
|
||||
|
||||
const sphereIndex = zonePoints.findIndex((point: any) =>
|
||||
point.equals(clickedObject.position)
|
||||
);
|
||||
if (sphereIndex !== -1) {
|
||||
const zoneIndex = Math.floor(sphereIndex / 4);
|
||||
const zoneUuid = zones[zoneIndex].zoneUuid;
|
||||
handleDeleteZone(zoneUuid);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (evt.button === 0) {
|
||||
if (isDragging && draggedSphere) {
|
||||
setIsDragging(false);
|
||||
setDraggedSphere(null);
|
||||
|
||||
const sphereIndex = zonePoints.findIndex(
|
||||
(point: any) => point === draggedSphere
|
||||
);
|
||||
if (sphereIndex !== -1) {
|
||||
const zoneIndex = Math.floor(sphereIndex / 4);
|
||||
|
||||
if (zoneIndex !== -1 && zones[zoneIndex]) {
|
||||
updateZoneToBackend(zones[zoneIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseMove = () => {
|
||||
if (!groupsRef.current) return;
|
||||
if (isLeftMouseDown) {
|
||||
drag = true;
|
||||
}
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersects = raycaster.intersectObjects(
|
||||
groupsRef.current.children,
|
||||
true
|
||||
);
|
||||
|
||||
if (
|
||||
intersects.length > 0 &&
|
||||
intersects[0].object.name.includes("point")
|
||||
) {
|
||||
gl.domElement.style.cursor =
|
||||
toolMode === "move" ? "pointer" : "default";
|
||||
} else {
|
||||
gl.domElement.style.cursor = "default";
|
||||
}
|
||||
if (isDragging && draggedSphere) {
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
const point = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
if (point) {
|
||||
draggedSphere.set(point.x, 0.15, point.z);
|
||||
|
||||
const sphereIndex = zonePoints.findIndex(
|
||||
(point: any) => point === draggedSphere
|
||||
);
|
||||
if (sphereIndex !== -1) {
|
||||
const zoneIndex = Math.floor(sphereIndex / 4);
|
||||
const cornerIndex = sphereIndex % 4;
|
||||
|
||||
const updatedZones = zones.map((zone: any, index: number) => {
|
||||
if (index === zoneIndex) {
|
||||
const updatedPoints = [...zone.points];
|
||||
updatedPoints[cornerIndex] = [point.x, 0.15, point.z];
|
||||
updatedPoints[4] = updatedPoints[0];
|
||||
return { ...zone, points: updatedPoints };
|
||||
}
|
||||
return zone;
|
||||
});
|
||||
|
||||
setZones(updatedZones);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onContext = (event: any) => {
|
||||
event.preventDefault();
|
||||
setStartPoint(null);
|
||||
setEndPoint(null);
|
||||
};
|
||||
|
||||
if (toolMode === "Zone" || deletePointOrLine || toolMode === "move") {
|
||||
canvasElement.addEventListener("mousedown", onMouseDown);
|
||||
canvasElement.addEventListener("mouseup", onMouseUp);
|
||||
canvasElement.addEventListener("mousemove", onMouseMove);
|
||||
canvasElement.addEventListener("contextmenu", onContext);
|
||||
}
|
||||
return () => {
|
||||
canvasElement.removeEventListener("mousedown", onMouseDown);
|
||||
canvasElement.removeEventListener("mouseup", onMouseUp);
|
||||
canvasElement.removeEventListener("mousemove", onMouseMove);
|
||||
canvasElement.removeEventListener("contextmenu", onContext);
|
||||
};
|
||||
}, [
|
||||
gl,
|
||||
camera,
|
||||
startPoint,
|
||||
toggleView,
|
||||
scene,
|
||||
toolMode,
|
||||
zones,
|
||||
isDragging,
|
||||
deletePointOrLine,
|
||||
zonePoints,
|
||||
draggedSphere,
|
||||
activeLayer,
|
||||
raycaster,
|
||||
pointer,
|
||||
controls,
|
||||
plane,
|
||||
setZones,
|
||||
setZonePoints,
|
||||
addZoneToBackend,
|
||||
handleDeleteZone,
|
||||
updateZoneToBackend,
|
||||
]);
|
||||
|
||||
useFrame(() => {
|
||||
if (!startPoint) return;
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
const point = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
if (point) {
|
||||
setEndPoint(point);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<group ref={groupsRef} name="zoneGroup">
|
||||
<group name="zones" visible={!toggleView}>
|
||||
{zones.map((zone: any) => (
|
||||
<group
|
||||
key={zone.zoneUuid}
|
||||
name={zone.zoneName}
|
||||
visible={zone.zoneUuid === selectedZone.zoneUuid}
|
||||
>
|
||||
{zone.points
|
||||
.slice(0, -1)
|
||||
.map((point: [number, number, number], index: number) => {
|
||||
const nextPoint = zone.points[index + 1];
|
||||
|
||||
const point1 = new THREE.Vector3(point[0], point[1], point[2]);
|
||||
const point2 = new THREE.Vector3(
|
||||
nextPoint[0],
|
||||
nextPoint[1],
|
||||
nextPoint[2]
|
||||
);
|
||||
|
||||
const planeWidth = point1.distanceTo(point2);
|
||||
const planeHeight = CONSTANTS.zoneConfig.height;
|
||||
|
||||
const midpoint = new THREE.Vector3(
|
||||
(point1.x + point2.x) / 2,
|
||||
CONSTANTS.zoneConfig.height / 2 +
|
||||
(zone.layer - 1) * CONSTANTS.zoneConfig.height,
|
||||
(point1.z + point2.z) / 2
|
||||
);
|
||||
|
||||
const angle = Math.atan2(
|
||||
point2.z - point1.z,
|
||||
point2.x - point1.x
|
||||
);
|
||||
|
||||
return (
|
||||
<mesh
|
||||
key={index}
|
||||
position={midpoint}
|
||||
rotation={[0, -angle, 0]}
|
||||
>
|
||||
<planeGeometry args={[planeWidth, planeHeight]} />
|
||||
<primitive
|
||||
object={zoneMaterial.clone()}
|
||||
attach="material"
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
})}
|
||||
{!toggleView &&
|
||||
(() => {
|
||||
const points3D = zone.points || [];
|
||||
const coords2D = points3D.map((p: any) => [p[0], p[2]]);
|
||||
|
||||
// Ensure the polygon is closed
|
||||
if (
|
||||
coords2D.length >= 4 &&
|
||||
(coords2D[0][0] !== coords2D[coords2D.length - 1][0] ||
|
||||
coords2D[0][1] !== coords2D[coords2D.length - 1][1])
|
||||
) {
|
||||
coords2D.push(coords2D[0]);
|
||||
}
|
||||
if (coords2D.length < 4) return null;
|
||||
|
||||
const polygon = turf.polygon([coords2D]);
|
||||
const center2D = turf.center(polygon).geometry.coordinates;
|
||||
|
||||
// Calculate the average Y value
|
||||
const sumY = points3D.reduce(
|
||||
(sum: number, p: any) => sum + p[1],
|
||||
0
|
||||
);
|
||||
const avgY = points3D.length > 0 ? sumY / points3D.length : 0;
|
||||
|
||||
const htmlPosition: [number, number, number] = [
|
||||
center2D[0],
|
||||
avgY + (CONSTANTS.zoneConfig.height || 0) + 1.5,
|
||||
center2D[1],
|
||||
];
|
||||
|
||||
return (
|
||||
<Html
|
||||
// data
|
||||
key={zone.zoneUuid}
|
||||
position={htmlPosition}
|
||||
// class
|
||||
className="zone-name-wrapper"
|
||||
// others
|
||||
center
|
||||
>
|
||||
<div className="zone-name">{zone.zoneName}</div>
|
||||
</Html>
|
||||
);
|
||||
})()}
|
||||
</group>
|
||||
))}
|
||||
</group>
|
||||
<group name="zoneLines" visible={toggleView}>
|
||||
{zones
|
||||
.filter((zone: any) => zone.layer === activeLayer)
|
||||
.map((zone: any) => (
|
||||
<Line
|
||||
key={zone.zoneUuid}
|
||||
points={zone.points}
|
||||
color="#007BFF"
|
||||
lineWidth={3}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (deletePointOrLine) {
|
||||
handleDeleteZone(zone.zoneUuid);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</group>
|
||||
<group name="zoneArea" visible={toggleView}>
|
||||
{zones.map((zone: any, index: any) => {
|
||||
if (!toggleView) return null;
|
||||
const points3D = zone.points;
|
||||
const coords2D = points3D.map((p: any) => [p[0], p[2]]);
|
||||
|
||||
if (
|
||||
coords2D.length < 4 ||
|
||||
coords2D[0][0] !== coords2D[coords2D.length - 1][0] ||
|
||||
coords2D[0][1] !== coords2D[coords2D.length - 1][1]
|
||||
) {
|
||||
coords2D.push(coords2D[0]);
|
||||
}
|
||||
if (coords2D.length < 4) return null;
|
||||
|
||||
const polygon = turf.polygon([coords2D]);
|
||||
const center2D = turf.center(polygon).geometry.coordinates;
|
||||
|
||||
const sumY = points3D.reduce((sum: number, p: any) => sum + p[1], 0);
|
||||
const avgY = sumY / points3D.length;
|
||||
|
||||
const area = computeArea(points3D, "zone");
|
||||
const formattedArea = `${area.toFixed(2)} m²`;
|
||||
|
||||
const htmlPosition: [number, number, number] = [
|
||||
center2D[0],
|
||||
avgY + CONSTANTS.zoneConfig.height,
|
||||
center2D[1],
|
||||
];
|
||||
return (
|
||||
<Html
|
||||
// data
|
||||
key={`${index}-${zone}`}
|
||||
position={htmlPosition}
|
||||
// class
|
||||
wrapperClass="distance-text-wrapper"
|
||||
className="distance-text"
|
||||
// other
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
center
|
||||
sprite
|
||||
>
|
||||
<div
|
||||
className={`distance area line-${zone}`}
|
||||
key={`${index}-${zone}`}
|
||||
>
|
||||
{zone.zoneName} ({formattedArea})
|
||||
</div>
|
||||
</Html>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
|
||||
<group name="zonePoints" visible={toggleView}>
|
||||
{zones
|
||||
.filter((zone: any) => zone.layer === activeLayer)
|
||||
.flatMap((zone: any) =>
|
||||
zone.points.slice(0, 4).map((point: any, pointIndex: number) => (
|
||||
<Sphere
|
||||
key={`${zone.zoneUuid}-point-${pointIndex}`}
|
||||
position={new THREE.Vector3(...point)}
|
||||
args={[0.3, 16, 16]}
|
||||
name={`point-${zone.zoneUuid}-${pointIndex}`}
|
||||
>
|
||||
<meshBasicMaterial color="red" />
|
||||
</Sphere>
|
||||
))
|
||||
)}
|
||||
</group>
|
||||
<group name="tempGroup" visible={toggleView}>
|
||||
{startPoint && endPoint && (
|
||||
<Line
|
||||
points={[
|
||||
[startPoint.x, 0.15, startPoint.z],
|
||||
[endPoint.x, 0.15, startPoint.z],
|
||||
[endPoint.x, 0.15, endPoint.z],
|
||||
[startPoint.x, 0.15, endPoint.z],
|
||||
[startPoint.x, 0.15, startPoint.z],
|
||||
]}
|
||||
color="#C164FF"
|
||||
lineWidth={3}
|
||||
/>
|
||||
)}
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
export default ZoneGroup;
|
||||
34
app/src/modules/builder/layout/layoutImage.tsx
Normal file
34
app/src/modules/builder/layout/layoutImage.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useMemo } from 'react';
|
||||
import layout1 from '../../../assets/gltf-glb/layouts/floorplane_1.glb';
|
||||
import layout2 from '../../../assets/gltf-glb/layouts/floorplane_2.glb';
|
||||
import useLayoutStore from '../../../store/builder/uselayoutStore';
|
||||
import { useGLTF } from '@react-three/drei';
|
||||
import { useToggleView } from '../../../store/builder/store';
|
||||
|
||||
const modelPaths: Record<string, string> = {
|
||||
'layout1': layout1,
|
||||
'layout2': layout2
|
||||
};
|
||||
|
||||
function LayoutModel() {
|
||||
const { toggleView } = useToggleView();
|
||||
const { currentLayout } = useLayoutStore();
|
||||
|
||||
// Always call the hook, but ensure it has a valid fallback
|
||||
const path = currentLayout ? modelPaths[currentLayout] : modelPaths.layout1;
|
||||
|
||||
// Explicitly cast the return type to ensure it's not an array
|
||||
const gltf = useGLTF(path) as any;
|
||||
|
||||
const cloned = useMemo(() => gltf.scene.clone(), [gltf]);
|
||||
|
||||
if (!currentLayout || !toggleView) return null;
|
||||
|
||||
return (
|
||||
<group position={[0, 0, 0]} dispose={null}>
|
||||
<primitive object={cloned} />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutModel;
|
||||
59
app/src/modules/builder/line/line.tsx
Normal file
59
app/src/modules/builder/line/line.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as THREE from 'three';
|
||||
import { useMemo } from "react";
|
||||
import * as Constants from '../../../types/world/worldConstants';
|
||||
import { Tube } from '@react-three/drei';
|
||||
|
||||
interface LineProps {
|
||||
points: [Point, Point];
|
||||
}
|
||||
|
||||
function Line({ points }: Readonly<LineProps>) {
|
||||
const path = useMemo(() => {
|
||||
const [start, end] = points.map(p => new THREE.Vector3(...p.position));
|
||||
return new THREE.LineCurve3(start, end);
|
||||
}, [points]);
|
||||
|
||||
const colors = getColor(points[0]);
|
||||
|
||||
function getColor(point: Point) {
|
||||
if (point.pointType === 'Aisle') {
|
||||
return {
|
||||
defaultLineColor: Constants.lineConfig.aisleColor,
|
||||
defaultDeleteColor: Constants.lineConfig.deleteColor,
|
||||
}
|
||||
} else if (point.pointType === 'Floor') {
|
||||
return {
|
||||
defaultLineColor: Constants.lineConfig.floorColor,
|
||||
defaultDeleteColor: Constants.lineConfig.deleteColor,
|
||||
}
|
||||
} else if (point.pointType === 'Wall') {
|
||||
return {
|
||||
defaultLineColor: Constants.lineConfig.wallColor,
|
||||
defaultDeleteColor: Constants.lineConfig.deleteColor,
|
||||
}
|
||||
} else if (point.pointType === 'Zone') {
|
||||
return {
|
||||
defaultLineColor: Constants.lineConfig.zoneColor,
|
||||
defaultDeleteColor: Constants.lineConfig.deleteColor,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
defaultLineColor: Constants.lineConfig.defaultColor,
|
||||
defaultDeleteColor: Constants.lineConfig.deleteColor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tube
|
||||
key={`${points[0].pointUuid}-${points[1].pointUuid}`}
|
||||
uuid={`${points[0].pointUuid}-${points[1].pointUuid}`}
|
||||
userData={points}
|
||||
args={[path, Constants.lineConfig.tubularSegments, Constants.lineConfig.radius, Constants.lineConfig.radialSegments, false]}
|
||||
>
|
||||
<meshStandardMaterial color={colors.defaultLineColor} />
|
||||
</Tube>
|
||||
);
|
||||
}
|
||||
|
||||
export default Line;
|
||||
25
app/src/modules/builder/line/reference/referenceLine.tsx
Normal file
25
app/src/modules/builder/line/reference/referenceLine.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as THREE from 'three';
|
||||
import { useMemo } from "react";
|
||||
import * as Constants from '../../../../types/world/worldConstants';
|
||||
import { Tube } from '@react-three/drei';
|
||||
|
||||
interface ReferenceLineProps {
|
||||
points: [Point, Point];
|
||||
}
|
||||
|
||||
function ReferenceLine({ points }: Readonly<ReferenceLineProps>) {
|
||||
const path = useMemo(() => {
|
||||
const [start, end] = points.map(p => new THREE.Vector3(...p.position));
|
||||
return new THREE.LineCurve3(start, end);
|
||||
}, [points]);
|
||||
|
||||
return (
|
||||
<Tube
|
||||
args={[path, Constants.lineConfig.tubularSegments, Constants.lineConfig.radius, Constants.lineConfig.radialSegments, false]}
|
||||
>
|
||||
<meshStandardMaterial color={Constants.lineConfig.helperColor} />
|
||||
</Tube>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReferenceLine;
|
||||
74
app/src/modules/builder/point/helpers/useAisleDragSnap.tsx
Normal file
74
app/src/modules/builder/point/helpers/useAisleDragSnap.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { useCallback } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { useAisleStore } from '../../../../store/builder/useAisleStore';
|
||||
|
||||
const SNAP_DISTANCE_THRESHOLD = 0.5; // Distance threshold for snapping in meters
|
||||
|
||||
const CAN_SNAP = true; // Whether snapping is enabled or not
|
||||
|
||||
export function useAislePointSnapping(point: Point) {
|
||||
const { getConnectedPoints } = useAisleStore();
|
||||
|
||||
const snapPosition = useCallback((newPosition: [number, number, number]): {
|
||||
position: [number, number, number],
|
||||
isSnapped: boolean,
|
||||
snapSources: THREE.Vector3[]
|
||||
} => {
|
||||
if (!CAN_SNAP) return { position: newPosition, isSnapped: false, snapSources: [] };
|
||||
|
||||
const connectedPoints = getConnectedPoints(point.pointUuid);
|
||||
if (connectedPoints.length === 0) {
|
||||
return {
|
||||
position: newPosition,
|
||||
isSnapped: false,
|
||||
snapSources: []
|
||||
};
|
||||
}
|
||||
|
||||
const newPos = new THREE.Vector3(...newPosition);
|
||||
let closestX: { pos: THREE.Vector3, dist: number } | null = null;
|
||||
let closestZ: { pos: THREE.Vector3, dist: number } | null = null;
|
||||
|
||||
for (const connectedPoint of connectedPoints) {
|
||||
const cPos = new THREE.Vector3(...connectedPoint.position);
|
||||
|
||||
const xDist = Math.abs(newPos.x - cPos.x);
|
||||
const zDist = Math.abs(newPos.z - cPos.z);
|
||||
|
||||
if (xDist < SNAP_DISTANCE_THRESHOLD) {
|
||||
if (!closestX || xDist < closestX.dist) {
|
||||
closestX = { pos: cPos, dist: xDist };
|
||||
}
|
||||
}
|
||||
|
||||
if (zDist < SNAP_DISTANCE_THRESHOLD) {
|
||||
if (!closestZ || zDist < closestZ.dist) {
|
||||
closestZ = { pos: cPos, dist: zDist };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const snappedPos = newPos.clone();
|
||||
const snapSources: THREE.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
|
||||
};
|
||||
}, [point.pointUuid, getConnectedPoints]);
|
||||
|
||||
return { snapPosition };
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useMemo } from 'react';
|
||||
import * as THREE from 'three';
|
||||
|
||||
const SNAP_ANGLE_THRESHOLD = 15; // Degrees within which to snap to orthogonal direction
|
||||
|
||||
const SNAP_DISTANCE_THRESHOLD = 0.5; // Minimum distance to consider for directional snap
|
||||
|
||||
const CAN_SNAP = true; // Whether snapping is enabled or not
|
||||
|
||||
export const useDirectionalSnapping = (
|
||||
currentPoint: [number, number, number] | null,
|
||||
previousPoint: [number, number, number] | null
|
||||
): { position: [number, number, number], isSnapped: boolean } => {
|
||||
return useMemo(() => {
|
||||
if (!currentPoint || !previousPoint || !CAN_SNAP) return { position: currentPoint || [0, 0, 0], isSnapped: false }; // No snapping if no points
|
||||
|
||||
const currentVec = new THREE.Vector2(currentPoint[0], currentPoint[2]);
|
||||
const previousVec = new THREE.Vector2(previousPoint[0], previousPoint[2]);
|
||||
const directionVec = new THREE.Vector2().subVectors(currentVec, previousVec);
|
||||
|
||||
const angle = THREE.MathUtils.radToDeg(directionVec.angle());
|
||||
const normalizedAngle = (angle + 360) % 360;
|
||||
const closestAngle = Math.round(normalizedAngle / 90) * 90 % 360;
|
||||
|
||||
const angleDiff = Math.abs(normalizedAngle - closestAngle);
|
||||
const shortestDiff = Math.min(angleDiff, 360 - angleDiff);
|
||||
|
||||
if (shortestDiff <= SNAP_ANGLE_THRESHOLD) {
|
||||
const distance = directionVec.length();
|
||||
const snappedDirection = new THREE.Vector2(
|
||||
Math.cos(THREE.MathUtils.degToRad(closestAngle)),
|
||||
Math.sin(THREE.MathUtils.degToRad(closestAngle))
|
||||
).multiplyScalar(distance);
|
||||
|
||||
const newPosition = new THREE.Vector2(previousPoint[0] + snappedDirection.x, previousPoint[2] + snappedDirection.y)
|
||||
|
||||
if (newPosition.distanceTo(currentVec) > SNAP_DISTANCE_THRESHOLD) {
|
||||
return { position: currentPoint, isSnapped: false };
|
||||
}
|
||||
|
||||
return {
|
||||
position: [
|
||||
previousPoint[0] + snappedDirection.x,
|
||||
currentPoint[1],
|
||||
previousPoint[2] + snappedDirection.y
|
||||
],
|
||||
isSnapped: true
|
||||
};
|
||||
}
|
||||
|
||||
return { position: currentPoint, isSnapped: false };
|
||||
}, [currentPoint, previousPoint]);
|
||||
};
|
||||
66
app/src/modules/builder/point/helpers/usePointSnapping.tsx
Normal file
66
app/src/modules/builder/point/helpers/usePointSnapping.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useAisleStore } from '../../../../store/builder/useAisleStore';
|
||||
import * as THREE from 'three';
|
||||
import { useWallStore } from '../../../../store/builder/useWallStore';
|
||||
|
||||
const SNAP_THRESHOLD = 0.5; // Distance threshold for snapping in meters
|
||||
|
||||
const CAN_SNAP = true; // Whether snapping is enabled or not
|
||||
|
||||
export const usePointSnapping = (currentPoint: { uuid: string, pointType: string, position: [number, number, number] } | null) => {
|
||||
const { aisles } = useAisleStore();
|
||||
const { walls } = useWallStore();
|
||||
|
||||
const getAllOtherAislePoints = useCallback(() => {
|
||||
if (!currentPoint) return [];
|
||||
|
||||
return aisles.flatMap(aisle =>
|
||||
aisle.points.filter(point => point.pointUuid !== currentPoint.uuid)
|
||||
);
|
||||
}, [aisles, currentPoint]);
|
||||
|
||||
const getAllOtherWallPoints = useCallback(() => {
|
||||
if (!currentPoint) return [];
|
||||
return walls.flatMap(wall =>
|
||||
wall.points.filter(point => point.pointUuid !== currentPoint.uuid)
|
||||
);
|
||||
}, [walls, currentPoint]);
|
||||
|
||||
const checkSnapForAisle = useCallback((position: [number, number, number]) => {
|
||||
if (!currentPoint || !CAN_SNAP) return { position: position, isSnapped: false, snappedPoint: null };
|
||||
|
||||
const otherPoints = getAllOtherAislePoints();
|
||||
const currentVec = new THREE.Vector3(...position);
|
||||
|
||||
for (const point of otherPoints) {
|
||||
const pointVec = new THREE.Vector3(...point.position);
|
||||
const distance = currentVec.distanceTo(pointVec);
|
||||
|
||||
if (distance <= SNAP_THRESHOLD && currentPoint.pointType === 'Aisle') {
|
||||
return { position: point.position, isSnapped: true, snappedPoint: point };
|
||||
}
|
||||
}
|
||||
|
||||
return { position: position, isSnapped: false, snappedPoint: null };
|
||||
}, [currentPoint, getAllOtherAislePoints]);
|
||||
|
||||
const checkSnapForWall = useCallback((position: [number, number, number]) => {
|
||||
if (!currentPoint || !CAN_SNAP) return { position: position, isSnapped: false, snappedPoint: null };
|
||||
|
||||
const otherPoints = getAllOtherWallPoints();
|
||||
const currentVec = new THREE.Vector3(...position);
|
||||
for (const point of otherPoints) {
|
||||
const pointVec = new THREE.Vector3(...point.position);
|
||||
const distance = currentVec.distanceTo(pointVec);
|
||||
if (distance <= SNAP_THRESHOLD && currentPoint.pointType === 'Wall') {
|
||||
return { position: point.position, isSnapped: true, snappedPoint: point };
|
||||
}
|
||||
}
|
||||
return { position: position, isSnapped: false, snappedPoint: null };
|
||||
}, [currentPoint, getAllOtherWallPoints]);
|
||||
|
||||
return {
|
||||
checkSnapForAisle,
|
||||
checkSnapForWall,
|
||||
};
|
||||
};
|
||||
227
app/src/modules/builder/point/point.tsx
Normal file
227
app/src/modules/builder/point/point.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import * as THREE from 'three';
|
||||
import * as Constants from '../../../types/world/worldConstants';
|
||||
import { useRef, useState, useEffect, useMemo } from 'react';
|
||||
import { useDeletePointOrLine, useToolMode } from '../../../store/builder/store';
|
||||
import { DragControls } from '@react-three/drei';
|
||||
import { useAisleStore } from '../../../store/builder/useAisleStore';
|
||||
import { useThree } from '@react-three/fiber';
|
||||
import { useBuilderStore } from '../../../store/builder/useBuilderStore';
|
||||
import { usePointSnapping } from './helpers/usePointSnapping';
|
||||
import { useAislePointSnapping } from './helpers/useAisleDragSnap';
|
||||
import { useWallStore } from '../../../store/builder/useWallStore';
|
||||
import { deleteAisleApi } from '../../../services/factoryBuilder/aisle/deleteAisleApi';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { createAisleApi } from '../../../services/factoryBuilder/aisle/createAisleApi';
|
||||
|
||||
function Point({ point }: { readonly point: Point }) {
|
||||
const materialRef = useRef<THREE.ShaderMaterial>(null);
|
||||
const { raycaster, camera, pointer } = useThree();
|
||||
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const { toolMode } = useToolMode();
|
||||
const { setPosition: setAislePosition, removePoint: removeAislePoint, getAislesByPointId } = useAisleStore();
|
||||
const { setPosition: setWallPosition, removePoint: removeWallPoint } = useWallStore();
|
||||
const { snapPosition } = useAislePointSnapping(point);
|
||||
const { checkSnapForAisle } = usePointSnapping({ uuid: point.pointUuid, pointType: point.pointType, position: point.position });
|
||||
const { hoveredPoint, setHoveredPoint } = useBuilderStore();
|
||||
const { deletePointOrLine } = useDeletePointOrLine();
|
||||
const { projectId } = useParams();
|
||||
const boxScale: [number, number, number] = Constants.pointConfig.boxScale;
|
||||
const colors = getColor(point);
|
||||
|
||||
function getColor(point: Point) {
|
||||
if (point.pointType === 'Aisle') {
|
||||
return {
|
||||
defaultInnerColor: Constants.pointConfig.defaultInnerColor,
|
||||
defaultOuterColor: Constants.pointConfig.aisleOuterColor,
|
||||
defaultDeleteColor: Constants.pointConfig.deleteColor,
|
||||
}
|
||||
} else if (point.pointType === 'Floor') {
|
||||
return {
|
||||
defaultInnerColor: Constants.pointConfig.defaultInnerColor,
|
||||
defaultOuterColor: Constants.pointConfig.floorOuterColor,
|
||||
defaultDeleteColor: Constants.pointConfig.deleteColor,
|
||||
}
|
||||
} else if (point.pointType === 'Wall') {
|
||||
return {
|
||||
defaultInnerColor: Constants.pointConfig.defaultInnerColor,
|
||||
defaultOuterColor: Constants.pointConfig.wallOuterColor,
|
||||
defaultDeleteColor: Constants.pointConfig.deleteColor,
|
||||
}
|
||||
} else if (point.pointType === 'Zone') {
|
||||
return {
|
||||
defaultInnerColor: Constants.pointConfig.defaultInnerColor,
|
||||
defaultOuterColor: Constants.pointConfig.zoneOuterColor,
|
||||
defaultDeleteColor: Constants.pointConfig.deleteColor,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
defaultInnerColor: Constants.pointConfig.defaultInnerColor,
|
||||
defaultOuterColor: Constants.pointConfig.defaultOuterColor,
|
||||
defaultDeleteColor: Constants.pointConfig.deleteColor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (materialRef.current && (toolMode === 'move' || deletePointOrLine)) {
|
||||
let innerColor;
|
||||
let outerColor;
|
||||
if (isHovered) {
|
||||
innerColor = deletePointOrLine ? colors.defaultDeleteColor : colors.defaultOuterColor;
|
||||
outerColor = deletePointOrLine ? colors.defaultDeleteColor : colors.defaultOuterColor;
|
||||
} else {
|
||||
innerColor = colors.defaultInnerColor;
|
||||
outerColor = colors.defaultOuterColor;
|
||||
}
|
||||
materialRef.current.uniforms.uInnerColor.value.set(innerColor);
|
||||
materialRef.current.uniforms.uOuterColor.value.set(outerColor);
|
||||
materialRef.current.uniformsNeedUpdate = true;
|
||||
} else if (materialRef.current && toolMode !== 'move') {
|
||||
materialRef.current.uniforms.uInnerColor.value.set(colors.defaultInnerColor);
|
||||
materialRef.current.uniforms.uOuterColor.value.set(colors.defaultOuterColor);
|
||||
materialRef.current.uniformsNeedUpdate = true;
|
||||
}
|
||||
}, [isHovered, colors.defaultInnerColor, colors.defaultOuterColor, colors.defaultDeleteColor, toolMode, deletePointOrLine]);
|
||||
|
||||
const uniforms = useMemo(() => ({
|
||||
uOuterColor: { value: new THREE.Color(colors.defaultOuterColor) },
|
||||
uInnerColor: { value: new THREE.Color(colors.defaultInnerColor) },
|
||||
}), [colors.defaultInnerColor, colors.defaultOuterColor]);
|
||||
|
||||
const handleDrag = (point: Point) => {
|
||||
if (toolMode === 'move' && isHovered) {
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
const position = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
if (point.pointType === 'Aisle') {
|
||||
if (position) {
|
||||
const newPosition: [number, number, number] = [position.x, position.y, position.z];
|
||||
const aisleSnappedPosition = snapPosition(newPosition);
|
||||
const finalSnappedPosition = checkSnapForAisle(aisleSnappedPosition.position);
|
||||
|
||||
setAislePosition(point.pointUuid, finalSnappedPosition.position);
|
||||
|
||||
}
|
||||
} else if (point.pointType === 'Wall') {
|
||||
if (position) {
|
||||
const newPosition: [number, number, number] = [position.x, position.y, position.z];
|
||||
|
||||
setWallPosition(point.pointUuid, newPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragEnd = (point: Point) => {
|
||||
if (deletePointOrLine) return;
|
||||
if (point.pointType === 'Aisle') {
|
||||
const updatedAisles = getAislesByPointId(point.pointUuid);
|
||||
if (updatedAisles.length > 0 && projectId) {
|
||||
updatedAisles.forEach((updatedAisle) => {
|
||||
createAisleApi(updatedAisle.aisleUuid, updatedAisle.points, updatedAisle.type, projectId)
|
||||
})
|
||||
}
|
||||
} else if (point.pointType === 'Wall') {
|
||||
// console.log('Wall after drag: ', point);
|
||||
}
|
||||
}
|
||||
|
||||
const handlePointClick = (point: Point) => {
|
||||
if (deletePointOrLine) {
|
||||
if (point.pointType === 'Aisle') {
|
||||
const removedAisles = removeAislePoint(point.pointUuid);
|
||||
if (removedAisles.length > 0) {
|
||||
removedAisles.forEach(aisle => {
|
||||
if (projectId)
|
||||
deleteAisleApi(aisle.aisleUuid, projectId)
|
||||
});
|
||||
setHoveredPoint(null);
|
||||
}
|
||||
}
|
||||
if (point.pointType === 'Wall') {
|
||||
const removedAisles = removeWallPoint(point.pointUuid);
|
||||
if (removedAisles.length > 0) {
|
||||
setHoveredPoint(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (hoveredPoint && hoveredPoint.pointUuid !== point.pointUuid) {
|
||||
setIsHovered(false);
|
||||
}
|
||||
}, [hoveredPoint])
|
||||
|
||||
if (!point) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DragControls
|
||||
axisLock='y'
|
||||
autoTransform={false}
|
||||
onDrag={() => { handleDrag(point) }}
|
||||
onDragEnd={() => { handleDragEnd(point) }}
|
||||
>
|
||||
<mesh
|
||||
key={point.pointUuid}
|
||||
uuid={point.pointUuid}
|
||||
name={`${point.pointType}-Point`}
|
||||
position={new THREE.Vector3(...point.position)}
|
||||
onClick={() => {
|
||||
handlePointClick(point);
|
||||
}}
|
||||
onPointerOver={() => {
|
||||
if (!hoveredPoint) {
|
||||
setHoveredPoint(point);
|
||||
setIsHovered(true)
|
||||
}
|
||||
}}
|
||||
onPointerOut={() => {
|
||||
if (hoveredPoint && hoveredPoint.pointUuid === point.pointUuid) {
|
||||
setHoveredPoint(null);
|
||||
}
|
||||
setIsHovered(false)
|
||||
}}
|
||||
userData={point}
|
||||
>
|
||||
<boxGeometry args={boxScale} />
|
||||
<shaderMaterial
|
||||
ref={materialRef}
|
||||
uniforms={uniforms}
|
||||
vertexShader={
|
||||
`
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`
|
||||
}
|
||||
fragmentShader={
|
||||
`
|
||||
varying vec2 vUv;
|
||||
uniform vec3 uOuterColor;
|
||||
uniform vec3 uInnerColor;
|
||||
|
||||
void main() {
|
||||
// Define the size of the white square as a proportion of the face
|
||||
float borderThickness = 0.2; // Adjust this value for border thickness
|
||||
if (vUv.x > borderThickness && vUv.x < 1.0 - borderThickness && vUv.y > borderThickness && vUv.y < 1.0 - borderThickness) {
|
||||
gl_FragColor = vec4(uInnerColor, 1.0); // Inner square
|
||||
} else {
|
||||
gl_FragColor = vec4(uOuterColor, 1.0); // Border
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
/>
|
||||
</mesh>
|
||||
</DragControls>
|
||||
);
|
||||
}
|
||||
|
||||
export default Point;
|
||||
61
app/src/modules/builder/point/reference/referencePoint.tsx
Normal file
61
app/src/modules/builder/point/reference/referencePoint.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import * as THREE from 'three';
|
||||
import * as Constants from '../../../../types/world/worldConstants';
|
||||
import { useRef, useMemo } from 'react';
|
||||
|
||||
function ReferencePoint({ point }: { readonly point: Point }) {
|
||||
const materialRef = useRef<THREE.ShaderMaterial>(null);
|
||||
|
||||
const boxScale: [number, number, number] = Constants.pointConfig.boxScale;
|
||||
const colors = {
|
||||
defaultInnerColor: Constants.pointConfig.defaultInnerColor,
|
||||
defaultOuterColor: Constants.pointConfig.helperColor,
|
||||
defaultDeleteColor: Constants.pointConfig.deleteColor,
|
||||
};
|
||||
|
||||
const uniforms = useMemo(() => ({
|
||||
uOuterColor: { value: new THREE.Color(colors.defaultOuterColor) },
|
||||
uInnerColor: { value: new THREE.Color(colors.defaultInnerColor) },
|
||||
}), [colors.defaultInnerColor, colors.defaultOuterColor, colors.defaultDeleteColor]);
|
||||
|
||||
if (!point) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<mesh
|
||||
position={new THREE.Vector3(...point.position)}
|
||||
>
|
||||
<boxGeometry args={boxScale} />
|
||||
<shaderMaterial
|
||||
ref={materialRef}
|
||||
uniforms={uniforms}
|
||||
vertexShader={`
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`}
|
||||
fragmentShader={`
|
||||
varying vec2 vUv;
|
||||
uniform vec3 uOuterColor;
|
||||
uniform vec3 uInnerColor;
|
||||
|
||||
void main() {
|
||||
// Define the size of the white square as a proportion of the face
|
||||
float borderThickness = 0.2; // Adjust this value for border thickness
|
||||
if (vUv.x > borderThickness && vUv.x < 1.0 - borderThickness &&
|
||||
vUv.y > borderThickness && vUv.y < 1.0 - borderThickness) {
|
||||
gl_FragColor = vec4(uInnerColor, 1.0); // Inner square
|
||||
} else {
|
||||
gl_FragColor = vec4(uOuterColor, 1.0); // Border
|
||||
}
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReferencePoint;
|
||||
@@ -0,0 +1,271 @@
|
||||
import { useMemo } from 'react';
|
||||
import * as turf from '@turf/turf';
|
||||
|
||||
export function useWallClassification(walls: Walls) {
|
||||
// Find all minimal rooms from the given walls
|
||||
const findRooms = () => {
|
||||
if (walls.length < 3) return [];
|
||||
|
||||
// Build a graph of point connections
|
||||
const graph = new Map<string, string[]>();
|
||||
const pointMap = new Map<string, Point>();
|
||||
|
||||
walls.forEach(wall => {
|
||||
const [p1, p2] = wall.points;
|
||||
pointMap.set(p1.pointUuid, p1);
|
||||
pointMap.set(p2.pointUuid, p2);
|
||||
|
||||
// Add connection from p1 to p2
|
||||
if (!graph.has(p1.pointUuid)) graph.set(p1.pointUuid, []);
|
||||
graph.get(p1.pointUuid)?.push(p2.pointUuid);
|
||||
|
||||
// Add connection from p2 to p1
|
||||
if (!graph.has(p2.pointUuid)) graph.set(p2.pointUuid, []);
|
||||
graph.get(p2.pointUuid)?.push(p1.pointUuid);
|
||||
});
|
||||
|
||||
// Find all minimal cycles (rooms) in the graph
|
||||
const allCycles: string[][] = [];
|
||||
|
||||
const findCycles = (startNode: string, currentNode: string, path: string[], depth = 0) => {
|
||||
if (depth > 20) return; // Prevent infinite recursion
|
||||
|
||||
path.push(currentNode);
|
||||
|
||||
const neighbors = graph.get(currentNode) || [];
|
||||
|
||||
for (const neighbor of neighbors) {
|
||||
if (path.length > 2 && neighbor === startNode) {
|
||||
// Found a cycle that returns to start
|
||||
const cycle = [...path, neighbor];
|
||||
|
||||
// Check if this is a new unique cycle
|
||||
if (!cycleExists(allCycles, cycle)) {
|
||||
allCycles.push(cycle);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!path.includes(neighbor)) {
|
||||
findCycles(startNode, neighbor, [...path], depth + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Start cycle detection from each node
|
||||
for (const [pointUuid] of graph) {
|
||||
findCycles(pointUuid, pointUuid, []);
|
||||
}
|
||||
|
||||
// Convert cycles to Point arrays and validate them
|
||||
const potentialRooms = allCycles
|
||||
.map(cycle => cycle.map(uuid => pointMap.get(uuid)!))
|
||||
.filter(room => isValidRoom(room));
|
||||
|
||||
const uniqueRooms = removeDuplicateRooms(potentialRooms);
|
||||
|
||||
// ✅ New logic that only removes redundant supersets
|
||||
const filteredRooms = removeSupersetLikeRooms(uniqueRooms, walls);
|
||||
|
||||
return filteredRooms;
|
||||
|
||||
};
|
||||
|
||||
const removeSupersetLikeRooms = (rooms: Point[][], walls: Wall[]): Point[][] => {
|
||||
const toRemove = new Set<number>();
|
||||
|
||||
const getPolygon = (points: Point[]) =>
|
||||
turf.polygon([points.map(p => [p.position[0], p.position[2]])]);
|
||||
|
||||
const getWallSet = (room: Point[]) => {
|
||||
const set = new Set<string>();
|
||||
for (let i = 0; i < room.length - 1; i++) {
|
||||
const p1 = room[i].pointUuid;
|
||||
const p2 = room[i + 1].pointUuid;
|
||||
|
||||
const wall = walls.find(w =>
|
||||
(w.points[0].pointUuid === p1 && w.points[1].pointUuid === p2) ||
|
||||
(w.points[0].pointUuid === p2 && w.points[1].pointUuid === p1)
|
||||
);
|
||||
|
||||
if (wall) {
|
||||
set.add(wall.wallUuid);
|
||||
}
|
||||
}
|
||||
return set;
|
||||
};
|
||||
|
||||
const roomPolygons = rooms.map(getPolygon);
|
||||
const roomAreas = roomPolygons.map(poly => turf.area(poly));
|
||||
const wallSets = rooms.map(getWallSet);
|
||||
|
||||
// First, identify all rooms that are completely contained within others
|
||||
for (let i = 0; i < rooms.length; i++) {
|
||||
for (let j = 0; j < rooms.length; j++) {
|
||||
if (i === j) continue;
|
||||
|
||||
// If room i completely contains room j
|
||||
if (turf.booleanContains(roomPolygons[i], roomPolygons[j])) {
|
||||
// Check if the contained room shares most of its walls with the containing room
|
||||
const sharedWalls = [...wallSets[j]].filter(w => wallSets[i].has(w));
|
||||
const shareRatio = sharedWalls.length / wallSets[j].size;
|
||||
|
||||
// If they share more than 50% of walls, mark the larger one for removal
|
||||
// UNLESS the smaller one is significantly smaller (likely a real room)
|
||||
if (shareRatio > 0.5 && (roomAreas[i] / roomAreas[j] > 2)) {
|
||||
toRemove.add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: handle cases where a room is divided by segmented walls
|
||||
for (let i = 0; i < rooms.length; i++) {
|
||||
if (toRemove.has(i)) continue;
|
||||
|
||||
for (let j = 0; j < rooms.length; j++) {
|
||||
if (i === j || toRemove.has(j)) continue;
|
||||
|
||||
// Check if these rooms share a significant portion of walls
|
||||
const sharedWalls = [...wallSets[i]].filter(w => wallSets[j].has(w));
|
||||
const shareRatio = Math.max(
|
||||
sharedWalls.length / wallSets[i].size,
|
||||
sharedWalls.length / wallSets[j].size
|
||||
);
|
||||
|
||||
// If they share more than 30% of walls and one is much larger
|
||||
if (shareRatio > 0.3) {
|
||||
const areaRatio = roomAreas[i] / roomAreas[j];
|
||||
if (areaRatio > 2) {
|
||||
// The larger room is likely the undivided version
|
||||
toRemove.add(i);
|
||||
} else if (areaRatio < 0.5) {
|
||||
// The smaller room might be an artifact
|
||||
toRemove.add(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rooms.filter((_, idx) => !toRemove.has(idx));
|
||||
};
|
||||
|
||||
// Check if a cycle already exists in our list (considering different orders)
|
||||
const cycleExists = (allCycles: string[][], newCycle: string[]) => {
|
||||
const newSet = new Set(newCycle);
|
||||
|
||||
return allCycles.some(existingCycle => {
|
||||
if (existingCycle.length !== newCycle.length) return false;
|
||||
const existingSet = new Set(existingCycle);
|
||||
return setsEqual(newSet, existingSet);
|
||||
});
|
||||
};
|
||||
|
||||
// Check if two sets are equal
|
||||
const setsEqual = <T,>(a: Set<T>, b: Set<T>) => {
|
||||
if (a.size !== b.size) return false;
|
||||
for (const item of a) if (!b.has(item)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
// Remove duplicate rooms (same set of points in different orders)
|
||||
const removeDuplicateRooms = (rooms: Point[][]) => {
|
||||
const uniqueRooms: Point[][] = [];
|
||||
const roomHashes = new Set<string>();
|
||||
|
||||
for (const room of rooms) {
|
||||
// Create a consistent hash for the room regardless of point order
|
||||
const hash = room
|
||||
.map(p => p.pointUuid)
|
||||
.sort()
|
||||
.join('-');
|
||||
|
||||
if (!roomHashes.has(hash)) {
|
||||
roomHashes.add(hash);
|
||||
uniqueRooms.push(room);
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueRooms;
|
||||
};
|
||||
|
||||
// Check if a room is valid (closed, non-self-intersecting polygon)
|
||||
const isValidRoom = (points: Point[]): boolean => {
|
||||
// Must have at least 4 points (first and last are same)
|
||||
if (points.length < 4) return false;
|
||||
|
||||
// Must be a closed loop
|
||||
if (points[0].pointUuid !== points[points.length - 1].pointUuid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const coordinates = points.map(p => [p.position[0], p.position[2]]);
|
||||
const polygon = turf.polygon([coordinates]);
|
||||
return turf.booleanValid(polygon);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Rest of the implementation remains the same...
|
||||
const rooms = useMemo(() => findRooms(), [walls]);
|
||||
|
||||
// Determine wall orientation relative to room
|
||||
const findWallType = (wall: Wall) => {
|
||||
// Find all rooms that contain this wall
|
||||
const containingRooms = rooms.filter(room => {
|
||||
for (let i = 0; i < room.length - 1; i++) {
|
||||
const p1 = room[i];
|
||||
const p2 = room[i + 1];
|
||||
if ((wall.points[0].pointUuid === p1.pointUuid && wall.points[1].pointUuid === p2.pointUuid) ||
|
||||
(wall.points[0].pointUuid === p2.pointUuid && wall.points[1].pointUuid === p1.pointUuid)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (containingRooms.length === 0) {
|
||||
return {
|
||||
type: 'segment',
|
||||
rooms: []
|
||||
};
|
||||
} else if (containingRooms.length === 1) {
|
||||
return {
|
||||
type: 'room',
|
||||
rooms: containingRooms
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'rooms',
|
||||
rooms: containingRooms
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Update the other functions to use the new return type
|
||||
const getWallType = (wall: Wall): {
|
||||
type: string;
|
||||
rooms: Point[][];
|
||||
} => {
|
||||
return findWallType(wall);
|
||||
};
|
||||
|
||||
const isRoomWall = (wall: Wall): boolean => {
|
||||
const type = findWallType(wall).type;
|
||||
return type === 'room' || type === 'rooms';
|
||||
};
|
||||
|
||||
const isSegmentWall = (wall: Wall): boolean => {
|
||||
return findWallType(wall).type === 'segment';
|
||||
};
|
||||
|
||||
return {
|
||||
rooms,
|
||||
getWallType,
|
||||
isRoomWall,
|
||||
isSegmentWall,
|
||||
findRooms,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import * as THREE from 'three';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
function useWallGeometry(wallLength: number, wallHeight: number, wallThickness: number) {
|
||||
return useMemo(() => {
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
|
||||
const halfLength = wallLength / 2;
|
||||
const halfThickness = wallThickness / 2;
|
||||
const height = wallHeight;
|
||||
|
||||
const vertices = [
|
||||
-halfLength, -height / 2, halfThickness,
|
||||
-halfLength, height / 2, halfThickness,
|
||||
halfLength, height / 2, halfThickness,
|
||||
halfLength, -height / 2, halfThickness,
|
||||
-halfLength, -height / 2, -halfThickness,
|
||||
-halfLength, height / 2, -halfThickness,
|
||||
halfLength, height / 2, -halfThickness,
|
||||
halfLength, -height / 2, -halfThickness,
|
||||
-halfLength, height / 2, halfThickness,
|
||||
-halfLength, height / 2, -halfThickness,
|
||||
halfLength, height / 2, -halfThickness,
|
||||
halfLength, height / 2, halfThickness,
|
||||
-halfLength, -height / 2, halfThickness,
|
||||
-halfLength, -height / 2, -halfThickness,
|
||||
halfLength, -height / 2, -halfThickness,
|
||||
halfLength, -height / 2, halfThickness,
|
||||
];
|
||||
|
||||
const indices = [
|
||||
0, 1, 2, 0, 2, 3,
|
||||
4, 6, 5, 4, 7, 6,
|
||||
0, 4, 5, 0, 5, 1,
|
||||
3, 2, 6, 3, 6, 7,
|
||||
8, 9, 10, 8, 10, 11,
|
||||
12, 14, 13, 12, 15, 14
|
||||
];
|
||||
|
||||
geometry.setIndex(indices);
|
||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
|
||||
geometry.setAttribute('uv', new THREE.Float32BufferAttribute([
|
||||
0, 0, 0, 1, 1, 1, 1, 0,
|
||||
0, 0, 0, 1, 1, 1, 1, 0,
|
||||
0, 0, 0, 1, 1, 1, 1, 0,
|
||||
0, 0, 0, 1, 1, 1, 1, 0,
|
||||
0, 0, 0, 1, 1, 1, 1, 0,
|
||||
0, 0, 0, 1, 1, 1, 1, 0
|
||||
], 2));
|
||||
|
||||
geometry.computeVertexNormals();
|
||||
|
||||
geometry.addGroup(0, 6, 0); // Front
|
||||
geometry.addGroup(6, 6, 1); // Back
|
||||
geometry.addGroup(12, 6, 2); // Left
|
||||
geometry.addGroup(18, 6, 3); // Right
|
||||
geometry.addGroup(24, 6, 4); // Top
|
||||
geometry.addGroup(30, 6, 5); // Bottom
|
||||
|
||||
return geometry;
|
||||
}, [wallLength, wallHeight, wallThickness]);
|
||||
}
|
||||
|
||||
export default useWallGeometry;
|
||||
128
app/src/modules/builder/wall/Instances/instance/wall.tsx
Normal file
128
app/src/modules/builder/wall/Instances/instance/wall.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import * as THREE from 'three';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import * as Constants from '../../../../../types/world/worldConstants';
|
||||
|
||||
import insideMaterial from '../../../../../assets/textures/floor/wall-tex.png';
|
||||
import outsideMaterial from '../../../../../assets/textures/floor/factory wall texture.jpg';
|
||||
import useWallGeometry from './helpers/useWallGeometry';
|
||||
import { useWallStore } from '../../../../../store/builder/useWallStore';
|
||||
import { useWallClassification } from './helpers/useWallClassification';
|
||||
import { useFrame, useThree } from '@react-three/fiber';
|
||||
import { useWallVisibility } from '../../../../../store/builder/store';
|
||||
import { Decal } from '@react-three/drei';
|
||||
import { Base } from '@react-three/csg';
|
||||
|
||||
function Wall({ wall }: { readonly wall: Wall }) {
|
||||
const { walls } = useWallStore();
|
||||
const { getWallType } = useWallClassification(walls);
|
||||
const wallType = getWallType(wall);
|
||||
const [startPoint, endPoint] = wall.points;
|
||||
const [visible, setVisible] = useState(true);
|
||||
const { wallVisibility } = useWallVisibility();
|
||||
const meshRef = useRef<any>();
|
||||
const { camera } = useThree();
|
||||
|
||||
const startX = startPoint.position[0];
|
||||
const startZ = startPoint.position[2];
|
||||
const endX = endPoint.position[0];
|
||||
const endZ = endPoint.position[2];
|
||||
|
||||
const wallLength = Math.sqrt((endX - startX) ** 2 + (endZ - startZ) ** 2);
|
||||
const angle = Math.atan2(endZ - startZ, endX - startX);
|
||||
|
||||
const centerX = (startX + endX) / 2;
|
||||
const centerZ = (startZ + endZ) / 2;
|
||||
const centerY = wall.wallHeight / 2;
|
||||
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
|
||||
const [insideWallTexture, outsideWallTexture] = useMemo(() => {
|
||||
const inside = textureLoader.load(insideMaterial);
|
||||
inside.wrapS = inside.wrapT = THREE.RepeatWrapping;
|
||||
inside.repeat.set(wallLength / 10, wall.wallHeight / 10);
|
||||
inside.colorSpace = THREE.SRGBColorSpace;
|
||||
|
||||
const outside = textureLoader.load(outsideMaterial);
|
||||
outside.wrapS = outside.wrapT = THREE.RepeatWrapping;
|
||||
outside.repeat.set(wallLength / 10, wall.wallHeight / 10);
|
||||
outside.colorSpace = THREE.SRGBColorSpace;
|
||||
|
||||
return [inside, outside];
|
||||
}, [wallLength, wall.wallHeight]);
|
||||
|
||||
const materials = useMemo(() => {
|
||||
const frontMaterial = insideWallTexture;
|
||||
|
||||
const backMaterial = outsideWallTexture;
|
||||
|
||||
return [
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: Constants.wallConfig.defaultColor,
|
||||
side: THREE.DoubleSide,
|
||||
map: frontMaterial
|
||||
}),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: Constants.wallConfig.defaultColor,
|
||||
side: THREE.DoubleSide,
|
||||
map: backMaterial
|
||||
}),
|
||||
new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide }), // Left
|
||||
new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide }), // Right
|
||||
new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide }), // Top
|
||||
new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide }) // Bottom
|
||||
];
|
||||
}, [insideWallTexture, outsideWallTexture, wall]);
|
||||
|
||||
const geometry = useWallGeometry(wallLength, wall.wallHeight, wall.wallThickness);
|
||||
|
||||
useFrame(() => {
|
||||
if (!meshRef.current) return;
|
||||
const v = new THREE.Vector3();
|
||||
const u = new THREE.Vector3();
|
||||
|
||||
if (!wallVisibility && wallType.type === 'room') {
|
||||
meshRef.current.getWorldDirection(v);
|
||||
camera.getWorldDirection(u);
|
||||
setVisible((2 * v.dot(u)) <= 0.1);
|
||||
|
||||
} else {
|
||||
setVisible(true);
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<group
|
||||
name={`Wall-${wall.wallUuid}`}
|
||||
userData={wall}
|
||||
position={[centerX, centerY, centerZ]}
|
||||
rotation={[0, -angle, 0]}
|
||||
visible={visible}
|
||||
>
|
||||
<Base ref={meshRef} geometry={geometry} visible>
|
||||
{materials.map((material, index) => (
|
||||
<primitive key={index} object={material} attach={`material-${index}`} />
|
||||
))}
|
||||
|
||||
{wall.decals.map((decal) => {
|
||||
return (
|
||||
<Decal
|
||||
// debug
|
||||
position={[decal.decalPosition[0], decal.decalPosition[1], wall.wallThickness / 2]}
|
||||
rotation={[0, 0, decal.decalRotation]}
|
||||
scale={[decal.decalScale, decal.decalScale, 0.001]}
|
||||
>
|
||||
<meshBasicMaterial
|
||||
map={outsideWallTexture}
|
||||
side={THREE.DoubleSide}
|
||||
polygonOffset
|
||||
polygonOffsetFactor={-1}
|
||||
/>
|
||||
</Decal>
|
||||
)
|
||||
})}
|
||||
</Base>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default Wall;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useToggleView } from "../../../../../store/builder/store";
|
||||
import Wall from "./wall"
|
||||
|
||||
function WallInstance({ wall }: { readonly wall: Wall }) {
|
||||
const { toggleView } = useToggleView();
|
||||
|
||||
return (
|
||||
<>
|
||||
{!toggleView && (
|
||||
<Wall wall={wall} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WallInstance
|
||||
79
app/src/modules/builder/wall/Instances/wallInstances.tsx
Normal file
79
app/src/modules/builder/wall/Instances/wallInstances.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { useWallStore } from '../../../../store/builder/useWallStore'
|
||||
import WallInstance from './instance/wallInstance';
|
||||
import Line from '../../line/line';
|
||||
import Point from '../../point/point';
|
||||
import { useToggleView } from '../../../../store/builder/store';
|
||||
import { Base, Geometry, Subtraction } from '@react-three/csg';
|
||||
import { BoxGeometry } from 'three';
|
||||
|
||||
function WallInstances() {
|
||||
const { walls } = useWallStore();
|
||||
const { toggleView } = useToggleView();
|
||||
const ref = useRef<any>();
|
||||
|
||||
useEffect(() => {
|
||||
// console.log('walls: ', walls);
|
||||
}, [walls])
|
||||
|
||||
const allPoints = useMemo(() => {
|
||||
const points: Point[] = [];
|
||||
const seenUuids = new Set<string>();
|
||||
|
||||
walls.forEach(aisle => {
|
||||
aisle.points.forEach(point => {
|
||||
if (!seenUuids.has(point.pointUuid)) {
|
||||
seenUuids.add(point.pointUuid);
|
||||
points.push(point);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return points;
|
||||
}, [walls]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<group name='Walls-Group' ref={ref}>
|
||||
<Geometry computeVertexNormals>
|
||||
|
||||
{walls.map((wall) => (
|
||||
<WallInstance key={wall.wallUuid} wall={wall} />
|
||||
))}
|
||||
|
||||
{/* <Base geometry={new BoxGeometry()} >
|
||||
<meshStandardMaterial />
|
||||
</Base>
|
||||
|
||||
<Geometry>
|
||||
<Subtraction scale={[5, 11, 5]} >
|
||||
<Geometry>
|
||||
<Base geometry={new BoxGeometry()} />
|
||||
</Geometry>
|
||||
</Subtraction>
|
||||
</Geometry> */}
|
||||
</Geometry>
|
||||
</group>
|
||||
|
||||
{toggleView && (
|
||||
<>
|
||||
<group name='Wall-Points-Group'>
|
||||
{allPoints.map((point) => (
|
||||
<Point key={point.pointUuid} point={point} />
|
||||
))}
|
||||
</group>
|
||||
|
||||
<group name='Wall-Lines-Group'>
|
||||
{walls.map((wall) => (
|
||||
<React.Fragment key={wall.wallUuid}>
|
||||
<Line points={wall.points} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</group>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WallInstances
|
||||
128
app/src/modules/builder/wall/wallCreator/referenceWall.tsx
Normal file
128
app/src/modules/builder/wall/wallCreator/referenceWall.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { useFrame, useThree } from '@react-three/fiber';
|
||||
import { Html } from '@react-three/drei';
|
||||
import { useBuilderStore } from '../../../../store/builder/useBuilderStore';
|
||||
import { useActiveLayer, useToolMode, useToggleView } from '../../../../store/builder/store';
|
||||
import { useDirectionalSnapping } from '../../point/helpers/useDirectionalSnapping';
|
||||
import { usePointSnapping } from '../../point/helpers/usePointSnapping';
|
||||
import ReferenceLine from '../../line/reference/referenceLine';
|
||||
|
||||
interface ReferenceWallProps {
|
||||
tempPoints: Point[];
|
||||
}
|
||||
|
||||
function ReferenceWall({ tempPoints }: Readonly<ReferenceWallProps>) {
|
||||
const { wallHeight, wallThickness, setSnappedPosition, setSnappedPoint } = useBuilderStore();
|
||||
|
||||
const { pointer, raycaster, camera } = useThree();
|
||||
const { toolMode } = useToolMode();
|
||||
const { toggleView } = useToggleView();
|
||||
const { activeLayer } = useActiveLayer();
|
||||
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
||||
const finalPosition = useRef<[number, number, number] | null>(null);
|
||||
|
||||
const [tempWall, setTempWall] = useState<Wall | null>(null);
|
||||
const [currentPosition, setCurrentPosition] = useState<[number, number, number]>(tempPoints[0]?.position);
|
||||
|
||||
const directionalSnap = useDirectionalSnapping(currentPosition, tempPoints[0]?.position || null);
|
||||
const { checkSnapForWall } = usePointSnapping({ uuid: 'temp-wall', pointType: 'Wall', position: directionalSnap.position || [0, 0, 0] });
|
||||
|
||||
useFrame(() => {
|
||||
if (toolMode === 'Wall' && toggleView && tempPoints.length === 1) {
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
|
||||
setCurrentPosition([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]);
|
||||
|
||||
if (intersectionPoint) {
|
||||
const snapped = checkSnapForWall([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]);
|
||||
|
||||
if (snapped.isSnapped && snapped.snappedPoint) {
|
||||
finalPosition.current = snapped.position;
|
||||
setSnappedPosition(snapped.position);
|
||||
setSnappedPoint(snapped.snappedPoint);
|
||||
} else if (directionalSnap.isSnapped) {
|
||||
finalPosition.current = directionalSnap.position;
|
||||
setSnappedPosition(directionalSnap.position);
|
||||
setSnappedPoint(null);
|
||||
} else {
|
||||
finalPosition.current = [intersectionPoint.x, intersectionPoint.y, intersectionPoint.z];
|
||||
setSnappedPosition(null);
|
||||
setSnappedPoint(null);
|
||||
}
|
||||
|
||||
if (!finalPosition.current) return;
|
||||
|
||||
const wallPoints: [Point, Point] = [
|
||||
tempPoints[0],
|
||||
{
|
||||
pointUuid: 'temp-point',
|
||||
pointType: 'Wall',
|
||||
position: finalPosition.current,
|
||||
layer: activeLayer,
|
||||
}
|
||||
];
|
||||
|
||||
setTempWall({
|
||||
wallUuid: 'temp-wall',
|
||||
points: wallPoints,
|
||||
outSideMaterial: 'default',
|
||||
inSideMaterial: 'default',
|
||||
wallThickness: wallThickness,
|
||||
wallHeight: wallHeight,
|
||||
decals: []
|
||||
})
|
||||
|
||||
}
|
||||
} else if (tempWall !== null) {
|
||||
setTempWall(null);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setTempWall(null);
|
||||
}, [toolMode, toggleView, tempPoints.length, wallHeight, wallThickness]);
|
||||
|
||||
if (!tempWall) return null;
|
||||
|
||||
const renderWall = () => {
|
||||
return (
|
||||
<ReferenceLine points={tempWall.points} />
|
||||
)
|
||||
};
|
||||
|
||||
const textPosition = new THREE.Vector3().addVectors(new THREE.Vector3(...tempWall.points[0].position), new THREE.Vector3(...tempWall.points[1].position)).divideScalar(2);
|
||||
const distance = new THREE.Vector3(...tempWall.points[0].position).distanceTo(new THREE.Vector3(...tempWall.points[1].position));
|
||||
|
||||
const rendertext = () => {
|
||||
return (
|
||||
<>
|
||||
{toggleView && (
|
||||
<Html
|
||||
key={tempWall.wallUuid}
|
||||
userData={tempWall}
|
||||
position={[textPosition.x, 1, textPosition.z]}
|
||||
wrapperClass="distance-text-wrapper"
|
||||
className="distance-text"
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
sprite
|
||||
>
|
||||
<div className={`distance ${tempWall.wallUuid}`}>{distance.toFixed(2)} m</div>
|
||||
</Html>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<group name="Wall-Reference-Group">
|
||||
{renderWall()}
|
||||
{rendertext()}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReferenceWall;
|
||||
156
app/src/modules/builder/wall/wallCreator/wallCreator.tsx
Normal file
156
app/src/modules/builder/wall/wallCreator/wallCreator.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import * as THREE from 'three'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useThree } from '@react-three/fiber';
|
||||
import { useActiveLayer, useSocketStore, useToggleView, useToolMode } from '../../../../store/builder/store';
|
||||
import { useWallStore } from '../../../../store/builder/useWallStore';
|
||||
import { useBuilderStore } from '../../../../store/builder/useBuilderStore';
|
||||
import ReferencePoint from '../../point/reference/referencePoint';
|
||||
import ReferenceWall from './referenceWall';
|
||||
|
||||
function WallCreator() {
|
||||
const { scene, camera, raycaster, gl, pointer } = useThree();
|
||||
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []);
|
||||
const { toggleView } = useToggleView();
|
||||
const { toolMode } = useToolMode();
|
||||
const { activeLayer } = useActiveLayer();
|
||||
const { socket } = useSocketStore();
|
||||
const { addWall, getWallPointById } = useWallStore();
|
||||
const drag = useRef(false);
|
||||
const isLeftMouseDown = useRef(false);
|
||||
const { wallThickness, wallHeight, snappedPosition, snappedPoint } = useBuilderStore();
|
||||
|
||||
const [tempPoints, setTempPoints] = useState<Point[]>([]);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const canvasElement = gl.domElement;
|
||||
|
||||
const onMouseDown = (evt: any) => {
|
||||
if (evt.button === 0) {
|
||||
isLeftMouseDown.current = true;
|
||||
drag.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUp = (evt: any) => {
|
||||
if (evt.button === 0) {
|
||||
isLeftMouseDown.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseMove = () => {
|
||||
if (isLeftMouseDown) {
|
||||
drag.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseClick = () => {
|
||||
if (drag.current || !toggleView) return;
|
||||
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
let position = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
if (!position) return;
|
||||
|
||||
const intersects = raycaster.intersectObjects(scene.children).find((intersect) => intersect.object.name === 'Wall-Point');
|
||||
|
||||
const newPoint: Point = {
|
||||
pointUuid: THREE.MathUtils.generateUUID(),
|
||||
pointType: 'Wall',
|
||||
position: [position.x, position.y, position.z],
|
||||
layer: activeLayer
|
||||
};
|
||||
|
||||
if (snappedPosition && snappedPoint) {
|
||||
newPoint.pointUuid = snappedPoint.pointUuid;
|
||||
newPoint.position = snappedPosition;
|
||||
newPoint.layer = snappedPoint.layer;
|
||||
}
|
||||
|
||||
if (snappedPosition && !snappedPoint) {
|
||||
newPoint.position = snappedPosition;
|
||||
}
|
||||
|
||||
if (intersects && !snappedPoint) {
|
||||
const point = getWallPointById(intersects.object.uuid);
|
||||
if (point) {
|
||||
newPoint.pointUuid = point.pointUuid;
|
||||
newPoint.position = point.position;
|
||||
newPoint.layer = point.layer;
|
||||
}
|
||||
}
|
||||
|
||||
if (tempPoints.length === 0) {
|
||||
setTempPoints([newPoint]);
|
||||
setIsCreating(true);
|
||||
} else {
|
||||
const wall: Wall = {
|
||||
wallUuid: THREE.MathUtils.generateUUID(),
|
||||
points: [tempPoints[0], newPoint],
|
||||
outSideMaterial: 'default',
|
||||
inSideMaterial: 'default',
|
||||
wallThickness: wallThickness,
|
||||
wallHeight: wallHeight,
|
||||
decals: []
|
||||
};
|
||||
addWall(wall);
|
||||
setTempPoints([newPoint]);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const onContext = (event: any) => {
|
||||
event.preventDefault();
|
||||
if (isCreating) {
|
||||
setTempPoints([]);
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (toolMode === "Wall" && toggleView) {
|
||||
canvasElement.addEventListener("mousedown", onMouseDown);
|
||||
canvasElement.addEventListener("mouseup", onMouseUp);
|
||||
canvasElement.addEventListener("mousemove", onMouseMove);
|
||||
canvasElement.addEventListener("click", onMouseClick);
|
||||
canvasElement.addEventListener("contextmenu", onContext);
|
||||
} else {
|
||||
if (tempPoints.length > 0 || isCreating) {
|
||||
setTempPoints([]);
|
||||
setIsCreating(false);
|
||||
}
|
||||
canvasElement.removeEventListener("mousedown", onMouseDown);
|
||||
canvasElement.removeEventListener("mouseup", onMouseUp);
|
||||
canvasElement.removeEventListener("mousemove", onMouseMove);
|
||||
canvasElement.removeEventListener("click", onMouseClick);
|
||||
canvasElement.removeEventListener("contextmenu", onContext);
|
||||
}
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("mousedown", onMouseDown);
|
||||
canvasElement.removeEventListener("mouseup", onMouseUp);
|
||||
canvasElement.removeEventListener("mousemove", onMouseMove);
|
||||
canvasElement.removeEventListener("click", onMouseClick);
|
||||
canvasElement.removeEventListener("contextmenu", onContext);
|
||||
};
|
||||
}, [gl, camera, scene, raycaster, pointer, plane, toggleView, toolMode, activeLayer, socket, tempPoints, isCreating, addWall, getWallPointById, snappedPosition, snappedPoint]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{toggleView &&
|
||||
<>
|
||||
<group name='Wall-Reference-Points-Group'>
|
||||
{tempPoints.map((point) => (
|
||||
<ReferencePoint key={point.pointUuid} point={point} />
|
||||
))}
|
||||
</group>
|
||||
|
||||
{tempPoints.length > 0 &&
|
||||
<ReferenceWall tempPoints={tempPoints} />
|
||||
}
|
||||
</>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WallCreator
|
||||
16
app/src/modules/builder/wall/wallGroup.tsx
Normal file
16
app/src/modules/builder/wall/wallGroup.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import WallCreator from './wallCreator/wallCreator'
|
||||
import WallInstances from './Instances/wallInstances'
|
||||
|
||||
function WallGroup() {
|
||||
return (
|
||||
<>
|
||||
|
||||
<WallCreator />
|
||||
|
||||
<WallInstances />
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WallGroup
|
||||
311
app/src/modules/collaboration/camera/collabCams.tsx
Normal file
311
app/src/modules/collaboration/camera/collabCams.tsx
Normal file
@@ -0,0 +1,311 @@
|
||||
import * as THREE from "three";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
|
||||
import camModel from "../../../assets/gltf-glb/camera face 2.gltf";
|
||||
import getActiveUsersData from "../../../services/factoryBuilder/collab/getActiveUsers";
|
||||
import { useActiveUsers, useCamMode, useSocketStore } from "../../../store/builder/store";
|
||||
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Html } from "@react-three/drei";
|
||||
import CollabUserIcon from "./collabUserIcon";
|
||||
import useModuleStore from "../../../store/useModuleStore";
|
||||
import { getAvatarColor } from "../functions/getAvatarColor";
|
||||
import { useSelectedUserStore } from "../../../store/collaboration/useCollabStore";
|
||||
import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
|
||||
import setCameraView from "../functions/setCameraView";
|
||||
|
||||
const CamModelsGroup = () => {
|
||||
const navigate = useNavigate();
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const email = localStorage.getItem("email");
|
||||
const { setActiveUsers } = useActiveUsers();
|
||||
const { socket } = useSocketStore();
|
||||
const { activeModule } = useModuleStore();
|
||||
const { selectedUser, setSelectedUser } = useSelectedUserStore();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const loader = new GLTFLoader();
|
||||
const dracoLoader = new DRACOLoader();
|
||||
dracoLoader.setDecoderPath("three/examples/jsm/libs/draco/gltf/");
|
||||
loader.setDRACOLoader(dracoLoader);
|
||||
|
||||
|
||||
const { camMode } = useCamMode();
|
||||
const { camera, controls } = useThree(); // Access R3F camera and controls
|
||||
|
||||
useEffect(() => {
|
||||
if (camMode !== "FollowPerson") return;
|
||||
// If a user is selected, set the camera view to their location
|
||||
// and update the camera and controls accordingly
|
||||
if (selectedUser?.location) {
|
||||
const { position, rotation, target } = selectedUser.location;
|
||||
if (rotation && target)
|
||||
setCameraView({
|
||||
controls,
|
||||
camera,
|
||||
position,
|
||||
rotation,
|
||||
target,
|
||||
username: selectedUser.name,
|
||||
});
|
||||
}
|
||||
}, [selectedUser, camera, controls, camMode]);
|
||||
|
||||
const [cams, setCams] = useState<any[]>([]);
|
||||
const [models, setModels] = useState<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
targetPosition: THREE.Vector3;
|
||||
targetRotation: THREE.Euler;
|
||||
target: THREE.Vector3;
|
||||
}
|
||||
>
|
||||
>({});
|
||||
|
||||
const dedupeCams = (cams: any[]) => {
|
||||
const seen = new Set();
|
||||
return cams.filter((cam) => {
|
||||
if (seen.has(cam.uuid)) return false;
|
||||
seen.add(cam.uuid);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const dedupeUsers = (users: any[]) => {
|
||||
const seen = new Set();
|
||||
return users.filter((user) => {
|
||||
if (seen.has(user._id)) return false;
|
||||
seen.add(user._id);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!email) navigate("/");
|
||||
|
||||
if (!socket) return;
|
||||
const organization = email!.split("@")[1].split(".")[0];
|
||||
|
||||
socket.on("userConnectResponse", (data: any) => {
|
||||
if (!groupRef.current) return;
|
||||
if (data.data.userData.email === email) return;
|
||||
if (socket.id === data.socketId || organization !== data.organization)
|
||||
return;
|
||||
|
||||
const model = groupRef.current.getObjectByProperty(
|
||||
"uuid",
|
||||
data.data.userData._id
|
||||
);
|
||||
if (model) {
|
||||
groupRef.current.remove(model);
|
||||
}
|
||||
|
||||
loader.load(camModel, (gltf) => {
|
||||
const newModel = gltf.scene.clone();
|
||||
newModel.uuid = data.data.userData._id;
|
||||
newModel.position.set(
|
||||
data.data.position.x,
|
||||
data.data.position.y,
|
||||
data.data.position.z
|
||||
);
|
||||
newModel.rotation.set(
|
||||
data.data.rotation.x,
|
||||
data.data.rotation.y,
|
||||
data.data.rotation.z
|
||||
);
|
||||
newModel.userData = data.data.userData;
|
||||
newModel.userData.target = new THREE.Vector3(
|
||||
data.data.target.x,
|
||||
data.data.target.y,
|
||||
data.data.target.z
|
||||
);
|
||||
|
||||
setCams((prev) => dedupeCams([...prev, newModel]));
|
||||
setActiveUsers((prev: any) =>
|
||||
dedupeUsers([...prev, data.data.userData])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("userDisConnectResponse", (data: any) => {
|
||||
if (!groupRef.current) return;
|
||||
if (socket.id === data.socketId || organization !== data.organization)
|
||||
return;
|
||||
|
||||
setCams((prev) =>
|
||||
prev.filter((cam) => cam.uuid !== data.data.userData._id)
|
||||
);
|
||||
setActiveUsers((prev: any) =>
|
||||
prev.filter((user: any) => user._id !== data.data.userData._id)
|
||||
);
|
||||
});
|
||||
|
||||
socket.on("v1:camera:Response:update", (data: any) => {
|
||||
// console.log('data: ', data);
|
||||
if (
|
||||
!groupRef.current ||
|
||||
socket.id === data.socketId ||
|
||||
organization !== data.organization
|
||||
)
|
||||
return;
|
||||
|
||||
if (selectedUser && selectedUser?.id === data.data.userId) {
|
||||
setSelectedUser({
|
||||
color: selectedUser.color,
|
||||
name: selectedUser.name,
|
||||
id: selectedUser.id,
|
||||
location: {
|
||||
position: data.data.position,
|
||||
rotation: data.data.rotation,
|
||||
target: data.data.target,
|
||||
},
|
||||
});
|
||||
}
|
||||
setModels((prev) => ({
|
||||
...prev,
|
||||
[data.data.userId]: {
|
||||
targetPosition: new THREE.Vector3(
|
||||
data.data.position.x,
|
||||
data.data.position.y,
|
||||
data.data.position.z
|
||||
),
|
||||
targetRotation: new THREE.Euler(
|
||||
data.data.rotation.x,
|
||||
data.data.rotation.y,
|
||||
data.data.rotation.z
|
||||
),
|
||||
target: new THREE.Vector3(
|
||||
data.data.target.x,
|
||||
data.data.target.y,
|
||||
data.data.target.z
|
||||
),
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("userConnectResponse");
|
||||
socket.off("userDisConnectResponse");
|
||||
socket.off("v1:camera:Response:update");
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [email, loader, navigate, setActiveUsers, socket]);
|
||||
|
||||
useFrame(() => {
|
||||
if (!groupRef.current) return;
|
||||
Object.keys(models).forEach((uuid) => {
|
||||
const model = groupRef.current!.getObjectByProperty("uuid", uuid);
|
||||
if (!model) return;
|
||||
|
||||
const { targetPosition, targetRotation } = models[uuid];
|
||||
model.position.lerp(targetPosition, 0.1);
|
||||
model.rotation.x = THREE.MathUtils.lerp(
|
||||
model.rotation.x,
|
||||
targetRotation.x,
|
||||
0.1
|
||||
);
|
||||
model.rotation.y = THREE.MathUtils.lerp(
|
||||
model.rotation.y,
|
||||
targetRotation.y,
|
||||
0.1
|
||||
);
|
||||
model.rotation.z = THREE.MathUtils.lerp(
|
||||
model.rotation.z,
|
||||
targetRotation.z,
|
||||
0.1
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!groupRef.current) return;
|
||||
const organization = email!.split("@")[1].split(".")[0];
|
||||
|
||||
getActiveUsersData(organization).then((data) => {
|
||||
const filteredData = data.cameraDatas.filter(
|
||||
(camera: any) => camera.userData.email !== email
|
||||
);
|
||||
|
||||
if (filteredData.length > 0) {
|
||||
loader.load(camModel, (gltf) => {
|
||||
const newCams = filteredData.map((cam: any) => {
|
||||
const newModel = gltf.scene.clone();
|
||||
newModel.uuid = cam.userData._id;
|
||||
newModel.position.set(
|
||||
cam.position.x,
|
||||
cam.position.y,
|
||||
cam.position.z
|
||||
);
|
||||
newModel.rotation.set(
|
||||
cam.rotation.x,
|
||||
cam.rotation.y,
|
||||
cam.rotation.z
|
||||
);
|
||||
newModel.userData = cam.userData;
|
||||
cam.userData.position = newModel.position;
|
||||
cam.userData.rotation = newModel.rotation;
|
||||
newModel.userData.target = cam.target;
|
||||
|
||||
return newModel;
|
||||
});
|
||||
|
||||
const users = filteredData.map((cam: any) => cam.userData);
|
||||
setActiveUsers((prev: any) => dedupeUsers([...prev, ...users]));
|
||||
setCams((prev) => dedupeCams([...prev, ...newCams]));
|
||||
});
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<group ref={groupRef} name="Cam-Model-Group">
|
||||
{cams.map((cam, index) => (
|
||||
<primitive
|
||||
key={cam.uuid}
|
||||
//eslint-disable-next-line
|
||||
object={cam}
|
||||
visible={
|
||||
selectedUser?.name !== cam.userData.userName &&
|
||||
activeModule !== "visualization" &&
|
||||
!isPlaying
|
||||
}
|
||||
>
|
||||
<Html
|
||||
as="div"
|
||||
center
|
||||
zIndexRange={[1, 0]}
|
||||
sprite
|
||||
style={{
|
||||
color: "white",
|
||||
textAlign: "center",
|
||||
fontFamily: "Arial, sans-serif",
|
||||
display: `${activeModule !== "visualization" ? "" : "none"}`,
|
||||
opacity: `${selectedUser?.name !== cam.userData.userName && !isPlaying
|
||||
? 1
|
||||
: 0
|
||||
}`,
|
||||
transition: "opacity .2s ease",
|
||||
}}
|
||||
position={[-0.015, 0, 0.7]}
|
||||
>
|
||||
<CollabUserIcon
|
||||
userImage={cam.userData.userImage ?? ""}
|
||||
userName={cam.userData.userName}
|
||||
id={cam.userData._id}
|
||||
color={getAvatarColor(index, cam.userData.userName)}
|
||||
position={cam.position}
|
||||
rotation={cam.rotation}
|
||||
target={cam.userData.target}
|
||||
/>
|
||||
</Html>
|
||||
</primitive>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
export default CamModelsGroup;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user