diff --git a/.env.example b/.env.example index da1a7ee..d839cb9 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,3 @@ -VITE_LIVEBLOCKS_API_KEY=YOU_LIVEBLOCKS_API_KEY \ No newline at end of file +# Realtime WebSocket backend URL. Optional. +# If unset: dev -> ws://localhost:8090 ; prod (https) -> wss:///blocs-rt +VITE_RT_URL=ws://localhost:8090 diff --git a/README.md b/README.md index 17152a6..606b303 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@ # 3D Lego-like Brick Builder Web App -Build and collaborate in real-time on 3D models using rectangular bricks. This multi-user application allows users to create intricate designs in a 3D space using Lego-like bricks. +Build and collaborate in real-time on 3D models using rectangular bricks. This multi-user application lets several people create designs together in a shared 3D space using Lego-like bricks. ![App Screenshot](/public/Screenshot.png) +> Fork of [bhushan6/lego-builder](https://github.com/bhushan6/lego-builder) (MIT — see `LICENSE`, attribution preserved). The original used the **Liveblocks** SaaS for real-time collaboration; this fork replaces it with a **self-hosted, sovereign WebSocket backend** (`server/index.js`) — no third-party SaaS, no API key. + ## Features - **3D Rendering**: Utilizes `three.js` and `react-three-fiber` for efficient and smooth 3D rendering. -- **Real-time Collaboration**: Integrated with `liveblocks` to enable users to collaborate in real-time. -- **Intuitive UI**: Built with `radix ui` to provide a seamless user experience. -- **State Management**: `Zustand` is being used as state management solution. +- **Real-time Collaboration**: Self-hosted WebSocket relay (`server/index.js`) synchronizes the shared build (`bricks`, `cursorColors`) and per-user presence/cursors across everyone in a room. +- **Intuitive UI**: Built with `radix ui`. +- **State Management**: `Zustand`, with a custom `roomSync` middleware (`src/store/roomSync.js`) that exposes the same store surface the components expect. ## Technologies Used @@ -17,64 +19,70 @@ Build and collaborate in real-time on 3D models using rectangular bricks. This m - [three.js](https://threejs.org/) - [react-three-fiber](https://github.com/pmndrs/react-three-fiber) - [radix ui](https://www.radix-ui.com/) -- [liveblocks](https://liveblocks.io/) - [zustand](https://docs.pmnd.rs/zustand/getting-started/introduction) +- [ws](https://github.com/websockets/ws) — WebSocket server for the real-time backend - [Vite](https://vitejs.dev/) -## Live Demo +## Real-time architecture -Experience the app live [here](<[your_live_project_link](https://lego-builder.vercel.app/)>). +- **`server/index.js`** — a standalone Node WebSocket relay (uses `ws`, no build step). One server hosts many rooms; per room it keeps the current shared storage snapshot (`{ bricks, cursorColors }`) plus the connected clients. Clients connect with `ws(s)://HOST/?room=`. JSON protocol: + - server → client: `init`, `join`, `leave`, `presence`, `storage`, `event` + - client → server: `presence`, `storage` (last-writer-wins), `event` (ephemeral cursors, relayed to others only, never persisted) +- **`src/store/roomSync.js`** — a Zustand middleware that drops in for `@liveblocks/zustand`. It exposes `state.liveblocks.{enterRoom, leaveRoom, status, others, room}` where `room` has `broadcastEvent`, `subscribe("event", cb)` and no-op `history.undo/redo` stubs — so the existing components work unchanged. ## Local Development ### Prerequisites -- [Node.js](https://nodejs.org/) -- [Yarn](https://yarnpkg.com/) or [npm](https://www.npmjs.com/) +- [Node.js](https://nodejs.org/) (18+; tested on Node 26) +- [npm](https://www.npmjs.com/) ### Setup 1. Clone the repository: ```bash - git clone https://github.com/bhushan6/lego-builder.git - ``` - -2. Navigate to the project directory: - - ```bash + git clone https://git.saillant.cc/electron-rare/lego-builder.git cd lego-builder ``` -3. Install the dependencies: - - ```bash - yarn install - ``` - - or +2. Install dependencies: ```bash npm install ``` -4. Create a .env file in the root directory and add your Liveblocks public API key: +3. (Optional) point the client at a specific realtime server. Create `.env`: ```bash - echo "VITE_LIVEBLOCKS_API_KEY=your_liveblocks_public_api_key" > .env + echo "VITE_RT_URL=ws://localhost:8090" > .env ``` - Replace your_liveblocks_public_api_key with your actual Liveblocks public API key. + If unset, the client falls back to `ws://localhost:8090` in dev, or + `wss:///blocs-rt` when served over HTTPS in production (that path + is routed to the relay by the deployment infra). + +4. Start the realtime server (in one terminal): -5. Start the development server: ```bash - yarn dev + npm run server # listens on $PORT (default 8090) ``` - or + +5. Start the dev server (in another terminal): + ```bash npm run dev ``` -6. Open your browser and navigate to http://127.0.0.1:5173/ (or the port you've configured). + +6. Open the app at the URL Vite prints (note the `/blocs-pro/` base path, + e.g. http://localhost:5173/blocs-pro/). Open it in two browsers/tabs and + join the **same Room ID** to collaborate. + +### Build + +```bash +npm run build # outputs dist/, served under base /blocs-pro/ +``` ## Contribution diff --git a/package-lock.json b/package-lock.json index 7d4e081..e746986 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,6 @@ "name": "lego-builder", "version": "0.0.0", "dependencies": { - "@liveblocks/client": "^1.2.2", - "@liveblocks/zustand": "^1.2.2", "@radix-ui/colors": "^2.0.1", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.4", @@ -29,6 +27,7 @@ "react-dom": "^18.2.0", "three": "^0.155.0", "use-keyboard-shortcut": "^1.1.6", + "ws": "^8.18.0", "zustand": "^4.4.1" }, "devDependencies": { @@ -984,31 +983,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@liveblocks/client": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@liveblocks/client/-/client-1.2.2.tgz", - "integrity": "sha512-cav7NIoZNGmOC07p/spaCiNwLL/eeKKTAscLb2wN5myXG+xWlf6sj6zIxMOSA1eTDcDDvDMsnkLUviUUK0QGWA==", - "dependencies": { - "@liveblocks/core": "1.2.2" - } - }, - "node_modules/@liveblocks/core": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@liveblocks/core/-/core-1.2.2.tgz", - "integrity": "sha512-1WALetZ600EN3D4VaSpXse2Plp5+7os78B1kAANlOmf/NtiQKIoEBE40xYBVd4QAJgX6YIYGjOsQQja0CUqs7g==" - }, - "node_modules/@liveblocks/zustand": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@liveblocks/zustand/-/zustand-1.2.2.tgz", - "integrity": "sha512-F8xTtR3yE2SJ5FYHMpdAu6mheSBubjc4AekNaoePg2uV8BfZljNT1VrU+1SKCCEddV1e96OOYQqP+iBJcryYhg==", - "dependencies": { - "@liveblocks/client": "1.2.2", - "@liveblocks/core": "1.2.2" - }, - "peerDependencies": { - "zustand": "^4.1.3" - } - }, "node_modules/@mediapipe/tasks-vision": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.2.tgz", @@ -5283,6 +5257,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -5937,28 +5932,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "@liveblocks/client": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@liveblocks/client/-/client-1.2.2.tgz", - "integrity": "sha512-cav7NIoZNGmOC07p/spaCiNwLL/eeKKTAscLb2wN5myXG+xWlf6sj6zIxMOSA1eTDcDDvDMsnkLUviUUK0QGWA==", - "requires": { - "@liveblocks/core": "1.2.2" - } - }, - "@liveblocks/core": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@liveblocks/core/-/core-1.2.2.tgz", - "integrity": "sha512-1WALetZ600EN3D4VaSpXse2Plp5+7os78B1kAANlOmf/NtiQKIoEBE40xYBVd4QAJgX6YIYGjOsQQja0CUqs7g==" - }, - "@liveblocks/zustand": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@liveblocks/zustand/-/zustand-1.2.2.tgz", - "integrity": "sha512-F8xTtR3yE2SJ5FYHMpdAu6mheSBubjc4AekNaoePg2uV8BfZljNT1VrU+1SKCCEddV1e96OOYQqP+iBJcryYhg==", - "requires": { - "@liveblocks/client": "1.2.2", - "@liveblocks/core": "1.2.2" - } - }, "@mediapipe/tasks-vision": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.2.tgz", @@ -8827,6 +8800,12 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "requires": {} + }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 0c2e926..c428d56 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,10 @@ "dev": "vite", "build": "vite build", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "preview": "vite preview", + "server": "node server/index.js" }, "dependencies": { - "@liveblocks/client": "^1.2.2", - "@liveblocks/zustand": "^1.2.2", "@radix-ui/colors": "^2.0.1", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.4", @@ -31,6 +30,7 @@ "react-dom": "^18.2.0", "three": "^0.155.0", "use-keyboard-shortcut": "^1.1.6", + "ws": "^8.18.0", "zustand": "^4.4.1" }, "devDependencies": { diff --git a/vite.config.js b/vite.config.js index 5a33944..529517a 100644 --- a/vite.config.js +++ b/vite.config.js @@ -3,5 +3,6 @@ import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ + base: '/blocs-pro/', plugins: [react()], })