@ifc-lite/viewer 1.17.4 → 1.17.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/.turbo/turbo-build.log +16 -16
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +117 -0
  4. package/DESKTOP_CONTRACT_VERSION +1 -1
  5. package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-86rgogji.js} +1 -1
  6. package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
  7. package/dist/assets/{exporters-ChAtBmlj.js → exporters-CcPS9MK5.js} +2274 -2227
  8. package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-BFUYA08u.js} +1 -1
  9. package/dist/assets/ids-DQ5jY0E8.js +1 -0
  10. package/dist/assets/ifc-lite_bg-BINvzoCP.wasm +0 -0
  11. package/dist/assets/{index-Co8E2-FE.js → index-Bfms9I4A.js} +35160 -33084
  12. package/dist/assets/index-_bfZsDCC.css +1 -0
  13. package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-DUyLCMZS.js} +104 -104
  14. package/dist/assets/{sandbox-DZiNLNMk.js → sandbox-C8575tul.js} +4340 -4322
  15. package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-BuZK7OST.js} +1 -1
  16. package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-JsqEGDV8.js} +1 -1
  17. package/dist/index.html +8 -7
  18. package/index.html +1 -0
  19. package/package.json +7 -7
  20. package/src/App.tsx +16 -2
  21. package/src/components/viewer/CesiumOverlay.tsx +62 -19
  22. package/src/components/viewer/ChatPanel.tsx +195 -91
  23. package/src/components/viewer/MainToolbar.tsx +4 -3
  24. package/src/components/viewer/PropertiesPanel.tsx +16 -2
  25. package/src/components/viewer/SettingsPage.tsx +252 -101
  26. package/src/components/viewer/ThemeSwitch.tsx +63 -7
  27. package/src/components/viewer/ViewerLayout.tsx +1 -0
  28. package/src/components/viewer/Viewport.tsx +14 -2
  29. package/src/components/viewer/ViewportContainer.tsx +49 -64
  30. package/src/components/viewer/ViewportOverlays.tsx +5 -2
  31. package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
  32. package/src/components/viewer/chat/ModelSelector.tsx +90 -54
  33. package/src/components/viewer/properties/GeoreferencingPanel.tsx +113 -51
  34. package/src/components/viewer/properties/LocationMap.tsx +9 -7
  35. package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
  36. package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
  37. package/src/components/viewer/tools/SectionPanel.tsx +39 -18
  38. package/src/components/viewer/useAnimationLoop.ts +9 -1
  39. package/src/components/viewer/useRenderUpdates.ts +1 -1
  40. package/src/hooks/ids/idsDataAccessor.ts +60 -24
  41. package/src/hooks/ingest/viewerModelIngest.ts +7 -2
  42. package/src/hooks/useIfcFederation.ts +326 -71
  43. package/src/hooks/useIfcLoader.ts +1 -0
  44. package/src/hooks/useViewControls.ts +13 -5
  45. package/src/index.css +484 -10
  46. package/src/lib/desktop-entitlement.ts +2 -4
  47. package/src/lib/geo/cesium-bridge.ts +15 -7
  48. package/src/lib/geo/effective-georef.test.ts +73 -0
  49. package/src/lib/geo/effective-georef.ts +111 -0
  50. package/src/lib/geo/reproject.ts +105 -19
  51. package/src/lib/llm/byok-guard.test.ts +77 -0
  52. package/src/lib/llm/byok-guard.ts +39 -0
  53. package/src/lib/llm/free-models.test.ts +0 -6
  54. package/src/lib/llm/models.ts +104 -42
  55. package/src/lib/llm/stream-client.ts +74 -110
  56. package/src/lib/llm/stream-direct.test.ts +130 -0
  57. package/src/lib/llm/stream-direct.ts +316 -0
  58. package/src/lib/llm/types.ts +14 -2
  59. package/src/main.tsx +1 -10
  60. package/src/services/api-keys.ts +73 -0
  61. package/src/store/constants.ts +20 -2
  62. package/src/store/index.ts +12 -5
  63. package/src/store/slices/cesiumSlice.ts +5 -0
  64. package/src/store/slices/chatSlice.test.ts +6 -76
  65. package/src/store/slices/chatSlice.ts +17 -58
  66. package/src/store/slices/sectionSlice.test.ts +87 -7
  67. package/src/store/slices/sectionSlice.ts +151 -5
  68. package/src/store/slices/uiSlice.ts +28 -5
  69. package/src/store/types.ts +26 -0
  70. package/src/utils/nativeSpatialDataStore.ts +4 -1
  71. package/src/utils/viewportUtils.ts +7 -2
  72. package/src/vite-env.d.ts +0 -4
  73. package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
  74. package/dist/assets/ids-B4jTqB1O.js +0 -1
  75. package/dist/assets/ifc-lite_bg-BX4E7TX8.wasm +0 -0
  76. package/dist/assets/index-DckuDqlv.css +0 -1
  77. package/src/components/viewer/UpgradePage.tsx +0 -71
  78. package/src/lib/desktop/ClerkDesktopEntitlementSync.tsx +0 -175
  79. package/src/lib/llm/ClerkChatSync.tsx +0 -74
  80. package/src/lib/llm/clerk-auth.ts +0 -62
package/src/index.css CHANGED
@@ -412,6 +412,490 @@ body {
412
412
  background-color: rgba(115, 218, 202, 0.05) !important;
413
413
  }
414
414
 
415
+ /* ═══════════════════════════════════════════════════════════════════════════
416
+ COLORFUL MODE — Brutalist Aurora
417
+ Shift+click the toggle. The same sharp, functional, dense UI — but the
418
+ sky opened up. An aurora gradient behind the 3D scene, frosted-glass
419
+ panels, and the full Tokyo Night accent palette in its natural places.
420
+ No decorative borders. No gradient buttons. Just atmosphere.
421
+ ═══════════════════════════════════════════════════════════════════════════ */
422
+
423
+ @custom-variant colorful (&:where(.colorful, .colorful *));
424
+
425
+ .colorful {
426
+ /* Tokyo Night accent palette — used only where colour already exists */
427
+ --cf-blue: #7aa2f7;
428
+ --cf-cyan: #7dcfff;
429
+ --cf-teal: #73daca;
430
+ --cf-magenta: #bb9af7;
431
+ --cf-purple: #9d7cd8;
432
+ --cf-orange: #ff9e64;
433
+ --cf-pink: #f7768e;
434
+ --cf-green: #9ece6a;
435
+ --cf-yellow: #e0af68;
436
+
437
+ /* Glass */
438
+ --cf-glass: rgba(255, 255, 255, 0.48);
439
+ --cf-glass-strong: rgba(255, 255, 255, 0.68);
440
+
441
+ /* ── Semantic tokens ── */
442
+ --color-background: #e0e6f0 !important;
443
+ --color-foreground: #1a1b2e !important;
444
+ --color-card: var(--cf-glass) !important;
445
+ --color-card-foreground: #1a1b2e !important;
446
+ --color-popover: var(--cf-glass-strong) !important;
447
+ --color-popover-foreground: #1a1b2e !important;
448
+ --color-primary: var(--cf-purple) !important;
449
+ --color-primary-foreground: #fff !important;
450
+ --color-secondary: rgba(157, 124, 216, 0.10) !important;
451
+ --color-secondary-foreground: #2e3050 !important;
452
+ --color-muted: rgba(0, 0, 0, 0.04) !important;
453
+ --color-muted-foreground: #555b78 !important;
454
+ --color-accent: rgba(157, 124, 216, 0.10) !important;
455
+ --color-accent-foreground: #2e3050 !important;
456
+ --color-destructive: var(--cf-pink) !important;
457
+ --color-destructive-foreground: #fff !important;
458
+ /* Borders: neutral, semi-transparent — NOT coloured */
459
+ --color-border: rgba(0, 0, 0, 0.10) !important;
460
+ --color-input: rgba(0, 0, 0, 0.06) !important;
461
+ --color-ring: var(--cf-purple) !important;
462
+
463
+ /* Hierarchy — lavender tints */
464
+ --hierarchy-selected-bg: rgba(157, 124, 216, 0.12);
465
+ --hierarchy-selected-text: #1a1b2e;
466
+ --hierarchy-text: #40445e;
467
+ --hierarchy-hover-bg: rgba(200, 195, 230, 0.15);
468
+
469
+ /* Tabs — warm peach undertone */
470
+ --tabs-bg: rgba(235, 215, 195, 0.25);
471
+ --tabs-border: rgba(0, 0, 0, 0.08);
472
+ --tab-text: #555b78;
473
+ --tab-active-bg: rgba(245, 230, 210, 0.55);
474
+ --tab-active-text: #1a1b2e;
475
+ }
476
+
477
+ /* ── Page background ── */
478
+ .colorful body,
479
+ .colorful #root {
480
+ background: #dde3f0 !important;
481
+ }
482
+
483
+ /* ── Toolbar: cool violet-tinted glass ── */
484
+ .colorful .border-b.bg-white,
485
+ .colorful .border-b.dark\:bg-black {
486
+ background: rgba(230, 225, 245, 0.60) !important;
487
+ backdrop-filter: blur(20px) saturate(1.5) !important;
488
+ -webkit-backdrop-filter: blur(20px) saturate(1.5) !important;
489
+ }
490
+
491
+ /* ── Panels & cards: frosted glass (neutral base) ── */
492
+ .colorful .bg-white,
493
+ .colorful .bg-card,
494
+ .colorful .bg-background {
495
+ background: var(--cf-glass) !important;
496
+ backdrop-filter: blur(16px) saturate(1.3) !important;
497
+ -webkit-backdrop-filter: blur(16px) saturate(1.3) !important;
498
+ }
499
+
500
+ .colorful .bg-zinc-50,
501
+ .colorful .bg-zinc-100 {
502
+ background-color: rgba(255, 255, 255, 0.25) !important;
503
+ }
504
+
505
+ .colorful .bg-zinc-900,
506
+ .colorful .bg-zinc-950,
507
+ .colorful .bg-black {
508
+ background: var(--cf-glass) !important;
509
+ backdrop-filter: blur(16px) saturate(1.3) !important;
510
+ -webkit-backdrop-filter: blur(16px) saturate(1.3) !important;
511
+ }
512
+
513
+ /* ── Tinted glass per panel — colour echoes the aurora ── */
514
+
515
+ /* Left panel: cool lavender tint (echoes aurora top) */
516
+ .colorful .panel-container:first-of-type,
517
+ .colorful [id="left-panel"] > .panel-container,
518
+ .colorful [id="left-panel"] > div {
519
+ background: rgba(200, 195, 230, 0.38) !important;
520
+ backdrop-filter: blur(16px) saturate(1.4) !important;
521
+ -webkit-backdrop-filter: blur(16px) saturate(1.4) !important;
522
+ }
523
+
524
+ /* Right panel: warm peach/amber tint (echoes aurora bottom) */
525
+ .colorful [id="right-panel"] > .panel-container,
526
+ .colorful [id="right-panel"] > div {
527
+ background: rgba(235, 215, 195, 0.40) !important;
528
+ backdrop-filter: blur(16px) saturate(1.4) !important;
529
+ -webkit-backdrop-filter: blur(16px) saturate(1.4) !important;
530
+ }
531
+
532
+ /* Resize handles: thin, tinted */
533
+ .colorful [data-panel-group-direction="horizontal"] > [data-resize-handle] {
534
+ background-color: rgba(157, 124, 216, 0.18) !important;
535
+ transition: background-color 0.15s;
536
+ }
537
+
538
+ .colorful [data-panel-group-direction="horizontal"] > [data-resize-handle]:hover {
539
+ background-color: rgba(157, 124, 216, 0.35) !important;
540
+ }
541
+
542
+ .colorful [data-panel-group-direction="horizontal"] > [data-resize-handle][data-resize-handle-active] {
543
+ background-color: rgba(157, 124, 216, 0.50) !important;
544
+ }
545
+
546
+ /* ── Borders: single neutral tone everywhere ── */
547
+ .colorful .border-zinc-100,
548
+ .colorful .border-zinc-200,
549
+ .colorful .border-zinc-300,
550
+ .colorful .border-zinc-400,
551
+ .colorful .border-zinc-500,
552
+ .colorful .border-zinc-600,
553
+ .colorful .border-zinc-700,
554
+ .colorful .border-zinc-800,
555
+ .colorful .border-zinc-900,
556
+ .colorful .border-zinc-950 {
557
+ border-color: rgba(0, 0, 0, 0.10) !important;
558
+ }
559
+
560
+ .colorful .border,
561
+ .colorful .border-b,
562
+ .colorful .border-t,
563
+ .colorful .border-l,
564
+ .colorful .border-r,
565
+ .colorful .border-border {
566
+ border-color: rgba(0, 0, 0, 0.10) !important;
567
+ }
568
+
569
+ /* Purple borders (property editor) stay purple but softened */
570
+ .colorful .border-purple-200,
571
+ .colorful .border-purple-300\/50,
572
+ .colorful .border-purple-500\/30,
573
+ .colorful .border-purple-700,
574
+ .colorful .border-purple-800 {
575
+ border-color: rgba(157, 124, 216, 0.25) !important;
576
+ }
577
+
578
+ /* ── Text ── */
579
+ .colorful .text-zinc-900,
580
+ .colorful .text-zinc-800,
581
+ .colorful .text-zinc-700,
582
+ .colorful .text-zinc-100 {
583
+ color: #1a1b2e !important;
584
+ }
585
+
586
+ .colorful .text-zinc-500,
587
+ .colorful .text-zinc-400,
588
+ .colorful .text-zinc-600 {
589
+ color: #555b78 !important;
590
+ }
591
+
592
+ .colorful .text-zinc-200,
593
+ .colorful .text-zinc-300 {
594
+ color: #40445e !important;
595
+ }
596
+
597
+ /* ── Primary: purple (from Tokyo Night magenta/purple family) ── */
598
+ .colorful .bg-primary {
599
+ background-color: var(--cf-purple) !important;
600
+ }
601
+
602
+ .colorful .text-primary {
603
+ color: var(--cf-purple) !important;
604
+ }
605
+
606
+ .colorful .border-primary {
607
+ border-color: var(--cf-purple) !important;
608
+ }
609
+
610
+ /* ── Shadows ── */
611
+ .colorful .shadow-lg,
612
+ .colorful .shadow-xl {
613
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08) !important;
614
+ }
615
+
616
+ /* ── Selection ── */
617
+ .colorful ::selection {
618
+ background-color: rgba(157, 124, 216, 0.25);
619
+ color: #1a1b2e;
620
+ }
621
+
622
+ /* ── Scrollbars ── */
623
+ .colorful .scrollbar-thin {
624
+ scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
625
+ }
626
+
627
+ .colorful .scrollbar-thin::-webkit-scrollbar-thumb {
628
+ background-color: rgba(0, 0, 0, 0.15);
629
+ }
630
+
631
+ .colorful .scrollbar-thin::-webkit-scrollbar-thumb:hover {
632
+ background-color: rgba(0, 0, 0, 0.25);
633
+ }
634
+
635
+ /* ── Hovers: tinted to match panel context ── */
636
+ .colorful .hover\:bg-primary:hover {
637
+ background-color: var(--cf-purple) !important;
638
+ }
639
+
640
+ .colorful .hover\:bg-zinc-50:hover,
641
+ .colorful .hover\:bg-zinc-100:hover,
642
+ .colorful .hover\:bg-zinc-200:hover,
643
+ .colorful .hover\:bg-white:hover {
644
+ background-color: rgba(200, 195, 230, 0.18) !important;
645
+ }
646
+
647
+ /* ── Active tabs ── */
648
+ .colorful [data-state="active"] {
649
+ background-color: var(--cf-glass-strong) !important;
650
+ color: #1a1b2e !important;
651
+ }
652
+
653
+ .colorful .data-\[state\=active\]\:bg-white[data-state="active"],
654
+ .colorful .data-\[state\=active\]\:bg-zinc-100[data-state="active"] {
655
+ background-color: var(--cf-glass-strong) !important;
656
+ }
657
+
658
+ /* Properties tabs: warm orange accent on active (echoes aurora horizon) */
659
+ .colorful .properties-tab-trigger[data-state="active"] {
660
+ border-top-color: var(--cf-orange) !important;
661
+ background: rgba(245, 230, 210, 0.50) !important;
662
+ }
663
+
664
+ /* ── Tabs container: warmer tint to match right panel ── */
665
+ .colorful .properties-tabs-list {
666
+ background-color: rgba(235, 215, 195, 0.30) !important;
667
+ border-bottom-color: rgba(0, 0, 0, 0.08) !important;
668
+ }
669
+
670
+ /* ── Tree selection: purple tint with left accent ── */
671
+ .colorful .tree-node.selected {
672
+ background-color: rgba(157, 124, 216, 0.12) !important;
673
+ border-left-color: var(--cf-purple) !important;
674
+ }
675
+
676
+ .colorful .hierarchy-item.selected {
677
+ background-color: rgba(157, 124, 216, 0.12) !important;
678
+ }
679
+
680
+ /* ── Hierarchy hover: subtle lavender ── */
681
+ .colorful .hierarchy-item:not(.selected):hover {
682
+ background-color: rgba(200, 195, 230, 0.20) !important;
683
+ }
684
+
685
+ /* ── Quantity cards ── */
686
+ .colorful .border-blue-200,
687
+ .colorful .border-blue-800,
688
+ .colorful .border-blue-900 {
689
+ border-color: rgba(122, 162, 247, 0.25) !important;
690
+ }
691
+
692
+ .colorful .text-blue-700,
693
+ .colorful .text-blue-400 {
694
+ color: #6a8fd6 !important;
695
+ }
696
+
697
+ .colorful .bg-blue-50\/20,
698
+ .colorful .bg-blue-950\/20 {
699
+ background-color: rgba(122, 162, 247, 0.06) !important;
700
+ }
701
+
702
+ /* ── Success / Location ── */
703
+ .colorful .border-emerald-500\/30 {
704
+ border-color: rgba(115, 218, 202, 0.25) !important;
705
+ }
706
+
707
+ .colorful .text-emerald-800,
708
+ .colorful .text-emerald-400 {
709
+ color: #3d9e88 !important;
710
+ }
711
+
712
+ .colorful .bg-emerald-50\/50,
713
+ .colorful .bg-emerald-900\/10 {
714
+ background-color: rgba(115, 218, 202, 0.06) !important;
715
+ }
716
+
717
+ /* ── Toolbar buttons ── */
718
+ .colorful .toolbar-button:hover {
719
+ background-color: rgba(200, 195, 230, 0.25) !important;
720
+ }
721
+
722
+ .colorful .toolbar-button.active {
723
+ background-color: var(--cf-purple) !important;
724
+ color: #fff !important;
725
+ }
726
+
727
+ /* ── Status bar (bottom): warm tint ── */
728
+ .colorful .border-t.bg-white,
729
+ .colorful .border-t.dark\:bg-black {
730
+ background: rgba(240, 225, 210, 0.50) !important;
731
+ backdrop-filter: blur(16px) saturate(1.3) !important;
732
+ -webkit-backdrop-filter: blur(16px) saturate(1.3) !important;
733
+ }
734
+
735
+ /* ── Drop overlay ── */
736
+ .colorful .bg-\[\#9ece6a\]\/10 {
737
+ background-color: rgba(115, 218, 202, 0.10) !important;
738
+ }
739
+
740
+ .colorful .border-\[\#9ece6a\] {
741
+ border-color: var(--cf-teal) !important;
742
+ }
743
+
744
+ .colorful .text-\[\#9ece6a\] {
745
+ color: var(--cf-teal) !important;
746
+ }
747
+
748
+ /* ── Popover / dropdown menus: glass ── */
749
+ .colorful [data-radix-popper-content-wrapper] [role="menu"],
750
+ .colorful [data-radix-popper-content-wrapper] [role="listbox"],
751
+ .colorful [data-radix-popper-content-wrapper] [data-radix-collection-item] {
752
+ background: var(--cf-glass-strong) !important;
753
+ backdrop-filter: blur(20px) saturate(1.4) !important;
754
+ -webkit-backdrop-filter: blur(20px) saturate(1.4) !important;
755
+ }
756
+
757
+ /* ═══════════════════════════════════════════════════════════════════════════
758
+ COLORFUL — Component-specific colour variations
759
+ Each section gets a subtle tint from the Tokyo palette, mapped to its
760
+ semantic role. Tasteful, not overwhelming.
761
+ ═══════════════════════════════════════════════════════════════════════════ */
762
+
763
+ /* ── Section headers in hierarchy: cool lavender bg ── */
764
+ .colorful .bg-zinc-100.border-b,
765
+ .colorful .bg-zinc-900.border-b {
766
+ background-color: rgba(200, 195, 230, 0.30) !important;
767
+ }
768
+
769
+ /* ── Section headers in properties: warm peach bg ── */
770
+ .colorful .bg-zinc-50.border-b {
771
+ background-color: rgba(240, 220, 195, 0.35) !important;
772
+ }
773
+
774
+ /* Properties section header text: warmer dark tone */
775
+ .colorful .bg-zinc-50 .text-zinc-700,
776
+ .colorful .bg-zinc-50 .text-zinc-600 {
777
+ color: #5a4a3a !important;
778
+ }
779
+
780
+ /* ── FILE INFORMATION / STATISTICS / PROJECT INFO section containers ── */
781
+ /* Alternate warm tints for each section in the properties panel */
782
+ .colorful .bg-zinc-50.dark\:bg-zinc-900\/50 {
783
+ background-color: rgba(240, 220, 195, 0.30) !important;
784
+ }
785
+
786
+ /* ── LOCATION section: teal/green tint (geography = nature) ── */
787
+ .colorful [class*="border-emerald"] {
788
+ border-color: rgba(115, 218, 202, 0.25) !important;
789
+ }
790
+
791
+ /* ── Landing page / empty state — orange-tinted grid world ── */
792
+
793
+ /* Viewport container bg — only the wrapper div, NOT the canvas.
794
+ The canvas has data-viewport="main"; we exclude it so the
795
+ inline aurora gradient isn't overridden by !important. */
796
+ .colorful div[data-viewport=""]:not(:has(canvas)),
797
+ .colorful div[data-viewport]:not([data-viewport="main"]) {
798
+ background-color: rgba(245, 235, 220, 0.60) !important;
799
+ }
800
+
801
+ /* Orange grid only on the empty-state overlay (no model loaded).
802
+ This div only exists when the landing page renders. */
803
+ .colorful [data-viewport] > .absolute.z-10 {
804
+ background-image:
805
+ linear-gradient(rgba(255, 158, 100, 0.08) 1px, transparent 1px),
806
+ linear-gradient(90deg, rgba(255, 158, 100, 0.08) 1px, transparent 1px);
807
+ background-size: 32px 32px;
808
+ }
809
+
810
+ /* Main landing card: warm glass with orange border */
811
+ .colorful .border-zinc-300.dark\:border-\[\#3b4261\] {
812
+ border-color: rgba(255, 158, 100, 0.28) !important;
813
+ background: rgba(255, 248, 240, 0.65) !important;
814
+ backdrop-filter: blur(16px) !important;
815
+ -webkit-backdrop-filter: blur(16px) !important;
816
+ }
817
+
818
+ /* Logo background layer: warm tint */
819
+ .colorful .-rotate-3.border {
820
+ background-color: rgba(255, 225, 195, 0.30) !important;
821
+ border-color: rgba(255, 158, 100, 0.18) !important;
822
+ }
823
+
824
+ /* Feature grid cards (SELECT / FILTER / ANALYZE): warm glass */
825
+ .colorful .bg-zinc-100.dark\:bg-\[\#1f2335\] {
826
+ background-color: rgba(255, 230, 200, 0.35) !important;
827
+ border-color: rgba(255, 158, 100, 0.20) !important;
828
+ }
829
+
830
+ /* Feature card icon boxes */
831
+ .colorful .bg-zinc-100 .bg-white.border,
832
+ .colorful .bg-zinc-100 .dark\:bg-\[\#16161e\].border {
833
+ background-color: rgba(255, 245, 230, 0.55) !important;
834
+ border-color: rgba(255, 158, 100, 0.15) !important;
835
+ }
836
+
837
+ /* Recent files list */
838
+ .colorful .bg-zinc-50.dark\:bg-\[\#1f2335\] {
839
+ background-color: rgba(255, 235, 210, 0.30) !important;
840
+ border-color: rgba(255, 158, 100, 0.15) !important;
841
+ }
842
+
843
+ /* Shortcuts badge */
844
+ .colorful .bg-zinc-100.dark\:bg-\[\#1f2335\].border.border-zinc-300 {
845
+ background-color: rgba(255, 230, 200, 0.35) !important;
846
+ border-color: rgba(255, 158, 100, 0.20) !important;
847
+ }
848
+
849
+ /* "Open .ifc file" hover */
850
+ .colorful .hover\:border-primary:hover {
851
+ border-color: var(--cf-orange) !important;
852
+ }
853
+
854
+ .colorful .hover\:text-primary:hover {
855
+ color: var(--cf-orange) !important;
856
+ }
857
+
858
+ /* Empty state dashed borders: orange */
859
+ .colorful .border-dashed {
860
+ border-color: rgba(255, 158, 100, 0.35) !important;
861
+ }
862
+
863
+ /* Empty state icon boxes (NO MODEL / NO SELECTION) */
864
+ .colorful .border-dashed.border-2 {
865
+ border-color: rgba(255, 158, 100, 0.30) !important;
866
+ background-color: rgba(255, 235, 210, 0.25) !important;
867
+ }
868
+
869
+ /* ── Hierarchy SPATIAL/CLASS/TYPE toggle ── */
870
+ /* Outline buttons: lavender tint */
871
+ .colorful [data-variant="outline"] {
872
+ border-color: rgba(157, 124, 216, 0.22) !important;
873
+ }
874
+
875
+ .colorful [data-variant="outline"]:hover {
876
+ background-color: rgba(200, 195, 230, 0.22) !important;
877
+ }
878
+
879
+ /* ── Properties panel collapsible sections hover (right panel only) ── */
880
+ .colorful [id="right-panel"] .hover\:bg-zinc-50:hover,
881
+ .colorful [id="right-panel"] .hover\:bg-zinc-900:hover {
882
+ background-color: rgba(240, 220, 195, 0.25) !important;
883
+ }
884
+
885
+ /* ── Properties subsection backgrounds ── */
886
+ .colorful .bg-zinc-50\/50,
887
+ .colorful .dark\:bg-zinc-900\/50 {
888
+ background-color: rgba(240, 225, 205, 0.25) !important;
889
+ }
890
+
891
+ /* ── Tooltip: dark, sharp ── */
892
+ .colorful [role="tooltip"] {
893
+ background: rgba(26, 27, 46, 0.90) !important;
894
+ color: #e0e6f0 !important;
895
+ backdrop-filter: blur(8px) !important;
896
+ -webkit-backdrop-filter: blur(8px) !important;
897
+ }
898
+
415
899
  /* Fix Radix ScrollArea viewport inner div: by default it uses display:table
416
900
  which expands to content width, preventing text truncation in flex children.
417
901
  Force block layout so width constraints cascade correctly. */
@@ -528,13 +1012,3 @@ body {
528
1012
  border-radius: 0.25rem;
529
1013
  animation: skeleton-pulse 2s ease-in-out infinite;
530
1014
  }
531
-
532
- /* Clerk overlays (billing/sidebar/modal) should always stack above app overlays. */
533
- .cl-portalRoot,
534
- .cl-modalBackdrop,
535
- .cl-modalContent,
536
- .cl-drawerRoot,
537
- .cl-drawerBackdrop,
538
- .cl-drawerContent {
539
- z-index: 100000 !important;
540
- }
@@ -26,10 +26,8 @@ interface ResolvedDesktopEntitlement {
26
26
 
27
27
  export function resolveDesktopEntitlement(options: ResolveDesktopEntitlementOptions): ResolvedDesktopEntitlement {
28
28
  // NOTE: `token`, `has`, `publicMetadata`, and `now` are accepted but
29
- // intentionally unused in this initial implementation. Full Clerk-based
30
- // entitlement resolution (plan checks via `has`, metadata-driven overrides,
31
- // token validation, and time-based expiry via `now`) will be wired up once
32
- // the Clerk integration is available in the desktop shell.
29
+ // intentionally unused in this implementation. They are kept for future
30
+ // extensibility if a desktop-specific entitlement provider is added.
33
31
  const { userId } = options;
34
32
  const entitlement: DesktopEntitlement = {
35
33
  ...getDefaultDesktopEntitlement(),
@@ -70,6 +70,7 @@ export async function createCesiumBridge(
70
70
  mapConversion: MapConversion,
71
71
  projectedCRS: ProjectedCRS,
72
72
  coordinateInfo?: CoordinateInfo,
73
+ lengthUnitScale = 1,
73
74
  ): Promise<CesiumBridge | null> {
74
75
  const projDef = await resolveProjection(projectedCRS);
75
76
  if (!projDef) return null;
@@ -98,9 +99,13 @@ export async function createCesiumBridge(
98
99
  const oIfcX = owx;
99
100
  const oIfcY = -owz;
100
101
  const oIfcZ = owy;
101
- const oEasting = mapConversion.eastings + hScale * (absc * oIfcX - ordi * oIfcY);
102
- const oNorthing = mapConversion.northings + hScale * (ordi * oIfcX + absc * oIfcY);
103
- const oHeight = mapConversion.orthogonalHeight + oIfcZ;
102
+ // Geometry coordinates (oIfcX/Y/Z) are already in metres (the geometry engine
103
+ // converts from the IFC file's native unit during extraction). MapConversion
104
+ // values use the unit from IfcProjectedCRS.MapUnit; fall back to project unit.
105
+ const mapScale = projectedCRS.mapUnitScale ?? lengthUnitScale;
106
+ const oEasting = mapConversion.eastings * mapScale + hScale * (absc * oIfcX - ordi * oIfcY);
107
+ const oNorthing = mapConversion.northings * mapScale + hScale * (ordi * oIfcX + absc * oIfcY);
108
+ const oHeight = mapConversion.orthogonalHeight * mapScale + oIfcZ;
104
109
 
105
110
  let originLon: number, originLat: number;
106
111
  try {
@@ -131,6 +136,8 @@ export async function createCesiumBridge(
131
136
  // So M = [hScale*absc, 0, hScale*ordi ]
132
137
  // [hScale*ordi, 0, -hScale*absc ]
133
138
  // [0, 1, 0 ]
139
+ // Viewer-space deltas are already in metres (geometry engine converts during
140
+ // extraction), so no lengthUnitScale needed here.
134
141
  const m00 = hScale * absc; // east from vx
135
142
  const m01 = 0; // east from vy
136
143
  const m02 = hScale * ordi; // east from vz
@@ -138,7 +145,7 @@ export async function createCesiumBridge(
138
145
  const m11 = 0; // north from vy
139
146
  const m12 = -hScale * absc; // north from vz
140
147
  const m20 = 0; // up from vx
141
- const m21 = 1; // up from vy
148
+ const m21 = 1; // up from vy (vertical = viewer Y, already metres)
142
149
  const m22 = 0; // up from vz
143
150
 
144
151
  // ── Cache for ECEF objects ──
@@ -288,9 +295,10 @@ export async function createCesiumBridge(
288
295
  const ifcX = wx;
289
296
  const ifcY = -wz;
290
297
  const ifcZ = wy;
291
- const easting = mapConversion.eastings + hScale * (absc * ifcX - ordi * ifcY);
292
- const northing = mapConversion.northings + hScale * (ordi * ifcX + absc * ifcY);
293
- const height = mapConversion.orthogonalHeight + ifcZ;
298
+ // Viewer coords (ifcX/Y/Z) are already in metres; only MapConversion values need scaling
299
+ const easting = mapConversion.eastings * mapScale + hScale * (absc * ifcX - ordi * ifcY);
300
+ const northing = mapConversion.northings * mapScale + hScale * (ordi * ifcX + absc * ifcY);
301
+ const height = mapConversion.orthogonalHeight * mapScale + ifcZ;
294
302
  try {
295
303
  const [lon, lat] = proj4(projDef!, 'WGS84', [easting, northing]);
296
304
  if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
@@ -0,0 +1,73 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ import { describe, it } from 'node:test';
6
+ import assert from 'node:assert';
7
+
8
+ import { inferMapUnitScale, mergeMapConversion, mergeProjectedCRS } from './effective-georef.js';
9
+ import type { MapConversion, ProjectedCRS } from '@ifc-lite/parser';
10
+
11
+ describe('effective georeferencing', () => {
12
+ it('recomputes map unit scale when the edited MapUnit changes', () => {
13
+ const original: ProjectedCRS = {
14
+ id: 1,
15
+ name: 'EPSG:28992',
16
+ mapUnit: 'METRE',
17
+ mapUnitScale: 1,
18
+ };
19
+
20
+ const merged = mergeProjectedCRS(original, { mapUnit: 'US SURVEY FOOT' }, 1);
21
+
22
+ assert.strictEqual(merged?.mapUnit, 'US SURVEY FOOT');
23
+ assert.strictEqual(merged?.mapUnitScale, 0.3048006096);
24
+ });
25
+
26
+ it('preserves the extracted map unit scale when MapUnit was not edited', () => {
27
+ const original: ProjectedCRS = {
28
+ id: 1,
29
+ name: 'EPSG:1234',
30
+ mapUnit: 'CUSTOM',
31
+ mapUnitScale: 2.5,
32
+ };
33
+
34
+ const merged = mergeProjectedCRS(original, { description: 'Edited CRS' }, 1);
35
+
36
+ assert.strictEqual(merged?.description, 'Edited CRS');
37
+ assert.strictEqual(merged?.mapUnitScale, 2.5);
38
+ });
39
+
40
+ it('overlays edited IfcMapConversion fields without dropping original rotation and scale', () => {
41
+ const original: MapConversion = {
42
+ id: 2,
43
+ sourceCRS: 10,
44
+ targetCRS: 11,
45
+ eastings: 100,
46
+ northings: 200,
47
+ orthogonalHeight: 5,
48
+ xAxisAbscissa: 0,
49
+ xAxisOrdinate: 1,
50
+ scale: 0.9999,
51
+ };
52
+
53
+ const merged = mergeMapConversion(original, { eastings: 150, orthogonalHeight: 9 });
54
+
55
+ assert.deepStrictEqual(merged, {
56
+ id: 2,
57
+ sourceCRS: 10,
58
+ targetCRS: 11,
59
+ eastings: 150,
60
+ northings: 200,
61
+ orthogonalHeight: 9,
62
+ xAxisAbscissa: 0,
63
+ xAxisOrdinate: 1,
64
+ scale: 0.9999,
65
+ });
66
+ });
67
+
68
+ it('infers common IFC map unit names', () => {
69
+ assert.strictEqual(inferMapUnitScale('FOOT'), 0.3048);
70
+ assert.strictEqual(inferMapUnitScale('METRE'), 1);
71
+ assert.strictEqual(inferMapUnitScale('MILLIMETRE'), 0.001);
72
+ });
73
+ });