[READ-ONLY] Mirror of https://github.com/flo-bit/room. tiny 3d rooms saved locally or in your bluesky account, svelte/threlte flo-bit.dev/room/
0

Configure Feed

Select the types of activity you want to include in your feed.

refactor pt2 hopefully this doesnt break everything

Florian (Mar 10, 2025, 2:30 AM +0100) c216dbd5 d40a0f20

+610 -622
+127
src/lib/editor/editorState.svelte.ts
··· 1 + import { trackEvent } from '$lib/analytics'; 2 + import { AllObjects } from '$lib/room/models'; 3 + import { type RoomObjectData, roomState, currentVersion } from '$lib/room/state.svelte'; 4 + import type { TransformControls } from '@threlte/extras'; 5 + 6 + export const editorState: { 7 + selectedObject: RoomObjectData | null; 8 + transformControls: TransformControls | undefined; 9 + placingObject: RoomObjectData | null; 10 + 11 + isEditing: boolean; 12 + } = $state({ 13 + selectedObject: null, 14 + transformControls: undefined, 15 + placingObject: null, 16 + 17 + isEditing: false 18 + }); 19 + 20 + export function applyTransformOfSelected() { 21 + if (!editorState.selectedObject || !editorState.transformControls?.object) return; 22 + editorState.selectedObject.position = [ 23 + editorState.transformControls.object.position.x, 24 + editorState.transformControls.object.position.y, 25 + editorState.transformControls.object.position.z 26 + ]; 27 + 28 + if (editorState.selectedObject.placement === 'wallX') { 29 + editorState.selectedObject.rotation = editorState.transformControls.object.rotation.x; 30 + } else if (editorState.selectedObject.placement === 'wallZ') { 31 + editorState.selectedObject.rotation = editorState.transformControls.object.rotation.z; 32 + } else { 33 + editorState.selectedObject.rotation = editorState.transformControls.object.rotation.y; 34 + } 35 + 36 + saveRoomToLocalStorage(); 37 + } 38 + 39 + export function rotateObject(rotation: number) { 40 + if (editorState.selectedObject) { 41 + applyTransformOfSelected(); 42 + 43 + const selectedObject = editorState.selectedObject; 44 + editorState.selectedObject = null; 45 + 46 + setTimeout(() => { 47 + // rotate right 48 + selectedObject.rotation -= rotation; 49 + editorState.selectedObject = selectedObject; 50 + }, 0); 51 + saveRoomToLocalStorage(); 52 + } else if (editorState.placingObject) { 53 + editorState.placingObject.rotation = (editorState.placingObject.rotation ?? 0) - rotation; 54 + saveRoomToLocalStorage(); 55 + } 56 + } 57 + 58 + export function rotateRight() { 59 + rotateObject(Math.PI / 8); 60 + } 61 + 62 + export function rotateLeft() { 63 + rotateObject(-Math.PI / 8); 64 + } 65 + 66 + export function deleteSelectedObject() { 67 + roomState.objects = roomState.objects.filter((object) => object !== editorState.selectedObject); 68 + 69 + editorState.selectedObject = null; 70 + 71 + saveRoomToLocalStorage(); 72 + } 73 + 74 + export function placeObject(point: { x: number; y: number; z: number }) { 75 + if (editorState.placingObject) { 76 + roomState.objects.push(editorState.placingObject); 77 + editorState.placingObject.position = [point.x, point.y, point.z]; 78 + editorState.selectedObject = editorState.placingObject; 79 + 80 + editorState.placingObject = null; 81 + 82 + saveRoomToLocalStorage(); 83 + } 84 + } 85 + 86 + export function makeSelectedObjectPlacingObject() { 87 + if (editorState.selectedObject && !editorState.placingObject) { 88 + editorState.placingObject = editorState.selectedObject; 89 + roomState.objects = roomState.objects.filter((object) => object !== editorState.selectedObject); 90 + } 91 + } 92 + 93 + export function getPossiblePlacementForPlacingObject() { 94 + if (!editorState.placingObject) return undefined; 95 + 96 + const object = editorState.placingObject; 97 + 98 + return AllObjects[object.kind]?.placement ?? 'floor'; 99 + } 100 + 101 + export function saveRoomToLocalStorage() { 102 + trackEvent('room_save_local', { count: roomState.objects.length }); 103 + 104 + localStorage.setItem('roomState', JSON.stringify(roomState)); 105 + } 106 + 107 + export function tryLoadingRoomFromLocalStorage(otherWiseReset: boolean = false) { 108 + const room = localStorage.getItem('roomState'); 109 + if (room) { 110 + trackEvent('room_load_local', { count: roomState.objects.length }); 111 + 112 + const roomObject = JSON.parse(room); 113 + roomState.floorColor = roomObject.floorColor; 114 + roomState.wallColor = roomObject.wallColor; 115 + roomState.objects = roomObject.objects; 116 + roomState.size = roomObject.size; 117 + roomState.id = roomObject.id ?? Math.random(); 118 + roomState.profile = roomObject.profile; 119 + } else if (otherWiseReset) { 120 + roomState.floorColor = '#a1a1a1'; 121 + roomState.wallColor = '#f1f1f1'; 122 + roomState.objects = []; 123 + roomState.size = { x: 2, z: 3 }; 124 + roomState.id = Math.random(); 125 + roomState.version = currentVersion; 126 + } 127 + }
+7 -19
src/lib/oauth/auth.svelte.ts
··· 12 12 import { metadata } from './const'; 13 13 import { AtpBaseClient } from '@atproto/api'; 14 14 import type { DidDocument } from '@atcute/client/utils/did'; 15 - import { 16 - applyTransformOfSelected, 17 - editorState, 18 - roomState, 19 - type BlueskyBlob 20 - } from '$lib/room/state.svelte'; 15 + import { type BlueskyBlob, type RoomStateType } from '$lib/room/state.svelte'; 21 16 import { modals } from '$lib/room/ui-state.svelte'; 22 17 import { getImage } from '$lib/room/images.svelte'; 23 18 import { trackEvent } from '$lib/analytics'; ··· 219 214 }); 220 215 }; 221 216 222 - export async function getRoom({ did }: { did: string }) { 217 + export async function getRoom({ did }: { did: string }): Promise<RoomStateType> { 223 218 const pds = await getPDS(did); 224 219 const rpc = new XRPC({ handler: new CredentialManager({ service: pds }) }); 225 220 ··· 233 228 234 229 // get profile 235 230 const profile = await getProfile({ did }); 231 + 236 232 // @ts-expect-error data.value is not typed 237 - data.value.roomState.did = did; 233 + const room = data.value.roomState as RoomStateType; 234 + room.profile = profile; 238 235 239 - // @ts-expect-error data.value is not typed 240 - return { room: data.value.roomState, profile: profile }; 236 + return room; 241 237 } 242 238 243 239 export async function getProfile({ did }: { did: string }) { ··· 261 257 export async function uploadImage(image: Blob) { 262 258 if (!client.profile) throw new Error('No profile'); 263 259 264 - console.log('uploading image', image); 265 - 266 260 // atcute version 267 261 const blobResponse = await client.rpc?.request({ 268 262 type: 'post', ··· 287 281 return `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${link}@jpeg`; 288 282 } 289 283 290 - export async function saveRoomToBluesky() { 284 + export async function saveRoomToBluesky(roomState: RoomStateType) { 291 285 if (!client.rpc || !client.profile) { 292 286 throw new Error('Failed to save room, please log in first'); 293 287 } 294 - 295 - roomState.did = client.profile.did; 296 - 297 - applyTransformOfSelected(); 298 - 299 - editorState.selectedObject = null; 300 288 301 289 try { 302 290 trackEvent('room_save_try');
+208
src/lib/room/EditingRoom.svelte
··· 1 + <script lang="ts"> 2 + import * as THREE from 'three'; 3 + import { T } from '@threlte/core'; 4 + import { roomState } from './state.svelte'; 5 + import RoomObject from './RoomObject.svelte'; 6 + import { Grid, RoundedBoxGeometry, TransformControls } from '@threlte/extras'; 7 + import { 8 + getPossiblePlacementForPlacingObject, 9 + editorState, 10 + placeObject, 11 + applyTransformOfSelected 12 + } from '$lib/editor/editorState.svelte'; 13 + 14 + type PointerEvent = { 15 + stopPropagation: () => void; 16 + point: { x: number; y: number; z: number }; 17 + normal: { x: number; y: number; z: number }; 18 + }; 19 + </script> 20 + 21 + <T.Group position.y={-1}> 22 + <T.Mesh 23 + onpointermove={(e: PointerEvent) => { 24 + if (getPossiblePlacementForPlacingObject() !== 'wall') return; 25 + 26 + e.stopPropagation(); 27 + 28 + if (editorState.placingObject) { 29 + editorState.placingObject.placement = 'wallZ'; 30 + editorState.placingObject.position = [e.point.x, e.point.y + 1, e.point.z]; 31 + } 32 + }} 33 + onclick={(e: PointerEvent) => { 34 + if (getPossiblePlacementForPlacingObject() !== 'wall') return; 35 + 36 + e.stopPropagation(); 37 + 38 + if (editorState.placingObject) { 39 + editorState.placingObject.placement = 'wallZ'; 40 + placeObject({ x: e.point.x, y: e.point.y + 1, z: e.point.z }); 41 + } 42 + }} 43 + position={[-0.05, 1 - 0.099, -roomState.size.x / 2 - 0.1]} 44 + receiveShadow 45 + castShadow 46 + ondblclick={() => { 47 + if (editorState.selectedObject) { 48 + applyTransformOfSelected(); 49 + 50 + editorState.selectedObject = null; 51 + } 52 + }} 53 + > 54 + <RoundedBoxGeometry args={[roomState.size.z + 0.18, 2, 0.1]} radius={0.03} /> 55 + <T.MeshStandardMaterial color={roomState.wallColor} /> 56 + </T.Mesh> 57 + 58 + <T.Mesh 59 + onpointermove={(e: PointerEvent) => { 60 + if (getPossiblePlacementForPlacingObject() !== 'wall') return; 61 + 62 + e.stopPropagation(); 63 + 64 + if (editorState.placingObject) { 65 + editorState.placingObject.placement = 'wallX'; 66 + editorState.placingObject.position = [e.point.x, e.point.y + 1, e.point.z]; 67 + } 68 + }} 69 + onclick={(e: PointerEvent) => { 70 + if (getPossiblePlacementForPlacingObject() !== 'wall') return; 71 + 72 + e.stopPropagation(); 73 + 74 + if (editorState.placingObject) { 75 + editorState.placingObject.placement = 'wallX'; 76 + placeObject({ x: e.point.x, y: e.point.y + 1, z: e.point.z }); 77 + } 78 + }} 79 + position={[-roomState.size.z / 2 - 0.1, 1 - 0.099, -0.05]} 80 + receiveShadow 81 + castShadow 82 + ondblclick={() => { 83 + if (editorState.selectedObject) { 84 + applyTransformOfSelected(); 85 + 86 + editorState.selectedObject = null; 87 + } 88 + }} 89 + > 90 + <RoundedBoxGeometry args={[0.1, 2, roomState.size.x + 0.18]} radius={0.03} /> 91 + <T.MeshStandardMaterial color={roomState.wallColor} /> 92 + </T.Mesh> 93 + 94 + <Grid 95 + gridSize={[roomState.size.z * 2, roomState.size.x * 2]} 96 + scale={0.5} 97 + sectionThickness={0} 98 + name="grid" 99 + /> 100 + 101 + <T.Mesh 102 + position={[-0.05, -0.0502, -0.05]} 103 + onpointermove={(e: PointerEvent) => { 104 + if (getPossiblePlacementForPlacingObject() !== 'floor') return; 105 + 106 + e.stopPropagation(); 107 + 108 + if (editorState.placingObject) { 109 + editorState.placingObject.position = [e.point.x, e.point.y + 1, e.point.z]; 110 + } 111 + }} 112 + onclick={(e: PointerEvent) => { 113 + if (getPossiblePlacementForPlacingObject() !== 'floor') return; 114 + 115 + e.stopPropagation(); 116 + 117 + placeObject({ x: e.point.x, y: e.point.y + 1, z: e.point.z }); 118 + }} 119 + ondblclick={() => { 120 + if (editorState.selectedObject) { 121 + applyTransformOfSelected(); 122 + 123 + editorState.selectedObject = null; 124 + } 125 + }} 126 + receiveShadow 127 + > 128 + <RoundedBoxGeometry 129 + args={[roomState.size.z + 0.15, 0.1, roomState.size.x + 0.15]} 130 + radius={0.03} 131 + /> 132 + <!-- <T.BoxGeometry args={[roomState.size.z, 0.1, roomState.size.x]} /> --> 133 + <T.MeshStandardMaterial color={roomState.floorColor} /> 134 + </T.Mesh> 135 + 136 + {#each roomState.objects as object, index (object.kind + index.toString())} 137 + {#if editorState.selectedObject === object} 138 + <TransformControls 139 + bind:controls={editorState.transformControls} 140 + position={object.position} 141 + showX={object.placement !== 'wallX'} 142 + showZ={object.placement !== 'wallZ'} 143 + rotation={[ 144 + object.placement === 'wallX' ? object.rotation : 0, 145 + object.placement === 'floor' ? object.rotation : 0, 146 + object.placement === 'wallZ' ? object.rotation : 0 147 + ]} 148 + enableRotate={false} 149 + onchange={(e) => { 150 + if (!e.target || !e.target.object) return; 151 + 152 + e.target.object.position.clamp( 153 + new THREE.Vector3(-roomState.size.z / 2 - 0.1, 0, -roomState.size.x / 2 - 0.1), 154 + new THREE.Vector3(roomState.size.z / 2 + 0.1, 2, roomState.size.x / 2 + 0.1) 155 + ); 156 + }} 157 + > 158 + <RoomObject 159 + kind={object.kind} 160 + colors={object.colors} 161 + placement={object.placement} 162 + image={object.image} 163 + isEditing 164 + /> 165 + </TransformControls> 166 + {:else} 167 + <T.Group 168 + onclick={(evt: PointerEvent) => { 169 + if (editorState.placingObject) { 170 + if (getPossiblePlacementForPlacingObject() !== 'floor') return; 171 + 172 + evt.stopPropagation(); 173 + placeObject({ x: evt.point.x, y: evt.point.y + 1, z: evt.point.z }); 174 + return; 175 + } 176 + evt.stopPropagation(); 177 + 178 + applyTransformOfSelected(); 179 + 180 + editorState.selectedObject = null; 181 + 182 + setTimeout(() => { 183 + editorState.selectedObject = object; 184 + }, 0); 185 + }} 186 + onpointermove={(e: PointerEvent) => { 187 + if (editorState.placingObject) { 188 + if (getPossiblePlacementForPlacingObject() !== 'floor') return; 189 + 190 + editorState.placingObject.position = [e.point.x, e.point.y + 1, e.point.z]; 191 + e.stopPropagation(); 192 + } 193 + }} 194 + > 195 + <RoomObject {...object} isEditing /> 196 + </T.Group> 197 + {/if} 198 + {/each} 199 + 200 + {#if editorState.placingObject} 201 + <RoomObject 202 + isEditing 203 + {...editorState.placingObject} 204 + colors={['#f1f1f1', '#f1f1f1', '#f1f1f1', '#f1f1f1', '#f1f1f1']} 205 + opacity={0.8} 206 + /> 207 + {/if} 208 + </T.Group>
+12 -7
src/lib/room/Image.svelte src/lib/room/models/Image.svelte
··· 2 2 import type { Props } from '@threlte/core'; 3 3 import { HTML } from '@threlte/extras'; 4 4 import type * as THREE from 'three'; 5 - import { getImage } from './images.svelte'; 5 + import { getImage } from '../images.svelte'; 6 6 import { getImageBlobUrl } from '$lib/oauth/auth.svelte'; 7 - import { roomState } from './state.svelte'; 8 7 9 8 let { 10 9 ref = $bindable(), 11 10 size = [270, 150], 12 11 image, 12 + did, 13 13 ...props 14 14 }: Props<THREE.Group> & { 15 15 ref?: THREE.Group; 16 16 size?: [number, number]; 17 17 image?: string; 18 + did?: string; 18 19 } = $props(); 19 20 20 21 let myImage = $derived.by(() => { ··· 23 24 if (typeof image === 'string' && image.startsWith('local:')) { 24 25 return getImage(image.replace('local:', '')); 25 26 // @ts-ignore 26 - } else if (typeof image === 'object' && image.$type === 'blob' && roomState.did) { 27 + } else if (typeof image === 'object' && image.$type === 'blob') { 27 28 // @ts-ignore 28 - return getImageBlobUrl({ did: roomState.did, link: image.ref.$link }); 29 + return getImageBlobUrl({ did: image.did, link: image.ref.$link }); 29 30 } 30 31 return null; 31 32 }); 33 + 34 + $inspect(myImage); 32 35 </script> 33 36 34 37 {#if image} 35 38 <HTML transform bind:ref occlude="blending" scale={0.1} pointerEvents="none" {...props}> 36 39 <div style="width: {size?.[0] ?? 270}px; height: {size?.[1] ?? 150}px;" class="bg-black"> 37 40 {#await myImage then myImage} 38 - {#if myImage} 39 - <img src={myImage} alt="" class="h-full w-full touch-none object-cover select-none" /> 40 - {/if} 41 + <img 42 + src={myImage} 43 + alt="" 44 + class={['h-full w-full touch-none object-cover select-none', !myImage ? 'hidden' : '']} 45 + /> 41 46 {/await} 42 47 </div> 43 48 </HTML>
+1 -1
src/lib/room/Outline.svelte
··· 8 8 OutlineEffect, 9 9 RenderPass 10 10 } from 'postprocessing'; 11 - import { editorState } from './state.svelte'; 11 + import { editorState } from '$lib/editor/editorState.svelte'; 12 12 13 13 const { scene, renderer, camera, size, autoRender, renderStage } = useThrelte(); 14 14
+11 -186
src/lib/room/Room.svelte
··· 1 1 <script lang="ts"> 2 - import * as THREE from 'three'; 3 2 import { T } from '@threlte/core'; 4 - import { 5 - applyTransformOfSelected, 6 - editorState, 7 - getPossiblePlacementForPlacingObject, 8 - placeObject, 9 - roomState, 10 - type RoomObjectData 11 - } from './state.svelte'; 3 + import { type RoomStateType } from './state.svelte'; 12 4 import RoomObject from './RoomObject.svelte'; 13 - import { Grid, RoundedBoxGeometry, TransformControls } from '@threlte/extras'; 5 + import { RoundedBoxGeometry } from '@threlte/extras'; 14 6 15 - type PointerEvent = { 16 - stopPropagation: () => void; 17 - point: { x: number; y: number; z: number }; 18 - normal: { x: number; y: number; z: number }; 19 - }; 7 + let { 8 + roomState = $bindable() 9 + }: { 10 + roomState: RoomStateType; 11 + } = $props(); 20 12 </script> 21 13 22 14 <T.Group position.y={-1}> 23 - <T.Mesh 24 - onpointermove={(e: PointerEvent) => { 25 - if (getPossiblePlacementForPlacingObject() !== 'wall') return; 26 - 27 - e.stopPropagation(); 28 - 29 - if (editorState.placingObject) { 30 - editorState.placingObject.placement = 'wallZ'; 31 - editorState.placingObject.position = [e.point.x, e.point.y + 1, e.point.z]; 32 - } 33 - }} 34 - onclick={(e: PointerEvent) => { 35 - if (getPossiblePlacementForPlacingObject() !== 'wall') return; 36 - 37 - e.stopPropagation(); 38 - 39 - if (editorState.placingObject) { 40 - editorState.placingObject.placement = 'wallZ'; 41 - placeObject({ x: e.point.x, y: e.point.y + 1, z: e.point.z }); 42 - } 43 - }} 44 - position={[-0.05, 1 - 0.099, -roomState.size.x / 2 - 0.1]} 45 - receiveShadow 46 - castShadow 47 - ondblclick={() => { 48 - if (editorState.selectedObject) { 49 - applyTransformOfSelected(); 50 - 51 - editorState.selectedObject = null; 52 - } 53 - }} 54 - > 15 + <T.Mesh position={[-0.05, 1 - 0.099, -roomState.size.x / 2 - 0.1]} receiveShadow castShadow> 55 16 <RoundedBoxGeometry args={[roomState.size.z + 0.18, 2, 0.1]} radius={0.03} /> 56 17 <T.MeshStandardMaterial color={roomState.wallColor} /> 57 18 </T.Mesh> 58 19 59 - <T.Mesh 60 - onpointermove={(e: PointerEvent) => { 61 - if (getPossiblePlacementForPlacingObject() !== 'wall') return; 62 - 63 - e.stopPropagation(); 64 - 65 - if (editorState.placingObject) { 66 - editorState.placingObject.placement = 'wallX'; 67 - editorState.placingObject.position = [e.point.x, e.point.y + 1, e.point.z]; 68 - } 69 - }} 70 - onclick={(e: PointerEvent) => { 71 - if (getPossiblePlacementForPlacingObject() !== 'wall') return; 72 - 73 - e.stopPropagation(); 74 - 75 - if (editorState.placingObject) { 76 - editorState.placingObject.placement = 'wallX'; 77 - placeObject({ x: e.point.x, y: e.point.y + 1, z: e.point.z }); 78 - } 79 - }} 80 - position={[-roomState.size.z / 2 - 0.1, 1 - 0.099, -0.05]} 81 - receiveShadow 82 - castShadow 83 - ondblclick={() => { 84 - if (editorState.selectedObject) { 85 - applyTransformOfSelected(); 86 - 87 - editorState.selectedObject = null; 88 - } 89 - }} 90 - > 20 + <T.Mesh position={[-roomState.size.z / 2 - 0.1, 1 - 0.099, -0.05]} receiveShadow castShadow> 91 21 <RoundedBoxGeometry args={[0.1, 2, roomState.size.x + 0.18]} radius={0.03} /> 92 22 <T.MeshStandardMaterial color={roomState.wallColor} /> 93 23 </T.Mesh> 94 24 95 - {#if editorState.isEditing} 96 - <Grid 97 - gridSize={[roomState.size.z * 2, roomState.size.x * 2]} 98 - scale={0.5} 99 - sectionThickness={0} 100 - name="grid" 101 - /> 102 - {/if} 103 - 104 - <T.Mesh 105 - position={[-0.05, -0.0502, -0.05]} 106 - onpointermove={(e: PointerEvent) => { 107 - if (getPossiblePlacementForPlacingObject() !== 'floor') return; 108 - 109 - e.stopPropagation(); 110 - 111 - if (editorState.placingObject) { 112 - editorState.placingObject.position = [e.point.x, e.point.y + 1, e.point.z]; 113 - } 114 - }} 115 - onclick={(e: PointerEvent) => { 116 - if (getPossiblePlacementForPlacingObject() !== 'floor') return; 117 - 118 - e.stopPropagation(); 119 - 120 - placeObject({ x: e.point.x, y: e.point.y + 1, z: e.point.z }); 121 - }} 122 - ondblclick={() => { 123 - if (editorState.selectedObject) { 124 - applyTransformOfSelected(); 125 - 126 - editorState.selectedObject = null; 127 - } 128 - }} 129 - receiveShadow 130 - > 25 + <T.Mesh position={[-0.05, -0.0502, -0.05]} receiveShadow> 131 26 <RoundedBoxGeometry 132 27 args={[roomState.size.z + 0.15, 0.1, roomState.size.x + 0.15]} 133 28 radius={0.03} 134 29 /> 135 - <!-- <T.BoxGeometry args={[roomState.size.z, 0.1, roomState.size.x]} /> --> 136 30 <T.MeshStandardMaterial color={roomState.floorColor} /> 137 31 </T.Mesh> 138 32 139 33 {#each roomState.objects as object, index (object.kind + index.toString())} 140 - {#if editorState.selectedObject === object} 141 - <TransformControls 142 - bind:controls={editorState.transformControls} 143 - position={object.position} 144 - showX={object.placement !== 'wallX'} 145 - showZ={object.placement !== 'wallZ'} 146 - rotation={[ 147 - object.placement === 'wallX' ? object.rotation : 0, 148 - object.placement === 'floor' ? object.rotation : 0, 149 - object.placement === 'wallZ' ? object.rotation : 0 150 - ]} 151 - enableRotate={false} 152 - onchange={(e) => { 153 - if (!e.target || !e.target.object) return; 154 - 155 - e.target.object.position.clamp( 156 - new THREE.Vector3(-roomState.size.z / 2 - 0.1, 0, -roomState.size.x / 2 - 0.1), 157 - new THREE.Vector3(roomState.size.z / 2 + 0.1, 2, roomState.size.x / 2 + 0.1) 158 - ); 159 - }} 160 - > 161 - <RoomObject 162 - kind={object.kind} 163 - colors={object.colors} 164 - placement={object.placement} 165 - image={object.image} 166 - /> 167 - </TransformControls> 168 - {:else if editorState.isEditing} 169 - <T.Group 170 - onclick={(evt: PointerEvent) => { 171 - if (editorState.placingObject) { 172 - if (getPossiblePlacementForPlacingObject() !== 'floor') return; 173 - 174 - evt.stopPropagation(); 175 - placeObject({ x: evt.point.x, y: evt.point.y + 1, z: evt.point.z }); 176 - return; 177 - } 178 - evt.stopPropagation(); 179 - 180 - applyTransformOfSelected(); 181 - 182 - editorState.selectedObject = null; 183 - 184 - setTimeout(() => { 185 - editorState.selectedObject = object; 186 - }, 0); 187 - }} 188 - onpointermove={(e: PointerEvent) => { 189 - if (editorState.placingObject) { 190 - if (getPossiblePlacementForPlacingObject() !== 'floor') return; 191 - 192 - editorState.placingObject.position = [e.point.x, e.point.y + 1, e.point.z]; 193 - e.stopPropagation(); 194 - } 195 - }} 196 - > 197 - <RoomObject {...object} /> 198 - </T.Group> 199 - {:else} 200 - <RoomObject {...object} /> 201 - {/if} 34 + <RoomObject {...object} /> 202 35 {/each} 203 - 204 - {#if editorState.placingObject} 205 - <RoomObject 206 - {...editorState.placingObject} 207 - colors={['#f1f1f1', '#f1f1f1', '#f1f1f1', '#f1f1f1', '#f1f1f1']} 208 - opacity={0.8} 209 - /> 210 - {/if} 211 36 </T.Group>
+3
src/lib/room/RoomObject.svelte
··· 11 11 scale, 12 12 rotation, 13 13 placement, 14 + isEditing, 14 15 ...restProps 15 16 }: { 16 17 kind: RoomObjectKind; ··· 18 19 colors?: string[]; 19 20 opacity?: number; 20 21 rotation?: number; 22 + isEditing?: boolean; 21 23 placement?: 'floor' | 'wallX' | 'wallZ'; 22 24 } & Props<THREE.Group> = $props(); 23 25 ··· 31 33 {...restProps} 32 34 {colors} 33 35 {opacity} 36 + {isEditing} 34 37 scale={scale ?? AllObjects[kind]?.scale ?? 1} 35 38 rotation={[ 36 39 placement === 'wallX' ? (rotation ?? 0) : 0,
+19 -14
src/lib/room/Scene.svelte
··· 1 1 <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import { base } from '$app/paths'; 2 4 import { T, useThrelte } from '@threlte/core'; 3 5 import { Environment, OrbitControls } from '@threlte/extras'; 4 6 import { interactivity } from '@threlte/extras'; 5 - interactivity(); 7 + import { ACESFilmicToneMapping } from 'three'; 8 + import { STLExporter } from 'three/examples/jsm/Addons.js'; 6 9 7 10 import Room from './Room.svelte'; 8 11 import Outline from './Outline.svelte'; 9 - import { applyTransformOfSelected, editorState, roomState } from './state.svelte'; 10 - import { onMount } from 'svelte'; 11 - import { ACESFilmicToneMapping } from 'three'; 12 - import { base } from '$app/paths'; 13 - import { STLExporter } from 'three/examples/jsm/Addons.js'; 12 + import EditingRoom from './EditingRoom.svelte'; 13 + import { roomState } from './state.svelte'; 14 + import { applyTransformOfSelected, editorState } from '$lib/editor/editorState.svelte'; 15 + interactivity(); 14 16 15 17 const { renderer, scene } = useThrelte(); 16 18 ··· 39 41 }); 40 42 41 43 // Listen for export event 42 - document.addEventListener("exportSTL", exportSTL); 44 + document.addEventListener('exportSTL', exportSTL); 43 45 }); 44 - 46 + 45 47 async function exportSTL() { 46 48 const exporter = new STLExporter(); 47 49 ··· 63 65 64 66 // Remove the grid from the scene 65 67 const grid = sceneCopy.getObjectByName('grid'); 66 - if(grid) { 68 + if (grid) { 67 69 grid.removeFromParent(); 68 70 } 69 71 70 72 // Export the modified copy 71 73 const stlString = exporter.parse(sceneCopy); 72 - const blob = new Blob([stlString], { type: "application/sla" }); 73 - const link = document.createElement("a"); 74 + const blob = new Blob([stlString], { type: 'application/sla' }); 75 + const link = document.createElement('a'); 74 76 link.href = URL.createObjectURL(blob); 75 - link.download = "room.stl"; 77 + link.download = 'room.stl'; 76 78 link.click(); 77 79 } 78 - 79 80 </script> 80 81 81 82 <T.PerspectiveCamera ··· 94 95 </T.PerspectiveCamera> 95 96 96 97 {#key roomState.id} 97 - <Room /> 98 + {#if editorState.isEditing} 99 + <EditingRoom /> 100 + {:else} 101 + <Room {roomState} /> 102 + {/if} 98 103 {/key} 99 104 100 105 <T.DirectionalLight position={[0, 10, 10]} intensity={lightIntensity} castShadow />
src/lib/room/Youtube.svelte src/lib/room/models/Youtube.svelte
+42
src/lib/room/bluesky.ts
··· 1 + import { trackEvent } from '$lib/analytics'; 2 + import { getRoom, resolveHandle } from '$lib/oauth/auth.svelte'; 3 + import { AllObjects } from './models'; 4 + import { roomState, type RoomObjectData } from './state.svelte'; 5 + 6 + export async function loadRoomFromBluesky(handle: string) { 7 + const did = await resolveHandle({ handle }); 8 + const room = await getRoom({ did }); 9 + 10 + const availableObjects = new Set(Object.keys(AllObjects)); 11 + 12 + const filteredObjects = room.objects.filter((object: RoomObjectData) => 13 + availableObjects.has(object.kind as string) 14 + ); 15 + if (filteredObjects.length !== room.objects.length) { 16 + console.warn('some objects were not found and removed!'); 17 + } 18 + 19 + for (const object of filteredObjects) { 20 + if (object.image && typeof object.image === 'object') { 21 + object.image.did = did; 22 + } 23 + } 24 + console.log('filteredObjects', filteredObjects); 25 + 26 + roomState.objects = filteredObjects; 27 + roomState.wallColor = room.wallColor; 28 + roomState.floorColor = room.floorColor; 29 + roomState.size = room.size; 30 + roomState.id = room.id; 31 + roomState.version = room.version; 32 + roomState.profile = room.profile; 33 + 34 + trackEvent('room_view', { 35 + view_handle: handle 36 + }); 37 + 38 + return { 39 + ...room, 40 + objects: filteredObjects 41 + }; 42 + }
+4 -7
src/lib/room/models/kaykit-furniture-kit/monitor.svelte
··· 13 13 import { type ExtraRoomObjectProps } from '../types'; 14 14 import RoomObjectMaterial from '../RoomObjectMaterial.svelte'; 15 15 import { base } from '$app/paths'; 16 - import Youtube from '$lib/room/Youtube.svelte'; 17 - import { editorState } from '$lib/room/state.svelte'; 16 + import Youtube from '$lib/room/models/Youtube.svelte'; 18 17 import { Spring } from 'svelte/motion'; 19 18 20 19 let { ··· 26 25 opacity, 27 26 link, 28 27 image, 28 + isEditing, 29 29 ...props 30 30 }: Props<THREE.Group> & { 31 31 ref?: THREE.Group; ··· 65 65 {:then gltf} 66 66 <T.Group 67 67 onpointerenter={() => { 68 - if (editorState.isEditing) return; 69 68 scale.target = 1.1; 70 69 onPointerEnter(); 71 70 }} 72 71 onpointerleave={() => { 73 - if (editorState.isEditing) return; 74 72 scale.target = 1.0; 75 73 onPointerLeave(); 76 74 }} ··· 82 80 <T.Mesh castShadow receiveShadow geometry={gltf.nodes.Cube18237_1.geometry} 83 81 ><RoomObjectMaterial colors={['#000000']} index={0} {opacity} /></T.Mesh 84 82 > 85 - {#if !editorState.isEditing && link} 83 + {#if link} 86 84 <Youtube 87 85 onclick={(evt: { stopPropagation: () => void }) => { 88 - if (editorState.isEditing) return; 86 + if (isEditing) return; 89 87 evt.stopPropagation(); 90 - 91 88 playPause?.(); 92 89 }} 93 90 position={[0, 0.66, 0.15]}
+1 -1
src/lib/room/models/kaykit-furniture-kit/pictureframe_large_a.svelte
··· 13 13 import { type ExtraRoomObjectProps } from '../types'; 14 14 import RoomObjectMaterial from '../RoomObjectMaterial.svelte'; 15 15 import { base } from '$app/paths'; 16 - import Image from '$lib/room/Image.svelte'; 16 + import Image from '$lib/room/models/Image.svelte'; 17 17 18 18 let { 19 19 fallback,
+1 -1
src/lib/room/models/kaykit-furniture-kit/pictureframe_large_b.svelte
··· 13 13 import { type ExtraRoomObjectProps } from '../types'; 14 14 import RoomObjectMaterial from '../RoomObjectMaterial.svelte'; 15 15 import { base } from '$app/paths'; 16 - import Image from '$lib/room/Image.svelte'; 16 + import Image from '$lib/room/models/Image.svelte'; 17 17 18 18 let { 19 19 fallback,
+2 -1
src/lib/room/models/kaykit-furniture-kit/pictureframe_medium.svelte
··· 13 13 import { type ExtraRoomObjectProps } from '../types'; 14 14 import RoomObjectMaterial from '../RoomObjectMaterial.svelte'; 15 15 import { base } from '$app/paths'; 16 - import Image from '$lib/room/Image.svelte'; 16 + import Image from '$lib/room/models/Image.svelte'; 17 17 18 18 let { 19 19 fallback, 20 20 error, 21 21 children, 22 + did, 22 23 ref = $bindable(), 23 24 colors, 24 25 opacity,
+1 -1
src/lib/room/models/kaykit-furniture-kit/pictureframe_small_a.svelte
··· 13 13 import { type ExtraRoomObjectProps } from '../types'; 14 14 import RoomObjectMaterial from '../RoomObjectMaterial.svelte'; 15 15 import { base } from '$app/paths'; 16 - import Image from '$lib/room/Image.svelte'; 16 + import Image from '$lib/room/models/Image.svelte'; 17 17 18 18 let { 19 19 fallback,
+1 -1
src/lib/room/models/kaykit-furniture-kit/pictureframe_small_b.svelte
··· 13 13 import { type ExtraRoomObjectProps } from '../types'; 14 14 import RoomObjectMaterial from '../RoomObjectMaterial.svelte'; 15 15 import { base } from '$app/paths'; 16 - import Image from '$lib/room/Image.svelte'; 16 + import Image from '$lib/room/models/Image.svelte'; 17 17 18 18 let { 19 19 fallback,
+1 -1
src/lib/room/models/kaykit-furniture-kit/pictureframe_small_c.svelte
··· 13 13 import { type ExtraRoomObjectProps } from '../types'; 14 14 import RoomObjectMaterial from '../RoomObjectMaterial.svelte'; 15 15 import { base } from '$app/paths'; 16 - import Image from '$lib/room/Image.svelte'; 16 + import Image from '$lib/room/models/Image.svelte'; 17 17 18 18 let { 19 19 fallback,
+1 -1
src/lib/room/models/kaykit-furniture-kit/pictureframe_standing_a.svelte
··· 13 13 import { type ExtraRoomObjectProps } from '../types'; 14 14 import RoomObjectMaterial from '../RoomObjectMaterial.svelte'; 15 15 import { base } from '$app/paths'; 16 - import Image from '$lib/room/Image.svelte'; 16 + import Image from '$lib/room/models/Image.svelte'; 17 17 18 18 let { 19 19 fallback,
+1 -1
src/lib/room/models/kaykit-furniture-kit/pictureframe_standing_b.svelte
··· 13 13 import { type ExtraRoomObjectProps } from '../types'; 14 14 import RoomObjectMaterial from '../RoomObjectMaterial.svelte'; 15 15 import { base } from '$app/paths'; 16 - import Image from '$lib/room/Image.svelte'; 16 + import Image from '$lib/room/models/Image.svelte'; 17 17 18 18 let { 19 19 fallback,
-14
src/lib/room/models/kenney-furniture-kit/chairDesk.svelte
··· 10 10 import { T, type Props } from '@threlte/core'; 11 11 import { useCursor, useGltf } from '@threlte/extras'; 12 12 import { base } from '$app/paths'; 13 - import { editorState } from '$lib/room/state.svelte'; 14 13 import { Spring } from 'svelte/motion'; 15 14 16 15 const { onPointerEnter, onPointerLeave } = useCursor(); ··· 64 63 material.opacity={opacity ?? gltf.materials.metalMedium.opacity} 65 64 material.transparent={opacity !== undefined} 66 65 position={[-0.16, 0.0, 0.16]} 67 - onclick={() => { 68 - if (editorState.isEditing) return; 69 - 70 - rotation.target += 1 + Math.random() * 2; 71 - }} 72 - onpointerenter={() => { 73 - if (editorState.isEditing) return; 74 - onPointerEnter(); 75 - }} 76 - onpointerleave={() => { 77 - if (editorState.isEditing) return; 78 - onPointerLeave(); 79 - }} 80 66 > 81 67 <T.Group position={[0.17, 0.19, -0.16]} rotation={[0, rotation.current, 0]}> 82 68 <T.Mesh
-13
src/lib/room/models/kenney-furniture-kit/lampRoundFloor.svelte
··· 10 10 import { T, type Props } from '@threlte/core'; 11 11 import { useCursor, useGltf } from '@threlte/extras'; 12 12 import { base } from '$app/paths'; 13 - import { editorState } from '$lib/room/state.svelte'; 14 13 15 14 const { onPointerEnter, onPointerLeave } = useCursor(); 16 15 ··· 52 51 {@render fallback?.()} 53 52 {:then gltf} 54 53 <T.Group 55 - onclick={() => { 56 - if (editorState.isEditing) return; 57 - lampOn = !lampOn; 58 - }} 59 - onpointerenter={() => { 60 - if (editorState.isEditing) return; 61 - onPointerEnter(); 62 - }} 63 - onpointerleave={() => { 64 - if (editorState.isEditing) return; 65 - onPointerLeave(); 66 - }} 67 54 > 68 55 <T.Mesh 69 56 castShadow
+3 -17
src/lib/room/models/kenney-furniture-kit/televisionModern.svelte
··· 10 10 import { T, type Props } from '@threlte/core'; 11 11 import { useCursor, useGltf } from '@threlte/extras'; 12 12 import { base } from '$app/paths'; 13 - import Youtube from '$lib/room/Youtube.svelte'; 14 - import { editorState } from '$lib/room/state.svelte'; 13 + import Youtube from '$lib/room/models/Youtube.svelte'; 15 14 16 15 let { 17 16 fallback, ··· 54 53 {#await gltf} 55 54 {@render fallback?.()} 56 55 {:then gltf} 57 - <T.Group 58 - onclick={() => { 59 - if (editorState.isEditing) return; 60 - playPause?.(); 61 - }} 62 - onpointerenter={() => { 63 - if (editorState.isEditing) return; 64 - onPointerEnter(); 65 - }} 66 - onpointerleave={() => { 67 - if (editorState.isEditing) return; 68 - onPointerLeave(); 69 - }} 70 - > 56 + <T.Group> 71 57 <T.Mesh 72 58 castShadow 73 59 receiveShadow ··· 77 63 material.opacity={opacity ?? gltf.materials.metalDark.opacity} 78 64 material.transparent={opacity !== undefined} 79 65 /> 80 - {#if !videoId || editorState.isEditing} 66 + {#if !videoId} 81 67 <T.Mesh 82 68 castShadow 83 69 receiveShadow
+10 -140
src/lib/room/state.svelte.ts
··· 1 - import type { TransformControls } from '@threlte/extras'; 2 - import { AllObjects, type RoomObjectKind } from './models'; 3 - import { trackEvent } from '$lib/analytics'; 1 + import { type RoomObjectKind } from './models'; 2 + 3 + import type { ProfileViewDetailed } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; 4 4 5 5 export const currentVersion = 0; 6 6 7 - export const roomState: { 7 + export type RoomStateType = { 8 8 wallColor: string; 9 9 floorColor: string; 10 10 objects: RoomObjectData[]; ··· 15 15 16 16 version: number; 17 17 18 - did: string; 19 - } = $state({ 18 + profile?: ProfileViewDetailed; 19 + }; 20 + 21 + export const roomState: RoomStateType = $state({ 20 22 wallColor: '#f1f1f1', 21 23 floorColor: '#a1a1a1', 22 24 ··· 26 28 27 29 id: 0, 28 30 29 - version: currentVersion, 30 - 31 - did: '' 32 - }); 33 - 34 - export const editorState: { 35 - startIndex: number; 36 - shownCount: number; 37 - 38 - selectedObject: RoomObjectData | null; 39 - transformControls: TransformControls | undefined; 40 - placingObject: RoomObjectData | null; 41 - 42 - isEditing: boolean; 43 - } = $state({ 44 - startIndex: 0, 45 - shownCount: 3, 46 - 47 - selectedObject: null, 48 - transformControls: undefined, 49 - placingObject: null, 50 - 51 - isEditing: false 31 + version: currentVersion 52 32 }); 53 33 54 34 export type BlueskyBlob = { ··· 58 38 }; 59 39 mimeType: string; 60 40 size: number; 41 + did: string; 61 42 }; 62 43 63 44 export type RoomObjectData = { ··· 69 50 link?: string; 70 51 image?: string | BlueskyBlob; 71 52 }; 72 - 73 - export function applyTransformOfSelected() { 74 - if (editorState.selectedObject && editorState.transformControls?.object) { 75 - editorState.selectedObject.position = [ 76 - editorState.transformControls.object.position.x, 77 - editorState.transformControls.object.position.y, 78 - editorState.transformControls.object.position.z 79 - ]; 80 - 81 - if (editorState.selectedObject.placement === 'wallX') { 82 - editorState.selectedObject.rotation = editorState.transformControls.object.rotation.x; 83 - } else if (editorState.selectedObject.placement === 'wallZ') { 84 - editorState.selectedObject.rotation = editorState.transformControls.object.rotation.z; 85 - } else { 86 - editorState.selectedObject.rotation = editorState.transformControls.object.rotation.y; 87 - } 88 - 89 - saveRoomToLocalStorage(); 90 - } 91 - } 92 - 93 - export function saveRoomToLocalStorage() { 94 - trackEvent('room_save_local', { count: roomState.objects.length }); 95 - 96 - localStorage.setItem('roomState', JSON.stringify(roomState)); 97 - } 98 - 99 - export function rotateObject(rotation: number) { 100 - if (editorState.selectedObject) { 101 - applyTransformOfSelected(); 102 - 103 - const selectedObject = editorState.selectedObject; 104 - editorState.selectedObject = null; 105 - 106 - setTimeout(() => { 107 - // rotate right 108 - selectedObject.rotation -= rotation; 109 - editorState.selectedObject = selectedObject; 110 - }, 0); 111 - saveRoomToLocalStorage(); 112 - } else if (editorState.placingObject) { 113 - editorState.placingObject.rotation = (editorState.placingObject.rotation ?? 0) - rotation; 114 - saveRoomToLocalStorage(); 115 - } 116 - } 117 - 118 - export function rotateRight() { 119 - rotateObject(Math.PI / 8); 120 - } 121 - 122 - export function rotateLeft() { 123 - rotateObject(-Math.PI / 8); 124 - } 125 - 126 - export function tryLoadingRoomFromLocalStorage(otherWiseReset: boolean = false) { 127 - const room = localStorage.getItem('roomState'); 128 - if (room) { 129 - trackEvent('room_load_local', { count: roomState.objects.length }); 130 - 131 - const roomObject = JSON.parse(room); 132 - roomState.floorColor = roomObject.floorColor; 133 - roomState.wallColor = roomObject.wallColor; 134 - roomState.objects = roomObject.objects; 135 - roomState.size = roomObject.size; 136 - roomState.id = roomObject.id ?? Math.random(); 137 - roomState.did = roomObject.did; 138 - } else if (otherWiseReset) { 139 - roomState.floorColor = '#a1a1a1'; 140 - roomState.wallColor = '#f1f1f1'; 141 - roomState.objects = []; 142 - roomState.size = { x: 2, z: 3 }; 143 - roomState.id = Math.random(); 144 - roomState.version = currentVersion; 145 - roomState.did = ''; 146 - } 147 - } 148 - 149 - export function deleteSelectedObject() { 150 - roomState.objects = roomState.objects.filter((object) => object !== editorState.selectedObject); 151 - 152 - editorState.selectedObject = null; 153 - 154 - saveRoomToLocalStorage(); 155 - } 156 - 157 - export function placeObject(point: { x: number; y: number; z: number }) { 158 - if (editorState.placingObject) { 159 - roomState.objects.push(editorState.placingObject); 160 - editorState.placingObject.position = [point.x, point.y, point.z]; 161 - editorState.selectedObject = editorState.placingObject; 162 - 163 - editorState.placingObject = null; 164 - 165 - saveRoomToLocalStorage(); 166 - } 167 - } 168 - 169 - export function makeSelectedObjectPlacingObject() { 170 - if (editorState.selectedObject && !editorState.placingObject) { 171 - editorState.placingObject = editorState.selectedObject; 172 - roomState.objects = roomState.objects.filter((object) => object !== editorState.selectedObject); 173 - } 174 - } 175 - 176 - export function getPossiblePlacementForPlacingObject() { 177 - if (!editorState.placingObject) return undefined; 178 - 179 - const object = editorState.placingObject; 180 - 181 - return AllObjects[object.kind]?.placement ?? 'floor'; 182 - }
-46
src/lib/room/ui-state.svelte.ts
··· 1 - import { trackEvent } from '$lib/analytics'; 2 - import { AllObjects } from './models'; 3 - import { getRoom, resolveHandle } from '$lib/oauth/auth.svelte'; 4 - import { roomState, type RoomObjectData } from './state.svelte'; 5 - import type { ProfileViewDetailed } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; 6 - 7 1 export const modals = $state({ 8 2 selectCategoryModalOpen: false, 9 3 roomSettingsModalState: false, ··· 12 6 successModalState: false, 13 7 signInSuccessModalState: false 14 8 }); 15 - 16 - export const userInfo: { 17 - handle: string; 18 - did: string; 19 - profile: ProfileViewDetailed | null; 20 - } = $state({ 21 - handle: '', 22 - did: '', 23 - profile: null 24 - }); 25 - 26 - export async function loadRoomFromBluesky(handle: string) { 27 - const did = await resolveHandle({ handle }); 28 - const room = await getRoom({ did }); 29 - 30 - roomState.did = room.room.did || did; 31 - roomState.wallColor = room.room.wallColor; 32 - roomState.floorColor = room.room.floorColor; 33 - 34 - const availableObjects = new Set(Object.keys(AllObjects)); 35 - 36 - roomState.objects = room.room.objects.filter((object: RoomObjectData) => 37 - availableObjects.has(object.kind as string) 38 - ); 39 - if (roomState.objects.length !== room.room.objects.length) { 40 - console.warn('some objects were not found and removed!'); 41 - } 42 - 43 - roomState.size = room.room.size; 44 - roomState.id = room.room.id; 45 - roomState.version = room.room.version; 46 - 47 - userInfo.profile = room.profile; 48 - 49 - trackEvent('room_view', { 50 - view_handle: handle 51 - }); 52 - 53 - return room.profile; 54 - }
+61
src/lib/ui/SignInSuccessModal.svelte
··· 1 + <script> 2 + import { Button } from "$lib/components/base/button"; 3 + import { Heading } from "$lib/components/base/heading"; 4 + import Modal from "$lib/components/base/modal/Modal.svelte"; 5 + import { tryLoadingRoomFromLocalStorage, editorState } from "$lib/editor/editorState.svelte"; 6 + import { client } from "$lib/oauth"; 7 + import { modals } from "$lib/room/ui-state.svelte"; 8 + 9 + $effect(() => { 10 + if (client.justLoggedIn) { 11 + modals.signInSuccessModalState = true; 12 + client.justLoggedIn = false; 13 + } 14 + }); 15 + </script> 16 + 17 + <Modal bind:open={modals.signInSuccessModalState}> 18 + <Heading class="flex items-center gap-4"> 19 + <svg 20 + role="img" 21 + viewBox="0 0 24 24" 22 + xmlns="http://www.w3.org/2000/svg" 23 + aria-hidden="true" 24 + fill="currentColor" 25 + class="text-accent-600 dark:text-accent-400 size-8" 26 + ><path 27 + d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z" 28 + /></svg 29 + > 30 + Sign in successful</Heading 31 + > 32 + 33 + <div class="text-base-800 dark:text-base-200 flex flex-col gap-2 text-sm"> 34 + <p>If you haven't already, create a room to get started.</p> 35 + 36 + <p>You can then save your room to your profile and share it on bluesky.</p> 37 + 38 + <p>Feel free to tag me when sharing your room so I can admire your creation 😊</p> 39 + 40 + <p class="mt-4"> 41 + my handle: <a 42 + href="https://bsky.app/profile/flo-bit.dev" 43 + target="_blank" 44 + class="text-accent-700 dark:text-accent-400 hover:text-accent-600 dark:hover:text-accent-500 font-medium" 45 + >@flo-bit.dev</a 46 + > 47 + </p> 48 + 49 + <Button 50 + class="mt-4" 51 + onclick={() => { 52 + tryLoadingRoomFromLocalStorage(true); 53 + 54 + editorState.isEditing = true; 55 + modals.signInSuccessModalState = false; 56 + }} 57 + > 58 + Go to my room 59 + </Button> 60 + </div> 61 + </Modal>
+8 -108
src/routes/+page.svelte
··· 2 2 import { Canvas } from '@threlte/core'; 3 3 import Scene from '$lib/room/Scene.svelte'; 4 4 import { onMount } from 'svelte'; 5 - import { client } from '$lib/oauth/auth.svelte'; 6 - import { 7 - applyTransformOfSelected, 8 - deleteSelectedObject, 9 - editorState, 10 - makeSelectedObjectPlacingObject, 11 - rotateLeft, 12 - rotateRight, 13 - tryLoadingRoomFromLocalStorage 14 - } from '$lib/room/state.svelte'; 15 - import { Button } from '$lib/components/base/button'; 16 - import Modal from '$lib/components/base/modal/Modal.svelte'; 17 5 import Heading from '$lib/components/base/heading/Heading.svelte'; 18 6 19 - import EditorUi from './EditorUI.svelte'; 20 - import PreviewUi from './PreviewUI.svelte'; 21 - import { loadRoomFromBluesky, modals, userInfo } from '$lib/room/ui-state.svelte'; 7 + import EditorUi from '$lib/editor/EditorUI.svelte'; 8 + import PreviewUi from '$lib/editor/PreviewUI.svelte'; 9 + import SignInSuccessModal from '$lib/ui/SignInSuccessModal.svelte'; 10 + import { editorState, tryLoadingRoomFromLocalStorage } from '$lib/editor/editorState.svelte'; 11 + import { loadRoomFromBluesky } from '$lib/room/bluesky'; 12 + import { roomState } from '$lib/room/state.svelte'; 22 13 23 14 let loading = $state(true); 24 15 25 - let db = $state<IDBDatabase | null>(null); 26 - 27 16 onMount(async () => { 28 - window.addEventListener('keydown', (e) => { 29 - if (e.key === 'x') { 30 - deleteSelectedObject(); 31 - 32 - if (editorState.placingObject) { 33 - editorState.placingObject = null; 34 - } 35 - } 36 - if (e.key === 'd' || e.key === 'ArrowRight') { 37 - rotateRight(); 38 - } else if (e.key === 'a' || e.key === 'ArrowLeft') { 39 - rotateLeft(); 40 - } 41 - 42 - // escape 43 - if (e.key === 'Escape') { 44 - editorState.placingObject = null; 45 - 46 - applyTransformOfSelected(); 47 - editorState.selectedObject = null; 48 - } 49 - 50 - if (e.key === 'g') { 51 - makeSelectedObjectPlacingObject(); 52 - } 53 - 54 - if (e.key === 'c') { 55 - // duplicate selected object 56 - if (editorState.selectedObject) { 57 - editorState.placingObject = JSON.parse(JSON.stringify(editorState.selectedObject)); 58 - 59 - applyTransformOfSelected(); 60 - editorState.selectedObject = null; 61 - } 62 - } 63 - }); 64 - 65 17 // get handle from url search params 66 18 let handle = new URLSearchParams(window.location.search).get('handle'); 67 19 if (!handle) { 68 20 tryLoadingRoomFromLocalStorage(); 69 21 loading = false; 70 - userInfo.handle = ''; 71 22 editorState.isEditing = true; 72 23 return; 73 24 } 74 25 75 - userInfo.handle = handle; 76 - 77 26 await loadRoomFromBluesky(handle); 78 27 79 28 loading = false; 80 29 }); 81 30 82 - $effect(() => { 83 - if (client.justLoggedIn) { 84 - modals.signInSuccessModalState = true; 85 - client.justLoggedIn = false; 86 - } 87 - }); 31 + $inspect(roomState); 88 32 </script> 89 33 90 34 <div class="fixed inset-0 -z-20 h-[100dvh] w-screen"> ··· 105 49 </div> 106 50 {/if} 107 51 108 - <Modal bind:open={modals.signInSuccessModalState}> 109 - <Heading class="flex items-center gap-4"> 110 - <svg 111 - role="img" 112 - viewBox="0 0 24 24" 113 - xmlns="http://www.w3.org/2000/svg" 114 - aria-hidden="true" 115 - fill="currentColor" 116 - class="text-accent-600 dark:text-accent-400 size-8" 117 - ><path 118 - d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z" 119 - /></svg 120 - > 121 - Sign in successful</Heading 122 - > 123 - 124 - <div class="text-base-800 dark:text-base-200 flex flex-col gap-2 text-sm"> 125 - <p>If you haven't already, create a room to get started.</p> 126 - 127 - <p>You can then save your room to your profile and share it on bluesky.</p> 128 - 129 - <p>Feel free to tag me when sharing your room so I can admire your creation 😊</p> 130 - 131 - <p class="mt-4"> 132 - my handle: <a 133 - href="https://bsky.app/profile/flo-bit.dev" 134 - target="_blank" 135 - class="text-accent-700 dark:text-accent-400 hover:text-accent-600 dark:hover:text-accent-500 font-medium" 136 - >@flo-bit.dev</a 137 - > 138 - </p> 139 - 140 - <Button 141 - class="mt-4" 142 - onclick={() => { 143 - tryLoadingRoomFromLocalStorage(true); 144 - 145 - editorState.isEditing = true; 146 - modals.signInSuccessModalState = false; 147 - }} 148 - > 149 - Go to my room 150 - </Button> 151 - </div> 152 - </Modal> 52 + <SignInSuccessModal />
-1
src/routes/ColorPickerPopover.svelte src/lib/editor/ColorPickerPopover.svelte
··· 1 1 <script lang="ts"> 2 - import { Subheading } from '$lib/components/base/heading'; 3 2 import * as Popover from '$lib/components/base/popover'; 4 3 import ColorPicker, { type OKhsv } from '$lib/components/extra/color-picker'; 5 4 import {
+70 -15
src/routes/EditorUI.svelte src/lib/editor/EditorUI.svelte
··· 2 2 import { Button } from '$lib/components/base/button'; 3 3 import { AllObjects } from '$lib/room/models'; 4 4 import { client } from '$lib/oauth'; 5 - import { 6 - applyTransformOfSelected, 7 - deleteSelectedObject, 8 - editorState, 9 - makeSelectedObjectPlacingObject, 10 - roomState, 11 - rotateLeft, 12 - rotateRight, 13 - saveRoomToLocalStorage 14 - } from '$lib/room/state.svelte'; 5 + import { roomState } from '$lib/room/state.svelte'; 15 6 import ColorPickerPopover from './ColorPickerPopover.svelte'; 16 7 17 8 import * as Popover from '$lib/components/base/popover'; ··· 27 18 import { Heading, Subheading } from '$lib/components/base/heading'; 28 19 import NumberInput from '$lib/components/base/number-input/NumberInput.svelte'; 29 20 import { Input } from '$lib/components/base/input'; 30 - import { loadRoomFromBluesky, modals, userInfo } from '$lib/room/ui-state.svelte'; 21 + import { modals } from '$lib/room/ui-state.svelte'; 31 22 import { blueskyLoginModalState } from '$lib/components/base/modal/BlueskyLoginModal.svelte'; 32 23 import { toast } from 'svelte-sonner'; 33 - import Picker from './Picker.svelte'; 24 + import Picker from './ObjectPicker.svelte'; 34 25 import ImageSelector from './ImageSelector.svelte'; 35 26 import { saveRoomToBluesky } from '$lib/oauth/auth.svelte'; 27 + import { onMount } from 'svelte'; 28 + import { loadRoomFromBluesky } from '$lib/room/bluesky'; 29 + import { 30 + editorState, 31 + saveRoomToLocalStorage, 32 + deleteSelectedObject, 33 + rotateRight, 34 + rotateLeft, 35 + applyTransformOfSelected, 36 + makeSelectedObjectPlacingObject 37 + } from './editorState.svelte'; 36 38 37 39 let lastUsedColors = $derived.by(() => { 38 40 let colors = []; ··· 75 77 okhsv_wall = rgb_to_okhsv(hex_to_rgb(roomState.wallColor)); 76 78 okhsv_floor = rgb_to_okhsv(hex_to_rgb(roomState.floorColor)); 77 79 }); 80 + 81 + onMount(() => { 82 + window.addEventListener('keydown', handleKeyDown); 83 + 84 + return () => { 85 + window.removeEventListener('keydown', handleKeyDown); 86 + }; 87 + }); 88 + 89 + function handleKeyDown(e: KeyboardEvent) { 90 + if (e.key === 'x') { 91 + deleteSelectedObject(); 92 + 93 + if (editorState.placingObject) { 94 + editorState.placingObject = null; 95 + } 96 + } 97 + if (e.key === 'd' || e.key === 'ArrowRight') { 98 + rotateRight(); 99 + } else if (e.key === 'a' || e.key === 'ArrowLeft') { 100 + rotateLeft(); 101 + } 102 + 103 + // escape 104 + if (e.key === 'Escape') { 105 + editorState.placingObject = null; 106 + 107 + applyTransformOfSelected(); 108 + editorState.selectedObject = null; 109 + } 110 + 111 + if (e.key === 'g') { 112 + makeSelectedObjectPlacingObject(); 113 + } 114 + 115 + if (e.key === 'c') { 116 + // duplicate selected object 117 + if (editorState.selectedObject) { 118 + editorState.placingObject = JSON.parse(JSON.stringify(editorState.selectedObject)); 119 + 120 + applyTransformOfSelected(); 121 + editorState.selectedObject = null; 122 + } 123 + } 124 + } 125 + 126 + async function saveRoom() { 127 + applyTransformOfSelected(); 128 + 129 + editorState.selectedObject = null; 130 + 131 + return saveRoomToBluesky(roomState); 132 + } 78 133 </script> 79 134 80 135 <Button ··· 329 384 {:else} 330 385 <Button 331 386 onclick={() => { 332 - toast.promise(saveRoomToBluesky(), { 387 + toast.promise(saveRoom(), { 333 388 loading: 'Saving...', 334 389 success: 'Room saved to your profile', 335 390 error: 'Failed to save room, please try again' ··· 387 442 onclick={() => { 388 443 applyTransformOfSelected(); 389 444 editorState.selectedObject = null; 390 - userInfo.handle = client.profile?.handle ?? ''; 391 - userInfo.profile = client.profile; 445 + // @ts-ignore 446 + roomState.profile = client.profile; 392 447 editorState.isEditing = false; 393 448 modals.roomSettingsModalState = false; 394 449 }}
+1 -1
src/routes/ImageSelector.svelte src/lib/editor/ImageSelector.svelte
··· 3 3 import { onMount } from 'svelte'; 4 4 import * as Popover from '$lib/components/base/popover'; 5 5 import { cn } from '$lib/utils'; 6 - import { editorState } from '$lib/room/state.svelte'; 7 6 import { buttonVariants } from '$lib/components/base/button'; 7 + import { editorState } from './editorState.svelte'; 8 8 9 9 // Open the IndexedDB on component mount 10 10 onMount(async () => {
+2 -2
src/routes/Picker.svelte src/lib/editor/ObjectPicker.svelte
··· 1 1 <script lang="ts"> 2 2 import { Canvas } from '@threlte/core'; 3 3 import { View } from '@threlte/extras'; 4 - import Scene from './Scene.svelte'; 4 + import Scene from './ObjectPreviewScene.svelte'; 5 5 import { objectCategories, visibleObjects, type RoomObjectKind } from '$lib/room/models'; 6 6 import Button from '$lib/components/base/button/Button.svelte'; 7 7 import Modal from '$lib/components/base/modal/Modal.svelte'; 8 - import { applyTransformOfSelected, editorState } from '$lib/room/state.svelte'; 9 8 import Subheading from '$lib/components/base/heading/Subheading.svelte'; 9 + import { editorState, applyTransformOfSelected } from './editorState.svelte'; 10 10 11 11 let items: { kind: string; dom: HTMLElement | undefined }[] = $state([]); 12 12
+12 -10
src/routes/PreviewUI.svelte src/lib/editor/PreviewUI.svelte
··· 1 1 <script> 2 2 import { Avatar } from '$lib/components/base/avatar'; 3 - 4 3 import { Button } from '$lib/components/base/button'; 5 4 import { Heading } from '$lib/components/base/heading'; 6 5 import Modal from '$lib/components/base/modal/Modal.svelte'; 7 6 import { client } from '$lib/oauth'; 8 - import { editorState, tryLoadingRoomFromLocalStorage } from '$lib/room/state.svelte'; 9 - import { modals, userInfo } from '$lib/room/ui-state.svelte'; 7 + import { roomState } from '$lib/room/state.svelte'; 8 + import { modals } from '$lib/room/ui-state.svelte'; 9 + import { editorState, tryLoadingRoomFromLocalStorage } from './editorState.svelte'; 10 10 </script> 11 11 12 12 <div class="pointer-events-none fixed top-8 right-4 left-4 flex items-center justify-center gap-2"> 13 - {#if userInfo.handle} 14 - <Avatar src={userInfo.profile?.avatar} /> 13 + {#if roomState.profile?.handle} 14 + <Avatar src={roomState.profile?.avatar} /> 15 15 {/if} 16 16 <Heading> 17 - {#if userInfo.handle} 17 + {#if roomState.profile?.handle} 18 18 <a 19 - href={`https://bsky.app/profile/${userInfo.handle}`} 19 + href={`https://bsky.app/profile/${roomState.profile?.handle}`} 20 20 target="_blank" 21 21 class="hover:text-accent-600 dark:hover:text-accent-500 pointer-events-auto" 22 - >{userInfo.handle}'s</a 22 + >{roomState.profile.handle}'s</a 23 23 > 24 24 {:else} 25 25 my ··· 42 42 <Button 43 43 size="lg" 44 44 onclick={() => { 45 - if (client.profile?.handle == userInfo.handle) { 45 + if (client.profile?.handle == roomState.profile?.handle) { 46 46 editorState.isEditing = true; 47 47 return; 48 48 } ··· 50 50 tryLoadingRoomFromLocalStorage(true); 51 51 52 52 editorState.isEditing = true; 53 + 54 + history.replaceState(null, '', location.pathname); 53 55 }} 54 56 > 55 - {client.profile?.handle == userInfo.handle ? 'Edit room' : 'Create room'} 57 + {client.profile?.handle == roomState.profile?.handle ? 'Edit room' : 'Create room'} 56 58 </Button> 57 59 </div> 58 60
src/routes/Scene.svelte src/lib/editor/ObjectPreviewScene.svelte
-13
src/routes/types.ts
··· 1 - import type * as THREE from 'three'; 2 - 3 - export type geoTypes = 4 - | THREE.BoxGeometry 5 - | THREE.SphereGeometry 6 - | THREE.DodecahedronGeometry 7 - | THREE.CylinderGeometry; 8 - 9 - export type itemType = { 10 - dom: HTMLElement | undefined; 11 - geometry: geoTypes; 12 - material: THREE.MeshStandardMaterial; 13 - };