Enhance builder functionality by implementing drag-and-drop for lines and points, adding hover effects, and improving wall distance display in the UI.
This commit is contained in:
@@ -48,14 +48,11 @@ function AisleInstances() {
|
||||
|
||||
{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
|
||||
@@ -68,7 +65,6 @@ function AisleInstances() {
|
||||
</div>
|
||||
</Html>
|
||||
}
|
||||
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -104,7 +104,7 @@ export default function Builder() {
|
||||
const { setWalls } = useWalls();
|
||||
const { refTextupdate, setRefTextUpdate } = useRefTextUpdate();
|
||||
const { projectId } = useParams();
|
||||
const { setHoveredPoint } = useBuilderStore();
|
||||
const { setHoveredPoint, setHoveredLine } = useBuilderStore();
|
||||
const { userId, organization } = getUserData();
|
||||
|
||||
// const loader = new GLTFLoader();
|
||||
@@ -127,7 +127,9 @@ export default function Builder() {
|
||||
dragPointControls
|
||||
);
|
||||
} else {
|
||||
setHoveredLine(null);
|
||||
setHoveredPoint(null);
|
||||
state.gl.domElement.style.cursor = 'default';
|
||||
setToolMode('cursor');
|
||||
loadWalls(lines, setWalls);
|
||||
setUpdateScene(true);
|
||||
|
||||
@@ -384,13 +384,7 @@ const ZoneGroup: React.FC = () => {
|
||||
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();
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
import * as THREE from 'three';
|
||||
import { useMemo } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import * as Constants from '../../../types/world/worldConstants';
|
||||
import { Tube } from '@react-three/drei';
|
||||
import { DragControls, Tube } from '@react-three/drei';
|
||||
import { useToolMode } from '../../../store/builder/store';
|
||||
import { useBuilderStore } from '../../../store/builder/useBuilderStore';
|
||||
import { useWallStore } from '../../../store/builder/useWallStore';
|
||||
import { useThree } from '@react-three/fiber';
|
||||
|
||||
interface LineProps {
|
||||
points: [Point, Point];
|
||||
}
|
||||
|
||||
function Line({ points }: Readonly<LineProps>) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const { raycaster, camera, pointer, gl } = useThree();
|
||||
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []);
|
||||
const [isDeletable, setIsDeletable] = useState(false);
|
||||
const { toolMode } = useToolMode();
|
||||
const { removeWallByPoints, setPosition } = useWallStore();
|
||||
const [dragOffset, setDragOffset] = useState<THREE.Vector3 | null>(null);
|
||||
const { hoveredLine, setHoveredLine, hoveredPoint } = useBuilderStore();
|
||||
|
||||
const path = useMemo(() => {
|
||||
const [start, end] = points.map(p => new THREE.Vector3(...p.position));
|
||||
return new THREE.LineCurve3(start, end);
|
||||
@@ -44,16 +57,119 @@ function Line({ points }: Readonly<LineProps>) {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (toolMode === '2D-Delete') {
|
||||
if (isHovered && !hoveredPoint) {
|
||||
setIsDeletable(true);
|
||||
} else {
|
||||
setIsDeletable(false);
|
||||
}
|
||||
} else {
|
||||
setIsDeletable(false);
|
||||
}
|
||||
}, [isHovered, colors.defaultLineColor, colors.defaultDeleteColor, toolMode, hoveredPoint]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hoveredLine && (hoveredLine[0].pointUuid !== points[0].pointUuid || hoveredLine[1].pointUuid !== points[1].pointUuid)) {
|
||||
setIsHovered(false);
|
||||
}
|
||||
}, [hoveredLine])
|
||||
|
||||
const handlePointClick = (points: [Point, Point]) => {
|
||||
if (toolMode === '2D-Delete') {
|
||||
if (points[0].pointType === 'Wall' && points[1].pointType === 'Wall') {
|
||||
removeWallByPoints(points);
|
||||
}
|
||||
gl.domElement.style.cursor = 'default';
|
||||
}
|
||||
}
|
||||
|
||||
const handleDrag = (points: [Point, Point]) => {
|
||||
if (toolMode === 'move' && isHovered && dragOffset) {
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
|
||||
if (hit) {
|
||||
gl.domElement.style.cursor = 'move';
|
||||
const positionWithOffset = new THREE.Vector3().addVectors(hit, dragOffset);
|
||||
|
||||
const start = new THREE.Vector3(...points[0].position);
|
||||
const end = new THREE.Vector3(...points[1].position);
|
||||
const midPoint = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5);
|
||||
|
||||
const delta = new THREE.Vector3().subVectors(positionWithOffset, midPoint);
|
||||
|
||||
const newStart = new THREE.Vector3().addVectors(start, delta);
|
||||
const newEnd = new THREE.Vector3().addVectors(end, delta);
|
||||
|
||||
setPosition(points[0].pointUuid, [newStart.x, newStart.y, newStart.z]);
|
||||
setPosition(points[1].pointUuid, [newEnd.x, newEnd.y, newEnd.z]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragStart = (points: [Point, Point]) => {
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
|
||||
if (hit && !hoveredPoint) {
|
||||
const start = new THREE.Vector3(...points[0].position);
|
||||
const end = new THREE.Vector3(...points[1].position);
|
||||
const midPoint = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5);
|
||||
|
||||
const offset = new THREE.Vector3().subVectors(midPoint, hit);
|
||||
setDragOffset(offset);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = (points: [Point, Point]) => {
|
||||
gl.domElement.style.cursor = 'default';
|
||||
setDragOffset(null);
|
||||
if (toolMode !== 'move') return;
|
||||
if (points[0].pointType === 'Wall' && points[1].pointType === 'Wall') {
|
||||
// console.log('Wall after drag: ', points);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tube
|
||||
name={`${points[0].pointType}-Line`}
|
||||
key={`${points[0].pointUuid}-${points[1].pointUuid}`}
|
||||
uuid={`${points[0].pointUuid}-${points[1].pointUuid}`}
|
||||
userData={{ points, path }}
|
||||
args={[path, Constants.lineConfig.tubularSegments, Constants.lineConfig.radius, Constants.lineConfig.radialSegments, false]}
|
||||
<DragControls
|
||||
axisLock="y"
|
||||
autoTransform={false}
|
||||
onDragStart={() => handleDragStart(points)}
|
||||
onDrag={() => handleDrag(points)}
|
||||
onDragEnd={() => handleDragEnd(points)}
|
||||
>
|
||||
<meshStandardMaterial color={colors.defaultLineColor} />
|
||||
</Tube>
|
||||
<Tube
|
||||
name={`${points[0].pointType}-Line`}
|
||||
key={`${points[0].pointUuid}-${points[1].pointUuid}`}
|
||||
uuid={`${points[0].pointUuid}-${points[1].pointUuid}`}
|
||||
userData={{ points, path }}
|
||||
args={[path, Constants.lineConfig.tubularSegments, Constants.lineConfig.radius, Constants.lineConfig.radialSegments, false]}
|
||||
onClick={() => {
|
||||
handlePointClick(points);
|
||||
}}
|
||||
onPointerOver={() => {
|
||||
if (!hoveredLine) {
|
||||
setHoveredLine(points);
|
||||
setIsHovered(true)
|
||||
if (toolMode === 'move' && !hoveredPoint) {
|
||||
gl.domElement.style.cursor = 'pointer';
|
||||
}
|
||||
}
|
||||
}}
|
||||
onPointerOut={() => {
|
||||
if (hoveredLine) {
|
||||
setHoveredLine(null);
|
||||
gl.domElement.style.cursor = 'default';
|
||||
}
|
||||
setIsHovered(false)
|
||||
}}
|
||||
>
|
||||
<meshStandardMaterial color={isDeletable ? colors.defaultDeleteColor : colors.defaultLineColor} />
|
||||
</Tube>
|
||||
</DragControls >
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@ import { useSceneContext } from '../../scene/sceneContext';
|
||||
|
||||
function Point({ point }: { readonly point: Point }) {
|
||||
const materialRef = useRef<THREE.ShaderMaterial>(null);
|
||||
const { raycaster, camera, pointer } = useThree();
|
||||
const { raycaster, camera, pointer, gl } = useThree();
|
||||
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [dragOffset, setDragOffset] = useState<THREE.Vector3 | null>(null);
|
||||
const { toolMode } = useToolMode();
|
||||
const { aisleStore } = useSceneContext();
|
||||
const { setPosition: setAislePosition, removePoint: removeAislePoint, getAislesByPointId } = aisleStore();
|
||||
@@ -91,33 +92,45 @@ function Point({ point }: { readonly point: Point }) {
|
||||
}), [colors.defaultInnerColor, colors.defaultOuterColor]);
|
||||
|
||||
const handleDrag = (point: Point) => {
|
||||
if (toolMode === 'move' && isHovered) {
|
||||
if (toolMode === 'move' && isHovered && dragOffset) {
|
||||
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 = snapAisleAngle(newPosition);
|
||||
const finalSnappedPosition = snapAislePoint(aisleSnappedPosition.position);
|
||||
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
|
||||
setAislePosition(point.pointUuid, finalSnappedPosition.position);
|
||||
if (hit) {
|
||||
gl.domElement.style.cursor = 'move';
|
||||
const positionWithOffset = new THREE.Vector3().addVectors(hit, dragOffset);
|
||||
const newPosition: [number, number, number] = [positionWithOffset.x, positionWithOffset.y, positionWithOffset.z];
|
||||
|
||||
}
|
||||
} else if (point.pointType === 'Wall') {
|
||||
if (position) {
|
||||
const newPosition: [number, number, number] = [position.x, position.y, position.z];
|
||||
const wallSnappedPosition = snapWallAngle(newPosition);
|
||||
const finalSnappedPosition = snapWallPoint(wallSnappedPosition.position);
|
||||
|
||||
setWallPosition(point.pointUuid, finalSnappedPosition.position);
|
||||
if (point.pointType === 'Aisle') {
|
||||
const aisleSnapped = snapAisleAngle(newPosition);
|
||||
const finalSnapped = snapAislePoint(aisleSnapped.position);
|
||||
setAislePosition(point.pointUuid, finalSnapped.position);
|
||||
} else if (point.pointType === 'Wall') {
|
||||
const wallSnapped = snapWallAngle(newPosition);
|
||||
const finalSnapped = snapWallPoint(wallSnapped.position);
|
||||
setWallPosition(point.pointUuid, finalSnapped.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragStart = (point: Point) => {
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
|
||||
if (hit) {
|
||||
const currentPosition = new THREE.Vector3(...point.position);
|
||||
const offset = new THREE.Vector3().subVectors(currentPosition, hit);
|
||||
setDragOffset(offset);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = (point: Point) => {
|
||||
if (toolMode === '2D-Delete') return;
|
||||
gl.domElement.style.cursor = 'default';
|
||||
setDragOffset(null);
|
||||
if (toolMode !== 'move') return;
|
||||
if (point.pointType === 'Aisle') {
|
||||
const updatedAisles = getAislesByPointId(point.pointUuid);
|
||||
if (updatedAisles.length > 0 && projectId) {
|
||||
@@ -148,6 +161,7 @@ function Point({ point }: { readonly point: Point }) {
|
||||
setHoveredPoint(null);
|
||||
}
|
||||
}
|
||||
gl.domElement.style.cursor = 'default';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,8 +179,9 @@ function Point({ point }: { readonly point: Point }) {
|
||||
<DragControls
|
||||
axisLock='y'
|
||||
autoTransform={false}
|
||||
onDrag={() => { handleDrag(point) }}
|
||||
onDragEnd={() => { handleDragEnd(point) }}
|
||||
onDragStart={() => handleDragStart(point)}
|
||||
onDrag={() => handleDrag(point)}
|
||||
onDragEnd={() => handleDragEnd(point)}
|
||||
>
|
||||
<mesh
|
||||
key={point.pointUuid}
|
||||
@@ -179,12 +194,16 @@ function Point({ point }: { readonly point: Point }) {
|
||||
onPointerOver={() => {
|
||||
if (!hoveredPoint) {
|
||||
setHoveredPoint(point);
|
||||
setIsHovered(true)
|
||||
setIsHovered(true);
|
||||
if (toolMode === 'move') {
|
||||
gl.domElement.style.cursor = 'pointer';
|
||||
}
|
||||
}
|
||||
}}
|
||||
onPointerOut={() => {
|
||||
if (hoveredPoint) {
|
||||
setHoveredPoint(null);
|
||||
gl.domElement.style.cursor = 'default';
|
||||
}
|
||||
setIsHovered(false)
|
||||
}}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import React, { useEffect, useMemo } 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 { Geometry } from '@react-three/csg';
|
||||
import { Vector3 } from 'three';
|
||||
import { Html } from '@react-three/drei';
|
||||
|
||||
function WallInstances() {
|
||||
const { walls } = useWallStore();
|
||||
@@ -34,21 +36,12 @@ function WallInstances() {
|
||||
<>
|
||||
|
||||
{!toggleView && (
|
||||
|
||||
<mesh name='Walls-Group'>
|
||||
{/* <Base name="base" geometry={box} scale={[3, 3, 3]} /> */}
|
||||
|
||||
<Geometry useGroups>
|
||||
{walls.map((wall) => (
|
||||
<WallInstance key={wall.wallUuid} wall={wall} />
|
||||
))}
|
||||
</Geometry>
|
||||
|
||||
{/* <Subtraction rotation={[0, Math.PI / 2, 0]} position={[-1.425, -0.45, 0]} scale={[1, 3, 1]}>
|
||||
<Geometry>
|
||||
<Base geometry={box} />
|
||||
</Geometry>
|
||||
</Subtraction> */}
|
||||
</mesh>
|
||||
)}
|
||||
|
||||
@@ -61,11 +54,42 @@ function WallInstances() {
|
||||
</group>
|
||||
|
||||
<group name='Wall-Lines-Group'>
|
||||
|
||||
{walls.map((wall) => (
|
||||
<React.Fragment key={wall.wallUuid}>
|
||||
<Line points={wall.points} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
{walls.map((wall) => {
|
||||
const textPosition = new Vector3().addVectors(new Vector3(...wall.points[0].position), new Vector3(...wall.points[1].position)).divideScalar(2);
|
||||
const distance = new Vector3(...wall.points[0].position).distanceTo(new Vector3(...wall.points[1].position));
|
||||
|
||||
return (
|
||||
< React.Fragment key={wall.wallUuid}>
|
||||
{toggleView &&
|
||||
<Html
|
||||
key={`${wall.points[0].pointUuid}_${wall.points[1].pointUuid}`}
|
||||
userData={wall}
|
||||
position={[textPosition.x, 1, textPosition.z]}
|
||||
wrapperClass="distance-text-wrapper"
|
||||
className="distance-text"
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
sprite
|
||||
>
|
||||
<div
|
||||
key={wall.wallUuid}
|
||||
className={`distance ${wall.wallUuid}`}
|
||||
>
|
||||
{distance.toFixed(2)} m
|
||||
</div>
|
||||
</Html>
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
|
||||
</group>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,8 @@ interface BuilderState {
|
||||
snappedPoint: Point | null;
|
||||
snappedPosition: [number, number, number] | null;
|
||||
|
||||
hoveredLine: [Point, Point] | null;
|
||||
|
||||
// Wall
|
||||
|
||||
wallThickness: number;
|
||||
@@ -47,6 +49,8 @@ interface BuilderState {
|
||||
setSnappedPoint: (point: Point | null) => void;
|
||||
setSnappedPosition: (position: [number, number, number] | null) => void;
|
||||
|
||||
setHoveredLine: (line: [Point, Point] | null) => void;
|
||||
|
||||
setSelectedAisle: (aisle: Object3D | null) => void;
|
||||
|
||||
setAisleType: (type: AisleTypes) => void;
|
||||
@@ -81,6 +85,8 @@ export const useBuilderStore = create<BuilderState>()(
|
||||
snappedPoint: null,
|
||||
snappedPosition: null,
|
||||
|
||||
hoveredLine: null,
|
||||
|
||||
// Wall
|
||||
|
||||
wallThickness: 0.5,
|
||||
@@ -128,6 +134,12 @@ export const useBuilderStore = create<BuilderState>()(
|
||||
});
|
||||
},
|
||||
|
||||
setHoveredLine: (line: [Point, Point] | null) => {
|
||||
set((state) => {
|
||||
state.hoveredLine = line;
|
||||
})
|
||||
},
|
||||
|
||||
setSnappedPoint: (point: Point | null) => {
|
||||
set((state) => {
|
||||
state.snappedPoint = point;
|
||||
|
||||
@@ -7,6 +7,7 @@ interface WallStore {
|
||||
addWall: (wall: Wall) => void;
|
||||
updateWall: (uuid: string, updated: Partial<Wall>) => void;
|
||||
removeWall: (uuid: string) => void;
|
||||
removeWallByPoints: (Points: [Point, Point]) => Wall | undefined;
|
||||
addDecal: (wallUuid: string, decal: Decal) => void;
|
||||
updateDecal: (decalUuid: string, decal: Decal) => void;
|
||||
removeDecal: (decalUuid: string) => void;
|
||||
@@ -48,6 +49,26 @@ export const useWallStore = create<WallStore>()(
|
||||
state.walls = state.walls.filter(w => w.wallUuid !== uuid);
|
||||
}),
|
||||
|
||||
removeWallByPoints: (points) => {
|
||||
let removedWall: Wall | undefined;
|
||||
const [pointA, pointB] = points;
|
||||
|
||||
set((state) => {
|
||||
state.walls = state.walls.filter(wall => {
|
||||
const wallPoints = wall.points.map(p => p.pointUuid);
|
||||
const hasBothPoints = wallPoints.includes(pointA.pointUuid) && wallPoints.includes(pointB.pointUuid);
|
||||
|
||||
if (hasBothPoints) {
|
||||
removedWall = JSON.parse(JSON.stringify(wall));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
return removedWall;
|
||||
},
|
||||
|
||||
addDecal: (wallUuid, decal) => set((state) => {
|
||||
const wallToUpdate = state.walls.find(w => w.wallUuid === wallUuid);
|
||||
if (wallToUpdate) {
|
||||
|
||||
Reference in New Issue
Block a user