diff --git a/apps/editor/components/editor.tsx b/apps/editor/components/editor.tsx
index ecdbd120..17043745 100644
--- a/apps/editor/components/editor.tsx
+++ b/apps/editor/components/editor.tsx
@@ -1,7 +1,39 @@
"use client";
+import { useScene } from "@pascal-app/core";
import { Viewer } from "@pascal-app/viewer";
export default function Editor() {
- return ;
+ return (
+
+
+
+
+ );
}
+
+const LevelModeSwitcher = () => {
+ const setLevelMode = useScene((state) => state.setLevelMode);
+ const levelMode = useScene((state) => state.levelMode);
+
+ return (
+
+
+
+
+ );
+};
diff --git a/apps/editor/public/items/ac-block/model.glb b/apps/editor/public/items/ac-block/model.glb
new file mode 100644
index 00000000..fb898e85
Binary files /dev/null and b/apps/editor/public/items/ac-block/model.glb differ
diff --git a/apps/editor/public/items/ac-block/thumbnail.webp b/apps/editor/public/items/ac-block/thumbnail.webp
new file mode 100644
index 00000000..fcb856a7
Binary files /dev/null and b/apps/editor/public/items/ac-block/thumbnail.webp differ
diff --git a/apps/editor/public/items/air-conditioner-block/model.glb b/apps/editor/public/items/air-conditioner-block/model.glb
new file mode 100644
index 00000000..13eae3f3
Binary files /dev/null and b/apps/editor/public/items/air-conditioner-block/model.glb differ
diff --git a/apps/editor/public/items/air-conditioner-block/thumbnail.webp b/apps/editor/public/items/air-conditioner-block/thumbnail.webp
new file mode 100644
index 00000000..325126b6
Binary files /dev/null and b/apps/editor/public/items/air-conditioner-block/thumbnail.webp differ
diff --git a/apps/editor/public/items/air-conditioner/model.glb b/apps/editor/public/items/air-conditioner/model.glb
new file mode 100644
index 00000000..6589b35a
Binary files /dev/null and b/apps/editor/public/items/air-conditioner/model.glb differ
diff --git a/apps/editor/public/items/air-conditioner/thumbnail.webp b/apps/editor/public/items/air-conditioner/thumbnail.webp
new file mode 100644
index 00000000..48876e98
Binary files /dev/null and b/apps/editor/public/items/air-conditioner/thumbnail.webp differ
diff --git a/apps/editor/public/items/alarm-keypad/model.glb b/apps/editor/public/items/alarm-keypad/model.glb
new file mode 100644
index 00000000..7f8fbe44
Binary files /dev/null and b/apps/editor/public/items/alarm-keypad/model.glb differ
diff --git a/apps/editor/public/items/alarm-keypad/thumbnail.webp b/apps/editor/public/items/alarm-keypad/thumbnail.webp
new file mode 100644
index 00000000..4ecffb60
Binary files /dev/null and b/apps/editor/public/items/alarm-keypad/thumbnail.webp differ
diff --git a/apps/editor/public/items/ceiling-fan/model.glb b/apps/editor/public/items/ceiling-fan/model.glb
new file mode 100644
index 00000000..dfb99045
Binary files /dev/null and b/apps/editor/public/items/ceiling-fan/model.glb differ
diff --git a/apps/editor/public/items/ceiling-fan/thumbnail.webp b/apps/editor/public/items/ceiling-fan/thumbnail.webp
new file mode 100644
index 00000000..5761fe58
Binary files /dev/null and b/apps/editor/public/items/ceiling-fan/thumbnail.webp differ
diff --git a/apps/editor/public/items/ceiling-light/model.glb b/apps/editor/public/items/ceiling-light/model.glb
new file mode 100644
index 00000000..dbf59fa0
Binary files /dev/null and b/apps/editor/public/items/ceiling-light/model.glb differ
diff --git a/apps/editor/public/items/ceiling-light/thumbnail.webp b/apps/editor/public/items/ceiling-light/thumbnail.webp
new file mode 100644
index 00000000..0018b23a
Binary files /dev/null and b/apps/editor/public/items/ceiling-light/thumbnail.webp differ
diff --git a/apps/editor/public/items/circular-ceiling-light/model.glb b/apps/editor/public/items/circular-ceiling-light/model.glb
new file mode 100644
index 00000000..2d4b650c
Binary files /dev/null and b/apps/editor/public/items/circular-ceiling-light/model.glb differ
diff --git a/apps/editor/public/items/circular-ceiling-light/thumbnail.webp b/apps/editor/public/items/circular-ceiling-light/thumbnail.webp
new file mode 100644
index 00000000..0e0d7d56
Binary files /dev/null and b/apps/editor/public/items/circular-ceiling-light/thumbnail.webp differ
diff --git a/apps/editor/public/items/couch-medium/model.glb b/apps/editor/public/items/couch-medium/model.glb
new file mode 100644
index 00000000..3bd28def
Binary files /dev/null and b/apps/editor/public/items/couch-medium/model.glb differ
diff --git a/apps/editor/public/items/couch-medium/thumbnail.webp b/apps/editor/public/items/couch-medium/thumbnail.webp
new file mode 100644
index 00000000..f8847ded
Binary files /dev/null and b/apps/editor/public/items/couch-medium/thumbnail.webp differ
diff --git a/apps/editor/public/items/couch-small/model.glb b/apps/editor/public/items/couch-small/model.glb
new file mode 100644
index 00000000..2e150397
Binary files /dev/null and b/apps/editor/public/items/couch-small/model.glb differ
diff --git a/apps/editor/public/items/couch-small/thumbnail.webp b/apps/editor/public/items/couch-small/thumbnail.webp
new file mode 100644
index 00000000..54322b1a
Binary files /dev/null and b/apps/editor/public/items/couch-small/thumbnail.webp differ
diff --git a/apps/editor/public/items/desk/model.glb b/apps/editor/public/items/desk/model.glb
new file mode 100644
index 00000000..57268caa
Binary files /dev/null and b/apps/editor/public/items/desk/model.glb differ
diff --git a/apps/editor/public/items/desk/thumbnail.webp b/apps/editor/public/items/desk/thumbnail.webp
new file mode 100644
index 00000000..401453b7
Binary files /dev/null and b/apps/editor/public/items/desk/thumbnail.webp differ
diff --git a/apps/editor/public/items/door-with-bar/model.glb b/apps/editor/public/items/door-with-bar/model.glb
new file mode 100644
index 00000000..2f140840
Binary files /dev/null and b/apps/editor/public/items/door-with-bar/model.glb differ
diff --git a/apps/editor/public/items/door-with-bar/thumbnail.webp b/apps/editor/public/items/door-with-bar/thumbnail.webp
new file mode 100644
index 00000000..67ced663
Binary files /dev/null and b/apps/editor/public/items/door-with-bar/thumbnail.webp differ
diff --git a/apps/editor/public/items/door/model.glb b/apps/editor/public/items/door/model.glb
new file mode 100644
index 00000000..5c3ca143
Binary files /dev/null and b/apps/editor/public/items/door/model.glb differ
diff --git a/apps/editor/public/items/door/thumbnail.webp b/apps/editor/public/items/door/thumbnail.webp
new file mode 100644
index 00000000..6cf99bf0
Binary files /dev/null and b/apps/editor/public/items/door/thumbnail.webp differ
diff --git a/apps/editor/public/items/doorway-front/model.glb b/apps/editor/public/items/doorway-front/model.glb
new file mode 100644
index 00000000..20e06d6b
Binary files /dev/null and b/apps/editor/public/items/doorway-front/model.glb differ
diff --git a/apps/editor/public/items/doorway-front/thumbnail.webp b/apps/editor/public/items/doorway-front/thumbnail.webp
new file mode 100644
index 00000000..e03de606
Binary files /dev/null and b/apps/editor/public/items/doorway-front/thumbnail.webp differ
diff --git a/apps/editor/public/items/electric-panel/model.glb b/apps/editor/public/items/electric-panel/model.glb
new file mode 100644
index 00000000..1f3c0854
Binary files /dev/null and b/apps/editor/public/items/electric-panel/model.glb differ
diff --git a/apps/editor/public/items/electric-panel/thumbnail.webp b/apps/editor/public/items/electric-panel/thumbnail.webp
new file mode 100644
index 00000000..21003a9e
Binary files /dev/null and b/apps/editor/public/items/electric-panel/thumbnail.webp differ
diff --git a/apps/editor/public/items/exit-sign/model.glb b/apps/editor/public/items/exit-sign/model.glb
new file mode 100644
index 00000000..d9763fe5
Binary files /dev/null and b/apps/editor/public/items/exit-sign/model.glb differ
diff --git a/apps/editor/public/items/exit-sign/thumbnail.webp b/apps/editor/public/items/exit-sign/thumbnail.webp
new file mode 100644
index 00000000..546d6b56
Binary files /dev/null and b/apps/editor/public/items/exit-sign/thumbnail.webp differ
diff --git a/apps/editor/public/items/fence/model.glb b/apps/editor/public/items/fence/model.glb
new file mode 100644
index 00000000..4a354cf2
Binary files /dev/null and b/apps/editor/public/items/fence/model.glb differ
diff --git a/apps/editor/public/items/fence/thumbnail.webp b/apps/editor/public/items/fence/thumbnail.webp
new file mode 100644
index 00000000..78c5bdc7
Binary files /dev/null and b/apps/editor/public/items/fence/thumbnail.webp differ
diff --git a/apps/editor/public/items/fire-alarm/model.glb b/apps/editor/public/items/fire-alarm/model.glb
new file mode 100644
index 00000000..9bd796ce
Binary files /dev/null and b/apps/editor/public/items/fire-alarm/model.glb differ
diff --git a/apps/editor/public/items/fire-alarm/thumbnail.webp b/apps/editor/public/items/fire-alarm/thumbnail.webp
new file mode 100644
index 00000000..709674e6
Binary files /dev/null and b/apps/editor/public/items/fire-alarm/thumbnail.webp differ
diff --git a/apps/editor/public/items/fire-detector/model.glb b/apps/editor/public/items/fire-detector/model.glb
new file mode 100644
index 00000000..dc327a88
Binary files /dev/null and b/apps/editor/public/items/fire-detector/model.glb differ
diff --git a/apps/editor/public/items/fire-detector/thumbnail.webp b/apps/editor/public/items/fire-detector/thumbnail.webp
new file mode 100644
index 00000000..78f74cad
Binary files /dev/null and b/apps/editor/public/items/fire-detector/thumbnail.webp differ
diff --git a/apps/editor/public/items/fire-extinguisher/model.glb b/apps/editor/public/items/fire-extinguisher/model.glb
new file mode 100644
index 00000000..caab4b62
Binary files /dev/null and b/apps/editor/public/items/fire-extinguisher/model.glb differ
diff --git a/apps/editor/public/items/fire-extinguisher/thumbnail.webp b/apps/editor/public/items/fire-extinguisher/thumbnail.webp
new file mode 100644
index 00000000..61473e54
Binary files /dev/null and b/apps/editor/public/items/fire-extinguisher/thumbnail.webp differ
diff --git a/apps/editor/public/items/flat-screen-tv/model.glb b/apps/editor/public/items/flat-screen-tv/model.glb
new file mode 100644
index 00000000..8a9e6786
Binary files /dev/null and b/apps/editor/public/items/flat-screen-tv/model.glb differ
diff --git a/apps/editor/public/items/flat-screen-tv/thumbnail.webp b/apps/editor/public/items/flat-screen-tv/thumbnail.webp
new file mode 100644
index 00000000..aa3bdcbe
Binary files /dev/null and b/apps/editor/public/items/flat-screen-tv/thumbnail.webp differ
diff --git a/apps/editor/public/items/freezer/model.glb b/apps/editor/public/items/freezer/model.glb
new file mode 100644
index 00000000..61b23deb
Binary files /dev/null and b/apps/editor/public/items/freezer/model.glb differ
diff --git a/apps/editor/public/items/freezer/thumbnail.webp b/apps/editor/public/items/freezer/thumbnail.webp
new file mode 100644
index 00000000..ba47ac99
Binary files /dev/null and b/apps/editor/public/items/freezer/thumbnail.webp differ
diff --git a/apps/editor/public/items/hedge/model.glb b/apps/editor/public/items/hedge/model.glb
new file mode 100644
index 00000000..b5af48c8
Binary files /dev/null and b/apps/editor/public/items/hedge/model.glb differ
diff --git a/apps/editor/public/items/hedge/thumbnail.webp b/apps/editor/public/items/hedge/thumbnail.webp
new file mode 100644
index 00000000..548cf59e
Binary files /dev/null and b/apps/editor/public/items/hedge/thumbnail.webp differ
diff --git a/apps/editor/public/items/kitchen-fridge/model.glb b/apps/editor/public/items/kitchen-fridge/model.glb
new file mode 100644
index 00000000..b9fb5c84
Binary files /dev/null and b/apps/editor/public/items/kitchen-fridge/model.glb differ
diff --git a/apps/editor/public/items/kitchen-fridge/thumbnail.webp b/apps/editor/public/items/kitchen-fridge/thumbnail.webp
new file mode 100644
index 00000000..fb523868
Binary files /dev/null and b/apps/editor/public/items/kitchen-fridge/thumbnail.webp differ
diff --git a/apps/editor/public/items/parking-spot/model.glb b/apps/editor/public/items/parking-spot/model.glb
new file mode 100644
index 00000000..62ccd485
Binary files /dev/null and b/apps/editor/public/items/parking-spot/model.glb differ
diff --git a/apps/editor/public/items/parking-spot/thumbnail.webp b/apps/editor/public/items/parking-spot/thumbnail.webp
new file mode 100644
index 00000000..d9920b0e
Binary files /dev/null and b/apps/editor/public/items/parking-spot/thumbnail.webp differ
diff --git a/apps/editor/public/items/rectangular-ceiling-light/model.glb b/apps/editor/public/items/rectangular-ceiling-light/model.glb
new file mode 100644
index 00000000..4743f6d5
Binary files /dev/null and b/apps/editor/public/items/rectangular-ceiling-light/model.glb differ
diff --git a/apps/editor/public/items/rectangular-ceiling-light/thumbnail.webp b/apps/editor/public/items/rectangular-ceiling-light/thumbnail.webp
new file mode 100644
index 00000000..0ebda5ec
Binary files /dev/null and b/apps/editor/public/items/rectangular-ceiling-light/thumbnail.webp differ
diff --git a/apps/editor/public/items/sink-cabinet/model.glb b/apps/editor/public/items/sink-cabinet/model.glb
new file mode 100644
index 00000000..1b48f992
Binary files /dev/null and b/apps/editor/public/items/sink-cabinet/model.glb differ
diff --git a/apps/editor/public/items/sink-cabinet/thumbnail.webp b/apps/editor/public/items/sink-cabinet/thumbnail.webp
new file mode 100644
index 00000000..d54969bc
Binary files /dev/null and b/apps/editor/public/items/sink-cabinet/thumbnail.webp differ
diff --git a/apps/editor/public/items/smoke-detector/model.glb b/apps/editor/public/items/smoke-detector/model.glb
new file mode 100644
index 00000000..e290938e
Binary files /dev/null and b/apps/editor/public/items/smoke-detector/model.glb differ
diff --git a/apps/editor/public/items/smoke-detector/thumbnail.webp b/apps/editor/public/items/smoke-detector/thumbnail.webp
new file mode 100644
index 00000000..7a7d8390
Binary files /dev/null and b/apps/editor/public/items/smoke-detector/thumbnail.webp differ
diff --git a/apps/editor/public/items/sprinkler/model.glb b/apps/editor/public/items/sprinkler/model.glb
new file mode 100644
index 00000000..bbf40e84
Binary files /dev/null and b/apps/editor/public/items/sprinkler/model.glb differ
diff --git a/apps/editor/public/items/sprinkler/thumbnail.webp b/apps/editor/public/items/sprinkler/thumbnail.webp
new file mode 100644
index 00000000..8c0b0682
Binary files /dev/null and b/apps/editor/public/items/sprinkler/thumbnail.webp differ
diff --git a/apps/editor/public/items/table/model.glb b/apps/editor/public/items/table/model.glb
new file mode 100644
index 00000000..47a14a1d
Binary files /dev/null and b/apps/editor/public/items/table/model.glb differ
diff --git a/apps/editor/public/items/table/thumbnail.webp b/apps/editor/public/items/table/thumbnail.webp
new file mode 100644
index 00000000..cea8cc41
Binary files /dev/null and b/apps/editor/public/items/table/thumbnail.webp differ
diff --git a/apps/editor/public/items/thermostat/model.glb b/apps/editor/public/items/thermostat/model.glb
new file mode 100644
index 00000000..298218d0
Binary files /dev/null and b/apps/editor/public/items/thermostat/model.glb differ
diff --git a/apps/editor/public/items/thermostat/thumbnail.webp b/apps/editor/public/items/thermostat/thumbnail.webp
new file mode 100644
index 00000000..220e9b6d
Binary files /dev/null and b/apps/editor/public/items/thermostat/thumbnail.webp differ
diff --git a/apps/editor/public/items/toilet/model.glb b/apps/editor/public/items/toilet/model.glb
new file mode 100644
index 00000000..d7512252
Binary files /dev/null and b/apps/editor/public/items/toilet/model.glb differ
diff --git a/apps/editor/public/items/toilet/thumbnail.webp b/apps/editor/public/items/toilet/thumbnail.webp
new file mode 100644
index 00000000..6c1bce63
Binary files /dev/null and b/apps/editor/public/items/toilet/thumbnail.webp differ
diff --git a/apps/editor/public/items/tree/model.glb b/apps/editor/public/items/tree/model.glb
new file mode 100644
index 00000000..5defa84b
Binary files /dev/null and b/apps/editor/public/items/tree/model.glb differ
diff --git a/apps/editor/public/items/tree/thumbnail.webp b/apps/editor/public/items/tree/thumbnail.webp
new file mode 100644
index 00000000..fc312335
Binary files /dev/null and b/apps/editor/public/items/tree/thumbnail.webp differ
diff --git a/apps/editor/public/items/wall-art-06/model.glb b/apps/editor/public/items/wall-art-06/model.glb
new file mode 100644
index 00000000..ce91fbb7
Binary files /dev/null and b/apps/editor/public/items/wall-art-06/model.glb differ
diff --git a/apps/editor/public/items/wall-art-06/thumbnail.webp b/apps/editor/public/items/wall-art-06/thumbnail.webp
new file mode 100644
index 00000000..3f0398fa
Binary files /dev/null and b/apps/editor/public/items/wall-art-06/thumbnail.webp differ
diff --git a/apps/editor/public/items/wall-sink/model.glb b/apps/editor/public/items/wall-sink/model.glb
new file mode 100644
index 00000000..45401e4a
Binary files /dev/null and b/apps/editor/public/items/wall-sink/model.glb differ
diff --git a/apps/editor/public/items/wall-sink/thumbnail.webp b/apps/editor/public/items/wall-sink/thumbnail.webp
new file mode 100644
index 00000000..97597c99
Binary files /dev/null and b/apps/editor/public/items/wall-sink/thumbnail.webp differ
diff --git a/apps/editor/public/items/window-round/model.glb b/apps/editor/public/items/window-round/model.glb
new file mode 100644
index 00000000..efe2c88d
Binary files /dev/null and b/apps/editor/public/items/window-round/model.glb differ
diff --git a/apps/editor/public/items/window-round/thumbnail.webp b/apps/editor/public/items/window-round/thumbnail.webp
new file mode 100644
index 00000000..97be02a4
Binary files /dev/null and b/apps/editor/public/items/window-round/thumbnail.webp differ
diff --git a/apps/editor/public/items/window-small-2/model.glb b/apps/editor/public/items/window-small-2/model.glb
new file mode 100644
index 00000000..65bef5d2
Binary files /dev/null and b/apps/editor/public/items/window-small-2/model.glb differ
diff --git a/apps/editor/public/items/window-small-2/thumbnail.webp b/apps/editor/public/items/window-small-2/thumbnail.webp
new file mode 100644
index 00000000..8e5c6ef4
Binary files /dev/null and b/apps/editor/public/items/window-small-2/thumbnail.webp differ
diff --git a/apps/editor/public/items/window-small/model.glb b/apps/editor/public/items/window-small/model.glb
new file mode 100644
index 00000000..16c8fcfe
Binary files /dev/null and b/apps/editor/public/items/window-small/model.glb differ
diff --git a/apps/editor/public/items/window-small/thumbnail.webp b/apps/editor/public/items/window-small/thumbnail.webp
new file mode 100644
index 00000000..ef8c377f
Binary files /dev/null and b/apps/editor/public/items/window-small/thumbnail.webp differ
diff --git a/apps/editor/public/items/window1-black-open-1731/model.glb b/apps/editor/public/items/window1-black-open-1731/model.glb
new file mode 100644
index 00000000..7a50eac0
Binary files /dev/null and b/apps/editor/public/items/window1-black-open-1731/model.glb differ
diff --git a/apps/editor/public/items/window1-black-open-1731/thumbnail.webp b/apps/editor/public/items/window1-black-open-1731/thumbnail.webp
new file mode 100644
index 00000000..1488455c
Binary files /dev/null and b/apps/editor/public/items/window1-black-open-1731/thumbnail.webp differ
diff --git a/bun.lock b/bun.lock
index ff04d206..18a2c378 100644
--- a/bun.lock
+++ b/bun.lock
@@ -50,13 +50,15 @@
"@react-three/drei": "^10",
"@react-three/fiber": "^9",
"@repo/typescript-config": "*",
- "three": "^0.176",
+ "@types/react": "^19.2.2",
+ "@types/three": "^0.182.0",
+ "three": "^0.182",
"typescript": "5.9.2",
},
"peerDependencies": {
"@react-three/drei": "^10",
"@react-three/fiber": "^9",
- "three": "^0.176",
+ "three": "^0.182",
},
},
"packages/eslint-config": {
@@ -902,7 +904,7 @@
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
- "three": ["three@0.176.0", "", {}, "sha512-PWRKYWQo23ojf9oZSlRGH8K09q7nRSWx6LY/HF/UUrMdYgN9i1e2OwJYHoQjwc6HF/4lvvYLC5YC1X8UJL2ZpA=="],
+ "three": ["three@0.182.0", "", {}, "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ=="],
"three-mesh-bvh": ["three-mesh-bvh@0.8.3", "", { "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg=="],
@@ -998,8 +1000,6 @@
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
- "@pascal-app/viewer/three": ["three@0.182.0", "", {}, "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ=="],
-
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
@@ -1036,8 +1036,6 @@
"tunnel-rat/zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
- "web/three": ["three@0.182.0", "", {}, "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ=="],
-
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
diff --git a/packages/core/package.json b/packages/core/package.json
index 46bc4dee..26ca1bbc 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -14,14 +14,16 @@
"peerDependencies": {
"@react-three/drei": "^10",
"@react-three/fiber": "^9",
- "three": "^0.176"
+ "three": "^0.182"
},
"devDependencies": {
"@react-three/drei": "^10",
"@react-three/fiber": "^9",
"@repo/typescript-config": "*",
- "three": "^0.176",
- "typescript": "5.9.2"
+ "@types/react": "^19.2.2",
+ "three": "^0.182",
+ "typescript": "5.9.2",
+ "@types/three": "^0.182.0"
},
"dependencies": {
"dedent": "^1.7.1",
diff --git a/packages/core/src/hooks/scene-registry/scene-registry.ts b/packages/core/src/hooks/scene-registry/scene-registry.ts
new file mode 100644
index 00000000..9a102e12
--- /dev/null
+++ b/packages/core/src/hooks/scene-registry/scene-registry.ts
@@ -0,0 +1,40 @@
+import * as THREE from "three";
+
+import { useLayoutEffect } from "react";
+
+export const sceneRegistry = {
+ // Master lookup: ID -> Object3D
+ nodes: new Map(),
+
+ // Categorized lookups: Type -> Set of IDs
+ // Using a Set is faster for adding/deleting than an Array
+ byType: {
+ level: new Set(),
+ wall: new Set(),
+ item: new Set(),
+ slab: new Set(),
+ },
+};
+
+export function useRegistry(
+ id: string,
+ type: keyof typeof sceneRegistry.byType,
+ ref: React.RefObject
+) {
+ useLayoutEffect(() => {
+ const obj = ref.current;
+ if (!obj) return;
+
+ // 1. Add to master map
+ sceneRegistry.nodes.set(id, obj);
+
+ // 2. Add to type-specific set
+ sceneRegistry.byType[type].add(id);
+
+ // 4. Cleanup when component unmounts
+ return () => {
+ sceneRegistry.nodes.delete(id);
+ sceneRegistry.byType[type].delete(id);
+ };
+ }, [id, type, ref]);
+}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index e7e73ffe..afcf1ddf 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -1,5 +1,12 @@
// Store
-export { default as useScene } from "./store/useScene"
+export { default as useScene } from "./store/useScene";
+
+// Hooks
+export { useRegistry } from "./hooks/scene-registry/scene-registry";
+
+// Systems
+export { LevelSystem } from "./systems/level/level-system";
+export { WallSystem } from "./systems/wall/wall-system";
// Schema
-export * from "./schema"
+export * from "./schema";
diff --git a/packages/core/src/schema/nodes/item.ts b/packages/core/src/schema/nodes/item.ts
index ce25c05c..558fe955 100644
--- a/packages/core/src/schema/nodes/item.ts
+++ b/packages/core/src/schema/nodes/item.ts
@@ -13,7 +13,9 @@ export const ItemNode = BaseNode.extend({
.object({
category: z.string(),
src: z.string(),
- dimensions: z.tuple([z.number(), z.number(), z.number()]), // [w, h, d]
+ dimensions: z
+ .tuple([z.number(), z.number(), z.number()])
+ .default([1, 1, 1]), // [w, h, d]
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]),
diff --git a/packages/core/src/schema/nodes/wall.ts b/packages/core/src/schema/nodes/wall.ts
index 53a3b228..4bcdf41f 100644
--- a/packages/core/src/schema/nodes/wall.ts
+++ b/packages/core/src/schema/nodes/wall.ts
@@ -1,6 +1,7 @@
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";
@@ -8,11 +9,7 @@ import { BaseNode, nodeType, objectId } from "../base";
export const WallNode = BaseNode.extend({
id: objectId("wall"),
type: nodeType("wall"),
- // get children() {
- // return z
- // .array(z.discriminatedUnion("type", [DoorNode, WindowNode, ItemNode]))
- // .default([]);
- // },
+ children: z.array(ItemNode.shape.id).default([]),
// Specific props
thickness: z.number().optional(),
height: z.number().optional(),
diff --git a/packages/core/src/schema/types.ts b/packages/core/src/schema/types.ts
index 7d0300c6..b8b48434 100644
--- a/packages/core/src/schema/types.ts
+++ b/packages/core/src/schema/types.ts
@@ -1,5 +1,6 @@
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";
@@ -9,6 +10,7 @@ export const AnyNode = z.discriminatedUnion("type", [
BuildingNode,
LevelNode,
WallNode,
+ ItemNode,
]);
export type AnyNode = z.infer;
diff --git a/packages/core/src/store/useScene.ts b/packages/core/src/store/useScene.ts
index 2096ff9e..b8b766c4 100644
--- a/packages/core/src/store/useScene.ts
+++ b/packages/core/src/store/useScene.ts
@@ -1,6 +1,7 @@
"use client";
import { create } from "zustand";
+import { ItemNode } from "../schema";
import { LevelNode } from "../schema/nodes/level";
import { WallNode } from "../schema/nodes/wall";
import { AnyNode, AnyNodeId } from "../schema/types";
@@ -72,6 +73,19 @@ const useScene = create()((set, get) => ({
children: [],
});
+ const window1 = ItemNode.parse({
+ type: "item",
+ name: "Window",
+ position: [2.5, 0.5, 0],
+ asset: {
+ category: "windows",
+ attachTo: "wall",
+ src: "/items/window-round/model.glb",
+ },
+ });
+
+ wall0.children.push(window1.id);
+
level0.children.push(wall0.id, wall1.id, wall2.id, wall3.id);
// Define all nodes flat
@@ -83,6 +97,7 @@ const useScene = create()((set, get) => ({
[wall1.id]: wall1,
[wall2.id]: wall2,
[wall3.id]: wall3,
+ [window1.id]: window1,
};
// Root nodes are the levels
diff --git a/packages/core/src/systems/level/level-system.tsx b/packages/core/src/systems/level/level-system.tsx
new file mode 100644
index 00000000..6b005030
--- /dev/null
+++ b/packages/core/src/systems/level/level-system.tsx
@@ -0,0 +1,25 @@
+import { useFrame } from "@react-three/fiber";
+import { lerp } from "three/src/math/MathUtils.js";
+import { sceneRegistry } from "../../hooks/scene-registry/scene-registry";
+import { LevelNode } from "../../schema";
+import useScene from "../../store/useScene";
+
+const LEVEL_HEIGHT = 2.5;
+const EXPLODED_GAP = 5;
+
+export const LevelSystem = () => {
+ useFrame((_, delta) => {
+ const levelMode = useScene.getState().levelMode;
+ sceneRegistry.byType.level.forEach((levelId) => {
+ const obj = sceneRegistry.nodes.get(levelId);
+ if (obj) {
+ 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);
+ }
+ });
+ });
+ return null;
+};
diff --git a/packages/core/src/systems/wall/wall-system.tsx b/packages/core/src/systems/wall/wall-system.tsx
new file mode 100644
index 00000000..85601b69
--- /dev/null
+++ b/packages/core/src/systems/wall/wall-system.tsx
@@ -0,0 +1,205 @@
+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/useScene";
+
+export const WallSystem = () => {
+ const { nodes, dirtyNodes, clearDirty } = useScene();
+
+ useFrame(() => {
+ if (dirtyNodes.size === 0) return;
+
+ dirtyNodes.forEach((id) => {
+ 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) {
+ // updateWallGeometry(node.parentId);
+ // return;
+ // }
+
+ // 2. If the wall itself is dirty
+ if (node.type === "wall" && mesh) {
+ updateWallGeometry(id);
+ }
+ clearDirty(id); // Reset for next frame
+ });
+ });
+
+ 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 mesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh;
+ if (!mesh) return;
+
+ const childrenIds = node.children || [];
+ const childrenNodes = childrenIds.map(
+ (childId) => useScene.getState().nodes[childId]
+ );
+
+ // Perform the Extrusion with Holes logic we discussed
+ const newGeo = generateExtrudedWall(node, childrenNodes);
+
+ mesh.geometry.dispose();
+ mesh.geometry = newGeo;
+
+ 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;
+}
+
+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.2;
+
+ // 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();
+
+ // 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 wallAngle = Math.atan2(
+ wallNode.end[1] - wallNode.start[1],
+ wallNode.end[0] - wallNode.start[0]
+ );
+
+ childrenNodes.forEach((child) => {
+ // Only process items that are intended to be wall cutouts
+ if (child.type !== "item") return;
+
+ const childMesh = sceneRegistry.nodes.get(child.id);
+ if (!childMesh) return;
+
+ const cutoutMesh = childMesh.getObjectByName("cutout") as THREE.Mesh;
+ if (!cutoutMesh) return;
+
+ const holePath = createPathFromCutout(cutoutMesh, wallStart, wallAngle);
+ if (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);
+
+ return geometry;
+}
+
+/**
+ * Creates a Path from a cutout mesh geometry, transforming vertices
+ * from world space to wall-local space.
+ *
+ * Wall-local space:
+ * - Origin at wall start point
+ * - X axis runs along the wall (toward end point)
+ * - Y axis is height (world Y)
+ */
+function createPathFromCutout(
+ cutoutMesh: THREE.Mesh,
+ wallStart: [number, number],
+ wallAngle: number
+): THREE.Path | null {
+ const geometry = cutoutMesh.geometry;
+ if (!geometry) return null;
+
+ const positions = geometry.attributes.position;
+ if (!positions) return null;
+
+ // Update world matrix to get correct world positions
+ 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();
+
+ // Precompute sin/cos for rotation
+ const cosAngle = Math.cos(-wallAngle);
+ const sinAngle = Math.sin(-wallAngle);
+
+ for (let i = 0; i < positions.count; i++) {
+ v3.fromBufferAttribute(positions, i);
+
+ // Transform to world space
+ 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];
+
+ // 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 (world Y)
+ const localX = worldX * cosAngle - worldZ * sinAngle;
+ const localY = v3.y; // Height stays the same
+
+ // Create a key for deduplication (with small tolerance)
+ const key = `${localX.toFixed(4)},${localY.toFixed(4)}`;
+ if (!seen.has(key)) {
+ seen.add(key);
+ uniquePoints.push(new THREE.Vector2(localX, localY));
+ }
+ }
+
+ if (uniquePoints.length < 3) return null;
+
+ // Sort points in counter-clockwise order around centroid
+ const centroid = new THREE.Vector2(0, 0);
+ for (const p of uniquePoints) {
+ centroid.add(p);
+ }
+ 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;
+ });
+
+ // Create the path
+ 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.closePath();
+
+ return path;
+}
diff --git a/packages/viewer/src/components/renderers/item/item-renderer.tsx b/packages/viewer/src/components/renderers/item/item-renderer.tsx
new file mode 100644
index 00000000..bdfdfccc
--- /dev/null
+++ b/packages/viewer/src/components/renderers/item/item-renderer.tsx
@@ -0,0 +1,25 @@
+import { ItemNode, useRegistry } from "@pascal-app/core";
+import { Clone } from "@react-three/drei/core/Clone";
+import { useGLTF } from "@react-three/drei/core/Gltf";
+import { useRef } from "react";
+import { Group } from "three";
+
+export const ItemRenderer = ({ node }: { node: ItemNode }) => {
+ const ref = useRef(null!);
+ const { scene, nodes } = useGLTF(node.asset.src);
+
+ useRegistry(node.id, node.type, ref);
+
+ if (nodes.cutout) {
+ nodes.cutout.visible = false;
+ }
+
+ return (
+
+ );
+};
diff --git a/packages/viewer/src/components/renderers/level/level-renderer.tsx b/packages/viewer/src/components/renderers/level/level-renderer.tsx
new file mode 100644
index 00000000..1446672b
--- /dev/null
+++ b/packages/viewer/src/components/renderers/level/level-renderer.tsx
@@ -0,0 +1,21 @@
+import { LevelNode, useRegistry } from "@pascal-app/core";
+import { useRef } from "react";
+import { Group } from "three";
+import { NodeRenderer } from "../node-renderer";
+
+export const LevelRenderer = ({ node }: { node: LevelNode }) => {
+ const ref = useRef(null!);
+
+ useRegistry(node.id, node.type, ref);
+ return (
+
+
+
+
+
+ {node.children.map((childId) => (
+
+ ))}
+
+ );
+};
diff --git a/packages/viewer/src/components/renderers/node-renderer.tsx b/packages/viewer/src/components/renderers/node-renderer.tsx
new file mode 100644
index 00000000..e9a2f959
--- /dev/null
+++ b/packages/viewer/src/components/renderers/node-renderer.tsx
@@ -0,0 +1,20 @@
+"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";
+
+export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode["id"] }) => {
+ const node = useScene((state) => state.nodes[nodeId]);
+
+ if (!node) return null;
+
+ return (
+ <>
+ {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 c789dd2d..569d0a4e 100644
--- a/packages/viewer/src/components/renderers/scene-renderer.tsx
+++ b/packages/viewer/src/components/renderers/scene-renderer.tsx
@@ -1,8 +1,7 @@
"use client";
-import { AnyNode, LevelNode, useScene, WallNode } from "@pascal-app/core";
-import { useRef } from "react";
-import * as THREE from "three/webgpu";
+import { useScene } from "@pascal-app/core";
+import { NodeRenderer } from "./node-renderer";
export const SceneRenderer = () => {
const rootNodes = useScene((state) => state.rootNodeIds);
@@ -11,56 +10,3 @@ export const SceneRenderer = () => {
));
};
-
-const NodeRenderer = ({ nodeId }: { nodeId: AnyNode["id"] }) => {
- const node = useScene((state) => state.nodes[nodeId]);
-
- if (!node) return null;
-
- return (
- <>
- {node.type === "level" && }
- {/* {node.type === "item" && } */}
- {node.type === "wall" && }
- >
- );
-};
-
-const LevelRenderer = ({ node }: { node: LevelNode }) => {
- const ref = useRef(null!);
-
- // useRegistry(node.id, node.type, ref);
- return (
-
-
-
-
-
- {node.children.map((childId) => (
-
- ))}
-
- );
-};
-
-const WallRenderer = ({ node }: { node: WallNode }) => {
- const ref = useRef(null!);
- console.log("node", node);
-
- // useRegistry(node.id, "wall", ref);
-
- return (
-
- {/* WallSystem will replace this geometry in the next frame */}
-
-
-
- {/* If you want windows to be inside the wall's local coordinate system:
- render children here.
- */}
- {/* {node.children.map((childId) => (
-
- ))} */}
-
- );
-};
diff --git a/packages/viewer/src/components/renderers/wall/wall-renderer.tsx b/packages/viewer/src/components/renderers/wall/wall-renderer.tsx
new file mode 100644
index 00000000..e10ca3e7
--- /dev/null
+++ b/packages/viewer/src/components/renderers/wall/wall-renderer.tsx
@@ -0,0 +1,22 @@
+import { useRegistry, WallNode } from "@pascal-app/core";
+import { useRef } from "react";
+import { Mesh } from "three";
+import { NodeRenderer } from "../node-renderer";
+
+export const WallRenderer = ({ node }: { node: WallNode }) => {
+ const ref = useRef(null!);
+
+ useRegistry(node.id, "wall", ref);
+
+ return (
+
+ {/* WallSystem will replace this geometry in the next frame */}
+
+
+
+ {node.children.map((childId) => (
+
+ ))}
+
+ );
+};
diff --git a/packages/viewer/src/components/viewer/viewer.tsx b/packages/viewer/src/components/viewer/viewer.tsx
index 9a850768..b6cb73c5 100644
--- a/packages/viewer/src/components/viewer/viewer.tsx
+++ b/packages/viewer/src/components/viewer/viewer.tsx
@@ -3,6 +3,7 @@
import { Environment, OrbitControls } from "@react-three/drei";
import { Canvas, ThreeToJSXElements } from "@react-three/fiber";
+import { LevelSystem, WallSystem } from "@pascal-app/core";
import { extend } from "@react-three/fiber";
import * as THREE from "three/webgpu";
import { SceneRenderer } from "../renderers/scene-renderer";
@@ -25,10 +26,15 @@ const Viewer: React.FC = () => {
return renderer;
}}
shadows
+ camera={{ position: [3, 3, 3], fov: 50 }}
>
+
+ {/* Default Systems */}
+
+
);
};