biome
This commit is contained in:
+15
-17
@@ -1,31 +1,29 @@
|
||||
import type { Metadata } from "next";
|
||||
import localFont from "next/font/local";
|
||||
import "./globals.css";
|
||||
import type { Metadata } from 'next'
|
||||
import localFont from 'next/font/local'
|
||||
import './globals.css'
|
||||
|
||||
const geistSans = localFont({
|
||||
src: "./fonts/GeistVF.woff",
|
||||
variable: "--font-geist-sans",
|
||||
});
|
||||
src: './fonts/GeistVF.woff',
|
||||
variable: '--font-geist-sans',
|
||||
})
|
||||
const geistMono = localFont({
|
||||
src: "./fonts/GeistMonoVF.woff",
|
||||
variable: "--font-geist-mono",
|
||||
});
|
||||
src: './fonts/GeistMonoVF.woff',
|
||||
variable: '--font-geist-mono',
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${geistSans.variable} ${geistMono.variable}`}>
|
||||
{children}
|
||||
</body>
|
||||
<body className={`${geistSans.variable} ${geistMono.variable}`}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Editor from "../components/editor";
|
||||
import Editor from '../components/editor'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
@@ -7,5 +7,5 @@ export default function Home() {
|
||||
<Editor />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,46 +1,38 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { sceneRegistry } from "@pascal-app/core";
|
||||
import { useViewer } from "@pascal-app/viewer";
|
||||
import { CameraControls, CameraControlsImpl } from "@react-three/drei";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { Vector3 } from "three";
|
||||
import { sceneRegistry } from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { CameraControls, CameraControlsImpl } from '@react-three/drei'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { Vector3 } from 'three'
|
||||
|
||||
const currentTarget = new Vector3();
|
||||
const currentTarget = new Vector3()
|
||||
|
||||
export const CustomCameraControls = () => {
|
||||
const controls = useRef<CameraControlsImpl>(null!);
|
||||
const currentLevelId = useViewer((state) => state.selection.levelId);
|
||||
const firstLoad = useRef(true);
|
||||
const controls = useRef<CameraControlsImpl>(null!)
|
||||
const currentLevelId = useViewer((state) => state.selection.levelId)
|
||||
const firstLoad = useRef(true)
|
||||
|
||||
useEffect(() => {
|
||||
let targetY = 0;
|
||||
let targetY = 0
|
||||
if (currentLevelId) {
|
||||
const levelMesh = sceneRegistry.nodes.get(currentLevelId);
|
||||
const levelMesh = sceneRegistry.nodes.get(currentLevelId)
|
||||
if (levelMesh) {
|
||||
targetY = levelMesh.position.y;
|
||||
targetY = levelMesh.position.y
|
||||
}
|
||||
}
|
||||
if (firstLoad.current) {
|
||||
firstLoad.current = false;
|
||||
(controls.current as CameraControlsImpl).setLookAt(
|
||||
20,
|
||||
20,
|
||||
20,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
);
|
||||
firstLoad.current = false
|
||||
;(controls.current as CameraControlsImpl).setLookAt(20, 20, 20, 0, 0, 0, true)
|
||||
}
|
||||
(controls.current as CameraControlsImpl).getTarget(currentTarget);
|
||||
(controls.current as CameraControlsImpl).moveTo(
|
||||
;(controls.current as CameraControlsImpl).getTarget(currentTarget)
|
||||
;(controls.current as CameraControlsImpl).moveTo(
|
||||
currentTarget.x,
|
||||
targetY,
|
||||
currentTarget.z,
|
||||
true,
|
||||
);
|
||||
}, [currentLevelId]);
|
||||
)
|
||||
}, [currentLevelId])
|
||||
|
||||
// Configure mouse buttons based on control mode and camera mode
|
||||
const mouseButtons = useMemo(() => {
|
||||
@@ -49,15 +41,15 @@ export const CustomCameraControls = () => {
|
||||
// cameraMode === 'orthographic'
|
||||
// ? CameraControlsImpl.ACTION.ZOOM
|
||||
// : CameraControlsImpl.ACTION.DOLLY
|
||||
const wheelAction = CameraControlsImpl.ACTION.DOLLY;
|
||||
const wheelAction = CameraControlsImpl.ACTION.DOLLY
|
||||
|
||||
return {
|
||||
left: CameraControlsImpl.ACTION.NONE,
|
||||
middle: CameraControlsImpl.ACTION.SCREEN_PAN,
|
||||
right: CameraControlsImpl.ACTION.ROTATE,
|
||||
wheel: wheelAction,
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <CameraControls ref={controls} mouseButtons={mouseButtons} />;
|
||||
};
|
||||
return <CameraControls ref={controls} mouseButtons={mouseButtons} />
|
||||
}
|
||||
|
||||
@@ -1,42 +1,23 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import {
|
||||
emitter,
|
||||
initSpatialGridSync,
|
||||
ItemNode,
|
||||
sceneRegistry,
|
||||
useRegistry,
|
||||
useScene,
|
||||
WallNode,
|
||||
} from "@pascal-app/core";
|
||||
import { useGridEvents, useViewer, Viewer } from "@pascal-app/viewer";
|
||||
import { initSpatialGridSync, sceneRegistry, useScene } from '@pascal-app/core'
|
||||
import { useGridEvents, useViewer, Viewer } from '@pascal-app/viewer'
|
||||
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { Color, MathUtils, Mesh, Object3D, Vector3 } from "three";
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { MathUtils, type Mesh } from 'three'
|
||||
|
||||
import {
|
||||
color,
|
||||
float,
|
||||
fract,
|
||||
fwidth,
|
||||
mix,
|
||||
oscSine,
|
||||
pass,
|
||||
positionLocal,
|
||||
time,
|
||||
uniform,
|
||||
} from "three/tsl";
|
||||
import { MeshBasicNodeMaterial, PostProcessing } from "three/webgpu";
|
||||
import { ActionMenu } from "../ui/action-menu";
|
||||
import { ToolManager } from "../tools/tool-manager";
|
||||
import { AppSidebar } from "../ui/sidebar/app-sidebar";
|
||||
import { SidebarProvider } from "../ui/primitives/sidebar";
|
||||
import { CustomCameraControls } from "./custom-camera-controls";
|
||||
import { SelectionManager } from "./selection-manager";
|
||||
import { color, float, fract, fwidth, mix, positionLocal } from 'three/tsl'
|
||||
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
||||
import { ToolManager } from '../tools/tool-manager'
|
||||
import { ActionMenu } from '../ui/action-menu'
|
||||
import { SidebarProvider } from '../ui/primitives/sidebar'
|
||||
import { AppSidebar } from '../ui/sidebar/app-sidebar'
|
||||
import { CustomCameraControls } from './custom-camera-controls'
|
||||
import { SelectionManager } from './selection-manager'
|
||||
|
||||
initSpatialGridSync();
|
||||
useScene.getState().loadScene();
|
||||
initSpatialGridSync()
|
||||
useScene.getState().loadScene()
|
||||
|
||||
export default function Editor() {
|
||||
return (
|
||||
@@ -57,18 +38,18 @@ export default function Editor() {
|
||||
<CustomCameraControls />
|
||||
</Viewer>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const TestUndo = () => {
|
||||
const { undo, redo, futureStates, pastStates } = useScene.temporal.getState();
|
||||
const { undo, redo, futureStates, pastStates } = useScene.temporal.getState()
|
||||
|
||||
return (
|
||||
<div className="absolute top-4 right-4 z-10 flex gap-2">
|
||||
<button
|
||||
className="px-4 py-2 rounded bg-white"
|
||||
onClick={() => {
|
||||
undo();
|
||||
undo()
|
||||
}}
|
||||
>
|
||||
Undo
|
||||
@@ -76,86 +57,86 @@ const TestUndo = () => {
|
||||
<button
|
||||
className="px-4 py-2 rounded bg-white"
|
||||
onClick={() => {
|
||||
redo();
|
||||
redo()
|
||||
}}
|
||||
>
|
||||
Redo
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const Grid = ({
|
||||
cellSize = 0.5,
|
||||
cellThickness = 0.5,
|
||||
cellColor = "#888888",
|
||||
cellColor = '#888888',
|
||||
sectionSize = 1,
|
||||
sectionThickness = 1,
|
||||
sectionColor = "#000000",
|
||||
sectionColor = '#000000',
|
||||
fadeDistance = 100,
|
||||
fadeStrength = 1,
|
||||
}: {
|
||||
cellSize?: number;
|
||||
cellThickness?: number;
|
||||
cellColor?: string;
|
||||
sectionSize?: number;
|
||||
sectionThickness?: number;
|
||||
sectionColor?: string;
|
||||
fadeDistance?: number;
|
||||
fadeStrength?: number;
|
||||
cellSize?: number
|
||||
cellThickness?: number
|
||||
cellColor?: string
|
||||
sectionSize?: number
|
||||
sectionThickness?: number
|
||||
sectionColor?: string
|
||||
fadeDistance?: number
|
||||
fadeStrength?: number
|
||||
}) => {
|
||||
const material = useMemo(() => {
|
||||
// Use xy since plane geometry is in XY space (before rotation)
|
||||
const pos = positionLocal.xy;
|
||||
const pos = positionLocal.xy
|
||||
|
||||
// Grid line function using fwidth for anti-aliasing
|
||||
// Returns 1 on grid lines, 0 elsewhere
|
||||
const getGrid = (size: number, thickness: number) => {
|
||||
const r = pos.div(size);
|
||||
const fw = fwidth(r);
|
||||
const r = pos.div(size)
|
||||
const fw = fwidth(r)
|
||||
// Distance to nearest grid line for each axis
|
||||
const grid = fract(r.sub(0.5)).sub(0.5).abs();
|
||||
const grid = fract(r.sub(0.5)).sub(0.5).abs()
|
||||
// Anti-aliased step: divide by fwidth and clamp
|
||||
const lineX = float(1).sub(
|
||||
grid.x
|
||||
.div(fw.x)
|
||||
.add(1 - thickness)
|
||||
.min(1),
|
||||
);
|
||||
)
|
||||
const lineY = float(1).sub(
|
||||
grid.y
|
||||
.div(fw.y)
|
||||
.add(1 - thickness)
|
||||
.min(1),
|
||||
);
|
||||
)
|
||||
// Combine both axes - max gives us lines in both directions
|
||||
return lineX.max(lineY);
|
||||
};
|
||||
return lineX.max(lineY)
|
||||
}
|
||||
|
||||
const g1 = getGrid(cellSize, cellThickness);
|
||||
const g2 = getGrid(sectionSize, sectionThickness);
|
||||
const g1 = getGrid(cellSize, cellThickness)
|
||||
const g2 = getGrid(sectionSize, sectionThickness)
|
||||
|
||||
// Distance fade from center
|
||||
const dist = pos.length();
|
||||
const fade = float(1).sub(dist.div(fadeDistance).min(1)).pow(fadeStrength);
|
||||
const dist = pos.length()
|
||||
const fade = float(1).sub(dist.div(fadeDistance).min(1)).pow(fadeStrength)
|
||||
|
||||
// Mix colors based on section grid
|
||||
const gridColor = mix(
|
||||
color(cellColor),
|
||||
color(sectionColor),
|
||||
float(sectionThickness).mul(g2).min(1),
|
||||
);
|
||||
)
|
||||
|
||||
// Combined alpha
|
||||
const alpha = g1.add(g2).mul(fade);
|
||||
const finalAlpha = mix(alpha.mul(0.75), alpha, g2);
|
||||
const alpha = g1.add(g2).mul(fade)
|
||||
const finalAlpha = mix(alpha.mul(0.75), alpha, g2)
|
||||
|
||||
return new MeshBasicNodeMaterial({
|
||||
transparent: true,
|
||||
colorNode: gridColor,
|
||||
opacityNode: finalAlpha,
|
||||
depthWrite: false,
|
||||
});
|
||||
})
|
||||
}, [
|
||||
cellSize,
|
||||
cellThickness,
|
||||
@@ -165,61 +146,52 @@ const Grid = ({
|
||||
sectionColor,
|
||||
fadeDistance,
|
||||
fadeStrength,
|
||||
]);
|
||||
])
|
||||
|
||||
const handlers = useGridEvents();
|
||||
const gridRef = useRef<Mesh>(null!);
|
||||
const handlers = useGridEvents()
|
||||
const gridRef = useRef<Mesh>(null!)
|
||||
|
||||
useFrame((_, delta) => {
|
||||
const currentLevelId = useViewer.getState().selection.levelId;
|
||||
let targetY = 0;
|
||||
const currentLevelId = useViewer.getState().selection.levelId
|
||||
let targetY = 0
|
||||
if (currentLevelId) {
|
||||
const levelMesh = sceneRegistry.nodes.get(currentLevelId);
|
||||
const levelMesh = sceneRegistry.nodes.get(currentLevelId)
|
||||
if (levelMesh) {
|
||||
targetY = levelMesh.position.y;
|
||||
targetY = levelMesh.position.y
|
||||
}
|
||||
}
|
||||
gridRef.current.position.y = MathUtils.lerp(
|
||||
gridRef.current.position.y,
|
||||
targetY,
|
||||
12 * delta,
|
||||
);
|
||||
});
|
||||
gridRef.current.position.y = MathUtils.lerp(gridRef.current.position.y, targetY, 12 * delta)
|
||||
})
|
||||
|
||||
return (
|
||||
<mesh
|
||||
rotation-x={-Math.PI / 2}
|
||||
material={material}
|
||||
{...handlers}
|
||||
ref={gridRef}
|
||||
>
|
||||
<mesh rotation-x={-Math.PI / 2} material={material} {...handlers} ref={gridRef}>
|
||||
<planeGeometry args={[fadeDistance * 2, fadeDistance * 2]} />
|
||||
</mesh>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const LevelModeSwitcher = () => {
|
||||
const setLevelMode = useViewer((state) => state.setLevelMode);
|
||||
const levelMode = useViewer((state) => state.levelMode);
|
||||
const setLevelMode = useViewer((state) => state.setLevelMode)
|
||||
const levelMode = useViewer((state) => state.levelMode)
|
||||
|
||||
return (
|
||||
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
||||
<button
|
||||
className={`px-4 py-2 rounded ${
|
||||
levelMode === "exploded" ? "bg-blue-500 text-white" : "bg-white"
|
||||
levelMode === 'exploded' ? 'bg-blue-500 text-white' : 'bg-white'
|
||||
}`}
|
||||
onClick={() => setLevelMode("exploded")}
|
||||
onClick={() => setLevelMode('exploded')}
|
||||
>
|
||||
Exploded
|
||||
</button>
|
||||
<button
|
||||
className={`px-4 py-2 rounded ${
|
||||
levelMode === "stacked" ? "bg-blue-500 text-white" : "bg-white"
|
||||
levelMode === 'stacked' ? 'bg-blue-500 text-white' : 'bg-white'
|
||||
}`}
|
||||
onClick={() => setLevelMode("stacked")}
|
||||
onClick={() => setLevelMode('stacked')}
|
||||
>
|
||||
Stacked
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,36 +1,13 @@
|
||||
import {
|
||||
emitter,
|
||||
initSpatialGridSync,
|
||||
ItemNode,
|
||||
type ItemNode,
|
||||
sceneRegistry,
|
||||
useRegistry,
|
||||
useScene,
|
||||
WallNode,
|
||||
type WallNode,
|
||||
} from "@pascal-app/core";
|
||||
import { useGridEvents, useViewer, Viewer } from "@pascal-app/viewer";
|
||||
import { useViewer } from "@pascal-app/viewer";
|
||||
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { Color, MathUtils, Mesh, Object3D, Vector3 } from "three";
|
||||
|
||||
import {
|
||||
color,
|
||||
float,
|
||||
fract,
|
||||
fwidth,
|
||||
mix,
|
||||
oscSine,
|
||||
pass,
|
||||
positionLocal,
|
||||
time,
|
||||
uniform,
|
||||
} from "three/tsl";
|
||||
import { MeshBasicNodeMaterial, PostProcessing } from "three/webgpu";
|
||||
import { ActionMenu } from "../ui/action-menu";
|
||||
import { ToolManager } from "../tools/tool-manager";
|
||||
import { AppSidebar } from "../ui/sidebar/app-sidebar";
|
||||
import { SidebarProvider } from "../ui/primitives/sidebar";
|
||||
import { CustomCameraControls } from "./custom-camera-controls";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export const SelectionManager = () => {
|
||||
const selectedItemId = useRef<ItemNode["id"] | WallNode["id"]>(null);
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import useEditor from "@/store/use-editor";
|
||||
import {
|
||||
emitter,
|
||||
GridEvent,
|
||||
type GridEvent,
|
||||
ItemNode,
|
||||
sceneRegistry,
|
||||
useRegistry,
|
||||
useScene,
|
||||
useSpatialQuery,
|
||||
WallEvent,
|
||||
WallNode,
|
||||
} from "@pascal-app/core";
|
||||
import { useViewer } from "@pascal-app/viewer";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { BoxGeometry, Mesh, MeshStandardMaterial, Vector3 } from "three";
|
||||
import { resolveLevelId } from "../../../../../packages/core/src/hooks/spatial-grid/spatial-grid-sync";
|
||||
type WallEvent,
|
||||
type WallNode,
|
||||
} from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { BoxGeometry, type Mesh, type MeshStandardMaterial, Vector3 } from 'three'
|
||||
import useEditor from '@/store/use-editor'
|
||||
import { resolveLevelId } from '../../../../../packages/core/src/hooks/spatial-grid/spatial-grid-sync'
|
||||
|
||||
/**
|
||||
* Snaps a position to 0.5 grid, with an offset to align item edges to grid lines.
|
||||
@@ -23,45 +22,45 @@ import { resolveLevelId } from "../../../../../packages/core/src/hooks/spatial-g
|
||||
*/
|
||||
function snapToGrid(position: number, dimension: number): number {
|
||||
// Check if half the dimension has a 0.25 remainder (odd multiple of 0.5)
|
||||
const halfDim = dimension / 2;
|
||||
const needsOffset = Math.abs((halfDim * 2) % 1 - 0.5) < 0.01;
|
||||
const offset = needsOffset ? 0.25 : 0;
|
||||
const halfDim = dimension / 2
|
||||
const needsOffset = Math.abs(((halfDim * 2) % 1) - 0.5) < 0.01
|
||||
const offset = needsOffset ? 0.25 : 0
|
||||
// Snap to 0.5 grid with offset
|
||||
return Math.round((position - offset) * 2) / 2 + offset;
|
||||
return Math.round((position - offset) * 2) / 2 + offset
|
||||
}
|
||||
|
||||
export const ItemTool: React.FC = () => {
|
||||
const cursorRef = useRef<Mesh>(null!);
|
||||
const draftItem = useRef<ItemNode | null>(null);
|
||||
const gridPosition = useRef(new Vector3(0, 0, 0));
|
||||
const selectedItem = useEditor((state) => state.selectedItem);
|
||||
const { canPlaceOnFloor, canPlaceOnWall } = useSpatialQuery();
|
||||
const isOnWall = useRef(false);
|
||||
const cursorRef = useRef<Mesh>(null!)
|
||||
const draftItem = useRef<ItemNode | null>(null)
|
||||
const gridPosition = useRef(new Vector3(0, 0, 0))
|
||||
const selectedItem = useEditor((state) => state.selectedItem)
|
||||
const { canPlaceOnFloor, canPlaceOnWall } = useSpatialQuery()
|
||||
const isOnWall = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedItem) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
let currentWallId: string | null = null;
|
||||
let currentWallId: string | null = null
|
||||
|
||||
const checkCanPlace = () => {
|
||||
const currentLevelId = useViewer.getState().selection.levelId;
|
||||
const currentLevelId = useViewer.getState().selection.levelId
|
||||
if (currentLevelId && draftItem.current) {
|
||||
let placeable = true;
|
||||
let placeable = true
|
||||
if (draftItem.current.asset.attachTo) {
|
||||
if (!isOnWall.current || !currentWallId) {
|
||||
placeable = false;
|
||||
placeable = false
|
||||
} else {
|
||||
const result = canPlaceOnWall(
|
||||
currentLevelId,
|
||||
currentWallId as WallNode["id"],
|
||||
currentWallId as WallNode['id'],
|
||||
gridPosition.current.x,
|
||||
gridPosition.current.y,
|
||||
draftItem.current.asset.dimensions,
|
||||
[draftItem.current.id],
|
||||
);
|
||||
placeable = result.valid;
|
||||
)
|
||||
placeable = result.valid
|
||||
}
|
||||
} else {
|
||||
placeable = canPlaceOnFloor(
|
||||
@@ -70,235 +69,213 @@ export const ItemTool: React.FC = () => {
|
||||
draftItem.current.asset.dimensions,
|
||||
[0, 0, 0],
|
||||
[draftItem.current.id],
|
||||
).valid;
|
||||
).valid
|
||||
}
|
||||
if (placeable) {
|
||||
(cursorRef.current.material as MeshStandardMaterial).color.set(
|
||||
"green",
|
||||
);
|
||||
return true;
|
||||
;(cursorRef.current.material as MeshStandardMaterial).color.set('green')
|
||||
return true
|
||||
} else {
|
||||
(cursorRef.current.material as MeshStandardMaterial).color.set("red");
|
||||
return false;
|
||||
;(cursorRef.current.material as MeshStandardMaterial).color.set('red')
|
||||
return false
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
const createDraftItem = () => {
|
||||
const currentLevelId = useViewer.getState().selection.levelId;
|
||||
const currentLevelId = useViewer.getState().selection.levelId
|
||||
if (!currentLevelId) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
useScene.temporal.getState().pause();
|
||||
useScene.temporal.getState().pause()
|
||||
draftItem.current = ItemNode.parse({
|
||||
position: [
|
||||
gridPosition.current.x,
|
||||
gridPosition.current.y,
|
||||
gridPosition.current.z,
|
||||
],
|
||||
position: [gridPosition.current.x, gridPosition.current.y, gridPosition.current.z],
|
||||
name: selectedItem.name,
|
||||
asset: selectedItem,
|
||||
});
|
||||
useScene.getState().createNode(draftItem.current, currentLevelId);
|
||||
checkCanPlace();
|
||||
};
|
||||
createDraftItem();
|
||||
})
|
||||
useScene.getState().createNode(draftItem.current, currentLevelId)
|
||||
checkCanPlace()
|
||||
}
|
||||
createDraftItem()
|
||||
|
||||
const onGridMove = (event: GridEvent) => {
|
||||
if (!cursorRef.current) return;
|
||||
if (!cursorRef.current) return
|
||||
|
||||
if (isOnWall.current) return;
|
||||
if (isOnWall.current) return
|
||||
|
||||
const [dimX, , dimZ] = selectedItem.dimensions;
|
||||
const [dimX, , dimZ] = selectedItem.dimensions
|
||||
gridPosition.current.set(
|
||||
snapToGrid(event.position[0], dimX),
|
||||
0,
|
||||
snapToGrid(event.position[2], dimZ),
|
||||
);
|
||||
)
|
||||
cursorRef.current.position.set(
|
||||
gridPosition.current.x,
|
||||
event.position[1],
|
||||
gridPosition.current.z,
|
||||
);
|
||||
checkCanPlace();
|
||||
)
|
||||
checkCanPlace()
|
||||
if (draftItem.current) {
|
||||
draftItem.current.position = [
|
||||
gridPosition.current.x,
|
||||
0,
|
||||
gridPosition.current.z,
|
||||
];
|
||||
draftItem.current.position = [gridPosition.current.x, 0, gridPosition.current.z]
|
||||
}
|
||||
};
|
||||
}
|
||||
const onGridClick = (event: GridEvent) => {
|
||||
const currentLevelId = useViewer.getState().selection.levelId;
|
||||
if (isOnWall.current) return;
|
||||
const currentLevelId = useViewer.getState().selection.levelId
|
||||
if (isOnWall.current) return
|
||||
|
||||
if (!currentLevelId || !draftItem.current || !checkCanPlace()) return;
|
||||
if (!currentLevelId || !draftItem.current || !checkCanPlace()) return
|
||||
|
||||
useScene.temporal.getState().resume();
|
||||
useScene.temporal.getState().resume()
|
||||
useScene.getState().updateNode(draftItem.current.id, {
|
||||
position: [gridPosition.current.x, 0, gridPosition.current.z],
|
||||
});
|
||||
draftItem.current = null;
|
||||
})
|
||||
draftItem.current = null
|
||||
|
||||
useScene.temporal.getState().pause();
|
||||
createDraftItem();
|
||||
};
|
||||
useScene.temporal.getState().pause()
|
||||
createDraftItem()
|
||||
}
|
||||
|
||||
const onWallEnter = (event: WallEvent) => {
|
||||
if (
|
||||
useViewer.getState().selection.levelId !==
|
||||
resolveLevelId(event.node, useScene.getState().nodes)
|
||||
) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (
|
||||
draftItem.current?.asset.attachTo === "wall" ||
|
||||
draftItem.current?.asset.attachTo === "wall-side"
|
||||
draftItem.current?.asset.attachTo === 'wall' ||
|
||||
draftItem.current?.asset.attachTo === 'wall-side'
|
||||
) {
|
||||
event.stopPropagation();
|
||||
isOnWall.current = true;
|
||||
currentWallId = event.node.id;
|
||||
event.stopPropagation()
|
||||
isOnWall.current = true
|
||||
currentWallId = event.node.id
|
||||
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;
|
||||
)
|
||||
draftItem.current.parentId = event.node.id
|
||||
useScene.getState().updateNode(draftItem.current.id, {
|
||||
position: [
|
||||
gridPosition.current.x,
|
||||
gridPosition.current.y,
|
||||
gridPosition.current.z,
|
||||
],
|
||||
position: [gridPosition.current.x, gridPosition.current.y, gridPosition.current.z],
|
||||
parentId: event.node.id,
|
||||
});
|
||||
checkCanPlace();
|
||||
})
|
||||
checkCanPlace()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const onWallLeave = (event: WallEvent) => {
|
||||
if (!isOnWall.current) return;
|
||||
isOnWall.current = false;
|
||||
currentWallId = null;
|
||||
event.stopPropagation();
|
||||
if (!draftItem.current) return;
|
||||
const currentLevelId = useViewer.getState().selection.levelId;
|
||||
draftItem.current.parentId = currentLevelId;
|
||||
if (!isOnWall.current) return
|
||||
isOnWall.current = false
|
||||
currentWallId = null
|
||||
event.stopPropagation()
|
||||
if (!draftItem.current) return
|
||||
const currentLevelId = useViewer.getState().selection.levelId
|
||||
draftItem.current.parentId = currentLevelId
|
||||
useScene.getState().updateNode(draftItem.current.id, {
|
||||
position: [
|
||||
gridPosition.current.x,
|
||||
gridPosition.current.y,
|
||||
gridPosition.current.z,
|
||||
],
|
||||
position: [gridPosition.current.x, gridPosition.current.y, gridPosition.current.z],
|
||||
parentId: currentLevelId,
|
||||
});
|
||||
checkCanPlace();
|
||||
};
|
||||
})
|
||||
checkCanPlace()
|
||||
}
|
||||
|
||||
const onWallClick = (event: WallEvent) => {
|
||||
event.stopPropagation();
|
||||
if (!isOnWall.current) return;
|
||||
event.stopPropagation()
|
||||
if (!isOnWall.current) return
|
||||
|
||||
const currentLevelId = useViewer.getState().selection.levelId;
|
||||
if (!currentLevelId || !draftItem.current || !checkCanPlace()) return;
|
||||
const currentLevelId = useViewer.getState().selection.levelId
|
||||
if (!currentLevelId || !draftItem.current || !checkCanPlace()) return
|
||||
|
||||
useScene.temporal.getState().resume();
|
||||
useScene.temporal.getState().resume()
|
||||
useScene.getState().updateNode(draftItem.current.id, {
|
||||
position: [
|
||||
gridPosition.current.x,
|
||||
gridPosition.current.y,
|
||||
gridPosition.current.z,
|
||||
],
|
||||
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.getState().dirtyNodes.add(event.node.id)
|
||||
draftItem.current = null
|
||||
|
||||
useScene.temporal.getState().pause();
|
||||
createDraftItem();
|
||||
checkCanPlace();
|
||||
};
|
||||
useScene.temporal.getState().pause()
|
||||
createDraftItem()
|
||||
checkCanPlace()
|
||||
}
|
||||
|
||||
const onWallMove = (event: WallEvent) => {
|
||||
if (isOnWall.current === false) return;
|
||||
event.stopPropagation();
|
||||
if (!draftItem.current) return;
|
||||
if (isOnWall.current === false) return
|
||||
event.stopPropagation()
|
||||
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(
|
||||
Math.round(event.position[0] * 2) / 2,
|
||||
Math.round(event.position[1] * 2) / 2,
|
||||
Math.round(event.position[2] * 2) / 2,
|
||||
);
|
||||
)
|
||||
|
||||
const {
|
||||
node: { start, end },
|
||||
} = event;
|
||||
const dx = end[0] - start[0];
|
||||
const dz = end[1] - start[1];
|
||||
const { normal } = event;
|
||||
const wallAngle = Math.atan2(dx, dz);
|
||||
} = event
|
||||
const dx = end[0] - start[0]
|
||||
const dz = end[1] - start[1]
|
||||
const { normal } = event
|
||||
const wallAngle = Math.atan2(dx, dz)
|
||||
|
||||
cursorRef.current.rotation.y = wallAngle + Math.PI / 2;
|
||||
const canPlace = checkCanPlace();
|
||||
cursorRef.current.rotation.y = wallAngle + Math.PI / 2
|
||||
const canPlace = checkCanPlace()
|
||||
if (draftItem.current && canPlace) {
|
||||
draftItem.current.position = [
|
||||
gridPosition.current.x,
|
||||
gridPosition.current.y,
|
||||
gridPosition.current.z,
|
||||
];
|
||||
const draftItemMesh = sceneRegistry.nodes.get(draftItem.current.id);
|
||||
]
|
||||
const draftItemMesh = sceneRegistry.nodes.get(draftItem.current.id)
|
||||
if (draftItemMesh) {
|
||||
draftItemMesh.position.copy(gridPosition.current);
|
||||
draftItemMesh.position.copy(gridPosition.current)
|
||||
}
|
||||
|
||||
useScene.getState().dirtyNodes.add(event.node.id);
|
||||
useScene.getState().dirtyNodes.add(event.node.id)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
emitter.on("grid:move", onGridMove);
|
||||
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);
|
||||
emitter.on('grid:move', onGridMove)
|
||||
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 boxGeometry = new BoxGeometry(
|
||||
selectedItem.dimensions[0],
|
||||
selectedItem.dimensions[1],
|
||||
selectedItem.dimensions[2],
|
||||
);
|
||||
boxGeometry.translate(0, selectedItem.dimensions[1] / 2, 0);
|
||||
cursorRef.current.geometry = boxGeometry;
|
||||
};
|
||||
setupBoundingBox();
|
||||
)
|
||||
boxGeometry.translate(0, selectedItem.dimensions[1] / 2, 0)
|
||||
cursorRef.current.geometry = boxGeometry
|
||||
}
|
||||
setupBoundingBox()
|
||||
|
||||
return () => {
|
||||
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:click", onGridClick);
|
||||
emitter.off("wall:enter", onWallEnter);
|
||||
emitter.off("wall:leave", onWallLeave);
|
||||
emitter.off("wall:click", onWallClick);
|
||||
emitter.off("wall:move", onWallMove);
|
||||
};
|
||||
}, [selectedItem]);
|
||||
useScene.temporal.getState().resume()
|
||||
emitter.off('grid:move', onGridMove)
|
||||
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, canPlaceOnFloor, canPlaceOnWall])
|
||||
|
||||
useFrame((_, delta) => {
|
||||
if (draftItem.current && !isOnWall.current) {
|
||||
const draftItemMesh = sceneRegistry.nodes.get(draftItem.current.id);
|
||||
const draftItemMesh = sceneRegistry.nodes.get(draftItem.current.id)
|
||||
if (draftItemMesh) {
|
||||
draftItemMesh.position.lerp(gridPosition.current, delta * 20);
|
||||
draftItemMesh.position.lerp(gridPosition.current, delta * 20)
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<group>
|
||||
@@ -307,5 +284,5 @@ export const ItemTool: React.FC = () => {
|
||||
<meshStandardMaterial color="red" wireframe />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import useEditor, { Phase, Tool } from "@/store/use-editor";
|
||||
import { WallTool } from "./wall/wall-tool";
|
||||
import { ItemTool } from "./item/item-tool";
|
||||
import useEditor, { type Phase, type Tool } from '@/store/use-editor'
|
||||
import { ItemTool } from './item/item-tool'
|
||||
import { WallTool } from './wall/wall-tool'
|
||||
|
||||
const tools: Record<Phase, Partial<Record<Tool, React.FC>>> = {
|
||||
site: {
|
||||
},
|
||||
site: {},
|
||||
structure: {
|
||||
wall: WallTool,
|
||||
item: ItemTool,
|
||||
},
|
||||
furnish: {
|
||||
item: ItemTool
|
||||
item: ItemTool,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const ToolManager: React.FC = () => {
|
||||
const phase = useEditor((state) => state.phase);
|
||||
const mode = useEditor((state) => state.mode);
|
||||
const tool = useEditor((state) => state.tool);
|
||||
const phase = useEditor((state) => state.phase)
|
||||
const mode = useEditor((state) => state.mode)
|
||||
const tool = useEditor((state) => state.tool)
|
||||
|
||||
if (mode !== "build" || tool === null) return null;
|
||||
if (mode !== 'build' || tool === null) return null
|
||||
|
||||
const Component = tools[phase]?.[tool]
|
||||
|
||||
const Component = tools[phase]?.[tool];
|
||||
|
||||
return Component ? <Component /> : null;
|
||||
};
|
||||
return Component ? <Component /> : null
|
||||
}
|
||||
|
||||
@@ -1,75 +1,62 @@
|
||||
import { emitter, GridEvent, useScene, WallNode } from "@pascal-app/core";
|
||||
import { useViewer } from "@pascal-app/viewer";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Line, Mesh, Vector3 } from "three";
|
||||
import { emitter, type GridEvent, useScene, WallNode } from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { type Line, type Mesh, Vector3 } from 'three'
|
||||
|
||||
const commitWallDrawing = (start: [number, number], end: [number, number]) => {
|
||||
const currentLevelId = useViewer.getState().selection.levelId;
|
||||
const { createNode } = useScene.getState();
|
||||
const currentLevelId = useViewer.getState().selection.levelId
|
||||
const { createNode } = useScene.getState()
|
||||
|
||||
if (!currentLevelId) return;
|
||||
if (!currentLevelId) return
|
||||
|
||||
const wall = WallNode.parse({ start, end });
|
||||
const wall = WallNode.parse({ start, end })
|
||||
|
||||
createNode(wall, currentLevelId);
|
||||
};
|
||||
createNode(wall, currentLevelId)
|
||||
}
|
||||
|
||||
export const WallTool: React.FC = () => {
|
||||
const cursorRef = useRef<Mesh>(null);
|
||||
const drawingLineRef = useRef<Line>(null!);
|
||||
const cursorRef = useRef<Mesh>(null)
|
||||
const drawingLineRef = useRef<Line>(null!)
|
||||
|
||||
useEffect(() => {
|
||||
let buildingState = 0;
|
||||
const startingPoint = new Vector3(0, 0, 0);
|
||||
const endingPoint = new Vector3(0, 0, 0);
|
||||
let gridPosition: [number, number] = [0, 0];
|
||||
let buildingState = 0
|
||||
const startingPoint = new Vector3(0, 0, 0)
|
||||
const endingPoint = new Vector3(0, 0, 0)
|
||||
let gridPosition: [number, number] = [0, 0]
|
||||
|
||||
drawingLineRef.current.geometry.setFromPoints([startingPoint, endingPoint]);
|
||||
drawingLineRef.current.geometry.setFromPoints([startingPoint, endingPoint])
|
||||
|
||||
const onGridMove = (event: GridEvent) => {
|
||||
if (!cursorRef.current) return;
|
||||
if (!cursorRef.current) return
|
||||
|
||||
gridPosition = [
|
||||
Math.round(event.position[0] * 2) / 2,
|
||||
Math.round(event.position[2] * 2) / 2,
|
||||
];
|
||||
cursorRef.current.position.set(
|
||||
gridPosition[0],
|
||||
event.position[1],
|
||||
gridPosition[1],
|
||||
);
|
||||
gridPosition = [Math.round(event.position[0] * 2) / 2, Math.round(event.position[2] * 2) / 2]
|
||||
cursorRef.current.position.set(gridPosition[0], event.position[1], gridPosition[1])
|
||||
if (buildingState === 1) {
|
||||
endingPoint.set(gridPosition[0], event.position[1], gridPosition[1]);
|
||||
endingPoint.set(gridPosition[0], event.position[1], gridPosition[1])
|
||||
}
|
||||
drawingLineRef.current.geometry.setFromPoints([
|
||||
startingPoint,
|
||||
endingPoint,
|
||||
]);
|
||||
};
|
||||
drawingLineRef.current.geometry.setFromPoints([startingPoint, endingPoint])
|
||||
}
|
||||
const onGridClick = (event: GridEvent) => {
|
||||
if (buildingState === 0) {
|
||||
startingPoint.set(gridPosition[0], event.position[1], gridPosition[1]);
|
||||
buildingState = 1;
|
||||
console.log("starting building at:", startingPoint);
|
||||
drawingLineRef.current.visible = true;
|
||||
startingPoint.set(gridPosition[0], event.position[1], gridPosition[1])
|
||||
buildingState = 1
|
||||
console.log('starting building at:', startingPoint)
|
||||
drawingLineRef.current.visible = true
|
||||
} else if (buildingState === 1) {
|
||||
commitWallDrawing(
|
||||
[startingPoint.x, startingPoint.z],
|
||||
[endingPoint.x, endingPoint.z],
|
||||
);
|
||||
drawingLineRef.current.visible = false;
|
||||
buildingState = 0;
|
||||
commitWallDrawing([startingPoint.x, startingPoint.z], [endingPoint.x, endingPoint.z])
|
||||
drawingLineRef.current.visible = false
|
||||
buildingState = 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
emitter.on("grid:move", onGridMove);
|
||||
emitter.on("grid:click", onGridClick);
|
||||
emitter.on('grid:move', onGridMove)
|
||||
emitter.on('grid:click', onGridClick)
|
||||
|
||||
return () => {
|
||||
emitter.off("grid:move", onGridMove);
|
||||
emitter.off("grid:click", onGridClick);
|
||||
};
|
||||
}, []);
|
||||
emitter.off('grid:move', onGridMove)
|
||||
emitter.off('grid:click', onGridClick)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<group>
|
||||
@@ -79,12 +66,7 @@ export const WallTool: React.FC = () => {
|
||||
</mesh>
|
||||
<group>
|
||||
{/* @ts-ignore */}
|
||||
<line
|
||||
ref={drawingLineRef}
|
||||
frustumCulled={false}
|
||||
renderOrder={1}
|
||||
visible={false}
|
||||
>
|
||||
<line ref={drawingLineRef} frustumCulled={false} renderOrder={1} visible={false}>
|
||||
<bufferGeometry />
|
||||
<lineDashedNodeMaterial
|
||||
color="blue"
|
||||
@@ -98,5 +80,5 @@ export const WallTool: React.FC = () => {
|
||||
</line>
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import * as React from "react";
|
||||
import * as React from 'react'
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener('change', onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener('change', onChange)
|
||||
}, [])
|
||||
|
||||
return !!isMobile;
|
||||
return !!isMobile
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
transpilePackages: ["three", "@pascal-app/viewer", "@pascal-app/core"],
|
||||
};
|
||||
transpilePackages: ['three', '@pascal-app/viewer', '@pascal-app/core'],
|
||||
}
|
||||
|
||||
export default nextConfig;
|
||||
export default nextConfig
|
||||
|
||||
@@ -1,111 +1,106 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import {
|
||||
AssetInput,
|
||||
BuildingNode,
|
||||
LevelNode,
|
||||
useScene,
|
||||
} from "@pascal-app/core";
|
||||
import { useViewer } from "@pascal-app/viewer";
|
||||
import { create } from "zustand";
|
||||
import { Asset } from "../../../packages/core/src/schema/nodes/item";
|
||||
import { type BuildingNode, type LevelNode, useScene } from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { create } from 'zustand'
|
||||
import type { Asset } from '../../../packages/core/src/schema/nodes/item'
|
||||
|
||||
export type Phase = "site" | "structure" | "furnish";
|
||||
export type Phase = 'site' | 'structure' | 'furnish'
|
||||
|
||||
export type Mode = "select" | "edit" | "delete" | "build";
|
||||
export type Mode = 'select' | 'edit' | 'delete' | 'build'
|
||||
|
||||
// Structure mode tools (building elements)
|
||||
export type StructureTool =
|
||||
| "wall"
|
||||
| "room"
|
||||
| "custom-room"
|
||||
| "slab"
|
||||
| "ceiling"
|
||||
| "roof"
|
||||
| "column"
|
||||
| "stair"
|
||||
| "item"
|
||||
| "zone";
|
||||
| 'wall'
|
||||
| 'room'
|
||||
| 'custom-room'
|
||||
| 'slab'
|
||||
| 'ceiling'
|
||||
| 'roof'
|
||||
| 'column'
|
||||
| 'stair'
|
||||
| 'item'
|
||||
| 'zone'
|
||||
|
||||
// Furnish mode tools (items and decoration)
|
||||
export type FurnishTool = "item";
|
||||
export type FurnishTool = 'item'
|
||||
|
||||
// Site mode tools
|
||||
export type SiteTool = "property-line";
|
||||
export type SiteTool = 'property-line'
|
||||
|
||||
// Catalog categories for furnish mode items
|
||||
export type CatalogCategory =
|
||||
| "furniture"
|
||||
| "appliance"
|
||||
| "bathroom"
|
||||
| "kitchen"
|
||||
| "outdoor"
|
||||
| "window"
|
||||
| "door";
|
||||
| 'furniture'
|
||||
| 'appliance'
|
||||
| 'bathroom'
|
||||
| 'kitchen'
|
||||
| 'outdoor'
|
||||
| 'window'
|
||||
| 'door'
|
||||
|
||||
// Combined tool type
|
||||
export type Tool = SiteTool | StructureTool | FurnishTool;
|
||||
export type Tool = SiteTool | StructureTool | FurnishTool
|
||||
|
||||
type EditorState = {
|
||||
phase: Phase;
|
||||
setPhase: (phase: Phase) => void;
|
||||
mode: Mode;
|
||||
setMode: (mode: Mode) => void;
|
||||
tool: Tool | null;
|
||||
setTool: (tool: Tool | null) => void;
|
||||
catalogCategory: CatalogCategory | null;
|
||||
setCatalogCategory: (category: CatalogCategory | null) => void;
|
||||
selectedItem: Asset | null;
|
||||
setSelectedItem: (item: Asset) => void;
|
||||
};
|
||||
phase: Phase
|
||||
setPhase: (phase: Phase) => void
|
||||
mode: Mode
|
||||
setMode: (mode: Mode) => void
|
||||
tool: Tool | null
|
||||
setTool: (tool: Tool | null) => void
|
||||
catalogCategory: CatalogCategory | null
|
||||
setCatalogCategory: (category: CatalogCategory | null) => void
|
||||
selectedItem: Asset | null
|
||||
setSelectedItem: (item: Asset) => void
|
||||
}
|
||||
|
||||
const useEditor = create<EditorState>()((set, get) => ({
|
||||
phase: "site",
|
||||
phase: 'site',
|
||||
setPhase: (phase) => {
|
||||
const currentPhase = get().phase;
|
||||
if (currentPhase === phase) return;
|
||||
const currentPhase = get().phase
|
||||
if (currentPhase === phase) return
|
||||
|
||||
set({ phase });
|
||||
set({ phase })
|
||||
|
||||
const viewer = useViewer.getState();
|
||||
const scene = useScene.getState();
|
||||
const viewer = useViewer.getState()
|
||||
const scene = useScene.getState()
|
||||
|
||||
switch (phase) {
|
||||
case "site":
|
||||
case 'site':
|
||||
// In Site mode, we zoom out and deselect specific levels/buildings
|
||||
viewer.resetSelection();
|
||||
viewer.setLevelMode("stacked");
|
||||
break;
|
||||
viewer.resetSelection()
|
||||
viewer.setLevelMode('stacked')
|
||||
break
|
||||
|
||||
case "structure":
|
||||
case 'structure':
|
||||
// In Structure mode, we often want to focus on a specific building/level
|
||||
// Auto-select the first building if none is selected
|
||||
if (!viewer.selection.buildingId) {
|
||||
const firstBuildingId = scene.rootNodeIds.find((id) => {
|
||||
const node = scene.nodes[id];
|
||||
return node?.type === "building" || null;
|
||||
});
|
||||
const node = scene.nodes[id]
|
||||
return node?.type === 'building' || null
|
||||
})
|
||||
if (firstBuildingId) {
|
||||
viewer.setSelection({
|
||||
buildingId: firstBuildingId as BuildingNode["id"],
|
||||
});
|
||||
const buildingNode = scene.nodes[firstBuildingId] as BuildingNode;
|
||||
const firstLevelId = buildingNode.children[0];
|
||||
buildingId: firstBuildingId as BuildingNode['id'],
|
||||
})
|
||||
const buildingNode = scene.nodes[firstBuildingId] as BuildingNode
|
||||
const firstLevelId = buildingNode.children[0]
|
||||
if (firstLevelId) {
|
||||
viewer.setSelection({ levelId: firstLevelId as LevelNode["id"] });
|
||||
viewer.setSelection({ levelId: firstLevelId as LevelNode['id'] })
|
||||
}
|
||||
}
|
||||
}
|
||||
viewer.setLevelMode("stacked"); // Better for structure editing
|
||||
break;
|
||||
viewer.setLevelMode('stacked') // Better for structure editing
|
||||
break
|
||||
|
||||
case "furnish":
|
||||
case 'furnish':
|
||||
// Maybe in furnish mode we force "solo" level view to see inside rooms
|
||||
viewer.setLevelMode("solo");
|
||||
break;
|
||||
viewer.setLevelMode('solo')
|
||||
break
|
||||
}
|
||||
},
|
||||
mode: "select",
|
||||
mode: 'select',
|
||||
setMode: (mode) => set({ mode }),
|
||||
tool: null,
|
||||
setTool: (tool) => set({ tool }),
|
||||
@@ -113,6 +108,6 @@ const useEditor = create<EditorState>()((set, get) => ({
|
||||
setCatalogCategory: (category) => set({ catalogCategory: category }),
|
||||
selectedItem: null,
|
||||
setSelectedItem: (item) => set({ selectedItem: item }),
|
||||
}));
|
||||
}))
|
||||
|
||||
export default useEditor;
|
||||
export default useEditor
|
||||
|
||||
Reference in New Issue
Block a user