diff --git a/apps/editor/app/layout.tsx b/apps/editor/app/layout.tsx
index 84695375..2e571934 100644
--- a/apps/editor/app/layout.tsx
+++ b/apps/editor/app/layout.tsx
@@ -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 (
-
- {children}
-
+ {children}
- );
+ )
}
diff --git a/apps/editor/app/page.tsx b/apps/editor/app/page.tsx
index 9c1a36ba..e9e2c256 100644
--- a/apps/editor/app/page.tsx
+++ b/apps/editor/app/page.tsx
@@ -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() {
- );
+ )
}
diff --git a/apps/editor/components/editor/custom-camera-controls.tsx b/apps/editor/components/editor/custom-camera-controls.tsx
index fe6a96be..1111c1ca 100644
--- a/apps/editor/components/editor/custom-camera-controls.tsx
+++ b/apps/editor/components/editor/custom-camera-controls.tsx
@@ -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(null!);
- const currentLevelId = useViewer((state) => state.selection.levelId);
- const firstLoad = useRef(true);
+ const controls = useRef(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 ;
-};
+ return
+}
diff --git a/apps/editor/components/editor/index.tsx b/apps/editor/components/editor/index.tsx
index 86bc4102..692a78a2 100644
--- a/apps/editor/components/editor/index.tsx
+++ b/apps/editor/components/editor/index.tsx
@@ -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() {
- );
+ )
}
const TestUndo = () => {
- const { undo, redo, futureStates, pastStates } = useScene.temporal.getState();
+ const { undo, redo, futureStates, pastStates } = useScene.temporal.getState()
return (
- );
-};
+ )
+}
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(null!);
+ const handlers = useGridEvents()
+ const gridRef = useRef(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 (
-
+
- );
-};
+ )
+}
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 (
setLevelMode("exploded")}
+ onClick={() => setLevelMode('exploded')}
>
Exploded
setLevelMode("stacked")}
+ onClick={() => setLevelMode('stacked')}
>
Stacked
- );
-};
+ )
+}
diff --git a/apps/editor/components/editor/selection-manager.tsx b/apps/editor/components/editor/selection-manager.tsx
index f7ee4145..e75d124b 100644
--- a/apps/editor/components/editor/selection-manager.tsx
+++ b/apps/editor/components/editor/selection-manager.tsx
@@ -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(null);
diff --git a/apps/editor/components/tools/item/item-tool.tsx b/apps/editor/components/tools/item/item-tool.tsx
index 05a9e087..6e4c0413 100644
--- a/apps/editor/components/tools/item/item-tool.tsx
+++ b/apps/editor/components/tools/item/item-tool.tsx
@@ -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(null!);
- const draftItem = useRef(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(null!)
+ const draftItem = useRef(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 (
@@ -307,5 +284,5 @@ export const ItemTool: React.FC = () => {
- );
-};
+ )
+}
diff --git a/apps/editor/components/tools/tool-manager.tsx b/apps/editor/components/tools/tool-manager.tsx
index a13c6e6e..3087370b 100644
--- a/apps/editor/components/tools/tool-manager.tsx
+++ b/apps/editor/components/tools/tool-manager.tsx
@@ -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>> = {
- 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 ? : null;
-};
\ No newline at end of file
+ return Component ? : null
+}
diff --git a/apps/editor/components/tools/wall/wall-tool.tsx b/apps/editor/components/tools/wall/wall-tool.tsx
index d16520f1..23998980 100644
--- a/apps/editor/components/tools/wall/wall-tool.tsx
+++ b/apps/editor/components/tools/wall/wall-tool.tsx
@@ -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(null);
- const drawingLineRef = useRef(null!);
+ const cursorRef = useRef(null)
+ const drawingLineRef = useRef(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 (
@@ -79,12 +66,7 @@ export const WallTool: React.FC = () => {
{/* @ts-ignore */}
-
+
{
- );
-};
+ )
+}
diff --git a/apps/editor/hooks/use-mobile.ts b/apps/editor/hooks/use-mobile.ts
index a93d5839..4331d5c5 100644
--- a/apps/editor/hooks/use-mobile.ts
+++ b/apps/editor/hooks/use-mobile.ts
@@ -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(
- undefined,
- );
+ const [isMobile, setIsMobile] = React.useState(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
}
diff --git a/apps/editor/lib/utils.ts b/apps/editor/lib/utils.ts
index a5ef1935..d32b0fe6 100644
--- a/apps/editor/lib/utils.ts
+++ b/apps/editor/lib/utils.ts
@@ -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))
}
diff --git a/apps/editor/next.config.ts b/apps/editor/next.config.ts
index c6c21d70..70f4f47b 100644
--- a/apps/editor/next.config.ts
+++ b/apps/editor/next.config.ts
@@ -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
diff --git a/apps/editor/store/use-editor.tsx b/apps/editor/store/use-editor.tsx
index cc37a570..75027e46 100644
--- a/apps/editor/store/use-editor.tsx
+++ b/apps/editor/store/use-editor.tsx
@@ -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()((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()((set, get) => ({
setCatalogCategory: (category) => set({ catalogCategory: category }),
selectedItem: null,
setSelectedItem: (item) => set({ selectedItem: item }),
-}));
+}))
-export default useEditor;
+export default useEditor
diff --git a/biome.jsonc b/biome.jsonc
index 9d1df77a..a9ed91cb 100644
--- a/biome.jsonc
+++ b/biome.jsonc
@@ -1,6 +1,5 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
- "extends": ["ultracite", "ultracite/core", "ultracite/next"],
"vcs": {
"enabled": true,
"clientKind": "git",
@@ -66,7 +65,7 @@
"correctness": {
"noUnusedVariables": "off",
"noUnusedFunctionParameters": "off",
- "noUnusedImports": "off",
+ "noUnusedImports": "error",
"useExhaustiveDependencies": "info",
"noPrecisionLoss": "off"
},
@@ -98,6 +97,10 @@
"packages/**/*.css",
"packages/**/*.md",
"packages/**/*.mdx",
+ "apps/**/*.ts",
+ "apps/**/*.tsx",
+ "apps/**/*.js",
+ "apps/**/*.jsx",
"!**/node_modules",
"!**/.next",
"!**/dist",
diff --git a/packages/core/src/events/bus.ts b/packages/core/src/events/bus.ts
index ce76f82e..400dfebe 100644
--- a/packages/core/src/events/bus.ts
+++ b/packages/core/src/events/bus.ts
@@ -1,49 +1,49 @@
-import mitt from "mitt";
-import { BuildingNode, ItemNode, WallNode } from "../schema";
-import { AnyNode } from "../schema/types";
+import mitt from 'mitt'
+import type { BuildingNode, ItemNode, WallNode } from '../schema'
+import type { AnyNode } from '../schema/types'
// Base event interfaces
export interface GridEvent {
- position: [number, number, number];
+ position: [number, number, number]
}
export interface NodeEvent {
- node: T;
- position: [number, number, number];
- localPosition: [number, number, number];
- normal?: [number, number, number];
- stopPropagation: () => void;
+ node: T
+ position: [number, number, number]
+ localPosition: [number, number, number]
+ normal?: [number, number, number]
+ stopPropagation: () => void
}
-export type WallEvent = NodeEvent;
-export type ItemEvent = NodeEvent;
-export type BuildingEvent = NodeEvent;
+export type WallEvent = NodeEvent
+export type ItemEvent = NodeEvent
+export type BuildingEvent = NodeEvent
// Event suffixes - exported for use in hooks
export const eventSuffixes = [
- "click",
- "move",
- "enter",
- "leave",
- "pointerdown",
- "pointerup",
- "context-menu",
- "double-click",
-] as const;
+ 'click',
+ 'move',
+ 'enter',
+ 'leave',
+ 'pointerdown',
+ 'pointerup',
+ 'context-menu',
+ 'double-click',
+] as const
-export type EventSuffix = (typeof eventSuffixes)[number];
+export type EventSuffix = (typeof eventSuffixes)[number]
type NodeEvents = {
- [K in `${T}:${EventSuffix}`]: E;
-};
+ [K in `${T}:${EventSuffix}`]: E
+}
type GridEvents = {
- [K in `grid:${EventSuffix}`]: GridEvent;
-};
+ [K in `grid:${EventSuffix}`]: GridEvent
+}
type EditorEvents = GridEvents &
- NodeEvents<"wall", WallEvent> &
- NodeEvents<"item", ItemEvent> &
- NodeEvents<"building", BuildingEvent>;
+ NodeEvents<'wall', WallEvent> &
+ NodeEvents<'item', ItemEvent> &
+ NodeEvents<'building', BuildingEvent>
-export const emitter = mitt();
+export const emitter = mitt()
diff --git a/packages/core/src/hooks/scene-registry/scene-registry.ts b/packages/core/src/hooks/scene-registry/scene-registry.ts
index 65e0ae93..0ccc6e08 100644
--- a/packages/core/src/hooks/scene-registry/scene-registry.ts
+++ b/packages/core/src/hooks/scene-registry/scene-registry.ts
@@ -1,6 +1,5 @@
-import * as THREE from "three";
-
-import { useLayoutEffect } from "react";
+import { useLayoutEffect } from 'react'
+import type * as THREE from 'three'
export const sceneRegistry = {
// Master lookup: ID -> Object3D
@@ -15,27 +14,27 @@ export const sceneRegistry = {
item: new Set(),
slab: new Set(),
},
-};
+}
export function useRegistry(
id: string,
type: keyof typeof sceneRegistry.byType,
- ref: React.RefObject
+ ref: React.RefObject,
) {
useLayoutEffect(() => {
- const obj = ref.current;
- if (!obj) return;
+ const obj = ref.current
+ if (!obj) return
// 1. Add to master map
- sceneRegistry.nodes.set(id, obj);
+ sceneRegistry.nodes.set(id, obj)
// 2. Add to type-specific set
- sceneRegistry.byType[type].add(id);
+ sceneRegistry.byType[type].add(id)
// 4. Cleanup when component unmounts
return () => {
- sceneRegistry.nodes.delete(id);
- sceneRegistry.byType[type].delete(id);
- };
- }, [id, type, ref]);
+ sceneRegistry.nodes.delete(id)
+ sceneRegistry.byType[type].delete(id)
+ }
+ }, [id, type, ref])
}
diff --git a/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts b/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts
index b3bb9ec2..524f2607 100644
--- a/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts
+++ b/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts
@@ -1,64 +1,58 @@
-import { AnyNode, ItemNode, WallNode } from "../../schema";
-import { SpatialGrid } from "./spatial-grid";
-import { WallSpatialGrid } from "./wall-spatial-grid";
+import type { AnyNode, ItemNode, WallNode } from '../../schema'
+import { SpatialGrid } from './spatial-grid'
+import { WallSpatialGrid } from './wall-spatial-grid'
export class SpatialGridManager {
- private floorGrids = new Map(); // levelId -> grid
- private wallGrids = new Map(); // levelId -> wall grid
- private walls = new Map(); // wallId -> wall data (for length calculations)
+ private floorGrids = new Map() // levelId -> grid
+ private wallGrids = new Map() // levelId -> wall grid
+ private walls = new Map() // wallId -> wall data (for length calculations)
constructor(private cellSize = 0.5) {}
private getFloorGrid(levelId: string): SpatialGrid {
if (!this.floorGrids.has(levelId)) {
- this.floorGrids.set(
- levelId,
- new SpatialGrid({ cellSize: this.cellSize }),
- );
+ this.floorGrids.set(levelId, new SpatialGrid({ cellSize: this.cellSize }))
}
- return this.floorGrids.get(levelId)!;
+ return this.floorGrids.get(levelId)!
}
private getWallGrid(levelId: string): WallSpatialGrid {
if (!this.wallGrids.has(levelId)) {
- this.wallGrids.set(levelId, new WallSpatialGrid());
+ this.wallGrids.set(levelId, new WallSpatialGrid())
}
- return this.wallGrids.get(levelId)!;
+ return this.wallGrids.get(levelId)!
}
private getWallLength(wallId: string): number {
- const wall = this.walls.get(wallId);
- if (!wall) return 0;
- const dx = wall.end[0] - wall.start[0];
- const dy = wall.end[1] - wall.start[1];
- return Math.sqrt(dx * dx + dy * dy);
+ const wall = this.walls.get(wallId)
+ if (!wall) return 0
+ const dx = wall.end[0] - wall.start[0]
+ const dy = wall.end[1] - wall.start[1]
+ return Math.sqrt(dx * dx + dy * dy)
}
private getWallHeight(wallId: string): number {
- const wall = this.walls.get(wallId);
- return wall?.height ?? 2.5; // Default wall height
+ const wall = this.walls.get(wallId)
+ return wall?.height ?? 2.5 // Default wall height
}
// Called when nodes change
handleNodeCreated(node: AnyNode, levelId: string) {
- if (node.type === "wall") {
- const wall = node as WallNode;
- this.walls.set(wall.id, wall);
- } else if (node.type === "item") {
- const item = node as ItemNode;
- if (
- item.asset.attachTo === "wall" ||
- item.asset.attachTo === "wall-side"
- ) {
+ if (node.type === 'wall') {
+ const wall = node as WallNode
+ this.walls.set(wall.id, wall)
+ } else if (node.type === 'item') {
+ const item = node as ItemNode
+ if (item.asset.attachTo === 'wall' || item.asset.attachTo === 'wall-side') {
// Wall-attached item - use parentId as the wall ID
- const wallId = item.parentId;
+ const wallId = item.parentId
if (wallId && this.walls.has(wallId)) {
- const wallLength = this.getWallLength(wallId);
+ const wallLength = this.getWallLength(wallId)
if (wallLength > 0) {
- const [width, height] = item.asset.dimensions;
- const halfW = width / wallLength / 2;
+ const [width, height] = item.asset.dimensions
+ const halfW = width / wallLength / 2
// Calculate t from local X position (position[0] is distance along wall)
- const t = item.position[0] / wallLength;
+ const t = item.position[0] / wallLength
// position[1] is the bottom of the item
this.getWallGrid(levelId).insert({
itemId: item.id,
@@ -67,7 +61,7 @@ export class SpatialGridManager {
tEnd: t + halfW,
yStart: item.position[1],
yEnd: item.position[1] + height,
- });
+ })
}
}
} else if (!item.asset.attachTo) {
@@ -77,31 +71,28 @@ export class SpatialGridManager {
item.position,
item.asset.dimensions,
item.rotation,
- );
+ )
}
}
}
handleNodeUpdated(node: AnyNode, levelId: string) {
- if (node.type === "wall") {
- const wall = node as WallNode;
- this.walls.set(wall.id, wall);
- } else if (node.type === "item") {
- const item = node as ItemNode;
- if (
- item.asset.attachTo === "wall" ||
- item.asset.attachTo === "wall-side"
- ) {
+ if (node.type === 'wall') {
+ const wall = node as WallNode
+ this.walls.set(wall.id, wall)
+ } else if (node.type === 'item') {
+ const item = node as ItemNode
+ if (item.asset.attachTo === 'wall' || item.asset.attachTo === 'wall-side') {
// Remove old placement and re-insert
- this.getWallGrid(levelId).removeByItemId(item.id);
- const wallId = item.parentId;
+ this.getWallGrid(levelId).removeByItemId(item.id)
+ const wallId = item.parentId
if (wallId && this.walls.has(wallId)) {
- const wallLength = this.getWallLength(wallId);
+ const wallLength = this.getWallLength(wallId)
if (wallLength > 0) {
- const [width, height] = item.asset.dimensions;
- const halfW = width / wallLength / 2;
+ const [width, height] = item.asset.dimensions
+ const halfW = width / wallLength / 2
// Calculate t from local X position (position[0] is distance along wall)
- const t = item.position[0] / wallLength;
+ const t = item.position[0] / wallLength
// position[1] is the bottom of the item
this.getWallGrid(levelId).insert({
itemId: item.id,
@@ -110,7 +101,7 @@ export class SpatialGridManager {
tEnd: t + halfW,
yStart: item.position[1],
yEnd: item.position[1] + height,
- });
+ })
}
}
} else if (!item.asset.attachTo) {
@@ -119,22 +110,22 @@ export class SpatialGridManager {
item.position,
item.asset.dimensions,
item.rotation,
- );
+ )
}
}
}
handleNodeDeleted(nodeId: string, nodeType: string, levelId: string) {
- if (nodeType === "wall") {
- this.walls.delete(nodeId);
+ if (nodeType === 'wall') {
+ this.walls.delete(nodeId)
// Remove all items attached to this wall from the spatial grid
- const removedItemIds = this.getWallGrid(levelId).removeWall(nodeId);
- return removedItemIds; // Caller can use this to delete the items from scene
- } else if (nodeType === "item") {
- this.getFloorGrid(levelId).remove(nodeId);
- this.getWallGrid(levelId).removeByItemId(nodeId);
+ const removedItemIds = this.getWallGrid(levelId).removeWall(nodeId)
+ return removedItemIds // Caller can use this to delete the items from scene
+ } else if (nodeType === 'item') {
+ this.getFloorGrid(levelId).remove(nodeId)
+ this.getWallGrid(levelId).removeByItemId(nodeId)
}
- return [];
+ return []
}
// Query methods
@@ -145,8 +136,8 @@ export class SpatialGridManager {
rotation: [number, number, number],
ignoreIds?: string[],
) {
- const grid = this.getFloorGrid(levelId);
- return grid.canPlace(position, dimensions, rotation, ignoreIds);
+ const grid = this.getFloorGrid(levelId)
+ return grid.canPlace(position, dimensions, rotation, ignoreIds)
}
/**
@@ -166,14 +157,14 @@ export class SpatialGridManager {
dimensions: [number, number, number],
ignoreIds?: string[],
) {
- const wallLength = this.getWallLength(wallId);
+ const wallLength = this.getWallLength(wallId)
if (wallLength === 0) {
- return { valid: false, conflictIds: [] };
+ return { valid: false, conflictIds: [] }
}
- const wallHeight = this.getWallHeight(wallId);
+ const wallHeight = this.getWallHeight(wallId)
// Convert local X position to parametric t (0-1)
- const tCenter = localX / wallLength;
- const [itemWidth, itemHeight] = dimensions;
+ const tCenter = localX / wallLength
+ const [itemWidth, itemHeight] = dimensions
return this.getWallGrid(levelId).canPlaceOnWall(
wallId,
wallLength,
@@ -183,24 +174,24 @@ export class SpatialGridManager {
localY,
itemHeight,
ignoreIds,
- );
+ )
}
getWallForItem(levelId: string, itemId: string): string | undefined {
- return this.getWallGrid(levelId).getWallForItem(itemId);
+ return this.getWallGrid(levelId).getWallForItem(itemId)
}
clearLevel(levelId: string) {
- this.floorGrids.delete(levelId);
- this.wallGrids.delete(levelId);
+ this.floorGrids.delete(levelId)
+ this.wallGrids.delete(levelId)
}
clear() {
- this.floorGrids.clear();
- this.wallGrids.clear();
- this.walls.clear();
+ this.floorGrids.clear()
+ this.wallGrids.clear()
+ this.walls.clear()
}
}
// Singleton instance
-export const spatialGridManager = new SpatialGridManager();
+export const spatialGridManager = new SpatialGridManager()
diff --git a/packages/core/src/hooks/spatial-grid/spatial-grid-sync.ts b/packages/core/src/hooks/spatial-grid/spatial-grid-sync.ts
index 4f658f8c..6668664b 100644
--- a/packages/core/src/hooks/spatial-grid/spatial-grid-sync.ts
+++ b/packages/core/src/hooks/spatial-grid/spatial-grid-sync.ts
@@ -1,70 +1,67 @@
-import { AnyNode } from "../../schema";
-import useScene from "../../store/use-scene";
-import { spatialGridManager } from "./spatial-grid-manager";
+import type { AnyNode } from '../../schema'
+import useScene from '../../store/use-scene'
+import { spatialGridManager } from './spatial-grid-manager'
-export function resolveLevelId(
- node: AnyNode,
- nodes: Record,
-): string {
+export function resolveLevelId(node: AnyNode, nodes: Record): string {
// If the node itself is a level
- if (node.type === "level") return node.id;
+ if (node.type === 'level') return node.id
// Walk up parent chain to find level
// This assumes you track parentId or can derive it
- let current: AnyNode | undefined = node;
+ let current: AnyNode | undefined = node
while (current) {
- if (current.type === "level") return current.id;
+ if (current.type === 'level') return current.id
// Find parent (you might need to add parentId to your schema or derive it)
if (!current.parentId) {
- current = undefined;
+ current = undefined
} else {
- current = nodes[current.parentId];
+ current = nodes[current.parentId]
}
}
- return "default"; // fallback for orphaned items
+ return 'default' // fallback for orphaned items
}
// Call this once at app initialization
export function initSpatialGridSync() {
- const store = useScene;
+ const store = useScene
// Subscribe to all changes
store.subscribe((state, prevState) => {
// Detect added nodes
for (const [id, node] of Object.entries(state.nodes)) {
- if (!prevState.nodes[id as AnyNode["id"]]) {
- const levelId = resolveLevelId(node, state.nodes);
- spatialGridManager.handleNodeCreated(node, levelId);
+ if (!prevState.nodes[id as AnyNode['id']]) {
+ const levelId = resolveLevelId(node, state.nodes)
+ spatialGridManager.handleNodeCreated(node, levelId)
}
}
// Detect removed nodes
for (const [id, node] of Object.entries(prevState.nodes)) {
- if (!state.nodes[id as AnyNode["id"]]) {
- const levelId = resolveLevelId(node, prevState.nodes);
- spatialGridManager.handleNodeDeleted(id, node.type, levelId);
+ if (!state.nodes[id as AnyNode['id']]) {
+ const levelId = resolveLevelId(node, prevState.nodes)
+ spatialGridManager.handleNodeDeleted(id, node.type, levelId)
}
}
// Detect updated nodes (items with position/rotation/parentId changes)
for (const [id, node] of Object.entries(state.nodes)) {
- const prev = prevState.nodes[id as AnyNode["id"]];
- if (prev && node.type === "item" && prev.type === "item") {
+ const prev = prevState.nodes[id as AnyNode['id']]
+ if (prev && node.type === 'item' && prev.type === 'item') {
if (
!arraysEqual(node.position, prev.position) ||
!arraysEqual(node.rotation, prev.rotation) ||
node.parentId !== prev.parentId
) {
- const levelId = resolveLevelId(node, state.nodes);
- spatialGridManager.handleNodeUpdated(node, levelId);
+ const levelId = resolveLevelId(node, state.nodes)
+ spatialGridManager.handleNodeUpdated(node, levelId)
}
}
}
- });
+ })
}
function arraysEqual(a: number[], b: number[]): boolean {
- return a.length === b.length && a.every((v, i) => v === b[i]);
+ return a.length === b.length && a.every((v, i) => v === b[i])
}
diff --git a/packages/core/src/hooks/spatial-grid/spatial-grid.ts b/packages/core/src/hooks/spatial-grid/spatial-grid.ts
index 9fe6061e..d23c0340 100644
--- a/packages/core/src/hooks/spatial-grid/spatial-grid.ts
+++ b/packages/core/src/hooks/spatial-grid/spatial-grid.ts
@@ -1,28 +1,25 @@
-type CellKey = `${number},${number}`;
+type CellKey = `${number},${number}`
interface GridCell {
- itemIds: Set;
+ itemIds: Set
}
interface SpatialGridConfig {
- cellSize: number; // e.g., 0.5 meters = Sims-style half-tile
+ cellSize: number // e.g., 0.5 meters = Sims-style half-tile
}
export class SpatialGrid {
- private cells = new Map();
- private itemCells = new Map>(); // reverse lookup
+ private cells = new Map()
+ private itemCells = new Map>() // reverse lookup
constructor(private config: SpatialGridConfig) {}
private posToCell(x: number, z: number): [number, number] {
- return [
- Math.floor(x / this.config.cellSize),
- Math.floor(z / this.config.cellSize),
- ];
+ return [Math.floor(x / this.config.cellSize), Math.floor(z / this.config.cellSize)]
}
private cellKey(cx: number, cz: number): CellKey {
- return `${cx},${cz}`;
+ return `${cx},${cz}`
}
// Get all cells an item occupies based on its AABB
@@ -33,34 +30,34 @@ export class SpatialGrid {
): CellKey[] {
// Simplified: axis-aligned bounding box
// For full rotation support, compute rotated corners
- const [x, , z] = position;
- const [w, , d] = dimensions;
- const yRot = rotation[1]; // Y-axis rotation
+ const [x, , z] = position
+ const [w, , d] = dimensions
+ const yRot = rotation[1] // Y-axis rotation
// Compute rotated footprint (simplified for 90° increments)
- const cos = Math.abs(Math.cos(yRot));
- const sin = Math.abs(Math.sin(yRot));
- const rotatedW = w * cos + d * sin;
- const rotatedD = w * sin + d * cos;
+ const cos = Math.abs(Math.cos(yRot))
+ const sin = Math.abs(Math.sin(yRot))
+ const rotatedW = w * cos + d * sin
+ const rotatedD = w * sin + d * cos
- const minX = x - rotatedW / 2;
- const maxX = x + rotatedW / 2;
- const minZ = z - rotatedD / 2;
- const maxZ = z + rotatedD / 2;
+ const minX = x - rotatedW / 2
+ const maxX = x + rotatedW / 2
+ const minZ = z - rotatedD / 2
+ const maxZ = z + rotatedD / 2
- const [minCx, minCz] = this.posToCell(minX, minZ);
+ const [minCx, minCz] = this.posToCell(minX, minZ)
// Use exclusive upper bound: subtract epsilon so exact boundaries don't overlap
// This allows adjacent items (touching but not overlapping) to not conflict
- const epsilon = 1e-6;
- const [maxCx, maxCz] = this.posToCell(maxX - epsilon, maxZ - epsilon);
+ const epsilon = 1e-6
+ const [maxCx, maxCz] = this.posToCell(maxX - epsilon, maxZ - epsilon)
- const keys: CellKey[] = [];
+ const keys: CellKey[] = []
for (let cx = minCx; cx <= maxCx; cx++) {
for (let cz = minCz; cz <= maxCz; cz++) {
- keys.push(this.cellKey(cx, cz));
+ keys.push(this.cellKey(cx, cz))
}
}
- return keys;
+ return keys
}
// Register an item
@@ -70,33 +67,33 @@ export class SpatialGrid {
dimensions: [number, number, number],
rotation: [number, number, number],
) {
- const cellKeys = this.getItemCells(position, dimensions, rotation);
+ const cellKeys = this.getItemCells(position, dimensions, rotation)
- this.itemCells.set(itemId, new Set(cellKeys));
+ this.itemCells.set(itemId, new Set(cellKeys))
for (const key of cellKeys) {
if (!this.cells.has(key)) {
- this.cells.set(key, { itemIds: new Set() });
+ this.cells.set(key, { itemIds: new Set() })
}
- this.cells.get(key)!.itemIds.add(itemId);
+ this.cells.get(key)!.itemIds.add(itemId)
}
}
// Remove an item
remove(itemId: string) {
- const cellKeys = this.itemCells.get(itemId);
- if (!cellKeys) return;
+ const cellKeys = this.itemCells.get(itemId)
+ if (!cellKeys) return
for (const key of cellKeys) {
- const cell = this.cells.get(key);
+ const cell = this.cells.get(key)
if (cell) {
- cell.itemIds.delete(itemId);
+ cell.itemIds.delete(itemId)
if (cell.itemIds.size === 0) {
- this.cells.delete(key);
+ this.cells.delete(key)
}
}
}
- this.itemCells.delete(itemId);
+ this.itemCells.delete(itemId)
}
// Update = remove + insert
@@ -106,8 +103,8 @@ export class SpatialGrid {
dimensions: [number, number, number],
rotation: [number, number, number],
) {
- this.remove(itemId);
- this.insert(itemId, position, dimensions, rotation);
+ this.remove(itemId)
+ this.insert(itemId, position, dimensions, rotation)
}
// Query: is this placement valid?
@@ -117,16 +114,16 @@ export class SpatialGrid {
rotation: [number, number, number],
ignoreIds: string[] = [],
): { valid: boolean; conflictIds: string[] } {
- const cellKeys = this.getItemCells(position, dimensions, rotation);
- const ignoreSet = new Set(ignoreIds);
- const conflicts = new Set();
+ const cellKeys = this.getItemCells(position, dimensions, rotation)
+ const ignoreSet = new Set(ignoreIds)
+ const conflicts = new Set()
for (const key of cellKeys) {
- const cell = this.cells.get(key);
+ const cell = this.cells.get(key)
if (cell) {
for (const id of cell.itemIds) {
if (!ignoreSet.has(id)) {
- conflicts.add(id);
+ conflicts.add(id)
}
}
}
@@ -135,29 +132,29 @@ export class SpatialGrid {
return {
valid: conflicts.size === 0,
conflictIds: [...conflicts],
- };
+ }
}
// Query: get all items near a point (for snapping, selection, etc.)
queryRadius(x: number, z: number, radius: number): string[] {
- const cellRadius = Math.ceil(radius / this.config.cellSize);
- const [cx, cz] = this.posToCell(x, z);
- const found = new Set();
+ const cellRadius = Math.ceil(radius / this.config.cellSize)
+ const [cx, cz] = this.posToCell(x, z)
+ const found = new Set()
for (let dx = -cellRadius; dx <= cellRadius; dx++) {
for (let dz = -cellRadius; dz <= cellRadius; dz++) {
- const cell = this.cells.get(this.cellKey(cx + dx, cz + dz));
+ const cell = this.cells.get(this.cellKey(cx + dx, cz + dz))
if (cell) {
for (const id of cell.itemIds) {
- found.add(id);
+ found.add(id)
}
}
}
}
- return [...found];
+ return [...found]
}
getItemCount(): number {
- return this.itemCells.size;
+ return this.itemCells.size
}
}
diff --git a/packages/core/src/hooks/spatial-grid/use-spatial-query.ts b/packages/core/src/hooks/spatial-grid/use-spatial-query.ts
index fe1e1ade..2d16d9b7 100644
--- a/packages/core/src/hooks/spatial-grid/use-spatial-query.ts
+++ b/packages/core/src/hooks/spatial-grid/use-spatial-query.ts
@@ -1,31 +1,25 @@
-import { useCallback } from "react";
-import { LevelNode, WallNode } from "../../schema";
-import { spatialGridManager } from "./spatial-grid-manager";
+import { useCallback } from 'react'
+import type { LevelNode, WallNode } from '../../schema'
+import { spatialGridManager } from './spatial-grid-manager'
export function useSpatialQuery() {
const canPlaceOnFloor = useCallback(
(
- levelId: LevelNode["id"],
+ levelId: LevelNode['id'],
position: [number, number, number],
dimensions: [number, number, number],
rotation: [number, number, number],
ignoreIds?: string[],
) => {
- return spatialGridManager.canPlaceOnFloor(
- levelId,
- position,
- dimensions,
- rotation,
- ignoreIds,
- );
+ return spatialGridManager.canPlaceOnFloor(levelId, position, dimensions, rotation, ignoreIds)
},
[],
- );
+ )
const canPlaceOnWall = useCallback(
(
- levelId: LevelNode["id"],
- wallId: WallNode["id"],
+ levelId: LevelNode['id'],
+ wallId: WallNode['id'],
localX: number,
localY: number,
dimensions: [number, number, number],
@@ -38,10 +32,10 @@ export function useSpatialQuery() {
localY,
dimensions,
ignoreIds,
- );
+ )
},
[],
- );
+ )
- return { canPlaceOnFloor, canPlaceOnWall };
+ return { canPlaceOnFloor, canPlaceOnWall }
}
diff --git a/packages/core/src/hooks/spatial-grid/wall-spatial-grid.ts b/packages/core/src/hooks/spatial-grid/wall-spatial-grid.ts
index af421c0b..deb60c6d 100644
--- a/packages/core/src/hooks/spatial-grid/wall-spatial-grid.ts
+++ b/packages/core/src/hooks/spatial-grid/wall-spatial-grid.ts
@@ -1,15 +1,15 @@
interface WallItemPlacement {
- itemId: string;
- wallId: string;
- tStart: number; // 0-1 parametric position along wall
- tEnd: number;
- yStart: number; // height range
- yEnd: number;
+ itemId: string
+ wallId: string
+ tStart: number // 0-1 parametric position along wall
+ tEnd: number
+ yStart: number // height range
+ yEnd: number
}
export class WallSpatialGrid {
- private wallItems = new Map(); // wallId -> placements
- private itemToWall = new Map(); // itemId -> wallId (reverse lookup)
+ private wallItems = new Map() // wallId -> placements
+ private itemToWall = new Map() // itemId -> wallId (reverse lookup)
canPlaceOnWall(
wallId: string,
@@ -21,82 +21,82 @@ export class WallSpatialGrid {
itemHeight: number,
ignoreIds: string[] = [],
): { valid: boolean; conflictIds: string[] } {
- const halfW = itemWidth / wallLength / 2;
- const tStart = tCenter - halfW;
- const tEnd = tCenter + halfW;
+ const halfW = itemWidth / wallLength / 2
+ const tStart = tCenter - halfW
+ const tEnd = tCenter + halfW
// yBottom is the bottom of the item, so yEnd = yBottom + itemHeight
- const yStart = yBottom;
- const yEnd = yBottom + itemHeight;
+ const yStart = yBottom
+ const yEnd = yBottom + itemHeight
// Check wall boundaries
if (tStart < 0 || tEnd > 1 || yStart < 0 || yEnd > wallHeight) {
- return { valid: false, conflictIds: [] };
+ return { valid: false, conflictIds: [] }
}
- const existing = this.wallItems.get(wallId) ?? [];
- const ignoreSet = new Set(ignoreIds);
- const conflicts: string[] = [];
+ const existing = this.wallItems.get(wallId) ?? []
+ const ignoreSet = new Set(ignoreIds)
+ const conflicts: string[] = []
for (const placement of existing) {
- if (ignoreSet.has(placement.itemId)) continue;
+ if (ignoreSet.has(placement.itemId)) continue
- const tOverlap = tStart < placement.tEnd && tEnd > placement.tStart;
- const yOverlap = yStart < placement.yEnd && yEnd > placement.yStart;
+ const tOverlap = tStart < placement.tEnd && tEnd > placement.tStart
+ const yOverlap = yStart < placement.yEnd && yEnd > placement.yStart
if (tOverlap && yOverlap) {
- conflicts.push(placement.itemId);
+ conflicts.push(placement.itemId)
}
}
- return { valid: conflicts.length === 0, conflictIds: conflicts };
+ return { valid: conflicts.length === 0, conflictIds: conflicts }
}
insert(placement: WallItemPlacement) {
- const { wallId, itemId } = placement;
+ const { wallId, itemId } = placement
if (!this.wallItems.has(wallId)) {
- this.wallItems.set(wallId, []);
+ this.wallItems.set(wallId, [])
}
- this.wallItems.get(wallId)!.push(placement);
- this.itemToWall.set(itemId, wallId);
+ this.wallItems.get(wallId)!.push(placement)
+ this.itemToWall.set(itemId, wallId)
}
remove(wallId: string, itemId: string) {
- const items = this.wallItems.get(wallId);
+ const items = this.wallItems.get(wallId)
if (items) {
- const idx = items.findIndex((p) => p.itemId === itemId);
- if (idx !== -1) items.splice(idx, 1);
+ const idx = items.findIndex((p) => p.itemId === itemId)
+ if (idx !== -1) items.splice(idx, 1)
}
- this.itemToWall.delete(itemId);
+ this.itemToWall.delete(itemId)
}
removeByItemId(itemId: string) {
- const wallId = this.itemToWall.get(itemId);
+ const wallId = this.itemToWall.get(itemId)
if (wallId) {
- this.remove(wallId, itemId);
+ this.remove(wallId, itemId)
}
}
// Useful for when a wall is deleted - remove all items on it
removeWall(wallId: string): string[] {
- const items = this.wallItems.get(wallId) ?? [];
- const removedIds = items.map((p) => p.itemId);
+ const items = this.wallItems.get(wallId) ?? []
+ const removedIds = items.map((p) => p.itemId)
for (const itemId of removedIds) {
- this.itemToWall.delete(itemId);
+ this.itemToWall.delete(itemId)
}
- this.wallItems.delete(wallId);
+ this.wallItems.delete(wallId)
- return removedIds; // Return removed item IDs in case you need to delete them from scene
+ return removedIds // Return removed item IDs in case you need to delete them from scene
}
// Get which wall an item is on
getWallForItem(itemId: string): string | undefined {
- return this.itemToWall.get(itemId);
+ return this.itemToWall.get(itemId)
}
clear() {
- this.wallItems.clear();
- this.itemToWall.clear();
+ this.wallItems.clear()
+ this.itemToWall.clear()
}
}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 83c4ed8a..bc0df323 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -1,27 +1,23 @@
// Store
-export { default as useScene } from "./store/use-scene";
+export type {
+ EventSuffix,
+ GridEvent,
+ ItemEvent,
+ NodeEvent,
+ WallEvent,
+} from './events/bus'
+// Events
+export { emitter, eventSuffixes } from './events/bus'
// Hooks
export {
sceneRegistry,
useRegistry,
-} from "./hooks/scene-registry/scene-registry";
-
-export { useSpatialQuery } from "./hooks/spatial-grid/use-spatial-query";
-export { initSpatialGridSync } from "./hooks/spatial-grid/spatial-grid-sync";
-
-// Systems
-export { WallSystem } from "./systems/wall/wall-system";
-
-// Events
-export { emitter, eventSuffixes } from "./events/bus";
-export type {
- ItemEvent,
- WallEvent,
- NodeEvent,
- GridEvent,
- EventSuffix,
-} from "./events/bus";
-
+} from './hooks/scene-registry/scene-registry'
+export { initSpatialGridSync } from './hooks/spatial-grid/spatial-grid-sync'
+export { useSpatialQuery } from './hooks/spatial-grid/use-spatial-query'
// Schema
-export * from "./schema";
+export * from './schema'
+export { default as useScene } from './store/use-scene'
+// Systems
+export { WallSystem } from './systems/wall/wall-system'
diff --git a/packages/core/src/schema/base.ts b/packages/core/src/schema/base.ts
index 154a5fa4..0e24ddf2 100644
--- a/packages/core/src/schema/base.ts
+++ b/packages/core/src/schema/base.ts
@@ -1,33 +1,32 @@
-import { customAlphabet } from "nanoid";
-import { z } from "zod";
-import { CameraSchema } from "./camera";
+import { customAlphabet } from 'nanoid'
+import { z } from 'zod'
+import { CameraSchema } from './camera'
-const customId = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 16);
+const customId = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 16)
/**
* Material preset name reference
* @example 'white', 'brick', 'wood', 'glass', 'preview-valid'
*/
-export const Material = z.string().optional();
+export const Material = z.string().optional()
export const generateId = (prefix: T): `${T}_${string}` =>
- `${prefix}_${customId()}` as `${T}_${string}`;
+ `${prefix}_${customId()}` as `${T}_${string}`
export const objectId = (prefix: T) => {
- const schema = z.templateLiteral([`${prefix}_`, z.string()]);
+ const schema = z.templateLiteral([`${prefix}_`, z.string()])
- return schema.default(() => generateId(prefix) as z.infer);
-};
-export const nodeType = (type: T) =>
- z.literal(type).default(type);
+ return schema.default(() => generateId(prefix) as z.infer)
+}
+export const nodeType = (type: T) => z.literal(type).default(type)
export const BaseNode = z.object({
- object: z.literal("node").default("node"),
+ object: z.literal('node').default('node'),
id: z.string(), // objectId('node'), @Aymericr: Thing is if we specify objectId here, when using BaseNode.extend, TS complains that the id is not assignable to the more specific type in the extended node
- type: nodeType("node"),
+ type: nodeType('node'),
name: z.string().optional(),
parentId: z.string().nullable().default(null),
visible: z.boolean().optional().default(true),
camera: CameraSchema.optional(),
metadata: z.json().optional().default({}),
-});
+})
-export type BaseNode = z.infer;
+export type BaseNode = z.infer
diff --git a/packages/core/src/schema/camera.ts b/packages/core/src/schema/camera.ts
index adf74f26..c691eaba 100644
--- a/packages/core/src/schema/camera.ts
+++ b/packages/core/src/schema/camera.ts
@@ -1,13 +1,13 @@
-import { z } from "zod";
+import { z } from 'zod'
-const Vector3Schema = z.tuple([z.number(), z.number(), z.number()]);
+const Vector3Schema = z.tuple([z.number(), z.number(), z.number()])
export const CameraSchema = z.object({
position: Vector3Schema,
target: Vector3Schema,
- mode: z.enum(["perspective", "orthographic"]).default("perspective"),
+ mode: z.enum(['perspective', 'orthographic']).default('perspective'),
fov: z.number().optional(), // For perspective
zoom: z.number().optional(), // For orthographic
-});
+})
-export type Camera = z.infer;
+export type Camera = z.infer
diff --git a/packages/core/src/schema/index.ts b/packages/core/src/schema/index.ts
index 19197c75..4ab32e74 100644
--- a/packages/core/src/schema/index.ts
+++ b/packages/core/src/schema/index.ts
@@ -1,20 +1,17 @@
// Base
-export { BaseNode, generateId, objectId, nodeType, Material } from "./base";
-
-// Nodes
-export { SiteNode } from "./nodes/site";
-export { BuildingNode } from "./nodes/building";
-export { LevelNode } from "./nodes/level";
-export { WallNode } from "./nodes/wall";
-export { ItemNode } from "./nodes/item";
-export type { AssetInput } from "./nodes/item";
-
-// Union types
-export { AnyNode } from "./types";
-export type { AnyNodeType, AnyNodeId } from "./types";
-
+export { BaseNode, generateId, Material, nodeType, objectId } from './base'
// Camera
-export { CameraSchema } from "./camera";
+export { CameraSchema } from './camera'
+export { BuildingNode } from './nodes/building'
+export type { AssetInput } from './nodes/item'
+export { ItemNode } from './nodes/item'
+export { LevelNode } from './nodes/level'
+// Nodes
+export { SiteNode } from './nodes/site'
+export { WallNode } from './nodes/wall'
+export type { AnyNodeId, AnyNodeType } from './types'
+// Union types
+export { AnyNode } from './types'
// Zones
-export type { Zone, ZonePolygon } from "./zone";
+export type { Zone, ZonePolygon } from './zone'
diff --git a/packages/core/src/schema/nodes/building.ts b/packages/core/src/schema/nodes/building.ts
index 1accb4f3..c4a86f5d 100644
--- a/packages/core/src/schema/nodes/building.ts
+++ b/packages/core/src/schema/nodes/building.ts
@@ -1,11 +1,11 @@
-import dedent from "dedent";
-import { z } from "zod";
-import { BaseNode, nodeType, objectId } from "../base";
-import { LevelNode } from "./level";
+import dedent from 'dedent'
+import { z } from 'zod'
+import { BaseNode, nodeType, objectId } from '../base'
+import { LevelNode } from './level'
export const BuildingNode = BaseNode.extend({
- id: objectId("building"),
- type: nodeType("building"),
+ id: objectId('building'),
+ type: nodeType('building'),
children: z.array(LevelNode.shape.id).default([]),
position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
rotation: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
@@ -15,7 +15,7 @@ export const BuildingNode = BaseNode.extend({
- position: position in site coordinate system
- rotation: rotation in site coordinate system
- children: array of level nodes (each level is a tree of floor and wall nodes)
- `
-);
+ `,
+)
-export type BuildingNode = z.infer;
+export type BuildingNode = z.infer
diff --git a/packages/core/src/schema/nodes/item.ts b/packages/core/src/schema/nodes/item.ts
index 6c993346..e6e0122b 100644
--- a/packages/core/src/schema/nodes/item.ts
+++ b/packages/core/src/schema/nodes/item.ts
@@ -1,6 +1,6 @@
-import dedent from "dedent";
-import { z } from "zod";
-import { BaseNode, nodeType, objectId } from "../base";
+import dedent from 'dedent'
+import { z } from 'zod'
+import { BaseNode, nodeType, objectId } from '../base'
const assetSchema = z.object({
id: z.string(),
@@ -9,22 +9,22 @@ const assetSchema = z.object({
thumbnail: z.string(),
src: z.string(),
dimensions: z.tuple([z.number(), z.number(), z.number()]).default([1, 1, 1]), // [w, h, d]
- attachTo: z.enum(["wall", "wall-side", "ceiling"]).optional(),
+ attachTo: z.enum(['wall', 'wall-side', 'ceiling']).optional(),
// These are "Corrective" transforms to normalize the GLB
offset: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
rotation: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
scale: z.tuple([z.number(), z.number(), z.number()]).default([1, 1, 1]),
-});
+})
-export type AssetInput = z.input;
-export type Asset = z.infer;
+export type AssetInput = z.input
+export type Asset = z.infer
export const ItemNode = BaseNode.extend({
- id: objectId("item"),
- type: nodeType("item"),
+ id: objectId('item'),
+ type: nodeType('item'),
position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
rotation: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
- side: z.enum(["front", "back"]).optional(),
+ side: z.enum(['front', 'back']).optional(),
// Wall attachment properties (only used when asset.attachTo is "wall" or "wall-side")
wallId: z.string().optional(),
@@ -42,6 +42,6 @@ export const ItemNode = BaseNode.extend({
- offset: corrective position offset for the model
- rotation: corrective rotation for the model
- scale: corrective scale for the model
-`);
+`)
-export type ItemNode = z.infer;
+export type ItemNode = z.infer
diff --git a/packages/core/src/schema/nodes/level.ts b/packages/core/src/schema/nodes/level.ts
index 24ec738c..1e641669 100644
--- a/packages/core/src/schema/nodes/level.ts
+++ b/packages/core/src/schema/nodes/level.ts
@@ -1,11 +1,11 @@
-import dedent from "dedent";
-import { z } from "zod";
-import { BaseNode, nodeType, objectId } from "../base";
-import { WallNode } from "./wall";
+import dedent from 'dedent'
+import { z } from 'zod'
+import { BaseNode, nodeType, objectId } from '../base'
+import { WallNode } from './wall'
export const LevelNode = BaseNode.extend({
- id: objectId("level"),
- type: nodeType("level"),
+ id: objectId('level'),
+ type: nodeType('level'),
children: z.array(WallNode.shape.id).default([]),
// Specific props
level: z.number().default(0),
@@ -14,7 +14,7 @@ export const LevelNode = BaseNode.extend({
Level node - used to represent a level in the building
- children: array of floor, wall, ceiling, roof, item nodes
- level: level number
- `
-);
+ `,
+)
-export type LevelNode = z.infer;
+export type LevelNode = z.infer
diff --git a/packages/core/src/schema/nodes/site.ts b/packages/core/src/schema/nodes/site.ts
index 3f4cc312..b621c741 100644
--- a/packages/core/src/schema/nodes/site.ts
+++ b/packages/core/src/schema/nodes/site.ts
@@ -1,16 +1,16 @@
// lib/scenegraph/schema/nodes/site.ts
-import dedent from "dedent";
-import { z } from "zod";
-import { BaseNode, nodeType, objectId } from "../base";
-import { BuildingNode } from "./building";
-import { ItemNode } from "./item";
+import dedent from 'dedent'
+import { z } from 'zod'
+import { BaseNode, nodeType, objectId } from '../base'
+import { BuildingNode } from './building'
+import { ItemNode } from './item'
// 2D Polygon
const PropertyLineData = z.object({
- type: z.literal("polygon"),
+ type: z.literal('polygon'),
points: z.array(z.tuple([z.number(), z.number()])),
-});
+})
// 3D Polygon/Mesh
// const TerrainData = z.object({
@@ -19,11 +19,11 @@ const PropertyLineData = z.object({
// })
export const SiteNode = BaseNode.extend({
- id: objectId("site"),
- type: nodeType("site"),
+ id: objectId('site'),
+ type: nodeType('site'),
// Specific props
polygon: PropertyLineData.optional().default({
- type: "polygon",
+ type: 'polygon',
// Default 30x30 square matching GRID_SIZE
points: [
[0, 0],
@@ -34,14 +34,14 @@ export const SiteNode = BaseNode.extend({
}),
// terrain: TerrainData,
children: z
- .array(z.discriminatedUnion("type", [BuildingNode, ItemNode]))
+ .array(z.discriminatedUnion('type', [BuildingNode, ItemNode]))
.default([BuildingNode.parse({})]),
}).describe(
dedent`
Site node - used to represent a site
- polygon: polygon data
- children: array of building and item nodes
- `
-);
+ `,
+)
-export type SiteNode = z.infer;
+export type SiteNode = z.infer
diff --git a/packages/core/src/schema/nodes/wall.ts b/packages/core/src/schema/nodes/wall.ts
index 4bcdf41f..6ce0ddef 100644
--- a/packages/core/src/schema/nodes/wall.ts
+++ b/packages/core/src/schema/nodes/wall.ts
@@ -1,14 +1,14 @@
-import dedent from "dedent";
-import { z } from "zod";
-import { BaseNode, nodeType, objectId } from "../base";
-import { ItemNode } from "./item";
+import dedent from 'dedent'
+import { z } from 'zod'
+import { BaseNode, nodeType, objectId } from '../base'
+import { ItemNode } from './item'
// import { DoorNode } from "./door";
// import { ItemNode } from "./item";
// import { WindowNode } from "./window";
export const WallNode = BaseNode.extend({
- id: objectId("wall"),
- type: nodeType("wall"),
+ id: objectId('wall'),
+ type: nodeType('wall'),
children: z.array(ItemNode.shape.id).default([]),
// Specific props
thickness: z.number().optional(),
@@ -24,6 +24,6 @@ export const WallNode = BaseNode.extend({
- start: start point of the wall in level coordinate system
- end: end point of the wall in level coordinate system
- size: size of the wall in grid units
- `
-);
-export type WallNode = z.infer;
+ `,
+)
+export type WallNode = z.infer
diff --git a/packages/core/src/schema/types.ts b/packages/core/src/schema/types.ts
index b8b48434..1d8b80b1 100644
--- a/packages/core/src/schema/types.ts
+++ b/packages/core/src/schema/types.ts
@@ -1,18 +1,18 @@
-import z from "zod";
-import { BuildingNode } from "./nodes/building";
-import { ItemNode } from "./nodes/item";
-import { LevelNode } from "./nodes/level";
-import { SiteNode } from "./nodes/site";
-import { WallNode } from "./nodes/wall";
+import z from 'zod'
+import { BuildingNode } from './nodes/building'
+import { ItemNode } from './nodes/item'
+import { LevelNode } from './nodes/level'
+import { SiteNode } from './nodes/site'
+import { WallNode } from './nodes/wall'
-export const AnyNode = z.discriminatedUnion("type", [
+export const AnyNode = z.discriminatedUnion('type', [
SiteNode,
BuildingNode,
LevelNode,
WallNode,
ItemNode,
-]);
+])
-export type AnyNode = z.infer;
-export type AnyNodeType = AnyNode["type"];
-export type AnyNodeId = AnyNode["id"];
+export type AnyNode = z.infer
+export type AnyNodeType = AnyNode['type']
+export type AnyNodeId = AnyNode['id']
diff --git a/packages/core/src/schema/zone.ts b/packages/core/src/schema/zone.ts
index bf6abf30..f79b6b34 100644
--- a/packages/core/src/schema/zone.ts
+++ b/packages/core/src/schema/zone.ts
@@ -1,21 +1,21 @@
-import dedent from "dedent";
-import { z } from "zod";
-import { objectId } from "./base";
-import { LevelNode } from "./nodes/level";
+import dedent from 'dedent'
+import { z } from 'zod'
+import { objectId } from './base'
+import { LevelNode } from './nodes/level'
// Polygon boundary for zone area - array of [x, z] coordinates
-export const ZonePolygon = z.array(z.tuple([z.number(), z.number()]));
+export const ZonePolygon = z.array(z.tuple([z.number(), z.number()]))
export const ZoneSchema = z
.object({
- id: objectId("zone"),
- object: z.literal("zone").default("zone"),
+ id: objectId('zone'),
+ object: z.literal('zone').default('zone'),
levelId: LevelNode.shape.id, // Required - must be attached to a level
name: z.string(),
// Polygon boundary - array of [x, z] coordinates defining the zone
polygon: ZonePolygon,
// Visual styling
- color: z.string().default("#3b82f6"), // Default blue
+ color: z.string().default('#3b82f6'), // Default blue
metadata: z.json().optional().default({}),
})
.describe(
@@ -29,7 +29,7 @@ export const ZoneSchema = z
- color: hex color for visual styling
- metadata: zone metadata (optional)
`,
- );
+ )
-export type Zone = z.infer;
-export type ZonePolygon = z.infer;
+export type Zone = z.infer
+export type ZonePolygon = z.infer
diff --git a/packages/core/src/store/actions/node-actions.ts b/packages/core/src/store/actions/node-actions.ts
index 73b42a45..a2c7b410 100644
--- a/packages/core/src/store/actions/node-actions.ts
+++ b/packages/core/src/store/actions/node-actions.ts
@@ -1,5 +1,5 @@
-import { AnyNode, AnyNodeId } from "../../schema";
-import { SceneState } from "../use-scene";
+import type { AnyNode, AnyNodeId } from '../../schema'
+import type { SceneState } from '../use-scene'
export const createNodesAction = (
set: (fn: (state: SceneState) => Partial) => void,
@@ -7,145 +7,140 @@ export const createNodesAction = (
ops: { node: AnyNode; parentId?: AnyNodeId }[],
) => {
set((state) => {
- const nextNodes = { ...state.nodes };
- const nextRootIds = [...state.rootNodeIds];
+ const nextNodes = { ...state.nodes }
+ const nextRootIds = [...state.rootNodeIds]
for (const { node, parentId } of ops) {
// 1. Assign parentId to the child (Safe because BaseNode has parentId)
const newNode = {
...node,
parentId: parentId ?? null,
- };
+ }
- nextNodes[newNode.id] = newNode;
+ nextNodes[newNode.id] = newNode
// 2. Update the Parent's children list
if (parentId && nextNodes[parentId]) {
- const parent = nextNodes[parentId];
+ const parent = nextNodes[parentId]
// Type Guard: Check if the parent node is a container that supports children
- if ("children" in parent && Array.isArray(parent.children)) {
+ if ('children' in parent && Array.isArray(parent.children)) {
nextNodes[parentId] = {
...parent,
// Use Set to prevent duplicate IDs if createNode is called twice
- children: Array.from(
- new Set([...parent.children, newNode.id]),
- ) as any, // We don't verify child types here
- };
+ children: Array.from(new Set([...parent.children, newNode.id])) as any, // We don't verify child types here
+ }
}
} else if (!parentId) {
// 3. Handle Root nodes
if (!nextRootIds.includes(newNode.id)) {
- nextRootIds.push(newNode.id);
+ nextRootIds.push(newNode.id)
}
}
}
- return { nodes: nextNodes, rootNodeIds: nextRootIds };
- });
+ return { nodes: nextNodes, rootNodeIds: nextRootIds }
+ })
// 4. System Sync
ops.forEach(({ node, parentId }) => {
- get().markDirty(node.id);
- if (parentId) get().markDirty(parentId);
- });
-};
+ get().markDirty(node.id)
+ if (parentId) get().markDirty(parentId)
+ })
+}
export const updateNodesAction = (
set: (fn: (state: SceneState) => Partial) => void,
get: () => SceneState,
updates: { id: AnyNodeId; data: Partial }[],
) => {
- const parentsToUpdate = new Set();
+ const parentsToUpdate = new Set()
set((state) => {
- const nextNodes = { ...state.nodes };
+ const nextNodes = { ...state.nodes }
for (const { id, data } of updates) {
- const currentNode = nextNodes[id];
- if (!currentNode) continue;
+ const currentNode = nextNodes[id]
+ if (!currentNode) continue
// Handle Reparenting Logic
- if (
- data.parentId !== undefined &&
- data.parentId !== currentNode.parentId
- ) {
+ if (data.parentId !== undefined && data.parentId !== currentNode.parentId) {
// 1. Remove from old parent
if (currentNode.parentId && nextNodes[currentNode.parentId]) {
- const oldParent = nextNodes[currentNode.parentId] as AnyContainerNode;
+ const oldParent = nextNodes[currentNode.parentId] as AnyContainerNode
nextNodes[oldParent.id] = {
...oldParent,
children: oldParent.children.filter((childId) => childId !== id),
- };
- parentsToUpdate.add(oldParent.id);
+ }
+ parentsToUpdate.add(oldParent.id)
}
// 2. Add to new parent
if (data.parentId && nextNodes[data.parentId]) {
- const newParent = nextNodes[data.parentId] as AnyContainerNode;
+ const newParent = nextNodes[data.parentId] as AnyContainerNode
nextNodes[newParent.id] = {
...newParent,
children: Array.from(new Set([...newParent.children, id])),
- };
- parentsToUpdate.add(newParent.id);
+ }
+ parentsToUpdate.add(newParent.id)
}
}
// Apply the update
- nextNodes[id] = { ...nextNodes[id], ...data };
+ nextNodes[id] = { ...nextNodes[id], ...data }
}
- return { nodes: nextNodes };
- });
+ return { nodes: nextNodes }
+ })
// Mark dirty
- updates.forEach((u) => get().markDirty(u.id));
- parentsToUpdate.forEach((pId) => get().markDirty(pId));
-};
+ updates.forEach((u) => get().markDirty(u.id))
+ parentsToUpdate.forEach((pId) => get().markDirty(pId))
+}
export const deleteNodesAction = (
set: (fn: (state: SceneState) => Partial) => void,
get: () => SceneState,
ids: AnyNodeId[],
) => {
- const parentsToMarkDirty = new Set();
+ const parentsToMarkDirty = new Set()
set((state) => {
- const nextNodes = { ...state.nodes };
- let nextRootIds = [...state.rootNodeIds];
+ const nextNodes = { ...state.nodes }
+ let nextRootIds = [...state.rootNodeIds]
for (const id of ids) {
- const node = nextNodes[id];
- if (!node) continue;
+ const node = nextNodes[id]
+ if (!node) continue
// 1. Remove reference from Parent
if (node.parentId && nextNodes[node.parentId]) {
- const parent = nextNodes[node.parentId] as AnyContainerNode;
+ const parent = nextNodes[node.parentId] as AnyContainerNode
if (parent.children) {
nextNodes[parent.id] = {
...parent,
children: parent.children.filter((cid) => cid !== id),
- };
- parentsToMarkDirty.add(parent.id);
+ }
+ parentsToMarkDirty.add(parent.id)
}
}
// 2. Remove from Root list
- nextRootIds = nextRootIds.filter((rid) => rid !== id);
+ nextRootIds = nextRootIds.filter((rid) => rid !== id)
// 3. Delete the node itself
- delete nextNodes[id];
+ delete nextNodes[id]
// Inside the deleteNodes loop
- if ("children" in node && node.children.length > 0) {
+ if ('children' in node && node.children.length > 0) {
// Recursively delete all children first
- get().deleteNodes(node.children);
+ get().deleteNodes(node.children)
}
}
- return { nodes: nextNodes, rootNodeIds: nextRootIds };
- });
+ return { nodes: nextNodes, rootNodeIds: nextRootIds }
+ })
// Notify systems that the parent has changed (e.g. Wall needs to fill a window hole)
- parentsToMarkDirty.forEach((pId) => get().markDirty(pId));
-};
+ parentsToMarkDirty.forEach((pId) => get().markDirty(pId))
+}
diff --git a/packages/core/src/store/use-scene.ts b/packages/core/src/store/use-scene.ts
index fc0bd186..a4bb0c38 100644
--- a/packages/core/src/store/use-scene.ts
+++ b/packages/core/src/store/use-scene.ts
@@ -1,37 +1,37 @@
-"use client";
+'use client'
-import { create } from "zustand";
-import { BuildingNode, ItemNode } from "../schema";
-import { LevelNode } from "../schema/nodes/level";
-import { WallNode } from "../schema/nodes/wall";
-import { AnyNode, AnyNodeId } from "../schema/types";
-import { temporal } from "zundo";
-import * as nodeActions from "./actions/node-actions";
+import { temporal } from 'zundo'
+import { create } from 'zustand'
+import { BuildingNode, ItemNode } from '../schema'
+import { LevelNode } from '../schema/nodes/level'
+import { WallNode } from '../schema/nodes/wall'
+import type { AnyNode, AnyNodeId } from '../schema/types'
+import * as nodeActions from './actions/node-actions'
export type SceneState = {
// 1. The Data: A flat dictionary of all nodes
- nodes: Record;
+ nodes: Record
// 2. The Root: Which nodes are at the top level?
- rootNodeIds: AnyNodeId[];
+ rootNodeIds: AnyNodeId[]
// 3. The "Dirty" Set: For the Wall/Physics systems
- dirtyNodes: Set;
+ dirtyNodes: Set
// Actions
- loadScene: () => void;
- markDirty: (id: AnyNodeId) => void;
- clearDirty: (id: AnyNodeId) => void;
+ loadScene: () => void
+ markDirty: (id: AnyNodeId) => void
+ clearDirty: (id: AnyNodeId) => void
- createNode: (node: AnyNode, parentId?: AnyNodeId) => void;
- createNodes: (ops: { node: AnyNode; parentId?: AnyNodeId }[]) => void;
+ createNode: (node: AnyNode, parentId?: AnyNodeId) => void
+ createNodes: (ops: { node: AnyNode; parentId?: AnyNodeId }[]) => void
- updateNode: (id: AnyNodeId, data: Partial) => void;
- updateNodes: (updates: { id: AnyNodeId; data: Partial }[]) => void;
+ updateNode: (id: AnyNodeId, data: Partial) => void
+ updateNodes: (updates: { id: AnyNodeId; data: Partial }[]) => void
- deleteNode: (id: AnyNodeId) => void;
- deleteNodes: (ids: AnyNodeId[]) => void;
-};
+ deleteNode: (id: AnyNodeId) => void
+ deleteNodes: (ids: AnyNodeId[]) => void
+}
// type PartializedStoreState = Pick;
@@ -50,70 +50,70 @@ const useScene = create()(
loadScene: () => {
const building = BuildingNode.parse({
children: [],
- });
+ })
const level0 = LevelNode.parse({
level: 0,
children: [],
- });
+ })
const level1 = LevelNode.parse({
level: 1,
children: [],
- });
+ })
const level2 = LevelNode.parse({
level: 2,
children: [],
- });
+ })
const wall0 = WallNode.parse({
start: [0, 0],
end: [5, 0],
children: [],
parentId: level0.id,
- });
+ })
const wall1 = WallNode.parse({
start: [0, 0],
end: [0, 5],
children: [],
parentId: level0.id,
- });
+ })
const wall2 = WallNode.parse({
start: [5, 5],
end: [0, 5],
children: [],
parentId: level0.id,
- });
+ })
const wall3 = WallNode.parse({
start: [5, 5],
end: [5, 0],
children: [],
parentId: level1.id,
- });
+ })
const window1 = ItemNode.parse({
- type: "item",
- name: "Window",
+ type: 'item',
+ name: 'Window',
position: [2.5, 0.5, 0],
parentId: wall3.id,
asset: {
- id: "window-round",
- name: "Round Window",
- thumbnail: "/items/window-small/thumbnail.png",
- category: "windows",
- attachTo: "wall",
- src: "/items/window-small/model.glb",
+ id: 'window-round',
+ name: 'Round Window',
+ thumbnail: '/items/window-small/thumbnail.png',
+ category: 'windows',
+ attachTo: 'wall',
+ src: '/items/window-small/model.glb',
},
- });
+ })
- wall3.children.push(window1.id);
+ wall3.children.push(window1.id)
- level0.children.push(wall0.id, wall1.id, wall2.id);
- level1.children.push(wall3.id);
+ level0.children.push(wall0.id, wall1.id, wall2.id)
+ level1.children.push(wall3.id)
- building.children.push(level0.id, level1.id, level2.id);
+ building.children.push(level0.id, level1.id, level2.id)
// Define all nodes flat
const nodes: Record = {
@@ -126,34 +126,31 @@ const useScene = create()(
[wall2.id]: wall2,
[wall3.id]: wall3,
[window1.id]: window1,
- };
+ }
// Root nodes are the levels
- const rootNodeIds = [building.id];
+ const rootNodeIds = [building.id]
- get().dirtyNodes.add(wall0.id);
- get().dirtyNodes.add(wall1.id);
- get().dirtyNodes.add(wall2.id);
- get().dirtyNodes.add(wall3.id);
- set({ nodes, rootNodeIds });
+ get().dirtyNodes.add(wall0.id)
+ get().dirtyNodes.add(wall1.id)
+ get().dirtyNodes.add(wall2.id)
+ get().dirtyNodes.add(wall3.id)
+ set({ nodes, rootNodeIds })
},
markDirty: (id) => {
- get().dirtyNodes.add(id);
+ get().dirtyNodes.add(id)
},
clearDirty: (id) => {
- get().dirtyNodes.delete(id);
+ get().dirtyNodes.delete(id)
},
createNodes: (ops) => nodeActions.createNodesAction(set, get, ops),
- createNode: (node, parentId) =>
- nodeActions.createNodesAction(set, get, [{ node, parentId }]),
+ createNode: (node, parentId) => nodeActions.createNodesAction(set, get, [{ node, parentId }]),
- updateNodes: (updates) =>
- nodeActions.updateNodesAction(set, get, updates),
- updateNode: (id, data) =>
- nodeActions.updateNodesAction(set, get, [{ id, data }]),
+ updateNodes: (updates) => nodeActions.updateNodesAction(set, get, updates),
+ updateNode: (id, data) => nodeActions.updateNodesAction(set, get, [{ id, data }]),
// --- DELETE ---
@@ -163,26 +160,26 @@ const useScene = create()(
}),
{
partialize: (state) => {
- const { nodes, rootNodeIds } = state; // Only track nodes and rootNodeIds in history
- return { nodes, rootNodeIds };
+ const { nodes, rootNodeIds } = state // Only track nodes and rootNodeIds in history
+ return { nodes, rootNodeIds }
},
limit: 50, // Limit to last 50 actions
},
),
-);
+)
-export default useScene;
+export default useScene
// Subscribe to the temporal store (Undo/Redo events)
useScene.temporal.subscribe((state, prevState) => {
// Check if we just jumped in time (Undo/Redo)
// If the 'nodes' object changed but it wasn't a normal 'set'
- const currentNodes = useScene.getState().nodes;
+ const currentNodes = useScene.getState().nodes
// Trigger a full scene re-validation
Object.values(currentNodes).forEach((node) => {
- if (node.type === "wall") {
- useScene.getState().markDirty(node.id);
+ if (node.type === 'wall') {
+ useScene.getState().markDirty(node.id)
}
- });
-});
+ })
+})
diff --git a/packages/core/src/systems/wall/wall-system.tsx b/packages/core/src/systems/wall/wall-system.tsx
index bf3a12f4..8e1780b7 100644
--- a/packages/core/src/systems/wall/wall-system.tsx
+++ b/packages/core/src/systems/wall/wall-system.tsx
@@ -1,19 +1,19 @@
-import { useFrame } from "@react-three/fiber";
-import * as THREE from "three";
-import { sceneRegistry } from "../../hooks/scene-registry/scene-registry";
-import { AnyNode, WallNode } from "../../schema";
-import useScene from "../../store/use-scene";
+import { useFrame } from '@react-three/fiber'
+import * as THREE from 'three'
+import { sceneRegistry } from '../../hooks/scene-registry/scene-registry'
+import type { AnyNode, WallNode } from '../../schema'
+import useScene from '../../store/use-scene'
export const WallSystem = () => {
- const { nodes, dirtyNodes, clearDirty } = useScene();
+ const { nodes, dirtyNodes, clearDirty } = useScene()
useFrame(() => {
- if (dirtyNodes.size === 0) return;
+ if (dirtyNodes.size === 0) return
dirtyNodes.forEach((id) => {
- const node = nodes[id];
- if (!node) return;
- const mesh = sceneRegistry.nodes.get(id) as THREE.Mesh;
+ const node = nodes[id]
+ if (!node) return
+ const mesh = sceneRegistry.nodes.get(id) as THREE.Mesh
// 1. If a window is dirty, we actually need to redraw its PARENT wall
// if ((node.type === 'window' || node.type === 'door') && node.parentId) {
@@ -22,117 +22,111 @@ export const WallSystem = () => {
// }
// 2. If the wall itself is dirty
- if (node.type === "wall" && mesh) {
- updateWallGeometry(id);
+ if (node.type === 'wall' && mesh) {
+ updateWallGeometry(id)
}
- clearDirty(id); // Reset for next frame
- });
- });
+ clearDirty(id) // Reset for next frame
+ })
+ })
- return null;
-};
+ return null
+}
// Optimization: Logic moved to a vanilla function so it can be called
// by the Editor or the System without React overhead
function updateWallGeometry(wallId: string) {
- const node = useScene.getState().nodes[wallId as WallNode["id"]];
- if (!node) return;
- if (node.type !== "wall") return;
+ const node = useScene.getState().nodes[wallId as WallNode['id']]
+ if (!node) return
+ if (node.type !== 'wall') return
- const mesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh;
- if (!mesh) return;
+ const mesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh
+ if (!mesh) return
- const childrenIds = node.children || [];
+ const childrenIds = node.children || []
const childrenNodes = childrenIds
.map((childId) => useScene.getState().nodes[childId])
- .filter((n): n is AnyNode => n !== undefined);
+ .filter((n): n is AnyNode => n !== undefined)
// Generate visual geometry with holes
- const newGeo = generateExtrudedWall(node, childrenNodes);
+ const newGeo = generateExtrudedWall(node, childrenNodes)
- mesh.geometry.dispose();
- mesh.geometry = newGeo;
+ mesh.geometry.dispose()
+ mesh.geometry = newGeo
// Update collision mesh with solid geometry (no holes)
- const collisionMesh = mesh.getObjectByName("collision-mesh") as THREE.Mesh;
+ 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;
+ 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
- const angle = Math.atan2(
- node.end[1] - node.start[1],
- node.end[0] - node.start[0],
- );
- mesh.rotation.y = -angle;
+ const angle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0])
+ mesh.rotation.y = -angle
}
-export function generateExtrudedWall(
- wallNode: WallNode,
- childrenNodes: AnyNode[],
-) {
+export function generateExtrudedWall(wallNode: WallNode, childrenNodes: AnyNode[]) {
// 1. Calculate Wall Dimensions
- const start = new THREE.Vector2(wallNode.start[0], wallNode.start[1]);
- const end = new THREE.Vector2(wallNode.end[0], wallNode.end[1]);
- const length = start.distanceTo(end);
- const height = wallNode.height || 2.5;
- const thickness = wallNode.thickness || 0.1;
+ const start = new THREE.Vector2(wallNode.start[0], wallNode.start[1])
+ const end = new THREE.Vector2(wallNode.end[0], wallNode.end[1])
+ const length = start.distanceTo(end)
+ const height = wallNode.height || 2.5
+ const thickness = wallNode.thickness || 0.1
// 2. Create the Main Wall Shape (a rectangle in 2D)
// We draw this on the XY plane, where X is "along the wall" and Y is "height"
- const shape = new THREE.Shape();
- shape.moveTo(0, 0);
- shape.lineTo(length, 0);
- shape.lineTo(length, height);
- shape.lineTo(0, height);
- shape.closePath();
+ const shape = new THREE.Shape()
+ shape.moveTo(0, 0)
+ shape.lineTo(length, 0)
+ shape.lineTo(length, height)
+ shape.lineTo(0, height)
+ shape.closePath()
// 3. Process Openings (Holes)
// Compute wall's transform info for converting world coords to wall-local coords
- const wallStart: [number, number] = [wallNode.start[0], wallNode.start[1]];
+ const wallStart: [number, number] = [wallNode.start[0], wallNode.start[1]]
const wallAngle = Math.atan2(
wallNode.end[1] - wallNode.start[1],
wallNode.end[0] - wallNode.start[0],
- );
+ )
// Get the wall mesh's world Y position (from level offset)
- const wallMesh = sceneRegistry.nodes.get(wallNode.id) as THREE.Mesh;
- const wallWorldY = wallMesh?.getWorldPosition(new THREE.Vector3()).y ?? 0;
+ const wallMesh = sceneRegistry.nodes.get(wallNode.id) as THREE.Mesh
+ const wallWorldY = wallMesh?.getWorldPosition(new THREE.Vector3()).y ?? 0
childrenNodes.forEach((child) => {
// Only process items that are intended to be wall cutouts
- if (child.type !== "item") return;
+ if (child.type !== 'item') return
- const childMesh = sceneRegistry.nodes.get(child.id);
+ const childMesh = sceneRegistry.nodes.get(child.id)
if (!childMesh) {
- return;
+ return
}
- const cutoutMesh = childMesh.getObjectByName("cutout") as THREE.Mesh;
- if (!cutoutMesh) return;
+ const cutoutMesh = childMesh.getObjectByName('cutout') as THREE.Mesh
+ if (!cutoutMesh) return
- const holePath = createPathFromCutout(cutoutMesh, wallStart, wallAngle, wallWorldY);
+ const holePath = createPathFromCutout(cutoutMesh, wallStart, wallAngle, wallWorldY)
if (holePath) {
- shape.holes.push(holePath);
+ shape.holes.push(holePath)
}
- });
+ })
// 4. Extrude the Shape into 3D
const geometry = new THREE.ExtrudeGeometry(shape, {
depth: thickness,
bevelEnabled: false,
- });
+ })
// 5. Pivot Alignment
// Center the geometry thickness so the "start/end" line is in the middle of the wall
- geometry.translate(0, 0, -thickness / 2);
+ geometry.translate(0, 0, -thickness / 2)
- return geometry;
+ return geometry
}
/**
@@ -150,72 +144,72 @@ function createPathFromCutout(
wallAngle: number,
wallWorldY: number,
): THREE.Path | null {
- const geometry = cutoutMesh.geometry;
- if (!geometry) return null;
+ const geometry = cutoutMesh.geometry
+ if (!geometry) return null
- const positions = geometry.attributes.position;
- if (!positions) return null;
+ const positions = geometry.attributes.position
+ if (!positions) return null
// Update world matrix to get correct world positions
- cutoutMesh.updateWorldMatrix(true, false);
+ cutoutMesh.updateWorldMatrix(true, false)
// Collect unique vertices (buffer geometry has duplicates for triangulation)
- const uniquePoints: THREE.Vector2[] = [];
- const seen = new Set();
- const v3 = new THREE.Vector3();
+ const uniquePoints: THREE.Vector2[] = []
+ const seen = new Set()
+ const v3 = new THREE.Vector3()
// Precompute sin/cos for rotation
- const cosAngle = Math.cos(-wallAngle);
- const sinAngle = Math.sin(-wallAngle);
+ const cosAngle = Math.cos(-wallAngle)
+ const sinAngle = Math.sin(-wallAngle)
for (let i = 0; i < positions.count; i++) {
- v3.fromBufferAttribute(positions, i);
+ v3.fromBufferAttribute(positions, i)
// Transform to world space
- v3.applyMatrix4(cutoutMesh.matrixWorld);
+ v3.applyMatrix4(cutoutMesh.matrixWorld)
// Transform from world space to wall-local space:
// 1. Translate so wall start is at origin (in XZ plane)
- const worldX = v3.x - wallStart[0];
- const worldZ = v3.z - wallStart[1];
+ const worldX = v3.x - wallStart[0]
+ const worldZ = v3.z - wallStart[1]
// 2. Rotate around Y axis to align wall with local X axis
// The wall shape is drawn on XY plane, so we need:
// - localX = distance along wall
// - localY = height relative to wall's Y position
- const localX = worldX * cosAngle - worldZ * sinAngle;
- const localY = v3.y - wallWorldY; // Subtract wall's world Y to get local height
+ const localX = worldX * cosAngle - worldZ * sinAngle
+ const localY = v3.y - wallWorldY // Subtract wall's world Y to get local height
// Create a key for deduplication (with small tolerance)
- const key = `${localX.toFixed(4)},${localY.toFixed(4)}`;
+ const key = `${localX.toFixed(4)},${localY.toFixed(4)}`
if (!seen.has(key)) {
- seen.add(key);
- uniquePoints.push(new THREE.Vector2(localX, localY));
+ seen.add(key)
+ uniquePoints.push(new THREE.Vector2(localX, localY))
}
}
- if (uniquePoints.length < 3) return null;
+ if (uniquePoints.length < 3) return null
// Sort points in counter-clockwise order around centroid
- const centroid = new THREE.Vector2(0, 0);
+ const centroid = new THREE.Vector2(0, 0)
for (const p of uniquePoints) {
- centroid.add(p);
+ centroid.add(p)
}
- centroid.divideScalar(uniquePoints.length);
+ centroid.divideScalar(uniquePoints.length)
uniquePoints.sort((a, b) => {
- const angleA = Math.atan2(a.y - centroid.y, a.x - centroid.x);
- const angleB = Math.atan2(b.y - centroid.y, b.x - centroid.x);
- return angleA - angleB;
- });
+ const angleA = Math.atan2(a.y - centroid.y, a.x - centroid.x)
+ const angleB = Math.atan2(b.y - centroid.y, b.x - centroid.x)
+ return angleA - angleB
+ })
// Create the path
- const path = new THREE.Path();
- path.moveTo(uniquePoints[0]?.x || 0, uniquePoints[0]?.y || 0);
+ const path = new THREE.Path()
+ path.moveTo(uniquePoints[0]?.x || 0, uniquePoints[0]?.y || 0)
for (let i = 1; i < uniquePoints.length; i++) {
- path.lineTo(uniquePoints[i]?.x || 0, uniquePoints[i]?.y || 0);
+ path.lineTo(uniquePoints[i]?.x || 0, uniquePoints[i]?.y || 0)
}
- path.closePath();
+ path.closePath()
- return path;
+ return path
}
diff --git a/packages/eslint-config/base.js b/packages/eslint-config/base.js
index 09d316ef..407c426f 100644
--- a/packages/eslint-config/base.js
+++ b/packages/eslint-config/base.js
@@ -1,8 +1,8 @@
-import js from "@eslint/js";
-import eslintConfigPrettier from "eslint-config-prettier";
-import turboPlugin from "eslint-plugin-turbo";
-import tseslint from "typescript-eslint";
-import onlyWarn from "eslint-plugin-only-warn";
+import js from '@eslint/js'
+import eslintConfigPrettier from 'eslint-config-prettier'
+import onlyWarn from 'eslint-plugin-only-warn'
+import turboPlugin from 'eslint-plugin-turbo'
+import tseslint from 'typescript-eslint'
/**
* A shared ESLint configuration for the repository.
@@ -18,7 +18,7 @@ export const config = [
turbo: turboPlugin,
},
rules: {
- "turbo/no-undeclared-env-vars": "warn",
+ 'turbo/no-undeclared-env-vars': 'warn',
},
},
{
@@ -27,6 +27,6 @@ export const config = [
},
},
{
- ignores: ["dist/**"],
+ ignores: ['dist/**'],
},
-];
+]
diff --git a/packages/eslint-config/next.js b/packages/eslint-config/next.js
index 4df088ac..a6f5ab14 100644
--- a/packages/eslint-config/next.js
+++ b/packages/eslint-config/next.js
@@ -1,12 +1,12 @@
-import js from "@eslint/js";
-import { globalIgnores } from "eslint/config";
-import eslintConfigPrettier from "eslint-config-prettier";
-import tseslint from "typescript-eslint";
-import pluginReactHooks from "eslint-plugin-react-hooks";
-import pluginReact from "eslint-plugin-react";
-import globals from "globals";
-import pluginNext from "@next/eslint-plugin-next";
-import { config as baseConfig } from "./base.js";
+import js from '@eslint/js'
+import pluginNext from '@next/eslint-plugin-next'
+import { globalIgnores } from 'eslint/config'
+import eslintConfigPrettier from 'eslint-config-prettier'
+import pluginReact from 'eslint-plugin-react'
+import pluginReactHooks from 'eslint-plugin-react-hooks'
+import globals from 'globals'
+import tseslint from 'typescript-eslint'
+import { config as baseConfig } from './base.js'
/**
* A custom ESLint configuration for libraries that use Next.js.
@@ -20,10 +20,10 @@ export const nextJsConfig = [
...tseslint.configs.recommended,
globalIgnores([
// Default ignores of eslint-config-next:
- ".next/**",
- "out/**",
- "build/**",
- "next-env.d.ts",
+ '.next/**',
+ 'out/**',
+ 'build/**',
+ 'next-env.d.ts',
]),
{
...pluginReact.configs.flat.recommended,
@@ -36,22 +36,22 @@ export const nextJsConfig = [
},
{
plugins: {
- "@next/next": pluginNext,
+ '@next/next': pluginNext,
},
rules: {
...pluginNext.configs.recommended.rules,
- ...pluginNext.configs["core-web-vitals"].rules,
+ ...pluginNext.configs['core-web-vitals'].rules,
},
},
{
plugins: {
- "react-hooks": pluginReactHooks,
+ 'react-hooks': pluginReactHooks,
},
- settings: { react: { version: "detect" } },
+ settings: { react: { version: 'detect' } },
rules: {
...pluginReactHooks.configs.recommended.rules,
// React scope no longer necessary with new JSX transform.
- "react/react-in-jsx-scope": "off",
+ 'react/react-in-jsx-scope': 'off',
},
},
-];
+]
diff --git a/packages/eslint-config/react-internal.js b/packages/eslint-config/react-internal.js
index daeccba2..f4b98d3b 100644
--- a/packages/eslint-config/react-internal.js
+++ b/packages/eslint-config/react-internal.js
@@ -1,10 +1,10 @@
-import js from "@eslint/js";
-import eslintConfigPrettier from "eslint-config-prettier";
-import tseslint from "typescript-eslint";
-import pluginReactHooks from "eslint-plugin-react-hooks";
-import pluginReact from "eslint-plugin-react";
-import globals from "globals";
-import { config as baseConfig } from "./base.js";
+import js from '@eslint/js'
+import eslintConfigPrettier from 'eslint-config-prettier'
+import pluginReact from 'eslint-plugin-react'
+import pluginReactHooks from 'eslint-plugin-react-hooks'
+import globals from 'globals'
+import tseslint from 'typescript-eslint'
+import { config as baseConfig } from './base.js'
/**
* A custom ESLint configuration for libraries that use React.
@@ -27,13 +27,13 @@ export const config = [
},
{
plugins: {
- "react-hooks": pluginReactHooks,
+ 'react-hooks': pluginReactHooks,
},
- settings: { react: { version: "detect" } },
+ settings: { react: { version: 'detect' } },
rules: {
...pluginReactHooks.configs.recommended.rules,
// React scope no longer necessary with new JSX transform.
- "react/react-in-jsx-scope": "off",
+ 'react/react-in-jsx-scope': 'off',
},
},
-];
+]
diff --git a/packages/ui/src/button.tsx b/packages/ui/src/button.tsx
index 78e5420c..858dd860 100644
--- a/packages/ui/src/button.tsx
+++ b/packages/ui/src/button.tsx
@@ -1,20 +1,17 @@
-"use client";
+'use client'
-import { ReactNode } from "react";
+import type { ReactNode } from 'react'
interface ButtonProps {
- children: ReactNode;
- className?: string;
- appName: string;
+ children: ReactNode
+ className?: string
+ appName: string
}
export const Button = ({ children, className, appName }: ButtonProps) => {
return (
- alert(`Hello from your ${appName} app!`)}
- >
+ alert(`Hello from your ${appName} app!`)}>
{children}
- );
-};
+ )
+}
diff --git a/packages/ui/src/card.tsx b/packages/ui/src/card.tsx
index 7b988937..c06b9615 100644
--- a/packages/ui/src/card.tsx
+++ b/packages/ui/src/card.tsx
@@ -1,4 +1,4 @@
-import { type JSX } from "react";
+import type { JSX } from 'react'
export function Card({
className,
@@ -6,10 +6,10 @@ export function Card({
children,
href,
}: {
- className?: string;
- title: string;
- children: React.ReactNode;
- href: string;
+ className?: string
+ title: string
+ children: React.ReactNode
+ href: string
}): JSX.Element {
return (
{children}
- );
+ )
}
diff --git a/packages/ui/src/code.tsx b/packages/ui/src/code.tsx
index f7cbd22a..087b4b32 100644
--- a/packages/ui/src/code.tsx
+++ b/packages/ui/src/code.tsx
@@ -1,11 +1,11 @@
-import { type JSX } from "react";
+import type { JSX } from 'react'
export function Code({
children,
className,
}: {
- children: React.ReactNode;
- className?: string;
+ children: React.ReactNode
+ className?: string
}): JSX.Element {
- return {children};
+ return {children}
}
diff --git a/packages/viewer/src/components/renderers/building/building-renderer.tsx b/packages/viewer/src/components/renderers/building/building-renderer.tsx
index 52161f69..3e835feb 100644
--- a/packages/viewer/src/components/renderers/building/building-renderer.tsx
+++ b/packages/viewer/src/components/renderers/building/building-renderer.tsx
@@ -1,19 +1,19 @@
-import { BuildingNode, useRegistry } from "@pascal-app/core";
-import { useRef } from "react";
-import { Group } from "three";
-import { NodeRenderer } from "../node-renderer";
-import { useNodeEvents } from "../../../hooks/use-node-events";
+import { type BuildingNode, useRegistry } from '@pascal-app/core'
+import { useRef } from 'react'
+import type { Group } from 'three'
+import { useNodeEvents } from '../../../hooks/use-node-events'
+import { NodeRenderer } from '../node-renderer'
export const BuildingRenderer = ({ node }: { node: BuildingNode }) => {
- const ref = useRef(null!);
+ const ref = useRef(null!)
- useRegistry(node.id, node.type, ref);
- const handlers = useNodeEvents(node, "building");
+ useRegistry(node.id, node.type, ref)
+ const handlers = useNodeEvents(node, 'building')
return (
{node.children.map((childId) => (
))}
- );
-};
+ )
+}
diff --git a/packages/viewer/src/components/renderers/item/item-renderer.tsx b/packages/viewer/src/components/renderers/item/item-renderer.tsx
index 7cf91d80..e0f09d12 100644
--- a/packages/viewer/src/components/renderers/item/item-renderer.tsx
+++ b/packages/viewer/src/components/renderers/item/item-renderer.tsx
@@ -1,21 +1,14 @@
-import {
- AnyNodeId,
- emitter,
- ItemNode,
- useRegistry,
- useScene,
-} from "@pascal-app/core";
-import { Clone } from "@react-three/drei/core/Clone";
-import { useGLTF } from "@react-three/drei/core/Gltf";
-import { ThreeEvent } from "@react-three/fiber/dist/declarations/src/core/events";
-import { Suspense, useCallback, useEffect, useRef } from "react";
-import { Group } from "three";
-import { useNodeEvents } from "../../../hooks/use-node-events";
+import { type AnyNodeId, type ItemNode, useRegistry, useScene } from '@pascal-app/core'
+import { Clone } from '@react-three/drei/core/Clone'
+import { useGLTF } from '@react-three/drei/core/Gltf'
+import { Suspense, useEffect, useRef } from 'react'
+import type { Group } from 'three'
+import { useNodeEvents } from '../../../hooks/use-node-events'
export const ItemRenderer = ({ node }: { node: ItemNode }) => {
- const ref = useRef(null!);
+ const ref = useRef(null!)
- useRegistry(node.id, node.type, ref);
+ useRegistry(node.id, node.type, ref)
return (
@@ -23,22 +16,22 @@ export const ItemRenderer = ({ node }: { node: ItemNode }) => {
- );
-};
+ )
+}
const ModelRenderer = ({ node }: { node: ItemNode }) => {
- const { scene, nodes } = useGLTF(node.asset.src);
+ const { scene, nodes } = useGLTF(node.asset.src)
if (nodes.cutout) {
- nodes.cutout.visible = false;
+ nodes.cutout.visible = false
}
- const handlers = useNodeEvents(node, "item");
+ const handlers = useNodeEvents(node, 'item')
useEffect(() => {
- if (!node.parentId) return;
- useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId);
- }, []);
+ if (!node.parentId) return
+ useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId)
+ }, [node.parentId])
return (
{
rotation={node.asset.rotation}
{...handlers}
/>
- );
-};
+ )
+}
diff --git a/packages/viewer/src/components/renderers/level/level-renderer.tsx b/packages/viewer/src/components/renderers/level/level-renderer.tsx
index 980c7018..0ee20ed9 100644
--- a/packages/viewer/src/components/renderers/level/level-renderer.tsx
+++ b/packages/viewer/src/components/renderers/level/level-renderer.tsx
@@ -1,12 +1,12 @@
-import { LevelNode, useRegistry } from "@pascal-app/core";
-import { useRef } from "react";
-import { Group } from "three";
-import { NodeRenderer } from "../node-renderer";
+import { type LevelNode, useRegistry } from '@pascal-app/core'
+import { useRef } from 'react'
+import type { Group } from 'three'
+import { NodeRenderer } from '../node-renderer'
export const LevelRenderer = ({ node }: { node: LevelNode }) => {
- const ref = useRef(null!);
+ const ref = useRef(null!)
- useRegistry(node.id, node.type, ref);
+ useRegistry(node.id, node.type, ref)
return (
@@ -18,5 +18,5 @@ export const LevelRenderer = ({ node }: { node: LevelNode }) => {
))}
- );
-};
+ )
+}
diff --git a/packages/viewer/src/components/renderers/node-renderer.tsx b/packages/viewer/src/components/renderers/node-renderer.tsx
index 57f80e2e..21e1d8c9 100644
--- a/packages/viewer/src/components/renderers/node-renderer.tsx
+++ b/packages/viewer/src/components/renderers/node-renderer.tsx
@@ -1,22 +1,22 @@
-"use client";
+'use client'
-import { AnyNode, useScene } from "@pascal-app/core";
-import { ItemRenderer } from "./item/item-renderer";
-import { LevelRenderer } from "./level/level-renderer";
-import { WallRenderer } from "./wall/wall-renderer";
-import { BuildingRenderer } from "./building/building-renderer";
+import { type AnyNode, useScene } from '@pascal-app/core'
+import { BuildingRenderer } from './building/building-renderer'
+import { ItemRenderer } from './item/item-renderer'
+import { LevelRenderer } from './level/level-renderer'
+import { WallRenderer } from './wall/wall-renderer'
-export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode["id"] }) => {
- const node = useScene((state) => state.nodes[nodeId]);
+export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode['id'] }) => {
+ const node = useScene((state) => state.nodes[nodeId])
- if (!node) return null;
+ if (!node) return null
return (
<>
- {node.type === "building" && }
- {node.type === "level" && }
- {node.type === "item" && }
- {node.type === "wall" && }
+ {node.type === 'building' && }
+ {node.type === 'level' && }
+ {node.type === 'item' && }
+ {node.type === 'wall' && }
>
- );
-};
+ )
+}
diff --git a/packages/viewer/src/components/renderers/scene-renderer.tsx b/packages/viewer/src/components/renderers/scene-renderer.tsx
index 569d0a4e..abbc9570 100644
--- a/packages/viewer/src/components/renderers/scene-renderer.tsx
+++ b/packages/viewer/src/components/renderers/scene-renderer.tsx
@@ -1,12 +1,10 @@
-"use client";
+'use client'
-import { useScene } from "@pascal-app/core";
-import { NodeRenderer } from "./node-renderer";
+import { useScene } from '@pascal-app/core'
+import { NodeRenderer } from './node-renderer'
export const SceneRenderer = () => {
- const rootNodes = useScene((state) => state.rootNodeIds);
+ const rootNodes = useScene((state) => state.rootNodeIds)
- return rootNodes.map((nodeId) => (
-
- ));
-};
+ return rootNodes.map((nodeId) => )
+}
diff --git a/packages/viewer/src/components/renderers/wall/wall-renderer.tsx b/packages/viewer/src/components/renderers/wall/wall-renderer.tsx
index f6f978a5..d5d51ae6 100644
--- a/packages/viewer/src/components/renderers/wall/wall-renderer.tsx
+++ b/packages/viewer/src/components/renderers/wall/wall-renderer.tsx
@@ -1,15 +1,15 @@
-import { useRegistry, WallNode } from "@pascal-app/core";
-import { useRef } from "react";
-import { Mesh } from "three";
-import { NodeRenderer } from "../node-renderer";
-import { useNodeEvents } from "../../../hooks/use-node-events";
+import { useRegistry, type WallNode } from '@pascal-app/core'
+import { useRef } from 'react'
+import type { Mesh } from 'three'
+import { useNodeEvents } from '../../../hooks/use-node-events'
+import { NodeRenderer } from '../node-renderer'
export const WallRenderer = ({ node }: { node: WallNode }) => {
- const ref = useRef(null!);
+ const ref = useRef(null!)
- useRegistry(node.id, "wall", ref);
+ useRegistry(node.id, 'wall', ref)
- const handlers = useNodeEvents(node, "wall");
+ const handlers = useNodeEvents(node, 'wall')
return (
@@ -24,5 +24,5 @@ export const WallRenderer = ({ node }: { node: WallNode }) => {
))}
- );
-};
+ )
+}
diff --git a/packages/viewer/src/components/viewer/index.tsx b/packages/viewer/src/components/viewer/index.tsx
index 2cc75cae..78045cb7 100644
--- a/packages/viewer/src/components/viewer/index.tsx
+++ b/packages/viewer/src/components/viewer/index.tsx
@@ -1,38 +1,36 @@
-"use client";
+'use client'
-import { Bvh, Environment, OrbitControls } from "@react-three/drei";
-import { Canvas, ThreeToJSXElements } from "@react-three/fiber";
+import { WallSystem } from '@pascal-app/core'
+import { Bvh, Environment } from '@react-three/drei'
+import { Canvas, extend, type ThreeToJSXElements } from '@react-three/fiber'
+import * as THREE from 'three/webgpu'
+import { LevelSystem } from '../../systems/level/level-system'
+import { SceneRenderer } from '../renderers/scene-renderer'
+import PostProcessing from './post-processing'
-import { WallSystem } from "@pascal-app/core";
-import { extend } from "@react-three/fiber";
-import * as THREE from "three/webgpu";
-import { SceneRenderer } from "../renderers/scene-renderer";
-import { LevelSystem } from "../../systems/level/level-system";
-import PostProcessing from "./post-processing";
-
-declare module "@react-three/fiber" {
+declare module '@react-three/fiber' {
interface ThreeElements extends ThreeToJSXElements {}
}
-extend(THREE as any);
+extend(THREE as any)
interface ViewerProps {
- children?: React.ReactNode;
+ children?: React.ReactNode
}
const Viewer: React.FC = ({ children }) => {
return (
- );
-};
+ )
+}
-export default Viewer;
+export default Viewer
diff --git a/packages/viewer/src/components/viewer/post-processing.tsx b/packages/viewer/src/components/viewer/post-processing.tsx
index 1ee0e189..811ef60f 100644
--- a/packages/viewer/src/components/viewer/post-processing.tsx
+++ b/packages/viewer/src/components/viewer/post-processing.tsx
@@ -1,99 +1,93 @@
-import { useFrame, useThree } from "@react-three/fiber";
-import { useEffect, useRef } from "react";
-import { Color, Object3D } from "three";
-import { oscSine, pass, time, uniform } from "three/tsl";
-import { outline } from "three/addons/tsl/display/OutlineNode.js";
-import { PostProcessing, WebGPURenderer } from "three/webgpu";
-import useViewer from "../../store/use-viewer";
+import { useFrame, useThree } from '@react-three/fiber'
+import { useEffect, useRef } from 'react'
+import { Color } from 'three'
+import { outline } from 'three/addons/tsl/display/OutlineNode.js'
+import { oscSine, pass, time, uniform } from 'three/tsl'
+import { PostProcessing, type WebGPURenderer } from 'three/webgpu'
+import useViewer from '../../store/use-viewer'
const PostProcessingPasses = ({}) => {
- const { gl: renderer, scene, camera } = useThree();
- const postProcessingRef = useRef(null);
+ const { gl: renderer, scene, camera } = useThree()
+ const postProcessingRef = useRef(null)
useEffect(() => {
if (!renderer || !scene || !camera) {
- return;
+ return
}
- const scenePass = pass(scene, camera);
+ const scenePass = pass(scene, camera)
function generateSelectedOutlinePass() {
- const edgeStrength = uniform(3);
- const edgeGlow = uniform(0);
- const edgeThickness = uniform(1);
- const visibleEdgeColor = uniform(new Color(0xffffff));
- const hiddenEdgeColor = uniform(new Color(0xf3ff47));
+ const edgeStrength = uniform(3)
+ const edgeGlow = uniform(0)
+ const edgeThickness = uniform(1)
+ const visibleEdgeColor = uniform(new Color(0xffffff))
+ const hiddenEdgeColor = uniform(new Color(0xf3ff47))
const outlinePass = outline(scene, camera, {
selectedObjects: useViewer.getState().outliner.selectedObjects,
edgeGlow,
edgeThickness,
- });
- const { visibleEdge, hiddenEdge } = outlinePass;
+ })
+ const { visibleEdge, hiddenEdge } = outlinePass
const outlineColor = visibleEdge
.mul(visibleEdgeColor)
.add(hiddenEdge.mul(hiddenEdgeColor))
- .mul(edgeStrength);
+ .mul(edgeStrength)
- return outlineColor;
+ return outlineColor
}
function generateHoverOutlinePass() {
- const edgeStrength = uniform(5);
- const edgeGlow = uniform(0.5);
- const edgeThickness = uniform(1.5);
- const pulsePeriod = uniform(3);
- const visibleEdgeColor = uniform(new Color(0x00aaff));
- const hiddenEdgeColor = uniform(new Color(0xf3ff47));
+ const edgeStrength = uniform(5)
+ const edgeGlow = uniform(0.5)
+ const edgeThickness = uniform(1.5)
+ const pulsePeriod = uniform(3)
+ const visibleEdgeColor = uniform(new Color(0x00aaff))
+ const hiddenEdgeColor = uniform(new Color(0xf3ff47))
const outlinePass = outline(scene, camera, {
selectedObjects: useViewer.getState().outliner.hoveredObjects,
edgeGlow,
edgeThickness,
- });
- const { visibleEdge, hiddenEdge } = outlinePass;
+ })
+ const { visibleEdge, hiddenEdge } = outlinePass
- const period = time.div(pulsePeriod).mul(2);
- const osc = oscSine(period).mul(0.5).add(0.5); // osc [ 0.5, 1.0 ]
+ const period = time.div(pulsePeriod).mul(2)
+ const osc = oscSine(period).mul(0.5).add(0.5) // osc [ 0.5, 1.0 ]
const outlineColor = visibleEdge
.mul(visibleEdgeColor)
.add(hiddenEdge.mul(hiddenEdgeColor))
- .mul(edgeStrength);
- const outlinePulse = pulsePeriod
- .greaterThan(0)
- .select(outlineColor.mul(osc), outlineColor);
- return outlinePulse;
+ .mul(edgeStrength)
+ const outlinePulse = pulsePeriod.greaterThan(0).select(outlineColor.mul(osc), outlineColor)
+ return outlinePulse
}
// Setup post-processing
- const postProcessing = new PostProcessing(
- renderer as unknown as WebGPURenderer,
- );
+ const postProcessing = new PostProcessing(renderer as unknown as WebGPURenderer)
- const selectedOutlinePass = generateSelectedOutlinePass();
- const hoverOutlinePass = generateHoverOutlinePass();
+ const selectedOutlinePass = generateSelectedOutlinePass()
+ const hoverOutlinePass = generateHoverOutlinePass()
- postProcessing.outputNode = selectedOutlinePass
- .add(hoverOutlinePass)
- .add(scenePass);
- postProcessingRef.current = postProcessing;
+ postProcessing.outputNode = selectedOutlinePass.add(hoverOutlinePass).add(scenePass)
+ postProcessingRef.current = postProcessing
return () => {
if (postProcessingRef.current) {
- postProcessingRef.current.dispose();
+ postProcessingRef.current.dispose()
}
- postProcessingRef.current = null;
- };
- }, [renderer, scene, camera]);
+ postProcessingRef.current = null
+ }
+ }, [renderer, scene, camera])
useFrame(() => {
if (postProcessingRef.current) {
- postProcessingRef.current.render();
+ postProcessingRef.current.render()
}
- }, 1);
+ }, 1)
- return null;
-};
+ return null
+}
-export default PostProcessingPasses;
+export default PostProcessingPasses
diff --git a/packages/viewer/src/hooks/use-grid-events.ts b/packages/viewer/src/hooks/use-grid-events.ts
index 069ea22d..ebcb21d9 100644
--- a/packages/viewer/src/hooks/use-grid-events.ts
+++ b/packages/viewer/src/hooks/use-grid-events.ts
@@ -1,33 +1,33 @@
-import { emitter, EventSuffix, GridEvent } from "@pascal-app/core";
-import { ThreeEvent } from "@react-three/fiber";
+import { type EventSuffix, emitter, type GridEvent } from '@pascal-app/core'
+import type { ThreeEvent } from '@react-three/fiber'
export function useGridEvents() {
const emit = (suffix: EventSuffix, e: ThreeEvent) => {
- const eventKey = `grid:${suffix}` as `grid:${EventSuffix}`;
+ const eventKey = `grid:${suffix}` as `grid:${EventSuffix}`
const payload: GridEvent = {
position: [e.point.x, e.point.y, e.point.z],
- };
+ }
- emitter.emit(eventKey, payload);
- };
+ emitter.emit(eventKey, payload)
+ }
return {
onPointerDown: (e: ThreeEvent) => {
- if (e.button !== 0) return;
- emit("pointerdown", e);
+ if (e.button !== 0) return
+ emit('pointerdown', e)
},
onPointerUp: (e: ThreeEvent) => {
- if (e.button !== 0) return;
- emit("pointerup", e);
+ if (e.button !== 0) return
+ emit('pointerup', e)
},
onClick: (e: ThreeEvent) => {
- if (e.button !== 0) return;
- emit("click", e);
+ if (e.button !== 0) return
+ emit('click', e)
},
- onPointerEnter: (e: ThreeEvent) => emit("enter", e),
- onPointerLeave: (e: ThreeEvent) => emit("leave", e),
- onPointerMove: (e: ThreeEvent) => emit("move", e),
- onDoubleClick: (e: ThreeEvent) => emit("double-click", e),
- onContextMenu: (e: ThreeEvent) => emit("context-menu", e),
- };
+ onPointerEnter: (e: ThreeEvent) => emit('enter', e),
+ onPointerLeave: (e: ThreeEvent) => emit('leave', e),
+ onPointerMove: (e: ThreeEvent) => emit('move', e),
+ onDoubleClick: (e: ThreeEvent) => emit('double-click', e),
+ onContextMenu: (e: ThreeEvent) => emit('context-menu', e),
+ }
}
diff --git a/packages/viewer/src/hooks/use-node-events.ts b/packages/viewer/src/hooks/use-node-events.ts
index a35e3aff..f0dd948e 100644
--- a/packages/viewer/src/hooks/use-node-events.ts
+++ b/packages/viewer/src/hooks/use-node-events.ts
@@ -1,60 +1,55 @@
import {
- BuildingNode,
+ type BuildingNode,
+ type EventSuffix,
emitter,
- EventSuffix,
- ItemEvent,
- ItemNode,
- WallEvent,
- WallNode,
-} from "@pascal-app/core";
-import { ThreeEvent } from "@react-three/fiber";
-import { BuildingEvent } from "../../../core/src/events/bus";
+ type ItemEvent,
+ type ItemNode,
+ type WallEvent,
+ type WallNode,
+} from '@pascal-app/core'
+import type { ThreeEvent } from '@react-three/fiber'
+import type { BuildingEvent } from '../../../core/src/events/bus'
type NodeConfig = {
- item: { node: ItemNode; event: ItemEvent };
- wall: { node: WallNode; event: WallEvent };
- building: { node: BuildingNode; event: BuildingEvent };
-};
+ item: { node: ItemNode; event: ItemEvent }
+ wall: { node: WallNode; event: WallEvent }
+ building: { node: BuildingNode; event: BuildingEvent }
+}
-type NodeType = keyof NodeConfig;
+type NodeType = keyof NodeConfig
-export function useNodeEvents(
- node: NodeConfig[T]["node"],
- type: T,
-) {
+export function useNodeEvents(node: NodeConfig[T]['node'], type: T) {
const emit = (suffix: EventSuffix, e: ThreeEvent) => {
- const eventKey = `${type}:${suffix}` as `${T}:${EventSuffix}`;
- const localPoint = e.object.worldToLocal(e.point.clone());
+ const eventKey = `${type}:${suffix}` as `${T}:${EventSuffix}`
+ const localPoint = e.object.worldToLocal(e.point.clone())
const payload = {
node,
position: [e.point.x, e.point.y, e.point.z],
localPosition: [localPoint.x, localPoint.y, localPoint.z],
- normal: e.face
- ? [e.face.normal.x, e.face.normal.y, e.face.normal.z]
- : undefined,
+ normal: e.face ? [e.face.normal.x, e.face.normal.y, e.face.normal.z] : undefined,
stopPropagation: () => e.stopPropagation(),
- } as NodeConfig[T]["event"];
+ } as NodeConfig[T]['event']
- emitter.emit(eventKey, payload);
- };
+ emitter.emit(eventKey, payload)
+ }
return {
onPointerDown: (e: ThreeEvent) => {
- if (e.button !== 0) return;
- emit("pointerdown", e);
+ if (e.button !== 0) return
+ emit('pointerdown', e)
},
onPointerUp: (e: ThreeEvent) => {
- if (e.button !== 0) return;
- emit("pointerup", e);
+ if (e.button !== 0) return
+ emit('pointerup', e)
},
onClick: (e: ThreeEvent) => {
- if (e.button !== 0) return;
- emit("click", e);
+ if (e.button !== 0) return
+ emit('click', e)
},
- onPointerEnter: (e: ThreeEvent) => emit("enter", e),
- onPointerLeave: (e: ThreeEvent) => emit("leave", e),
- onPointerMove: (e: ThreeEvent) => emit("move", e),
- onDoubleClick: (e: ThreeEvent) => emit("double-click", e),
- onContextMenu: (e: ThreeEvent) => emit("context-menu", e),
- };
+ onPointerEnter: (e: ThreeEvent) => emit('enter', e),
+ onPointerLeave: (e: ThreeEvent) => emit('leave', e),
+ onPointerMove: (e: ThreeEvent) => emit('move', e),
+ onDoubleClick: (e: ThreeEvent) => emit('double-click', e),
+ onContextMenu: (e: ThreeEvent) => emit('context-menu', e),
+ }
}
diff --git a/packages/viewer/src/index.ts b/packages/viewer/src/index.ts
index c8d80d17..31d4fd9e 100644
--- a/packages/viewer/src/index.ts
+++ b/packages/viewer/src/index.ts
@@ -1,5 +1,3 @@
-export { default as Viewer } from "./components/viewer";
-
-export { default as useViewer } from "./store/use-viewer";
-
-export { useGridEvents } from "./hooks/use-grid-events";
+export { default as Viewer } from './components/viewer'
+export { useGridEvents } from './hooks/use-grid-events'
+export { default as useViewer } from './store/use-viewer'
diff --git a/packages/viewer/src/store/use-viewer.ts b/packages/viewer/src/store/use-viewer.ts
index 856e1e11..0634b422 100644
--- a/packages/viewer/src/store/use-viewer.ts
+++ b/packages/viewer/src/store/use-viewer.ts
@@ -1,58 +1,58 @@
-"use client";
+'use client'
-import { BuildingNode, ItemNode, LevelNode, Zone } from "@pascal-app/core";
-import { Object3D } from "three";
+import type { BuildingNode, ItemNode, LevelNode, Zone } from '@pascal-app/core'
+import type { Object3D } from 'three'
-import { create } from "zustand";
+import { create } from 'zustand'
type SelectionPath = {
- buildingId: BuildingNode["id"] | null;
- levelId: LevelNode["id"] | null;
- zoneId: Zone["id"] | null;
- selectedIds: ItemNode["id"][]; // For items/assets (multi-select)
-};
+ buildingId: BuildingNode['id'] | null
+ levelId: LevelNode['id'] | null
+ zoneId: Zone['id'] | null
+ selectedIds: ItemNode['id'][] // For items/assets (multi-select)
+}
type Outliner = {
- selectedObjects: Object3D[];
- hoveredObjects: Object3D[];
-};
+ selectedObjects: Object3D[]
+ hoveredObjects: Object3D[]
+}
type ViewerState = {
- selection: SelectionPath;
- levelMode: "stacked" | "exploded" | "solo" | "manual";
+ selection: SelectionPath
+ levelMode: 'stacked' | 'exploded' | 'solo' | 'manual'
// Actions
- setLevelMode: (mode: "stacked" | "exploded" | "solo" | "manual") => void;
+ setLevelMode: (mode: 'stacked' | 'exploded' | 'solo' | 'manual') => void
// Smart selection update
- setSelection: (updates: Partial) => void;
- resetSelection: () => void;
+ setSelection: (updates: Partial) => void
+ resetSelection: () => void
- outliner: Outliner; // No setter as we will manipulate directly the arrays
-};
+ outliner: Outliner // No setter as we will manipulate directly the arrays
+}
const useViewer = create()((set, get) => ({
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
- levelMode: "stacked",
+ levelMode: 'stacked',
setLevelMode: (mode) => set({ levelMode: mode }),
setSelection: (updates) =>
set((state) => {
- const newSelection = { ...state.selection, ...updates };
+ const newSelection = { ...state.selection, ...updates }
// Hierarchy Guard: If we change a high-level parent, reset the children
if (updates.buildingId !== undefined) {
- newSelection.levelId = null;
- newSelection.zoneId = null;
- newSelection.selectedIds = [];
+ newSelection.levelId = null
+ newSelection.zoneId = null
+ newSelection.selectedIds = []
} else if (updates.levelId !== undefined) {
- newSelection.zoneId = null;
- newSelection.selectedIds = [];
+ newSelection.zoneId = null
+ newSelection.selectedIds = []
} else if (updates.zoneId !== undefined) {
- newSelection.selectedIds = [];
+ newSelection.selectedIds = []
}
- return { selection: newSelection };
+ return { selection: newSelection }
}),
resetSelection: () =>
@@ -66,6 +66,6 @@ const useViewer = create()((set, get) => ({
}),
outliner: { selectedObjects: [], hoveredObjects: [] },
-}));
+}))
-export default useViewer;
+export default useViewer
diff --git a/packages/viewer/src/systems/level/level-system.tsx b/packages/viewer/src/systems/level/level-system.tsx
index cf3941ab..3f573f6d 100644
--- a/packages/viewer/src/systems/level/level-system.tsx
+++ b/packages/viewer/src/systems/level/level-system.tsx
@@ -1,24 +1,24 @@
-import { LevelNode, sceneRegistry, useScene } from "@pascal-app/core";
-import { useFrame } from "@react-three/fiber";
-import { lerp } from "three/src/math/MathUtils.js";
-import useViewer from "../../store/use-viewer";
+import { type LevelNode, sceneRegistry, useScene } from '@pascal-app/core'
+import { useFrame } from '@react-three/fiber'
+import { lerp } from 'three/src/math/MathUtils.js'
+import useViewer from '../../store/use-viewer'
-const LEVEL_HEIGHT = 2.5;
-const EXPLODED_GAP = 5;
+const LEVEL_HEIGHT = 2.5
+const EXPLODED_GAP = 5
export const LevelSystem = () => {
useFrame((_, delta) => {
- const levelMode = useViewer.getState().levelMode;
+ const levelMode = useViewer.getState().levelMode
sceneRegistry.byType.level.forEach((levelId) => {
- const obj = sceneRegistry.nodes.get(levelId);
+ const obj = sceneRegistry.nodes.get(levelId)
if (obj) {
- const level = useScene.getState().nodes[levelId as LevelNode["id"]];
+ const level = useScene.getState().nodes[levelId as LevelNode['id']]
const targetY =
((level as any).level || 0) *
- (LEVEL_HEIGHT + (levelMode === "stacked" ? 0 : EXPLODED_GAP));
- obj.position.y = lerp(obj.position.y, targetY, delta * 3);
+ (LEVEL_HEIGHT + (levelMode === 'stacked' ? 0 : EXPLODED_GAP))
+ obj.position.y = lerp(obj.position.y, targetY, delta * 3)
}
- });
- });
- return null;
-};
+ })
+ })
+ return null
+}