[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.

new models final, new object picker, some cleanup

Florian (Mar 7, 2025, 8:59 PM +0100) b780ce5c 45aaaa1c

+400 -258
-107
src/lib/HudScene.svelte
··· 1 - <script lang="ts"> 2 - import { T, useThrelte } from '@threlte/core'; 3 - import { interactivity, useCursor, useViewport } from '@threlte/extras'; 4 - import { ACESFilmicToneMapping } from 'three'; 5 - import { applyTransformOfSelected, editorState } from './state.svelte'; 6 - import { onMount } from 'svelte'; 7 - import { visibleKeys, visibleObjects, type RoomObjectKind } from './models'; 8 - 9 - interactivity(); 10 - 11 - const { onPointerEnter, onPointerLeave } = useCursor(); 12 - 13 - const viewport = useViewport(); 14 - 15 - const { renderer } = useThrelte(); 16 - 17 - onMount(() => { 18 - renderer.toneMapping = ACESFilmicToneMapping; 19 - renderer.toneMappingExposure = 0.2; 20 - 21 - window.addEventListener('resize', (e) => { 22 - updateShownCount(); 23 - }); 24 - 25 - updateShownCount(); 26 - }); 27 - 28 - function updateShownCount() { 29 - editorState.shownCount = Math.max(Math.floor(window.innerWidth / 200), 3); 30 - } 31 - 32 - let hoveredKey: string | null = $state(null); 33 - 34 - let objectsShown = $derived( 35 - Object.values(visibleObjects).slice( 36 - editorState.startIndex, 37 - editorState.startIndex + editorState.shownCount 38 - ) 39 - ); 40 - 41 - $inspect(objectsShown); 42 - </script> 43 - 44 - <T.OrthographicCamera makeDefault zoom={80} position={[0, 0, 10]} /> 45 - <T.AmbientLight intensity={Math.PI / 2} /> 46 - 47 - <T.DirectionalLight position={[10, 10, 10]} intensity={3} /> 48 - 49 - {#each objectsShown as Kind, i} 50 - <T.Group 51 - position={[ 52 - $viewport.width * (((i + 0.5) / objectsShown.length - 0.5) * 0.8), 53 - -$viewport.height / 2 + 0.3, 54 - 0 55 - ]} 56 - > 57 - <Kind.component scale={0.5} rotation={[0.2, 0.5, 0]} /> 58 - 59 - <T.Mesh 60 - position={[0, 0.2, -10]} 61 - onclick={async () => { 62 - if (editorState.placingObject) { 63 - editorState.placingObject = null; 64 - 65 - // wait 10 ms 66 - await new Promise((resolve) => setTimeout(resolve, 10)); 67 - } 68 - 69 - applyTransformOfSelected(); 70 - 71 - setTimeout(() => { 72 - editorState.selectedObject = null; 73 - }, 100); 74 - 75 - console.log(visibleKeys[i + editorState.startIndex], editorState.placingObject); 76 - 77 - editorState.placingObject = { 78 - kind: visibleKeys[i + editorState.startIndex] as RoomObjectKind, 79 - position: [0, 0, 0], 80 - rotation: 0, 81 - colors: ['#f1f1f1', '#f1f1f1', '#f1f1f1', '#f1f1f1', '#f1f1f1'], 82 - placement: 'floor' 83 - }; 84 - }} 85 - onpointerenter={() => { 86 - hoveredKey = visibleKeys[i + editorState.startIndex]; 87 - 88 - onPointerEnter(); 89 - }} 90 - onpointerleave={() => { 91 - hoveredKey = null; 92 - 93 - onPointerLeave(); 94 - }} 95 - > 96 - <T.PlaneGeometry args={[1.5, 2]} /> 97 - <T.MeshBasicMaterial 98 - opacity={editorState.placingObject?.kind === visibleKeys[i + editorState.startIndex] 99 - ? 0.5 100 - : hoveredKey === visibleKeys[i + editorState.startIndex] 101 - ? 0.15 102 - : 0} 103 - transparent 104 - /> 105 - </T.Mesh> 106 - </T.Group> 107 - {/each}
+17 -25
src/lib/Room.svelte
··· 1 1 <script lang="ts"> 2 2 import * as THREE from 'three'; 3 - import { T, type Props } from '@threlte/core'; 4 - import Wall from './Wall.svelte'; 3 + import { T } from '@threlte/core'; 5 4 import { applyTransformOfSelected, editorState, roomState, rotateObject } from './state.svelte'; 6 5 import RoomObject from './RoomObject.svelte'; 7 - import { Grid, TransformControls } from '@threlte/extras'; 6 + import { Grid, RoundedBoxGeometry, TransformControls } from '@threlte/extras'; 8 7 import { onMount } from 'svelte'; 9 8 10 9 onMount(() => { ··· 21 20 let snap = 0.125 * 0.125; 22 21 </script> 23 22 24 - <T.Group position.y={editorState.isEditing ? 0 : -1}> 25 - <!-- walls on left and right --> 26 - {#each Array.from({ length: roomState.size.x }, (_, i) => i) as x} 27 - <Wall position={[-roomState.size.z / 2 - 0.05, 0.9, x - roomState.size.x / 2 + 0.5]} /> 28 - {/each} 23 + <T.Group> 24 + <T.Mesh position={[-0.05, 1 - 0.099, -roomState.size.x / 2 - 0.1]} receiveShadow> 25 + <RoundedBoxGeometry args={[roomState.size.z + 0.18, 2, 0.1]} radius={0.03} /> 26 + <T.MeshStandardMaterial color={roomState.wallColor} /> 27 + </T.Mesh> 29 28 30 - <!-- walls on left and right --> 31 - {#each Array.from({ length: roomState.size.z }, (_, i) => i) as z} 32 - {#if z !== 0} 33 - <Wall 34 - position={[z - roomState.size.z / 2 + 0.5, 0.9, -roomState.size.x / 2 - 0.05]} 35 - rotation.y={Math.PI / 2} 36 - /> 37 - {:else} 38 - <Wall 39 - position={[z - roomState.size.z / 2 + 0.5 - 0.05, 0.9, -roomState.size.x / 2 - 0.05]} 40 - rotation.y={Math.PI / 2} 41 - size={[0.1, 2, 1.1]} 42 - /> 43 - {/if} 44 - {/each} 29 + <T.Mesh position={[-roomState.size.z / 2 - 0.1, 1 - 0.099, -0.05]} receiveShadow> 30 + <RoundedBoxGeometry args={[0.1, 2, roomState.size.x + 0.18]} radius={0.03} /> 31 + <T.MeshStandardMaterial color={roomState.wallColor} /> 32 + </T.Mesh> 45 33 46 34 {#if editorState.isEditing} 47 35 <Grid ··· 52 40 {/if} 53 41 54 42 <T.Mesh 55 - position={[0, -0.0502, 0]} 43 + position={[-0.05, -0.0502, -0.05]} 56 44 onpointermove={(e: { point: { x: number; z: number } }) => { 57 45 let point = e.point; 58 46 ··· 88 76 }} 89 77 receiveShadow 90 78 > 91 - <T.BoxGeometry args={[roomState.size.z, 0.1, roomState.size.x]} /> 79 + <RoundedBoxGeometry 80 + args={[roomState.size.z + 0.15, 0.1, roomState.size.x + 0.15]} 81 + radius={0.03} 82 + /> 83 + <!-- <T.BoxGeometry args={[roomState.size.z, 0.1, roomState.size.x]} /> --> 92 84 <T.MeshStandardMaterial color={roomState.floorColor} /> 93 85 </T.Mesh> 94 86
+2 -1
src/lib/RoomObject.svelte
··· 8 8 selected, 9 9 colors, 10 10 opacity, 11 + scale, 11 12 ...restProps 12 13 }: { 13 14 kind: RoomObjectKind; ··· 27 28 {colors} 28 29 {opacity} 29 30 videoId={'https://www.youtube.com/watch?v=k1BneeJTDcU'} 30 - scale={AllObjects[kind]?.scale ?? 1} 31 + scale={scale ?? AllObjects[kind]?.scale ?? 1} 31 32 /> 32 33 {/snippet}
+3 -10
src/lib/Scene.svelte
··· 1 1 <script lang="ts"> 2 2 import { T, useThrelte } from '@threlte/core'; 3 - import { Environment, HUD, OrbitControls } from '@threlte/extras'; 3 + import { Environment, OrbitControls } from '@threlte/extras'; 4 4 import { interactivity } from '@threlte/extras'; 5 5 interactivity(); 6 6 7 7 import Room from './Room.svelte'; 8 8 import Outline from './Outline.svelte'; 9 - import HudScene from './HudScene.svelte'; 10 - import { editorState, roomState } from './state.svelte'; 9 + import { roomState } from './state.svelte'; 11 10 import { onMount } from 'svelte'; 12 11 import { ACESFilmicToneMapping } from 'three'; 13 12 import { base } from '$app/paths'; ··· 42 41 43 42 <T.PerspectiveCamera 44 43 makeDefault 45 - position={[4, 4, 4]} 44 + position={[3, 3, 3]} 46 45 oncreate={(ref) => { 47 46 ref.lookAt(0, 0, 0); 48 47 }} ··· 64 63 <T.AmbientLight intensity={0.1} /> 65 64 66 65 <Outline /> 67 - 68 - {#if editorState.isEditing} 69 - <HUD> 70 - <HudScene /> 71 - </HUD> 72 - {/if} 73 66 74 67 <Environment isBackground={false} url={base + '/env/workshop.jpg'} />
-15
src/lib/Wall.svelte
··· 1 - <script lang="ts"> 2 - import type * as THREE from 'three'; 3 - import { T, type Props } from '@threlte/core'; 4 - import { roomState } from './state.svelte'; 5 - 6 - let { 7 - size = [0.1, 2, 1], 8 - ...restProps 9 - }: Props<THREE.Mesh> & { size?: [number, number, number] } = $props(); 10 - </script> 11 - 12 - <T.Mesh {...restProps} receiveShadow castShadow> 13 - <T.BoxGeometry args={size} /> 14 - <T.MeshStandardMaterial color={roomState.wallColor} /> 15 - </T.Mesh>
+132 -61
src/lib/models.ts
··· 95 95 import KayKitShelfBSmall from './models/kaykit-furniture-kit/shelf_b_small.svelte'; 96 96 import KayKitCabinetMedium from './models/kaykit-furniture-kit/cabinet_medium.svelte'; 97 97 98 - export const AllObjects = { 98 + export const AllObjects: { 99 + [key: string]: { 100 + component: unknown; 101 + colors: number; 102 + visible: boolean; 103 + placement?: 'floor' | 'wall'; 104 + image?: boolean; 105 + scale?: number; 106 + category?: string; 107 + }; 108 + } = { 99 109 'kenney-furniture-kit-table': { 100 110 component: Table, 101 111 colors: 1, ··· 324 334 visible: true, 325 335 placement: 'wall', 326 336 image: false, 327 - scale: 0.4 337 + scale: 0.4, 338 + category: 'miscellaneous' 328 339 }, 329 340 'kaykit-furniture-kit-rug_rectangle-pattern': { 330 341 component: KayKitRugRectanglePattern, ··· 332 343 visible: true, 333 344 placement: 'floor', 334 345 image: false, 335 - scale: 0.4 346 + scale: 0.4, 347 + category: 'carpets' 336 348 }, 337 349 'kaykit-furniture-kit-table_small': { 338 350 component: KayKitTableSmall, ··· 340 352 visible: true, 341 353 placement: 'floor', 342 354 image: false, 343 - scale: 0.4 355 + scale: 0.4, 356 + category: 'tables & cabinets' 344 357 }, 345 358 'kaykit-furniture-kit-couch': { 346 359 component: KayKitCouch, ··· 348 361 visible: true, 349 362 placement: 'floor', 350 363 image: false, 351 - scale: 0.4 364 + scale: 0.4, 365 + category: 'chairs & sofas' 352 366 }, 353 367 'kaykit-furniture-kit-armchair_pillows': { 354 368 component: KayKitArmchairPillows, ··· 356 370 visible: true, 357 371 placement: 'floor', 358 372 image: false, 359 - scale: 0.4 373 + scale: 0.4, 374 + category: 'chairs & sofas' 360 375 }, 361 376 'kaykit-furniture-kit-chair_b_wood': { 362 377 component: KayKitChairBWood, ··· 364 379 visible: true, 365 380 placement: 'floor', 366 381 image: false, 367 - scale: 0.4 382 + scale: 0.4, 383 + category: 'chairs & sofas' 368 384 }, 369 385 'kaykit-furniture-kit-shelf_b_big': { 370 386 component: KayKitShelfBBig, ··· 372 388 visible: true, 373 389 placement: 'wall', 374 390 image: false, 375 - scale: 0.4 391 + scale: 0.4, 392 + category: 'wall accessories' 376 393 }, 377 394 'kaykit-furniture-kit-chair_desk_a': { 378 395 component: KayKitChairDeskA, ··· 380 397 visible: true, 381 398 placement: 'floor', 382 399 image: false, 383 - scale: 0.4 400 + scale: 0.4, 401 + category: 'desk & accessories' 384 402 }, 385 403 'kaykit-furniture-kit-mousepad': { 386 404 component: KayKitMousepad, ··· 388 406 visible: true, 389 407 placement: 'floor', 390 408 image: false, 391 - scale: 0.4 409 + scale: 0.4, 410 + category: 'desk & accessories' 392 411 }, 393 412 'kaykit-furniture-kit-pictureframe_standing_b': { 394 413 component: KayKitPictureframeStandingB, 395 414 colors: 1, 396 - visible: true, 415 + visible: false, 397 416 placement: 'floor', 398 417 image: true, 399 - scale: 0.4 418 + scale: 0.4, 419 + category: 'desk & accessories' 400 420 }, 401 421 'kaykit-furniture-kit-bed_double_b': { 402 422 component: KayKitBedDoubleB, ··· 404 424 visible: true, 405 425 placement: 'floor', 406 426 image: false, 407 - scale: 0.4 427 + scale: 0.4, 428 + category: 'beds' 408 429 }, 409 430 'kaykit-furniture-kit-shelf_a_small': { 410 431 component: KayKitShelfASmall, ··· 412 433 visible: true, 413 434 placement: 'wall', 414 435 image: false, 415 - scale: 0.4 436 + scale: 0.4, 437 + category: 'wall accessories' 416 438 }, 417 439 'kaykit-furniture-kit-mug_a': { 418 440 component: KayKitMugA, ··· 420 442 visible: true, 421 443 placement: 'floor', 422 444 image: false, 423 - scale: 0.4 445 + scale: 0.4, 446 + category: 'desk & accessories' 424 447 }, 425 448 'kaykit-furniture-kit-rug_rounded': { 426 449 component: KayKitRugRounded, ··· 428 451 visible: true, 429 452 placement: 'floor', 430 453 image: false, 431 - scale: 0.4 454 + scale: 0.4, 455 + category: 'carpets' 432 456 }, 433 457 'kaykit-furniture-kit-mousepad_large': { 434 458 component: KayKitMousepadLarge, ··· 436 460 visible: true, 437 461 placement: 'floor', 438 462 image: false, 439 - scale: 0.4 463 + scale: 0.4, 464 + category: 'desk & accessories' 440 465 }, 441 466 'kaykit-furniture-kit-cabinet_small': { 442 467 component: KayKitCabinetSmall, ··· 444 469 visible: true, 445 470 placement: 'floor', 446 471 image: false, 447 - scale: 0.4 472 + scale: 0.4, 473 + category: 'tables & cabinets' 448 474 }, 449 475 'kaykit-furniture-kit-chair_a': { 450 476 component: KayKitChairA, ··· 452 478 visible: true, 453 479 placement: 'floor', 454 480 image: false, 455 - scale: 0.4 481 + scale: 0.4, 482 + category: 'chairs & sofas' 456 483 }, 457 484 'kaykit-furniture-kit-chair_c': { 458 485 component: KayKitChairC, ··· 460 487 visible: true, 461 488 placement: 'floor', 462 489 image: false, 463 - scale: 0.4 490 + scale: 0.4, 491 + category: 'chairs & sofas' 464 492 }, 465 493 'kaykit-furniture-kit-pictureframe_large_a': { 466 494 component: KayKitPictureframeLargeA, ··· 468 496 visible: true, 469 497 placement: 'wall', 470 498 image: true, 471 - scale: 0.4 499 + scale: 0.4, 500 + category: 'wall accessories' 472 501 }, 473 502 'kaykit-furniture-kit-chair_a_wood': { 474 503 component: KayKitChairAWood, ··· 476 505 visible: true, 477 506 placement: 'floor', 478 507 image: false, 479 - scale: 0.4 508 + scale: 0.4, 509 + category: 'chairs & sofas' 480 510 }, 481 511 'kaykit-furniture-kit-bed_single_b': { 482 512 component: KayKitBedSingleB, ··· 484 514 visible: true, 485 515 placement: 'floor', 486 516 image: false, 487 - scale: 0.4 517 + scale: 0.4, 518 + category: 'beds' 488 519 }, 489 520 'kaykit-furniture-kit-chair_stool': { 490 521 component: KayKitChairStool, ··· 492 523 visible: true, 493 524 placement: 'floor', 494 525 image: false, 495 - scale: 0.4 526 + scale: 0.4, 527 + category: 'chairs & sofas' 496 528 }, 497 529 'kaykit-furniture-kit-pictureframe_small_b': { 498 530 component: KayKitPictureframeSmallB, ··· 500 532 visible: true, 501 533 placement: 'wall', 502 534 image: true, 503 - scale: 0.4 535 + scale: 0.4, 536 + category: 'wall accessories' 504 537 }, 505 538 'kaykit-furniture-kit-pillow': { 506 539 component: KayKitPillow, ··· 508 541 visible: true, 509 542 placement: 'floor', 510 543 image: false, 511 - scale: 0.4 544 + scale: 0.4, 545 + category: 'miscellaneous' 512 546 }, 513 547 'kaykit-furniture-kit-gameconsole_handheld': { 514 548 component: KayKitGameconsoleHandheld, ··· 516 550 visible: true, 517 551 placement: 'floor', 518 552 image: false, 519 - scale: 0.4 553 + scale: 0.4, 554 + category: 'miscellaneous' 520 555 }, 521 556 'kaykit-furniture-kit-bed_double_a': { 522 557 component: KayKitBedDoubleA, ··· 524 559 visible: true, 525 560 placement: 'floor', 526 561 image: false, 527 - scale: 0.4 562 + scale: 0.4, 563 + category: 'beds' 528 564 }, 529 565 'kaykit-furniture-kit-pictureframe_standing_a': { 530 566 component: KayKitPictureframeStandingA, 531 567 colors: 1, 532 - visible: true, 568 + visible: false, 533 569 placement: 'floor', 534 570 image: true, 535 - scale: 0.4 571 + scale: 0.4, 572 + category: 'desk & accessories' 536 573 }, 537 574 'kaykit-furniture-kit-cactus_large': { 538 575 component: KayKitCactusLarge, ··· 540 577 visible: true, 541 578 placement: 'floor', 542 579 image: false, 543 - scale: 0.4 580 + scale: 0.4, 581 + category: 'miscellaneous' 544 582 }, 545 583 'kaykit-furniture-kit-couch_pillows': { 546 584 component: KayKitCouchPillows, ··· 548 586 visible: true, 549 587 placement: 'floor', 550 588 image: false, 551 - scale: 0.4 589 + scale: 0.4, 590 + category: 'chairs & sofas' 552 591 }, 553 592 'kaykit-furniture-kit-pictureframe_medium': { 554 593 component: KayKitPictureframeMedium, ··· 556 595 visible: true, 557 596 placement: 'wall', 558 597 image: true, 559 - scale: 0.4 598 + scale: 0.4, 599 + category: 'wall accessories' 560 600 }, 561 601 'kaykit-furniture-kit-armchair': { 562 602 component: KayKitArmchair, ··· 564 604 visible: true, 565 605 placement: 'floor', 566 606 image: false, 567 - scale: 0.4 607 + scale: 0.4, 608 + category: 'chairs & sofas' 568 609 }, 569 610 'kaykit-furniture-kit-lamp_table': { 570 611 component: KayKitLampTable, ··· 572 613 visible: true, 573 614 placement: 'floor', 574 615 image: false, 575 - scale: 0.4 616 + scale: 0.4, 617 + category: 'lights' 576 618 }, 577 619 'kaykit-furniture-kit-mouse': { 578 620 component: KayKitMouse, ··· 580 622 visible: true, 581 623 placement: 'floor', 582 624 image: false, 583 - scale: 0.4 625 + scale: 0.4, 626 + category: 'desk & accessories' 584 627 }, 585 628 'kaykit-furniture-kit-mug_b': { 586 629 component: KayKitMugB, ··· 588 631 visible: true, 589 632 placement: 'floor', 590 633 image: false, 591 - scale: 0.4 634 + scale: 0.4, 635 + category: 'desk & accessories' 592 636 }, 593 637 'kaykit-furniture-kit-book_single': { 594 638 component: KayKitBookSingle, ··· 596 640 visible: true, 597 641 placement: 'wall', 598 642 image: false, 599 - scale: 0.4 643 + scale: 0.4, 644 + category: 'miscellaneous' 600 645 }, 601 646 'kaykit-furniture-kit-chair_desk_b': { 602 647 component: KayKitChairDeskB, ··· 604 649 visible: true, 605 650 placement: 'floor', 606 651 image: false, 607 - scale: 0.4 652 + scale: 0.4, 653 + category: 'desk & accessories' 608 654 }, 609 655 'kaykit-furniture-kit-cactus_small': { 610 656 component: KayKitCactusSmall, ··· 612 658 visible: true, 613 659 placement: 'floor', 614 660 image: false, 615 - scale: 0.4 661 + scale: 0.4, 662 + category: 'miscellaneous' 616 663 }, 617 664 'kaykit-furniture-kit-table_low': { 618 665 component: KayKitTableLow, ··· 620 667 visible: true, 621 668 placement: 'floor', 622 669 image: false, 623 - scale: 0.4 670 + scale: 0.4, 671 + category: 'tables & cabinets' 624 672 }, 625 673 'kaykit-furniture-kit-lamp_standing': { 626 674 component: KayKitLampStanding, ··· 628 676 visible: true, 629 677 placement: 'floor', 630 678 image: false, 631 - scale: 0.4 679 + scale: 0.4, 680 + category: 'lights' 632 681 }, 633 682 'kaykit-furniture-kit-desk': { 634 683 component: KayKitDesk, ··· 636 685 visible: true, 637 686 placement: 'floor', 638 687 image: false, 639 - scale: 0.4 688 + scale: 0.4, 689 + category: 'desk & accessories' 640 690 }, 641 691 'kaykit-furniture-kit-rug_rectangle': { 642 692 component: KayKitRugRectangle, ··· 644 694 visible: true, 645 695 placement: 'floor', 646 696 image: false, 647 - scale: 0.4 697 + scale: 0.4, 698 + category: 'carpets' 648 699 }, 649 700 'kaykit-furniture-kit-pictureframe_large_b': { 650 701 component: KayKitPictureframeLargeB, ··· 652 703 visible: true, 653 704 placement: 'wall', 654 705 image: true, 655 - scale: 0.4 706 + scale: 0.4, 707 + category: 'wall accessories' 656 708 }, 657 709 'kaykit-furniture-kit-cup': { 658 710 component: KayKitCup, ··· 660 712 visible: true, 661 713 placement: 'floor', 662 714 image: false, 663 - scale: 0.4 715 + scale: 0.4, 716 + category: 'desk & accessories' 664 717 }, 665 718 'kaykit-furniture-kit-pictureframe_small_a': { 666 719 component: KayKitPictureframeSmallA, ··· 668 721 visible: true, 669 722 placement: 'wall', 670 723 image: true, 671 - scale: 0.4 724 + scale: 0.4, 725 + category: 'wall accessories' 672 726 }, 673 727 'kaykit-furniture-kit-keyboard': { 674 728 component: KayKitKeyboard, ··· 676 730 visible: true, 677 731 placement: 'floor', 678 732 image: false, 679 - scale: 0.4 733 + scale: 0.4, 734 + category: 'desk & accessories' 680 735 }, 681 736 'kaykit-furniture-kit-bed_single_a': { 682 737 component: KayKitBedSingleA, ··· 684 739 visible: true, 685 740 placement: 'floor', 686 741 image: false, 687 - scale: 0.4 742 + scale: 0.4, 743 + category: 'beds' 688 744 }, 689 745 'kaykit-furniture-kit-desk_large': { 690 746 component: KayKitDeskLarge, ··· 692 748 visible: true, 693 749 placement: 'floor', 694 750 image: false, 695 - scale: 0.4 751 + scale: 0.4, 752 + category: 'desk & accessories' 696 753 }, 697 754 'kaykit-furniture-kit-monitor': { 698 755 component: KayKitMonitor, ··· 700 757 visible: true, 701 758 placement: 'floor', 702 759 image: true, 703 - scale: 0.4 760 + scale: 0.4, 761 + category: 'desk & accessories' 704 762 }, 705 763 'kaykit-furniture-kit-lamp_desk': { 706 764 component: KayKitLampDesk, ··· 708 766 visible: true, 709 767 placement: 'floor', 710 768 image: false, 711 - scale: 0.4 769 + scale: 0.4, 770 + category: 'lights' 712 771 }, 713 772 'kaykit-furniture-kit-chair_stool_wood': { 714 773 component: KayKitChairStoolWood, ··· 716 775 visible: true, 717 776 placement: 'floor', 718 777 image: false, 719 - scale: 0.4 778 + scale: 0.4, 779 + category: 'chairs & sofas' 720 780 }, 721 781 'kaykit-furniture-kit-pictureframe_small_c': { 722 782 component: KayKitPictureframeSmallC, ··· 724 784 visible: true, 725 785 placement: 'wall', 726 786 image: true, 727 - scale: 0.4 787 + scale: 0.4, 788 + category: 'wall accessories' 728 789 }, 729 790 'kaykit-furniture-kit-chair_b': { 730 791 component: KayKitChairB, ··· 732 793 visible: true, 733 794 placement: 'floor', 734 795 image: false, 735 - scale: 0.4 796 + scale: 0.4, 797 + category: 'chairs & sofas' 736 798 }, 737 799 'kaykit-furniture-kit-shelf_a_big': { 738 800 component: KayKitShelfABig, ··· 740 802 visible: true, 741 803 placement: 'wall', 742 804 image: false, 743 - scale: 0.4 805 + scale: 0.4, 806 + category: 'wall accessories' 744 807 }, 745 808 'kaykit-furniture-kit-table_medium_long': { 746 809 component: KayKitTableMediumLong, ··· 748 811 visible: true, 749 812 placement: 'floor', 750 813 image: false, 751 - scale: 0.4 814 + scale: 0.4, 815 + category: 'tables & cabinets' 752 816 }, 753 817 'kaykit-furniture-kit-cup_pencils': { 754 818 component: KayKitCupPencils, ··· 756 820 visible: true, 757 821 placement: 'floor', 758 822 image: false, 759 - scale: 0.4 823 + scale: 0.4, 824 + category: 'desk & accessories' 760 825 }, 761 826 'kaykit-furniture-kit-table_medium': { 762 827 component: KayKitTableMedium, ··· 764 829 visible: true, 765 830 placement: 'floor', 766 831 image: false, 767 - scale: 0.4 832 + scale: 0.4, 833 + category: 'tables & cabinets' 768 834 }, 769 835 'kaykit-furniture-kit-shelf_b_small': { 770 836 component: KayKitShelfBSmall, ··· 772 838 visible: true, 773 839 placement: 'wall', 774 840 image: false, 775 - scale: 0.4 841 + scale: 0.4, 842 + category: 'wall accessories' 776 843 }, 777 844 'kaykit-furniture-kit-cabinet_medium': { 778 845 component: KayKitCabinetMedium, ··· 780 847 visible: true, 781 848 placement: 'floor', 782 849 image: false, 783 - scale: 0.4 850 + scale: 0.4, 851 + category: 'tables & cabinets' 784 852 } 785 853 }; 786 854 ··· 791 859 ); 792 860 export const visibleKeys = Object.keys(visibleObjects); 793 861 862 + export const objectCategories = Array.from( 863 + new Set(Object.values(visibleObjects).map((object) => object.category)) 864 + ); 794 865 795 866 export type RoomObjectKind = keyof typeof AllObjects;
+3 -3
src/lib/state.svelte.ts
··· 14 14 15 15 version: number; 16 16 } = $state({ 17 - wallColor: '#f97316', 18 - floorColor: '#fde047', 17 + wallColor: '#f1f1f1', 18 + floorColor: '#a1a1a1', 19 19 20 20 objects: [], 21 21 ··· 55 55 }; 56 56 57 57 export function applyTransformOfSelected() { 58 - if (editorState.selectedObject && editorState.transformControls) { 58 + if (editorState.selectedObject && editorState.transformControls?.object) { 59 59 editorState.selectedObject.position = [ 60 60 editorState.transformControls.object.position.x, 61 61 editorState.transformControls.object.position.y,
+24 -23
src/routes/+page.svelte
··· 34 34 import Avatar from '$lib/components/base/avatar/Avatar.svelte'; 35 35 import ColorPickerPopover from './ColorPickerPopover.svelte'; 36 36 import { AllObjects, visibleKeys } from '$lib/models'; 37 + import Picker from './Picker.svelte'; 37 38 38 39 let saving = $state(false); 39 40 async function saveRoomToBluesky() { ··· 123 124 let availableObjects = new Set(Object.keys(AllObjects)); 124 125 125 126 roomState.objects = room.room.objects.filter((object: RoomObjectData) => 126 - availableObjects.has(object.kind) 127 + availableObjects.has(object.kind as string) 127 128 ); 128 129 if (roomState.objects.length !== room.room.objects.length) { 129 130 console.warn('some objects were not found and removed!'); ··· 229 230 // return first 14 colors 230 231 return colors.slice(0, 14); 231 232 }); 233 + 234 + let selectCategoryModalOpen = $state(false); 232 235 </script> 233 236 234 - <div class="fixed inset-0 h-[100dvh] w-screen"> 237 + <div class="fixed inset-0 -z-20 h-[100dvh] w-screen"> 235 238 <Canvas> 236 239 <Scene /> 237 240 </Canvas> 238 241 </div> 239 242 240 243 {#if editorState.isEditing} 241 - <Button 242 - class="fixed right-4 bottom-4" 243 - onclick={() => { 244 - editorState.startIndex += editorState.shownCount; 245 - if (editorState.startIndex >= visibleKeys.length) { 246 - editorState.startIndex = 0; 247 - } 248 - }}>&rarr;</Button 249 - > 244 + <Button class="fixed bottom-4 left-4 -z-10" size="iconLg" onclick={() => { 245 + console.log('clicked', selectCategoryModalOpen); 246 + selectCategoryModalOpen = true; 247 + }}> 248 + <svg 249 + xmlns="http://www.w3.org/2000/svg" 250 + fill="none" 251 + viewBox="0 0 24 24" 252 + stroke-width="2.5" 253 + stroke="currentColor" 254 + > 255 + <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> 256 + </svg> 257 + </Button> 250 258 251 - <Button 252 - class="fixed bottom-4 left-4" 253 - onclick={() => { 254 - editorState.startIndex -= editorState.shownCount; 255 - if (editorState.startIndex < 0) { 256 - editorState.startIndex = visibleKeys.length - editorState.shownCount; 257 - } 258 - }}>&larr;</Button 259 - > 260 - 261 - <div class="fixed top-4 left-4 flex flex-col items-start gap-2"> 259 + <div class="fixed top-4 left-4 flex flex-col items-start gap-2 -z-10"> 262 260 <Button 263 261 size="iconLg" 264 262 onclick={() => { ··· 425 423 {/if} 426 424 {/if} 427 425 </div> 428 - <div class="fixed top-4 right-4 flex flex-col items-end gap-2"> 426 + <div class="fixed top-4 right-4 flex flex-col items-end gap-2 -z-10"> 429 427 {#if !client || !client.isLoggedIn} 430 428 <Button 431 429 onclick={() => { ··· 622 620 roomState.floorColor = '#a1a1a1'; 623 621 624 622 saveRoomToLocalStorage(); 623 + roomSettingsModalState = false; 625 624 }} 626 625 > 627 626 Clear room ··· 824 823 </Button> 825 824 </div> 826 825 </Modal> 826 + 827 + <Picker bind:selectCategoryModalOpen />
+146
src/routes/Picker.svelte
··· 1 + <script lang="ts"> 2 + import { Canvas } from '@threlte/core'; 3 + import { View } from '@threlte/extras'; 4 + import Scene from './Scene.svelte'; 5 + import { objectCategories, visibleObjects, type RoomObjectKind } from '$lib/models'; 6 + import Button from '$lib/components/base/button/Button.svelte'; 7 + import Modal from '$lib/components/base/modal/Modal.svelte'; 8 + import { applyTransformOfSelected, editorState } from '$lib/state.svelte'; 9 + import Subheading from '$lib/components/base/heading/Subheading.svelte'; 10 + 11 + let items: { kind: string; dom: HTMLElement | undefined }[] = $state([]); 12 + 13 + $effect(() => { 14 + items = Object.entries(visibleObjects) 15 + .filter(([kind, object]) => object.category === category) 16 + .map(([kind, object]) => ({ 17 + kind: kind, 18 + dom: undefined 19 + })); 20 + }); 21 + 22 + let open = $state(false); 23 + 24 + let categoryIndex = $state(0); 25 + let category = $derived(objectCategories[categoryIndex]); 26 + 27 + console.log(objectCategories); 28 + 29 + function humanReadableName(key: string): string { 30 + // Remove a common prefix if present (adjust if your keys differ) 31 + let name = key.replace(/^kaykit-furniture-kit-/, ''); 32 + // Replace underscores and dashes with spaces 33 + name = name.replace(/[-_]+/g, ' '); 34 + // Capitalize the first letter of each word 35 + return name 36 + .split(' ') 37 + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) 38 + .join(' '); 39 + } 40 + 41 + let { selectCategoryModalOpen = $bindable(false) }: { selectCategoryModalOpen: boolean } = 42 + $props(); 43 + 44 + async function addObject(kind: RoomObjectKind) { 45 + if (editorState.placingObject) { 46 + editorState.placingObject = null; 47 + 48 + // wait 10 ms 49 + await new Promise((resolve) => setTimeout(resolve, 10)); 50 + } 51 + 52 + applyTransformOfSelected(); 53 + 54 + setTimeout(() => { 55 + editorState.selectedObject = null; 56 + }, 100); 57 + 58 + editorState.placingObject = { 59 + kind: kind, 60 + position: [0, 0, 0], 61 + rotation: 0, 62 + colors: ['#f1f1f1', '#f1f1f1', '#f1f1f1', '#f1f1f1', '#f1f1f1'], 63 + placement: 'floor' 64 + }; 65 + } 66 + </script> 67 + 68 + <Modal open={selectCategoryModalOpen} class="z-[100]"> 69 + <Subheading class="mb-4">Add some furniture:</Subheading> 70 + <div class="grid grid-cols-2 gap-4"> 71 + {#each objectCategories as category, i} 72 + <Button 73 + onclick={() => { 74 + categoryIndex = i; 75 + open = true; 76 + selectCategoryModalOpen = false; 77 + }}>{category}</Button 78 + > 79 + {/each} 80 + </div> 81 + </Modal> 82 + 83 + {#if open} 84 + <button 85 + class="fixed inset-0 bg-black/50" 86 + aria-label="Close" 87 + onclick={() => { 88 + open = false; 89 + }} 90 + ></button> 91 + {/if} 92 + 93 + {#key category} 94 + <div 95 + class={[ 96 + 'bg-base-200 dark:bg-base-800 relative mx-2 my-4 h-full min-h-[80dvh] overscroll-y-none rounded-2xl p-4 sm:mx-20 md:mt-20', 97 + open ? 'block' : 'hidden' 98 + ]} 99 + > 100 + <Button 101 + size="iconLg" 102 + class="absolute top-4 left-4" 103 + onclick={() => { 104 + open = false; 105 + selectCategoryModalOpen = true; 106 + }} 107 + > 108 + <svg 109 + xmlns="http://www.w3.org/2000/svg" 110 + fill="none" 111 + viewBox="0 0 24 24" 112 + stroke-width="1.5" 113 + stroke="currentColor" 114 + > 115 + <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" /> 116 + </svg> 117 + </Button> 118 + <div 119 + class="relative mx-auto grid h-full max-w-4xl grid-cols-2 gap-4 overflow-y-scroll overscroll-y-none md:grid-cols-3 lg:grid-cols-4" 120 + > 121 + {#each items as item, i} 122 + <button 123 + id="item" 124 + class="hover:bg-base-100/20 dark:hover:bg-base-900/20 m-4 inline-block cursor-pointer rounded-2xl p-4 transition-all duration-300 hover:scale-105" 125 + onclick={() => { 126 + addObject(item.kind as RoomObjectKind); 127 + open = false; 128 + }} 129 + > 130 + <div class="aspect-square" bind:this={item.dom}></div> 131 + <div class="mt-2 text-[#888]">{humanReadableName(item.kind)}</div> 132 + </button> 133 + {/each} 134 + </div> 135 + </div> 136 + 137 + <div id="canvas" class="pointer-events-none fixed top-0 h-full w-full"> 138 + <Canvas> 139 + {#each items as item} 140 + <View dom={item.dom}> 141 + <Scene kind={item.kind} /> 142 + </View> 143 + {/each} 144 + </Canvas> 145 + </div> 146 + {/key}
+29
src/routes/Scene.svelte
··· 1 + <script lang="ts"> 2 + import RoomObject from '$lib/RoomObject.svelte'; 3 + import { T, useTask, useThrelte } from '@threlte/core'; 4 + import { OrbitControls } from '@threlte/extras'; 5 + import { Color } from 'three'; 6 + import { Align } from '@threlte/extras'; 7 + 8 + let { kind } = $props(); 9 + 10 + const { scene } = useThrelte(); 11 + let rotation = $state(0); 12 + 13 + useTask((delta) => { 14 + rotation += delta; 15 + }); 16 + </script> 17 + 18 + <T.PerspectiveCamera 19 + makeDefault 20 + position={[0, 0, 2]} 21 + fov={50} 22 + near={1} 23 + far={10} 24 + ></T.PerspectiveCamera> 25 + 26 + <T.HemisphereLight args={[0xaaaaaa, 0x444444, 1]} /> 27 + <T.DirectionalLight args={[0xffffff, 1]} position={[1, 1, 1]} /> 28 + 29 + <RoomObject {kind} rotation={[0.5, rotation, 0]} scale={0.4} />
+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 + };
+11 -4
src/lib/models/RoomObjectMaterial.svelte
··· 1 1 <script lang="ts"> 2 - import { T } from '@threlte/core'; 2 + import { T, type Props } from '@threlte/core'; 3 3 import { defaultColorImage, defaultColors } from './types'; 4 + import type { MeshStandardMaterial } from 'three'; 4 5 5 6 let { 6 7 index, ··· 8 9 opacity, 9 10 youtube, 10 11 image, 11 - isImage 12 + isImage, 13 + ...props 12 14 }: { 13 15 index?: number; 14 16 colors?: string[]; 15 17 opacity?: number; 16 18 youtube?: string; 17 19 image?: string; 18 - } = $props(); 20 + } & Props<MeshStandardMaterial> = $props(); 19 21 20 22 function getColor() { 21 23 if (image) { ··· 25 27 } 26 28 </script> 27 29 28 - <T.MeshStandardMaterial color={getColor()} {opacity} transparent={opacity ? opacity < 1 : false} /> 30 + <T.MeshStandardMaterial 31 + color={getColor()} 32 + {opacity} 33 + transparent={opacity ? opacity < 1 : false} 34 + {...props} 35 + />
+3 -2
src/lib/models/kaykit-furniture-kit/lamp_desk.svelte
··· 58 58 <T.Mesh castShadow receiveShadow geometry={gltf.nodes.Cylinder629_1.geometry} 59 59 ><RoomObjectMaterial index={1} {colors} {opacity} /></T.Mesh 60 60 > 61 - <T.Mesh castShadow receiveShadow geometry={gltf.nodes.Cylinder629_2.geometry} 62 - ><RoomObjectMaterial index={2} {colors} {opacity} /></T.Mesh 61 + <T.Mesh geometry={gltf.nodes.Cylinder629_2.geometry} 62 + ><RoomObjectMaterial emissive={colors?.[2]} emissiveIntensity={1} index={2} {colors} {opacity} /></T.Mesh 63 63 > 64 + <T.PointLight position={[0, 0.8, 0.45]} color={colors?.[2]} intensity={1} /> 64 65 </T.Group> 65 66 {:catch err} 66 67 {@render error?.({ error: err })}
+6 -4
src/lib/models/kaykit-furniture-kit/lamp_standing.svelte
··· 55 55 {:then gltf} 56 56 <T.Group> 57 57 <T.Mesh castShadow receiveShadow geometry={gltf.nodes.Sphere002.geometry} 58 - ><RoomObjectMaterial index={0} {colors} {opacity} /></T.Mesh 58 + ><RoomObjectMaterial index={0} {colors} opacity={0.95} emissive={colors?.[2]} emissiveIntensity={0.5} /></T.Mesh 59 59 > 60 60 <T.Mesh castShadow receiveShadow geometry={gltf.nodes.Sphere002_1.geometry} 61 - ><RoomObjectMaterial index={1} {colors} {opacity} /></T.Mesh 61 + ><RoomObjectMaterial index={1} {colors} {opacity} /></T.Mesh 62 62 > 63 - <T.Mesh castShadow receiveShadow geometry={gltf.nodes.Sphere002_2.geometry} 64 - ><RoomObjectMaterial index={2} {colors} {opacity} /></T.Mesh 63 + <T.Mesh geometry={gltf.nodes.Sphere002_2.geometry} 64 + ><RoomObjectMaterial emissive={colors?.[2]} emissiveIntensity={1} index={2} {colors} {opacity} /></T.Mesh 65 65 > 66 + 67 + <T.PointLight position={[0, 2, 0.0]} color={colors?.[2]} intensity={1} /> 66 68 </T.Group> 67 69 {:catch err} 68 70 {@render error?.({ error: err })}
+11 -3
src/lib/models/kaykit-furniture-kit/lamp_table.svelte
··· 53 53 {:then gltf} 54 54 <T.Group> 55 55 <T.Mesh castShadow receiveShadow geometry={gltf.nodes.Sphere001.geometry} 56 - ><RoomObjectMaterial index={0} {colors} {opacity} /></T.Mesh 56 + ><RoomObjectMaterial 57 + index={0} 58 + {colors} 59 + opacity={0.95} 60 + emissive={colors?.[2]} 61 + emissiveIntensity={0.5} 62 + /></T.Mesh 57 63 > 58 64 <T.Mesh castShadow receiveShadow geometry={gltf.nodes.Sphere001_1.geometry} 59 65 ><RoomObjectMaterial index={1} {colors} {opacity} /></T.Mesh 60 66 > 61 - <T.Mesh castShadow receiveShadow geometry={gltf.nodes.Sphere001_2.geometry} 62 - ><RoomObjectMaterial index={2} {colors} {opacity} /></T.Mesh 67 + <T.Mesh geometry={gltf.nodes.Sphere001_2.geometry} 68 + ><RoomObjectMaterial emissive={colors?.[2]} emissiveIntensity={1} index={2} {colors} {opacity} /></T.Mesh 63 69 > 70 + 71 + <T.PointLight position={[0, 0.4, 0]} color={colors?.[2]} intensity={1} /> 64 72 </T.Group> 65 73 {:catch err} 66 74 {@render error?.({ error: err })}