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 */} + + ); };