drafting wall attach system
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
|||||||
useRegistry,
|
useRegistry,
|
||||||
useScene,
|
useScene,
|
||||||
useSpatialQuery,
|
useSpatialQuery,
|
||||||
|
WallEvent,
|
||||||
WallNode,
|
WallNode,
|
||||||
} from "@pascal-app/core";
|
} from "@pascal-app/core";
|
||||||
import { useViewer } from "@pascal-app/viewer";
|
import { useViewer } from "@pascal-app/viewer";
|
||||||
@@ -23,9 +24,33 @@ export const ItemTool: React.FC = () => {
|
|||||||
const { canPlace } = useSpatialQuery();
|
const { canPlace } = useSpatialQuery();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log("item-tool-reloaded");
|
||||||
if (!selectedItem) {
|
if (!selectedItem) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isOnWall = false;
|
||||||
|
let wallLocked = false;
|
||||||
|
|
||||||
|
const checkCanPlace = () => {
|
||||||
|
const currentLevelId = useViewer.getState().currentLevelId;
|
||||||
|
if (currentLevelId && draftItem.current) {
|
||||||
|
const placeable = canPlace(
|
||||||
|
currentLevelId,
|
||||||
|
[gridPosition.current.x, 0, gridPosition.current.z],
|
||||||
|
selectedItem.dimensions,
|
||||||
|
[0, 0, 0],
|
||||||
|
[draftItem.current.id],
|
||||||
|
);
|
||||||
|
if (placeable.valid) {
|
||||||
|
cursorRef.current.material.color.set("green");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
cursorRef.current.material.color.set("red");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
const createDraftItem = () => {
|
const createDraftItem = () => {
|
||||||
const { currentLevelId } = useViewer.getState();
|
const { currentLevelId } = useViewer.getState();
|
||||||
if (!currentLevelId) {
|
if (!currentLevelId) {
|
||||||
@@ -42,11 +67,13 @@ export const ItemTool: React.FC = () => {
|
|||||||
asset: selectedItem,
|
asset: selectedItem,
|
||||||
});
|
});
|
||||||
useScene.getState().createNode(draftItem.current, currentLevelId);
|
useScene.getState().createNode(draftItem.current, currentLevelId);
|
||||||
|
checkCanPlace();
|
||||||
};
|
};
|
||||||
createDraftItem();
|
createDraftItem();
|
||||||
|
|
||||||
const onGridMove = (event: GridEvent) => {
|
const onGridMove = (event: GridEvent) => {
|
||||||
if (!cursorRef.current) return;
|
if (!cursorRef.current) return;
|
||||||
|
if (isOnWall) return;
|
||||||
|
|
||||||
gridPosition.current.set(
|
gridPosition.current.set(
|
||||||
Math.round(event.position[0] * 2) / 2,
|
Math.round(event.position[0] * 2) / 2,
|
||||||
@@ -64,36 +91,19 @@ export const ItemTool: React.FC = () => {
|
|||||||
0,
|
0,
|
||||||
gridPosition.current.z,
|
gridPosition.current.z,
|
||||||
];
|
];
|
||||||
|
checkCanPlace();
|
||||||
const currentLevelId = useViewer.getState().currentLevelId;
|
|
||||||
if (currentLevelId) {
|
|
||||||
const placeable = canPlace(
|
|
||||||
currentLevelId,
|
|
||||||
[gridPosition.current.x, 0, gridPosition.current.z],
|
|
||||||
selectedItem.dimensions,
|
|
||||||
[0, 0, 0],
|
|
||||||
[draftItem.current.id],
|
|
||||||
);
|
|
||||||
if (placeable.valid) {
|
|
||||||
cursorRef.current.material.color.set("green");
|
|
||||||
} else {
|
|
||||||
cursorRef.current.material.color.set("red");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onGridClick = (event: GridEvent) => {
|
const onGridClick = (event: GridEvent) => {
|
||||||
const { currentLevelId } = useViewer.getState();
|
const { currentLevelId } = useViewer.getState();
|
||||||
|
if (isOnWall) return;
|
||||||
|
|
||||||
if (!currentLevelId || !draftItem.current) return;
|
if (!currentLevelId || !draftItem.current || !checkCanPlace()) return;
|
||||||
|
|
||||||
|
console.log("ongridclick action");
|
||||||
useScene.temporal.getState().resume();
|
useScene.temporal.getState().resume();
|
||||||
useScene.getState().updateNode(draftItem.current.id, {
|
useScene.getState().updateNode(draftItem.current.id, {
|
||||||
position: [
|
position: [gridPosition.current.x, 0, gridPosition.current.z],
|
||||||
gridPosition.current.x,
|
|
||||||
gridPosition.current.y,
|
|
||||||
gridPosition.current.z,
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
draftItem.current = null;
|
draftItem.current = null;
|
||||||
|
|
||||||
@@ -101,8 +111,95 @@ export const ItemTool: React.FC = () => {
|
|||||||
createDraftItem();
|
createDraftItem();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onWallEnter = (event: WallEvent) => {
|
||||||
|
console.log("Wall enter", event);
|
||||||
|
event.stopPropagation();
|
||||||
|
if (
|
||||||
|
draftItem.current?.asset.attachTo === "wall" ||
|
||||||
|
draftItem.current?.asset.attachTo === "wall-side"
|
||||||
|
) {
|
||||||
|
isOnWall = true;
|
||||||
|
gridPosition.current.set(
|
||||||
|
Math.round(event.localPosition[0] * 2) / 2,
|
||||||
|
Math.round(event.localPosition[1] * 2) / 2,
|
||||||
|
Math.round(event.localPosition[2] * 2) / 2,
|
||||||
|
);
|
||||||
|
draftItem.current.parentId = event.node.id;
|
||||||
|
useScene.getState().updateNode(draftItem.current.id, {
|
||||||
|
position: [
|
||||||
|
gridPosition.current.x,
|
||||||
|
gridPosition.current.y,
|
||||||
|
gridPosition.current.z,
|
||||||
|
],
|
||||||
|
parentId: event.node.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWallLeave = (event: WallEvent) => {
|
||||||
|
console.log("Wall leave", event);
|
||||||
|
isOnWall = false;
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWallClick = (event: WallEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (wallLocked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { currentLevelId } = useViewer.getState();
|
||||||
|
if (!currentLevelId || !draftItem.current || !checkCanPlace()) return;
|
||||||
|
|
||||||
|
wallLocked = true;
|
||||||
|
console.log("onwallclick action");
|
||||||
|
useScene.temporal.getState().resume();
|
||||||
|
useScene.getState().updateNode(draftItem.current.id, {
|
||||||
|
position: [
|
||||||
|
gridPosition.current.x,
|
||||||
|
gridPosition.current.y,
|
||||||
|
gridPosition.current.z,
|
||||||
|
],
|
||||||
|
parentId: event.node.id,
|
||||||
|
});
|
||||||
|
useScene.getState().dirtyNodes.add(event.node.id);
|
||||||
|
draftItem.current = null;
|
||||||
|
|
||||||
|
useScene.temporal.getState().pause();
|
||||||
|
createDraftItem();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWallMove = (event: WallEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
console.log("Wall move", event);
|
||||||
|
wallLocked = false;
|
||||||
|
if (!draftItem.current) return;
|
||||||
|
gridPosition.current.set(
|
||||||
|
Math.round(event.localPosition[0] * 2) / 2,
|
||||||
|
Math.round(event.localPosition[1] * 2) / 2,
|
||||||
|
Math.round(event.localPosition[2] * 2) / 2,
|
||||||
|
);
|
||||||
|
cursorRef.current.position.set(
|
||||||
|
event.position[0],
|
||||||
|
event.position[1],
|
||||||
|
event.position[2],
|
||||||
|
);
|
||||||
|
if (draftItem.current) {
|
||||||
|
draftItem.current.position = [
|
||||||
|
gridPosition.current.x,
|
||||||
|
gridPosition.current.y,
|
||||||
|
gridPosition.current.z,
|
||||||
|
];
|
||||||
|
useScene.getState().dirtyNodes.add(event.node.id);
|
||||||
|
checkCanPlace();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
emitter.on("grid:move", onGridMove);
|
emitter.on("grid:move", onGridMove);
|
||||||
emitter.on("grid:click", onGridClick);
|
emitter.on("grid:click", onGridClick);
|
||||||
|
emitter.on("wall:enter", onWallEnter);
|
||||||
|
emitter.on("wall:move", onWallMove);
|
||||||
|
emitter.on("wall:click", onWallClick);
|
||||||
|
emitter.on("wall:leave", onWallLeave);
|
||||||
|
|
||||||
const setupBoundingBox = () => {
|
const setupBoundingBox = () => {
|
||||||
const boxGeometry = new BoxGeometry(
|
const boxGeometry = new BoxGeometry(
|
||||||
@@ -119,8 +216,13 @@ export const ItemTool: React.FC = () => {
|
|||||||
if (draftItem.current) {
|
if (draftItem.current) {
|
||||||
useScene.getState().deleteNode(draftItem.current.id);
|
useScene.getState().deleteNode(draftItem.current.id);
|
||||||
}
|
}
|
||||||
|
useScene.temporal.getState().resume();
|
||||||
emitter.off("grid:move", onGridMove);
|
emitter.off("grid:move", onGridMove);
|
||||||
emitter.off("grid:click", onGridClick);
|
emitter.off("grid:click", onGridClick);
|
||||||
|
emitter.off("wall:enter", onWallEnter);
|
||||||
|
emitter.off("wall:leave", onWallLeave);
|
||||||
|
emitter.off("wall:click", onWallClick);
|
||||||
|
emitter.off("wall:move", onWallMove);
|
||||||
};
|
};
|
||||||
}, [selectedItem]);
|
}, [selectedItem]);
|
||||||
|
|
||||||
|
|||||||
@@ -325,10 +325,10 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
name: "Window",
|
name: "Window",
|
||||||
thumbnail: "/items/window-small/thumbnail.webp",
|
thumbnail: "/items/window-small/thumbnail.webp",
|
||||||
src: "/items/window-small/model.glb",
|
src: "/items/window-small/model.glb",
|
||||||
offset: [0, 0.5, 0],
|
offset: [0, 0, 0],
|
||||||
scale: [1, 1, 1],
|
scale: [1, 1, 1],
|
||||||
rotation: [0, Math.PI, 0],
|
rotation: [0, Math.PI, 0],
|
||||||
dimensions: [2, 1, 1],
|
dimensions: [1, 1.5, 0.1],
|
||||||
attachTo: "wall",
|
attachTo: "wall",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -336,10 +336,10 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
name: "Window",
|
name: "Window",
|
||||||
thumbnail: "/items/window-small-2/thumbnail.webp",
|
thumbnail: "/items/window-small-2/thumbnail.webp",
|
||||||
src: "/items/window-small-2/model.glb",
|
src: "/items/window-small-2/model.glb",
|
||||||
offset: [0, 0.5, 0],
|
offset: [0, 0, 0],
|
||||||
scale: [1, 1, 1],
|
scale: [1, 1, 1],
|
||||||
rotation: [0, Math.PI, 0],
|
rotation: [0, Math.PI, 0],
|
||||||
dimensions: [2, 1, 1],
|
dimensions: [1, 1, 0.1],
|
||||||
attachTo: "wall",
|
attachTo: "wall",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface GridEvent {
|
|||||||
export interface NodeEvent<T extends AnyNode = AnyNode> {
|
export interface NodeEvent<T extends AnyNode = AnyNode> {
|
||||||
node: T;
|
node: T;
|
||||||
position: [number, number, number];
|
position: [number, number, number];
|
||||||
|
localPosition: [number, number, number];
|
||||||
normal?: [number, number, number];
|
normal?: [number, number, number];
|
||||||
stopPropagation: () => void;
|
stopPropagation: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ const useScene = create<SceneState>()(
|
|||||||
type: "item",
|
type: "item",
|
||||||
name: "Window",
|
name: "Window",
|
||||||
position: [2.5, 0.5, 0],
|
position: [2.5, 0.5, 0],
|
||||||
|
parentId: wall0.id,
|
||||||
asset: {
|
asset: {
|
||||||
name: "Round Window",
|
name: "Round Window",
|
||||||
thumbnail: "/items/window-round/thumbnail.png",
|
thumbnail: "/items/window-round/thumbnail.png",
|
||||||
|
|||||||
@@ -47,12 +47,20 @@ function updateWallGeometry(wallId: string) {
|
|||||||
.map((childId) => useScene.getState().nodes[childId])
|
.map((childId) => useScene.getState().nodes[childId])
|
||||||
.filter((n): n is AnyNode => n !== undefined);
|
.filter((n): n is AnyNode => n !== undefined);
|
||||||
|
|
||||||
// Perform the Extrusion with Holes logic we discussed
|
// Generate visual geometry with holes
|
||||||
const newGeo = generateExtrudedWall(node, childrenNodes);
|
const newGeo = generateExtrudedWall(node, childrenNodes);
|
||||||
|
|
||||||
mesh.geometry.dispose();
|
mesh.geometry.dispose();
|
||||||
mesh.geometry = newGeo;
|
mesh.geometry = newGeo;
|
||||||
|
|
||||||
|
// Update collision mesh with solid geometry (no holes)
|
||||||
|
const collisionMesh = mesh.getObjectByName("collision-mesh") as THREE.Mesh;
|
||||||
|
if (collisionMesh) {
|
||||||
|
const collisionGeo = generateExtrudedWall(node, []); // No children = no holes
|
||||||
|
collisionMesh.geometry.dispose();
|
||||||
|
collisionMesh.geometry = collisionGeo;
|
||||||
|
}
|
||||||
|
|
||||||
mesh.position.set(node.start[0], 0, node.start[1]);
|
mesh.position.set(node.start[0], 0, node.start[1]);
|
||||||
|
|
||||||
// Rotate mesh to look at 'end' point
|
// Rotate mesh to look at 'end' point
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { emitter, ItemNode, useRegistry } from "@pascal-app/core";
|
import {
|
||||||
|
AnyNodeId,
|
||||||
|
emitter,
|
||||||
|
ItemNode,
|
||||||
|
useRegistry,
|
||||||
|
useScene,
|
||||||
|
} from "@pascal-app/core";
|
||||||
import { Clone } from "@react-three/drei/core/Clone";
|
import { Clone } from "@react-three/drei/core/Clone";
|
||||||
import { useGLTF } from "@react-three/drei/core/Gltf";
|
import { useGLTF } from "@react-three/drei/core/Gltf";
|
||||||
import { ThreeEvent } from "@react-three/fiber/dist/declarations/src/core/events";
|
import { ThreeEvent } from "@react-three/fiber/dist/declarations/src/core/events";
|
||||||
import { Suspense, useCallback, useRef } from "react";
|
import { Suspense, useCallback, useEffect, useRef } from "react";
|
||||||
import { Group } from "three";
|
import { Group } from "three";
|
||||||
import { useNodeEvents } from "../../../hooks/use-node-events";
|
import { useNodeEvents } from "../../../hooks/use-node-events";
|
||||||
|
|
||||||
@@ -29,6 +35,11 @@ const ModelRenderer = ({ node }: { node: ItemNode }) => {
|
|||||||
|
|
||||||
const handlers = useNodeEvents(node, "item");
|
const handlers = useNodeEvents(node, "item");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!node.parentId) return;
|
||||||
|
useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Clone
|
<Clone
|
||||||
object={scene}
|
object={scene}
|
||||||
|
|||||||
@@ -12,15 +12,13 @@ export const WallRenderer = ({ node }: { node: WallNode }) => {
|
|||||||
const handlers = useNodeEvents(node, "wall");
|
const handlers = useNodeEvents(node, "wall");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh
|
<mesh ref={ref} castShadow receiveShadow>
|
||||||
ref={ref}
|
|
||||||
castShadow
|
|
||||||
receiveShadow
|
|
||||||
{...handlers}
|
|
||||||
>
|
|
||||||
{/* WallSystem will replace this geometry in the next frame */}
|
{/* WallSystem will replace this geometry in the next frame */}
|
||||||
<boxGeometry args={[0, 0, 0]} />
|
<boxGeometry args={[0, 0, 0]} />
|
||||||
<meshStandardMaterial color="lightgray" />
|
<meshStandardMaterial color="lightgray" />
|
||||||
|
<mesh name="collision-mesh" {...handlers} visible={false}>
|
||||||
|
<boxGeometry args={[0, 0, 0]} />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
{node.children.map((childId) => (
|
{node.children.map((childId) => (
|
||||||
<NodeRenderer key={childId} nodeId={childId} />
|
<NodeRenderer key={childId} nodeId={childId} />
|
||||||
|
|||||||
@@ -18,12 +18,17 @@ type NodeConfig = {
|
|||||||
|
|
||||||
type NodeType = keyof NodeConfig;
|
type NodeType = keyof NodeConfig;
|
||||||
|
|
||||||
export function useNodeEvents<T extends NodeType>(node: NodeConfig[T]["node"], type: T) {
|
export function useNodeEvents<T extends NodeType>(
|
||||||
|
node: NodeConfig[T]["node"],
|
||||||
|
type: T,
|
||||||
|
) {
|
||||||
const emit = (suffix: EventSuffix, e: ThreeEvent<PointerEvent>) => {
|
const emit = (suffix: EventSuffix, e: ThreeEvent<PointerEvent>) => {
|
||||||
const eventKey = `${type}:${suffix}` as `${T}:${EventSuffix}`;
|
const eventKey = `${type}:${suffix}` as `${T}:${EventSuffix}`;
|
||||||
|
const localPoint = e.object.worldToLocal(e.point.clone());
|
||||||
const payload = {
|
const payload = {
|
||||||
node,
|
node,
|
||||||
position: [e.point.x, e.point.y, e.point.z],
|
position: [e.point.x, e.point.y, e.point.z],
|
||||||
|
localPosition: [localPoint.x, localPoint.y, localPoint.z],
|
||||||
normal: e.face
|
normal: e.face
|
||||||
? [e.face.normal.x, e.face.normal.y, e.face.normal.z]
|
? [e.face.normal.x, e.face.normal.y, e.face.normal.z]
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user