1import { 2 ModelCacheService 3} from "./chunk-EYHDXLP4.js"; 4import { 5 PersonManagementModal, 6 isEditableKeyboardEvent 7} from "./chunk-LVTFBYQP.js"; 8import { 9 BuildingLabelDataBuilder, 10 ModelInstance, 11 require_browser 12} from "./chunk-MFFXGQSG.js"; 13import { 14 AmbientLight, 15 Box3, 16 BufferAttribute, 17 BufferGeometry, 18 CircleGeometry, 19 Clock, 20 Color, 21 DEFAULT_MODEL_3D_TRANSFORM, 22 DirectionalLight, 23 DoubleSide, 24 GROUND_PATCH_COLORS, 25 Group, 26 Line, 27 LineBasicMaterial, 28 LineDashedMaterial, 29 MathUtils, 30 Matrix4, 31 Mesh, 32 MeshBasicMaterial, 33 Model3DConfigBuilderService, 34 Object3D, 35 OrthographicCamera, 36 Plane, 37 PlaneGeometry, 38 ROOM_TYPE_COLORS, 39 Raycaster, 40 RingGeometry, 41 Scene, 42 ShaderMaterial, 43 Shape, 44 ShapeGeometry, 45 SphereGeometry, 46 TextureLoader, 47 Vector2, 48 Vector3, 49 WebGLRenderer, 50 three_module_exports 51} from "./chunk-RB7A2ZLZ.js"; 52import { 53 Clipboard 54} from "./chunk-Q4KBCFNF.js"; 55import "./chunk-JKLLDOQG.js"; 56import "./chunk-33EQSEY6.js"; 57import { 58 UnsavedChangesService 59} from "./chunk-CRXNTBFP.js"; 60import { 61 NavigationDestinationResolverService, 62 Share 63} from "./chunk-4AZFRFV3.js"; 64import "./chunk-2B6XAQ3B.js"; 65import "./chunk-CZMIP4ZQ.js"; 66import "./chunk-ODH7QDZO.js"; 67import "./chunk-4UEQF233.js"; 68import { 69 ApiService 70} from "./chunk-UN7A4OF7.js"; 71import "./chunk-K52XM66R.js"; 72import { 73 PointUtils, 74 createGPSTransform, 75 transformGPSToMap 76} from "./chunk-PYYASMRY.js"; 77import "./chunk-63DLEHZF.js"; 78import "./chunk-PFRFKH6U.js"; 79import "./chunk-UZKEIBGO.js"; 80import "./chunk-2E4THSOE.js"; 81import "./chunk-JISY7VJD.js"; 82import { 83 Geolocation 84} from "./chunk-KP4RHBP7.js"; 85import { 86 SettingsService 87} from "./chunk-QI7JV4VS.js"; 88import "./chunk-S7OELOHR.js"; 89import "./chunk-OTQ6GTAJ.js"; 90import { 91 AlertController, 92 IonBackdrop, 93 IonButton, 94 IonButtons, 95 IonContent, 96 IonHeader, 97 IonLabel, 98 IonRange, 99 IonSelect, 100 IonSelectModal, 101 IonSelectOption, 102 IonTitle, 103 IonToolbar, 104 MenuController, 105 ModalController 106} from "./chunk-RLZ522BW.js"; 107import { 108 BuildingModel, 109 DatabaseService, 110 EntityType, 111 FloorCornerModel, 112 FloorModel, 113 GroundPatchCornerModel, 114 GroundPatchModel, 115 NodeConnectionModel, 116 NodeModel, 117 RoomCornerModel, 118 RoomModel 119} from "./chunk-RLJCY5HS.js"; 120import { 121 DeepLinkService 122} from "./chunk-4ATQDGKC.js"; 123import { 124 ToastService 125} from "./chunk-GR5DEE5Y.js"; 126import { 127 TranslateModule, 128 TranslatePipe 129} from "./chunk-O5XYYQCR.js"; 130import { 131 Building, 132 Floor, 133 FloorCorner, 134 GroundPatch, 135 GroundPatchCorner, 136 GroundPatchType, 137 Node, 138 NodeConnection, 139 NodeType, 140 Person, 141 PersonRoom, 142 Room, 143 RoomCorner, 144 RoomType 145} from "./chunk-QGEJYSW7.js"; 146import "./chunk-MAIAEXGG.js"; 147import "./chunk-L4O6SE3A.js"; 148import "./chunk-INK5THDW.js"; 149import { 150 NgIcon, 151 lucideArrowDownUp, 152 lucideArrowUpDown, 153 lucideArrowUpFromLine, 154 lucideBox, 155 lucideBriefcase, 156 lucideBuilding, 157 lucideBuilding2, 158 lucideChartBar, 159 lucideCheck, 160 lucideChevronDown, 161 lucideChevronLeft, 162 lucideChevronRight, 163 lucideCircle, 164 lucideCircleCheck, 165 lucideCircleDot, 166 lucideCircleX, 167 lucideClipboard, 168 lucideCrosshair, 169 lucideDoorOpen, 170 lucideEllipsisVertical, 171 lucideExternalLink, 172 lucideFocus, 173 lucideFolderTree, 174 lucideGrid3x3, 175 lucideImage, 176 lucideInfo, 177 lucideLayers, 178 lucideLink2, 179 lucideLoaderCircle, 180 lucideLocate, 181 lucideLockKeyhole, 182 lucideMapPin, 183 lucideMaximize, 184 lucideMaximize2, 185 lucideMenu, 186 lucideMountain, 187 lucideMove, 188 lucideNavigation, 189 lucideNetwork, 190 lucidePenTool, 191 lucidePencil, 192 lucidePlus, 193 lucideRadio, 194 lucideRedo2, 195 lucideRotateCcw, 196 lucideRuler, 197 lucideSave, 198 lucideScan, 199 lucideSearch, 200 lucideSearchX, 201 lucideSettings, 202 lucideSettings2, 203 lucideShare2, 204 lucideShowerHead, 205 lucideSquare, 206 lucideSun, 207 lucideTag, 208 lucideTarget, 209 lucideTrash2, 210 lucideTreeDeciduous, 211 lucideTrees, 212 lucideTriangleAlert, 213 lucideUndo2, 214 lucideUserPlus, 215 lucideUsers, 216 lucideUtensils, 217 lucideWaves, 218 lucideX, 219 provideIcons 220} from "./chunk-GMPIA5L4.js"; 221import { 222 ActivatedRoute, 223 ChangeDetectionStrategy, 224 ChangeDetectorRef, 225 CheckboxControlValueAccessor, 226 CommonModule, 227 Component, 228 ConnectableObservable, 229 DOCUMENT, 230 DecimalPipe, 231 DefaultValueAccessor, 232 Directive, 233 ElementRef, 234 EventEmitter, 235 FormsModule, 236 HostListener, 237 Inject, 238 Injectable, 239 InjectionToken, 240 Injector, 241 Input, 242 IterableDiffers, 243 MaxValidator, 244 MinValidator, 245 NgClass, 246 NgControlStatus, 247 NgModel, 248 NgModule, 249 NgSelectOption, 250 NgZone, 251 NumberValueAccessor, 252 Observable, 253 Optional, 254 Output, 255 PLATFORM_ID, 256 Renderer2, 257 RendererFactory2, 258 SelectControlValueAccessor, 259 Subject, 260 Subscription, 261 TemplateRef, 262 ViewChild, 263 ViewContainerRef, 264 ViewEncapsulation, 265 afterNextRender, 266 animationFrameScheduler, 267 asapScheduler, 268 auditTime, 269 booleanAttribute, 270 computed, 271 distinctUntilChanged, 272 effect, 273 filter,
274 firstValueFrom, 275 forwardRef, 276 inject, 277 isObservable, 278 isPlatformBrowser, 279 of, 280 pairwise, 281 setClassMetadata, 282 shareReplay, 283 signal, 284 startWith, 285 switchMap, 286 takeUntil, 287 ɵNgSelectMultipleOption, 288 ɵsetClassDebugInfo, 289 ɵɵInheritDefinitionFeature, 290 ɵɵNgOnChangesFeature, 291 ɵɵProvidersFeature, 292 ɵɵadvance, 293 ɵɵattribute, 294 ɵɵclassProp, 295 ɵɵconditional, 296 ɵɵdefineComponent, 297 ɵɵdefineDirective, 298 ɵɵdefineInjectable, 299 ɵɵdefineInjector, 300 ɵɵdefineNgModule, 301 ɵɵdirectiveInject, 302 ɵɵelement, 303 ɵɵelementContainer, 304 ɵɵelementContainerEnd, 305 ɵɵelementContainerStart, 306 ɵɵelementEnd, 307 ɵɵelementStart, 308 ɵɵgetCurrentView, 309 ɵɵinject, 310 ɵɵlistener, 311 ɵɵloadQuery, 312 ɵɵnextContext, 313 ɵɵpipe, 314 ɵɵpipeBind1, 315 ɵɵpipeBind2, 316 ɵɵprojection, 317 ɵɵprojectionDef, 318 ɵɵproperty, 319 ɵɵpropertyInterpolate1, 320 ɵɵpureFunction1, 321 ɵɵqueryRefresh, 322 ɵɵrepeater, 323 ɵɵrepeaterCreate, 324 ɵɵrepeaterTrackByIdentity, 325 ɵɵresetView, 326 ɵɵresolveWindow, 327 ɵɵrestoreView, 328 ɵɵsanitizeUrl, 329 ɵɵstyleProp, 330 ɵɵtemplate, 331 ɵɵtext, 332 ɵɵtextInterpolate, 333 ɵɵtextInterpolate1, 334 ɵɵtextInterpolate2, 335 ɵɵtwoWayBindingSet, 336 ɵɵtwoWayListener, 337 ɵɵtwoWayProperty, 338 ɵɵviewQuery 339} from "./chunk-UQ6LLOJ6.js"; 340import "./chunk-BD3NMR2S.js"; 341import "./chunk-6HPUB4U6.js"; 342import "./chunk-JTG2GCS6.js"; 343import "./chunk-WGFJL45C.js"; 344import "./chunk-7WVISTDL.js"; 345import "./chunk-2S57T3GS.js"; 346import "./chunk-R4QHACMO.js"; 347import "./chunk-4MEJEBWM.js"; 348import "./chunk-RIBVNXHQ.js"; 349import "./chunk-Z42OVMMO.js"; 350import "./chunk-O5S42IXR.js"; 351import "./chunk-QBR2RTFG.js"; 352import "./chunk-MIOLVGSM.js"; 353import "./chunk-V3YFOG6Q.js"; 354import "./chunk-LPMBLWVS.js"; 355import "./chunk-5LKHIIQE.js"; 356import "./chunk-ITE2OHLM.js"; 357import "./chunk-C46CB72A.js"; 358import "./chunk-SRPSML5O.js"; 359import "./chunk-NRSVX4KN.js"; 360import "./chunk-T4GYAOTA.js"; 361import "./chunk-2VHYZHLL.js"; 362import "./chunk-S6LHOZ5A.js"; 363import "./chunk-Y6HZOH3Q.js"; 364import "./chunk-TWQHADEW.js"; 365import "./chunk-3J4L7S6G.js"; 366import "./chunk-IL42VKT6.js"; 367import "./chunk-LONO5O5R.js"; 368import "./chunk-KFBF5L4X.js"; 369import "./chunk-PWC4K6CW.js"; 370import "./chunk-R6JLIESI.js"; 371import "./chunk-UG6K57PI.js"; 372import "./chunk-UR6VIRNL.js"; 373import "./chunk-OJO5RD3M.js"; 374import { 375 __async, 376 __publicField, 377 __spreadProps, 378 __spreadValues, 379 __toESM 380} from "./chunk-CO64IJQE.js"; 381
382// src/app/shared/components/toolbar-container/toolbar-container.component.ts 383var _c0 = ["*"]; 384var _ToolbarContainerComponent = class _ToolbarContainerComponent { 385 constructor() { 386 this.position = "right"; 387 } 388}; 389_ToolbarContainerComponent.\u0275fac = function ToolbarContainerComponent_Factory(__ngFactoryType__) { 390 return new (__ngFactoryType__ || _ToolbarContainerComponent)(); 391}; 392_ToolbarContainerComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _ToolbarContainerComponent, selectors: [["app-toolbar-container"]], inputs: { position: "position" }, ngContentSelectors: _c0, decls: 2, vars: 18, consts: [[1, "tool-palette-container"]], template: function ToolbarContainerComponent_Template(rf, ctx) { 393 if (rf & 1) { 394 \u0275\u0275projectionDef(); 395 \u0275\u0275elementStart(0, "div", 0); 396 \u0275\u0275projection(1); 397 \u0275\u0275elementEnd(); 398 } 399 if (rf & 2) { 400 \u0275\u0275classProp("tool-palette-top-left", ctx.position === "top-left")("tool-palette-top", ctx.position === "top")("tool-palette-top-right", ctx.position === "top-right")("tool-palette-left", ctx.position === "left")("tool-palette-center", ctx.position === "center")("tool-palette-right", ctx.position === "right")("tool-palette-bottom-left", ctx.position === "bottom-left")("tool-palette-bottom", ctx.position === "bottom")("tool-palette-bottom-right", ctx.position === "bottom-right"); 401 } 402}, styles: ["\n\n.tool-palette-container[_ngcontent-%COMP%] {\n --tool-button-size: 40px;\n --tool-button-size-mobile: 48px;\n --tool-icon-size: 1.3em;\n --tool-icon-size-mobile: 1.4em;\n --tool-icon-size-compact: 14px;\n --tool-active-border: 2px;\n --tool-hotkey-offset: 3em;\n --tool-hotkey-offset-mobile: 3.5em;\n --tool-hotkey-height: 1.5em;\n --tool-hotkey-font-size: 0.7em;\n --tool-border-radius: 12px;\n --tool-border-radius-small: 6px;\n --tool-spacing: 15px;\n --tool-spacing-small: 4px;\n --tool-gap: 8px;\n position: absolute;\n z-index: 100;\n}\n.tool-palette-container.tool-palette-top-left[_ngcontent-%COMP%] {\n left: var(--toolbar-left, var(--tool-spacing));\n top: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-top-right[_ngcontent-%COMP%] {\n right: var(--tool-spacing);\n top: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-bottom-left[_ngcontent-%COMP%] {\n left: var(--tool-spacing);\n bottom: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-bottom-right[_ngcontent-%COMP%] {\n right: var(--tool-spacing);\n bottom: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-top[_ngcontent-%COMP%] {\n top: var(--tool-spacing);\n left: 50%;\n transform: translateX(-50%);\n}\n.tool-palette-container.tool-palette-bottom[_ngcontent-%COMP%] {\n bottom: var(--tool-spacing);\n left: 50%;\n transform: translateX(-50%);\n}\n.tool-palette-container.tool-palette-left[_ngcontent-%COMP%] {\n left: var(--tool-spacing);\n top: 50%;\n transform: translateY(-50%);\n}\n.tool-palette-container.tool-palette-right[_ngcontent-%COMP%] {\n right: var(--tool-spacing);\n top: 50%;\n transform: translateY(-50%);\n}\n.tool-palette-container.tool-palette-center[_ngcontent-%COMP%] {\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n}\n.tool-palette-list[_ngcontent-%COMP%] {\n background: var(--tool-palette-bg);\n border-radius: var(--tool-border-radius);\n box-shadow: var(--tool-palette-shadow);\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light);\n list-style: none;\n padding: 0;\n margin: 0;\n overflow: visible;\n}\n.tool-palette-list[_ngcontent-%COMP%] + .tool-palette-list[_ngcontent-%COMP%] {\n margin-top: var(--tool-spacing);\n}\n.tool-palette-item[_ngcontent-%COMP%] {\n border-top: 1px solid var(--tool-palette-border);\n transition: all 0.2s ease;\n position: relative;\n z-index: 1;\n}\n.tool-palette-item[_ngcontent-%COMP%]:first-child {\n border-top: none;\n}\n.tool-palette-item[_ngcontent-%COMP%]:hover {\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n z-index: 10;\n}\n.tool-palette-item.tool-palette-active[_ngcontent-%COMP%] {\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n border-left: var(--tool-active-border) solid var(--ion-color-primary);\n}\n.tool-palette-item.tool-palette-active[_ngcontent-%COMP%] .tool-palette-hotkey[_ngcontent-%COMP%] {\n font-size: 0.85em;\n}\n.tool-palette-item.tool-palette-active[_ngcontent-%COMP%] .tool-palette-button[_ngcontent-%COMP%] {\n color: var(--ion-color-primary);\n}\n.tool-palette-item.tool-palette-active[_ngcontent-%COMP%] .tool-palette-button[_ngcontent-%COMP%]:hover {\n
402color: var(--ion-color-primary-shade);\n}\n.tool-palette-item[_ngcontent-%COMP%]:last-child {\n border-bottom-left-radius: var(--tool-border-radius);\n border-bottom-right-radius: var(--tool-border-radius);\n}\n.tool-palette-item[_ngcontent-%COMP%]:first-child {\n border-top-left-radius: var(--tool-border-radius);\n border-top-right-radius: var(--tool-border-radius);\n}\n.tool-palette-button[_ngcontent-%COMP%] {\n width: var(--tool-button-size);\n height: var(--tool-button-size);\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n cursor: pointer;\n color: var(--ion-text-color);\n transition: all 0.2s ease;\n}\n.tool-palette-button[_ngcontent-%COMP%]:hover {\n color: var(--tool-palette-hover-primary);\n transform: scale(1.05);\n}\n.tool-palette-button[_ngcontent-%COMP%]:active {\n transform: scale(0.95);\n}\n.tool-palette-button.active[_ngcontent-%COMP%] {\n color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n border-left: var(--tool-active-border) solid var(--ion-color-primary);\n}\n.tool-palette-button.active[_ngcontent-%COMP%]:hover {\n color: var(--ion-color-primary-shade);\n}\n.tool-palette-item[_ngcontent-%COMP%]:first-child .tool-palette-button.active[_ngcontent-%COMP%] {\n border-top-left-radius: var(--tool-border-radius);\n}\n.tool-palette-item[_ngcontent-%COMP%]:last-child .tool-palette-button.active[_ngcontent-%COMP%] {\n border-bottom-left-radius: var(--tool-border-radius);\n}\n.tool-palette-button.disabled[_ngcontent-%COMP%] {\n opacity: 0.4;\n cursor: not-allowed;\n pointer-events: auto;\n}\n.tool-palette-button.disabled[_ngcontent-%COMP%]:hover {\n color: var(--ion-text-color);\n transform: none;\n}\n.tool-palette-button.disabled[_ngcontent-%COMP%]:active {\n transform: none;\n}\n.tool-palette-button.loading[_ngcontent-%COMP%] {\n cursor: wait;\n pointer-events: auto;\n}\n.tool-palette-button.loading[_ngcontent-%COMP%]:hover {\n transform: none;\n}\n.tool-palette-button.loading[_ngcontent-%COMP%]:active {\n transform: none;\n}\n.tool-palette-button[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n font-size: var(--tool-icon-size);\n}\n.tool-palette-button[_ngcontent-%COMP%] ng-icon.spin[_ngcontent-%COMP%] {\n animation: _ngcontent-%COMP%_spin 1s linear infinite;\n}\n.tool-palette-button.compact[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n font-size: var(--tool-icon-size-compact);\n}\n@keyframes _ngcontent-%COMP%_spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n.tool-palette-hotkey[_ngcontent-%COMP%] {\n position: absolute;\n background: var(--tool-palette-hotkey-bg);\n border: 1px solid var(--tool-palette-border-light);\n border-radius: var(--tool-border-radius-small);\n right: var(--tool-hotkey-offset);\n min-width: 2em;\n height: var(--tool-hotkey-height);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 0.5em;\n font-size: var(--tool-hotkey-font-size);\n font-weight: 600;\n color: var(--tool-palette-hotkey-text);\n box-shadow: 0 1px 3px var(--shadow-color-light);\n -webkit-backdrop-filter: blur(5px);\n backdrop-filter: blur(5px);\n white-space: nowrap;\n margin-left: var(--tool-spacing-small);\n}\n.tool-palette-top-left[_ngcontent-%COMP%] .tool-palette-hotkey[_ngcontent-%COMP%], \n.tool-palette-left[_ngcontent-%COMP%] .tool-palette-hotkey[_ngcontent-%COMP%], \n.tool-palette-bottom-left[_ngcontent-%COMP%] .tool-palette-hotkey[_ngcontent-%COMP%] {\n right: auto;\n left: var(--tool-hotkey-offset);\n margin-left: 0;\n margin-right: var(--tool-spacing-small);\n}\n.tool-palette-active[_ngcontent-%COMP%] {\n color: white;\n box-shadow: 0 0 0 2px var(--tool-palette-bg), 0 2px 6px var(--tool-palette-active-bg, rgba(255, 0, 0, 0.3));\n font-size: 0.7em;\n}\n@media (prefers-reduced-motion: no-preference) {\n .tool-palette-active[_ngcontent-%COMP%] {\n animation: _ngcontent-%COMP%_activeGlow 2s ease-in-out infinite;\n }\n}\n@keyframes _ngcontent-%COMP%_activeGlow {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.7;\n }\n}\n.tool-palette-button[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid var(--ion-color-primary);\n outline-offset: 2px;\n border-radius: var(--tool-border-radius-small);\n}\n@media (max-width: 768px) {\n .tool-palette-container[_ngcontent-%COMP%] {\n --tool-button-size: var(--tool-button-size-mobile);\n --tool-icon-size: var(--tool-icon-size-mobile);\n --tool-hotkey-offset: var(--tool-hotkey-offset-mobile);\n --tool-icon-size-compact: 16px;\n --tool-spacing: 20px;\n }\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] {\n border-top: 1px solid var(--tool-palette-border-light);\n margin-top: var(--tool-gap);\n padding-top: var(--tool-gap);\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] .tool-palette-button[_ngcontent-%COMP%] {\n min-height: var(--tool-button-size);\n transition: all 0.2s ease;\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] .tool-palette-button.scale-coefficient[_ng
402content-%COMP%] {\n font-size: 12px;\n font-weight: bold;\n color: var(--tool-palette-scale-color);\n display: flex;\n align-items: center;\n gap: var(--tool-spacing-small);\n border-radius: var(--tool-border-radius-small);\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] .tool-palette-button.scale-coefficient[_ngcontent-%COMP%]:hover {\n color: var(--tool-palette-scale-hover);\n background-color: rgba(255, 152, 0, 0.1);\n transform: scale(1.02);\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] .tool-palette-button.scale-coefficient[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n font-size: 14px;\n}\n.tool-palette-item[_ngcontent-%COMP%] ul.actions[_ngcontent-%COMP%] {\n list-style: none;\n padding: 0;\n margin: 0;\n}\n.tool-palette-item[_ngcontent-%COMP%] ul.actions[_ngcontent-%COMP%] li[_ngcontent-%COMP%] {\n width: var(--tool-button-size);\n height: var(--tool-button-size);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.75em;\n cursor: pointer;\n border-top: 1px solid var(--tool-palette-border-light);\n color: var(--ion-text-color);\n transition: all 0.2s ease;\n}\n.tool-palette-item[_ngcontent-%COMP%] ul.actions[_ngcontent-%COMP%] li[_ngcontent-%COMP%]:hover {\n color: var(--tool-palette-hover-secondary);\n background: rgba(255, 123, 0, 0.05);\n}\n.tool-palette-item[_ngcontent-%COMP%] ul.actions[_ngcontent-%COMP%] li.active[_ngcontent-%COMP%] {\n color: var(--tool-palette-active-bg);\n background: rgba(255, 0, 0, 0.1);\n}"], changeDetection: 0 }); 403var ToolbarContainerComponent = _ToolbarContainerComponent; 404(() => { 405 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(ToolbarContainerComponent, [{ 406 type: Component, 407 args: [{ selector: "app-toolbar-container", standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `<div 408 class="tool-palette-container" 409 [class.tool-palette-top-left]="position === 'top-left'" 410 [class.tool-palette-top]="position === 'top'" 411 [class.tool-palette-top-right]="position === 'top-right'" 412 [class.tool-palette-left]="position === 'left'" 413 [class.tool-palette-center]="position === 'center'" 414 [class.tool-palette-right]="position === 'right'" 415 [class.tool-palette-bottom-left]="position === 'bottom-left'" 416 [class.tool-palette-bottom]="position === 'bottom'" 417 [class.tool-palette-bottom-right]="position === 'bottom-right'" 418> 419 <ng-content /> 420</div> 421`, styles: ["/* src/app/shared/styles/tool-palette.scss */\n.tool-palette-container {\n --tool-button-size: 40px;\n --tool-button-size-mobile: 48px;\n --tool-icon-size: 1.3em;\n --tool-icon-size-mobile: 1.4em;\n --tool-icon-size-compact: 14px;\n --tool-active-border: 2px;\n --tool-hotkey-offset: 3em;\n --tool-hotkey-offset-mobile: 3.5em;\n --tool-hotkey-height: 1.5em;\n --tool-hotkey-font-size: 0.7em;\n --tool-border-radius: 12px;\n --tool-border-radius-small: 6px;\n --tool-spacing: 15px;\n --tool-spacing-small: 4px;\n --tool-gap: 8px;\n position: absolute;\n z-index: 100;\n}\n.tool-palette-container.tool-palette-top-left {\n left: var(--toolbar-left, var(--tool-spacing));\n top: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-top-right {\n right: var(--tool-spacing);\n top: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-bottom-left {\n left: var(--tool-spacing);\n bottom: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-bottom-right {\n right: var(--tool-spacing);\n bottom: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-top {\n top: var(--tool-spacing);\n left: 50%;\n transform: translateX(-50%);\n}\n.tool-palette-container.tool-palette-bottom {\n bottom: var(--tool-spacing);\n left: 50%;\n transform: translateX(-50%);\n}\n.tool-palette-container.tool-palette-left {\n left: var(--tool-spacing);\n top: 50%;\n transform: translateY(-50%);\n}\n.tool-palette-container.tool-palette-right {\n right: var(--tool-spacing);\n top: 50%;\n transform: translateY(-50%);\n}\n.tool-palette-container.tool-palette-center {\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n}\n.tool-palette-list {\n background: var(--tool-palette-bg);\n border-radius: var(--tool-border-radius);\n box-shadow: var(--tool-palette-shadow);\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light);\n list-style: none;\n padding: 0;\n margin: 0;\n overflow: visible;\n}\n.tool-palette-list + .tool-palette-list {\n margin-top: var(--tool-spacing);\n}\n.tool-palette-item {\n border-top: 1px solid var(--tool-palette-border);\n transition: all 0.2s ease;\n position: relative;\n z-index: 1;\n}\n.tool-palette-item:first-child {\n border-top: none;\n}\n.tool-palette-item:hover {\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n z-index: 10;\n}\n.tool-palette-item.tool-palette-active {\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n border-left: var(--tool-active-border) solid var(--ion-color-primary);\n}\n.tool-palette-item.tool-palette-active .tool-palette-hotkey {\n font-size: 0.85em;\n}\n.tool-palette-item.tool-palette-active .tool-palette-button {\n color: var(--ion-color-primary);\n}\n.tool-palette-item.tool-palette-active .tool-palette-button:hover {\n
421color: var(--ion-color-primary-shade);\n}\n.tool-palette-item:last-child {\n border-bottom-left-radius: var(--tool-border-radius);\n border-bottom-right-radius: var(--tool-border-radius);\n}\n.tool-palette-item:first-child {\n border-top-left-radius: var(--tool-border-radius);\n border-top-right-radius: var(--tool-border-radius);\n}\n.tool-palette-button {\n width: var(--tool-button-size);\n height: var(--tool-button-size);\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n cursor: pointer;\n color: var(--ion-text-color);\n transition: all 0.2s ease;\n}\n.tool-palette-button:hover {\n color: var(--tool-palette-hover-primary);\n transform: scale(1.05);\n}\n.tool-palette-button:active {\n transform: scale(0.95);\n}\n.tool-palette-button.active {\n color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n border-left: var(--tool-active-border) solid var(--ion-color-primary);\n}\n.tool-palette-button.active:hover {\n color: var(--ion-color-primary-shade);\n}\n.tool-palette-item:first-child .tool-palette-button.active {\n border-top-left-radius: var(--tool-border-radius);\n}\n.tool-palette-item:last-child .tool-palette-button.active {\n border-bottom-left-radius: var(--tool-border-radius);\n}\n.tool-palette-button.disabled {\n opacity: 0.4;\n cursor: not-allowed;\n pointer-events: auto;\n}\n.tool-palette-button.disabled:hover {\n color: var(--ion-text-color);\n transform: none;\n}\n.tool-palette-button.disabled:active {\n transform: none;\n}\n.tool-palette-button.loading {\n cursor: wait;\n pointer-events: auto;\n}\n.tool-palette-button.loading:hover {\n transform: none;\n}\n.tool-palette-button.loading:active {\n transform: none;\n}\n.tool-palette-button ng-icon {\n font-size: var(--tool-icon-size);\n}\n.tool-palette-button ng-icon.spin {\n animation: spin 1s linear infinite;\n}\n.tool-palette-button.compact ng-icon {\n font-size: var(--tool-icon-size-compact);\n}\n@keyframes spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n.tool-palette-hotkey {\n position: absolute;\n background: var(--tool-palette-hotkey-bg);\n border: 1px solid var(--tool-palette-border-light);\n border-radius: var(--tool-border-radius-small);\n right: var(--tool-hotkey-offset);\n min-width: 2em;\n height: var(--tool-hotkey-height);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 0.5em;\n font-size: var(--tool-hotkey-font-size);\n font-weight: 600;\n color: var(--tool-palette-hotkey-text);\n box-shadow: 0 1px 3px var(--shadow-color-light);\n -webkit-backdrop-filter: blur(5px);\n backdrop-filter: blur(5px);\n white-space: nowrap;\n margin-left: var(--tool-spacing-small);\n}\n.tool-palette-top-left .tool-palette-hotkey,\n.tool-palette-left .tool-palette-hotkey,\n.tool-palette-bottom-left .tool-palette-hotkey {\n right: auto;\n left: var(--tool-hotkey-offset);\n margin-left: 0;\n margin-right: var(--tool-spacing-small);\n}\n.tool-palette-active {\n color: white;\n box-shadow: 0 0 0 2px var(--tool-palette-bg), 0 2px 6px var(--tool-palette-active-bg, rgba(255, 0, 0, 0.3));\n font-size: 0.7em;\n}\n@media (prefers-reduced-motion: no-preference) {\n .tool-palette-active {\n animation: activeGlow 2s ease-in-out infinite;\n }\n}\n@keyframes activeGlow {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.7;\n }\n}\n.tool-palette-button:focus-visible {\n outline: 2px solid var(--ion-color-primary);\n outline-offset: 2px;\n border-radius: var(--tool-border-radius-small);\n}\n@media (max-width: 768px) {\n .tool-palette-container {\n --tool-button-size: var(--tool-button-size-mobile);\n --tool-icon-size: var(--tool-icon-size-mobile);\n --tool-hotkey-offset: var(--tool-hotkey-offset-mobile);\n --tool-icon-size-compact: 16px;\n --tool-spacing: 20px;\n }\n}\n.tool-palette-floor-transform {\n border-top: 1px solid var(--tool-palette-border-light);\n margin-top: var(--tool-gap);\n padding-top: var(--tool-gap);\n}\n.tool-palette-floor-transform .tool-palette-button {\n min-height: var(--tool-button-size);\n transition: all 0.2s ease;\n}\n.tool-palette-floor-transform .tool-palette-button.scale-coefficient {\n font-size: 12px;\n font-weight: bold;\n color: var(--tool-palette-scale-color);\n display: flex;\n align-items: center;\n gap: var(--tool-spacing-small);\n border-radius: var(--tool-border-radius-small);\n}\n.tool-palette-floor-transform .tool-palette-button.scale-coefficient:hover {\n
421color: var(--tool-palette-scale-hover);\n background-color: rgba(255, 152, 0, 0.1);\n transform: scale(1.02);\n}\n.tool-palette-floor-transform .tool-palette-button.scale-coefficient ng-icon {\n font-size: 14px;\n}\n.tool-palette-item ul.actions {\n list-style: none;\n padding: 0;\n margin: 0;\n}\n.tool-palette-item ul.actions li {\n width: var(--tool-button-size);\n height: var(--tool-button-size);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.75em;\n cursor: pointer;\n border-top: 1px solid var(--tool-palette-border-light);\n color: var(--ion-text-color);\n transition: all 0.2s ease;\n}\n.tool-palette-item ul.actions li:hover {\n color: var(--tool-palette-hover-secondary);\n background: rgba(255, 123, 0, 0.05);\n}\n.tool-palette-item ul.actions li.active {\n color: var(--tool-palette-active-bg);\n background: rgba(255, 0, 0, 0.1);\n}\n"] }] 422 }], null, { position: [{ 423 type: Input 424 }] }); 425})(); 426(() => { 427 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(ToolbarContainerComponent, { className: "ToolbarContainerComponent", filePath: "src/app/shared/components/toolbar-container/toolbar-container.component.ts", lineNumber: 21 }); 428})(); 429
430// src/app/shared/components/modal-overlay/modal-overlay.component.ts 431var _c02 = ["*"]; 432var _ModalOverlayComponent = class _ModalOverlayComponent { 433 constructor() { 434 this.closeOnBackdropClick = true; 435 this.backdropClick = new EventEmitter(); 436 } 437 onBackdropClick() { 438 if (this.closeOnBackdropClick) { 439 this.backdropClick.emit(); 440 } 441 } 442}; 443_ModalOverlayComponent.\u0275fac = function ModalOverlayComponent_Factory(__ngFactoryType__) { 444 return new (__ngFactoryType__ || _ModalOverlayComponent)(); 445}; 446_ModalOverlayComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _ModalOverlayComponent, selectors: [["app-modal-overlay"]], inputs: { closeOnBackdropClick: "closeOnBackdropClick" }, outputs: { backdropClick: "backdropClick" }, ngContentSelectors: _c02, decls: 4, vars: 0, consts: [[1, "modal-overlay"], ["tappable", "", 1, "app-backdrop-blur", 3, "ionBackdropTap"], [1, "modal-content"]], template: function ModalOverlayComponent_Template(rf, ctx) { 447 if (rf & 1) { 448 \u0275\u0275projectionDef(); 449 \u0275\u0275elementStart(0, "div", 0)(1, "ion-backdrop", 1); 450 \u0275\u0275listener("ionBackdropTap", function ModalOverlayComponent_Template_ion_backdrop_ionBackdropTap_1_listener() { 451 return ctx.onBackdropClick(); 452 }); 453 \u0275\u0275elementEnd(); 454 \u0275\u0275elementStart(2, "div", 2); 455 \u0275\u0275projection(3); 456 \u0275\u0275elementEnd()(); 457 } 458}, dependencies: [IonBackdrop], styles: ["\n\n.modal-overlay[_ngcontent-%COMP%] {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 1000;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.modal-overlay[_ngcontent-%COMP%] ion-backdrop[_ngcontent-%COMP%] {\n z-index: 1;\n opacity: 1;\n}\n.modal-content[_ngcontent-%COMP%] {\n z-index: 2;\n position: relative;\n margin: 20px;\n animation: _ngcontent-%COMP%_modalEnter 0.2s ease-out;\n}\n@keyframes _ngcontent-%COMP%_modalEnter {\n from {\n opacity: 0;\n transform: scale(0.95);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}"], changeDetection: 0 }); 459var ModalOverlayComponent = _ModalOverlayComponent; 460(() => { 461 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(ModalOverlayComponent, [{ 462 type: Component, 463 args: [{ selector: "app-modal-overlay", standalone: true, imports: [IonBackdrop], changeDetection: ChangeDetectionStrategy.OnPush, template: '<div class="modal-overlay">\n <ion-backdrop tappable class="app-backdrop-blur" (ionBackdropTap)="onBackdropClick()"></ion-backdrop>\n <div class="modal-content">\n <ng-content></ng-content>\n </div>\n</div>\n', styles: ["/* src/app/shared/components/modal-overlay/modal-overlay.component.scss */\n.modal-overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 1000;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.modal-overlay ion-backdrop {\n z-index: 1;\n opacity: 1;\n}\n.modal-content {\n z-index: 2;\n position: relative;\n margin: 20px;\n animation: modalEnter 0.2s ease-out;\n}\n@keyframes modalEnter {\n from {\n opacity: 0;\n transform: scale(0.95);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n"] }] 464 }], null, { closeOnBackdropClick: [{ 465 type: Input 466 }], backdropClick: [{ 467 type: Output 468 }] }); 469})(); 470(() => { 471 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(ModalOverlayComponent, { className: "ModalOverlayComponent", filePath: "src/app/shared/components/modal-overlay/modal-overlay.component.ts", lineNumber: 34 }); 472})(); 473
474// src/app/shared/services/keyboard-shortcut.service.ts 475var _KeyboardShortcutService = class _KeyboardShortcutService { 476 constructor() { 477 this.shortcuts = /* @__PURE__ */ new Map(); 478 this.enabled = true; 479 this.boundHandler = this.handleKeyDown.bind(this); 480 document.addEventListener("keydown", this.boundHandler); 481 } 482 ngOnDestroy() { 483 document.removeEventListener("keydown", this.boundHandler); 484 this.shortcuts.clear(); 485 } 486 register(key, callback, description) { 487 const normalizedKey = this.normalizeKey(key); 488 this.shortcuts.set(normalizedKey, { key, callback, description }); 489 } 490 unregister(key) { 491 const normalizedKey = this.normalizeKey(key); 492 this.shortcuts.delete(normalizedKey); 493 } 494 setEnabled(enabled) { 495 this.enabled = enabled; 496 } 497 isEnabled() { 498 return this.enabled; 499 } 500 handleKeyDown(event) { 501 if (!this.enabled) 502 return; 503 if (isEditableKeyboardEvent(event)) 504 return; 505 const normalizedKey = this.normalizeKey(event.key); 506 const registration = this.shortcuts.get(normalizedKey); 507 if (registration) { 508 event.preventDefault(); 509 registration.callback(); 510 } 511 } 512 normalizeKey(key) { 513 return key.toLowerCase(); 514 } 515}; 516_KeyboardShortcutService.\u0275fac = function KeyboardShortcutService_Factory(__ngFactoryType__) { 517 return new (__ngFactoryType__ || _KeyboardShortcutService)(); 518}; 519_KeyboardShortcutService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _KeyboardShortcutService, factory: _KeyboardShortcutService.\u0275fac, providedIn: "root" }); 520var KeyboardShortcutService = _KeyboardShortcutService; 521(() => { 522 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(KeyboardShortcutService, [{ 523 type: Injectable, 524 args: [{ 525 providedIn: "root" 526 }] 527 }], () => [], null); 528})(); 529
530// src/app/shared/components/tool-button/tool-button.component.ts 531function ToolButtonComponent_Conditional_1_Template(rf, ctx) { 532 if (rf & 1) { 533 \u0275\u0275elementStart(0, "span", 1); 534 \u0275\u0275text(1); 535 \u0275\u0275elementEnd(); 536 } 537 if (rf & 2) { 538 const ctx_r0 = \u0275\u0275nextContext(); 539 \u0275\u0275advance(); 540 \u0275\u0275textInterpolate(ctx_r0.hotkey.toUpperCase()); 541 } 542} 543function ToolButtonComponent_Conditional_2_Template(rf, ctx) { 544 if (rf & 1) { 545 \u0275\u0275element(0, "ng-icon", 2); 546 } 547} 548function ToolButtonComponent_Conditional_3_Template(rf, ctx) { 549 if (rf & 1) { 550 \u0275\u0275element(0, "ng-icon", 3); 551 } 552 if (rf & 2) { 553 const ctx_r0 = \u0275\u0275nextContext(); 554 \u0275\u0275property("name", ctx_r0.icon); 555 } 556} 557var _ToolButtonComponent = class _ToolButtonComponent { 558 constructor(keyboardShortcut) { 559 this.keyboardShortcut = keyboardShortcut; 560 this.active = false; 561 this.disabled = false; 562 this.loading = false; 563 this.size = "default"; 564 this.clicked = new EventEmitter(); 565 } 566 /** Tooltip text with hotkey appended if available */ 567 get tooltip() { 568 if (!this.title) 569 return ""; 570 if (!this.hotkey) 571 return this.title; 572 return `${this.title} (${this.hotkey.toUpperCase()})`; 573 } 574 ngOnInit() { 575 if (this.hotkey) { 576 this.keyboardShortcut.register(this.hotkey, () => this.clicked.emit(), this.title); 577 } 578 } 579 ngOnDestroy() { 580 if (this.hotkey) { 581 this.keyboardShortcut.unregister(this.hotkey); 582 } 583 } 584 onClick(event) { 585 event.preventDefault(); 586 if (!this.disabled && !this.loading) { 587 this.clicked.emit(); 588 } 589 } 590}; 591_ToolButtonComponent.\u0275fac = function ToolButtonComponent_Factory(__ngFactoryType__) { 592 return new (__ngFactoryType__ || _ToolButtonComponent)(\u0275\u0275directiveInject(KeyboardShortcutService)); 593}; 594_ToolButtonComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _ToolButtonComponent, selectors: [["app-tool-button"]], inputs: { icon: "icon", hotkey: "hotkey", active: "active", disabled: "disabled", loading: "loading", title: "title", size: "size" }, outputs: { clicked: "clicked" }, features: [\u0275\u0275ProvidersFeature([], [provideIcons({ lucideLoaderCircle })])], decls: 4, vars: 11, consts: [[1, "tool-palette-button", 3, "pointerdown", "title"], [1, "tool-palette-hotkey"], ["name", "lucideLoaderCircle", 1, "spin"], [3, "name"]], template: function ToolButtonComponent_Template(rf, ctx) { 595 if (rf & 1) { 596 \u0275\u0275elementStart(0, "div", 0); 597 \u0275\u0275listener("pointerdown", function ToolButtonComponent_Template_div_pointerdown_0_listener($event) { 598 return ctx.onClick($event); 599 }); 600 \u0275\u0275template(1, ToolButtonComponent_Conditional_1_Template, 2, 1, "span", 1)(2, ToolButtonComponent_Conditional_2_Template, 1, 0, "ng-icon", 2)(3, ToolButtonComponent_Conditional_3_Template, 1, 1, "ng-icon", 3); 601 \u0275\u0275elementEnd(); 602 } 603 if (rf & 2) { 604 \u0275\u0275classProp("active", ctx.active)("disabled", ctx.disabled)("loading", ctx.loading)("compact", ctx.size === "compact"); 605 \u0275\u0275property("title", ctx.tooltip); 606 \u0275\u0275advance(); 607 \u0275\u0275conditional(ctx.hotkey ? 1 : -1); 608 \u0275\u0275advance(); 609 \u0275\u0275conditional(ctx.loading ? 2 : 3); 610 } 611}, dependencies: [NgIcon], styles: ["\n\n.tool-palette-container[_ngcontent-%COMP%] {\n --tool-button-size: 40px;\n --tool-button-size-mobile: 48px;\n --tool-icon-size: 1.3em;\n --tool-icon-size-mobile: 1.4em;\n --tool-icon-size-compact: 14px;\n --tool-active-border: 2px;\n --tool-hotkey-offset: 3em;\n --tool-hotkey-offset-mobile: 3.5em;\n --tool-hotkey-height: 1.5em;\n --tool-hotkey-font-size: 0.7em;\n --tool-border-radius: 12px;\n --tool-border-radius-small: 6px;\n --tool-spacing: 15px;\n --tool-spacing-small: 4px;\n --tool-gap: 8px;\n position: absolute;\n z-index: 100;\n}\n.tool-palette-container.tool-palette-top-left[_ngcontent-%COMP%] {\n left: var(--toolbar-left, var(--tool-spacing));\n top: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-top-right[_ngcontent-%COMP%] {\n right: var(--tool-spacing);\n top: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-bottom-left[_ngcontent-%COMP%] {\n left: var(--tool-spacing);\n bottom: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-bottom-right[_ngcontent-%COMP%] {\n right: var(--tool-spacing);\n bottom: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-top[_ngcontent-%COMP%] {\n top: var(--tool-spacing);\n left: 50%;\n transform: translateX(-50%);\n}\n.tool-palette-container.tool-palette-bottom[_ngcontent-%COMP%] {\n bottom: var(--tool-spacing);\n left: 50%;\n transform: translateX(-50%);\n}\n.tool-palette-container.tool-palette-left[_ngcontent-%COMP%] {\n left: var(--tool-spacing);\n top: 50%;\n transform: translateY(-50%);\n}\n.tool-palette-container.tool-palette-right[_ngcontent-%COMP%] {\n right: var(--tool-spacing);\n top: 50%;\n transform: translateY(-50%);\n}\n.tool-palette-container.tool-palette-center[_ngcontent-%COMP%] {\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n}\n.tool-palette-list[_ngcontent-%COMP%] {\n background: var(--tool-palette-bg);\n border-radius: var(--tool-border-radius);\n box-shadow: var(--tool-palette-shadow);\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light);\n list-style: none;\n padding: 0;\n margin: 0;\n overflow: visible;\n}\n.tool-palette-list[_ngcontent-%COMP%] + .tool-palette-list[_ngcontent-%COMP%] {\n margin-top: var(--tool-spacing);\n}\n.tool-palette-item[_ngcontent-%COMP%] {\n border-top: 1px solid var(--tool-palette-border);\n transition: all 0.2s ease;\n position: relative;\n z-index: 1;\n}\n.tool-palette-item[_ngcontent-%COMP%]:first-child {\n border-top: none;\n}\n.tool-palette-item[_ngcontent-%COMP%]:hover {\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n z-index: 10;\n}\n.tool-palette-item.tool-palette-active[_ngcontent-%COMP%] {\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n border-left: var(--tool-active-border) solid var(--ion-color-primary);\n}\n.tool-palette-item.tool-palette-active[_ngcontent-%COMP%] .tool-palette-hotkey[_ngcontent-%COMP%] {\n font-size: 0.85em;\n}\n.tool-palette-item.tool-palette-active[_ngcontent-%COMP%] .tool-palette-button[_ngcontent-%COMP%] {\n color: var(--ion-color-primary);\n}\n.tool-palette-item.tool-palette-active[_ngcontent-%COMP%] .tool-palette-button[_ngcontent-%COMP%]:hover {\n
611color: var(--ion-color-primary-shade);\n}\n.tool-palette-item[_ngcontent-%COMP%]:last-child {\n border-bottom-left-radius: var(--tool-border-radius);\n border-bottom-right-radius: var(--tool-border-radius);\n}\n.tool-palette-item[_ngcontent-%COMP%]:first-child {\n border-top-left-radius: var(--tool-border-radius);\n border-top-right-radius: var(--tool-border-radius);\n}\n.tool-palette-button[_ngcontent-%COMP%] {\n width: var(--tool-button-size);\n height: var(--tool-button-size);\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n cursor: pointer;\n color: var(--ion-text-color);\n transition: all 0.2s ease;\n}\n.tool-palette-button[_ngcontent-%COMP%]:hover {\n color: var(--tool-palette-hover-primary);\n transform: scale(1.05);\n}\n.tool-palette-button[_ngcontent-%COMP%]:active {\n transform: scale(0.95);\n}\n.tool-palette-button.active[_ngcontent-%COMP%] {\n color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n border-left: var(--tool-active-border) solid var(--ion-color-primary);\n}\n.tool-palette-button.active[_ngcontent-%COMP%]:hover {\n color: var(--ion-color-primary-shade);\n}\n.tool-palette-item[_ngcontent-%COMP%]:first-child .tool-palette-button.active[_ngcontent-%COMP%] {\n border-top-left-radius: var(--tool-border-radius);\n}\n.tool-palette-item[_ngcontent-%COMP%]:last-child .tool-palette-button.active[_ngcontent-%COMP%] {\n border-bottom-left-radius: var(--tool-border-radius);\n}\n.tool-palette-button.disabled[_ngcontent-%COMP%] {\n opacity: 0.4;\n cursor: not-allowed;\n pointer-events: auto;\n}\n.tool-palette-button.disabled[_ngcontent-%COMP%]:hover {\n color: var(--ion-text-color);\n transform: none;\n}\n.tool-palette-button.disabled[_ngcontent-%COMP%]:active {\n transform: none;\n}\n.tool-palette-button.loading[_ngcontent-%COMP%] {\n cursor: wait;\n pointer-events: auto;\n}\n.tool-palette-button.loading[_ngcontent-%COMP%]:hover {\n transform: none;\n}\n.tool-palette-button.loading[_ngcontent-%COMP%]:active {\n transform: none;\n}\n.tool-palette-button[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n font-size: var(--tool-icon-size);\n}\n.tool-palette-button[_ngcontent-%COMP%] ng-icon.spin[_ngcontent-%COMP%] {\n animation: _ngcontent-%COMP%_spin 1s linear infinite;\n}\n.tool-palette-button.compact[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n font-size: var(--tool-icon-size-compact);\n}\n@keyframes _ngcontent-%COMP%_spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n.tool-palette-hotkey[_ngcontent-%COMP%] {\n position: absolute;\n background: var(--tool-palette-hotkey-bg);\n border: 1px solid var(--tool-palette-border-light);\n border-radius: var(--tool-border-radius-small);\n right: var(--tool-hotkey-offset);\n min-width: 2em;\n height: var(--tool-hotkey-height);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 0.5em;\n font-size: var(--tool-hotkey-font-size);\n font-weight: 600;\n color: var(--tool-palette-hotkey-text);\n box-shadow: 0 1px 3px var(--shadow-color-light);\n -webkit-backdrop-filter: blur(5px);\n backdrop-filter: blur(5px);\n white-space: nowrap;\n margin-left: var(--tool-spacing-small);\n}\n.tool-palette-top-left[_ngcontent-%COMP%] .tool-palette-hotkey[_ngcontent-%COMP%], \n.tool-palette-left[_ngcontent-%COMP%] .tool-palette-hotkey[_ngcontent-%COMP%], \n.tool-palette-bottom-left[_ngcontent-%COMP%] .tool-palette-hotkey[_ngcontent-%COMP%] {\n right: auto;\n left: var(--tool-hotkey-offset);\n margin-left: 0;\n margin-right: var(--tool-spacing-small);\n}\n.tool-palette-active[_ngcontent-%COMP%] {\n color: white;\n box-shadow: 0 0 0 2px var(--tool-palette-bg), 0 2px 6px var(--tool-palette-active-bg, rgba(255, 0, 0, 0.3));\n font-size: 0.7em;\n}\n@media (prefers-reduced-motion: no-preference) {\n .tool-palette-active[_ngcontent-%COMP%] {\n animation: _ngcontent-%COMP%_activeGlow 2s ease-in-out infinite;\n }\n}\n@keyframes _ngcontent-%COMP%_activeGlow {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.7;\n }\n}\n.tool-palette-button[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid var(--ion-color-primary);\n outline-offset: 2px;\n border-radius: var(--tool-border-radius-small);\n}\n@media (max-width: 768px) {\n .tool-palette-container[_ngcontent-%COMP%] {\n --tool-button-size: var(--tool-button-size-mobile);\n --tool-icon-size: var(--tool-icon-size-mobile);\n --tool-hotkey-offset: var(--tool-hotkey-offset-mobile);\n --tool-icon-size-compact: 16px;\n --tool-spacing: 20px;\n }\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] {\n border-top: 1px solid var(--tool-palette-border-light);\n margin-top: var(--tool-gap);\n padding-top: var(--tool-gap);\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] .tool-palette-button[_ngcontent-%COMP%] {\n min-height: var(--tool-button-size);\n transition: all 0.2s ease;\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] .tool-palette-button.scale-coefficient[_ng
611content-%COMP%] {\n font-size: 12px;\n font-weight: bold;\n color: var(--tool-palette-scale-color);\n display: flex;\n align-items: center;\n gap: var(--tool-spacing-small);\n border-radius: var(--tool-border-radius-small);\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] .tool-palette-button.scale-coefficient[_ngcontent-%COMP%]:hover {\n color: var(--tool-palette-scale-hover);\n background-color: rgba(255, 152, 0, 0.1);\n transform: scale(1.02);\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] .tool-palette-button.scale-coefficient[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n font-size: 14px;\n}\n.tool-palette-item[_ngcontent-%COMP%] ul.actions[_ngcontent-%COMP%] {\n list-style: none;\n padding: 0;\n margin: 0;\n}\n.tool-palette-item[_ngcontent-%COMP%] ul.actions[_ngcontent-%COMP%] li[_ngcontent-%COMP%] {\n width: var(--tool-button-size);\n height: var(--tool-button-size);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.75em;\n cursor: pointer;\n border-top: 1px solid var(--tool-palette-border-light);\n color: var(--ion-text-color);\n transition: all 0.2s ease;\n}\n.tool-palette-item[_ngcontent-%COMP%] ul.actions[_ngcontent-%COMP%] li[_ngcontent-%COMP%]:hover {\n color: var(--tool-palette-hover-secondary);\n background: rgba(255, 123, 0, 0.05);\n}\n.tool-palette-item[_ngcontent-%COMP%] ul.actions[_ngcontent-%COMP%] li.active[_ngcontent-%COMP%] {\n color: var(--tool-palette-active-bg);\n background: rgba(255, 0, 0, 0.1);\n}"], changeDetection: 0 }); 612var ToolButtonComponent = _ToolButtonComponent; 613(() => { 614 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(ToolButtonComponent, [{ 615 type: Component, 616 args: [{ selector: "app-tool-button", standalone: true, imports: [NgIcon], viewProviders: [provideIcons({ lucideLoaderCircle })], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div 617 class="tool-palette-button" 618 [class.active]="active" 619 [class.disabled]="disabled" 620 [class.loading]="loading" 621 [class.compact]="size === 'compact'" 622 [title]="tooltip" 623 (pointerdown)="onClick($event)" 624> 625 @if (hotkey) { 626 <span class="tool-palette-hotkey">{{ hotkey.toUpperCase() }}</span> 627 } 628 @if (loading) { 629 <ng-icon name="lucideLoaderCircle" class="spin" /> 630 } @else { 631 <ng-icon [name]="icon" /> 632 } 633</div> 634`, styles: ["/* src/app/shared/styles/tool-palette.scss */\n.tool-palette-container {\n --tool-button-size: 40px;\n --tool-button-size-mobile: 48px;\n --tool-icon-size: 1.3em;\n --tool-icon-size-mobile: 1.4em;\n --tool-icon-size-compact: 14px;\n --tool-active-border: 2px;\n --tool-hotkey-offset: 3em;\n --tool-hotkey-offset-mobile: 3.5em;\n --tool-hotkey-height: 1.5em;\n --tool-hotkey-font-size: 0.7em;\n --tool-border-radius: 12px;\n --tool-border-radius-small: 6px;\n --tool-spacing: 15px;\n --tool-spacing-small: 4px;\n --tool-gap: 8px;\n position: absolute;\n z-index: 100;\n}\n.tool-palette-container.tool-palette-top-left {\n left: var(--toolbar-left, var(--tool-spacing));\n top: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-top-right {\n right: var(--tool-spacing);\n top: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-bottom-left {\n left: var(--tool-spacing);\n bottom: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-bottom-right {\n right: var(--tool-spacing);\n bottom: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-top {\n top: var(--tool-spacing);\n left: 50%;\n transform: translateX(-50%);\n}\n.tool-palette-container.tool-palette-bottom {\n bottom: var(--tool-spacing);\n left: 50%;\n transform: translateX(-50%);\n}\n.tool-palette-container.tool-palette-left {\n left: var(--tool-spacing);\n top: 50%;\n transform: translateY(-50%);\n}\n.tool-palette-container.tool-palette-right {\n right: var(--tool-spacing);\n top: 50%;\n transform: translateY(-50%);\n}\n.tool-palette-container.tool-palette-center {\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n}\n.tool-palette-list {\n background: var(--tool-palette-bg);\n border-radius: var(--tool-border-radius);\n box-shadow: var(--tool-palette-shadow);\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light);\n list-style: none;\n padding: 0;\n margin: 0;\n overflow: visible;\n}\n.tool-palette-list + .tool-palette-list {\n margin-top: var(--tool-spacing);\n}\n.tool-palette-item {\n border-top: 1px solid var(--tool-palette-border);\n transition: all 0.2s ease;\n position: relative;\n z-index: 1;\n}\n.tool-palette-item:first-child {\n border-top: none;\n}\n.tool-palette-item:hover {\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n z-index: 10;\n}\n.tool-palette-item.tool-palette-active {\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n border-left: var(--tool-active-border) solid var(--ion-color-primary);\n}\n.tool-palette-item.tool-palette-active .tool-palette-hotkey {\n font-size: 0.85em;\n}\n.tool-palette-item.tool-palette-active .tool-palette-button {\n color: var(--ion-color-primary);\n}\n.tool-palette-item.tool-palette-active .tool-palette-button:hover {\n
634color: var(--ion-color-primary-shade);\n}\n.tool-palette-item:last-child {\n border-bottom-left-radius: var(--tool-border-radius);\n border-bottom-right-radius: var(--tool-border-radius);\n}\n.tool-palette-item:first-child {\n border-top-left-radius: var(--tool-border-radius);\n border-top-right-radius: var(--tool-border-radius);\n}\n.tool-palette-button {\n width: var(--tool-button-size);\n height: var(--tool-button-size);\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n cursor: pointer;\n color: var(--ion-text-color);\n transition: all 0.2s ease;\n}\n.tool-palette-button:hover {\n color: var(--tool-palette-hover-primary);\n transform: scale(1.05);\n}\n.tool-palette-button:active {\n transform: scale(0.95);\n}\n.tool-palette-button.active {\n color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n border-left: var(--tool-active-border) solid var(--ion-color-primary);\n}\n.tool-palette-button.active:hover {\n color: var(--ion-color-primary-shade);\n}\n.tool-palette-item:first-child .tool-palette-button.active {\n border-top-left-radius: var(--tool-border-radius);\n}\n.tool-palette-item:last-child .tool-palette-button.active {\n border-bottom-left-radius: var(--tool-border-radius);\n}\n.tool-palette-button.disabled {\n opacity: 0.4;\n cursor: not-allowed;\n pointer-events: auto;\n}\n.tool-palette-button.disabled:hover {\n color: var(--ion-text-color);\n transform: none;\n}\n.tool-palette-button.disabled:active {\n transform: none;\n}\n.tool-palette-button.loading {\n cursor: wait;\n pointer-events: auto;\n}\n.tool-palette-button.loading:hover {\n transform: none;\n}\n.tool-palette-button.loading:active {\n transform: none;\n}\n.tool-palette-button ng-icon {\n font-size: var(--tool-icon-size);\n}\n.tool-palette-button ng-icon.spin {\n animation: spin 1s linear infinite;\n}\n.tool-palette-button.compact ng-icon {\n font-size: var(--tool-icon-size-compact);\n}\n@keyframes spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n.tool-palette-hotkey {\n position: absolute;\n background: var(--tool-palette-hotkey-bg);\n border: 1px solid var(--tool-palette-border-light);\n border-radius: var(--tool-border-radius-small);\n right: var(--tool-hotkey-offset);\n min-width: 2em;\n height: var(--tool-hotkey-height);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 0.5em;\n font-size: var(--tool-hotkey-font-size);\n font-weight: 600;\n color: var(--tool-palette-hotkey-text);\n box-shadow: 0 1px 3px var(--shadow-color-light);\n -webkit-backdrop-filter: blur(5px);\n backdrop-filter: blur(5px);\n white-space: nowrap;\n margin-left: var(--tool-spacing-small);\n}\n.tool-palette-top-left .tool-palette-hotkey,\n.tool-palette-left .tool-palette-hotkey,\n.tool-palette-bottom-left .tool-palette-hotkey {\n right: auto;\n left: var(--tool-hotkey-offset);\n margin-left: 0;\n margin-right: var(--tool-spacing-small);\n}\n.tool-palette-active {\n color: white;\n box-shadow: 0 0 0 2px var(--tool-palette-bg), 0 2px 6px var(--tool-palette-active-bg, rgba(255, 0, 0, 0.3));\n font-size: 0.7em;\n}\n@media (prefers-reduced-motion: no-preference) {\n .tool-palette-active {\n animation: activeGlow 2s ease-in-out infinite;\n }\n}\n@keyframes activeGlow {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.7;\n }\n}\n.tool-palette-button:focus-visible {\n outline: 2px solid var(--ion-color-primary);\n outline-offset: 2px;\n border-radius: var(--tool-border-radius-small);\n}\n@media (max-width: 768px) {\n .tool-palette-container {\n --tool-button-size: var(--tool-button-size-mobile);\n --tool-icon-size: var(--tool-icon-size-mobile);\n --tool-hotkey-offset: var(--tool-hotkey-offset-mobile);\n --tool-icon-size-compact: 16px;\n --tool-spacing: 20px;\n }\n}\n.tool-palette-floor-transform {\n border-top: 1px solid var(--tool-palette-border-light);\n margin-top: var(--tool-gap);\n padding-top: var(--tool-gap);\n}\n.tool-palette-floor-transform .tool-palette-button {\n min-height: var(--tool-button-size);\n transition: all 0.2s ease;\n}\n.tool-palette-floor-transform .tool-palette-button.scale-coefficient {\n font-size: 12px;\n font-weight: bold;\n color: var(--tool-palette-scale-color);\n display: flex;\n align-items: center;\n gap: var(--tool-spacing-small);\n border-radius: var(--tool-border-radius-small);\n}\n.tool-palette-floor-transform .tool-palette-button.scale-coefficient:hover {\n
634color: var(--tool-palette-scale-hover);\n background-color: rgba(255, 152, 0, 0.1);\n transform: scale(1.02);\n}\n.tool-palette-floor-transform .tool-palette-button.scale-coefficient ng-icon {\n font-size: 14px;\n}\n.tool-palette-item ul.actions {\n list-style: none;\n padding: 0;\n margin: 0;\n}\n.tool-palette-item ul.actions li {\n width: var(--tool-button-size);\n height: var(--tool-button-size);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.75em;\n cursor: pointer;\n border-top: 1px solid var(--tool-palette-border-light);\n color: var(--ion-text-color);\n transition: all 0.2s ease;\n}\n.tool-palette-item ul.actions li:hover {\n color: var(--tool-palette-hover-secondary);\n background: rgba(255, 123, 0, 0.05);\n}\n.tool-palette-item ul.actions li.active {\n color: var(--tool-palette-active-bg);\n background: rgba(255, 0, 0, 0.1);\n}\n"] }] 635 }], () => [{ type: KeyboardShortcutService }], { icon: [{ 636 type: Input 637 }], hotkey: [{ 638 type: Input 639 }], active: [{ 640 type: Input 641 }], disabled: [{ 642 type: Input 643 }], loading: [{ 644 type: Input 645 }], title: [{ 646 type: Input 647 }], size: [{ 648 type: Input 649 }], clicked: [{ 650 type: Output 651 }] }); 652})(); 653(() => { 654 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(ToolButtonComponent, { className: "ToolButtonComponent", filePath: "src/app/shared/components/tool-button/tool-button.component.ts", lineNumber: 23 }); 655})(); 656
657// src/app/pages/campus-map-v3/components/view-toolbar/view-toolbar.component.ts 658function ViewToolbarComponent_Conditional_18_Template(rf, ctx) { 659 if (rf & 1) { 660 const _r1 = \u0275\u0275getCurrentView(); 661 \u0275\u0275elementStart(0, "ul", 0)(1, "li", 1)(2, "app-tool-button", 9); 662 \u0275\u0275listener("clicked", function ViewToolbarComponent_Conditional_18_Template_app_tool_button_clicked_2_listener() { 663 \u0275\u0275restoreView(_r1); 664 const ctx_r1 = \u0275\u0275nextContext(); 665 return \u0275\u0275resetView(ctx_r1.drawFloorOutlineClicked.emit()); 666 }); 667 \u0275\u0275elementEnd()()(); 668 } 669 if (rf & 2) { 670 const ctx_r1 = \u0275\u0275nextContext(); 671 \u0275\u0275advance(2); 672 \u0275\u0275property("active", ctx_r1.isDrawingFloorOutline); 673 } 674} 675function ViewToolbarComponent_Conditional_19_Conditional_3_Template(rf, ctx) { 676 if (rf & 1) { 677 const _r4 = \u0275\u0275getCurrentView(); 678 \u0275\u0275elementStart(0, "li", 1)(1, "app-tool-button", 11); 679 \u0275\u0275listener("clicked", function ViewToolbarComponent_Conditional_19_Conditional_3_Template_app_tool_button_clicked_1_listener() { 680 \u0275\u0275restoreView(_r4); 681 const ctx_r1 = \u0275\u0275nextContext(2); 682 return \u0275\u0275resetView(ctx_r1.model3DSettingsClicked.emit()); 683 }); 684 \u0275\u0275elementEnd()(); 685 \u0275\u0275elementStart(2, "li", 1)(3, "app-tool-button", 12); 686 \u0275\u0275listener("clicked", function ViewToolbarComponent_Conditional_19_Conditional_3_Template_app_tool_button_clicked_3_listener() { 687 \u0275\u0275restoreView(_r4); 688 const ctx_r1 = \u0275\u0275nextContext(2); 689 return \u0275\u0275resetView(ctx_r1.rulerToggled.emit(!ctx_r1.isRulerActive)); 690 }); 691 \u0275\u0275elementEnd()(); 692 } 693 if (rf & 2) { 694 const ctx_r1 = \u0275\u0275nextContext(2); 695 \u0275\u0275advance(3); 696 \u0275\u0275property("active", ctx_r1.isRulerActive); 697 } 698} 699function ViewToolbarComponent_Conditional_19_Template(rf, ctx) { 700 if (rf & 1) { 701 const _r3 = \u0275\u0275getCurrentView(); 702 \u0275\u0275elementStart(0, "ul", 0)(1, "li", 1)(2, "app-tool-button", 10); 703 \u0275\u0275listener("clicked", function ViewToolbarComponent_Conditional_19_Template_app_tool_button_clicked_2_listener() { 704 \u0275\u0275restoreView(_r3); 705 const ctx_r1 = \u0275\u0275nextContext(); 706 return \u0275\u0275resetView(ctx_r1.model3DToggled.emit(!ctx_r1.show3DModel)); 707 }); 708 \u0275\u0275elementEnd()(); 709 \u0275\u0275template(3, ViewToolbarComponent_Conditional_19_Conditional_3_Template, 4, 1); 710 \u0275\u0275elementEnd(); 711 } 712 if (rf & 2) { 713 const ctx_r1 = \u0275\u0275nextContext(); 714 \u0275\u0275advance(2); 715 \u0275\u0275property("active", ctx_r1.show3DModel)("loading", ctx_r1.is3DModelLoading); 716 \u0275\u0275advance(); 717 \u0275\u0275conditional(ctx_r1.show3DModel ? 3 : -1); 718 } 719} 720var _ViewToolbarComponent = class _ViewToolbarComponent { 721 constructor() { 722 this.showNodes = false; 723 this.isAddNodeMode = false; 724 this.showRooms = false; 725 this.isDrawingRoom = false;
726 this.isDrawingGroundPatch = false; 727 this.isDrawingFloor = false; 728 this.showFloorOutlineButton = false; 729 this.isDrawingFloorOutline = false; 730 this.show3DModel = false; 731 this.has3DModel = false; 732 this.is3DModelLoading = false; 733 this.isRulerActive = false; 734 this.model3DToggled = new EventEmitter(); 735 this.model3DSettingsClicked = new EventEmitter(); 736 this.rulerToggled = new EventEmitter(); 737 this.fitToView = new EventEmitter(); 738 this.addNodeClicked = new EventEmitter(); 739 this.addRoomClicked = new EventEmitter(); 740 this.drawFloorOutlineClicked = new EventEmitter(); 741 this.menuClicked = new EventEmitter(); 742 this.createGroundPatchClicked = new EventEmitter(); 743 this.createFloorClicked = new EventEmitter(); 744 this.gpsDebugClicked = new EventEmitter(); 745 } 746}; 747_ViewToolbarComponent.\u0275fac = function ViewToolbarComponent_Factory(__ngFactoryType__) { 748 return new (__ngFactoryType__ || _ViewToolbarComponent)(); 749}; 750_ViewToolbarComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _ViewToolbarComponent, selectors: [["app-view-toolbar"]], inputs: { showNodes: "showNodes", isAddNodeMode: "isAddNodeMode", showRooms: "showRooms", isDrawingRoom: "isDrawingRoom", isDrawingGroundPatch: "isDrawingGroundPatch", isDrawingFloor: "isDrawingFloor", showFloorOutlineButton: "showFloorOutlineButton", isDrawingFloorOutline: "isDrawingFloorOutline", show3DModel: "show3DModel", has3DModel: "has3DModel", is3DModelLoading: "is3DModelLoading", isRulerActive: "isRulerActive" }, outputs: { model3DToggled: "model3DToggled", model3DSettingsClicked: "model3DSettingsClicked", rulerToggled: "rulerToggled", fitToView: "fitToView", addNodeClicked: "addNodeClicked", addRoomClicked: "addRoomClicked", drawFloorOutlineClicked: "drawFloorOutlineClicked", menuClicked: "menuClicked", createGroundPatchClicked: "createGroundPatchClicked", createFloorClicked: "createFloorClicked", gpsDebugClicked: "gpsDebugClicked" }, features: [\u0275\u0275ProvidersFeature([], [provideIcons({ lucideMenu, lucideMaximize2, lucideBox, lucideSettings2, lucideRuler, lucidePlus, lucideSquare, lucidePenTool, lucideTrees, lucideLayers, lucideLocate })])], decls: 20, vars: 6, consts: [[1, "tool-palette-list"], [1, "tool-palette-item"], ["icon", "lucideMenu", "title", "App Menu", 3, "clicked"], ["icon", "lucideMaximize2", "hotkey", "F", "title", "Fit to View", 3, "clicked"], ["icon", "lucideTrees", "hotkey", "G", "title", "Create Ground Patch", 3, "clicked", "active"], ["icon", "lucideLayers", "hotkey", "L", "title", "Create Floor", 3, "clicked", "active"], ["icon", "lucideSquare", "hotkey", "R", "title", "Create Room", 3, "clicked", "active"], ["icon", "lucidePlus", "hotkey", "A", "title", "Create Node", 3, "clicked", "active"], ["icon", "lucideLocate", "hotkey", "D", "title", "GPS Transform Debug", 3, "clicked"], ["icon", "lucidePenTool", "hotkey", "D", "title", "Draw Floor Outline", 3, "clicked", "active"], ["icon", "lucideBox", "hotkey", "3", "title", "Show 3D Model", 3, "clicked", "active", "loading"], ["icon", "lucideSettings2", "title", "3D Model Settings", 3, "clicked"], ["icon", "lucideRuler", "hotkey", "M", "title", "Measure/Scale Tool", 3, "clicked", "active"]], template: function ViewToolbarComponent_Template(rf, ctx) { 751 if (rf & 1) { 752 \u0275\u0275elementStart(0, "ul", 0)(1, "li", 1)(2, "app-tool-button", 2); 753 \u0275\u0275listener("clicked", function ViewToolbarComponent_Template_app_tool_button_clicked_2_listener() { 754 return ctx.menuClicked.emit(); 755 }); 756 \u0275\u0275elementEnd()()(); 757 \u0275\u0275elementStart(3, "ul", 0)(4, "li", 1)(5, "app-tool-button", 3); 758 \u0275\u0275listener("clicked", function ViewToolbarComponent_Template_app_tool_button_clicked_5_listener() { 759 return ctx.fitToView.emit(); 760 }); 761 \u0275\u0275elementEnd()()(); 762 \u0275\u0275elementStart(6, "ul", 0)(7, "li", 1)(8, "app-tool-button", 4); 763 \u0275\u0275listener("clicked", function ViewToolbarComponent_Template_app_tool_button_clicked_8_listener() { 764 return ctx.createGroundPatchClicked.emit(); 765 }); 766 \u0275\u0275elementEnd()(); 767 \u0275\u0275elementStart(9, "li", 1)(10, "app-tool-button", 5); 768 \u0275\u0275listener("clicked", function ViewToolbarComponent_Template_app_tool_button_clicked_10_listener() {
769 return ctx.createFloorClicked.emit(); 770 }); 771 \u0275\u0275elementEnd()(); 772 \u0275\u0275elementStart(11, "li", 1)(12, "app-tool-button", 6); 773 \u0275\u0275listener("clicked", function ViewToolbarComponent_Template_app_tool_button_clicked_12_listener() { 774 return ctx.addRoomClicked.emit(); 775 }); 776 \u0275\u0275elementEnd()(); 777 \u0275\u0275elementStart(13, "li", 1)(14, "app-tool-button", 7); 778 \u0275\u0275listener("clicked", function ViewToolbarComponent_Template_app_tool_button_clicked_14_listener() { 779 return ctx.addNodeClicked.emit(); 780 }); 781 \u0275\u0275elementEnd()()(); 782 \u0275\u0275elementStart(15, "ul", 0)(16, "li", 1)(17, "app-tool-button", 8); 783 \u0275\u0275listener("clicked", function ViewToolbarComponent_Template_app_tool_button_clicked_17_listener() { 784 return ctx.gpsDebugClicked.emit(); 785 }); 786 \u0275\u0275elementEnd()()(); 787 \u0275\u0275template(18, ViewToolbarComponent_Conditional_18_Template, 3, 1, "ul", 0)(19, ViewToolbarComponent_Conditional_19_Template, 4, 3, "ul", 0); 788 } 789 if (rf & 2) { 790 \u0275\u0275advance(8); 791 \u0275\u0275property("active", ctx.isDrawingGroundPatch); 792 \u0275\u0275advance(2); 793 \u0275\u0275property("active", ctx.isDrawingFloor); 794 \u0275\u0275advance(2); 795 \u0275\u0275property("active", ctx.isDrawingRoom); 796 \u0275\u0275advance(2); 797 \u0275\u0275property("active", ctx.isAddNodeMode); 798 \u0275\u0275advance(4); 799 \u0275\u0275conditional(ctx.showFloorOutlineButton ? 18 : -1); 800 \u0275\u0275advance(); 801 \u0275\u0275conditional(ctx.has3DModel ? 19 : -1); 802 } 803}, dependencies: [ToolButtonComponent], styles: ["\n\n.tool-palette-container[_ngcontent-%COMP%] {\n --tool-button-size: 40px;\n --tool-button-size-mobile: 48px;\n --tool-icon-size: 1.3em;\n --tool-icon-size-mobile: 1.4em;\n --tool-icon-size-compact: 14px;\n --tool-active-border: 2px;\n --tool-hotkey-offset: 3em;\n --tool-hotkey-offset-mobile: 3.5em;\n --tool-hotkey-height: 1.5em;\n --tool-hotkey-font-size: 0.7em;\n --tool-border-radius: 12px;\n --tool-border-radius-small: 6px;\n --tool-spacing: 15px;\n --tool-spacing-small: 4px;\n --tool-gap: 8px;\n position: absolute;\n z-index: 100;\n}\n.tool-palette-container.tool-palette-top-left[_ngcontent-%COMP%] {\n left: var(--toolbar-left, var(--tool-spacing));\n top: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-top-right[_ngcontent-%COMP%] {\n right: var(--tool-spacing);\n top: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-bottom-left[_ngcontent-%COMP%] {\n left: var(--tool-spacing);\n bottom: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-bottom-right[_ngcontent-%COMP%] {\n right: var(--tool-spacing);\n bottom: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-top[_ngcontent-%COMP%] {\n top: var(--tool-spacing);\n left: 50%;\n transform: translateX(-50%);\n}\n.tool-palette-container.tool-palette-bottom[_ngcontent-%COMP%] {\n bottom: var(--tool-spacing);\n left: 50%;\n transform: translateX(-50%);\n}\n.tool-palette-container.tool-palette-left[_ngcontent-%COMP%] {\n left: var(--tool-spacing);\n top: 50%;\n transform: translateY(-50%);\n}\n.tool-palette-container.tool-palette-right[_ngcontent-%COMP%] {\n right: var(--tool-spacing);\n top: 50%;\n transform: translateY(-50%);\n}\n.tool-palette-container.tool-palette-center[_ngcontent-%COMP%] {\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n}\n.tool-palette-list[_ngcontent-%COMP%] {\n background: var(--tool-palette-bg);\n border-radius: var(--tool-border-radius);\n box-shadow: var(--tool-palette-shadow);\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light);\n list-style: none;\n padding: 0;\n margin: 0;\n overflow: visible;\n}\n.tool-palette-list[_ngcontent-%COMP%] + .tool-palette-list[_ngcontent-%COMP%] {\n margin-top: var(--tool-spacing);\n}\n.tool-palette-item[_ngcontent-%COMP%] {\n border-top: 1px solid var(--tool-palette-border);\n transition: all 0.2s ease;\n position: relative;\n z-index: 1;\n}\n.tool-palette-item[_ngcontent-%COMP%]:first-child {\n border-top: none;\n}\n.tool-palette-item[_ngcontent-%COMP%]:hover {\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n z-index: 10;\n}\n.tool-palette-item.tool-palette-active[_ngcontent-%COMP%] {\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n border-left: var(--tool-active-border) solid var(--ion-color-primary);\n}\n.tool-palette-item.tool-palette-active[_ngcontent-%COMP%] .tool-palette-hotkey[_ngcontent-%COMP%] {\n font-size: 0.85em;\n}\n.tool-palette-item.tool-palette-active[_ngcontent-%COMP%] .tool-palette-button[_ngcontent-%COMP%] {\n color: var(--ion-color-primary);\n}\n.tool-palette-item.tool-palette-active[_ngcontent-%COMP%] .tool-palette-button[_ngcontent-%COMP%]:hover {\n
803color: var(--ion-color-primary-shade);\n}\n.tool-palette-item[_ngcontent-%COMP%]:last-child {\n border-bottom-left-radius: var(--tool-border-radius);\n border-bottom-right-radius: var(--tool-border-radius);\n}\n.tool-palette-item[_ngcontent-%COMP%]:first-child {\n border-top-left-radius: var(--tool-border-radius);\n border-top-right-radius: var(--tool-border-radius);\n}\n.tool-palette-button[_ngcontent-%COMP%] {\n width: var(--tool-button-size);\n height: var(--tool-button-size);\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n cursor: pointer;\n color: var(--ion-text-color);\n transition: all 0.2s ease;\n}\n.tool-palette-button[_ngcontent-%COMP%]:hover {\n color: var(--tool-palette-hover-primary);\n transform: scale(1.05);\n}\n.tool-palette-button[_ngcontent-%COMP%]:active {\n transform: scale(0.95);\n}\n.tool-palette-button.active[_ngcontent-%COMP%] {\n color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n border-left: var(--tool-active-border) solid var(--ion-color-primary);\n}\n.tool-palette-button.active[_ngcontent-%COMP%]:hover {\n color: var(--ion-color-primary-shade);\n}\n.tool-palette-item[_ngcontent-%COMP%]:first-child .tool-palette-button.active[_ngcontent-%COMP%] {\n border-top-left-radius: var(--tool-border-radius);\n}\n.tool-palette-item[_ngcontent-%COMP%]:last-child .tool-palette-button.active[_ngcontent-%COMP%] {\n border-bottom-left-radius: var(--tool-border-radius);\n}\n.tool-palette-button.disabled[_ngcontent-%COMP%] {\n opacity: 0.4;\n cursor: not-allowed;\n pointer-events: auto;\n}\n.tool-palette-button.disabled[_ngcontent-%COMP%]:hover {\n color: var(--ion-text-color);\n transform: none;\n}\n.tool-palette-button.disabled[_ngcontent-%COMP%]:active {\n transform: none;\n}\n.tool-palette-button.loading[_ngcontent-%COMP%] {\n cursor: wait;\n pointer-events: auto;\n}\n.tool-palette-button.loading[_ngcontent-%COMP%]:hover {\n transform: none;\n}\n.tool-palette-button.loading[_ngcontent-%COMP%]:active {\n transform: none;\n}\n.tool-palette-button[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n font-size: var(--tool-icon-size);\n}\n.tool-palette-button[_ngcontent-%COMP%] ng-icon.spin[_ngcontent-%COMP%] {\n animation: _ngcontent-%COMP%_spin 1s linear infinite;\n}\n.tool-palette-button.compact[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n font-size: var(--tool-icon-size-compact);\n}\n@keyframes _ngcontent-%COMP%_spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n.tool-palette-hotkey[_ngcontent-%COMP%] {\n position: absolute;\n background: var(--tool-palette-hotkey-bg);\n border: 1px solid var(--tool-palette-border-light);\n border-radius: var(--tool-border-radius-small);\n right: var(--tool-hotkey-offset);\n min-width: 2em;\n height: var(--tool-hotkey-height);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 0.5em;\n font-size: var(--tool-hotkey-font-size);\n font-weight: 600;\n color: var(--tool-palette-hotkey-text);\n box-shadow: 0 1px 3px var(--shadow-color-light);\n -webkit-backdrop-filter: blur(5px);\n backdrop-filter: blur(5px);\n white-space: nowrap;\n margin-left: var(--tool-spacing-small);\n}\n.tool-palette-top-left[_ngcontent-%COMP%] .tool-palette-hotkey[_ngcontent-%COMP%], \n.tool-palette-left[_ngcontent-%COMP%] .tool-palette-hotkey[_ngcontent-%COMP%], \n.tool-palette-bottom-left[_ngcontent-%COMP%] .tool-palette-hotkey[_ngcontent-%COMP%] {\n right: auto;\n left: var(--tool-hotkey-offset);\n margin-left: 0;\n margin-right: var(--tool-spacing-small);\n}\n.tool-palette-active[_ngcontent-%COMP%] {\n color: white;\n box-shadow: 0 0 0 2px var(--tool-palette-bg), 0 2px 6px var(--tool-palette-active-bg, rgba(255, 0, 0, 0.3));\n font-size: 0.7em;\n}\n@media (prefers-reduced-motion: no-preference) {\n .tool-palette-active[_ngcontent-%COMP%] {\n animation: _ngcontent-%COMP%_activeGlow 2s ease-in-out infinite;\n }\n}\n@keyframes _ngcontent-%COMP%_activeGlow {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.7;\n }\n}\n.tool-palette-button[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid var(--ion-color-primary);\n outline-offset: 2px;\n border-radius: var(--tool-border-radius-small);\n}\n@media (max-width: 768px) {\n .tool-palette-container[_ngcontent-%COMP%] {\n --tool-button-size: var(--tool-button-size-mobile);\n --tool-icon-size: var(--tool-icon-size-mobile);\n --tool-hotkey-offset: var(--tool-hotkey-offset-mobile);\n --tool-icon-size-compact: 16px;\n --tool-spacing: 20px;\n }\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] {\n border-top: 1px solid var(--tool-palette-border-light);\n margin-top: var(--tool-gap);\n padding-top: var(--tool-gap);\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] .tool-palette-button[_ngcontent-%COMP%] {\n min-height: var(--tool-button-size);\n transition: all 0.2s ease;\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] .tool-palette-button.scale-coefficient[_ng
803content-%COMP%] {\n font-size: 12px;\n font-weight: bold;\n color: var(--tool-palette-scale-color);\n display: flex;\n align-items: center;\n gap: var(--tool-spacing-small);\n border-radius: var(--tool-border-radius-small);\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] .tool-palette-button.scale-coefficient[_ngcontent-%COMP%]:hover {\n color: var(--tool-palette-scale-hover);\n background-color: rgba(255, 152, 0, 0.1);\n transform: scale(1.02);\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] .tool-palette-button.scale-coefficient[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n font-size: 14px;\n}\n.tool-palette-item[_ngcontent-%COMP%] ul.actions[_ngcontent-%COMP%] {\n list-style: none;\n padding: 0;\n margin: 0;\n}\n.tool-palette-item[_ngcontent-%COMP%] ul.actions[_ngcontent-%COMP%] li[_ngcontent-%COMP%] {\n width: var(--tool-button-size);\n height: var(--tool-button-size);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.75em;\n cursor: pointer;\n border-top: 1px solid var(--tool-palette-border-light);\n color: var(--ion-text-color);\n transition: all 0.2s ease;\n}\n.tool-palette-item[_ngcontent-%COMP%] ul.actions[_ngcontent-%COMP%] li[_ngcontent-%COMP%]:hover {\n color: var(--tool-palette-hover-secondary);\n background: rgba(255, 123, 0, 0.05);\n}\n.tool-palette-item[_ngcontent-%COMP%] ul.actions[_ngcontent-%COMP%] li.active[_ngcontent-%COMP%] {\n color: var(--tool-palette-active-bg);\n background: rgba(255, 0, 0, 0.1);\n}"], changeDetection: 0 }); 804var ViewToolbarComponent = _ViewToolbarComponent; 805(() => { 806 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(ViewToolbarComponent, [{ 807 type: Component, 808 args: [{ selector: "app-view-toolbar", standalone: true, imports: [ToolButtonComponent], viewProviders: [provideIcons({ lucideMenu, lucideMaximize2, lucideBox, lucideSettings2, lucideRuler, lucidePlus, lucideSquare, lucidePenTool, lucideTrees, lucideLayers, lucideLocate })], changeDetection: ChangeDetectionStrategy.OnPush, template: '<!-- App Menu -->\n<ul class="tool-palette-list">\n <li class="tool-palette-item">\n <app-tool-button\n icon="lucideMenu"\n title="App Menu"\n (clicked)="menuClicked.emit()"\n />\n </li>\n</ul>\n\n<!-- View Actions -->\n<ul class="tool-palette-list">\n <li class="tool-palette-item">\n <app-tool-button\n icon="lucideMaximize2"\n hotkey="F"\n title="Fit to View"\n (clicked)="fitToView.emit()"\n />\n </li>\n</ul>\n\n<!-- Create Tools (unified) -->\n<ul class="tool-palette-list">\n <li class="tool-palette-item">\n <app-tool-button\n icon="lucideTrees"\n hotkey="G"\n title="Create Ground Patch"\n [active]="isDrawingGroundPatch"\n (clicked)="createGroundPatchClicked.emit()"\n />\n </li>\n <li class="tool-palette-item">\n <app-tool-button\n icon="lucideLayers"\n hotkey="L"\n title="Create Floor"\n [active]="isDrawingFloor"\n (clicked)="createFloorClicked.emit()"\n />\n </li>\n <li class="tool-palette-item">\n <app-tool-button\n icon="lucideSquare"\n hotkey="R"\n title="Create Room"\n [active]="isDrawingRoom"\n (clicked)="addRoomClicked.emit()"\n />\n </li>\n <li class="tool-palette-item">\n <app-tool-button\n icon="lucidePlus"\n hotkey="A"\n title="Create Node"\n [active]="isAddNodeMode"\n (clicked)="addNodeClicked.emit()"\n />\n </li>\n</ul>\n\n<!-- GPS Transform -->\n<ul class="tool-palette-list">\n <li class="tool-palette-item">\n <app-tool-button\n icon="lucideLocate"\n hotkey="D"\n title="GPS Transform Debug"\n (clicked)="gpsDebugClicked.emit()"\n />\n </li>\n</ul>\n\n@if (showFloorOutlineButton) {\n <ul class="tool-palette-list">\n <li class="tool-palette-item">\n <app-tool-button\n icon="lucidePenTool"\n hotkey="D"\n title="Draw Floor Outline"\n [active]="isDrawingFloorOutline"\n (clicked)="drawFloorOutlineClicked.emit()"\n />\n </li>\n </ul>\n}\n\n@if (has3DModel) {\n <ul class="tool-palette-list">\n <li class="tool-palette-item">\n <app-tool-button\n icon="lucideBox"\n hotkey="3"\n title="Show 3D Model"\n [active]="show3DModel"\n [loading]="is3DModelLoading"\n (clicked)="model3DToggled.emit(!show3DModel)"\n />\n </li>\n @if (show3DModel) {\n <li class="tool-palette-item">\n <app-tool-button\n icon="lucideSettings2"\n title="3D Model Settings"\n (clicked)="model3DSettingsClicked.emit
808()"\n />\n </li>\n <li class="tool-palette-item">\n <app-tool-button\n icon="lucideRuler"\n hotkey="M"\n title="Measure/Scale Tool"\n [active]="isRulerActive"\n (clicked)="rulerToggled.emit(!isRulerActive)"\n />\n </li>\n }\n </ul>\n }\n', styles: ["/* src/app/shared/styles/tool-palette.scss */\n.tool-palette-container {\n --tool-button-size: 40px;\n --tool-button-size-mobile: 48px;\n --tool-icon-size: 1.3em;\n --tool-icon-size-mobile: 1.4em;\n --tool-icon-size-compact: 14px;\n --tool-active-border: 2px;\n --tool-hotkey-offset: 3em;\n --tool-hotkey-offset-mobile: 3.5em;\n --tool-hotkey-height: 1.5em;\n --tool-hotkey-font-size: 0.7em;\n --tool-border-radius: 12px;\n --tool-border-radius-small: 6px;\n --tool-spacing: 15px;\n --tool-spacing-small: 4px;\n --tool-gap: 8px;\n position: absolute;\n z-index: 100;\n}\n.tool-palette-container.tool-palette-top-left {\n left: var(--toolbar-left, var(--tool-spacing));\n top: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-top-right {\n right: var(--tool-spacing);\n top: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-bottom-left {\n left: var(--tool-spacing);\n bottom: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-bottom-right {\n right: var(--tool-spacing);\n bottom: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-top {\n top: var(--tool-spacing);\n left: 50%;\n transform: translateX(-50%);\n}\n.tool-palette-container.tool-palette-bottom {\n bottom: var(--tool-spacing);\n left: 50%;\n transform: translateX(-50%);\n}\n.tool-palette-container.tool-palette-left {\n left: var(--tool-spacing);\n top: 50%;\n transform: translateY(-50%);\n}\n.tool-palette-container.tool-palette-right {\n right: var(--tool-spacing);\n top: 50%;\n transform: translateY(-50%);\n}\n.tool-palette-container.tool-palette-center {\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n}\n.tool-palette-list {\n background: var(--tool-palette-bg);\n border-radius: var(--tool-border-radius);\n box-shadow: var(--tool-palette-shadow);\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light);\n list-style: none;\n padding: 0;\n margin: 0;\n overflow: visible;\n}\n.tool-palette-list + .tool-palette-list {\n margin-top: var(--tool-spacing);\n}\n.tool-palette-item {\n border-top: 1px solid var(--tool-palette-border);\n transition: all 0.2s ease;\n position: relative;\n z-index: 1;\n}\n.tool-palette-item:first-child {\n border-top: none;\n}\n.tool-palette-item:hover {\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n z-index: 10;\n}\n.tool-palette-item.tool-palette-active {\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n border-left: var(--tool-active-border) solid var(--ion-color-primary);\n}\n.tool-palette-item.tool-palette-active .tool-palette-hotkey {\n font-size: 0.85em;\n}\n.tool-palette-item.tool-palette-active .tool-palette-button {\n color: var(--ion-color-primary);\n}\n.tool-palette-item.tool-palette-active .tool-palette-button:hover {\n color: var(--ion-color-primary-shade);\n}\n.tool-palette-item:last-child {\n border-bottom-left-radius: var(--tool-border-radius);\n border-bottom-right-radius: var(--tool-border-radius);\n}\n.tool-palette-item:first-child {\n border-top-left-radius: var(--tool-border-radius);\n border-top-right-radius: var(--tool-border-radius);\n}\n.tool-palette-button {\n width: var(--tool-button-size);\n height: var(--tool-button-size);\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n cursor: pointer;\n color: var(--ion-text-color);\n transition: all 0.2s ease;\n}\n.tool-palette-button:hover {\n color: var(--tool-palette-hover-primary);\n transform: scale(1.05);\n}\n.tool-palette-button:active {\n transform: scale(0.95);\n}\n.tool-palette-button.active {\n color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n border-left: var(--tool-active-border) solid var(--ion-color-primary);\n}
808\n.tool-palette-button.active:hover {\n color: var(--ion-color-primary-shade);\n}\n.tool-palette-item:first-child .tool-palette-button.active {\n border-top-left-radius: var(--tool-border-radius);\n}\n.tool-palette-item:last-child .tool-palette-button.active {\n border-bottom-left-radius: var(--tool-border-radius);\n}\n.tool-palette-button.disabled {\n opacity: 0.4;\n cursor: not-allowed;\n pointer-events: auto;\n}\n.tool-palette-button.disabled:hover {\n color: var(--ion-text-color);\n transform: none;\n}\n.tool-palette-button.disabled:active {\n transform: none;\n}\n.tool-palette-button.loading {\n cursor: wait;\n pointer-events: auto;\n}\n.tool-palette-button.loading:hover {\n transform: none;\n}\n.tool-palette-button.loading:active {\n transform: none;\n}\n.tool-palette-button ng-icon {\n font-size: var(--tool-icon-size);\n}\n.tool-palette-button ng-icon.spin {\n animation: spin 1s linear infinite;\n}\n.tool-palette-button.compact ng-icon {\n font-size: var(--tool-icon-size-compact);\n}\n@keyframes spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n.tool-palette-hotkey {\n position: absolute;\n background: var(--tool-palette-hotkey-bg);\n border: 1px solid var(--tool-palette-border-light);\n border-radius: var(--tool-border-radius-small);\n right: var(--tool-hotkey-offset);\n min-width: 2em;\n height: var(--tool-hotkey-height);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 0.5em;\n font-size: var(--tool-hotkey-font-size);\n font-weight: 600;\n color: var(--tool-palette-hotkey-text);\n box-shadow: 0 1px 3px var(--shadow-color-light);\n -webkit-backdrop-filter: blur(5px);\n backdrop-filter: blur(5px);\n white-space: nowrap;\n margin-left: var(--tool-spacing-small);\n}\n.tool-palette-top-left .tool-palette-hotkey,\n.tool-palette-left .tool-palette-hotkey,\n.tool-palette-bottom-left .tool-palette-hotkey {\n right: auto;\n left: var(--tool-hotkey-offset);\n margin-left: 0;\n margin-right: var(--tool-spacing-small);\n}\n.tool-palette-active {\n color: white;\n box-shadow: 0 0 0 2px var(--tool-palette-bg), 0 2px 6px var(--tool-palette-active-bg, rgba(255, 0, 0, 0.3));\n font-size: 0.7em;\n}\n@media (prefers-reduced-motion: no-preference) {\n .tool-palette-active {\n animation: activeGlow 2s ease-in-out infinite;\n }\n}\n@keyframes activeGlow {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.7;\n }\n}\n.tool-palette-button:focus-visible {\n outline: 2px solid var(--ion-color-primary);\n outline-offset: 2px;\n border-radius: var(--tool-border-radius-small);\n}\n@media (max-width: 768px) {\n .tool-palette-container {\n --tool-button-size: var(--tool-button-size-mobile);\n --tool-icon-size: var(--tool-icon-size-mobile);\n --tool-hotkey-offset: var(--tool-hotkey-offset-mobile);\n --tool-icon-size-compact: 16px;\n --tool-spacing: 20px;\n }\n}\n.tool-palette-floor-transform {\n border-top: 1px solid var(--tool-palette-border-light);\n margin-top: var(--tool-gap);\n padding-top: var(--tool-gap);\n}\n.tool-palette-floor-transform .tool-palette-button {\n min-height: var(--tool-button-size);\n transition: all 0.2s ease;\n}\n.tool-palette-floor-transform .tool-palette-button.scale-coefficient {\n font-size: 12px;\n font-weight: bold;\n color: var(--tool-palette-scale-color);\n display: flex;\n align-items: center;\n gap: var(--tool-spacing-small);\n border-radius: var(--tool-border-radius-small);\n}\n.tool-palette-floor-transform .tool-palette-button.scale-coefficient:hover {\n color: var(--tool-palette-scale-hover);\n background-color: rgba(255, 152, 0, 0.1);\n transform: scale(1.02);\n}\n.tool-palette-floor-transform .tool-palette-button.scale-coefficient ng-icon {\n font-size: 14px;\n}\n.tool-palette-item ul.actions {\n list-style: none;\n padding: 0;\n margin: 0;\n}\n.tool-palette-item ul.actions li {\n width: var(--tool-button-size);\n height: var(--tool-button-size);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.75em;\n cursor: pointer;\n border-top: 1px solid var(--tool-palette-border-light);\n color: var(--ion-text-color);\n transition: all 0.2s ease;\n}
808\n.tool-palette-item ul.actions li:hover {\n color: var(--tool-palette-hover-secondary);\n background: rgba(255, 123, 0, 0.05);\n}\n.tool-palette-item ul.actions li.active {\n color: var(--tool-palette-active-bg);\n background: rgba(255, 0, 0, 0.1);\n}\n"] }] 809 }], null, { showNodes: [{ 810 type: Input 811 }], isAddNodeMode: [{ 812 type: Input 813 }], showRooms: [{ 814 type: Input 815 }], isDrawingRoom: [{ 816 type: Input 817 }], isDrawingGroundPatch: [{ 818 type: Input 819 }], isDrawingFloor: [{ 820 type: Input 821 }], showFloorOutlineButton: [{ 822 type: Input 823 }], isDrawingFloorOutline: [{ 824 type: Input 825 }], show3DModel: [{ 826 type: Input 827 }], has3DModel: [{ 828 type: Input 829 }], is3DModelLoading: [{ 830 type: Input 831 }], isRulerActive: [{ 832 type: Input 833 }], model3DToggled: [{ 834 type: Output 835 }], model3DSettingsClicked: [{ 836 type: Output 837 }], rulerToggled: [{ 838 type: Output 839 }], fitToView: [{ 840 type: Output 841 }], addNodeClicked: [{ 842 type: Output 843 }], addRoomClicked: [{ 844 type: Output 845 }], drawFloorOutlineClicked: [{ 846 type: Output 847 }], menuClicked: [{ 848 type: Output 849 }], createGroundPatchClicked: [{ 850 type: Output 851 }], createFloorClicked: [{ 852 type: Output 853 }], gpsDebugClicked: [{ 854 type: Output 855 }] }); 856})(); 857(() => { 858 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(ViewToolbarComponent, { className: "ViewToolbarComponent", filePath: "src/app/pages/campus-map-v3/components/view-toolbar/view-toolbar.component.ts", lineNumber: 21 }); 859})(); 860
861// src/app/pages/campus-map-v3/models/selection.ts 862var SELECTION_ACTIONS = [ 863 { 864 id: "viewDetails", 865 label: "Details", 866 icon: "lucideInfo", 867 hotkey: "I", 868 supportedTypes: ["nodeConnection"], 869 multiSelect: false 870 }, 871 { 872 id: "edit", 873 label: "Edit", 874 icon: "lucidePencil", 875 hotkey: "E", 876 supportedTypes: ["room", "node", "floor", "nodeConnection", "groundPatch", "building"], 877 multiSelect: false 878 }, 879 { 880 id: "connect", 881 label: "Connect", 882 icon: "lucideLink2", 883 hotkey: "C", 884 supportedTypes: ["node"], 885 multiSelect: false 886 }, 887 { 888 id: "shareLink", 889 label: "Share Link", 890 icon: "lucideShare2", 891 supportedTypes: ["room", "floor", "node"], 892 multiSelect: false 893 }, 894 { 895 id: "editShape", 896 label: "Edit Shape", 897 icon: "lucidePenTool", 898 hotkey: "S", 899 supportedTypes: ["room", "floor", "groundPatch"], 900 multiSelect: false 901 }, 902 { 903 id: "editBuilding", 904 label: "Edit Building", 905 icon: "lucideBuilding", 906 supportedTypes: ["floor"], 907 multiSelect: false 908 }, 909 { 910 id: "delete", 911 label: "Delete", 912 icon: "lucideTrash2", 913 hotkey: "Delete", 914 supportedTypes: ["room", "node", "floor", "nodeConnection", "groundPatch", "building"], 915 multiSelect: true, 916 requiresConfirmation: true 917 }, 918 { 919 id: "deselect", 920 label: "Clear", 921 icon: "lucideX", 922 hotkey: "Escape", 923 supportedTypes: ["room", "node", "floor", "nodeConnection", "groundPatch", "building"], 924 multiSelect: true 925 } 926]; 927function getAvailableActions(entities) { 928 if (entities.length === 0) { 929 return []; 930 } 931 if (entities.length === 1) { 932 const type = entities[0].type; 933 return SELECTION_ACTIONS.filter((action) => action.supportedTypes.includes(type)); 934 } 935 const types = new Set(entities.map((e) => e.type)); 936 return SELECTION_ACTIONS.filter((action) => { 937 if (!action.multiSelect) 938 return false; 939 return [...types].every((type) => action.supportedTypes.includes(type)); 940 }); 941} 942function formatEntityType(type) { 943 switch (type) { 944 case "room": 945 return "Room"; 946 case "node": 947 return "Node"; 948 case "floor": 949 return "Floor"; 950 case "nodeConnection": 951 return "Connection"; 952 case "groundPatch": 953 return "Ground Patch"; 954 case "building": 955 return "Building"; 956 } 957} 958function formatSelectionSummary(entities) { 959 var _a3; 960 if (entities.length === 0) 961 return ""; 962 if (entities.length === 1) { 963 const entity = entities[0]; 964 const typeName = formatEntityType(entity.type); 965 return entity.name ? `${typeName}: "${entity.name}"` : typeName; 966 } 967 const counts = /* @__PURE__ */ new Map(); 968 for (const entity of entities) { 969 counts.set(entity.type, ((_a3 = counts.get(entity.type)) != null ? _a3 : 0) + 1); 970 } 971 const parts = []; 972 for (const [type, count] of counts) { 973 const typeName = formatEntityType(type); 974 parts.push(`${count} ${typeName}${count > 1 ? "s" : ""}`); 975 } 976 return parts.join(", ") + " Selected"; 977} 978
979// src/app/pages/campus-map-v3/components/selection-toolbar/selection-toolbar.component.ts 980var _forTrack0 = ($index, $item) => $item.id; 981function SelectionToolbarComponent_Conditional_0_For_5_Template(rf, ctx) { 982 if (rf & 1) { 983 const _r1 = \u0275\u0275getCurrentView(); 984 \u0275\u0275elementStart(0, "button", 4); 985 \u0275\u0275listener("click", function SelectionToolbarComponent_Conditional_0_For_5_Template_button_click_0_listener() { 986 const action_r2 = \u0275\u0275restoreView(_r1).$implicit; 987 const ctx_r2 = \u0275\u0275nextContext(2); 988 return \u0275\u0275resetView(ctx_r2.onAction(action_r2.id)); 989 }); 990 \u0275\u0275element(1, "ng-icon", 5); 991 \u0275\u0275elementStart(2, "span", 6); 992 \u0275\u0275text(3); 993 \u0275\u0275elementEnd()(); 994 } 995 if (rf & 2) { 996 const action_r2 = ctx.$implicit; 997 const ctx_r2 = \u0275\u0275nextContext(2); 998 \u0275\u0275classProp("destructive", action_r2.id === "delete")("active", action_r2.id === "editShape" && ctx_r2.isEditingShape || action_r2.id === "connect" && ctx_r2.isConnectMode); 999 \u0275\u0275property("title", action_r2.label + (action_r2.hotkey ? " (" + action_r2.hotkey + ")" : "")); 1000 \u0275\u0275advance(); 1001 \u0275\u0275property("name", action_r2.icon); 1002 \u0275\u0275advance(2); 1003 \u0275\u0275textInterpolate(action_r2.label); 1004 } 1005} 1006function SelectionToolbarComponent_Conditional_0_Template(rf, ctx) { 1007 if (rf & 1) { 1008 \u0275\u0275elementStart(0, "div", 0)(1, "span", 1); 1009 \u0275\u0275text(2); 1010 \u0275\u0275elementEnd(); 1011 \u0275\u0275elementStart(3, "div", 2); 1012 \u0275\u0275repeaterCreate(4, SelectionToolbarComponent_Conditional_0_For_5_Template, 4, 7, "button", 3, _forTrack0); 1013 \u0275\u0275elementEnd()(); 1014 } 1015 if (rf & 2) { 1016 const ctx_r2 = \u0275\u0275nextContext(); 1017 \u0275\u0275advance(2); 1018 \u0275\u0275textInterpolate(ctx_r2.selectionSummary); 1019 \u0275\u0275advance(2); 1020 \u0275\u0275repeater(ctx_r2.availableActions); 1021 } 1022} 1023var _SelectionToolbarComponent = class _SelectionToolbarComponent { 1024 constructor() { 1025 this.selectedEntities = []; 1026 this.availableActions = []; 1027 this.isEditingShape = false;
1028 this.isConnectMode = false; 1029 this.actionTriggered = new EventEmitter(); 1030 } 1031 get selectionSummary() { 1032 return formatSelectionSummary(this.selectedEntities); 1033 } 1034 get hasSelection() { 1035 return this.selectedEntities.length > 0; 1036 } 1037 onAction(action) { 1038 this.actionTriggered.emit(action); 1039 } 1040}; 1041_SelectionToolbarComponent.\u0275fac = function SelectionToolbarComponent_Factory(__ngFactoryType__) { 1042 return new (__ngFactoryType__ || _SelectionToolbarComponent)(); 1043}; 1044_SelectionToolbarComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _SelectionToolbarComponent, selectors: [["app-selection-toolbar"]], inputs: { selectedEntities: "selectedEntities", availableActions: "availableActions", isEditingShape: "isEditingShape", isConnectMode: "isConnectMode" }, outputs: { actionTriggered: "actionTriggered" }, features: [\u0275\u0275ProvidersFeature([], [provideIcons({ lucideBuilding, lucideDoorOpen, lucideInfo, lucideLink2, lucidePencil, lucidePenTool, lucideShare2, lucideTrash2, lucideX })])], decls: 1, vars: 1, consts: [[1, "selection-toolbar"], [1, "selection-summary"], [1, "selection-actions"], [1, "action-button", 3, "destructive", "active", "title"], [1, "action-button", 3, "click", "title"], ["size", "20", 3, "name"], [1, "action-label"]], template: function SelectionToolbarComponent_Template(rf, ctx) { 1045 if (rf & 1) { 1046 \u0275\u0275template(0, SelectionToolbarComponent_Conditional_0_Template, 6, 1, "div", 0); 1047 } 1048 if (rf & 2) { 1049 \u0275\u0275conditional(ctx.hasSelection ? 0 : -1); 1050 } 1051}, dependencies: [NgIcon], styles: ["\n\n.selection-toolbar[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 16px;\n padding: 10px 16px;\n background: var(--tool-palette-bg, white);\n border-radius: 12px;\n box-shadow: var(--tool-palette-shadow, 2px 2px 8px rgba(0, 0, 0, 0.15));\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light, rgba(0, 0, 0, 0.05));\n}\n.selection-summary[_ngcontent-%COMP%] {\n font-size: 14px;\n font-weight: 500;\n color: var(--ion-text-color, #333);\n white-space: nowrap;\n min-width: 100px;\n}\n.selection-actions[_ngcontent-%COMP%] {\n display: flex;\n gap: 4px;\n}\n.action-button[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 12px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: var(--ion-text-color, #333);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n.action-button[_ngcontent-%COMP%]:hover {\n background: rgba(var(--ion-color-primary-rgb, 56, 128, 255), 0.1);\n color: var(--ion-color-primary, #3880ff);\n}\n.action-button[_ngcontent-%COMP%]:active {\n transform: scale(0.95);\n}\n.action-button.destructive[_ngcontent-%COMP%]:hover {\n background: rgba(var(--ion-color-danger-rgb, 235, 68, 90), 0.1);\n color: var(--ion-color-danger, #eb445a);\n}\n.action-button.active[_ngcontent-%COMP%] {\n background: rgba(var(--ion-color-primary-rgb, 56, 128, 255), 0.15);\n color: var(--ion-color-primary, #3880ff);\n}\n.action-button[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n flex-shrink: 0;\n}\n@media (max-width: 480px) {\n .action-label[_ngcontent-%COMP%] {\n display: none;\n }\n}\n.dark[_nghost-%COMP%] .selection-toolbar[_ngcontent-%COMP%], .dark [_nghost-%COMP%] .selection-toolbar[_ngcontent-%COMP%] {\n background: var(--tool-palette-bg, #3a3a3a);\n}\n.dark[_nghost-%COMP%] .selection-summary[_ngcontent-%COMP%], .dark [_nghost-%COMP%] .selection-summary[_ngcontent-%COMP%] {\n color: var(--ion-text-color, #fff);\n}\n.dark[_nghost-%COMP%] .action-button[_ngcontent-%COMP%], .dark [_nghost-%COMP%] .action-button[_ngcontent-%COMP%] {\n color: var(--ion-text-color, #fff);\n}"], changeDetection: 0 }); 1052var SelectionToolbarComponent = _SelectionToolbarComponent; 1053(() => { 1054 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(SelectionToolbarComponent, [{ 1055 type: Component, 1056 args: [{ selector: "app-selection-toolbar", standalone: true, imports: [NgIcon], viewProviders: [provideIcons({ lucideBuilding, lucideDoorOpen, lucideInfo, lucideLink2, lucidePencil, lucidePenTool, lucideShare2, lucideTrash2, lucideX })], changeDetection: ChangeDetectionStrategy.OnPush, template: `@if (hasSelection) { 1057 <div class="selection-toolbar"> 1058 <span class="selection-summary">{{ selectionSummary }}</span> 1059 1060 <div class="selection-actions"> 1061 @for (action of availableActions; track action.id) { 1062 <button 1063 class="action-button" 1064 [class.destructive]="action.id === 'delete'" 1065 [class.active]="(action.id === 'editShape' && isEditingShape) || (action.id === 'connect' && isConnectMode)" 1066 [title]="action.label + (action.hotkey ? ' (' + action.hotkey + ')' : '')" 1067 (click)="onAction(action.id)" 1068 > 1069 <ng-icon [name]="action.icon" size="20" />
1070 <span class="action-label">{{ action.label }}</span> 1071 </button> 1072 } 1073 </div> 1074 </div> 1075} 1076`, styles: ["/* src/app/pages/campus-map-v3/components/selection-toolbar/selection-toolbar.component.scss */\n.selection-toolbar {\n display: flex;\n align-items: center;\n gap: 16px;\n padding: 10px 16px;\n background: var(--tool-palette-bg, white);\n border-radius: 12px;\n box-shadow: var(--tool-palette-shadow, 2px 2px 8px rgba(0, 0, 0, 0.15));\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light, rgba(0, 0, 0, 0.05));\n}\n.selection-summary {\n font-size: 14px;\n font-weight: 500;\n color: var(--ion-text-color, #333);\n white-space: nowrap;\n min-width: 100px;\n}\n.selection-actions {\n display: flex;\n gap: 4px;\n}\n.action-button {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 12px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: var(--ion-text-color, #333);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n.action-button:hover {\n background: rgba(var(--ion-color-primary-rgb, 56, 128, 255), 0.1);\n color: var(--ion-color-primary, #3880ff);\n}\n.action-button:active {\n transform: scale(0.95);\n}\n.action-button.destructive:hover {\n background: rgba(var(--ion-color-danger-rgb, 235, 68, 90), 0.1);\n color: var(--ion-color-danger, #eb445a);\n}\n.action-button.active {\n background: rgba(var(--ion-color-primary-rgb, 56, 128, 255), 0.15);\n color: var(--ion-color-primary, #3880ff);\n}\n.action-button ng-icon {\n flex-shrink: 0;\n}\n@media (max-width: 480px) {\n .action-label {\n display: none;\n }\n}\n:host-context(.dark) .selection-toolbar {\n background: var(--tool-palette-bg, #3a3a3a);\n}\n:host-context(.dark) .selection-summary {\n color: var(--ion-text-color, #fff);\n}\n:host-context(.dark) .action-button {\n color: var(--ion-text-color, #fff);\n}\n"] }] 1077 }], null, { selectedEntities: [{ 1078 type: Input 1079 }], availableActions: [{ 1080 type: Input 1081 }], isEditingShape: [{ 1082 type: Input 1083 }], isConnectMode: [{ 1084 type: Input 1085 }], actionTriggered: [{ 1086 type: Output 1087 }] }); 1088})(); 1089(() => { 1090 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(SelectionToolbarComponent, { className: "SelectionToolbarComponent", filePath: "src/app/pages/campus-map-v3/components/selection-toolbar/selection-toolbar.component.ts", lineNumber: 28 }); 1091})(); 1092
1093// src/app/pages/campus-map-v3/components/editor-toolbar/editor-toolbar.component.ts 1094function EditorToolbarComponent_Conditional_5_Template(rf, ctx) { 1095 if (rf & 1) { 1096 const _r1 = \u0275\u0275getCurrentView(); 1097 \u0275\u0275elementStart(0, "li", 1)(1, "app-tool-button", 4); 1098 \u0275\u0275listener("clicked", function EditorToolbarComponent_Conditional_5_Template_app_tool_button_clicked_1_listener() { 1099 \u0275\u0275restoreView(_r1); 1100 const ctx_r1 = \u0275\u0275nextContext(); 1101 return \u0275\u0275resetView(ctx_r1.editorAction.emit("save")); 1102 }); 1103 \u0275\u0275elementEnd()(); 1104 \u0275\u0275elementStart(2, "li", 1)(3, "app-tool-button", 5); 1105 \u0275\u0275listener("clicked", function EditorToolbarComponent_Conditional_5_Template_app_tool_button_clicked_3_listener() { 1106 \u0275\u0275restoreView(_r1); 1107 const ctx_r1 = \u0275\u0275nextContext(); 1108 return \u0275\u0275resetView(ctx_r1.editorAction.emit("discard")); 1109 }); 1110 \u0275\u0275elementEnd()(); 1111 } 1112 if (rf & 2) { 1113 \u0275\u0275advance(); 1114 \u0275\u0275property("active", true); 1115 } 1116} 1117var _EditorToolbarComponent = class _EditorToolbarComponent { 1118 constructor() { 1119 this.canUndo = false; 1120 this.canRedo = false; 1121 this.hasUnsavedChanges = false; 1122 this.undoDescription = ""; 1123 this.redoDescription = ""; 1124 this.editorAction = new EventEmitter(); 1125 } 1126}; 1127_EditorToolbarComponent.\u0275fac = function EditorToolbarComponent_Factory(__ngFactoryType__) { 1128 return new (__ngFactoryType__ || _EditorToolbarComponent)(); 1129}; 1130_EditorToolbarComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _EditorToolbarComponent, selectors: [["app-editor-toolbar"]], inputs: { canUndo: "canUndo", canRedo: "canRedo", hasUnsavedChanges: "hasUnsavedChanges", undoDescription: "undoDescription", redoDescription: "redoDescription" }, outputs: { editorAction: "editorAction" }, features: [\u0275\u0275ProvidersFeature([], [provideIcons({ lucideUndo2, lucideRedo2, lucideSave, lucideRotateCcw })])], decls: 6, vars: 5, consts: [[1, "tool-palette-list"], [1, "tool-palette-item"], ["icon", "lucideUndo2", 3, "clicked", "title", "disabled"], ["icon", "lucideRedo2", 3, "clicked", "title", "disabled"], ["icon", "lucideSave", "title", "Save Campus (Ctrl+S)", 3, "clicked", "active"], ["icon", "lucideRotateCcw", "title", "Discard Changes", 3, "clicked"]], template: function EditorToolbarComponent_Template(rf, ctx) { 1131 if (rf & 1) { 1132 \u0275\u0275elementStart(0, "ul", 0)(1, "li", 1)(2, "app-tool-button", 2); 1133 \u0275\u0275listener("clicked", function EditorToolbarComponent_Template_app_tool_button_clicked_2_listener() { 1134 return ctx.editorAction.emit("undo"); 1135 }); 1136 \u0275\u0275elementEnd()(); 1137 \u0275\u0275elementStart(3, "li", 1)(4, "app-tool-button", 3); 1138 \u0275\u0275listener("clicked", function EditorToolbarComponent_Template_app_tool_button_clicked_4_listener() { 1139 return ctx.editorAction.emit("redo"); 1140 }); 1141 \u0275\u0275elementEnd()(); 1142 \u0275\u0275template(5, EditorToolbarComponent_Conditional_5_Template, 4, 1); 1143 \u0275\u0275elementEnd(); 1144 } 1145 if (rf & 2) { 1146 \u0275\u0275advance(2); 1147 \u0275\u0275property("title", "Undo" + (ctx.undoDescription ? ": " + ctx.undoDescription : "") + " (Ctrl+Z)")("disabled", !ctx.canUndo); 1148 \u0275\u0275advance(2); 1149 \u0275\u0275property("title", "Redo" + (ctx.redoDescription ? ": " + ctx.redoDescription : "") + " (Ctrl+Y)")("disabled", !ctx.canRedo); 1150 \u0275\u0275advance(); 1151 \u0275\u0275conditional(ctx.hasUnsavedChanges ? 5 : -1); 1152 } 1153}, dependencies: [ToolButtonComponent], styles: ["\n\n[_nghost-%COMP%] {\n display: flex;\n gap: 8px;\n}\n.save-actions[_ngcontent-%COMP%] {\n margin-left: 4px;\n padding-left: 4px;\n border-left: 1px solid rgba(0, 0, 0, 0.1);\n}", "\n\n.tool-palette-container[_ngcontent-%COMP%] {\n --tool-button-size: 40px;\n --tool-button-size-mobile: 48px;\n --tool-icon-size: 1.3em;\n --tool-icon-size-mobile: 1.4em;\n --tool-icon-size-compact: 14px;\n --tool-active-border: 2px;\n --tool-hotkey-offset: 3em;\n --tool-hotkey-offset-mobile: 3.5em;\n --tool-hotkey-height: 1.5em;\n --tool-hotkey-font-size: 0.7em;\n --tool-border-radius: 12px;\n --tool-border-radius-small: 6px;\n --tool-spacing: 15px;\n --tool-spacing-small: 4px;\n --tool-gap: 8px;\n position: absolute;\n z-index: 100;\n}\n.tool-palette-container.tool-palette-top-left[_ngcontent-%COMP%] {\n left: var(--toolbar-left, var(--tool-spacing));\n top: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-top-right[_ngcontent-%COMP%] {\n right: var(--tool-spacing);\n top: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-bottom-left[_ngcontent-%COMP%] {\n left: var(--tool-spacing);\n bottom: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-bottom-right[_ngcontent-%COMP%] {\n right: var(--tool-spacing);\n bottom: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-top[_ngcontent-%COMP%] {\n top: var(--tool-spacing);\n left: 50%;\n transform: translateX(-50%);\n}\n.tool-palette-container.tool-palette-bottom[_ngcontent-%COMP%] {\n bottom: var(--tool-spacing);\n left: 50%;\n transform: translateX(-50%);\n}\n.tool-palette-container.tool-palette-left[_ngcontent-%COMP%] {\n left: var(--tool-spacing);\n top: 50%;\n transform: translateY(-50%);\n}\n.tool-palette-container.tool-palette-right[_ngcontent-%COMP%] {\n right: var(--tool-spacing);\n top: 50%;\n transform: translateY(-50%);\n}\n.tool-palette-container.tool-palette-center[_ngcontent-%COMP%] {\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n}\n.tool-palette-list[_ngcontent-%COMP%] {\n background: var(--tool-palette-bg);\n border-radius: var(--tool-border-radius);\n box-shadow: var(--tool-palette-shadow);\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light);\n list-style: none;\n padding: 0;\n margin: 0;\n overflow: visible;\n}\n.tool-palette-list[_ngcontent-%COMP%] + .tool-palette-list[_ngcontent-%COMP%] {\n margin-top: var(--tool-spacing);\n}\n.tool-palette-item[_ngcontent-%COMP%] {\n border-top: 1px solid var(--tool-palette-border);\n transition: all 0.2s ease;\n position: relative;\n z-index: 1;\n}\n.tool-palette-item[_ngcontent-%COMP%]:first-child {\n border-top: none;\n}\n.tool-palette-item[_ngcontent-%COMP%]:hover {\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n z-index: 10;\n}\n.tool-palette-item.tool-palette-active[_ngcontent-%COMP%] {\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n border-left: var(--tool-active-border) solid var(--ion-color-primary);\n}\n.tool-palette-item.tool-palette-active[_ngcontent-%COMP%] .tool-palette-hotkey[_ngcontent-%COMP%] {\n font-size: 0.85em;\n}\n.tool-palette-item.tool-palette-active[_ngcontent-%COMP%] .tool-palette-button[_ngcontent-%COMP%] {\n color: var(--ion-color-primary);\n}\n.tool-palette-item.tool-palette-active[_ngcontent-%COMP%] .tool-palette-button[_ngcontent-%COMP%]:hover {\n
1153color: var(--ion-color-primary-shade);\n}\n.tool-palette-item[_ngcontent-%COMP%]:last-child {\n border-bottom-left-radius: var(--tool-border-radius);\n border-bottom-right-radius: var(--tool-border-radius);\n}\n.tool-palette-item[_ngcontent-%COMP%]:first-child {\n border-top-left-radius: var(--tool-border-radius);\n border-top-right-radius: var(--tool-border-radius);\n}\n.tool-palette-button[_ngcontent-%COMP%] {\n width: var(--tool-button-size);\n height: var(--tool-button-size);\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n cursor: pointer;\n color: var(--ion-text-color);\n transition: all 0.2s ease;\n}\n.tool-palette-button[_ngcontent-%COMP%]:hover {\n color: var(--tool-palette-hover-primary);\n transform: scale(1.05);\n}\n.tool-palette-button[_ngcontent-%COMP%]:active {\n transform: scale(0.95);\n}\n.tool-palette-button.active[_ngcontent-%COMP%] {\n color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n border-left: var(--tool-active-border) solid var(--ion-color-primary);\n}\n.tool-palette-button.active[_ngcontent-%COMP%]:hover {\n color: var(--ion-color-primary-shade);\n}\n.tool-palette-item[_ngcontent-%COMP%]:first-child .tool-palette-button.active[_ngcontent-%COMP%] {\n border-top-left-radius: var(--tool-border-radius);\n}\n.tool-palette-item[_ngcontent-%COMP%]:last-child .tool-palette-button.active[_ngcontent-%COMP%] {\n border-bottom-left-radius: var(--tool-border-radius);\n}\n.tool-palette-button.disabled[_ngcontent-%COMP%] {\n opacity: 0.4;\n cursor: not-allowed;\n pointer-events: auto;\n}\n.tool-palette-button.disabled[_ngcontent-%COMP%]:hover {\n color: var(--ion-text-color);\n transform: none;\n}\n.tool-palette-button.disabled[_ngcontent-%COMP%]:active {\n transform: none;\n}\n.tool-palette-button.loading[_ngcontent-%COMP%] {\n cursor: wait;\n pointer-events: auto;\n}\n.tool-palette-button.loading[_ngcontent-%COMP%]:hover {\n transform: none;\n}\n.tool-palette-button.loading[_ngcontent-%COMP%]:active {\n transform: none;\n}\n.tool-palette-button[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n font-size: var(--tool-icon-size);\n}\n.tool-palette-button[_ngcontent-%COMP%] ng-icon.spin[_ngcontent-%COMP%] {\n animation: _ngcontent-%COMP%_spin 1s linear infinite;\n}\n.tool-palette-button.compact[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n font-size: var(--tool-icon-size-compact);\n}\n@keyframes _ngcontent-%COMP%_spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n.tool-palette-hotkey[_ngcontent-%COMP%] {\n position: absolute;\n background: var(--tool-palette-hotkey-bg);\n border: 1px solid var(--tool-palette-border-light);\n border-radius: var(--tool-border-radius-small);\n right: var(--tool-hotkey-offset);\n min-width: 2em;\n height: var(--tool-hotkey-height);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 0.5em;\n font-size: var(--tool-hotkey-font-size);\n font-weight: 600;\n color: var(--tool-palette-hotkey-text);\n box-shadow: 0 1px 3px var(--shadow-color-light);\n -webkit-backdrop-filter: blur(5px);\n backdrop-filter: blur(5px);\n white-space: nowrap;\n margin-left: var(--tool-spacing-small);\n}\n.tool-palette-top-left[_ngcontent-%COMP%] .tool-palette-hotkey[_ngcontent-%COMP%], \n.tool-palette-left[_ngcontent-%COMP%] .tool-palette-hotkey[_ngcontent-%COMP%], \n.tool-palette-bottom-left[_ngcontent-%COMP%] .tool-palette-hotkey[_ngcontent-%COMP%] {\n right: auto;\n left: var(--tool-hotkey-offset);\n margin-left: 0;\n margin-right: var(--tool-spacing-small);\n}\n.tool-palette-active[_ngcontent-%COMP%] {\n color: white;\n box-shadow: 0 0 0 2px var(--tool-palette-bg), 0 2px 6px var(--tool-palette-active-bg, rgba(255, 0, 0, 0.3));\n font-size: 0.7em;\n}\n@media (prefers-reduced-motion: no-preference) {\n .tool-palette-active[_ngcontent-%COMP%] {\n animation: _ngcontent-%COMP%_activeGlow 2s ease-in-out infinite;\n }\n}\n@keyframes _ngcontent-%COMP%_activeGlow {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.7;\n }\n}\n.tool-palette-button[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid var(--ion-color-primary);\n outline-offset: 2px;\n border-radius: var(--tool-border-radius-small);\n}\n@media (max-width: 768px) {\n .tool-palette-container[_ngcontent-%COMP%] {\n --tool-button-size: var(--tool-button-size-mobile);\n --tool-icon-size: var(--tool-icon-size-mobile);\n --tool-hotkey-offset: var(--tool-hotkey-offset-mobile);\n --tool-icon-size-compact: 16px;\n --tool-spacing: 20px;\n }\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] {\n border-top: 1px solid var(--tool-palette-border-light);\n margin-top: var(--tool-gap);\n padding-top: var(--tool-gap);\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] .tool-palette-button[_ngcontent-%COMP%] {\n min-height: var(--tool-button-size);\n transition: all 0.2s ease;\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] .tool-palette-button.scale-coefficient[_ng
1153content-%COMP%] {\n font-size: 12px;\n font-weight: bold;\n color: var(--tool-palette-scale-color);\n display: flex;\n align-items: center;\n gap: var(--tool-spacing-small);\n border-radius: var(--tool-border-radius-small);\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] .tool-palette-button.scale-coefficient[_ngcontent-%COMP%]:hover {\n color: var(--tool-palette-scale-hover);\n background-color: rgba(255, 152, 0, 0.1);\n transform: scale(1.02);\n}\n.tool-palette-floor-transform[_ngcontent-%COMP%] .tool-palette-button.scale-coefficient[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n font-size: 14px;\n}\n.tool-palette-item[_ngcontent-%COMP%] ul.actions[_ngcontent-%COMP%] {\n list-style: none;\n padding: 0;\n margin: 0;\n}\n.tool-palette-item[_ngcontent-%COMP%] ul.actions[_ngcontent-%COMP%] li[_ngcontent-%COMP%] {\n width: var(--tool-button-size);\n height: var(--tool-button-size);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.75em;\n cursor: pointer;\n border-top: 1px solid var(--tool-palette-border-light);\n color: var(--ion-text-color);\n transition: all 0.2s ease;\n}\n.tool-palette-item[_ngcontent-%COMP%] ul.actions[_ngcontent-%COMP%] li[_ngcontent-%COMP%]:hover {\n color: var(--tool-palette-hover-secondary);\n background: rgba(255, 123, 0, 0.05);\n}\n.tool-palette-item[_ngcontent-%COMP%] ul.actions[_ngcontent-%COMP%] li.active[_ngcontent-%COMP%] {\n color: var(--tool-palette-active-bg);\n background: rgba(255, 0, 0, 0.1);\n}"], changeDetection: 0 }); 1154var EditorToolbarComponent = _EditorToolbarComponent; 1155(() => { 1156 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(EditorToolbarComponent, [{ 1157 type: Component, 1158 args: [{ selector: "app-editor-toolbar", standalone: true, imports: [ToolButtonComponent], viewProviders: [provideIcons({ lucideUndo2, lucideRedo2, lucideSave, lucideRotateCcw })], changeDetection: ChangeDetectionStrategy.OnPush, template: `<ul class="tool-palette-list"> 1159 <li class="tool-palette-item"> 1160 <app-tool-button 1161 icon="lucideUndo2" 1162 [title]="'Undo' + (undoDescription ? ': ' + undoDescription : '') + ' (Ctrl+Z)'" 1163 [disabled]="!canUndo" 1164 (clicked)="editorAction.emit('undo')" 1165 /> 1166 </li> 1167 <li class="tool-palette-item"> 1168 <app-tool-button 1169 icon="lucideRedo2" 1170 [title]="'Redo' + (redoDescription ? ': ' + redoDescription : '') + ' (Ctrl+Y)'" 1171 [disabled]="!canRedo" 1172 (clicked)="editorAction.emit('redo')" 1173 /> 1174 </li> 1175 1176 @if (hasUnsavedChanges) { 1177 <li class="tool-palette-item"> 1178 <app-tool-button 1179 icon="lucideSave" 1180 title="Save Campus (Ctrl+S)" 1181 [active]="true" 1182 (clicked)="editorAction.emit('save')" 1183 /> 1184 </li> 1185 <li class="tool-palette-item"> 1186 <app-tool-button 1187 icon="lucideRotateCcw" 1188 title="Discard Changes" 1189 (clicked)="editorAction.emit('discard')" 1190 /> 1191 </li> 1192 } 1193</ul> 1194`, styles: ["/* src/app/pages/campus-map-v3/components/editor-toolbar/editor-toolbar.component.scss */\n:host {\n display: flex;\n gap: 8px;\n}\n.save-actions {\n margin-left: 4px;\n padding-left: 4px;\n border-left: 1px solid rgba(0, 0, 0, 0.1);\n}\n", "/* src/app/shared/styles/tool-palette.scss */\n.tool-palette-container {\n --tool-button-size: 40px;\n --tool-button-size-mobile: 48px;\n --tool-icon-size: 1.3em;\n --tool-icon-size-mobile: 1.4em;\n --tool-icon-size-compact: 14px;\n --tool-active-border: 2px;\n --tool-hotkey-offset: 3em;\n --tool-hotkey-offset-mobile: 3.5em;\n --tool-hotkey-height: 1.5em;\n --tool-hotkey-font-size: 0.7em;\n --tool-border-radius: 12px;\n --tool-border-radius-small: 6px;\n --tool-spacing: 15px;\n --tool-spacing-small: 4px;\n --tool-gap: 8px;\n position: absolute;\n z-index: 100;\n}\n.tool-palette-container.tool-palette-top-left {\n left: var(--toolbar-left, var(--tool-spacing));\n top: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-top-right {\n right: var(--tool-spacing);\n top: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-bottom-left {\n left: var(--tool-spacing);\n bottom: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-bottom-right {\n right: var(--tool-spacing);\n bottom: var(--tool-spacing);\n}\n.tool-palette-container.tool-palette-top {\n top: var(--tool-spacing);\n left: 50%;\n transform: translateX(-50%);\n}\n.tool-palette-container.tool-palette-bottom {\n bottom: var(--tool-spacing);\n left: 50%;\n transform: translateX(-50%);\n}\n.tool-palette-container.tool-palette-left {\n left: var(--tool-spacing);\n top: 50%;\n transform: translateY(-50%);\n}\n.tool-palette-container.tool-palette-right {\n right: var(--tool-spacing);\n top: 50%;\n transform: translateY(-50%);\n}\n.tool-palette-container.tool-palette-center {\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n}\n.tool-palette-list {\n background: var(--tool-palette-bg);\n border-radius: var(--tool-border-radius);\n box-shadow: var(--tool-palette-shadow);\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light);\n list-style: none;\n padding: 0;\n margin: 0;\n overflow: visible;\n}\n.tool-palette-list + .tool-palette-list {\n margin-top: var(--tool-spacing);\n}\n.tool-palette-item {\n border-top: 1px solid var(--tool-palette-border);\n transition: all 0.2s ease;\n position: relative;\n z-index: 1;\n}\n.tool-palette-item:first-child {\n border-top: none;\n}\n.tool-palette-item:hover {\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n z-index: 10;\n}\n.tool-palette-item.tool-palette-active {\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n border-left: var(--tool-active-border) solid var(--ion-color-primary);\n}\n.tool-palette-item.tool-palette-active .tool-palette-hotkey {\n font-size: 0.85em;\n}\n.tool-palette-item.tool-palette-active .tool-palette-button {\n color: var(--ion-color-primary);\n}\n.tool-palette-item.tool-palette-active .tool-palette-button:hover {\n
1194color: var(--ion-color-primary-shade);\n}\n.tool-palette-item:last-child {\n border-bottom-left-radius: var(--tool-border-radius);\n border-bottom-right-radius: var(--tool-border-radius);\n}\n.tool-palette-item:first-child {\n border-top-left-radius: var(--tool-border-radius);\n border-top-right-radius: var(--tool-border-radius);\n}\n.tool-palette-button {\n width: var(--tool-button-size);\n height: var(--tool-button-size);\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n cursor: pointer;\n color: var(--ion-text-color);\n transition: all 0.2s ease;\n}\n.tool-palette-button:hover {\n color: var(--tool-palette-hover-primary);\n transform: scale(1.05);\n}\n.tool-palette-button:active {\n transform: scale(0.95);\n}\n.tool-palette-button.active {\n color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n border-left: var(--tool-active-border) solid var(--ion-color-primary);\n}\n.tool-palette-button.active:hover {\n color: var(--ion-color-primary-shade);\n}\n.tool-palette-item:first-child .tool-palette-button.active {\n border-top-left-radius: var(--tool-border-radius);\n}\n.tool-palette-item:last-child .tool-palette-button.active {\n border-bottom-left-radius: var(--tool-border-radius);\n}\n.tool-palette-button.disabled {\n opacity: 0.4;\n cursor: not-allowed;\n pointer-events: auto;\n}\n.tool-palette-button.disabled:hover {\n color: var(--ion-text-color);\n transform: none;\n}\n.tool-palette-button.disabled:active {\n transform: none;\n}\n.tool-palette-button.loading {\n cursor: wait;\n pointer-events: auto;\n}\n.tool-palette-button.loading:hover {\n transform: none;\n}\n.tool-palette-button.loading:active {\n transform: none;\n}\n.tool-palette-button ng-icon {\n font-size: var(--tool-icon-size);\n}\n.tool-palette-button ng-icon.spin {\n animation: spin 1s linear infinite;\n}\n.tool-palette-button.compact ng-icon {\n font-size: var(--tool-icon-size-compact);\n}\n@keyframes spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n.tool-palette-hotkey {\n position: absolute;\n background: var(--tool-palette-hotkey-bg);\n border: 1px solid var(--tool-palette-border-light);\n border-radius: var(--tool-border-radius-small);\n right: var(--tool-hotkey-offset);\n min-width: 2em;\n height: var(--tool-hotkey-height);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 0.5em;\n font-size: var(--tool-hotkey-font-size);\n font-weight: 600;\n color: var(--tool-palette-hotkey-text);\n box-shadow: 0 1px 3px var(--shadow-color-light);\n -webkit-backdrop-filter: blur(5px);\n backdrop-filter: blur(5px);\n white-space: nowrap;\n margin-left: var(--tool-spacing-small);\n}\n.tool-palette-top-left .tool-palette-hotkey,\n.tool-palette-left .tool-palette-hotkey,\n.tool-palette-bottom-left .tool-palette-hotkey {\n right: auto;\n left: var(--tool-hotkey-offset);\n margin-left: 0;\n margin-right: var(--tool-spacing-small);\n}\n.tool-palette-active {\n color: white;\n box-shadow: 0 0 0 2px var(--tool-palette-bg), 0 2px 6px var(--tool-palette-active-bg, rgba(255, 0, 0, 0.3));\n font-size: 0.7em;\n}\n@media (prefers-reduced-motion: no-preference) {\n .tool-palette-active {\n animation: activeGlow 2s ease-in-out infinite;\n }\n}\n@keyframes activeGlow {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.7;\n }\n}\n.tool-palette-button:focus-visible {\n outline: 2px solid var(--ion-color-primary);\n outline-offset: 2px;\n border-radius: var(--tool-border-radius-small);\n}\n@media (max-width: 768px) {\n .tool-palette-container {\n --tool-button-size: var(--tool-button-size-mobile);\n --tool-icon-size: var(--tool-icon-size-mobile);\n --tool-hotkey-offset: var(--tool-hotkey-offset-mobile);\n --tool-icon-size-compact: 16px;\n --tool-spacing: 20px;\n }\n}\n.tool-palette-floor-transform {\n border-top: 1px solid var(--tool-palette-border-light);\n margin-top: var(--tool-gap);\n padding-top: var(--tool-gap);\n}\n.tool-palette-floor-transform .tool-palette-button {\n min-height: var(--tool-button-size);\n transition: all 0.2s ease;\n}\n.tool-palette-floor-transform .tool-palette-button.scale-coefficient {\n font-size: 12px;\n font-weight: bold;\n color: var(--tool-palette-scale-color);\n display: flex;\n align-items: center;\n gap: var(--tool-spacing-small);\n border-radius: var(--tool-border-radius-small);\n}\n.tool-palette-floor-transform .tool-palette-button.scale-coefficient:hover {\n
1194color: var(--tool-palette-scale-hover);\n background-color: rgba(255, 152, 0, 0.1);\n transform: scale(1.02);\n}\n.tool-palette-floor-transform .tool-palette-button.scale-coefficient ng-icon {\n font-size: 14px;\n}\n.tool-palette-item ul.actions {\n list-style: none;\n padding: 0;\n margin: 0;\n}\n.tool-palette-item ul.actions li {\n width: var(--tool-button-size);\n height: var(--tool-button-size);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.75em;\n cursor: pointer;\n border-top: 1px solid var(--tool-palette-border-light);\n color: var(--ion-text-color);\n transition: all 0.2s ease;\n}\n.tool-palette-item ul.actions li:hover {\n color: var(--tool-palette-hover-secondary);\n background: rgba(255, 123, 0, 0.05);\n}\n.tool-palette-item ul.actions li.active {\n color: var(--tool-palette-active-bg);\n background: rgba(255, 0, 0, 0.1);\n}\n"] }] 1195 }], null, { canUndo: [{ 1196 type: Input 1197 }], canRedo: [{ 1198 type: Input 1199 }], hasUnsavedChanges: [{ 1200 type: Input 1201 }], undoDescription: [{ 1202 type: Input 1203 }], redoDescription: [{ 1204 type: Input 1205 }], editorAction: [{ 1206 type: Output 1207 }] }); 1208})(); 1209(() => { 1210 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(EditorToolbarComponent, { className: "EditorToolbarComponent", filePath: "src/app/pages/campus-map-v3/components/editor-toolbar/editor-toolbar.component.ts", lineNumber: 23 }); 1211})(); 1212
1213// src/app/shared/components/level-selector/level-selector.component.ts 1214var _forTrack02 = ($index, $item) => $item.level; 1215function LevelSelectorComponent_For_2_Conditional_0_Template(rf, ctx) { 1216 if (rf & 1) { 1217 const _r1 = \u0275\u0275getCurrentView(); 1218 \u0275\u0275elementStart(0, "button", 3); 1219 \u0275\u0275listener("click", function LevelSelectorComponent_For_2_Conditional_0_Template_button_click_0_listener() { 1220 \u0275\u0275restoreView(_r1); 1221 const item_r2 = \u0275\u0275nextContext().$implicit; 1222 const ctx_r2 = \u0275\u0275nextContext(); 1223 return \u0275\u0275resetView(ctx_r2.selectLevel(item_r2.level)); 1224 }); 1225 \u0275\u0275text(1); 1226 \u0275\u0275elementEnd(); 1227 } 1228 if (rf & 2) { 1229 const item_r2 = \u0275\u0275nextContext().$implicit; 1230 const ctx_r2 = \u0275\u0275nextContext(); 1231 \u0275\u0275classProp("active", item_r2.level === ctx_r2.currentLevel); 1232 \u0275\u0275attribute("aria-label", "Level " + ctx_r2.formatLevel(item_r2.level))("aria-pressed", item_r2.level === ctx_r2.currentLevel); 1233 \u0275\u0275advance(); 1234 \u0275\u0275textInterpolate1(" ", ctx_r2.formatLevel(item_r2.level), " "); 1235 } 1236} 1237function LevelSelectorComponent_For_2_Conditional_1_Template(rf, ctx) { 1238 if (rf & 1) { 1239 const _r4 = \u0275\u0275getCurrentView(); 1240 \u0275\u0275elementStart(0, "button", 4); 1241 \u0275\u0275listener("click", function LevelSelectorComponent_For_2_Conditional_1_Template_button_click_0_listener() { 1242 \u0275\u0275restoreView(_r4); 1243 const ctx_r2 = \u0275\u0275nextContext(2); 1244 return \u0275\u0275resetView(ctx_r2.expandLevels()); 1245 }); 1246 \u0275\u0275text(1, " \u22EE "); 1247 \u0275\u0275elementEnd(); 1248 } 1249} 1250function LevelSelectorComponent_For_2_Template(rf, ctx) { 1251 if (rf & 1) { 1252 \u0275\u0275template(0, LevelSelectorComponent_For_2_Conditional_0_Template, 2, 5, "button", 1)(1, LevelSelectorComponent_For_2_Conditional_1_Template, 2, 0, "button", 2); 1253 } 1254 if (rf & 2) { 1255 const item_r2 = ctx.$implicit; 1256 \u0275\u0275conditional(item_r2.type === "button" ? 0 : item_r2.type === "condensed" ? 1 : -1); 1257 } 1258} 1259var _LevelSelectorComponent = class _LevelSelectorComponent { 1260 constructor(keyboardShortcut) { 1261 this.keyboardShortcut = keyboardShortcut; 1262 this.levels = []; 1263 this.currentLevel = 1; 1264 this.enableKeyboardShortcuts = true; 1265 this.maxVisibleLevels = 12; 1266 this.levelSelected = new EventEmitter(); 1267 this.displayItems = []; 1268 this.isExpanded = false; 1269 this.shortcutKeys = ["arrowup", "arrowdown", "home", "end"]; 1270 } 1271 ngOnInit() { 1272 if (this.enableKeyboardShortcuts) { 1273 this.registerShortcuts(); 1274 } 1275 this.updateDisplayItems(); 1276 } 1277 ngOnChanges(changes) { 1278 if (changes["levels"] || changes["currentLevel"] || changes["maxVisibleLevels"]) { 1279 this.updateDisplayItems(); 1280 } 1281 } 1282 ngOnDestroy() { 1283 this.unregisterShortcuts(); 1284 } 1285 /** 1286 * Format a level number for display 1287 * Level 1 = "G" (Ground) 1288 * Level 0 = "LG" (Lower Ground) 1289 * Level < 0 = "B1", "B2", etc. (Basement: -1âB1, -2âB2) 1290 * Level > 1 = number as string 1291 */ 1292 formatLevel(level) { 1293 if (level === 1) 1294 return "G"; 1295 if (level === 0) 1296 return "LG"; 1297 if (level < 0) 1298 return `B${Math.abs(level)}`; 1299 return level.toString(); 1300 } 1301 /** 1302 * Select a level and emit the event 1303 */ 1304 selectLevel(level) { 1305 this.levelSelected.emit(level); 1306 } 1307 /** 1308 * Expand to show all levels (disable condensing) 1309 */ 1310 expandLevels() { 1311 this.isExpanded = true; 1312 this.updateDisplayItems(); 1313 } 1314 /** 1315 * Update the display items based on levels, current selection, and condensing rules 1316 */ 1317 updateDisplayItems() { 1318 if (!this.levels || this.levels.length === 0) { 1319 this.displayItems = []; 1320 return; 1321 } 1322 const sortedLevels = [...this.levels].sort((a, b) => b - a); 1323 if (this.isExpanded || sortedLevels.length <= this.maxVisibleLevels) { 1324 this.displayItems = sortedLevels.map((level) => ({ type: "button", level })); 1325 return; 1326 } 1327 const currentIdx = sortedLevels.indexOf(this.currentLevel); 1328 const nearbyRange = 2; 1329 const visibleIndices = /* @__PURE__ */ new Set(); 1330 visibleIndices.add(0); 1331 visibleIndices.add(sortedLevels.length - 1); 1332 if (currentIdx >= 0) { 1333 for (let i = Math.max(0, currentIdx - nearbyRange); i <= Math.min(sortedLevels.length - 1, currentIdx + nearbyRange); i++) { 1334 visibleIndices.add(i); 1335 } 1336 } 1337 this.displayItems = []; 1338 let lastAddedIdx = -1; 1339 for (let i = 0; i < sortedLevels.length; i++) { 1340 if (visibleIndices.has(i)) { 1341 if (lastAddedIdx >= 0 && i - lastAddedIdx > 1) { 1342 this.displayItems.push({ type: "condensed", level: -9999 }); 1343 } 1344 this.displayItems.push({ type: "button", level: sortedLevels[i] }); 1345 lastAddedIdx = i; 1346 } 1347 } 1348 } 1349 registerShortcuts() { 1350 this.keyboardShortcut.register("arrowup", () => this.goUp(), "Level up"); 1351 this.keyboardShortcut.register("arrowdown", () => this.goDown(), "Level down"); 1352 this.keyboardShortcut.register("home", () =>
1352 this.goToGround(), "Ground level"); 1353 this.keyboardShortcut.register("end", () => this.goToTop(), "Top level"); 1354 } 1355 unregisterShortcuts() { 1356 this.shortcutKeys.forEach((key) => this.keyboardShortcut.unregister(key)); 1357 } 1358 goUp() { 1359 if (this.levels.length === 0) 1360 return; 1361 const sortedLevels = [...this.levels].sort((a, b) => b - a); 1362 const idx = sortedLevels.indexOf(this.currentLevel); 1363 if (idx > 0) { 1364 this.selectLevel(sortedLevels[idx - 1]); 1365 } 1366 } 1367 goDown() { 1368 if (this.levels.length === 0) 1369 return; 1370 const sortedLevels = [...this.levels].sort((a, b) => b - a); 1371 const idx = sortedLevels.indexOf(this.currentLevel); 1372 if (idx >= 0 && idx < sortedLevels.length - 1) { 1373 this.selectLevel(sortedLevels[idx + 1]); 1374 } 1375 } 1376 goToGround() { 1377 if (this.levels.includes(1)) { 1378 this.selectLevel(1); 1379 } 1380 } 1381 goToTop() { 1382 if (this.levels.length === 0) 1383 return; 1384 const topLevel = Math.max(...this.levels); 1385 this.selectLevel(topLevel); 1386 } 1387}; 1388_LevelSelectorComponent.\u0275fac = function LevelSelectorComponent_Factory(__ngFactoryType__) { 1389 return new (__ngFactoryType__ || _LevelSelectorComponent)(\u0275\u0275directiveInject(KeyboardShortcutService)); 1390}; 1391_LevelSelectorComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _LevelSelectorComponent, selectors: [["app-level-selector"]], inputs: { levels: "levels", currentLevel: "currentLevel", enableKeyboardShortcuts: "enableKeyboardShortcuts", maxVisibleLevels: "maxVisibleLevels" }, outputs: { levelSelected: "levelSelected" }, features: [\u0275\u0275NgOnChangesFeature], decls: 3, vars: 2, consts: [[1, "level-selector"], [1, "level-button", 3, "active"], ["aria-label", "Show all levels", 1, "level-condensed"], [1, "level-button", 3, "click"], ["aria-label", "Show all levels", 1, "level-condensed", 3, "click"]], template: function LevelSelectorComponent_Template(rf, ctx) { 1392 if (rf & 1) { 1393 \u0275\u0275elementStart(0, "div", 0); 1394 \u0275\u0275repeaterCreate(1, LevelSelectorComponent_For_2_Template, 2, 1, null, null, _forTrack02); 1395 \u0275\u0275elementEnd(); 1396 } 1397 if (rf & 2) { 1398 \u0275\u0275classProp("expanded", ctx.isExpanded); 1399 \u0275\u0275advance(); 1400 \u0275\u0275repeater(ctx.displayItems); 1401 } 1402}, dependencies: [CommonModule], styles: ["\n\n.level-selector[_ngcontent-%COMP%] {\n position: absolute;\n right: 16px;\n top: 50%;\n transform: translateY(-50%);\n z-index: 100;\n display: flex;\n flex-direction: column;\n gap: 2px;\n background: rgba(255, 255, 255, 0.95);\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border-radius: 12px;\n padding: 8px 4px;\n box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);\n max-height: 70vh;\n overflow-y: auto;\n scrollbar-width: none;\n}\n.level-selector[_ngcontent-%COMP%]::-webkit-scrollbar {\n display: none;\n}\n.level-button[_ngcontent-%COMP%] {\n width: 40px;\n height: 40px;\n min-height: 40px;\n border-radius: 8px;\n border: none;\n background: transparent;\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n color: var(--ion-text-color, #333);\n}\n.level-button[_ngcontent-%COMP%]:hover {\n background: rgba(var(--ion-color-primary-rgb, 56, 128, 255), 0.1);\n}\n.level-button[_ngcontent-%COMP%]:active {\n transform: scale(0.95);\n}\n.level-button.active[_ngcontent-%COMP%] {\n background: var(--ion-color-primary, #3880ff);\n color: white;\n font-weight: 600;\n}\n.level-condensed[_ngcontent-%COMP%] {\n width: 40px;\n height: 24px;\n min-height: 24px;\n border-radius: 4px;\n border: none;\n background: transparent;\n font-size: 16px;\n font-weight: bold;\n color: var(--ion-color-medium, #92949c);\n cursor: pointer;\n transition: all 0.15s ease;\n}\n.level-condensed[_ngcontent-%COMP%]:hover {\n background: rgba(var(--ion-color-primary-rgb, 56, 128, 255), 0.05);\n color: var(--ion-color-primary, #3880ff);\n}\n@media (max-width: 768px) {\n .level-button[_ngcontent-%COMP%] {\n width: 44px;\n height: 44px;\n min-height: 44px;\n }\n .level-condensed[_ngcontent-%COMP%] {\n width: 44px;\n }\n}"], changeDetection: 0 }); 1403var LevelSelectorComponent = _LevelSelectorComponent; 1404(() => { 1405 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(LevelSelectorComponent, [{ 1406 type: Component, 1407 args: [{ selector: "app-level-selector", standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="level-selector" [class.expanded]="isExpanded"> 1408 @for (item of displayItems; track item.level) { 1409 @if (item.type === 'button') { 1410 <button 1411 class="level-button" 1412 [class.active]="item.level === currentLevel" 1413 (click)="selectLevel(item.level)" 1414 [attr.aria-label]="'Level ' + formatLevel(item.level)" 1415 [attr.aria-pressed]="item.level === currentLevel"> 1416 {{ formatLevel(item.level) }} 1417 </button> 1418 } @else if (item.type === 'condensed') { 1419 <button 1420 class="level-condensed" 1421 (click)="expandLevels()" 1422 aria-label="Show all levels"> 1423 \u22EE 1424 </button> 1425 } 1426 } 1427</div> 1428`, styles: ["/* src/app/shared/components/level-selector/level-selector.component.scss */\n.level-selector {\n position: absolute;\n right: 16px;\n top: 50%;\n transform: translateY(-50%);\n z-index: 100;\n display: flex;\n flex-direction: column;\n gap: 2px;\n background: rgba(255, 255, 255, 0.95);\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border-radius: 12px;\n padding: 8px 4px;\n box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);\n max-height: 70vh;\n overflow-y: auto;\n scrollbar-width: none;\n}\n.level-selector::-webkit-scrollbar {\n display: none;\n}\n.level-button {\n width: 40px;\n height: 40px;\n min-height: 40px;\n border-radius: 8px;\n border: none;\n background: transparent;\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;
1428\n color: var(--ion-text-color, #333);\n}\n.level-button:hover {\n background: rgba(var(--ion-color-primary-rgb, 56, 128, 255), 0.1);\n}\n.level-button:active {\n transform: scale(0.95);\n}\n.level-button.active {\n background: var(--ion-color-primary, #3880ff);\n color: white;\n font-weight: 600;\n}\n.level-condensed {\n width: 40px;\n height: 24px;\n min-height: 24px;\n border-radius: 4px;\n border: none;\n background: transparent;\n font-size: 16px;\n font-weight: bold;\n color: var(--ion-color-medium, #92949c);\n cursor: pointer;\n transition: all 0.15s ease;\n}\n.level-condensed:hover {\n background: rgba(var(--ion-color-primary-rgb, 56, 128, 255), 0.05);\n color: var(--ion-color-primary, #3880ff);\n}\n@media (max-width: 768px) {\n .level-button {\n width: 44px;\n height: 44px;\n min-height: 44px;\n }\n .level-condensed {\n width: 44px;\n }\n}\n"] }] 1429 }], () => [{ type: KeyboardShortcutService }], { levels: [{ 1430 type: Input 1431 }], currentLevel: [{ 1432 type: Input 1433 }], enableKeyboardShortcuts: [{ 1434 type: Input 1435 }], maxVisibleLevels: [{ 1436 type: Input 1437 }], levelSelected: [{ 1438 type: Output 1439 }] }); 1440})(); 1441(() => { 1442 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(LevelSelectorComponent, { className: "LevelSelectorComponent", filePath: "src/app/shared/components/level-selector/level-selector.component.ts", lineNumber: 42 }); 1443})(); 1444
1445// src/app/pages/campus-map-v3/services/selection.service.ts 1446var _SelectionService = class _SelectionService { 1447 constructor() { 1448 this.state = signal({ 1449 entities: /* @__PURE__ */ new Map(), 1450 primaryEntity: null 1451 }); 1452 this.selection = this.state.asReadonly(); 1453 this.selectedCount = computed(() => this.state().entities.size); 1454 this.hasSelection = computed(() => this.state().entities.size > 0); 1455 this.selectedEntities = computed(() => [...this.state().entities.values()]); 1456 this.selectedTypes = computed(() => { 1457 const types = /* @__PURE__ */ new Set(); 1458 for (const entity of this.state().entities.values()) { 1459 types.add(entity.type); 1460 } 1461 return [...types]; 1462 }); 1463 this.availableActions = computed(() => { 1464 return getAvailableActions(this.selectedEntities()); 1465 }); 1466 this.primaryEntity = computed(() => this.state().primaryEntity); 1467 } 1468 /** 1469 * Select an entity with the specified mode. 1470 * @param entity The entity to select 1471 * @param mode 'replace' clears existing selection, 'toggle' adds/removes 1472 */ 1473 select(entity, mode) { 1474 this.state.update((state) => { 1475 var _a3, _b2; 1476 const newEntities = new Map(mode === "replace" ? [] : state.entities); 1477 if (mode === "toggle" && newEntities.has(entity.id)) { 1478 newEntities.delete(entity.id); 1479 } else { 1480 newEntities.set(entity.id, entity); 1481 } 1482 let primary = null; 1483 if (newEntities.size > 0) { 1484 if (mode === "replace") { 1485 primary = entity; 1486 } else if (newEntities.has(entity.id)) { 1487 primary = (_a3 = state.primaryEntity) != null ? _a3 : entity; 1488 } else { 1489 primary = (_b2 = newEntities.values().next().value) != null ? _b2 : null; 1490 } 1491 } 1492 return { 1493 entities: newEntities, 1494 primaryEntity: primary 1495 }; 1496 }); 1497 } 1498 /** 1499 * Add multiple entities to the current selection (additive). 1500 * @param entities Array of entities to add 1501 */ 1502 selectMany(entities) { 1503 if (entities.length === 0) 1504 return; 1505 this.state.update((state) => { 1506 var _a3; 1507 const newEntities = new Map(state.entities); 1508 for (const entity of entities) { 1509 newEntities.set(entity.id, entity); 1510 } 1511 const primary = (_a3 = state.primaryEntity) != null ? _a3 : entities[0]; 1512 return { 1513 entities: newEntities, 1514 primaryEntity: primary 1515 }; 1516 }); 1517 } 1518 /** 1519 * Deselect a specific entity by ID. 1520 */ 1521 deselect(id) { 1522 this.state.update((state) => { 1523 var _a3; 1524 if (!state.entities.has(id)) 1525 return state; 1526 const newEntities = new Map(state.entities); 1527 newEntities.delete(id); 1528 let primary = state.primaryEntity; 1529 if ((primary == null ? void 0 : primary.id) === id) { 1530 primary = (_a3 = newEntities.values().next().value) != null ? _a3 : null; 1531 } 1532 return { 1533 entities: newEntities, 1534 primaryEntity: primary 1535 }; 1536 }); 1537 } 1538 /** 1539 * Clear all selections. 1540 */ 1541 deselectAll() { 1542 this.state.set({ 1543 entities: /* @__PURE__ */ new Map(), 1544 primaryEntity: null 1545 }); 1546 } 1547 /** 1548 * Check if an entity is currently selected. 1549 */ 1550 isSelected(id) { 1551 return this.state().entities.has(id); 1552 } 1553 /** 1554 * Get a selected entity by ID. 1555 */ 1556 getSelected(id) { 1557 return this.state().entities.get(id); 1558 } 1559}; 1560_SelectionService.\u0275fac = function SelectionService_Factory(__ngFactoryType__) { 1561 return new (__ngFactoryType__ || _SelectionService)(); 1562}; 1563_SelectionService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _SelectionService, factory: _SelectionService.\u0275fac }); 1564var SelectionService = _SelectionService; 1565(() => { 1566 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(SelectionService, [{ 1567 type: Injectable 1568 }], null, null); 1569})(); 1570
1571// src/app/pages/campus-map-v3/services/editor-history.service.ts 1572var MAX_HISTORY_SIZE = 50; 1573var _EditorHistoryService = class _EditorHistoryService { 1574 constructor(database) { 1575 this.database = database; 1576 this._undoStack = signal([]); 1577 this._redoStack = signal([]); 1578 this._saveCallbacks = []; 1579 this.canUndo = computed(() => this._undoStack().length > 0); 1580 this.canRedo = computed(() => this._redoStack().length > 0); 1581 this.hasUnsavedChanges = computed(() => this._undoStack().length > 0); 1582 this.undoDescription = computed(() => { 1583 const stack = this._undoStack(); 1584 return stack.length > 0 ? stack[stack.length - 1].description : ""; 1585 }); 1586 this.redoDescription = computed(() => { 1587 const stack = this._redoStack(); 1588 return stack.length > 0 ? stack[stack.length - 1].description : ""; 1589 }); 1590 this.undoStack = this._undoStack.asReadonly(); 1591 this.redoStack = this._redoStack.asReadonly(); 1592 } 1593 /** 1594 * Execute a command and add to undo stack. 1595 */ 1596 executeCommand(command) { 1597 command.execute(); 1598 this._undoStack.update((stack) => { 1599 const newStack = [...stack, command]; 1600 if (newStack.length > MAX_HISTORY_SIZE) { 1601 newStack.shift(); 1602 } 1603 return newStack; 1604 }); 1605 this._redoStack.set([]); 1606 } 1607 /** 1608 * Undo the most recent command. 1609 */ 1610 undo() { 1611 const stack = this._undoStack(); 1612 if (stack.length === 0) 1613 return; 1614 const command = stack[stack.length - 1]; 1615 command.undo(); 1616 this._undoStack.update((s) => s.slice(0, -1)); 1617 this._redoStack.update((s) => [...s, command]); 1618 } 1619 /** 1620 * Redo the most recently undone command. 1621 */ 1622 redo() { 1623 const stack = this._redoStack(); 1624 if (stack.length === 0) 1625 return; 1626 const command = stack[stack.length - 1]; 1627 command.redo(); 1628 this._redoStack.update((s) => s.slice(0, -1)); 1629 this._undoStack.update((s) => [...s, command]); 1630 } 1631 /** 1632 * Register a callback to be invoked during saveChanges(). 1633 * Used by controllers to persist non-entity data (e.g., 3D model transforms). 1634 */ 1635 registerSaveCallback(callback) { 1636 this._saveCallbacks.push(callback); 1637 } 1638 /** 1639 * Unregister a previously registered save callback. 1640 */ 1641 unregisterSaveCallback(callback) { 1642 const index = this._saveCallbacks.indexOf(callback); 1643 if (index !== -1) { 1644 this._saveCallbacks.splice(index, 1); 1645 } 1646 } 1647 /** 1648 * Save all changes to storage. 1649 * Persists all entities with isChanged=true, runs registered callbacks, and clears history. 1650 */ 1651 saveChanges() { 1652 return __async(this, null, function* () { 1653 for (const model of this.database.Models.values()) { 1654 if (model.entity.isChanged) { 1655 yield this.database.updateEntity(model.entity); 1656 } 1657 } 1658 for (const callback of this._saveCallbacks) { 1659 yield callback(); 1660 } 1661 this.clearHistory(); 1662 }); 1663 } 1664 /** 1665 * Discard all changes from this editing session. 1666 * Reloads data from storage - pre-existing changes are preserved automatically. 1667 */ 1668 discardChanges() { 1669 return __async(this, null, function* () { 1670 yield this.database.loadData(); 1671 this.clearHistory(); 1672 }); 1673 } 1674 /** 1675 * Clear all history and reset state. 1676 */ 1677 clearHistory() { 1678 this._undoStack.set([]); 1679 this._redoStack.set([]); 1680 } 1681 /** 1682 * Check if there are any operations in the undo stack. 1683 */ 1684 hasHistory() { 1685 return this._undoStack().length > 0 || this._redoStack().length > 0; 1686 } 1687}; 1688_EditorHistoryService.\u0275fac = function EditorHistoryService_Factory(__ngFactoryType__) { 1689 return new (__ngFactoryType__ || _EditorHistoryService)(\u0275\u0275inject(DatabaseService)); 1690}; 1691_EditorHistoryService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _EditorHistoryService, factory: _EditorHistoryService.\u0275fac }); 1692var EditorHistoryService = _EditorHistoryService; 1693(() => { 1694 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(EditorHistoryService, [{ 1695 type: Injectable 1696 }], () => [{ type: DatabaseService }], null); 1697})(); 1698
1699// src/app/pages/campus-map-v3/models/tree.ts 1700function getTreeNodeIcon(type) { 1701 switch (type) { 1702 case "campus": 1703 return "lucideMapPin"; 1704 case "building": 1705 return "lucideBuilding"; 1706 case "floor": 1707 return "lucideLayers"; 1708 case "room": 1709 return "lucideDoorOpen"; 1710 case "node": 1711 return "lucideCircle"; 1712 case "outdoorNodes": 1713 return "lucideTrees"; 1714 } 1715} 1716
1717// src/app/pages/campus-map-v3/services/tree-state.service.ts 1718var _TreeStateService = class _TreeStateService { 1719 constructor() { 1720 this.database = inject(DatabaseService); 1721 this.selectionService = inject(SelectionService); 1722 this.expandedIds = signal(/* @__PURE__ */ new Set()); 1723 this.currentCampus = signal(null); 1724 this.refreshTrigger = signal(0); 1725 this.flattenedItems = computed(() => { 1726 this.refreshTrigger(); 1727 return this.buildFlatList(); 1728 }); 1729 this.itemCount = computed(() => this.flattenedItems().length); 1730 } 1731 /** 1732 * Set the current campus to display in the tree. 1733 */ 1734 setCampus(campus) { 1735 this.currentCampus.set(campus); 1736 if ((campus == null ? void 0 : campus.entity.id) && !this.expandedIds().has(campus.entity.id)) { 1737 this.expand(campus.entity.id); 1738 } 1739 } 1740 /** 1741 * Force the tree to rebuild. 1742 * Call this after modifying entities that should appear in the tree. 1743 */ 1744 refresh() { 1745 this.refreshTrigger.update((v) => v + 1); 1746 } 1747 /** 1748 * Toggle expand/collapse state for a node. 1749 */ 1750 toggle(id) { 1751 this.expandedIds.update((ids) => { 1752 const newIds = new Set(ids); 1753 if (newIds.has(id)) { 1754 newIds.delete(id); 1755 } else { 1756 newIds.add(id); 1757 } 1758 return newIds; 1759 }); 1760 } 1761 /** 1762 * Expand a node (show children). 1763 */ 1764 expand(id) { 1765 this.expandedIds.update((ids) => { 1766 const newIds = new Set(ids); 1767 newIds.add(id); 1768 return newIds; 1769 }); 1770 } 1771 /** 1772 * Collapse a node (hide children). 1773 */ 1774 collapse(id) { 1775 this.expandedIds.update((ids) => { 1776 const newIds = new Set(ids); 1777 newIds.delete(id); 1778 return newIds; 1779 }); 1780 } 1781 /** 1782 * Expand all nodes in the tree. 1783 */ 1784 expandAll() { 1785 const allIds = /* @__PURE__ */ new Set(); 1786 const campus = this.currentCampus(); 1787 if (!campus) 1788 return; 1789 if (campus.entity.id) 1790 allIds.add(campus.entity.id); 1791 for (const building of campus.buildings) { 1792 if (building.entity.id) 1793 allIds.add(building.entity.id); 1794 for (const floor of building.floors) { 1795 if (floor.entity.id) 1796 allIds.add(floor.entity.id); 1797 } 1798 } 1799 allIds.add("outdoor-nodes"); 1800 this.expandedIds.set(allIds); 1801 } 1802 /** 1803 * Collapse all nodes in the tree. 1804 */ 1805 collapseAll() { 1806 this.expandedIds.set(/* @__PURE__ */ new Set()); 1807 } 1808 /** 1809 * Check if a node is expanded. 1810 */ 1811 isExpanded(id) { 1812 return this.expandedIds().has(id); 1813 } 1814 /** 1815 * Expand all parent nodes to make an entity visible in the tree. 1816 */ 1817 expandToEntity(entityId) { 1818 const campus = this.currentCampus(); 1819 if (!campus) 1820 return; 1821 const ancestors = []; 1822 const building = campus.buildings.find((b) => b.entity.id === entityId); 1823 if (building) { 1824 if (campus.entity.id) 1825 ancestors.push(campus.entity.id); 1826 this.expandMultiple(ancestors); 1827 return; 1828 } 1829 for (const b of campus.buildings) { 1830 const floor = b.floors.find((f) => f.entity.id === entityId); 1831 if (floor) { 1832 if (campus.entity.id) 1833 ancestors.push(campus.entity.id); 1834 if (b.entity.id) 1835 ancestors.push(b.entity.id); 1836 this.expandMultiple(ancestors); 1837 return; 1838 } 1839 } 1840 for (const b of campus.buildings) { 1841 for (const f of b.floors) { 1842 const room = f.rooms.find((r) => r.entity.id === entityId); 1843 const node = f.nodes.find((n) => n.entity.id === entityId); 1844 if (room || node) { 1845 if (campus.entity.id) 1846 ancestors.push(campus.entity.id); 1847 if (b.entity.id) 1848 ancestors.push(b.entity.id); 1849 if (f.entity.id) 1850 ancestors.push(f.entity.id); 1851 this.expandMultiple(ancestors); 1852 return; 1853 } 1854 } 1855 } 1856 const outdoorNode = campus.nodes.find((n) => n.entity.id === entityId); 1857 if (outdoorNode) { 1858 if (campus.entity.id) 1859 ancestors.push(campus.entity.id); 1860 ancestors.push("outdoor-nodes"); 1861 this.expandMultiple(ancestors); 1862 } 1863 } 1864 expandMultiple(ids) { 1865 this.expandedIds.update((current) => { 1866 const newIds = new Set(current); 1867 for (const id of ids) { 1868 newIds.add(id); 1869 } 1870 return newIds; 1871 }); 1872 } 1873 /** 1874 * Build the flattened list of visible tree items. 1875 */ 1876 buildFlatList() { 1877 var _a3, _b2, _c, _d, _e, _f, _g, _h, _i, _j, _k; 1878 const campus = this.currentCampus(); 1879 if (!campus) 1880 return []; 1881 const result = []; 1882 const selectedIds = new Set(this.selectionService.selectedEntities().map((e) => e.id)); 1883 const expanded = this.expandedIds(); 1884 const sortedBuildings = [...campus.buildings].filter((b) => !b.entity.isDeleted).sort((a, b) => { 1885 var _a4, _b3; 1886 return ((_a4 = a.entity.name) != null ? _a4 : "").localeCompare((_b3 = b.entity.name) != null ? _b3 : ""); 1887 }); 1888 const campusId = (_a3 = campus.entity.id) != null ? _a3 : "campus"; 1889 const outdoorNodes = this.getOutdoorNodes(campus); 1890 const campusItem = this.createItem(campusId, "campus", (_b2 = campus.entity.name) != null ? _b2 : "Campus", 0, sortedBuildings.length > 0 || outdoorNodes.length > 0, expanded.has(campusId), sortedBuildings.length, selectedIds.has(campusId)); 1891 result.push(campusItem); 1892 if (!expanded.has(campusId)) 1893 return result; 1894 for (const building of sortedBuildings) { 1895 const buildingId = (_c = building.entity.id) != null ? _c : "";
1896 const floors = this.getSortedFloors(building); 1897 const buildingItem = this.createItem(buildingId, "building", (_d = building.entity.name) != null ? _d : "Unnamed Building", 1, floors.length > 0, expanded.has(buildingId), floors.length, selectedIds.has(buildingId)); 1898 result.push(buildingItem); 1899 if (!expanded.has(buildingId)) 1900 continue; 1901 for (const floor of floors) { 1902 const floorId = (_e = floor.entity.id) != null ? _e : ""; 1903 const rooms = this.getSortedRooms(floor); 1904 const nodes = this.getSortedNodes(floor); 1905 const childCount = rooms.length + nodes.length; 1906 const floorItem = this.createItem(floorId, "floor", this.formatFloorName(floor), 2, childCount > 0, expanded.has(floorId), childCount, selectedIds.has(floorId), buildingId, (_f = floor.entity.level) != null ? _f : void 0); 1907 result.push(floorItem); 1908 if (!expanded.has(floorId)) 1909 continue; 1910 for (const room of rooms) { 1911 const roomId = (_g = room.entity.id) != null ? _g : ""; 1912 result.push(this.createItem(roomId, "room", this.formatRoomName(room), 3, false, false, void 0, selectedIds.has(roomId), floorId)); 1913 } 1914 for (const node of nodes) { 1915 const nodeId = (_h = node.entity.id) != null ? _h : ""; 1916 result.push(this.createItem(nodeId, "node", (_i = node.entity.tagName) != null ? _i : `Node ${nodeId.slice(0, 8)}`, 3, false, false, void 0, selectedIds.has(nodeId), floorId)); 1917 } 1918 } 1919 } 1920 if (outdoorNodes.length > 0) { 1921 const outdoorId = "outdoor-nodes"; 1922 result.push(this.createItem(outdoorId, "outdoorNodes", "Outdoor Nodes", 1, true, expanded.has(outdoorId), outdoorNodes.length, false)); 1923 if (expanded.has(outdoorId)) { 1924 for (const node of outdoorNodes) { 1925 const nodeId = (_j = node.entity.id) != null ? _j : ""; 1926 result.push(this.createItem(nodeId, "node", (_k = node.entity.tagName) != null ? _k : `Node ${nodeId.slice(0, 8)}`, 2, false, false, void 0, selectedIds.has(nodeId), outdoorId)); 1927 } 1928 } 1929 } 1930 return result; 1931 } 1932 createItem(id, type, name, depth, hasChildren, isExpanded, childCount, isSelected, parentId, level) { 1933 return { 1934 id, 1935 type, 1936 name, 1937 depth, 1938 hasChildren, 1939 isExpanded, 1940 childCount, 1941 icon: getTreeNodeIcon(type), 1942 isSelected, 1943 parentId, 1944 level 1945 }; 1946 } 1947 getSortedFloors(building) { 1948 return [...building.floors].filter((f) => !f.entity.isDeleted).sort((a, b) => { 1949 var _a3, _b2; 1950 return ((_a3 = b.entity.level) != null ? _a3 : 0) - ((_b2 = a.entity.level) != null ? _b2 : 0); 1951 }); 1952 } 1953 getSortedRooms(floor) { 1954 return [...floor.rooms].filter((r) => !r.entity.isDeleted).sort((a, b) => { 1955 var _a3, _b2, _c, _d; 1956 const numA = (_a3 = a.entity.number) != null ? _a3 : ""; 1957 const numB = (_b2 = b.entity.number) != null ? _b2 : ""; 1958 if (numA || numB) { 1959 return numA.localeCompare(numB, void 0, { numeric: true }); 1960 } 1961 return ((_c = a.entity.name) != null ? _c : "").localeCompare((_d = b.entity.name) != null ? _d : ""); 1962 }); 1963 } 1964 getSortedNodes(floor) { 1965 return [...floor.nodes].filter((n) => !n.entity.isDeleted).sort((a, b) => { 1966 var _a3, _b2; 1967 return ((_a3 = a.entity.tagName) != null ? _a3 : "").localeCompare((_b2 = b.entity.tagName) != null ? _b2 : ""); 1968 }); 1969 } 1970 getOutdoorNodes(campus) { 1971 return [...campus.nodes].filter((n) => !n.entity.isDeleted && !n.entity.floorId).sort((a, b) => { 1972 var _a3, _b2; 1973 return ((_a3 = a.entity.tagName) != null ? _a3 : "").localeCompare((_b2 = b.entity.tagName) != null ? _b2 : ""); 1974 }); 1975 } 1976 formatFloorName(floor) { 1977 const level = floor.entity.level; 1978 let levelStr = ""; 1979 if (level === void 0 || level === null) { 1980 levelStr = ""; 1981 } else if (level === 1) { 1982 levelStr = "G"; 1983 } else if (level === 0) { 1984 levelStr = "LG"; 1985 } else if (level < 0) { 1986 levelStr = `B${Math.abs(level)}`; 1987 } else { 1988 levelStr = `${level}`; 1989 } 1990 const name = floor.entity.name; 1991 if (name && levelStr) { 1992 return `${name} (${levelStr})`; 1993 } else if (name) { 1994 return name; 1995 } else if (levelStr) { 1996 return `Level ${levelStr}`; 1997 } 1998 return "Unnamed Floor"; 1999 } 2000 /** 2001 * Format room display name - always shows room number for easier navigation. 2002 * Format: "101 - Conference Room" or "101" if no name, or "Conference Room" if no number. 2003 */ 2004 formatRoomName(room) { 2005 const number = room.entity.number; 2006 const name = room.entity.name; 2007 if (number && name) { 2008 return `${number} - ${name}`; 2009 } else if (number) { 2010 return number; 2011 } else if (name) { 2012 return name; 2013 } 2014 return "Unnamed Room"; 2015 } 2016}; 2017_TreeStateService.\u0275fac = function TreeStateService_Factory(__ngFactoryType__) { 2018 return new (__ngFactoryType__ || _TreeStateService)(); 2019}; 2020_TreeStateService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _TreeStateService, factory: _TreeStateService.\u0275fac }); 2021var TreeStateService = _TreeStateService; 2022(() => { 2023 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(TreeStateService, [{ 2024 type: Injectable 2025 }], null, null); 2026})(); 2027
2028// src/app/pages/campus-map-v3/services/view-state.service.ts 2029var _ViewStateService = class _ViewStateService { 2030 constructor() { 2031 this.settings = inject(SettingsService); 2032 this._selectedLevel = signal(1); 2033 this._availableLevels = signal([]); 2034 this._showBuildings = signal(true); 2035 this._showRooms = signal(true); 2036 this._showNodes = signal(false); 2037 this._showGroundReference = signal(true); 2038 this._showOutdoorNodesAlways = signal(false); 2039 this._showRoomLabels = signal(true); 2040 this._showOutlinesOnly = signal(false); 2041 this._showFloorBackgrounds = signal(false); 2042 this._sidebarCollapsed = signal(false); 2043 this._centerOnSelection = signal(true); 2044 this._isMobileView = signal(false); 2045 this.wasAutoCollapsed = false; 2046 this.selectedLevel = this._selectedLevel.asReadonly(); 2047 this.availableLevels = this._availableLevels.asReadonly(); 2048 this.showBuildings = this._showBuildings.asReadonly(); 2049 this.showRooms = this._showRooms.asReadonly(); 2050 this.showNodes = this._showNodes.asReadonly(); 2051 this.showGroundReference = this._showGroundReference.asReadonly(); 2052 this.showOutdoorNodesAlways = this._showOutdoorNodesAlways.asReadonly(); 2053 this.showRoomLabels = this._showRoomLabels.asReadonly(); 2054 this.showOutlinesOnly = this._showOutlinesOnly.asReadonly(); 2055 this.showFloorBackgrounds = this._showFloorBackgrounds.asReadonly(); 2056 this.sidebarCollapsed = this._sidebarCollapsed.asReadonly(); 2057 this.centerOnSelection = this._centerOnSelection.asReadonly(); 2058 this.isMobileView = this._isMobileView.asReadonly(); 2059 this.shouldShowGroundReference = computed(() => this._showGroundReference() && this._selectedLevel() !== 1); 2060 } 2061 /** 2062 * Load initial state from persisted settings. 2063 * Call this during component initialization. 2064 */ 2065 loadFromSettings() { 2066 var _a3, _b2, _c, _d, _e, _f, _g, _h, _i, _j; 2067 const s = this.settings.currentSettings; 2068 this._showBuildings.set((_a3 = s.mapV3ShowBuildings) != null ? _a3 : true); 2069 this._showRooms.set((_b2 = s.mapV3ShowRooms) != null ? _b2 : true); 2070 this._showNodes.set((_c = s.mapV3ShowNodes) != null ? _c : false); 2071 this._showGroundReference.set((_d = s.mapV3ShowGroundReference) != null ? _d : true); 2072 this._sidebarCollapsed.set((_e = s.mapV3SidebarCollapsed) != null ? _e : false); 2073 this._centerOnSelection.set((_f = s.mapV3CenterOnSelection) != null ? _f : true); 2074 this._showOutdoorNodesAlways.set((_g = s.mapV3ShowOutdoorNodesAlways) != null ? _g : false); 2075 this._showRoomLabels.set((_h = s.mapV3ShowRoomLabels) != null ? _h : true); 2076 this._showOutlinesOnly.set((_i = s.mapV3ShowOutlinesOnly) != null ? _i : false); 2077 this._showFloorBackgrounds.set((_j = s.mapV3ShowFloorBackgrounds) != null ? _j : false); 2078 this.checkMobileAutoCollapse(); 2079 } 2080 /** 2081 * Set the available levels for the current campus. 2082 */ 2083 setAvailableLevels(levels) { 2084 this._availableLevels.set(levels); 2085 } 2086 /** 2087 * Select a floor level. 2088 * @returns true if level changed, false if same level 2089 */ 2090 selectLevel(level) { 2091 if (level === this._selectedLevel()) 2092 return false; 2093 this._selectedLevel.set(level); 2094 return true; 2095 } 2096 /** 2097 * Initialize to default level based on available levels. 2098 */ 2099 initializeDefaultLevel() { 2100 const levels = this._availableLevels(); 2101 if (levels.length === 0) 2102 return; 2103 const defaultLevel = levels.includes(1) ? 1 : levels[levels.length - 1]; 2104 this._selectedLevel.set(defaultLevel); 2105 } 2106 // ========================================================================= 2107 // Display Toggle Methods 2108 // ========================================================================= 2109 toggleBuildings(visible) { 2110 this._showBuildings.set(visible); 2111 this.settings.updateSetting("mapV3ShowBuildings", visible); 2112 } 2113 toggleRooms(visible) { 2114 this._showRooms.set(visible); 2115 this.settings.updateSetting("mapV3ShowRooms", visible); 2116 } 2117 toggleNodes(visible) { 2118 this._showNodes.set(visible); 2119 this.settings.updateSetting("mapV3ShowNodes", visible); 2120 } 2121 toggleGroundReference(visible) { 2122 this._showGroundReference.set(visible); 2123 this.settings.updateSetting("mapV3ShowGroundReference", visible); 2124 } 2125 toggleOutdoorNodesAlways(visible) { 2126 this._showOutdoorNodesAlways.set(visible); 2127 this.settings.updateSetting("mapV3ShowOutdoorNodesAlways", visible); 2128 } 2129 toggleRoomLabels(visible) { 2130 this._showRoomLabels.set(visible); 2131 this.settings.updateSetting("mapV3ShowRoomLabels", visible); 2132 }
2133 toggleOutlinesOnly(enabled) { 2134 this._showOutlinesOnly.set(enabled); 2135 this.settings.updateSetting("mapV3ShowOutlinesOnly", enabled); 2136 } 2137 toggleFloorBackgrounds(visible) { 2138 this._showFloorBackgrounds.set(visible); 2139 this.settings.updateSetting("mapV3ShowFloorBackgrounds", visible); 2140 } 2141 // ========================================================================= 2142 // Sidebar Methods 2143 // ========================================================================= 2144 /** 2145 * Toggle sidebar collapsed state. 2146 * @param collapsed New collapsed state 2147 * @param persist Whether to persist to settings (false for auto-collapse) 2148 */ 2149 toggleSidebar(collapsed, persist = true) { 2150 this._sidebarCollapsed.set(collapsed); 2151 this.wasAutoCollapsed = false; 2152 if (persist) { 2153 this.settings.updateSetting("mapV3SidebarCollapsed", collapsed); 2154 } 2155 } 2156 toggleCenterOnSelection(enabled) { 2157 this._centerOnSelection.set(enabled); 2158 this.settings.updateSetting("mapV3CenterOnSelection", enabled); 2159 } 2160 /** 2161 * Auto-collapse sidebar on mobile/small screens. 2162 * Auto-expand when returning to desktop if it was auto-collapsed. 2163 */ 2164 checkMobileAutoCollapse() { 2165 if (typeof window === "undefined") 2166 return; 2167 const isMobile = window.innerWidth < 768; 2168 this._isMobileView.set(isMobile); 2169 if (isMobile && !this._sidebarCollapsed()) { 2170 this._sidebarCollapsed.set(true); 2171 this.wasAutoCollapsed = true; 2172 } else if (!isMobile && this._sidebarCollapsed() && this.wasAutoCollapsed) { 2173 this._sidebarCollapsed.set(false); 2174 this.wasAutoCollapsed = false; 2175 } 2176 } 2177 /** 2178 * Auto-collapse sidebar on mobile when navigating to an entity. 2179 */ 2180 autoCollapseSidebarOnMobile() { 2181 if (this._isMobileView() && this._centerOnSelection() && !this._sidebarCollapsed()) { 2182 this._sidebarCollapsed.set(true); 2183 this.wasAutoCollapsed = true; 2184 } 2185 } 2186 // ========================================================================= 2187 // Entity Visibility Helpers 2188 // ========================================================================= 2189 /** 2190 * Check if an entity type is currently visible based on display toggles. 2191 */ 2192 isEntityTypeVisible(type) { 2193 switch (type) { 2194 case "node": 2195 case "nodeConnection": 2196 return this._showNodes(); 2197 case "room": 2198 return this._showRooms(); 2199 case "floor": 2200 case "building": 2201 return this._showBuildings(); 2202 case "groundPatch": 2203 return true; 2204 } 2205 } 2206}; 2207_ViewStateService.\u0275fac = function ViewStateService_Factory(__ngFactoryType__) { 2208 return new (__ngFactoryType__ || _ViewStateService)(); 2209}; 2210_ViewStateService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _ViewStateService, factory: _ViewStateService.\u0275fac }); 2211var ViewStateService = _ViewStateService; 2212(() => { 2213 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(ViewStateService, [{ 2214 type: Injectable 2215 }], null, null); 2216})(); 2217
2218// src/app/pages/campus-map-v3/services/campus-data.service.ts 2219var _CampusDataService = class _CampusDataService { 2220 constructor() { 2221 this.database = inject(DatabaseService); 2222 this._campus = signal(null); 2223 this.campus = this._campus.asReadonly(); 2224 } 2225 /** 2226 * Get the current campus model (non-reactive access). 2227 */ 2228 getCampus() { 2229 return this._campus(); 2230 } 2231 /** 2232 * Load campus by ID. 2233 * @returns The loaded campus model, or null if not found 2234 */ 2235 loadCampus(campusId) { 2236 const campusModel = this.database.Models.get(campusId); 2237 if (campusModel && campusModel.type === EntityType.CAMPUS) { 2238 const campus = campusModel; 2239 this._campus.set(campus); 2240 return campus; 2241 } 2242 return null; 2243 } 2244 /** 2245 * Load the first available campus. 2246 * @returns The loaded campus model, or null if none found 2247 */ 2248 loadFirstCampus() { 2249 const firstCampus = Array.from(this.database.Models.values()).find((m) => m.type === EntityType.CAMPUS && !m.entity.isDeleted); 2250 if (firstCampus == null ? void 0 : firstCampus.entity.id) { 2251 return this.loadCampus(firstCampus.entity.id); 2252 } 2253 return null; 2254 } 2255 // ========================================================================= 2256 // Floor Queries 2257 // ========================================================================= 2258 /** 2259 * Get all floors in the current campus. 2260 */ 2261 getAllFloorsInCampus() { 2262 const campus = this._campus(); 2263 if (!campus) 2264 return []; 2265 return Array.from(this.database.Models.values()).filter((m) => m.type === EntityType.FLOOR && !m.entity.isDeleted).map((m) => m).filter((floor) => { 2266 var _a3, _b2; 2267 return ((_b2 = (_a3 = floor.building) == null ? void 0 : _a3.campus) == null ? void 0 : _b2.entity.id) === campus.entity.id; 2268 }); 2269 } 2270 /** 2271 * Get floors at a specific level. 2272 * @param level The floor level (1 = ground floor) 2273 */ 2274 getFloorsAtLevel(level) { 2275 return this.getAllFloorsInCampus().filter((f) => f.entity.level === level && f.corners.filter((c) => !c.entity.isDeleted).length >= 3); 2276 } 2277 /** 2278 * Compute available levels from all floors in the campus. 2279 * @returns Sorted array of levels (descending, top floor first) 2280 */ 2281 computeAvailableLevels() { 2282 const floors = this.getAllFloorsInCampus(); 2283 const levels = [...new Set(floors.map((f) => { 2284 var _a3; 2285 return (_a3 = f.entity.level) != null ? _a3 : 1; 2286 }))]; 2287 return levels.sort((a, b) => b - a); 2288 } 2289 // ========================================================================= 2290 // Ground Patch Queries 2291 // ========================================================================= 2292 /** 2293 * Get all ground patches for the current campus. 2294 */ 2295 getGroundPatches() { 2296 const campus = this._campus(); 2297 if (!campus) 2298 return []; 2299 return Array.from(this.database.Models.values()).filter((m) => m.type === EntityType.GROUND_PATCH && !m.entity.isDeleted).map((m) => m).filter((patch) => patch.entity.campusId === campus.entity.id); 2300 } 2301 // ========================================================================= 2302 // Entity Bounds Calculation 2303 // ========================================================================= 2304 /** 2305 * Get the bounding box of an entity for camera fitting. 2306 * @param entityId The entity ID 2307 * @returns Bounds object or null if entity has no valid bounds 2308 */ 2309 getEntityBounds(entityId) { 2310 var _a3, _b2, _c, _d; 2311 const model = this.database.Models.get(entityId); 2312 if (!model) 2313 return null; 2314 switch (model.type) { 2315 case EntityType.NODE: { 2316 const node = model; 2317 const x = node.entity.locationX; 2318 const y = node.entity.locationY; 2319 if (x == null || y == null) 2320 return null; 2321 const nodeRadius = 500; 2322 return { 2323 minX: x - nodeRadius, 2324 maxX: x + nodeRadius, 2325 minY: y - nodeRadius, 2326 maxY: y + nodeRadius 2327 }; 2328 } 2329 case EntityType.ROOM: 2330 case EntityType.FLOOR: { 2331 const polygon = model; 2332 const corners = (_a3 = polygon.corners) == null ? void 0 : _a3.filter((c) => !c.entity.isDeleted); 2333 if (!corners || corners.length === 0) 2334 return null; 2335 return this.computePolygonBounds(corners); 2336 } 2337 case EntityType.BUILDING: { 2338 const building = model; 2339 if (!building.floors || building.floors.length === 0) 2340 return null; 2341 let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; 2342 for (const floor of building.floors) { 2343 if (floor.entity.isDeleted || !floor.corners) 2344 continue; 2345 for (const corner of floor.corners) { 2346 if (corner.entity.isDeleted) 2347 continue; 2348 const cx = (_b2 = corner.entity.locationX) != null ? _b2 : 0; 2349 const cy = (_c = corner.entity.locationY) != null ? _c : 0; 2350 minX = Math.min(minX, cx); 2351 maxX = Math.max(maxX, cx); 2352 minY = Math.min(minY, cy); 2353 maxY = Math.max(maxY, cy); 2354 } 2355 } 2356 if (minX === Infinity) 2357 return null; 2358 return { minX, maxX, minY, maxY }; 2359 } 2360 case EntityType.GROUND_PATCH: { 2361 const patch = model; 2362 const corners = (_d = patch.corners) == null ? void 0 : _d.filter((c) => !c.entity.isDeleted); 2363 if (!corners || corners.length === 0) 2364 return null; 2365 return this.computePolygonBounds(corners); 2366 } 2367 default: 2368 return null; 2369 } 2370 } 2371 /** 2372 * Compute bounding box from polygon corners. 2373 */ 2374 computePolygonBounds(corners) { 2375 var _a3, _b2; 2376 if (!corners || corners.length === 0) 2377 return null; 2378 let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; 2379 for (const corner of corners) { 2380 const cx = (_a3 = corner.entity.locationX) != null ? _a3 : 0; 2381 const cy = (_b2 = corner.entity.locationY) != null ? _b2 : 0; 2382 minX = Math.min(minX, cx); 2383 maxX = Math.max(maxX, cx); 2384 minY = Math.min(minY, cy); 2385 maxY = Math.max(maxY, cy); 2386 } 2387 if (minX === Infinity) 2388 return null; 2389 return { minX, maxX, minY, maxY }; 2390 } 2391}; 2392_CampusDataService.\u0275fac = function CampusDataService_Factory(__ngFactoryType__) { 2393 return new (__ngFactoryType__ || _CampusDataService)(); 2394}; 2395_CampusDataService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _CampusDataService, factory: _CampusDataService.\u0275fac }); 2396var CampusDataService = _CampusDataService; 2397(() => { 2398 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(CampusDataService, [{ 2399 type: Injectable 2400 }], null, null); 2401})(); 2402
2403// src/app/pages/campus-map-v3/commands/editor-command.ts 2404var BaseEditorCommand = class { 2405 constructor() { 2406 this.id = crypto.randomUUID(); 2407 } 2408 redo() { 2409 this.execute(); 2410 } 2411 /** 2412 * Format entity type for display. 2413 */ 2414 formatType(type) { 2415 switch (type) { 2416 case "node": 2417 return "node"; 2418 case "room": 2419 return "room"; 2420 case "floor": 2421 return "floor"; 2422 case "groundPatch": 2423 return "ground patch"; 2424 case "nodeConnection": 2425 return "connection"; 2426 case "building": 2427 return "building"; 2428 } 2429 } 2430 /** 2431 * Format count and type for display. 2432 */ 2433 formatCountAndType(count, type) { 2434 const typeName = this.formatType(type); 2435 if (count === 1) { 2436 return typeName; 2437 } 2438 return `${count} ${typeName}s`; 2439 } 2440}; 2441
2442// src/app/pages/campus-map-v3/commands/update-node-command.ts 2443var UpdateNodeCommand = class extends BaseEditorCommand { 2444 constructor(config) { 2445 super(); 2446 this.previousValues = {}; 2447 this.nodeId = config.nodeId; 2448 this.changes = config.changes; 2449 this.database = config.database; 2450 this.onVisualUpdate = config.onVisualUpdate; 2451 this.description = this.buildDescription(); 2452 } 2453 execute() { 2454 const model = this.database.Models.get(this.nodeId); 2455 if (!model) 2456 return; 2457 const entity = model.entity; 2458 if (Object.keys(this.previousValues).length === 0) { 2459 for (const key of Object.keys(this.changes)) { 2460 this.previousValues[key] = entity[key]; 2461 } 2462 } 2463 for (const [key, value] of Object.entries(this.changes)) { 2464 entity[key] = value; 2465 } 2466 entity.isChanged = true; 2467 this.onVisualUpdate(); 2468 } 2469 undo() { 2470 const model = this.database.Models.get(this.nodeId); 2471 if (!model) 2472 return; 2473 const entity = model.entity; 2474 for (const [key, value] of Object.entries(this.previousValues)) { 2475 entity[key] = value; 2476 } 2477 entity.isChanged = true; 2478 this.onVisualUpdate(); 2479 } 2480 buildDescription() { 2481 const changedFields = Object.keys(this.changes); 2482 if (changedFields.length === 0) { 2483 return "Update node"; 2484 } 2485 if (changedFields.includes("locationX") || changedFields.includes("locationY")) { 2486 return "Move node"; 2487 } 2488 if (changedFields.includes("type")) { 2489 return `Change node type to ${this.changes.type}`; 2490 } 2491 if (changedFields.includes("isCampusEntrance")) { 2492 return this.changes.isCampusEntrance ? "Mark campus entrance" : "Unmark campus entrance"; 2493 } 2494 if (changedFields.includes("tagName") || changedFields.includes("tagDeviceId")) { 2495 return "Update node beacon"; 2496 } 2497 if (changedFields.includes("latitude") || changedFields.includes("longitude") || changedFields.includes("altitude")) { 2498 return "Update node GPS"; 2499 } 2500 if (changedFields.includes("floorId")) { 2501 return "Change node floor"; 2502 } 2503 return "Update node"; 2504 } 2505}; 2506
2507// src/app/pages/campus-map-v3/commands/update-floor-command.ts 2508var UpdateFloorCommand = class extends BaseEditorCommand { 2509 constructor(config) { 2510 super(); 2511 this.previousValues = {}; 2512 this.floorId = config.floorId; 2513 this.changes = config.changes; 2514 this.database = config.database; 2515 this.onVisualUpdate = config.onVisualUpdate; 2516 this.description = this.buildDescription(); 2517 } 2518 execute() { 2519 const model = this.database.Models.get(this.floorId); 2520 if (!model) 2521 return; 2522 const entity = model.entity; 2523 if (Object.keys(this.previousValues).length === 0) { 2524 for (const key of Object.keys(this.changes)) { 2525 this.previousValues[key] = entity[key]; 2526 } 2527 } 2528 for (const [key, value] of Object.entries(this.changes)) { 2529 entity[key] = value; 2530 } 2531 entity.isChanged = true; 2532 this.onVisualUpdate(); 2533 } 2534 undo() { 2535 const model = this.database.Models.get(this.floorId); 2536 if (!model) 2537 return; 2538 const entity = model.entity; 2539 for (const [key, value] of Object.entries(this.previousValues)) { 2540 entity[key] = value; 2541 } 2542 entity.isChanged = true; 2543 this.onVisualUpdate(); 2544 } 2545 buildDescription() { 2546 const changedFields = Object.keys(this.changes); 2547 if (changedFields.length === 0) { 2548 return "Update floor"; 2549 } 2550 if (changedFields.includes("name") && changedFields.length === 1) { 2551 return `Rename floor to "${this.changes.name}"`; 2552 } 2553 if (changedFields.includes("level") && changedFields.length === 1) { 2554 const levelDisplay = this.changes.level === 1 ? "G" : `${this.changes.level}`; 2555 return `Change floor level to ${levelDisplay}`; 2556 } 2557 if (changedFields.includes("name") && changedFields.includes("level")) { 2558 return "Update floor details"; 2559 } 2560 return "Update floor"; 2561 } 2562}; 2563
2564// src/app/pages/campus-map-v3/commands/update-building-command.ts 2565var UpdateBuildingCommand = class extends BaseEditorCommand { 2566 constructor(config) { 2567 super(); 2568 this.previousValues = {}; 2569 this.buildingId = config.buildingId; 2570 this.changes = config.changes; 2571 this.database = config.database; 2572 this.onVisualUpdate = config.onVisualUpdate; 2573 this.description = this.buildDescription(); 2574 } 2575 execute() { 2576 const model = this.database.Models.get(this.buildingId); 2577 if (!model) 2578 return; 2579 const entity = model.entity; 2580 if (Object.keys(this.previousValues).length === 0) { 2581 for (const key of Object.keys(this.changes)) { 2582 this.previousValues[key] = entity[key]; 2583 } 2584 } 2585 for (const [key, value] of Object.entries(this.changes)) { 2586 entity[key] = value; 2587 } 2588 entity.isChanged = true; 2589 this.onVisualUpdate(); 2590 } 2591 undo() { 2592 const model = this.database.Models.get(this.buildingId); 2593 if (!model) 2594 return; 2595 const entity = model.entity; 2596 for (const [key, value] of Object.entries(this.previousValues)) { 2597 entity[key] = value; 2598 } 2599 entity.isChanged = true; 2600 this.onVisualUpdate(); 2601 } 2602 buildDescription() { 2603 if (this.changes.name !== void 0) { 2604 return `Rename building to "${this.changes.name}"`; 2605 } 2606 if (this.changes.address !== void 0) { 2607 return "Update building address"; 2608 } 2609 if (this.changes.defaultDestinationNodeId !== void 0) { 2610 return this.changes.defaultDestinationNodeId ? "Update building default destination" : "Clear building default destination"; 2611 } 2612 return "Update building"; 2613 } 2614}; 2615
2616// src/app/pages/campus-map-v3/commands/update-room-command.ts 2617var UpdateRoomCommand = class extends BaseEditorCommand { 2618 constructor(config) { 2619 super(); 2620 this.previousValues = {}; 2621 this.roomId = config.roomId; 2622 this.changes = config.changes; 2623 this.database = config.database; 2624 this.onVisualUpdate = config.onVisualUpdate; 2625 this.description = this.buildDescription(); 2626 } 2627 execute() { 2628 const model = this.database.Models.get(this.roomId); 2629 if (!model) 2630 return; 2631 const entity = model.entity; 2632 if (Object.keys(this.previousValues).length === 0) { 2633 for (const key of Object.keys(this.changes)) { 2634 this.previousValues[key] = entity[key]; 2635 } 2636 } 2637 for (const [key, value] of Object.entries(this.changes)) { 2638 entity[key] = value; 2639 } 2640 entity.isChanged = true; 2641 this.onVisualUpdate(); 2642 } 2643 undo() { 2644 const model = this.database.Models.get(this.roomId); 2645 if (!model) 2646 return; 2647 const entity = model.entity; 2648 for (const [key, value] of Object.entries(this.previousValues)) { 2649 entity[key] = value; 2650 } 2651 entity.isChanged = true; 2652 this.onVisualUpdate(); 2653 } 2654 buildDescription() { 2655 const changedFields = Object.keys(this.changes); 2656 if (changedFields.length === 0) { 2657 return "Update room"; 2658 } 2659 if (changedFields.includes("name") && changedFields.length === 1) { 2660 return `Rename room to "${this.changes.name}"`; 2661 } 2662 if (changedFields.includes("number") && changedFields.length === 1) { 2663 return `Change room number to "${this.changes.number}"`; 2664 } 2665 if (changedFields.includes("roomType") && changedFields.length === 1) { 2666 return `Change room type to ${this.changes.roomType}`; 2667 } 2668 if (changedFields.includes("hideLabel") && changedFields.length === 1) { 2669 return this.changes.hideLabel ? "Hide room label" : "Show room label"; 2670 } 2671 return "Update room details"; 2672 } 2673}; 2674
2675// src/app/pages/campus-map-v3/commands/update-ground-patch-command.ts 2676var UpdateGroundPatchCommand = class extends BaseEditorCommand { 2677 constructor(config) { 2678 super(); 2679 this.previousValues = {}; 2680 this.groundPatchId = config.groundPatchId; 2681 this.changes = config.changes; 2682 this.database = config.database; 2683 this.onVisualUpdate = config.onVisualUpdate; 2684 this.description = this.buildDescription(); 2685 } 2686 execute() { 2687 const model = this.database.Models.get(this.groundPatchId); 2688 if (!model) 2689 return; 2690 const entity = model.entity; 2691 if (Object.keys(this.previousValues).length === 0) { 2692 for (const key of Object.keys(this.changes)) { 2693 this.previousValues[key] = entity[key]; 2694 } 2695 } 2696 for (const [key, value] of Object.entries(this.changes)) { 2697 entity[key] = value; 2698 } 2699 entity.isChanged = true; 2700 this.onVisualUpdate(); 2701 } 2702 undo() { 2703 const model = this.database.Models.get(this.groundPatchId); 2704 if (!model) 2705 return; 2706 const entity = model.entity; 2707 for (const [key, value] of Object.entries(this.previousValues)) { 2708 entity[key] = value; 2709 } 2710 entity.isChanged = true; 2711 this.onVisualUpdate(); 2712 } 2713 buildDescription() { 2714 const changedFields = Object.keys(this.changes); 2715 if (changedFields.length === 0) { 2716 return "Update ground patch"; 2717 } 2718 if (changedFields.includes("type") && changedFields.length === 1) { 2719 return `Change ground type to ${this.changes.type}`; 2720 } 2721 return "Update ground patch"; 2722 } 2723}; 2724
2725// src/app/pages/campus-map-v3/commands/create-building-command.ts 2726var CreateBuildingCommand = class extends BaseEditorCommand { 2727 constructor(config) { 2728 var _a3; 2729 super(); 2730 this.createdBuildingId = null; 2731 this.building = config.building; 2732 this.database = config.database; 2733 this.onVisualUpdate = config.onVisualUpdate; 2734 this.onBuildingCreated = config.onBuildingCreated; 2735 this.description = `Create building "${(_a3 = this.building.name) != null ? _a3 : "New Building"}"`; 2736 } 2737 execute() { 2738 return __async(this, null, function* () { 2739 if (!this.building.id) { 2740 this.building.id = crypto.randomUUID(); 2741 } 2742 this.building.isDeleted = false; 2743 this.building.isChanged = true; 2744 this.building.version = 0; 2745 const model = yield this.database.addEntity(this.building, false); 2746 this.createdBuildingId = this.building.id; 2747 this.database.fixModelRelations(model); 2748 this.onVisualUpdate(); 2749 if (this.onBuildingCreated && this.createdBuildingId) { 2750 this.onBuildingCreated(this.createdBuildingId); 2751 } 2752 }); 2753 } 2754 undo() { 2755 if (!this.createdBuildingId) 2756 return; 2757 const model = this.database.Models.get(this.createdBuildingId); 2758 if (!model) 2759 return; 2760 model.entity.isDeleted = true; 2761 model.entity.isChanged = true; 2762 this.database.Models.delete(this.createdBuildingId); 2763 this.onVisualUpdate(); 2764 } 2765 redo() { 2766 if (!this.createdBuildingId) { 2767 this.execute(); 2768 return; 2769 } 2770 const buildingModel = new BuildingModel(this.building); 2771 this.building.isDeleted = false; 2772 this.building.isChanged = true; 2773 this.database.Models.set(this.createdBuildingId, buildingModel); 2774 this.database.fixModelRelations(buildingModel); 2775 this.onVisualUpdate(); 2776 if (this.onBuildingCreated) { 2777 this.onBuildingCreated(this.createdBuildingId); 2778 } 2779 } 2780}; 2781
2782// src/app/pages/campus-map-v3/commands/create-floor-command.ts 2783var CreateFloorCommand = class extends BaseEditorCommand { 2784 constructor(config) { 2785 var _a3; 2786 super(); 2787 this.createdFloorId = null; 2788 this.floor = config.floor; 2789 this.database = config.database; 2790 this.onVisualUpdate = config.onVisualUpdate; 2791 this.onFloorCreated = config.onFloorCreated; 2792 this.description = `Create floor "${(_a3 = this.floor.name) != null ? _a3 : "New Floor"}"`; 2793 } 2794 execute() { 2795 return __async(this, null, function* () { 2796 if (!this.floor.id) { 2797 this.floor.id = crypto.randomUUID(); 2798 } 2799 this.floor.isDeleted = false; 2800 this.floor.isChanged = true; 2801 this.floor.version = 0; 2802 const model = yield this.database.addEntity(this.floor, false); 2803 this.createdFloorId = this.floor.id; 2804 this.database.fixModelRelations(model); 2805 this.onVisualUpdate(); 2806 if (this.onFloorCreated && this.createdFloorId) { 2807 this.onFloorCreated(this.createdFloorId); 2808 } 2809 }); 2810 } 2811 undo() { 2812 if (!this.createdFloorId) 2813 return; 2814 const model = this.database.Models.get(this.createdFloorId); 2815 if (!model) 2816 return; 2817 model.entity.isDeleted = true; 2818 model.entity.isChanged = true; 2819 this.database.Models.delete(this.createdFloorId); 2820 this.onVisualUpdate(); 2821 } 2822 redo() { 2823 if (!this.createdFloorId) { 2824 this.execute(); 2825 return; 2826 } 2827 const floorModel = new FloorModel(this.floor); 2828 this.floor.isDeleted = false; 2829 this.floor.isChanged = true; 2830 this.database.Models.set(this.createdFloorId, floorModel); 2831 this.database.fixModelRelations(floorModel); 2832 this.onVisualUpdate(); 2833 if (this.onFloorCreated) { 2834 this.onFloorCreated(this.createdFloorId); 2835 } 2836 } 2837}; 2838
2839// src/app/pages/campus-map-v3/commands/create-room-command.ts 2840var CreateRoomCommand = class extends BaseEditorCommand { 2841 constructor(config) { 2842 var _a3, _b2; 2843 super(); 2844 this.createdRoomId = null; 2845 this.room = config.room; 2846 this.database = config.database; 2847 this.onVisualUpdate = config.onVisualUpdate; 2848 this.onRoomCreated = config.onRoomCreated; 2849 this.description = `Create room "${(_b2 = (_a3 = this.room.name) != null ? _a3 : this.room.number) != null ? _b2 : "New Room"}"`; 2850 } 2851 execute() { 2852 return __async(this, null, function* () { 2853 if (!this.room.id) { 2854 this.room.id = crypto.randomUUID(); 2855 } 2856 this.room.isDeleted = false; 2857 this.room.isChanged = true; 2858 this.room.version = 0; 2859 const model = yield this.database.addEntity(this.room, false); 2860 this.createdRoomId = this.room.id; 2861 this.database.fixModelRelations(model); 2862 this.onVisualUpdate(); 2863 if (this.onRoomCreated && this.createdRoomId) { 2864 this.onRoomCreated(this.createdRoomId); 2865 } 2866 }); 2867 } 2868 undo() { 2869 if (!this.createdRoomId) 2870 return; 2871 const model = this.database.Models.get(this.createdRoomId); 2872 if (!model) 2873 return; 2874 model.entity.isDeleted = true; 2875 model.entity.isChanged = true; 2876 this.database.Models.delete(this.createdRoomId); 2877 this.onVisualUpdate(); 2878 } 2879 redo() { 2880 if (!this.createdRoomId) { 2881 this.execute(); 2882 return; 2883 } 2884 const roomModel = new RoomModel(this.room); 2885 this.room.isDeleted = false; 2886 this.room.isChanged = true; 2887 this.database.Models.set(this.createdRoomId, roomModel); 2888 this.database.fixModelRelations(roomModel); 2889 this.onVisualUpdate(); 2890 if (this.onRoomCreated) { 2891 this.onRoomCreated(this.createdRoomId); 2892 } 2893 } 2894}; 2895
2896// src/app/pages/campus-map-v3/services/entity-edit.service.ts 2897var _EntityEditService = class _EntityEditService { 2898 constructor() { 2899 this.database = inject(DatabaseService); 2900 this.historyService = inject(EditorHistoryService); 2901 this.treeStateService = inject(TreeStateService); 2902 this._activePanel = signal(null); 2903 this._editingEntity = signal(null); 2904 this._isNew = signal(false); 2905 this._pendingChanges = signal({}); 2906 this._extraContext = signal({}); 2907 this.onVisualUpdate = null; 2908 this.activePanel = this._activePanel.asReadonly(); 2909 this.isNew = this._isNew.asReadonly(); 2910 this.showNodeEditPanel = computed(() => this._activePanel() === "node"); 2911 this.showFloorEditPanel = computed(() => this._activePanel() === "floor"); 2912 this.showBuildingEditPanel = computed(() => this._activePanel() === "building"); 2913 this.showRoomEditPanel = computed(() => this._activePanel() === "room"); 2914 this.showGroundPatchEditPanel = computed(() => this._activePanel() === "groundPatch"); 2915 this.editingNode = computed(() => this._activePanel() === "node" ? this._editingEntity() : null); 2916 this.editingFloor = computed(() => this._activePanel() === "floor" ? this._editingEntity() : null); 2917 this.editingBuilding = computed(() => this._activePanel() === "building" ? this._editingEntity() : null); 2918 this.editingRoom = computed(() => this._activePanel() === "room" ? this._editingEntity() : null); 2919 this.editingGroundPatch = computed(() => this._activePanel() === "groundPatch" ? this._editingEntity() : null); 2920 this.editingNodeFloorName = computed(() => { 2921 var _a3; 2922 return (_a3 = this._extraContext()["floorName"]) != null ? _a3 : ""; 2923 }); 2924 this.isNodeNew = computed(() => this._activePanel() === "node" ? this._isNew() : false); 2925 this.isBuildingNew = computed(() => this._activePanel() === "building" ? this._isNew() : false); 2926 this.isRoomNew = computed(() => this._activePanel() === "room" ? this._isNew() : false); 2927 this.isGroundPatchNew = computed(() => this._activePanel() === "groundPatch" ? this._isNew() : false); 2928 this.isFloorNew = computed(() => this._activePanel() === "floor" ? this._isNew() : false); 2929 } 2930 /** 2931 * Set the callback for visual updates after commands execute. 2932 */ 2933 setOnVisualUpdate(callback) { 2934 this.onVisualUpdate = callback; 2935 } 2936 // ========================================================================= 2937 // Node Panel 2938 // ========================================================================= 2939 /** 2940 * Open the node edit panel. 2941 */ 2942 openNodePanel(nodeId, isNew) { 2943 const model = this.database.Models.get(nodeId); 2944 if (!model || model.type !== EntityType.NODE) 2945 return; 2946 const nodeModel = model; 2947 this._editingEntity.set(nodeModel); 2948 this._isNew.set(isNew); 2949 this._pendingChanges.set({}); 2950 this._extraContext.set({ 2951 floorName: this.getNodeFloorDisplayName(nodeModel) 2952 }); 2953 this._activePanel.set("node"); 2954 } 2955 /** 2956 * Get display name for a node's floor. 2957 */ 2958 getNodeFloorDisplayName(node) { 2959 if (!node.floor) { 2960 return node.entity.type === NodeType.Outdoor ? "Outdoor" : "No floor"; 2961 } 2962 const floor = node.floor; 2963 const building = floor.building; 2964 const levelDisplay = floor.entity.level === 1 ? "G" : `${floor.entity.level}`; 2965 if (building == null ? void 0 : building.entity.name) { 2966 return `${building.entity.name} - Level ${levelDisplay}`; 2967 } 2968 return `Level ${levelDisplay}`; 2969 } 2970 // ========================================================================= 2971 // Floor Panel 2972 // ========================================================================= 2973 /** 2974 * Open the floor edit panel. 2975 */ 2976 openFloorPanel(floorId) { 2977 const model = this.database.Models.get(floorId); 2978 if (!model || model.type !== EntityType.FLOOR) 2979 return; 2980 this._editingEntity.set(model); 2981 this._isNew.set(false); 2982 this._pendingChanges.set({}); 2983 this._extraContext.set({}); 2984 this._activePanel.set("floor"); 2985 } 2986 /** 2987 * Open the floor edit panel in create mode with a new floor. 2988 */ 2989 openNewFloorPanel(tempModel) { 2990 this._editingEntity.set(tempModel); 2991 this._isNew.set(true); 2992 this._pendingChanges.set({}); 2993 this._extraContext.set({}); 2994 this._activePanel.set("floor"); 2995 } 2996 // ========================================================================= 2997 // Building Panel 2998 // ========================================================================= 2999 /** 3000 * Open the building edit panel for an existing building. 3001 */ 3002 openBuildingPanel(buildingId) { 3003 const model = this.database.Models.get(buildingId); 3004 if (!model || model.type !== EntityType.BUILDING) 3005 return; 3006 this._editingEntity.set(model); 3007 this._isNew.set(false); 3008 this._pendingChanges.set({}); 3009 this._extraContext.set({}); 3010 this._activePanel.set("building"); 3011 } 3012 /** 3013 * Open the building edit panel in create mode with a new building. 3014 */ 3015 openNewBuildingPanel(tempModel) { 3016 this._editingEntity.set(tempModel); 3017 this._isNew.set(true); 3018 this._pendingChanges.set({}); 3019 this._extraContext.set({}); 3020 this._activePanel.set("building"); 3021 } 3022 // ========================================================================= 3023 // Room Panel 3024 // ========================================================================= 3025 /** 3026 * Open the room edit panel for an existing room. 3027 */ 3028 openRoomPanel(roomId, isNew = false) { 3029 const model = this.database.Models.get(roomId); 3030 if (!model || model.type !== EntityType.ROOM) 3031 return; 3032 this._editingEntity.set(model); 3033 this._isNew.set(isNew); 3034 this._pendingChanges.set({}); 3035 this._extraContext.set({}); 3036 this._activePanel.set("room"); 3037 } 3038 /** 3039 * Open the room edit panel in create mode with a new room. 3040 */ 3041 openNewRoomPanel(tempModel) { 3042 this._editingEntity.set(tempModel); 3043 this._isNew.set(true); 3044 this._pendingChanges.set({}); 3045 this._extraContext.set({}); 3046 this._activePanel.set("room"); 3047 } 3048 // ========================================================================= 3049 // Ground Patch Panel 3050 // ========================================================================= 3051 /** 3052 * Open the ground patch edit panel. 3053 */ 3054 openGroundPatchPanel(groundPatchId, isNew = false) { 3055 const model = this.database.Models.get(groundPatchId); 3056 if (!model || model.type !== EntityType.GROUND_PATCH) 3057 return; 3058 this._editingEntity.set(model); 3059 this._isNew.set(isNew); 3060 this._pendingChanges.set({}); 3061 this._extraContext.set({}); 3062 this._activePanel.set("groundPatch"); 3063 } 3064 // ========================================================================= 3065 // Generic Operations 3066 // ========================================================================= 3067 /** 3068 * Record a change to the pending changes. 3069 */
3070 recordChange(changes) { 3071 this._pendingChanges.update((current) => __spreadValues(__spreadValues({}, current), changes)); 3072 } 3073 /** 3074 * Check if there are pending changes. 3075 */ 3076 hasPendingChanges() { 3077 return Object.keys(this._pendingChanges()).length > 0; 3078 } 3079 /** 3080 * Get the pending changes. 3081 */ 3082 getPendingChanges() { 3083 return this._pendingChanges(); 3084 } 3085 /** 3086 * Apply pending changes and close the panel. 3087 * Creates the appropriate command based on the active panel type. 3088 */ 3089 apply() { 3090 const panel = this._activePanel(); 3091 const entity = this._editingEntity(); 3092 const changes = this._pendingChanges(); 3093 const isNew = this._isNew(); 3094 if (!panel || !entity) { 3095 this.closePanel(); 3096 return; 3097 } 3098 if (!isNew && Object.keys(changes).length === 0) { 3099 this.closePanel(); 3100 return; 3101 } 3102 switch (panel) { 3103 case "node": 3104 this.applyNodeChanges(entity, changes); 3105 break; 3106 case "floor": 3107 this.applyFloorChanges(entity, changes, isNew); 3108 break; 3109 case "building": 3110 this.applyBuildingChanges(entity, changes, isNew); 3111 break; 3112 case "room": 3113 this.applyRoomChanges(entity, changes, isNew); 3114 break; 3115 case "groundPatch": 3116 this.applyGroundPatchChanges(entity, changes); 3117 break; 3118 } 3119 this.closePanel(); 3120 } 3121 /** 3122 * Cancel editing and close the panel. 3123 */ 3124 cancel() { 3125 this.closePanel(); 3126 } 3127 /** 3128 * Close the active panel. 3129 */ 3130 closePanel() { 3131 this._activePanel.set(null); 3132 this._editingEntity.set(null); 3133 this._isNew.set(false); 3134 this._pendingChanges.set({}); 3135 this._extraContext.set({}); 3136 } 3137 // ========================================================================= 3138 // Apply Helpers 3139 // ========================================================================= 3140 applyNodeChanges(node, changes) { 3141 const command = new UpdateNodeCommand({ 3142 nodeId: node.entity.id, 3143 changes, 3144 database: this.database, 3145 onVisualUpdate: () => { 3146 var _a3; 3147 return (_a3 = this.onVisualUpdate) == null ? void 0 : _a3.call(this); 3148 } 3149 }); 3150 this.historyService.executeCommand(command); 3151 } 3152 applyFloorChanges(floor, changes, isNew) { 3153 var _a3, _b2; 3154 if (isNew) { 3155 const newFloor = Object.assign(new Floor(), __spreadProps(__spreadValues({}, floor.entity), { 3156 name: (_a3 = changes["name"]) != null ? _a3 : floor.entity.name, 3157 level: (_b2 = changes["level"]) != null ? _b2 : floor.entity.level 3158 })); 3159 const command = new CreateFloorCommand({ 3160 floor: newFloor, 3161 database: this.database, 3162 onVisualUpdate: () => { 3163 var _a4; 3164 return (_a4 = this.onVisualUpdate) == null ? void 0 : _a4.call(this); 3165 }, 3166 onFloorCreated: () => { 3167 this.treeStateService.refresh(); 3168 } 3169 }); 3170 this.historyService.executeCommand(command); 3171 } else { 3172 const command = new UpdateFloorCommand({ 3173 floorId: floor.entity.id, 3174 changes, 3175 database: this.database, 3176 onVisualUpdate: () => { 3177 var _a4; 3178 return (_a4 = this.onVisualUpdate) == null ? void 0 : _a4.call(this); 3179 } 3180 }); 3181 this.historyService.executeCommand(command); 3182 } 3183 } 3184 applyBuildingChanges(building, changes, isNew) { 3185 var _a3; 3186 if (isNew) { 3187 const newBuilding = Object.assign(new Building(), __spreadProps(__spreadValues({}, building.entity), { 3188 name: (_a3 = changes["name"]) != null ? _a3 : building.entity.name, 3189 address: Object.prototype.hasOwnProperty.call(changes, "address") ? changes["address"] : building.entity.address 3190 })); 3191 const command = new CreateBuildingCommand({ 3192 building: newBuilding, 3193 database: this.database, 3194 onVisualUpdate: () => { 3195 var _a4; 3196 return (_a4 = this.onVisualUpdate) == null ? void 0 : _a4.call(this); 3197 }, 3198 onBuildingCreated: () => { 3199 this.treeStateService.refresh(); 3200 } 3201 }); 3202 this.historyService.executeCommand(command); 3203 } else { 3204 const command = new UpdateBuildingCommand({ 3205 buildingId: building.entity.id, 3206 changes, 3207 database: this.database, 3208 onVisualUpdate: () => { 3209 var _a4; 3210 return (_a4 = this.onVisualUpdate) == null ? void 0 : _a4.call(this); 3211 } 3212 }); 3213 this.historyService.executeCommand(command); 3214 } 3215 } 3216 applyRoomChanges(room, changes, isNew) { 3217 var _a3, _b2, _c, _d, _e; 3218 if (isNew) { 3219 const newRoom = Object.assign(new Room(), __spreadProps(__spreadValues({}, room.entity), { 3220 number: (_a3 = changes["number"]) != null ? _a3 : room.entity.number, 3221 name: (_b2 = changes["name"]) != null ? _b2 : room.entity.name, 3222 description: (_c = changes["description"]) != null ? _c : room.entity.description, 3223 roomType: (_d = changes["roomType"]) != null ? _d : room.entity.roomType, 3224 hideLabel: (_e = changes["hideLabel"]) != null ? _e : room.entity.hideLabel 3225 })); 3226 const command = new CreateRoomCommand({ 3227 room: newRoom, 3228 database: this.database, 3229 onVisualUpdate: () => { 3230 var _a4; 3231 return (_a4 = this.onVisualUpdate) == null ? void 0 : _a4.call(this); 3232 }, 3233 onRoomCreated: () => { 3234 this.treeStateService.refresh(); 3235 } 3236 }); 3237 this.historyService.executeCommand(command); 3238 } else { 3239 const command = new UpdateRoomCommand({ 3240 roomId: room.entity.id, 3241 changes, 3242 database: this.database, 3243 onVisualUpdate: () => { 3244 var _a4; 3245 return (_a4 = this.onVisualUpdate) == null ? void 0 : _a4.call(this); 3246 } 3247 }); 3248 this.historyService.executeCommand(command); 3249 } 3250 } 3251 applyGroundPatchChanges(groundPatch, changes) { 3252 if (Object.keys(changes).length > 0) { 3253 const command = new UpdateGroundPatchCommand({ 3254 groundPatchId: groundPatch.entity.id, 3255 changes, 3256 database: this.database, 3257 onVisualUpdate: () => { 3258 var _a3; 3259 return (_a3 = this.onVisualUpdate) == null ? void 0 : _a3.call(this); 3260 } 3261 }); 3262 this.historyService.executeCommand(command); 3263 } 3264 } 3265}; 3266_EntityEditService.\u0275fac = function EntityEditService_Factory(__ngFactoryType__) { 3267 return new (__ngFactoryType__ || _EntityEditService)(); 3268}; 3269_EntityEditService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _EntityEditService, factory: _EntityEditService.\u0275fac }); 3270var EntityEditService = _EntityEditService; 3271(() => { 3272 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(EntityEditService, [{ 3273 type: Injectable 3274 }], null, null); 3275})(); 3276
3277// src/app/pages/campus-map-v3/services/model-3d.service.ts 3278var _Model3DService = class _Model3DService { 3279 constructor() { 3280 this.apiService = inject(ApiService); 3281 this.databaseService = inject(DatabaseService); 3282 this.model3dConfigBuilder = inject(Model3DConfigBuilderService); 3283 this._campusId = null; 3284 this._models = signal([]); 3285 this._activeModelId = signal(null); 3286 this._show3DModel = signal(false); 3287 this._isLoading = signal(false); 3288 this._loadProgress = signal(0); 3289 this._showTransformPanel = signal(false); 3290 this._currentTransform = signal(__spreadValues({}, DEFAULT_MODEL_3D_TRANSFORM)); 3291 this._isTransformSaving = signal(false); 3292 this._gamma = signal(1); 3293 this.gamma = this._gamma.asReadonly(); 3294 this._isRulerActive = signal(false); 3295 this.models = this._models.asReadonly(); 3296 this.activeModelId = this._activeModelId.asReadonly(); 3297 this.show3DModel = this._show3DModel.asReadonly(); 3298 this.isLoading = this._isLoading.asReadonly(); 3299 this.loadProgress = this._loadProgress.asReadonly(); 3300 this.showTransformPanel = this._showTransformPanel.asReadonly(); 3301 this.currentTransform = this._currentTransform.asReadonly(); 3302 this.isTransformSaving = this._isTransformSaving.asReadonly(); 3303 this.isRulerActive = this._isRulerActive.asReadonly(); 3304 this.has3DModel = computed(() => this._models().some((model) => !!model.entity.modelUploadId)); 3305 this.modelOptions = computed(() => this._models().filter((model) => !!model.entity.modelUploadId && !!model.entity.id).map((model) => ({ 3306 id: model.entity.id, 3307 name: model.entity.name || model.entity.id 3308 }))); 3309 this.activeModel = computed(() => { 3310 var _a3; 3311 const activeId = this._activeModelId(); 3312 const models = this._models(); 3313 return (_a3 = models.find((model) => model.entity.id === activeId)) != null ? _a3 : null; 3314 }); 3315 this.canShowSettings = computed(() => this._show3DModel() && this.has3DModel()); 3316 } 3317 // ========================================================================= 3318 // Model list / active model 3319 // ========================================================================= 3320 /** 3321 (Re)read the campus' 3D model entities from the local entity database. 3322 Keeps the current active model when it still exists; otherwise falls back 3323 to the first model with an FBX upload and reloads the panel transform. 3324 */ 3325 refreshModels() { 3326 var _a3; 3327 const models = this._campusId ? this.databaseService.getCampus3DModels(this._campusId) : []; 3328 this._models.set(models); 3329 const usableIds = models.filter((model) => !!model.entity.modelUploadId && !!model.entity.id).map((model) => model.entity.id); 3330 const current = this._activeModelId(); 3331 if (current && usableIds.includes(current)) { 3332 return; 3333 } 3334 this._activeModelId.set((_a3 = usableIds[0]) != null ? _a3 : null); 3335 this.loadActiveModelTransform(); 3336 } 3337 /** 3338 * Load 3D model info from the local entity system for a campus. 3339 * Returns null when the campus has no usable (FBX-bearing) model. 3340 */ 3341 loadModelInfo(campusId) { 3342 return __async(this, null, function* () { 3343 this._campusId = campusId; 3344 this._activeModelId.set(null); 3345 this.refreshModels(); 3346 const models = this._models(); 3347 return this.has3DModel() ? models : null; 3348 }); 3349 } 3350 /** 3351 * Select the model the transform panel (and ruler scaling) edits. 3352 * Reloads the slider values from that model's entity. 3353 */ 3354 setActiveModel(modelId) { 3355 if (this._activeModelId() === modelId) { 3356 return; 3357 } 3358 this._activeModelId.set(modelId); 3359 this.loadActiveModelTransform(); 3360 } 3361 /** Non-reactive accessor for the active model id. */ 3362 getActiveModelId() { 3363 return this._activeModelId(); 3364 } 3365 /** Non-reactive accessor for the active model entity. */ 3366 getActiveModel() { 3367 return this.activeModel(); 3368 } 3369 /** 3370 * Build the renderer config from all campus models that have an FBX upload. 3371 */ 3372 buildModel3DConfig() { 3373 return this.model3dConfigBuilder.fromCampus3DModels(this._models()); 3374 } 3375 // ========================================================================= 3376 // 3D Model Visibility 3377 // ========================================================================= 3378 /** 3379 * Toggle 3D model visibility. 3380 */ 3381 toggle3DModel(visible) { 3382 this._show3DModel.set(visible); 3383 } 3384 /** 3385 * Set loading state. 3386 */ 3387 setLoadingState(loading, progress) { 3388 this._isLoading.set(loading); 3389 if (progress !== void 0) { 3390 this._loadProgress.set(progress); 3391 } 3392 } 3393 /** 3394 * Set load complete. 3395 */ 3396 setLoadComplete() { 3397 this._isLoading.set(false); 3398 this._loadProgress.set(100); 3399 } 3400 /** 3401 * Set load error. 3402 */ 3403 setLoadError() { 3404 this._isLoading.set(false); 3405 this._loadProgress.set(0); 3406 this._show3DModel.set(false); 3407 } 3408 // ========================================================================= 3409 // Transform Panel 3410 // ========================================================================= 3411 /** 3412 * Toggle transform panel visibility. 3413 */ 3414 toggleTransformPanel() { 3415 this._showTransformPanel.update((v) => !v); 3416 } 3417 /** 3418 * Close transform panel. 3419 */ 3420 closeTransformPanel() { 3421 this._showTransformPanel.set(false); 3422 } 3423 /** 3424 * Get the active model's current transform (non-reactive). 3425 */ 3426 getTransform() { 3427 return this._currentTransform(); 3428 } 3429 /** 3430 * Update the active model's transform (for real-time preview). 3431 */ 3432 updateTransform(partial) { 3433 this._currentTransform.update((t) => __spreadValues(__spreadValues({}, t), partial)); 3434 if (partial.gamma !== void 0) { 3435 this._gamma.set(Math.max(0.5, Math.min(5, partial.gamma))); 3436 } 3437 } 3438 /** 3439 * Get the active model's current gamma value. 3440 */ 3441 getGamma() { 3442 return this._gamma(); 3443 } 3444 /** 3445 * Update gamma value of the active model. 3446 */ 3447 updateGamma(value) { 3448 const clamped = Math.max(0.5, Math.min(5, value)); 3449 this._gamma.set(clamped); 3450 this._currentTransform.update((t) => __spreadProps(__spreadValues({}, t), { gamma: clamped })); 3451 } 3452 /** 3453 * Reset the active model's transform to defaults. 3454 */ 3455 resetTransform() { 3456 this._currentTransform.set(__spreadValues({}, DEFAULT_MODEL_3D_TRANSFORM)); 3457 this._gamma.set(DEFAULT_MODEL_3D_TRANSFORM.gamma); 3458 } 3459 /** 3460 * Save the active model's transform (incl. gamma) via the REST API, then 3461 * mirror the server state back into the local entity database. 3462 * Returns false when there is nothing to save; API failures are thrown so 3463 * the caller's save flow can keep the unsaved-changes history. 3464 */ 3465 saveTransform(_campusId) { 3466 return __async(this, null, function* () { 3467 if (this._isTransformSaving()) 3468 return false;
3469 const modelId = this._activeModelId(); 3470 if (!modelId) 3471 return false; 3472 this._isTransformSaving.set(true); 3473 try { 3474 const transform = this._currentTransform(); 3475 yield firstValueFrom(this.apiService.updateCampusModel3DTransform(modelId, { 3476 offsetX: transform.offsetX, 3477 offsetY: transform.offsetY, 3478 offsetZ: transform.offsetZ, 3479 rotationX: transform.rotationX, 3480 rotationY: transform.rotationY, 3481 rotationZ: transform.rotationZ, 3482 scale: transform.scale, 3483 gamma: transform.gamma 3484 })); 3485 yield this.refreshModelFromServer(modelId); 3486 return true; 3487 } catch (error) { 3488 console.error("Failed to persist campus 3D model transform:", error); 3489 throw error; 3490 } finally { 3491 this._isTransformSaving.set(false); 3492 } 3493 }); 3494 } 3495 /** 3496 * Save only the active model's gamma (brightness) via the REST API. 3497 */ 3498 saveGamma() { 3499 return __async(this, null, function* () { 3500 if (this._isTransformSaving()) 3501 return false; 3502 const modelId = this._activeModelId(); 3503 if (!modelId) 3504 return false; 3505 this._isTransformSaving.set(true);
3506 try { 3507 yield firstValueFrom(this.apiService.updateCampusModel3DTransform(modelId, { 3508 gamma: this._gamma() 3509 })); 3510 yield this.refreshModelFromServer(modelId); 3511 return true; 3512 } catch (error) { 3513 console.error("Failed to persist campus 3D model gamma:", error); 3514 throw error; 3515 } finally { 3516 this._isTransformSaving.set(false); 3517 } 3518 }); 3519 } 3520 // ========================================================================= 3521 // Ruler Tool 3522 // ========================================================================= 3523 /** 3524 * Toggle ruler tool. 3525 */ 3526 toggleRuler(active) { 3527 this._isRulerActive.set(active); 3528 } 3529 /** 3530 * Apply scale from measurement (against the active model). 3531 * Returns the new scale value. 3532 */ 3533 calculateScaleFromMeasurement(modelDistance, realLengthCm) { 3534 const currentScale = this._currentTransform().scale; 3535 const newScale = realLengthCm / modelDistance * currentScale; 3536 return Math.max(0.01, newScale); 3537 } 3538 /** 3539 * Update the active model's scale and return the previous scale for undo. 3540 */ 3541 setScale(newScale) { 3542 const previousScale = this._currentTransform().scale; 3543 this._currentTransform.update((t) => __spreadProps(__spreadValues({}, t), { scale: newScale })); 3544 return previousScale; 3545 } 3546 // ========================================================================= 3547 // Private helpers 3548 // ========================================================================= 3549 /** 3550 * Pull the latest server state for one model and mirror it into the local 3551 * entity database (which fires campusModelsChanged$). 3552 */ 3553 refreshModelFromServer(modelId) { 3554 return __async(this, null, function* () { 3555 const dto = yield firstValueFrom(this.apiService.getCampusModel3D(modelId)); 3556 yield this.databaseService.applyServerCampus3DModel(dto); 3557 this.refreshModels(); 3558 }); 3559 } 3560 /** 3561 * Load slider values (transform + gamma) from the active model's entity. 3562 */ 3563 loadActiveModelTransform() { 3564 var _a3, _b2, _c, _d, _e, _f, _g, _h, _i; 3565 const model = this.activeModel(); 3566 const entity = model == null ? void 0 : model.entity; 3567 if (!entity) { 3568 this._currentTransform.set(__spreadValues({}, DEFAULT_MODEL_3D_TRANSFORM)); 3569 this._gamma.set(DEFAULT_MODEL_3D_TRANSFORM.gamma); 3570 return; 3571 } 3572 this._currentTransform.set({ 3573 offsetX: (_a3 = entity.offsetX) != null ? _a3 : 0, 3574 offsetY: (_b2 = entity.offsetY) != null ? _b2 : 0, 3575 offsetZ: (_c = entity.offsetZ) != null ? _c : 0, 3576 rotationX: (_d = entity.rotationX) != null ? _d : 0, 3577 rotationY: (_e = entity.rotationY) != null ? _e : 0, 3578 rotationZ: (_f = entity.rotationZ) != null ? _f : 0, 3579 scale: (_g = entity.scale) != null ? _g : 1, 3580 gamma: (_h = entity.gamma) != null ? _h : 1 3581 }); 3582 this._gamma.set((_i = entity.gamma) != null ? _i : 1); 3583 } 3584}; 3585_Model3DService.\u0275fac = function Model3DService_Factory(__ngFactoryType__) { 3586 return new (__ngFactoryType__ || _Model3DService)(); 3587}; 3588_Model3DService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _Model3DService, factory: _Model3DService.\u0275fac }); 3589var Model3DService = _Model3DService; 3590(() => { 3591 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(Model3DService, [{ 3592 type: Injectable 3593 }], null, null); 3594})(); 3595
3596// src/app/pages/campus-map-v3/services/editor-mode.service.ts 3597var EditorMode; 3598(function(EditorMode2) { 3599 EditorMode2["None"] = "none"; 3600 EditorMode2["AddNode"] = "addNode"; 3601 EditorMode2["Connect"] = "connect"; 3602 EditorMode2["DrawRoom"] = "drawRoom"; 3603 EditorMode2["DrawFloorOutline"] = "drawFloorOutline"; 3604 EditorMode2["DrawGroundPatch"] = "drawGroundPatch"; 3605 EditorMode2["EditShape"] = "editShape"; 3606})(EditorMode || (EditorMode = {})); 3607var _EditorModeService = class _EditorModeService { 3608 constructor() { 3609 this._activeMode = signal(EditorMode.None); 3610 this._connectSourceNodeId = signal(null); 3611 this._editingEntityId = signal(null); 3612 this._editingEntityType = signal(null); 3613 this._drawingFloorId = signal(null); 3614 this._pendingFloorData = signal(null); 3615 this.activeMode = this._activeMode.asReadonly(); 3616 this.connectSourceNodeId = this._connectSourceNodeId.asReadonly(); 3617 this.editingEntityId = this._editingEntityId.asReadonly(); 3618 this.editingEntityType = this._editingEntityType.asReadonly(); 3619 this.drawingFloorId = this._drawingFloorId.asReadonly(); 3620 this.pendingFloorData = this._pendingFloorData.asReadonly(); 3621 this.editingRoomId = this._editingEntityId.asReadonly(); 3622 this.isAddNodeMode = computed(() => this._activeMode() === EditorMode.AddNode); 3623 this.isConnectMode = computed(() => this._activeMode() === EditorMode.Connect); 3624 this.isDrawingRoom = computed(() => this._activeMode() === EditorMode.DrawRoom); 3625 this.isDrawingFloorOutline = computed(() => this._activeMode() === EditorMode.DrawFloorOutline); 3626 this.isDrawingGroundPatch = computed(() => this._activeMode() === EditorMode.DrawGroundPatch); 3627 this.isEditingShape = computed(() => this._activeMode() === EditorMode.EditShape); 3628 this.isAnyModeActive = computed(() => this._activeMode() !== EditorMode.None); 3629 this.callbacks = null; 3630 } 3631 /** 3632 * Register callbacks from the page component. 3633 * Must be called during component initialization. 3634 */ 3635 setCallbacks(callbacks) { 3636 this.callbacks = callbacks; 3637 } 3638 // ========================================================================= 3639 // Generic Mode Lifecycle 3640 // ========================================================================= 3641 /** 3642 * Enter a new editor mode. 3643 * Automatically exits any currently active mode first. 3644 * 3645 * @param mode The mode to enter 3646 * @param param Optional parameter (sourceNodeId for Connect, roomId for EditShape) 3647 */ 3648 enterMode(mode, param) { 3649 if (!this.callbacks) { 3650 return; 3651 } 3652 if (this._activeMode() !== EditorMode.None) { 3653 this.exitCurrentMode(); 3654 } 3655 this._activeMode.set(mode); 3656 if (mode === EditorMode.Connect) { 3657 this._connectSourceNodeId.set(param != null ? param : null); 3658 } else if (mode === EditorMode.EditShape) { 3659 this._editingEntityId.set(param != null ? param : null); 3660 } else if (mode === EditorMode.DrawFloorOutline) { 3661 this._drawingFloorId.set(param != null ? param : null); 3662 } 3663 this.callbacks.disposeSelectionController(); 3664 if (mode === EditorMode.EditShape) { 3665 this.callbacks.disableDragControllers(); 3666 } else { 3667 this.callbacks.setCameraControlEnabled(false); 3668 } 3669 if (mode === EditorMode.DrawRoom || mode === EditorMode.DrawFloorOutline || mode === EditorMode.DrawGroundPatch) { 3670 this.callbacks.deselectAll(); 3671 } 3672 const controller = this.callbacks.getController(mode); 3673 if (controller) { 3674 controller.activate(param); 3675 } 3676 } 3677 /** 3678 * Exit the currently active mode and return to None. 3679 */ 3680 exitCurrentMode() { 3681 if (!this.callbacks) 3682 return; 3683 const currentMode = this._activeMode(); 3684 if (currentMode === EditorMode.None) 3685 return; 3686 const controller = this.callbacks.getController(currentMode); 3687 if (controller) { 3688 controller.deactivate(); 3689 } 3690 if (currentMode === EditorMode.EditShape) { 3691 this.callbacks.clearCornerHandles(); 3692 this.callbacks.clearEdgeMidpoints(); 3693 this.callbacks.refreshView(); 3694 this.callbacks.enableDragControllers(); 3695 } 3696 this.callbacks.setCameraControlEnabled(true); 3697 this.callbacks.createSelectionController(); 3698 this._activeMode.set(EditorMode.None); 3699 this._connectSourceNodeId.set(null); 3700 this._editingEntityId.set(null); 3701 this._editingEntityType.set(null); 3702 this._drawingFloorId.set(null); 3703 this._pendingFloorData.set(null); 3704 } 3705 // ========================================================================= 3706 // High-Level API 3707 // ========================================================================= 3708 /** 3709 * Toggle add node mode on/off. 3710 */ 3711 toggleAddNode() { 3712 if (this._activeMode() === EditorMode.AddNode) { 3713 this.exitCurrentMode(); 3714 } else { 3715 this.enterMode(EditorMode.AddNode); 3716 } 3717 } 3718 /** 3719 * Toggle connect mode on/off. 3720 * Requires a source node ID to start connecting from. 3721 * 3722 * @param sourceNodeId The node to connect from 3723 */ 3724 toggleConnect(sourceNodeId) { 3725 if (this._activeMode() === EditorMode.Connect) { 3726 this.exitCurrentMode(); 3727 } else { 3728 this.enterMode(EditorMode.Connect, sourceNodeId); 3729 } 3730 } 3731 /** 3732 * Toggle draw room mode on/off. 3733 */ 3734 toggleDrawRoom() { 3735 if (this._activeMode() === EditorMode.DrawRoom) { 3736 this.exitCurrentMode(); 3737 } else { 3738 this.enterMode(EditorMode.DrawRoom); 3739 } 3740 } 3741 /** 3742 * Toggle draw floor outline mode on/off for an existing floor. 3743 * Requires a floor ID to draw the outline for. 3744 * 3745 * @param floorId The floor to draw outline for 3746 */ 3747 toggleDrawFloorOutline(floorId) { 3748 if (this._activeMode() === EditorMode.DrawFloorOutline) { 3749 this.exitCurrentMode(); 3750 } else { 3751 this.enterMode(EditorMode.DrawFloorOutline, floorId); 3752 } 3753 } 3754 /** 3755 * Set pending floor data for immediate drawing. 3756 * Used when user selects building and wants to draw outline right away. 3757 * 3758 * @param floorData Data for the new floor to be created 3759 */ 3760 setPendingFloorData(floorData) { 3761 this._pendingFloorData.set(floorData); 3762 } 3763 /** 3764 * Start drawing floor outline for a new floor with pending data. 3765 * Must call setPendingFloorData() first. 3766 */ 3767 startDrawingFloorOutline() { 3768 if (this._activeMode() === EditorMode.DrawFloorOutline) { 3769 this.exitCurrentMode(); 3770 } 3771 this.enterMode(EditorMode.DrawFloorOutline, "new-floor-pending"); 3772 } 3773 /** 3774 * Enter draw floor outline mode for a NEW floor. 3775 * The floor will be created along with its outline when the polygon is completed. 3776 * 3777 * @param floorData Data for the new floor to be created 3778 */ 3779 enterDrawFloorOutlineForNewFloor(floorData) { 3780 if (this._activeMode() === EditorMode.DrawFloorOutline) { 3781 this.exitCurrentMode(); 3782 } 3783 this._pendingFloorData.set(floorData); 3784 this.enterMode(EditorMode.DrawFloorOutline, "new-floor-pending"); 3785 } 3786 /** 3787 * Toggle draw ground patch mode on/off. 3788 * Requires a campus ID to create ground patch in. 3789 * 3790 * @param campusId The campus to create ground patch in 3791 */ 3792 toggleDrawGroundPatch(campusId) { 3793 if (this._activeMode() === EditorMode.DrawGroundPatch) { 3794 this.exitCurrentMode(); 3795 } else { 3796 this.enterMode(EditorMode.DrawGroundPatch, campusId); 3797 } 3798 } 3799 /** 3800 * Enter shape edit mode for a specific entity (room or floor). 3801 * Does not toggle - use exitCurrentMode() to exit. 3802 *
3803 * @param entityId The entity ID to edit 3804 * @param entityType The type of entity ('room' or 'floor') 3805 */ 3806 enterShapeEdit(entityId, entityType = "room") { 3807 this._editingEntityType.set(entityType); 3808 this.enterMode(EditorMode.EditShape, entityId); 3809 } 3810 /** 3811 * Check if a specific mode is currently active. 3812 */ 3813 isModeActive(mode) { 3814 return this._activeMode() === mode; 3815 } 3816}; 3817_EditorModeService.\u0275fac = function EditorModeService_Factory(__ngFactoryType__) { 3818 return new (__ngFactoryType__ || _EditorModeService)(); 3819}; 3820_EditorModeService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _EditorModeService, factory: _EditorModeService.\u0275fac }); 3821var EditorModeService = _EditorModeService; 3822(() => { 3823 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(EditorModeService, [{ 3824 type: Injectable 3825 }], null, null); 3826})(); 3827
vendor: 4,981 bytes, lines 3828-3964
3828// node_modules/three/examples/jsm/renderers/CSS2DRenderer.js 3829var CSS2DObject = class extends Object3D { 3830 /** 3831 * Constructs a new CSS2D object. 3832 * 3833 * @param {HTMLElement} [element] - The DOM element. 3834 */ 3835 constructor(element = document.createElement("div")) { 3836 super(); 3837 this.isCSS2DObject = true; 3838 this.element = element; 3839 this.element.style.position = "absolute"; 3840 this.element.style.userSelect = "none"; 3841 this.element.setAttribute("draggable", false); 3842 this.center = new Vector2(0.5, 0.5); 3843 this.addEventListener("removed", function() { 3844 this.traverse(function(object) { 3845 if (object.element && object.element instanceof object.element.ownerDocument.defaultView.Element && object.element.parentNode !== null) { 3846 object.element.remove(); 3847 } 3848 }); 3849 }); 3850 } 3851 copy(source, recursive) { 3852 super.copy(source, recursive); 3853 this.element = source.element.cloneNode(true); 3854 this.center = source.center; 3855 return this; 3856 } 3857}; 3858var _vector = new Vector3(); 3859var _viewMatrix = new Matrix4(); 3860var _viewProjectionMatrix = new Matrix4(); 3861var _a = new Vector3(); 3862var _b = new Vector3(); 3863var CSS2DRenderer = class { 3864 /** 3865 * Constructs a new CSS2D renderer. 3866 * 3867 * @param {CSS2DRenderer~Parameters} [parameters] - The parameters. 3868 */ 3869 constructor(parameters = {}) { 3870 const _this = this; 3871 let _width, _height; 3872 let _widthHalf, _heightHalf; 3873 const cache = { 3874 objects: /* @__PURE__ */ new WeakMap() 3875 }; 3876 const domElement = parameters.element !== void 0 ? parameters.element : document.createElement("div"); 3877 domElement.style.overflow = "hidden"; 3878 this.domElement = domElement; 3879 this.sortObjects = true; 3880 this.getSize = function() { 3881 return { 3882 width: _width, 3883 height: _height 3884 }; 3885 }; 3886 this.render = function(scene, camera) { 3887 if (scene.matrixWorldAutoUpdate === true) scene.updateMatrixWorld(); 3888 if (camera.parent === null && camera.matrixWorldAutoUpdate === true) camera.updateMatrixWorld(); 3889 _viewMatrix.copy(camera.matrixWorldInverse); 3890 _viewProjectionMatrix.multiplyMatrices(camera.projectionMatrix, _viewMatrix); 3891 renderObject(scene, scene, camera); 3892 if (this.sortObjects) zOrder(scene); 3893 }; 3894 this.setSize = function(width, height) { 3895 _width = width; 3896 _height = height; 3897 _widthHalf = _width / 2; 3898 _heightHalf = _height / 2; 3899 domElement.style.width = width + "px"; 3900 domElement.style.height = height + "px"; 3901 }; 3902 function hideObject(object) { 3903 if (object.isCSS2DObject) object.element.style.display = "none"; 3904 for (let i = 0, l = object.children.length; i < l; i++) { 3905 hideObject(object.children[i]); 3906 } 3907 } 3908 function renderObject(object, scene, camera) { 3909 if (object.visible === false) { 3910 hideObject(object); 3911 return; 3912 } 3913 if (object.isCSS2DObject) { 3914 _vector.setFromMatrixPosition(object.matrixWorld); 3915 _vector.applyMatrix4(_viewProjectionMatrix); 3916 const visible = _vector.z >= -1 && _vector.z <= 1 && object.layers.test(camera.layers) === true; 3917 const element = object.element; 3918 element.style.display = visible === true ? "" : "none"; 3919 if (visible === true) { 3920 object.onBeforeRender(_this, scene, camera); 3921 element.style.transform = "translate(" + -100 * object.center.x + "%," + -100 * object.center.y + "%)translate(" + (_vector.x * _widthHalf + _widthHalf) + "px," + (-_vector.y * _heightHalf + _heightHalf) + "px)"; 3922 if (element.parentNode !== domElement) { 3923 domElement.appendChild(element); 3924 } 3925 object.onAfterRender(_this, scene, camera); 3926 } 3927 const objectData = { 3928 distanceToCameraSquared: getDistanceToSquared(camera, object) 3929 }; 3930 cache.objects.set(object, objectData); 3931 } 3932 for (let i = 0, l = object.children.length; i < l; i++) { 3933 renderObject(object.children[i], scene, camera); 3934 } 3935 } 3936 function getDistanceToSquared(object1, object2) { 3937 _a.setFromMatrixPosition(object1.matrixWorld); 3938 _b.setFromMatrixPosition(object2.matrixWorld); 3939 return _a.distanceToSquared(_b); 3940 } 3941 function filterAndFlatten(scene) { 3942 const result = []; 3943 scene.traverseVisible(function(object) { 3944 if (object.isCSS2DObject) result.push(object); 3945 }); 3946 return result; 3947 } 3948 function zOrder(scene) { 3949 const sorted = filterAndFlatten(scene).sort(function(a, b) { 3950 if (a.renderOrder !== b.renderOrder) { 3951 return b.renderOrder - a.renderOrder; 3952 } 3953 const distanceA = cache.objects.get(a).distanceToCameraSquared; 3954 const distanceB = cache.objects.get(b).distanceToCameraSquared; 3955 return distanceA - distanceB; 3956 }); 3957 const zMax = sorted.length; 3958 for (let i = 0, l = sorted.length; i < l; i++) { 3959 sorted[i].element.style.zIndex = zMax - i; 3960 } 3961 } 3962 } 3963}; 3964
3965// src/app/pages/campus-map-v3/utils/circle-shader.ts 3966var circleVertexShader = ( 3967 /* glsl */ 3968 ` 3969 varying vec2 vUv; 3970 3971 void main() { 3972 vUv = uv; 3973 gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 3974 } 3975` 3976); 3977var circleFragmentShader = ( 3978 /* glsl */ 3979 ` 3980 precision highp float; 3981 3982 uniform vec3 uColor; 3983 uniform float uOutlineWidth; 3984 uniform vec3 uOutlineColor; 3985 uniform bool uFillEnabled; 3986 3987 varying vec2 vUv; 3988 3989 void main() { 3990 vec2 center = vec2(0.5); 3991 float dist = length(vUv - center); 3992 float radius = 0.5; 3993 3994 // Screen-space anti-aliasing width based on fragment derivatives 3995 float aa = fwidth(dist) * 1.5; 3996 3997 if (uOutlineWidth > 0.0) { 3998 // Ring/outline mode: hollow circle 3999 float innerRadius = radius - uOutlineWidth; 4000 4001 // Compute alpha for outer and inner edges 4002 float outerAlpha = 1.0 - smoothstep(radius - aa, radius, dist); 4003 float innerAlpha = smoothstep(innerRadius - aa, innerRadius, dist); 4004 float alpha = uFillEnabled ? outerAlpha : outerAlpha * innerAlpha; 4005 4006 if (alpha < 0.01) discard; 4007 if (uFillEnabled) { 4008 float fillMix = 1.0 - smoothstep(innerRadius - aa, innerRadius + aa, dist); 4009 gl_FragColor = vec4(mix(uOutlineColor, uColor, fillMix), alpha); 4010 } else { 4011 gl_FragColor = vec4(uOutlineColor, alpha); 4012 } 4013 } else { 4014 // Solid fill mode 4015 float alpha = 1.0 - smoothstep(radius - aa, radius, dist); 4016 4017 if (alpha < 0.01) discard; 4018 gl_FragColor = vec4(uColor, alpha); 4019 } 4020 } 4021` 4022); 4023
4024// src/app/pages/campus-map-v3/utils/material-palette-v3.ts 4025var DEFAULT_NODE_COLORS = { 4026 Regular: 10395294, 4027 // Gray 4028 Door: 2201331, 4029 // Blue 4030 Stairs: 16750592, 4031 // Orange 4032 Elevator: 5025616, 4033 // Green 4034 Outdoor: 16771899 4035 // Yellow 4036}; 4037var CAMPUS_ENTRANCE_OUTLINE_COLOR = 26972; 4038var FLOOR_COLORS = { 4039 fill: 15658734, 4040 // Light gray 4041 outline: 4342338 4042 // Dark gray 4043}; 4044var ROOM_OUTLINE_COLOR = 7697781; 4045var CONNECTION_COLOR = 7697781; 4046var SELECTION_COLORS = { 4047 outline: 16711935, 4048 // Magenta 4049 fill: 16711935 4050 // Magenta (semi-transparent) 4051}; 4052var DESTINATION_HIGHLIGHT_COLORS = { 4053 inner: 1483775, 4054 // Navigation blue 4055 outer: 16777215 4056 // White contrast ring 4057}; 4058var GHOST_COLOR = 6710886; 4059var GROUND_REFERENCE_COLOR = 8947848; 4060var VALID_CONNECTION_COLOR = 5025616; 4061var SHAPE_EDIT_COLORS = { 4062 cornerHandle: 16777215, 4063 // White 4064 cornerHandleStroke: 4342338, 4065 // Dark gray stroke 4066 cornerHovered: 48340, 4067 // Cyan 4068 cornerSelected: 16711935, 4069 // Magenta (matches selection) 4070 midpoint: 48340, 4071 // Cyan 4072 preview: 48340, 4073 // Cyan 4074 closeIndicator: 5025616 4075 // Green 4076}; 4077var ROTATION_COLORS = { 4078 handle: 48340, 4079 // Cyan (matches shape edit) 4080 handleHover: 5099745, 4081 // Light cyan 4082 grip: 16777215, 4083 // White 4084 gripStroke: 48340, 4085 // Cyan 4086 pivot: 16711935 4087 // Magenta (matches selection) 4088}; 4089var MaterialPaletteV3 = class { 4090 constructor(config = {}) { 4091 this.cache = /* @__PURE__ */ new Map(); 4092 this.roomColors = __spreadValues(__spreadValues({}, ROOM_TYPE_COLORS), config.roomColors); 4093 this.nodeColors = __spreadValues(__spreadValues({}, DEFAULT_NODE_COLORS), config.nodeColors); 4094 } 4095 // ========================================================================= 4096 // Floor Materials 4097 // ========================================================================= 4098 /** 4099 * Get material for floor fill. 4100 */ 4101 getFloorFillMaterial() { 4102 const key = "floor-fill"; 4103 let material = this.cache.get(key); 4104 if (!material) { 4105 material = new MeshBasicMaterial({ 4106 color: FLOOR_COLORS.fill, 4107 side: DoubleSide, 4108 depthWrite: true, 4109 depthTest: true 4110 }); 4111 this.cache.set(key, material); 4112 } 4113 return material; 4114 } 4115 /** 4116 * Get material for floor outline stroke. 4117 */ 4118 getFloorOutlineMaterial() { 4119 const key = "floor-outline"; 4120 let material = this.cache.get(key); 4121 if (!material) { 4122 material = new LineBasicMaterial({ 4123 color: FLOOR_COLORS.outline, 4124 linewidth: 2 4125 }); 4126 this.cache.set(key, material); 4127 } 4128 return material; 4129 } 4130 // ========================================================================= 4131 // Room Materials 4132 // ========================================================================= 4133 /** 4134 * Get material for room fill by room type. 4135 */ 4136 getRoomFillMaterial(roomType) { 4137 var _a3, _b2; 4138 const key = `room-fill-${roomType}`; 4139 let material = this.cache.get(key); 4140 if (!material) { 4141 const color = (_b2 = (_a3 = this.roomColors[roomType]) != null ? _a3 : this.roomColors["Regular"]) != null ? _b2 : 16119285; 4142 material = new MeshBasicMaterial({ 4143 color, 4144 side: DoubleSide, 4145 // TODO: revert to FrontSide after fixing winding 4146 depthWrite: true, 4147 depthTest: true 4148 }); 4149 this.cache.set(key, material); 4150 } 4151 return material; 4152 } 4153 /** 4154 * Get material for room outline stroke. 4155 */ 4156 getRoomOutlineMaterial() { 4157 const key = "room-outline"; 4158 let material = this.cache.get(key); 4159 if (!material) { 4160 material = new LineBasicMaterial({ 4161 color: ROOM_OUTLINE_COLOR,
4162 linewidth: 1 4163 }); 4164 this.cache.set(key, material); 4165 } 4166 return material; 4167 } 4168 // ========================================================================= 4169 // Ground Patch Materials 4170 // ========================================================================= 4171 /** 4172 * Get material for ground patch by patch type. 4173 */ 4174 getGroundPatchMaterial(patchType) { 4175 var _a3, _b2; 4176 const key = `ground-patch-${patchType}`; 4177 let material = this.cache.get(key); 4178 if (!material) { 4179 const color = (_b2 = (_a3 = GROUND_PATCH_COLORS[patchType]) != null ? _a3 : GROUND_PATCH_COLORS["Grass"]) != null ? _b2 : 9488021; 4180 material = new MeshBasicMaterial({ 4181 color, 4182 side: DoubleSide, 4183 // TODO: revert to FrontSide after fixing winding 4184 depthWrite: true, 4185 depthTest: true 4186 }); 4187 this.cache.set(key, material); 4188 } 4189 return material; 4190 } 4191 /** 4192 * Get material for ground patch outline stroke. 4193 * Used in wireframe mode to show patch boundaries. 4194 */ 4195 getGroundPatchOutlineMaterial() { 4196 const key = "ground-patch-outline"; 4197 let material = this.cache.get(key); 4198 if (!material) { 4199 material = new LineBasicMaterial({ 4200 color: 6710886, 4201 // Gray outline, similar to floor outlines 4202 linewidth: 1 4203 }); 4204 this.cache.set(key, material); 4205 } 4206 return material; 4207 } 4208 // ========================================================================= 4209 // Node Materials 4210 // ========================================================================= 4211 /** 4212 * Get material for node by node type. 4213 */ 4214 getNodeMaterial(nodeType) { 4215 var _a3, _b2; 4216 const key = `node-${nodeType}`; 4217 let material = this.cache.get(key); 4218 if (!material) { 4219 const color = (_b2 = (_a3 = this.nodeColors[nodeType]) != null ? _a3 : this.nodeColors["Regular"]) != null ? _b2 : 10395294; 4220 material = new MeshBasicMaterial({ 4221 color, 4222 side: DoubleSide, 4223 depthWrite: true, 4224 depthTest: true 4225 }); 4226 this.cache.set(key, material); 4227 } 4228 return material; 4229 } 4230 /** 4231 * Get material for connection lines between nodes. 4232 */ 4233 getConnectionMaterial() { 4234 const key = "connection-line"; 4235 let material = this.cache.get(key); 4236 if (!material) { 4237 material = new LineBasicMaterial({ 4238 color: CONNECTION_COLOR, 4239 linewidth: 1.5 4240 }); 4241 this.cache.set(key, material); 4242 } 4243 return material; 4244 } 4245 /** 4246 * Get SDF shader material for rendering perfect node circles. 4247 * Uses PlaneGeometry with fragment shader for pixel-perfect circles at any zoom. 4248 */ 4249 getNodeCircleMaterial(nodeType, isCampusEntrance = false) { 4250 var _a3, _b2; 4251 const key = `node-circle-${nodeType}-${isCampusEntrance ? "campus-entrance" : "standard"}`; 4252 let material = this.cache.get(key); 4253 if (!material) { 4254 const colorHex = (_b2 = (_a3 = this.nodeColors[nodeType]) != null ? _a3 : this.nodeColors["Regular"]) != null ? _b2 : 10395294; 4255 material = new ShaderMaterial({ 4256 vertexShader: circleVertexShader, 4257 fragmentShader: circleFragmentShader, 4258 uniforms: { 4259 uColor: { value: new Color(colorHex) }, 4260 uOutlineWidth: { value: isCampusEntrance ? 0.12 : 0 }, 4261 uOutlineColor: { value: new Color(isCampusEntrance ? CAMPUS_ENTRANCE_OUTLINE_COLOR : 0) }, 4262 uFillEnabled: { value: true } 4263 }, 4264 transparent: true, 4265 side: DoubleSide, 4266 depthWrite: true, 4267 depthTest: true 4268 }); 4269 this.cache.set(key, material); 4270 } 4271 return material; 4272 } 4273 /** 4274 * Get SDF shader material for node selection highlight ring. 4275 * Renders a hollow circle outline for visual selection feedback. 4276 */ 4277 getNodeHighlightMaterial() { 4278 const key = "node-highlight-circle"; 4279 let material = this.cache.get(key); 4280 if (!material) { 4281 material = new ShaderMaterial({ 4282 vertexShader: circleVertexShader, 4283 fragmentShader: circleFragmentShader, 4284 uniforms: { 4285 uColor: { value: new Color(SELECTION_COLORS.outline) }, 4286 uOutlineWidth: { value: 0.08 }, 4287 uOutlineColor: { value: new Color(SELECTION_COLORS.outline) }, 4288 uFillEnabled: { value: false } 4289 }, 4290 transparent: true, 4291 side: DoubleSide, 4292 depthWrite: false, 4293 depthTest: true 4294 }); 4295 this.cache.set(key, material); 4296 } 4297 return material; 4298 } 4299 /** 4300 * Get SDF shader material for resolved destination node highlight ring. 4301 * Uses navigation blue so destination state is distinct from selection. 4302 */ 4303 getNodeDestinationHighlightMaterial() {
4304 const key = "node-destination-highlight-circle"; 4305 let material = this.cache.get(key); 4306 if (!material) { 4307 material = new ShaderMaterial({ 4308 vertexShader: circleVertexShader, 4309 fragmentShader: circleFragmentShader, 4310 uniforms: { 4311 uColor: { value: new Color(DESTINATION_HIGHLIGHT_COLORS.inner) }, 4312 uOutlineWidth: { value: 0.08 }, 4313 uOutlineColor: { value: new Color(DESTINATION_HIGHLIGHT_COLORS.inner) }, 4314 uFillEnabled: { value: false } 4315 }, 4316 transparent: true, 4317 side: DoubleSide, 4318 depthWrite: false, 4319 depthTest: true 4320 }); 4321 this.cache.set(key, material); 4322 } 4323 return material; 4324 } 4325 /** 4326 * Get SDF shader material for the outer white destination contrast ring. 4327 */ 4328 getNodeDestinationOuterHighlightMaterial() { 4329 const key = "node-destination-highlight-outer-circle"; 4330 let material = this.cache.get(key); 4331 if (!material) { 4332 material = new ShaderMaterial({ 4333 vertexShader: circleVertexShader, 4334 fragmentShader: circleFragmentShader, 4335 uniforms: { 4336 uColor: { value: new Color(DESTINATION_HIGHLIGHT_COLORS.outer) }, 4337 uOutlineWidth: { value: 0.07 }, 4338 uOutlineColor: { value: new Color(DESTINATION_HIGHLIGHT_COLORS.outer) }, 4339 uFillEnabled: { value: false } 4340 }, 4341 transparent: true, 4342 side: DoubleSide, 4343 depthWrite: false, 4344 depthTest: true 4345 }); 4346 this.cache.set(key, material); 4347 } 4348 return material; 4349 } 4350 // ========================================================================= 4351 // Selection Materials 4352 // ========================================================================= 4353 /** 4354 * Get material for selection highlight outline. 4355 * Used to visually indicate selected entities. 4356 */ 4357 getSelectionOutlineMaterial() { 4358 const key = "selection-outline"; 4359 let material = this.cache.get(key); 4360 if (!material) { 4361 material = new LineBasicMaterial({ 4362 color: SELECTION_COLORS.outline, 4363 linewidth: 3 4364 }); 4365 this.cache.set(key, material); 4366 } 4367 return material; 4368 } 4369 /** 4370 * Get material for selection highlight fill overlay. 4371 * Semi-transparent to show selection state without obscuring content. 4372 */ 4373 getSelectionFillMaterial() { 4374 const key = "selection-fill"; 4375 let material = this.cache.get(key); 4376 if (!material) { 4377 material = new MeshBasicMaterial({ 4378 color: SELECTION_COLORS.fill, 4379 transparent: true, 4380 opacity: 0.15, 4381 side: DoubleSide, 4382 depthWrite: false, 4383 depthTest: true 4384 }); 4385 this.cache.set(key, material); 4386 } 4387 return material; 4388 } 4389 // ========================================================================= 4390 // Ghost Materials (Drag Preview) 4391 // ========================================================================= 4392 /** 4393 * Get dashed line material for drag preview ghost outline. 4394 * Shows original position while entity is being dragged. 4395 * NOTE: Requires calling line.computeLineDistances() after geometry creation. 4396 */ 4397 getGhostOutlineMaterial() { 4398 const key = "ghost-outline"; 4399 let material = this.cache.get(key); 4400 if (!material) { 4401 material = new LineDashedMaterial({ 4402 color: GHOST_COLOR, 4403 dashSize: 10, 4404 gapSize: 5, 4405 transparent: true, 4406 opacity: 0.6, 4407 depthWrite: false, 4408 depthTest: true 4409 }); 4410 this.cache.set(key, material); 4411 } 4412 return material; 4413 } 4414 /** 4415 * Get solid line material for valid connection target preview. 4416 * Shows green line when hovering over a valid target node. 4417 */ 4418 getValidConnectionMaterial() { 4419 const key = "valid-connection"; 4420 let material = this.cache.get(key); 4421 if (!material) { 4422 material = new LineBasicMaterial({ 4423 color: VALID_CONNECTION_COLOR, 4424 linewidth: 2, 4425 transparent: true, 4426 opacity: 0.9, 4427 depthWrite: false, 4428 depthTest: true 4429 }); 4430 this.cache.set(key, material); 4431 } 4432 return material; 4433 } 4434 // ========================================================================= 4435 // Ground Reference Materials 4436 // ========================================================================= 4437 /** 4438 * Get material for ground reference floor outlines. 4439 * Shows ground floor footprints at 50% opacity when viewing upper floors. 4440 */ 4441 getGroundReferenceOutlineMaterial() {
4442 const key = "ground-reference-outline"; 4443 let material = this.cache.get(key); 4444 if (!material) { 4445 material = new LineBasicMaterial({ 4446 color: GROUND_REFERENCE_COLOR, 4447 linewidth: 1.5, 4448 transparent: true, 4449 opacity: 0.5, 4450 depthWrite: false, 4451 depthTest: true 4452 }); 4453 this.cache.set(key, material); 4454 } 4455 return material; 4456 } 4457 // ========================================================================= 4458 // Shape Editing Materials (Polygon Draw/Edit) 4459 // ========================================================================= 4460 /** 4461 * Get material for corner handle (normal state). 4462 * White square with dark stroke for visibility on any background. 4463 */ 4464 getCornerHandleMaterial() { 4465 const key = "corner-handle"; 4466 let material = this.cache.get(key); 4467 if (!material) { 4468 material = new MeshBasicMaterial({ 4469 color: SHAPE_EDIT_COLORS.cornerHandle, 4470 side: DoubleSide, 4471 depthWrite: true, 4472 depthTest: true 4473 }); 4474 this.cache.set(key, material); 4475 } 4476 return material; 4477 } 4478 /** 4479 * Get material for corner handle outline (border). 4480 */ 4481 getCornerHandleOutlineMaterial() { 4482 const key = "corner-handle-outline"; 4483 let material = this.cache.get(key); 4484 if (!material) { 4485 material = new LineBasicMaterial({ 4486 color: SHAPE_EDIT_COLORS.cornerHandleStroke, 4487 linewidth: 1 4488 }); 4489 this.cache.set(key, material); 4490 } 4491 return material; 4492 } 4493 /** 4494 * Get material for selected corner handle. 4495 * Magenta fill to match selection highlight color. 4496 */ 4497 getCornerHandleSelectedMaterial() { 4498 const key = "corner-handle-selected"; 4499 let material = this.cache.get(key); 4500 if (!material) { 4501 material = new MeshBasicMaterial({ 4502 color: SHAPE_EDIT_COLORS.cornerSelected, 4503 side: DoubleSide, 4504 depthWrite: true, 4505 depthTest: true 4506 }); 4507 this.cache.set(key, material); 4508 } 4509 return material; 4510 } 4511 /** 4512 * Get material for hovered corner handle outline. 4513 * Cyan stroke for hover affordance. 4514 */ 4515 getCornerHandleHoveredOutlineMaterial() { 4516 const key = "corner-handle-hovered-outline"; 4517 let material = this.cache.get(key); 4518 if (!material) { 4519 material = new LineBasicMaterial({ 4520 color: SHAPE_EDIT_COLORS.cornerHovered, 4521 linewidth: 2 4522 }); 4523 this.cache.set(key, material); 4524 } 4525 return material; 4526 } 4527 /** 4528 * Get material for edge midpoint handle. 4529 * Semi-transparent cyan circle for add-corner affordance. 4530 */ 4531 getMidpointHandleMaterial() { 4532 const key = "midpoint-handle"; 4533 let material = this.cache.get(key); 4534 if (!material) { 4535 material = new MeshBasicMaterial({ 4536 color: SHAPE_EDIT_COLORS.midpoint, 4537 transparent: true, 4538 opacity: 0.8, 4539 side: DoubleSide, 4540 depthWrite: false, 4541 depthTest: true 4542 }); 4543 this.cache.set(key, material); 4544 } 4545 return material; 4546 } 4547 /** 4548 * Get dashed line material for polygon drawing preview. 4549 * Cyan dashed line showing polygon under construction. 4550 * NOTE: Requires calling line.computeLineDistances() after geometry creation. 4551 */ 4552 getPolygonPreviewMaterial() { 4553 const key = "polygon-preview"; 4554 let material = this.cache.get(key); 4555 if (!material) { 4556 material = new LineDashedMaterial({ 4557 color: SHAPE_EDIT_COLORS.preview, 4558 dashSize: 15, 4559 gapSize: 8, 4560 linewidth: 2, 4561 transparent: true, 4562 opacity: 0.9, 4563 depthWrite: false, 4564 depthTest: true 4565 }); 4566 this.cache.set(key, material); 4567 } 4568 return material; 4569 } 4570 /** 4571 * Get solid line material for polygon preview edges.
4572 * Used for completed edges (not the rubber-band cursor line). 4573 */ 4574 getPolygonPreviewSolidMaterial() { 4575 const key = "polygon-preview-solid"; 4576 let material = this.cache.get(key); 4577 if (!material) { 4578 material = new LineBasicMaterial({ 4579 color: SHAPE_EDIT_COLORS.preview, 4580 linewidth: 2, 4581 transparent: true, 4582 opacity: 0.9, 4583 depthWrite: false, 4584 depthTest: true 4585 }); 4586 this.cache.set(key, material); 4587 } 4588 return material; 4589 } 4590 /** 4591 * Get material for vertex markers during polygon drawing. 4592 * Small cyan circles at each placed vertex. 4593 */ 4594 getVertexMarkerMaterial() { 4595 const key = "vertex-marker"; 4596 let material = this.cache.get(key); 4597 if (!material) { 4598 material = new MeshBasicMaterial({ 4599 color: SHAPE_EDIT_COLORS.preview, 4600 side: DoubleSide, 4601 depthWrite: true, 4602 depthTest: true 4603 }); 4604 this.cache.set(key, material); 4605 } 4606 return material; 4607 } 4608 /** 4609 * Get material for close indicator ring around first vertex. 4610 * Green ring appears when close enough to close the polygon. 4611 */ 4612 getCloseIndicatorMaterial() { 4613 const key = "close-indicator"; 4614 let material = this.cache.get(key); 4615 if (!material) { 4616 material = new MeshBasicMaterial({ 4617 color: SHAPE_EDIT_COLORS.closeIndicator, 4618 transparent: true, 4619 opacity: 0.8, 4620 side: DoubleSide, 4621 depthWrite: false, 4622 depthTest: true 4623 }); 4624 this.cache.set(key, material); 4625 } 4626 return material; 4627 } 4628 // ========================================================================= 4629 // Rotation Handle Materials 4630 // ========================================================================= 4631 /** 4632 * Get material for rotation handle arc (normal state). 4633 */ 4634 getRotationHandleMaterial() { 4635 const key = "rotation-handle"; 4636 let material = this.cache.get(key); 4637 if (!material) { 4638 material = new LineBasicMaterial({ 4639 color: ROTATION_COLORS.handle, 4640 linewidth: 4, 4641 transparent: true, 4642 opacity: 0.9, 4643 depthWrite: false, 4644 depthTest: true 4645 }); 4646 this.cache.set(key, material); 4647 } 4648 return material; 4649 } 4650 /** 4651 * Get material for rotation handle arc (hover state). 4652 */ 4653 getRotationHandleHoverMaterial() { 4654 const key = "rotation-handle-hover"; 4655 let material = this.cache.get(key); 4656 if (!material) { 4657 material = new LineBasicMaterial({ 4658 color: ROTATION_COLORS.handleHover, 4659 linewidth: 5, 4660 transparent: true, 4661 opacity: 1, 4662 depthWrite: false, 4663 depthTest: true 4664 }); 4665 this.cache.set(key, material); 4666 } 4667 return material; 4668 } 4669 /** 4670 * Get material for rotation grip circle fill. 4671 */ 4672 getRotationGripMaterial() { 4673 const key = "rotation-grip"; 4674 let material = this.cache.get(key); 4675 if (!material) { 4676 material = new MeshBasicMaterial({ 4677 color: ROTATION_COLORS.grip, 4678 side: DoubleSide, 4679 depthWrite: true, 4680 depthTest: true 4681 }); 4682 this.cache.set(key, material); 4683 } 4684 return material; 4685 } 4686 /** 4687 * Get material for rotation grip circle outline. 4688 */ 4689 getRotationGripOutlineMaterial() { 4690 const key = "rotation-grip-outline"; 4691 let material = this.cache.get(key); 4692 if (!material) { 4693 material = new LineBasicMaterial({ 4694 color: ROTATION_COLORS.gripStroke, 4695 linewidth: 3 4696 }); 4697 this.cache.set(key, material); 4698 } 4699 return material; 4700 } 4701 /** 4702 * Get material for rotation pivot marker. 4703 */ 4704 getRotationPivotMaterial() { 4705 const key = "rotation-pivot"; 4706 let material = this.cache.get(key); 4707 if (!material) { 4708 material = new MeshBasicMaterial({ 4709 color: ROTATION_COLORS.pivot, 4710 transparent: true, 4711 opacity: 0.8, 4712 side: DoubleSide, 4713 depthWrite: false, 4714 depthTest: true 4715 }); 4716 this.cache.set(key, material); 4717 } 4718 return material; 4719 } 4720 // ========================================================================= 4721 // Lifecycle 4722 // ========================================================================= 4723 /** 4724 * Dispose all cached materials to free GPU memory. 4725 */ 4726 dispose() { 4727 for (const material of this.cache.values()) { 4728 material.dispose(); 4729 } 4730 this.cache.clear(); 4731 } 4732}; 4733
4734// src/app/pages/campus-map-v3/services/map-scene.service.ts 4735var _MapSceneService = class _MapSceneService { 4736 constructor() { 4737 this.isInitialized = signal(false); 4738 this.isDisposed = signal(false); 4739 this.dirty = true; 4740 this.animationFrameId = null; 4741 this.resizeObserver = null; 4742 this.config = null; 4743 this.zoomChangeSubscribers = /* @__PURE__ */ new Set(); 4744 } 4745 // ========================================================================= 4746 // Public API 4747 // ========================================================================= 4748 /** 4749 * Initialize the Three.js scene with the given configuration. 4750 * Must be called before using any scene objects. 4751 */ 4752 initialize(config) { 4753 if (this.isInitialized()) { 4754 return; 4755 } 4756 this.config = config; 4757 this.initThreeJS(config.container); 4758 this.initMaterials(); 4759 this.setupResizeObserver(config.container); 4760 this.startRenderLoop(); 4761 this.isInitialized.set(true); 4762 } 4763 /** 4764 * Dispose all Three.js resources. 4765 * Call this in ngOnDestroy. 4766 */ 4767 dispose() { 4768 var _a3, _b2, _c, _d; 4769 if (this.isDisposed()) 4770 return; 4771 this.isDisposed.set(true); 4772 if (this.animationFrameId !== null) { 4773 cancelAnimationFrame(this.animationFrameId); 4774 this.animationFrameId = null; 4775 } 4776 if (this.resizeObserver) { 4777 this.resizeObserver.disconnect(); 4778 this.resizeObserver = null; 4779 } 4780 (_a3 = this.materials) == null ? void 0 : _a3.dispose(); 4781 const canvas = (_b2 = this.renderer) == null ? void 0 : _b2.domElement; 4782 if (canvas == null ? void 0 : canvas.parentElement) { 4783 canvas.parentElement.removeChild(canvas); 4784 } 4785 const labelElement = (_c = this.labelRenderer) == null ? void 0 : _c.domElement; 4786 if (labelElement == null ? void 0 : labelElement.parentElement) { 4787 labelElement.parentElement.removeChild(labelElement); 4788 } 4789 (_d = this.renderer) == null ? void 0 : _d.dispose(); 4790 this.isInitialized.set(false); 4791 } 4792 /** 4793 * Mark the scene as needing a redraw. 4794 * Call this after any visual changes. 4795 */ 4796 markDirty() { 4797 this.dirty = true; 4798 } 4799 /** 4800 * Get a context object with all scene resources. 4801 * Useful for passing to controllers and renderers. 4802 */ 4803 getContext() { 4804 this.ensureInitialized(); 4805 return { 4806 scene: this.scene, 4807 camera: this.camera, 4808 renderer: this.renderer, 4809 labelRenderer: this.labelRenderer, 4810 clock: this.clock, 4811 materials: this.materials, 4812 markDirty: () => this.markDirty() 4813 }; 4814 } 4815 // Getters for individual scene objects 4816 getScene() { 4817 this.ensureInitialized(); 4818 return this.scene; 4819 } 4820 getCamera() { 4821 this.ensureInitialized(); 4822 return this.camera; 4823 } 4824 getRenderer() { 4825 this.ensureInitialized(); 4826 return this.renderer; 4827 } 4828 getLabelRenderer() { 4829 this.ensureInitialized(); 4830 return this.labelRenderer; 4831 } 4832 getClock() { 4833 this.ensureInitialized(); 4834 return this.clock; 4835 } 4836 getMaterials() { 4837 this.ensureInitialized(); 4838 return this.materials; 4839 } 4840 getDirectionalLight() { 4841 this.ensureInitialized(); 4842 return this.directionalLight; 4843 } 4844 getAmbientLight() { 4845 this.ensureInitialized(); 4846 return this.ambientLight; 4847 } 4848 /** 4849 * Get the canvas container height. 4850 */ 4851 getCanvasHeight() { 4852 var _a3, _b2; 4853 return (_b2 = (_a3 = this.config) == null ? void 0 : _a3.container.clientHeight) != null ? _b2 : 0; 4854 } 4855 // ========================================================================= 4856 // Private Initialization 4857 // ========================================================================= 4858 ensureInitialized() { 4859 if (!this.isInitialized()) { 4860 throw new Error("MapSceneService: Not initialized. Call initialize() first."); 4861 } 4862 } 4863 /** 4864 * Initialize Three.js scene, camera, and renderers. 4865 */ 4866 initThreeJS(container) { 4867 const width = container.clientWidth; 4868 const height = container.clientHeight; 4869 this.scene = new Scene(); 4870 this.scene.background = new Color(16119285); 4871 const aspect = width / height || 1; 4872 const frustumSize = 1e3;
4873 this.camera = new OrthographicCamera( 4874 -frustumSize * aspect / 2, 4875 frustumSize * aspect / 2, 4876 -frustumSize / 2, 4877 // top (negative for Y-down) 4878 frustumSize / 2, 4879 // bottom (positive for Y-down) 4880 0.1, 4881 1e4 4882 ); 4883 this.camera.position.z = 100; 4884 this.camera.up.set(0, 1, 0); 4885 this.ambientLight = new AmbientLight(16777215, 1); 4886 this.scene.add(this.ambientLight); 4887 this.directionalLight = new DirectionalLight(16777215, 0.8); 4888 this.directionalLight.position.set(500, -500, 1e3); 4889 this.scene.add(this.directionalLight); 4890 this.renderer = new WebGLRenderer({ 4891 antialias: true, 4892 powerPreference: "high-performance", 4893 alpha: false 4894 }); 4895 this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); 4896 this.renderer.setSize(width, height); 4897 this.renderer.shadowMap.enabled = true; 4898 container.appendChild(this.renderer.domElement); 4899 this.labelRenderer = new CSS2DRenderer(); 4900 this.labelRenderer.setSize(width, height); 4901 this.labelRenderer.domElement.style.position = "absolute"; 4902 this.labelRenderer.domElement.style.top = "0"; 4903 this.labelRenderer.domElement.style.left = "0"; 4904 this.labelRenderer.domElement.style.pointerEvents = "none"; 4905 this.labelRenderer.domElement.style.zIndex = "0"; 4906 container.appendChild(this.labelRenderer.domElement); 4907 this.clock = new Clock(); 4908 } 4909 /** 4910 * Initialize the material palette. 4911 */ 4912 initMaterials() { 4913 this.materials = new MaterialPaletteV3(); 4914 } 4915 // ========================================================================= 4916 // Render Loop 4917 // ========================================================================= 4918 /** 4919 * Start the render loop with dirty flag pattern. 4920 */ 4921 startRenderLoop() { 4922 const loop = () => { 4923 var _a3; 4924 if (this.isDisposed()) 4925 return; 4926 this.animationFrameId = requestAnimationFrame(loop); 4927 const delta = this.clock.getDelta(); 4928 if ((_a3 = this.config) == null ? void 0 : _a3.onUpdate) { 4929 const needsRender = this.config.onUpdate(delta); 4930 if (needsRender) { 4931 this.dirty = true; 4932 } 4933 } 4934 if (this.dirty) { 4935 this.renderer.render(this.scene, this.camera); 4936 this.labelRenderer.render(this.scene, this.camera); 4937 this.dirty = false; 4938 } 4939 }; 4940 loop(); 4941 } 4942 // ========================================================================= 4943 // Resize Handling 4944 // ========================================================================= 4945 /** 4946 * Set up ResizeObserver to detect container size changes. 4947 */ 4948 setupResizeObserver(container) { 4949 if (typeof ResizeObserver === "undefined") 4950 return; 4951 this.resizeObserver = new ResizeObserver(() => { 4952 this.handleResize(); 4953 }); 4954 this.resizeObserver.observe(container); 4955 } 4956 /** 4957 * Handle container resize - updates renderer and camera. 4958 */ 4959 handleResize() { 4960 var _a3, _b2; 4961 const container = (_a3 = this.config) == null ? void 0 : _a3.container; 4962 if (!container || !this.renderer || !this.camera) 4963 return; 4964 const width = container.clientWidth; 4965 const height = container.clientHeight; 4966 if (width === 0 || height === 0) 4967 return; 4968 this.renderer.setSize(width, height); 4969 this.labelRenderer.setSize(width, height); 4970 const aspect = width / height; 4971 const frustumSize = 1e3; 4972 this.camera.left = -frustumSize * aspect / 2; 4973 this.camera.right = frustumSize * aspect / 2; 4974 this.camera.top = -frustumSize / 2; 4975 this.camera.bottom = frustumSize / 2; 4976 this.camera.updateProjectionMatrix(); 4977 this.markDirty(); 4978 if ((_b2 = this.config) == null ? void 0 : _b2.onZoomChange) { 4979 this.config.onZoomChange(this.camera.zoom, height); 4980 } 4981 for (const subscriber of this.zoomChangeSubscribers) { 4982 subscriber(); 4983 } 4984 } 4985 /** 4986 * Subscribe to camera zoom changes. 4987 * Returns an unsubscribe function. 4988 */ 4989 subscribeZoomChange(handler) { 4990 this.zoomChangeSubscribers.add(handler); 4991 return () => { 4992 this.zoomChangeSubscribers.delete(handler); 4993 }; 4994 } 4995}; 4996_MapSceneService.\u0275fac = function MapSceneService_Factory(__ngFactoryType__) { 4997 return new (__ngFactoryType__ || _MapSceneService)(); 4998}; 4999_MapSceneService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _MapSceneService, factory: _MapSceneService.\u0275fac }); 5000var MapSceneService = _MapSceneService; 5001(() => { 5002 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(MapSceneService, [{ 5003 type: Injectable 5004 }], null, null); 5005})(); 5006
5007// src/app/pages/campus-map-v3/services/gps-transform-manager.service.ts 5008var _GPSTransformManagerService = class _GPSTransformManagerService { 5009 constructor() { 5010 this.database = inject(DatabaseService); 5011 this.campusData = inject(CampusDataService); 5012 this._gpsTransform = signal(null); 5013 this.gpsTransform = this._gpsTransform.asReadonly(); 5014 this._transformQuality = signal(null); 5015 this.transformQuality = this._transformQuality.asReadonly(); 5016 this._referencePoints = signal([]); 5017 this.referencePoints = this._referencePoints.asReadonly(); 5018 this.hasValidTransform = computed(() => { 5019 const transform = this._gpsTransform(); 5020 const quality = this._transformQuality(); 5021 return transform !== null && (quality == null ? void 0 : quality.isReliable) === true; 5022 }); 5023 this.recalculateTransform(); 5024 } 5025 /** 5026 * Recalculate GPS transform from current campus nodes. 5027 * Call this when nodes are added/modified/removed. 5028 */ 5029 recalculateTransform() { 5030 const campus = this.campusData.getCampus(); 5031 if (!campus) { 5032 this._gpsTransform.set(null); 5033 this._transformQuality.set(null); 5034 this._referencePoints.set([]); 5035 return; 5036 } 5037 const referencePoints = []; 5038 for (const node of campus.nodes) { 5039 if (node.entity.latitude != null && node.entity.longitude != null && node.entity.locationX != null && node.entity.locationY != null) { 5040 referencePoints.push({ 5041 gps: { 5042 latitude: node.entity.latitude, 5043 longitude: node.entity.longitude 5044 }, 5045 map: { 5046 x: node.entity.locationX, 5047 y: node.entity.locationY 5048 }, 5049 id: node.entity.id 5050 }); 5051 } 5052 } 5053 this._referencePoints.set(referencePoints); 5054 if (referencePoints.length >= 2) { 5055 try { 5056 const transform = createGPSTransform(referencePoints); 5057 this._gpsTransform.set(transform); 5058 this._transformQuality.set(transform.quality); 5059 console.log("[GPSTransformManager] Transform created with", referencePoints.length, "points"); 5060 } catch (error) { 5061 console.error("[GPSTransformManager] Failed to create transform:", error); 5062 this._gpsTransform.set(null); 5063 this._transformQuality.set(null); 5064 } 5065 } else { 5066 console.log("[GPSTransformManager] Not enough reference points:", referencePoints.length); 5067 this._gpsTransform.set(null); 5068 this._transformQuality.set(null); 5069 } 5070 } 5071 /** 5072 * Transform GPS coordinates to map coordinates. 5073 * @param gps GPS coordinates 5074 * @returns Map coordinates, or null if transform not available 5075 */ 5076 transformToMap(gps) { 5077 const transform = this._gpsTransform(); 5078 if (!transform) 5079 return null; 5080 try { 5081 const mapPoint = transformGPSToMap(gps, transform); 5082 return { x: mapPoint.x, y: mapPoint.y }; 5083 } catch (error) { 5084 console.error("[GPSTransformManager] Transform failed:", error); 5085 return null; 5086 } 5087 } 5088 /** 5089 * Check if a node has GPS coordinates. 5090 */ 5091 hasGPS(node) { 5092 return node.entity.latitude != null && node.entity.longitude != null; 5093 } 5094 /** 5095 * Get nodes with GPS coordinates in current campus. 5096 */ 5097 getNodesWithGPS() { 5098 const campus = this.campusData.getCampus(); 5099 if (!campus) 5100 return []; 5101 return campus.nodes.filter((node) => this.hasGPS(node)); 5102 } 5103 /** 5104 * Get campus data service (for external access). 5105 */ 5106 getCampusDataService() { 5107 return this.campusData; 5108 } 5109 /** 5110 * Clear GPS transform (called when all GPS coordinates are removed). 5111 */ 5112 clearTransform() { 5113 this._gpsTransform.set(null); 5114 this._transformQuality.set(null); 5115 this._referencePoints.set([]); 5116 } 5117}; 5118_GPSTransformManagerService.\u0275fac = function GPSTransformManagerService_Factory(__ngFactoryType__) { 5119 return new (__ngFactoryType__ || _GPSTransformManagerService)(); 5120}; 5121_GPSTransformManagerService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _GPSTransformManagerService, factory: _GPSTransformManagerService.\u0275fac }); 5122var GPSTransformManagerService = _GPSTransformManagerService; 5123(() => { 5124 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(GPSTransformManagerService, [{ 5125 type: Injectable 5126 }], () => [], null); 5127})(); 5128
vendor: 460 bytes, lines 5129-5142
5129// node_modules/@angular/cdk/fesm2022/element-x4z00URv.mjs 5130function coerceNumberProperty(value, fallbackValue = 0) { 5131 if (_isNumberValue(value)) { 5132 return Number(value); 5133 } 5134 return arguments.length === 2 ? fallbackValue : 0; 5135} 5136function _isNumberValue(value) { 5137 return !isNaN(parseFloat(value)) && !isNaN(Number(value)); 5138} 5139function coerceElement(elementOrRef) { 5140 return elementOrRef instanceof ElementRef ? elementOrRef.nativeElement : elementOrRef; 5141} 5142
vendor: 3,354 bytes, lines 5143-5204
5143// node_modules/@angular/cdk/fesm2022/platform-DmdVEw_C.mjs 5144var hasV8BreakIterator; 5145try { 5146 hasV8BreakIterator = typeof Intl !== "undefined" && Intl.v8BreakIterator; 5147} catch { 5148 hasV8BreakIterator = false; 5149} 5150var _Platform = class _Platform { 5151 _platformId = inject(PLATFORM_ID); 5152 // We want to use the Angular platform check because if the Document is shimmed 5153 // without the navigator, the following checks will fail. This is preferred because 5154 // sometimes the Document may be shimmed without the user's knowledge or intention 5155 /** Whether the Angular application is being rendered in the browser. */ 5156 isBrowser = this._platformId ? isPlatformBrowser(this._platformId) : typeof document === "object" && !!document; 5157 /** Whether the current browser is Microsoft Edge. */ 5158 EDGE = this.isBrowser && /(edge)/i.test(navigator.userAgent); 5159 /** Whether the current rendering engine is Microsoft Trident. */ 5160 TRIDENT = this.isBrowser && /(msie|trident)/i.test(navigator.userAgent); 5161 // EdgeHTML and Trident mock Blink specific things and need to be excluded from this check. 5162 /** Whether the current rendering engine is Blink. */ 5163 BLINK = this.isBrowser && !!(window.chrome || hasV8BreakIterator) && typeof CSS !== "undefined" && !this.EDGE && !this.TRIDENT; 5164 // Webkit is part of the userAgent in EdgeHTML, Blink and Trident. Therefore we need to 5165 // ensure that Webkit runs standalone and is not used as another engine's base. 5166 /** Whether the current rendering engine is WebKit. */ 5167 WEBKIT = this.isBrowser && /AppleWebKit/i.test(navigator.userAgent) && !this.BLINK && !this.EDGE && !this.TRIDENT; 5168 /** Whether the current platform is Apple iOS. */ 5169 IOS = this.isBrowser && /iPad|iPhone|iPod/.test(navigator.userAgent) && !("MSStream" in window); 5170 // It's difficult to detect the plain Gecko engine, because most of the browsers identify 5171 // them self as Gecko-like browsers and modify the userAgent's according to that. 5172 // Since we only cover one explicit Firefox case, we can simply check for Firefox 5173 // instead of having an unstable check for Gecko. 5174 /** Whether the current browser is Firefox. */ 5175 FIREFOX = this.isBrowser && /(firefox|minefield)/i.test(navigator.userAgent); 5176 /** Whether the current platform is Android. */ 5177 // Trident on mobile adds the android platform to the userAgent to trick detections. 5178 ANDROID = this.isBrowser && /android/i.test(navigator.userAgent) && !this.TRIDENT; 5179 // Safari browsers will include the Safari keyword in their userAgent. Some browsers may fake 5180 // this and just place the Safari keyword in the userAgent. To be more safe about Safari every 5181 // Safari browser should also use Webkit as its layout engine. 5182 /** Whether the current browser is Safari. */ 5183 SAFARI = this.isBrowser && /safari/i.test(navigator.userAgent) && this.WEBKIT; 5184 constructor() { 5185 } 5186}; 5187__publicField(_Platform, "\u0275fac", function Platform_Factory(__ngFactoryType__) { 5188 return new (__ngFactoryType__ || _Platform)(); 5189}); 5190__publicField(_Platform, "\u0275prov", /* @__PURE__ */ \u0275\u0275defineInjectable({ 5191 token: _Platform, 5192 factory: _Platform.\u0275fac, 5193 providedIn: "root" 5194})); 5195var Platform = _Platform; 5196(() => { 5197 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(Platform, [{ 5198 type: Injectable, 5199 args: [{ 5200 providedIn: "root" 5201 }] 5202 }], () => [], null); 5203})(); 5204
vendor: 1,919 bytes, lines 5205-5257
5205// node_modules/@angular/cdk/fesm2022/directionality-CBXD4hga.mjs 5206var DIR_DOCUMENT = new InjectionToken("cdk-dir-doc", { 5207 providedIn: "root", 5208 factory: DIR_DOCUMENT_FACTORY 5209}); 5210function DIR_DOCUMENT_FACTORY() { 5211 return inject(DOCUMENT); 5212} 5213var RTL_LOCALE_PATTERN = /^(ar|ckb|dv|he|iw|fa|nqo|ps|sd|ug|ur|yi|.*[-_](Adlm|Arab|Hebr|Nkoo|Rohg|Thaa))(?!.*[-_](Latn|Cyrl)($|-|_))($|-|_)/i; 5214function _resolveDirectionality(rawValue) { 5215 const value = (rawValue == null ? void 0 : rawValue.toLowerCase()) || ""; 5216 if (value === "auto" && typeof navigator !== "undefined" && (navigator == null ? void 0 : navigator.language)) { 5217 return RTL_LOCALE_PATTERN.test(navigator.language) ? "rtl" : "ltr"; 5218 } 5219 return value === "rtl" ? "rtl" : "ltr"; 5220} 5221var _Directionality = class _Directionality { 5222 /** The current 'ltr' or 'rtl' value. */ 5223 value = "ltr"; 5224 /** Stream that emits whenever the 'ltr' / 'rtl' state changes. */ 5225 change = new EventEmitter(); 5226 constructor() { 5227 const _document = inject(DIR_DOCUMENT, { 5228 optional: true 5229 }); 5230 if (_document) { 5231 const bodyDir = _document.body ? _document.body.dir : null; 5232 const htmlDir = _document.documentElement ? _document.documentElement.dir : null; 5233 this.value = _resolveDirectionality(bodyDir || htmlDir || "ltr"); 5234 } 5235 } 5236 ngOnDestroy() { 5237 this.change.complete(); 5238 } 5239}; 5240__publicField(_Directionality, "\u0275fac", function Directionality_Factory(__ngFactoryType__) { 5241 return new (__ngFactoryType__ || _Directionality)(); 5242}); 5243__publicField(_Directionality, "\u0275prov", /* @__PURE__ */ \u0275\u0275defineInjectable({ 5244 token: _Directionality, 5245 factory: _Directionality.\u0275fac, 5246 providedIn: "root" 5247})); 5248var Directionality = _Directionality; 5249(() => { 5250 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(Directionality, [{ 5251 type: Injectable, 5252 args: [{ 5253 providedIn: "root" 5254 }] 5255 }], () => [], null); 5256})(); 5257
vendor: 2,162 bytes, lines 5258-5314
5258// node_modules/@angular/cdk/fesm2022/scrolling-BkvA05C8.mjs 5259var RtlScrollAxisType; 5260(function(RtlScrollAxisType2) { 5261 RtlScrollAxisType2[RtlScrollAxisType2["NORMAL"] = 0] = "NORMAL"; 5262 RtlScrollAxisType2[RtlScrollAxisType2["NEGATED"] = 1] = "NEGATED"; 5263 RtlScrollAxisType2[RtlScrollAxisType2["INVERTED"] = 2] = "INVERTED"; 5264})(RtlScrollAxisType || (RtlScrollAxisType = {})); 5265var rtlScrollAxisType; 5266var scrollBehaviorSupported; 5267function supportsScrollBehavior() { 5268 if (scrollBehaviorSupported == null) { 5269 if (typeof document !== "object" || !document || typeof Element !== "function" || !Element) { 5270 scrollBehaviorSupported = false; 5271 return scrollBehaviorSupported; 5272 } 5273 if ("scrollBehavior" in document.documentElement.style) { 5274 scrollBehaviorSupported = true; 5275 } else { 5276 const scrollToFunction = Element.prototype.scrollTo; 5277 if (scrollToFunction) { 5278 scrollBehaviorSupported = !/\{\s*\[native code\]\s*\}/.test(scrollToFunction.toString()); 5279 } else { 5280 scrollBehaviorSupported = false; 5281 } 5282 } 5283 } 5284 return scrollBehaviorSupported; 5285} 5286function getRtlScrollAxisType() { 5287 if (typeof document !== "object" || !document) { 5288 return RtlScrollAxisType.NORMAL; 5289 } 5290 if (rtlScrollAxisType == null) { 5291 const scrollContainer = document.createElement("div"); 5292 const containerStyle = scrollContainer.style; 5293 scrollContainer.dir = "rtl"; 5294 containerStyle.width = "1px"; 5295 containerStyle.overflow = "auto"; 5296 containerStyle.visibility = "hidden"; 5297 containerStyle.pointerEvents = "none"; 5298 containerStyle.position = "absolute"; 5299 const content = document.createElement("div"); 5300 const contentStyle = content.style; 5301 contentStyle.width = "2px"; 5302 contentStyle.height = "1px"; 5303 scrollContainer.appendChild(content); 5304 document.body.appendChild(scrollContainer); 5305 rtlScrollAxisType = RtlScrollAxisType.NORMAL; 5306 if (scrollContainer.scrollLeft === 0) { 5307 scrollContainer.scrollLeft = 1; 5308 rtlScrollAxisType = scrollContainer.scrollLeft === 0 ? RtlScrollAxisType.NEGATED : RtlScrollAxisType.INVERTED; 5309 } 5310 scrollContainer.remove(); 5311 } 5312 return rtlScrollAxisType; 5313} 5314
vendor: 2,632 bytes, lines 5315-5417
5315// node_modules/@angular/cdk/fesm2022/bidi.mjs 5316var _Dir = class _Dir { 5317 /** Normalized direction that accounts for invalid/unsupported values. */ 5318 _dir = "ltr"; 5319 /** Whether the `value` has been set to its initial value. */ 5320 _isInitialized = false; 5321 /** Direction as passed in by the consumer. */ 5322 _rawDir; 5323 /** Event emitted when the direction changes. */ 5324 change = new EventEmitter(); 5325 /** @docs-private */ 5326 get dir() { 5327 return this._dir; 5328 } 5329 set dir(value) { 5330 const previousValue = this._dir; 5331 this._dir = _resolveDirectionality(value); 5332 this._rawDir = value; 5333 if (previousValue !== this._dir && this._isInitialized) { 5334 this.change.emit(this._dir); 5335 } 5336 } 5337 /** Current layout direction of the element. */ 5338 get value() { 5339 return this.dir; 5340 } 5341 /** Initialize once default value has been set. */ 5342 ngAfterContentInit() { 5343 this._isInitialized = true; 5344 } 5345 ngOnDestroy() { 5346 this.change.complete(); 5347 } 5348}; 5349__publicField(_Dir, "\u0275fac", function Dir_Factory(__ngFactoryType__) { 5350 return new (__ngFactoryType__ || _Dir)(); 5351}); 5352__publicField(_Dir, "\u0275dir", /* @__PURE__ */ \u0275\u0275defineDirective({ 5353 type: _Dir, 5354 selectors: [["", "dir", ""]], 5355 hostVars: 1, 5356 hostBindings: function Dir_HostBindings(rf, ctx) { 5357 if (rf & 2) { 5358 \u0275\u0275attribute("dir", ctx._rawDir); 5359 } 5360 }, 5361 inputs: { 5362 dir: "dir" 5363 }, 5364 outputs: { 5365 change: "dirChange" 5366 }, 5367 exportAs: ["dir"], 5368 features: [\u0275\u0275ProvidersFeature([{ 5369 provide: Directionality, 5370 useExisting: _Dir 5371 }])] 5372})); 5373var Dir = _Dir; 5374(() => { 5375 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(Dir, [{ 5376 type: Directive, 5377 args: [{ 5378 selector: "[dir]", 5379 providers: [{ 5380 provide: Directionality, 5381 useExisting: Dir 5382 }], 5383 host: { 5384 "[attr.dir]": "_rawDir" 5385 }, 5386 exportAs: "dir" 5387 }] 5388 }], null, { 5389 change: [{ 5390 type: Output, 5391 args: ["dirChange"] 5392 }], 5393 dir: [{ 5394 type: Input 5395 }] 5396 }); 5397})(); 5398var _BidiModule = class _BidiModule { 5399}; 5400__publicField(_BidiModule, "\u0275fac", function BidiModule_Factory(__ngFactoryType__) { 5401 return new (__ngFactoryType__ || _BidiModule)(); 5402}); 5403__publicField(_BidiModule, "\u0275mod", /* @__PURE__ */ \u0275\u0275defineNgModule({ 5404 type: _BidiModule 5405})); 5406__publicField(_BidiModule, "\u0275inj", /* @__PURE__ */ \u0275\u0275defineInjector({})); 5407var BidiModule = _BidiModule; 5408(() => { 5409 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(BidiModule, [{ 5410 type: NgModule, 5411 args: [{ 5412 imports: [Dir], 5413 exports: [Dir] 5414 }] 5415 }], null, null); 5416})(); 5417
vendor: 225 bytes, lines 5418-5424
5418// node_modules/@angular/cdk/fesm2022/data-source-D34wiQZj.mjs 5419var DataSource = class { 5420}; 5421function isDataSource(value) { 5422 return value && typeof value.connect === "function" && !(value instanceof ConnectableObservable); 5423} 5424
vendor: 4,452 bytes, lines 5425-5541
5425// node_modules/@angular/cdk/fesm2022/recycle-view-repeater-strategy-DoWdPqVw.mjs 5426var ArrayDataSource = class extends DataSource { 5427 _data; 5428 constructor(_data) { 5429 super(); 5430 this._data = _data; 5431 } 5432 connect() { 5433 return isObservable(this._data) ? this._data : of(this._data); 5434 } 5435 disconnect() { 5436 } 5437}; 5438var _ViewRepeaterOperation; 5439(function(_ViewRepeaterOperation2) { 5440 _ViewRepeaterOperation2[_ViewRepeaterOperation2["REPLACED"] = 0] = "REPLACED"; 5441 _ViewRepeaterOperation2[_ViewRepeaterOperation2["INSERTED"] = 1] = "INSERTED"; 5442 _ViewRepeaterOperation2[_ViewRepeaterOperation2["MOVED"] = 2] = "MOVED"; 5443 _ViewRepeaterOperation2[_ViewRepeaterOperation2["REMOVED"] = 3] = "REMOVED"; 5444})(_ViewRepeaterOperation || (_ViewRepeaterOperation = {})); 5445var _VIEW_REPEATER_STRATEGY = new InjectionToken("_ViewRepeater"); 5446var _RecycleViewRepeaterStrategy = class { 5447 /** 5448 * The size of the cache used to store unused views. 5449 * Setting the cache size to `0` will disable caching. Defaults to 20 views. 5450 */ 5451 viewCacheSize = 20; 5452 /** 5453 * View cache that stores embedded view instances that have been previously stamped out, 5454 * but don't are not currently rendered. The view repeater will reuse these views rather than 5455 * creating brand new ones. 5456 * 5457 * TODO(michaeljamesparsons) Investigate whether using a linked list would improve performance. 5458 */ 5459 _viewCache = []; 5460 /** Apply changes to the DOM. */ 5461 applyChanges(changes, viewContainerRef, itemContextFactory, itemValueResolver, itemViewChanged) { 5462 changes.forEachOperation((record, adjustedPreviousIndex, currentIndex) => { 5463 let view; 5464 let operation; 5465 if (record.previousIndex == null) { 5466 const viewArgsFactory = () => itemContextFactory(record, adjustedPreviousIndex, currentIndex); 5467 view = this._insertView(viewArgsFactory, currentIndex, viewContainerRef, itemValueResolver(record)); 5468 operation = view ? _ViewRepeaterOperation.INSERTED : _ViewRepeaterOperation.REPLACED; 5469 } else if (currentIndex == null) { 5470 this._detachAndCacheView(adjustedPreviousIndex, viewContainerRef); 5471 operation = _ViewRepeaterOperation.REMOVED; 5472 } else { 5473 view = this._moveView(adjustedPreviousIndex, currentIndex, viewContainerRef, itemValueResolver(record)); 5474 operation = _ViewRepeaterOperation.MOVED; 5475 } 5476 if (itemViewChanged) { 5477 itemViewChanged({ 5478 context: view == null ? void 0 : view.context, 5479 operation, 5480 record 5481 }); 5482 } 5483 }); 5484 } 5485 detach() { 5486 for (const view of this._viewCache) { 5487 view.destroy(); 5488 } 5489 this._viewCache = []; 5490 } 5491 /** 5492 * Inserts a view for a new item, either from the cache or by creating a new 5493 * one. Returns `undefined` if the item was inserted into a cached view. 5494 */ 5495 _insertView(viewArgsFactory, currentIndex, viewContainerRef, value) { 5496 const cachedView = this._insertViewFromCache(currentIndex, viewContainerRef); 5497 if (cachedView) { 5498 cachedView.context.$implicit = value; 5499 return void 0; 5500 } 5501 const viewArgs = viewArgsFactory(); 5502 return viewContainerRef.createEmbeddedView(viewArgs.templateRef, viewArgs.context, viewArgs.index); 5503 } 5504 /** Detaches the view at the given index and inserts into the view cache. */ 5505 _detachAndCacheView(index, viewContainerRef) { 5506 const detachedView = viewContainerRef.detach(index); 5507 this._maybeCacheView(detachedView, viewContainerRef); 5508 } 5509 /** Moves view at the previous index to the current index. */ 5510 _moveView(adjustedPreviousIndex, currentIndex, viewContainerRef, value) { 5511 const view = viewContainerRef.get(adjustedPreviousIndex); 5512 viewContainerRef.move(view, currentIndex); 5513 view.context.$implicit = value; 5514 return view; 5515 } 5516 /** 5517 * Cache the given detached view. If the cache is full, the view will be 5518 * destroyed. 5519 */ 5520 _maybeCacheView(view, viewContainerRef) { 5521 if (this._viewCache.length < this.viewCacheSize) { 5522 this._viewCache.push(view); 5523 } else { 5524 const index = viewContainerRef.indexOf(view); 5525 if (index === -1) { 5526 view.destroy(); 5527 } else { 5528 viewContainerRef.remove(index); 5529 } 5530 } 5531 } 5532 /** Inserts a recycled view from the cache at the given index. */ 5533 _insertViewFromCache(index, viewContainerRef) { 5534 const cachedView = this._viewCache.pop(); 5535 if (cachedView) { 5536 viewContainerRef.insert(cachedView, index); 5537 } 5538 return cachedView || null; 5539 } 5540}; 5541
vendor: 58,929 bytes, lines 5542-7011
5542// node_modules/@angular/cdk/fesm2022/scrolling.mjs 5543var _c03 = ["contentWrapper"]; 5544var _c1 = ["*"]; 5545var VIRTUAL_SCROLL_STRATEGY = new InjectionToken("VIRTUAL_SCROLL_STRATEGY"); 5546var FixedSizeVirtualScrollStrategy = class { 5547 _scrolledIndexChange = new Subject(); 5548 /** @docs-private Implemented as part of VirtualScrollStrategy. */ 5549 scrolledIndexChange = this._scrolledIndexChange.pipe(distinctUntilChanged()); 5550 /** The attached viewport. */ 5551 _viewport = null; 5552 /** The size of the items in the virtually scrolling list. */ 5553 _itemSize; 5554 /** The minimum amount of buffer rendered beyond the viewport (in pixels). */ 5555 _minBufferPx; 5556 /** The number of buffer items to render beyond the edge of the viewport (in pixels). */ 5557 _maxBufferPx; 5558 /** 5559 * @param itemSize The size of the items in the virtually scrolling list. 5560 * @param minBufferPx The minimum amount of buffer (in pixels) before needing to render more 5561 * @param maxBufferPx The amount of buffer (in pixels) to render when rendering more. 5562 */ 5563 constructor(itemSize, minBufferPx, maxBufferPx) { 5564 this._itemSize = itemSize; 5565 this._minBufferPx = minBufferPx; 5566 this._maxBufferPx = maxBufferPx; 5567 } 5568 /** 5569 * Attaches this scroll strategy to a viewport. 5570 * @param viewport The viewport to attach this strategy to. 5571 */ 5572 attach(viewport) { 5573 this._viewport = viewport; 5574 this._updateTotalContentSize(); 5575 this._updateRenderedRange(); 5576 } 5577 /** Detaches this scroll strategy from the currently attached viewport. */ 5578 detach() { 5579 this._scrolledIndexChange.complete(); 5580 this._viewport = null; 5581 } 5582 /** 5583 * Update the item size and buffer size. 5584 * @param itemSize The size of the items in the virtually scrolling list. 5585 * @param minBufferPx The minimum amount of buffer (in pixels) before needing to render more 5586 * @param maxBufferPx The amount of buffer (in pixels) to render when rendering more. 5587 */ 5588 updateItemAndBufferSize(itemSize, minBufferPx, maxBufferPx) { 5589 if (maxBufferPx < minBufferPx && (typeof ngDevMode === "undefined" || ngDevMode)) { 5590 throw Error("CDK virtual scroll: maxBufferPx must be greater than or equal to minBufferPx"); 5591 } 5592 this._itemSize = itemSize; 5593 this._minBufferPx = minBufferPx; 5594 this._maxBufferPx = maxBufferPx; 5595 this._updateTotalContentSize(); 5596 this._updateRenderedRange(); 5597 } 5598 /** @docs-private Implemented as part of VirtualScrollStrategy. */ 5599 onContentScrolled() { 5600 this._updateRenderedRange(); 5601 } 5602 /** @docs-private Implemented as part of VirtualScrollStrategy. */ 5603 onDataLengthChanged() { 5604 this._updateTotalContentSize(); 5605 this._updateRenderedRange(); 5606 } 5607 /** @docs-private Implemented as part of VirtualScrollStrategy. */ 5608 onContentRendered() { 5609 } 5610 /** @docs-private Implemented as part of VirtualScrollStrategy. */ 5611 onRenderedOffsetChanged() { 5612 } 5613 /** 5614 * Scroll to the offset for the given index. 5615 * @param index The index of the element to scroll to. 5616 * @param behavior The ScrollBehavior to use when scrolling. 5617 */ 5618 scrollToIndex(index, behavior) { 5619 if (this._viewport) { 5620 this._viewport.scrollToOffset(index * this._itemSize, behavior); 5621 } 5622 } 5623 /** Update the viewport's total content size. */ 5624 _updateTotalContentSize() { 5625 if (!this._viewport) { 5626 return; 5627 } 5628 this._viewport.setTotalContentSize(this._viewport.getDataLength() * this._itemSize); 5629 } 5630 /** Update the viewport's rendered range. */ 5631 _updateRenderedRange() { 5632 if (!this._viewport) { 5633 return; 5634 } 5635 const renderedRange = this._viewport.getRenderedRange(); 5636 const newRange = { 5637 start: renderedRange.start, 5638 end: renderedRange.end 5639 }; 5640 const viewportSize = this._viewport.getViewportSize(); 5641 const dataLength = this._viewport.getDataLength(); 5642 let scrollOffset = this._viewport.measureScrollOffset(); 5643 let firstVisibleIndex = this._itemSize > 0 ? scrollOffset / this._itemSize : 0; 5644 if (newRange.end > dataLength) { 5645 const maxVisibleItems = Math.ceil(viewportSize / this._itemSize); 5646 const newVisibleIndex = Math.max(0, Math.min(firstVisibleIndex, dataLength - maxVisibleItems)); 5647 if (firstVisibleIndex != newVisibleIndex) { 5648 firstVisibleIndex = newVisibleIndex; 5649 scrollOffset = newVisibleIndex * this._itemSize; 5650 newRange.start = Math.floor(firstVisibleIndex); 5651 } 5652 newRange.end = Math.max(0, Math.min(dataLength, newRange.start + maxVisibleItems)); 5653 } 5654 const startBuffer = scrollOffset - newRange.start * this._itemSize; 5655 if (startBuffer < this._minBufferPx && newRange.start != 0) { 5656 const expandStart = Math.ceil((this._maxBufferPx - startBuffer) / this._itemSize); 5657 newRange.start = Math.max(0, newRange.start - expandStart); 5658 newRange.end = Math.min(dataLength, Math.ceil(firstVisibleIndex + (viewportSize + this._minBufferPx) / this._itemSize)); 5659 } else { 5660 const endBuffer = newRange.end * this._itemSize - (scrollOffset + viewportSize); 5661 if (endBuffer < this._minBufferPx && newRange.end != dataLength) { 5662 const expandEnd = Math.ceil((this._maxBufferPx - endBuffer) / this._itemSize); 5663 if (expandEnd > 0) { 5664 newRange.end = Math.min(dataLength, newRange.end + expandEnd); 5665 newRange.start = Math.max(0, Math.floor(firstVisibleIndex - this._minBufferPx / this._itemSize)); 5666 } 5667 } 5668 } 5669 this._viewport.setRenderedRange(newRange); 5670 this._viewport.setRenderedContentOffset(this._itemSize * newRange.start); 5671 this._scrolledIndexChange.next(Math.floor(firstVisibleIndex)); 5672 } 5673}; 5674function _fixedSizeVirtualScrollStrategyFactory(fixedSizeDir) { 5675 return fixedSizeDir._scrollStrategy; 5676} 5677var _CdkFixedSizeVirtualScroll = class _CdkFixedSizeVirtualScroll { 5678 /** The size of the items in the list (in pixels). */ 5679 get itemSize() { 5680 return this._itemSize; 5681 } 5682 set itemSize(value) { 5683 this._itemSize = coerceNumberProperty(value); 5684 } 5685 _itemSize = 20; 5686 /** 5687 * The minimum amount of buffer rendered beyond the viewport (in pixels). 5688 * If the amount of buffer dips below this number, more items will be rendered. Defaults to 100px. 5689 */ 5690 get minBufferPx() { 5691 return this._minBufferPx; 5692 } 5693 set minBufferPx(value) { 5694 this._minBufferPx = coerceNumberProperty(value); 5695 } 5696 _minBufferPx = 100; 5697 /** 5698 * The number of pixels worth of buffer to render for when rendering new items. Defaults to 200px. 5699 */ 5700 get maxBufferPx() { 5701 return this._maxBufferPx; 5702 } 5703 set maxBufferPx(value) { 5704 this._maxBufferPx = coerceNumberProperty(value); 5705 } 5706 _maxBufferPx = 200; 5707 /** The scroll strategy used by this directive. */ 5708 _scrollStrategy = new FixedSizeVirtualScrollStrategy(this.itemSize, this.minBufferPx, this.maxBufferPx); 5709 ngOnChanges() { 5710 this._scrollStrategy.updateItemAndBufferSize(this.itemSize, this.minBufferPx, this.maxBufferPx); 5711 } 5712}; 5713__publicField(_CdkFixedSizeVirtualScroll, "\u0275fac", function CdkFixedSizeVirtualScroll_Factory(__ngFactoryType__) { 5714 return new (__ngFactoryType__ || _CdkFixedSizeVirtualScroll)(); 5715}); 5716__publicField(_CdkFixedSizeVirtualScroll, "\u0275dir", /* @__PURE__ */ \u0275\u0275defineDirective({ 5717 type: _CdkFixedSizeVirtualScroll, 5718 selectors: [["cdk-virtual-scroll-viewport", "itemSize", ""]], 5719 inputs: { 5720 itemSize: "itemSize", 5721 minBufferPx: "minBufferPx", 5722 maxBufferPx: "maxBufferPx" 5723 }, 5724 features: [\u0275\u0275ProvidersFeature([{ 5725 provide: VIRTUAL_SCROLL_STRATEGY, 5726 useFactory: _fixedSizeVirtualScrollStrategyFactory, 5727 deps: [forwardRef(() => _CdkFixedSizeVirtualScroll)] 5728 }]), \u0275\u0275NgOnChangesFeature] 5729})); 5730var CdkFixedSizeVirtualScroll = _CdkFixedSizeVirtualScroll; 5731(() => { 5732 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(CdkFixedSizeVirtualScroll, [{ 5733 type: Directive, 5734 args: [{ 5735 selector: "cdk-virtual-scroll-viewport[itemSize]", 5736 providers: [{ 5737 provide: VIRTUAL_SCROLL_STRATEGY, 5738 useFactory: _fixedSizeVirtualScrollStrategyFactory, 5739 deps: [forwardRef(() => CdkFixedSizeVirtualScroll)] 5740 }] 5741 }] 5742 }], null, { 5743 itemSize: [{ 5744 type: Input 5745 }], 5746 minBufferPx: [{ 5747 type: Input 5748 }], 5749 maxBufferPx: [{ 5750 type: Input 5751 }] 5752 }); 5753})(); 5754var DEFAULT_SCROLL_TIME = 20; 5755var _ScrollDispatcher = class _ScrollDispatcher { 5756 _ngZone = inject(NgZone); 5757 _platform = inject(Platform); 5758 _renderer = inject(RendererFactory2).createRenderer(null, null); 5759 _cleanupGlobalListener; 5760 constructor() { 5761 } 5762 /** Subject for notifying that a registered scrollable reference element has been scrolled. */ 5763 _scrolled = new Subject(); 5764 /** Keeps track of the amount of subscriptions to `scrolled`. Used for cleaning up afterwards. */ 5765 _scrolledCount = 0; 5766 /** 5767 * Map of all the scrollable references that are registered with the service and their 5768 * scroll event subscriptions. 5769 */ 5770 scrollContainers = /* @__PURE__ */ new Map(); 5771 /** 5772 * Registers a scrollable instance with the service and listens for its scrolled events. When the 5773 * scrollable is scrolled, the service emits the event to its scrolled observable. 5774 * @param scrollable Scrollable instance to be registered. 5775 */ 5776 register(scrollable) { 5777 if (!this.scrollContainers.has(scrollable)) { 5778 this.scrollContainers.set(scrollable, scrollable.elementScrolled().subscribe(() => this._scrolled.next(scrollable))); 5779 } 5780 } 5781 /** 5782 * De-registers a Scrollable reference and unsubscribes from its scroll event observable. 5783 * @param scrollable Scrollable instance to be deregistered. 5784 */ 5785 deregister(scrollable) { 5786 const scrollableReference = this.scrollContainers.get(scrollable); 5787 if (scrollableReference) { 5788 scrollableReference.unsubscribe(); 5789 this.scrollContainers.delete(scrollable); 5790 } 5791 } 5792 /** 5793 * Returns an observable that emits an event whenever any of the registered Scrollable 5794 * references (or window, document, or body) fire a scrolled event. Can provide a time in ms 5795 * to override the default "throttle" time. 5796 * 5797 * **Note:** in order to avoid hitting change detection for every scroll event, 5798 * all of the events emitted from this stream will be run outside the Angular zone. 5799 * If you need to update any data bindings as a result of a scroll event, you have 5800 * to run the callback using `NgZone.run`. 5801 */ 5802 scrolled(auditTimeInMs = DEFAULT_SCROLL_TIME) { 5803 if (!this._platform.isBrowser) { 5804 return of(); 5805 } 5806 return new Observable((observer) => { 5807 if (!this._cleanupGlobalListener) { 5808 this._cleanupGlobalListener = this._ngZone.runOutsideAngular(() => this._renderer.listen("document", "scroll", () => this._scrolled.next())); 5809 } 5810 const subscription = auditTimeInMs > 0 ? this._scrolled.pipe(auditTime(auditTimeInMs)).subscribe(observer) : this._scrolled.subscribe(observer); 5811 this._scrolledCount++; 5812 return () => { 5813 var _a3; 5814 subscription.unsubscribe(); 5815 this._scrolledCount--; 5816 if (!this._scrolledCount) { 5817 (_a3 = this._cleanupGlobalListener) == null ? void 0 : _a3.call(this); 5818 this._cleanupGlobalListener = void 0; 5819 } 5820 }; 5821 }); 5822 } 5823 ngOnDestroy() { 5824 var _a3; 5825 (_a3 = this._cleanupGlobalListener) == null ? void 0 : _a3.call(this); 5826 this._cleanupGlobalListener = void 0; 5827 this.scrollContainers.forEach((_, container) => this.deregister(container)); 5828 this._scrolled.complete(); 5829 } 5830 /** 5831 * Returns an observable that emits whenever any of the 5832 * scrollable ancestors of an element are scrolled. 5833 * @param elementOrElementRef Element whose ancestors to listen for. 5834 * @param auditTimeInMs Time to throttle the scroll events. 5835 */ 5836 ancestorScrolled(elementOrElementRef, auditTimeInMs) { 5837 const ancestors = this.getAncestorScrollContainers(elementOrElementRef); 5838 return this.scrolled(auditTimeInMs).pipe(filter((target) => !target || ancestors.indexOf(target) > -1)); 5839 } 5840 /** Returns all registered Scrollables that contain the provided element. */ 5841 getAncestorScrollContainers(elementOrElementRef) { 5842 const scrollingContainers = []; 5843 this.scrollContainers.forEach((_subscription, scrollable) => { 5844 if (this._scrollableContainsElement(scrollable, elementOrElementRef)) { 5845 scrollingContainers.push(scrollable); 5846 } 5847 }); 5848 return scrollingContainers; 5849 } 5850 /** Returns true if the element is contained within the provided Scrollable. */ 5851 _scrollableContainsElement(scrollable, elementOrElementRef) { 5852 let element = coerceElement(elementOrElementRef); 5853 let scrollableElement = scrollable.getElementRef().nativeElement; 5854 do { 5855 if (element == scrollableElement) { 5856 return true; 5857 } 5858 } while (element = element.parentElement); 5859 return false; 5860 } 5861}; 5862__publicField(_ScrollDispatcher, "\u0275fac", function ScrollDispatcher_Factory(__ngFactoryType__) { 5863 return new (__ngFactoryType__ || _ScrollDispatcher)(); 5864}); 5865__publicField(_ScrollDispatcher, "\u0275prov", /* @__PURE__ */ \u0275\u0275defineInjectable({ 5866 token: _ScrollDispatcher, 5867 factory: _ScrollDispatcher.\u0275fac, 5868 providedIn: "root" 5869})); 5870var ScrollDispatcher = _ScrollDispatcher; 5871(() => { 5872 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(ScrollDispatcher, [{ 5873 type: Injectable, 5874 args: [{ 5875 providedIn: "root" 5876 }] 5877 }], () => [], null); 5878})(); 5879var _CdkScrollable = class _CdkScrollable { 5880 elementRef = inject(ElementRef); 5881 scrollDispatcher = inject(ScrollDispatcher); 5882 ngZone = inject(NgZone); 5883 dir = inject(Directionality, { 5884 optional: true 5885 }); 5886 _scrollElement = this.elementRef.nativeElement; 5887 _destroyed = new Subject(); 5888 _renderer = inject(Renderer2); 5889 _cleanupScroll; 5890 _elementScrolled = new Subject(); 5891 constructor() { 5892 } 5893 ngOnInit() { 5894 this._cleanupScroll = this.ngZone.runOutsideAngular(() => this._renderer.listen(this._scrollElement, "scroll", (event) => this._elementScrolled.next(event))); 5895 this.scrollDispatcher.register(this); 5896 } 5897 ngOnDestroy() { 5898 var _a3; 5899 (_a3 = this._cleanupScroll) == null ? void 0 : _a3.call(this); 5900 this._elementScrolled.complete(); 5901 this.scrollDispatcher.deregister(this); 5902 this._destroyed.next(); 5903 this._destroyed.complete(); 5904 } 5905 /** Returns observable that emits when a scroll event is fired on the host element. */ 5906 elementScrolled() { 5907 return this._elementScrolled; 5908 } 5909 /** Gets the ElementRef for the viewport. */ 5910 getElementRef() { 5911 return this.elementRef; 5912 } 5913 /** 5914 * Scrolls to the specified offsets. This is a normalized version of the browser's native scrollTo 5915 * method, since browsers are not consistent about what scrollLeft means in RTL. For this method 5916 * left and right always refer to the left and right side of the scrolling container irrespective 5917 * of the layout direction. start and end refer to left and right in an LTR context and vice-versa 5918 * in an RTL context. 5919 * @param options specified the offsets to scroll to. 5920 */ 5921 scrollTo(options) { 5922 const el = this.elementRef.nativeElement; 5923 const isRtl = this.dir && this.dir.value == "rtl"; 5924 if (options.left == null) { 5925 options.left = isRtl ? options.end : options.start; 5926 } 5927 if (options.right == null) { 5928 options.right = isRtl ? options.start : options.end; 5929 } 5930 if (options.bottom != null) { 5931 options.top = el.scrollHeight - el.clientHeight - options.bottom; 5932 } 5933 if (isRtl && getRtlScrollAxisType() != RtlScrollAxisType.NORMAL) { 5934 if (options.left != null) { 5935 options.right = el.scrollWidth - el.clientWidth - options.left; 5936 } 5937 if (getRtlScrollAxisType() == RtlScrollAxisType.INVERTED) { 5938 options.left = options.right; 5939 } else if (getRtlScrollAxisType() == RtlScrollAxisType.NEGATED) { 5940 options.left = options.right ? -options.right : options.right; 5941 } 5942 } else { 5943 if (options.right != null) { 5944 options.left = el.scrollWidth - el.clientWidth - options.right; 5945 } 5946 } 5947 this._applyScrollToOptions(options); 5948 } 5949 _applyScrollToOptions(options) { 5950 const el = this.elementRef.nativeElement; 5951 if (supportsScrollBehavior()) { 5952 el.scrollTo(options); 5953 } else { 5954 if (options.top != null) { 5955 el.scrollTop = options.top; 5956 } 5957 if (options.left != null) { 5958 el.scrollLeft = options.left; 5959 } 5960 } 5961 } 5962 /** 5963 * Measures the scroll offset relative to the specified edge of the viewport. This method can be 5964 * used instead of directly checking scrollLeft or scrollTop, since browsers are not consistent 5965 * about what scrollLeft means in RTL. The values returned by this method are normalized such that 5966 * left and right always refer to the left and right side of the scrolling container irrespective 5967 * of the layout direction. start and end refer to left and right in an LTR context and vice-versa 5968 * in an RTL context. 5969 * @param from The edge to measure from. 5970 */ 5971 measureScrollOffset(from) { 5972 const LEFT = "left"; 5973 const RIGHT = "right"; 5974 const el = this.elementRef.nativeElement; 5975 if (from == "top") { 5976 return el.scrollTop; 5977 } 5978 if (from == "bottom") { 5979 return el.scrollHeight - el.clientHeight - el.scrollTop; 5980 } 5981 const isRtl = this.dir && this.dir.value == "rtl"; 5982 if (from == "start") { 5983 from = isRtl ? RIGHT : LEFT; 5984 } else if (from == "end") { 5985 from = isRtl ? LEFT : RIGHT; 5986 } 5987 if (isRtl && getRtlScrollAxisType() == RtlScrollAxisType.INVERTED) { 5988 if (from == LEFT) { 5989 return el.scrollWidth - el.clientWidth - el.scrollLeft; 5990 } else { 5991 return el.scrollLeft; 5992 } 5993 } else if (isRtl && getRtlScrollAxisType() == RtlScrollAxisType.NEGATED) { 5994 if (from == LEFT) { 5995 return el.scrollLeft + el.scrollWidth - el.clientWidth; 5996 } else { 5997 return -el.scrollLeft; 5998 } 5999 } else { 6000 if (from == LEFT) { 6001 return el.scrollLeft; 6002 } else { 6003 return el.scrollWidth - el.clientWidth - el.scrollLeft; 6004 } 6005 } 6006 } 6007}; 6008__publicField(_CdkScrollable, "\u0275fac", function CdkScrollable_Factory(__ngFactoryType__) { 6009 return new (__ngFactoryType__ || _CdkScrollable)(); 6010}); 6011__publicField(_CdkScrollable, "\u0275dir", /* @__PURE__ */ \u0275\u0275defineDirective({ 6012 type: _CdkScrollable, 6013 selectors: [["", "cdk-scrollable", ""], ["", "cdkScrollable", ""]] 6014})); 6015var CdkScrollable = _CdkScrollable; 6016(() => { 6017 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(CdkScrollable, [{ 6018 type: Directive, 6019 args: [{ 6020 selector: "[cdk-scrollable], [cdkScrollable]" 6021 }] 6022 }], () => [], null); 6023})(); 6024var DEFAULT_RESIZE_TIME = 20; 6025var _ViewportRuler = class _ViewportRuler { 6026 _platform = inject(Platform); 6027 _listeners; 6028 /** Cached viewport dimensions. */ 6029 _viewportSize; 6030 /** Stream of viewport change events. */ 6031 _change = new Subject(); 6032 /** Used to reference correct document/window */ 6033 _document = inject(DOCUMENT, { 6034 optional: true 6035 }); 6036 constructor() { 6037 const ngZone = inject(NgZone); 6038 const renderer = inject(RendererFactory2).createRenderer(null, null); 6039 ngZone.runOutsideAngular(() => { 6040 if (this._platform.isBrowser) { 6041 const changeListener = (event) => this._change.next(event); 6042 this._listeners = [renderer.listen("window", "resize", changeListener), renderer.listen("window", "orientationchange", changeListener)]; 6043 } 6044 this.change().subscribe(() => this._viewportSize = null); 6045 }); 6046 } 6047 ngOnDestroy() { 6048 var _a3; 6049 (_a3 = this._listeners) == null ? void 0 : _a3.forEach((cleanup) => cleanup()); 6050 this._change.complete(); 6051 } 6052 /** Returns the viewport's width and height. */ 6053 getViewportSize() { 6054 if (!this._viewportSize) { 6055 this._updateViewportSize(); 6056 } 6057 const output = { 6058 width: this._viewportSize.width, 6059 height: this._viewportSize.height 6060 }; 6061 if (!this._platform.isBrowser) { 6062 this._viewportSize = null; 6063 } 6064 return output; 6065 } 6066 /** Gets a DOMRect for the viewport's bounds. */ 6067 getViewportRect() { 6068 const scrollPosition = this.getViewportScrollPosition(); 6069 const { 6070 width, 6071 height 6072 } = this.getViewportSize(); 6073 return { 6074 top: scrollPosition.top, 6075 left: scrollPosition.left, 6076 bottom: scrollPosition.top + height, 6077 right: scrollPosition.left + width, 6078 height, 6079 width 6080 }; 6081 } 6082 /** Gets the (top, left) scroll position of the viewport. */ 6083 getViewportScrollPosition() { 6084 if (!this._platform.isBrowser) { 6085 return { 6086 top: 0, 6087 left: 0 6088 }; 6089 } 6090 const document2 = this._document; 6091 const window2 = this._getWindow(); 6092 const documentElement = document2.documentElement; 6093 const documentRect = documentElement.getBoundingClientRect(); 6094 const top = -documentRect.top || document2.body.scrollTop || window2.scrollY || documentElement.scrollTop || 0; 6095 const left = -documentRect.left || document2.body.scrollLeft || window2.scrollX || documentElement.scrollLeft || 0; 6096 return { 6097 top, 6098 left 6099 }; 6100 } 6101 /** 6102 * Returns a stream that emits whenever the size of the viewport changes. 6103 * This stream emits outside of the Angular zone. 6104 * @param throttleTime Time in milliseconds to throttle the stream. 6105 */ 6106 change(throttleTime = DEFAULT_RESIZE_TIME) { 6107 return throttleTime > 0 ? this._change.pipe(auditTime(throttleTime)) : this._change; 6108 } 6109 /** Use defaultView of injected document if available or fallback to global window reference */ 6110 _getWindow() { 6111 return this._document.defaultView || window; 6112 } 6113 /** Updates the cached viewport size. */ 6114 _updateViewportSize() { 6115 const window2 = this._getWindow(); 6116 this._viewportSize = this._platform.isBrowser ? { 6117 width: window2.innerWidth, 6118 height: window2.innerHeight 6119 } : { 6120 width: 0, 6121 height: 0 6122 }; 6123 } 6124}; 6125__publicField(_ViewportRuler, "\u0275fac", function ViewportRuler_Factory(__ngFactoryType__) { 6126 return new (__ngFactoryType__ || _ViewportRuler)(); 6127}); 6128__publicField(_ViewportRuler, "\u0275prov", /* @__PURE__ */ \u0275\u0275defineInjectable({ 6129 token: _ViewportRuler, 6130 factory: _ViewportRuler.\u0275fac, 6131 providedIn: "root" 6132})); 6133var ViewportRuler = _ViewportRuler; 6134(() => { 6135 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(ViewportRuler, [{ 6136 type: Injectable, 6137 args: [{ 6138 providedIn: "root" 6139 }] 6140 }], () => [], null); 6141})(); 6142var VIRTUAL_SCROLLABLE = new InjectionToken("VIRTUAL_SCROLLABLE"); 6143var _CdkVirtualScrollable = class _CdkVirtualScrollable extends CdkScrollable { 6144 constructor() { 6145 super(); 6146 } 6147 /** 6148 * Measure the viewport size for the provided orientation. 6149 * 6150 * @param orientation The orientation to measure the size from. 6151 */ 6152 measureViewportSize(orientation) { 6153 const viewportEl = this.elementRef.nativeElement; 6154 return orientation === "horizontal" ? viewportEl.clientWidth : viewportEl.clientHeight; 6155 } 6156}; 6157__publicField(_CdkVirtualScrollable, "\u0275fac", function CdkVirtualScrollable_Factory(__ngFactoryType__) { 6158 return new (__ngFactoryType__ || _CdkVirtualScrollable)(); 6159}); 6160__publicField(_CdkVirtualScrollable, "\u0275dir", /* @__PURE__ */ \u0275\u0275defineDirective({ 6161 type: _CdkVirtualScrollable, 6162 features: [\u0275\u0275InheritDefinitionFeature] 6163})); 6164var CdkVirtualScrollable = _CdkVirtualScrollable; 6165(() => { 6166 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(CdkVirtualScrollable, [{ 6167 type: Directive 6168 }], () => [], null); 6169})(); 6170function rangesEqual(r1, r2) { 6171 return r1.start == r2.start && r1.end == r2.end; 6172} 6173var SCROLL_SCHEDULER = typeof requestAnimationFrame !== "undefined" ? animationFrameScheduler : asapScheduler; 6174var _CdkVirtualScrollViewport = class _CdkVirtualScrollViewport extends CdkVirtualScrollable { 6175 elementRef = inject(ElementRef); 6176 _changeDetectorRef = inject(ChangeDetectorRef); 6177 _scrollStrategy = inject(VIRTUAL_SCROLL_STRATEGY, { 6178 optional: true 6179 }); 6180 scrollable = inject(VIRTUAL_SCROLLABLE, { 6181 optional: true 6182 }); 6183 _platform = inject(Platform); 6184 /** Emits when the viewport is detached from a CdkVirtualForOf. */ 6185 _detachedSubject = new Subject(); 6186 /** Emits when the rendered range changes. */ 6187 _renderedRangeSubject = new Subject(); 6188 /** The direction the viewport scrolls. */ 6189 get orientation() { 6190 return this._orientation; 6191 } 6192 set orientation(orientation) { 6193 if (this._orientation !== orientation) { 6194 this._orientation = orientation; 6195 this._calculateSpacerSize(); 6196 } 6197 } 6198 _orientation = "vertical"; 6199 /** 6200 * Whether rendered items should persist in the DOM after scrolling out of view. By default, items 6201 * will be removed. 6202 */ 6203 appendOnly = false; 6204 // Note: we don't use the typical EventEmitter here because we need to subscribe to the scroll 6205 // strategy lazily (i.e. only if the user is actually listening to the events). We do this because 6206 // depending on how the strategy calculates the scrolled index, it may come at a cost to 6207 // performance. 6208 /** Emits when the index of the first element visible in the viewport changes. */ 6209 scrolledIndexChange = new Observable((observer) => this._scrollStrategy.scrolledIndexChange.subscribe((index) => Promise.resolve().then(() => this.ngZone.run(() => observer.next(index))))); 6210 /** The element that wraps the rendered content. */ 6211 _contentWrapper; 6212 /** A stream that emits whenever the rendered range changes. */ 6213 renderedRangeStream = this._renderedRangeSubject; 6214 /** 6215 * The total size of all content (in pixels), including content that is not currently rendered. 6216 */ 6217 _totalContentSize = 0; 6218 /** A string representing the `style.width` property value to be used for the spacer element. */ 6219 _totalContentWidth = ""; 6220 /** A string representing the `style.height` property value to be used for the spacer element. */ 6221 _totalContentHeight = ""; 6222 /** 6223 * The CSS transform applied to the rendered subset of items so that they appear within the bounds 6224 * of the visible viewport. 6225 */ 6226 _renderedContentTransform; 6227 /** The currently rendered range of indices. */ 6228 _renderedRange = { 6229 start: 0, 6230 end: 0 6231 }; 6232 /** The length of the data bound to this viewport (in number of items). */ 6233 _dataLength = 0; 6234 /** The size of the viewport (in pixels). */ 6235 _viewportSize = 0; 6236 /** the currently attached CdkVirtualScrollRepeater. */ 6237 _forOf; 6238 /** The last rendered content offset that was set. */ 6239 _renderedContentOffset = 0; 6240 /** 6241 * Whether the last rendered content offset was to the end of the content (and therefore needs to 6242 * be rewritten as an offset to the start of the content). 6243 */ 6244 _renderedContentOffsetNeedsRewrite = false; 6245 /** Whether there is a pending change detection cycle. */ 6246 _isChangeDetectionPending = false; 6247 /** A list of functions to run after the next change detection cycle. */ 6248 _runAfterChangeDetection = []; 6249 /** Subscription to changes in the viewport size. */ 6250 _viewportChanges = Subscription.EMPTY; 6251 _injector = inject(Injector); 6252 _isDestroyed = false; 6253 constructor() { 6254 super(); 6255 const viewportRuler = inject(ViewportRuler); 6256 if (!this._scrollStrategy && (typeof ngDevMode === "undefined" || ngDevMode)) { 6257 throw Error('Error: cdk-virtual-scroll-viewport requires the "itemSize" property to be set.'); 6258 } 6259 this._viewportChanges = viewportRuler.change().subscribe(() => { 6260 this.checkViewportSize(); 6261 }); 6262 if (!this.scrollable) { 6263 this.elementRef.nativeElement.classList.add("cdk-virtual-scrollable"); 6264 this.scrollable = this; 6265 } 6266 } 6267 ngOnInit() { 6268 if (!this._platform.isBrowser) { 6269 return; 6270 } 6271 if (this.scrollable === this) { 6272 super.ngOnInit(); 6273 } 6274 this.ngZone.runOutsideAngular(() => Promise.resolve().then(() => { 6275 this._measureViewportSize(); 6276 this._scrollStrategy.attach(this); 6277 this.scrollable.elementScrolled().pipe( 6278 // Start off with a fake scroll event so we properly detect our initial position. 6279 startWith(null), 6280 // Collect multiple events into one until the next animation frame. This way if 6281 // there are multiple scroll events in the same frame we only need to recheck 6282 // our layout once. 6283 auditTime(0, SCROLL_SCHEDULER), 6284 // Usually `elementScrolled` is completed when the scrollable is destroyed, but 6285 // that may not be the case if a `CdkVirtualScrollableElement` is used so we have 6286 // to unsubscribe here just in case. 6287 takeUntil(this._destroyed) 6288 ).subscribe(() => this._scrollStrategy.onContentScrolled()); 6289 this._markChangeDetectionNeeded(); 6290 })); 6291 } 6292 ngOnDestroy() { 6293 this.detach(); 6294 this._scrollStrategy.detach(); 6295 this._renderedRangeSubject.complete(); 6296 this._detachedSubject.complete(); 6297 this._viewportChanges.unsubscribe(); 6298 this._isDestroyed = true; 6299 super.ngOnDestroy(); 6300 } 6301 /** Attaches a `CdkVirtualScrollRepeater` to this viewport. */ 6302 attach(forOf) { 6303 if (this._forOf && (typeof ngDevMode === "undefined" || ngDevMode)) { 6304 throw Error("CdkVirtualScrollViewport is already attached."); 6305 } 6306 this.ngZone.runOutsideAngular(() => { 6307 this._forOf = forOf; 6308 this._forOf.dataStream.pipe(takeUntil(this._detachedSubject)).subscribe((data) => { 6309 const newLength = data.length; 6310 if (newLength !== this._dataLength) { 6311 this._dataLength = newLength; 6312 this._scrollStrategy.onDataLengthChanged(); 6313 } 6314 this._doChangeDetection(); 6315 }); 6316 }); 6317 } 6318 /** Detaches the current `CdkVirtualForOf`. */ 6319 detach() { 6320 this._forOf = null; 6321 this._detachedSubject.next(); 6322 } 6323 /** Gets the length of the data bound to this viewport (in number of items). */ 6324 getDataLength() { 6325 return this._dataLength; 6326 } 6327 /** Gets the size of the viewport (in pixels). */ 6328 getViewportSize() { 6329 return this._viewportSize; 6330 } 6331 // TODO(mmalerba): This is technically out of sync with what's really rendered until a render 6332 // cycle happens. I'm being careful to only call it after the render cycle is complete and before 6333 // setting it to something else, but its error prone and should probably be split into 6334 // `pendingRange` and `renderedRange`, the latter reflecting whats actually in the DOM. 6335 /** Get the current rendered range of items. */ 6336 getRenderedRange() { 6337 return this._renderedRange; 6338 } 6339 measureBoundingClientRectWithScrollOffset(from) { 6340 return this.getElementRef().nativeElement.getBoundingClientRect()[from]; 6341 } 6342 /** 6343 * Sets the total size of all content (in pixels), including content that is not currently 6344 * rendered. 6345 */ 6346 setTotalContentSize(size) { 6347 if (this._totalContentSize !== size) { 6348 this._totalContentSize = size; 6349 this._calculateSpacerSize(); 6350 this._markChangeDetectionNeeded(); 6351 } 6352 } 6353 /** Sets the currently rendered range of indices. */ 6354 setRenderedRange(range) { 6355 if (!rangesEqual(this._renderedRange, range)) { 6356 if (this.appendOnly) { 6357 range = { 6358 start: 0, 6359 end: Math.max(this._renderedRange.end, range.end) 6360 }; 6361 } 6362 this._renderedRangeSubject.next(this._renderedRange = range); 6363 this._markChangeDetectionNeeded(() => this._scrollStrategy.onContentRendered()); 6364 } 6365 } 6366 /** 6367 * Gets the offset from the start of the viewport to the start of the rendered data (in pixels). 6368 */ 6369 getOffsetToRenderedContentStart() { 6370 return this._renderedContentOffsetNeedsRewrite ? null : this._renderedContentOffset; 6371 } 6372 /** 6373 * Sets the offset from the start of the viewport to either the start or end of the rendered data 6374 * (in pixels). 6375 */ 6376 setRenderedContentOffset(offset, to = "to-start") { 6377 offset = this.appendOnly && to === "to-start" ? 0 : offset; 6378 const isRtl = this.dir && this.dir.value == "rtl"; 6379 const isHorizontal = this.orientation == "horizontal"; 6380 const axis = isHorizontal ? "X" : "Y"; 6381 const axisDirection = isHorizontal && isRtl ? -1 : 1; 6382 let transform = `translate${axis}(${Number(axisDirection * offset)}px)`; 6383 this._renderedContentOffset = offset; 6384 if (to === "to-end") { 6385 transform += ` translate${axis}(-100%)`; 6386 this._renderedContentOffsetNeedsRewrite = true; 6387 } 6388 if (this._renderedContentTransform != transform) { 6389 this._renderedContentTransform = transform; 6390 this._markChangeDetectionNeeded(() => { 6391 if (this._renderedContentOffsetNeedsRewrite) { 6392 this._renderedContentOffset -= this.measureRenderedContentSize(); 6393 this._renderedContentOffsetNeedsRewrite = false; 6394 this.setRenderedContentOffset(this._renderedContentOffset); 6395 } else { 6396 this._scrollStrategy.onRenderedOffsetChanged(); 6397 } 6398 }); 6399 } 6400 } 6401 /** 6402 * Scrolls to the given offset from the start of the viewport. Please note that this is not always 6403 * the same as setting `scrollTop` or `scrollLeft`. In a horizontal viewport with right-to-left 6404 * direction, this would be the equivalent of setting a fictional `scrollRight` property. 6405 * @param offset The offset to scroll to. 6406 * @param behavior The ScrollBehavior to use when scrolling. Default is behavior is `auto`. 6407 */ 6408 scrollToOffset(offset, behavior = "auto") { 6409 const options = { 6410 behavior 6411 }; 6412 if (this.orientation === "horizontal") { 6413 options.start = offset; 6414 } else { 6415 options.top = offset; 6416 } 6417 this.scrollable.scrollTo(options); 6418 } 6419 /** 6420 * Scrolls to the offset for the given index. 6421 * @param index The index of the element to scroll to. 6422 * @param behavior The ScrollBehavior to use when scrolling. Default is behavior is `auto`. 6423 */ 6424 scrollToIndex(index, behavior = "auto") { 6425 this._scrollStrategy.scrollToIndex(index, behavior); 6426 } 6427 /** 6428 * Gets the current scroll offset from the start of the scrollable (in pixels). 6429 * @param from The edge to measure the offset from. Defaults to 'top' in vertical mode and 'start' 6430 * in horizontal mode. 6431 */ 6432 measureScrollOffset(from) { 6433 let measureScrollOffset; 6434 if (this.scrollable == this) { 6435 measureScrollOffset = (_from) => super.measureScrollOffset(_from); 6436 } else { 6437 measureScrollOffset = (_from) => this.scrollable.measureScrollOffset(_from); 6438 } 6439 return Math.max(0, measureScrollOffset(from != null ? from : this.orientation === "horizontal" ? "start" : "top") - this.measureViewportOffset()); 6440 } 6441 /** 6442 * Measures the offset of the viewport from the scrolling container 6443 * @param from The edge to measure from. 6444 */ 6445 measureViewportOffset(from) { 6446 var _a3; 6447 let fromRect; 6448 const LEFT = "left"; 6449 const RIGHT = "right"; 6450 const isRtl = ((_a3 = this.dir) == null ? void 0 : _a3.value) == "rtl"; 6451 if (from == "start") { 6452 fromRect = isRtl ? RIGHT : LEFT; 6453 } else if (from == "end") { 6454 fromRect = isRtl ? LEFT : RIGHT; 6455 } else if (from) { 6456 fromRect = from; 6457 } else { 6458 fromRect = this.orientation === "horizontal" ? "left" : "top"; 6459 } 6460 const scrollerClientRect = this.scrollable.measureBoundingClientRectWithScrollOffset(fromRect); 6461 const viewportClientRect = this.elementRef.nativeElement.getBoundingClientRect()[fromRect]; 6462 return viewportClientRect - scrollerClientRect; 6463 } 6464 /** Measure the combined size of all of the rendered items. */ 6465 measureRenderedContentSize() { 6466 const contentEl = this._contentWrapper.nativeElement; 6467 return this.orientation === "horizontal" ? contentEl.offsetWidth : contentEl.offsetHeight; 6468 } 6469 /** 6470 * Measure the total combined size of the given range. Throws if the range includes items that are 6471 * not rendered. 6472 */ 6473 measureRangeSize(range) { 6474 if (!this._forOf) { 6475 return 0; 6476 } 6477 return this._forOf.measureRangeSize(range, this.orientation); 6478 } 6479 /** Update the viewport dimensions and re-render. */ 6480 checkViewportSize() { 6481 this._measureViewportSize(); 6482 this._scrollStrategy.onDataLengthChanged(); 6483 } 6484 /** Measure the viewport size. */ 6485 _measureViewportSize() { 6486 this._viewportSize = this.scrollable.measureViewportSize(this.orientation); 6487 } 6488 /** Queue up change detection to run. */ 6489 _markChangeDetectionNeeded(runAfter) { 6490 if (runAfter) { 6491 this._runAfterChangeDetection.push(runAfter); 6492 } 6493 if (!this._isChangeDetectionPending) { 6494 this._isChangeDetectionPending = true; 6495 this.ngZone.runOutsideAngular(() => Promise.resolve().then(() => { 6496 this._doChangeDetection(); 6497 })); 6498 } 6499 } 6500 /** Run change detection. */ 6501 _doChangeDetection() { 6502 if (this._isDestroyed) { 6503 return; 6504 } 6505 this.ngZone.run(() => { 6506 this._changeDetectorRef.markForCheck(); 6507 this._contentWrapper.nativeElement.style.transform = this._renderedContentTransform; 6508 afterNextRender(() => { 6509 this._isChangeDetectionPending = false; 6510 const runAfterChangeDetection = this._runAfterChangeDetection; 6511 this._runAfterChangeDetection = []; 6512 for (const fn of runAfterChangeDetection) { 6513 fn(); 6514 } 6515 }, { 6516 injector: this._injector 6517 }); 6518 }); 6519 } 6520 /** Calculates the `style.width` and `style.height` for the spacer element. */ 6521 _calculateSpacerSize() { 6522 this._totalContentHeight = this.orientation === "horizontal" ? "" : `${this._totalContentSize}px`; 6523 this._totalContentWidth = this.orientation === "horizontal" ? `${this._totalContentSize}px` : ""; 6524 } 6525}; 6526__publicField(_CdkVirtualScrollViewport, "\u0275fac", function CdkVirtualScrollViewport_Factory(__ngFactoryType__) { 6527 return new (__ngFactoryType__ || _CdkVirtualScrollViewport)(); 6528}); 6529__publicField(_CdkVirtualScrollViewport, "\u0275cmp", /* @__PURE__ */ \u0275\u0275defineComponent({ 6530 type: _CdkVirtualScrollViewport, 6531 selectors: [["cdk-virtual-scroll-viewport"]], 6532 viewQuery: function CdkVirtualScrollViewport_Query(rf, ctx) { 6533 if (rf & 1) { 6534 \u0275\u0275viewQuery(_c03, 7); 6535 } 6536 if (rf & 2) { 6537 let _t; 6538 \u0275\u0275queryRefresh(_t = \u0275\u0275loadQuery()) && (ctx._contentWrapper = _t.first); 6539 } 6540 }, 6541 hostAttrs: [1, "cdk-virtual-scroll-viewport"], 6542 hostVars: 4, 6543 hostBindings: function CdkVirtualScrollViewport_HostBindings(rf, ctx) { 6544 if (rf & 2) { 6545 \u0275\u0275classProp("cdk-virtual-scroll-orientation-horizontal", ctx.orientation === "horizontal")("cdk-virtual-scroll-orientation-vertical", ctx.orientation !== "horizontal"); 6546 } 6547 }, 6548 inputs: { 6549 orientation: "orientation", 6550 appendOnly: [2, "appendOnly", "appendOnly", booleanAttribute] 6551 }, 6552 outputs: { 6553 scrolledIndexChange: "scrolledIndexChange" 6554 }, 6555 features: [\u0275\u0275ProvidersFeature([{ 6556 provide: CdkScrollable, 6557 useFactory: (virtualScrollable, viewport) => virtualScrollable || viewport, 6558 deps: [[new Optional(), new Inject(VIRTUAL_SCROLLABLE)], _CdkVirtualScrollViewport] 6559 }]), \u0275\u0275InheritDefinitionFeature], 6560 ngContentSelectors: _c1, 6561 decls: 4, 6562 vars: 4, 6563 consts: [["contentWrapper", ""], [1, "cdk-virtual-scroll-content-wrapper"], [1, "cdk-virtual-scroll-spacer"]], 6564 template: function CdkVirtualScrollViewport_Template(rf, ctx) { 6565 if (rf & 1) { 6566 \u0275\u0275projectionDef(); 6567 \u0275\u0275elementStart(0, "div", 1, 0); 6568 \u0275\u0275projection(2); 6569 \u0275\u0275elementEnd(); 6570 \u0275\u0275element(3, "div", 2); 6571 } 6572 if (rf & 2) { 6573 \u0275\u0275advance(3); 6574 \u0275\u0275styleProp("width", ctx._totalContentWidth)("height", ctx._totalContentHeight); 6575 } 6576 }, 6577 styles: ["cdk-virtual-scroll-viewport{display:block;position:relative;transform:translateZ(0)}.cdk-virtual-scrollable{overflow:auto;will-change:scroll-position;contain:strict}.cdk-virtual-scroll-content-wrapper{position:absolute;top:0;left:0;contain:content}[dir=rtl] .cdk-virtual-scroll-content-wrapper{right:0;left:auto}.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper{min-height:100%}.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper>dl:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper>ol:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper>table:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper>ul:not([cdkVirtualFor]){padding-left:0;padding-right:0;margin-left:0;margin-right:0;border-left-width:0;border-right-width:0;outline:none}.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper{min-width:100%}.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper>dl:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper>ol:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper>table:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper>ul:not([cdkVirtualFor]){padding-top:0;padding-bottom:0;margin-top:0;margin-bottom:0;border-top-width:0;border-bottom-width:0;outline:none}.cdk-virtual-scroll-spacer{height:1px;transform-origin:0 0;flex:0 0 auto}[dir=rtl] .cdk-virtual-scroll-spacer{transform-origin:100% 0}\n"], 6578 encapsulation: 2, 6579 changeDetection: 0 6580})); 6581var CdkVirtualScrollViewport = _CdkVirtualScrollViewport; 6582(() => { 6583 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(CdkVirtualScrollViewport, [{ 6584 type: Component, 6585 args: [{ 6586 selector: "cdk-virtual-scroll-viewport", 6587 host: { 6588 "class": "cdk-virtual-scroll-viewport", 6589 "[class.cdk-virtual-scroll-orientation-horizontal]": 'orientation === "horizontal"', 6590 "[class.cdk-virtual-scroll-orientation-vertical]": 'orientation !== "horizontal"' 6591 }, 6592 encapsulation: ViewEncapsulation.None, 6593 changeDetection: ChangeDetectionStrategy.OnPush, 6594 providers: [{ 6595 provide: CdkScrollable, 6596 useFactory: (virtualScrollable, viewport) => virtualScrollable || viewport, 6597 deps: [[new Optional(), new Inject(VIRTUAL_SCROLLABLE)], CdkVirtualScrollViewport] 6598 }], 6599 template: '<!--\n Wrap the rendered content in an element that will be used to offset it based on the scroll\n position.\n-->\n<div #contentWrapper class="cdk-virtual-scroll-content-wrapper">\n <ng-content></ng-content>\n</div>\n<!--\n Spacer used to force the scrolling container to the correct size for the *total* number of items\n so that the scrollbar captures the size of the entire data set.\n-->\n<div class="cdk-virtual-scroll-spacer"\n [style.width]="_totalContentWidth" [style.height]="_totalContentHeight"></div>\n', 6600 styles: ["cdk-virtual-scroll-viewport{display:block;position:relative;transform:translateZ(0)}.cdk-virtual-scrollable{overflow:auto;will-change:scroll-position;contain:strict}.cdk-virtual-scroll-content-wrapper{position:absolute;top:0;left:0;contain:content}[dir=rtl] .cdk-virtual-scroll-content-wrapper{right:0;left:auto}.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper{min-height:100%}.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper>dl:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper>ol:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper>table:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper>ul:not([cdkVirtualFor]){padding-left:0;padding-right:0;margin-left:0;margin-right:0;border-left-width:0;border-right-width:0;outline:none}.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper{min-width:100%}.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper>dl:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper>ol:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper>table:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper>ul:not([cdkVirtualFor]){padding-top:0;padding-bottom:0;margin-top:0;margin-bottom:0;border-top-width:0;border-bottom-width:0;outline:none}.cdk-virtual-scroll-spacer{height:1px;transform-origin:0 0;flex:0 0 auto}[dir=rtl] .cdk-virtual-scroll-spacer{transform-origin:100% 0}\n"] 6601 }] 6602 }], () => [], { 6603 orientation: [{ 6604 type: Input 6605 }], 6606 appendOnly: [{ 6607 type: Input, 6608 args: [{ 6609 transform: booleanAttribute 6610 }] 6611 }], 6612 scrolledIndexChange: [{ 6613 type: Output 6614 }], 6615 _contentWrapper: [{ 6616 type: ViewChild, 6617 args: ["contentWrapper", { 6618 static: true 6619 }] 6620 }] 6621 }); 6622})(); 6623function getOffset(orientation, direction, node) { 6624 const el = node; 6625 if (!el.getBoundingClientRect) { 6626 return 0; 6627 } 6628 const rect = el.getBoundingClientRect(); 6629 if (orientation === "horizontal") { 6630 return direction === "start" ? rect.left : rect.right; 6631 } 6632 return direction === "start" ? rect.top : rect.bottom; 6633} 6634var _CdkVirtualForOf = class _CdkVirtualForOf { 6635 _viewContainerRef = inject(ViewContainerRef); 6636 _template = inject(TemplateRef); 6637 _differs = inject(IterableDiffers); 6638 _viewRepeater = inject(_VIEW_REPEATER_STRATEGY); 6639 _viewport = inject(CdkVirtualScrollViewport, { 6640 skipSelf: true 6641 }); 6642 /** Emits when the rendered view of the data changes. */ 6643 viewChange = new Subject(); 6644 /** Subject that emits when a new DataSource instance is given. */ 6645 _dataSourceChanges = new Subject(); 6646 /** The DataSource to display. */ 6647 get cdkVirtualForOf() { 6648 return this._cdkVirtualForOf; 6649 } 6650 set cdkVirtualForOf(value) { 6651 this._cdkVirtualForOf = value; 6652 if (isDataSource(value)) { 6653 this._dataSourceChanges.next(value); 6654 } else { 6655 this._dataSourceChanges.next(new ArrayDataSource(isObservable(value) ? value : Array.from(value || []))); 6656 } 6657 } 6658 _cdkVirtualForOf; 6659 /** 6660 * The `TrackByFunction` to use for tracking changes. The `TrackByFunction` takes the index and 6661 * the item and produces a value to be used as the item's identity when tracking changes. 6662 */ 6663 get cdkVirtualForTrackBy() { 6664 return this._cdkVirtualForTrackBy; 6665 } 6666 set cdkVirtualForTrackBy(fn) { 6667 this._needsUpdate = true; 6668 this._cdkVirtualForTrackBy = fn ? (index, item) => fn(index + (this._renderedRange ? this._renderedRange.start : 0), item) : void 0; 6669 } 6670 _cdkVirtualForTrackBy; 6671 /** The template used to stamp out new elements. */ 6672 set cdkVirtualForTemplate(value) { 6673 if (value) { 6674 this._needsUpdate = true; 6675 this._template = value; 6676 } 6677 } 6678 /** 6679 * The size of the cache used to store templates that are not being used for re-use later. 6680 * Setting the cache size to `0` will disable caching. Defaults to 20 templates. 6681 */ 6682 get cdkVirtualForTemplateCacheSize() { 6683 return this._viewRepeater.viewCacheSize; 6684 } 6685 set cdkVirtualForTemplateCacheSize(size) { 6686 this._viewRepeater.viewCacheSize = coerceNumberProperty(size); 6687 } 6688 /** Emits whenever the data in the current DataSource changes. */ 6689 dataStream = this._dataSourceChanges.pipe( 6690 // Start off with null `DataSource`. 6691 startWith(null), 6692 // Bundle up the previous and current data sources so we can work with both. 6693 pairwise(), 6694 // Use `_changeDataSource` to disconnect from the previous data source and connect to the 6695 // new one, passing back a stream of data changes which we run through `switchMap` to give 6696 // us a data stream that emits the latest data from whatever the current `DataSource` is. 6697 switchMap(([prev, cur]) => this._changeDataSource(prev, cur)), 6698 // Replay the last emitted data when someone subscribes. 6699 shareReplay(1) 6700 ); 6701 /** The differ used to calculate changes to the data. */ 6702 _differ = null; 6703 /** The most recent data emitted from the DataSource. */ 6704 _data; 6705 /** The currently rendered items. */ 6706 _renderedItems; 6707 /** The currently rendered range of indices. */ 6708 _renderedRange; 6709 /** Whether the rendered data should be updated during the next ngDoCheck cycle. */ 6710 _needsUpdate = false; 6711 _destroyed = new Subject(); 6712 constructor() { 6713 const ngZone = inject(NgZone); 6714 this.dataStream.subscribe((data) => { 6715 this._data = data; 6716 this._onRenderedDataChange(); 6717 }); 6718 this._viewport.renderedRangeStream.pipe(takeUntil(this._destroyed)).subscribe((range) => { 6719 this._renderedRange = range; 6720 if (this.viewChange.observers.length) { 6721 ngZone.run(() => this.viewChange.next(this._renderedRange)); 6722 } 6723 this._onRenderedDataChange(); 6724 }); 6725 this._viewport.attach(this); 6726 } 6727 /** 6728 * Measures the combined size (width for horizontal orientation, height for vertical) of all items 6729 * in the specified range. Throws an error if the range includes items that are not currently 6730 * rendered. 6731 */ 6732 measureRangeSize(range, orientation) { 6733 if (range.start >= range.end) { 6734 return 0; 6735 } 6736 if ((range.start < this._renderedRange.start || range.end > this._renderedRange.end) && (typeof ngDevMode === "undefined" || ngDevMode)) { 6737 throw Error(`Error: attempted to measure an item that isn't rendered.`); 6738 } 6739 const renderedStartIndex = range.start - this._renderedRange.start; 6740 const rangeLen = range.end - range.start; 6741 let firstNode; 6742 let lastNode; 6743 for (let i = 0; i < rangeLen; i++) { 6744 const view = this._viewContainerRef.get(i + renderedStartIndex); 6745 if (view && view.rootNodes.length) { 6746 firstNode = lastNode = view.rootNodes[0]; 6747 break; 6748 } 6749 } 6750 for (let i = rangeLen - 1; i > -1; i--) { 6751 const view = this._viewContainerRef.get(i + renderedStartIndex); 6752 if (view && view.rootNodes.length) { 6753 lastNode = view.rootNodes[view.rootNodes.length - 1]; 6754 break; 6755 } 6756 } 6757 return firstNode && lastNode ? getOffset(orientation, "end", lastNode) - getOffset(orientation, "start", firstNode) : 0; 6758 } 6759 ngDoCheck() { 6760 if (this._differ && this._needsUpdate) { 6761 const changes = this._differ.diff(this._renderedItems); 6762 if (!changes) { 6763 this._updateContext(); 6764 } else { 6765 this._applyChanges(changes); 6766 } 6767 this._needsUpdate = false; 6768 } 6769 } 6770 ngOnDestroy() { 6771 this._viewport.detach(); 6772 this._dataSourceChanges.next(void 0); 6773 this._dataSourceChanges.complete(); 6774 this.viewChange.complete(); 6775 this._destroyed.next(); 6776 this._destroyed.complete(); 6777 this._viewRepeater.detach(); 6778 } 6779 /** React to scroll state changes in the viewport. */ 6780 _onRenderedDataChange() { 6781 if (!this._renderedRange) { 6782 return; 6783 } 6784 this._renderedItems = this._data.slice(this._renderedRange.start, this._renderedRange.end); 6785 if (!this._differ) { 6786 this._differ = this._differs.find(this._renderedItems).create((index, item) => { 6787 return this.cdkVirtualForTrackBy ? this.cdkVirtualForTrackBy(index, item) : item; 6788 }); 6789 } 6790 this._needsUpdate = true; 6791 } 6792 /** Swap out one `DataSource` for another. */ 6793 _changeDataSource(oldDs, newDs) { 6794 if (oldDs) { 6795 oldDs.disconnect(this); 6796 } 6797 this._needsUpdate = true; 6798 return newDs ? newDs.connect(this) : of(); 6799 } 6800 /** Update the `CdkVirtualForOfContext` for all views. */ 6801 _updateContext() { 6802 const count = this._data.length; 6803 let i = this._viewContainerRef.length; 6804 while (i--) { 6805 const view = this._viewContainerRef.get(i); 6806 view.context.index = this._renderedRange.start + i; 6807 view.context.count = count; 6808 this._updateComputedContextProperties(view.context); 6809 view.detectChanges(); 6810 } 6811 } 6812 /** Apply changes to the DOM. */ 6813 _applyChanges(changes) { 6814 this._viewRepeater.applyChanges(changes, this._viewContainerRef, (record, _adjustedPreviousIndex, currentIndex) => this._getEmbeddedViewArgs(record, currentIndex), (record) => record.item); 6815 changes.forEachIdentityChange((record) => { 6816 const view = this._viewContainerRef.get(record.currentIndex); 6817 view.context.$implicit = record.item; 6818 }); 6819 const count = this._data.length; 6820 let i = this._viewContainerRef.length; 6821 while (i--) { 6822 const view = this._viewContainerRef.get(i); 6823 view.context.index = this._renderedRange.start + i; 6824 view.context.count = count; 6825 this._updateComputedContextProperties(view.context); 6826 } 6827 } 6828 /** Update the computed properties on the `CdkVirtualForOfContext`. */ 6829 _updateComputedContextProperties(context) { 6830 context.first = context.index === 0; 6831 context.last = context.index === context.count - 1; 6832 context.even = context.index % 2 === 0; 6833 context.odd = !context.even; 6834 } 6835 _getEmbeddedViewArgs(record, index) { 6836 return { 6837 templateRef: this._template, 6838 context: { 6839 $implicit: record.item, 6840 // It's guaranteed that the iterable is not "undefined" or "null" because we only 6841 // generate views for elements if the "cdkVirtualForOf" iterable has elements. 6842 cdkVirtualForOf: this._cdkVirtualForOf, 6843 index: -1, 6844 count: -1, 6845 first: false, 6846 last: false, 6847 odd: false, 6848 even: false 6849 }, 6850 index 6851 }; 6852 } 6853 static ngTemplateContextGuard(directive, context) { 6854 return true; 6855 } 6856}; 6857__publicField(_CdkVirtualForOf, "\u0275fac", function CdkVirtualForOf_Factory(__ngFactoryType__) { 6858 return new (__ngFactoryType__ || _CdkVirtualForOf)(); 6859}); 6860__publicField(_CdkVirtualForOf, "\u0275dir", /* @__PURE__ */ \u0275\u0275defineDirective({ 6861 type: _CdkVirtualForOf, 6862 selectors: [["", "cdkVirtualFor", "", "cdkVirtualForOf", ""]], 6863 inputs: { 6864 cdkVirtualForOf: "cdkVirtualForOf", 6865 cdkVirtualForTrackBy: "cdkVirtualForTrackBy", 6866 cdkVirtualForTemplate: "cdkVirtualForTemplate", 6867 cdkVirtualForTemplateCacheSize: "cdkVirtualForTemplateCacheSize" 6868 }, 6869 features: [\u0275\u0275ProvidersFeature([{ 6870 provide: _VIEW_REPEATER_STRATEGY, 6871 useClass: _RecycleViewRepeaterStrategy 6872 }])] 6873})); 6874var CdkVirtualForOf = _CdkVirtualForOf; 6875(() => { 6876 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(CdkVirtualForOf, [{ 6877 type: Directive, 6878 args: [{ 6879 selector: "[cdkVirtualFor][cdkVirtualForOf]", 6880 providers: [{ 6881 provide: _VIEW_REPEATER_STRATEGY, 6882 useClass: _RecycleViewRepeaterStrategy 6883 }] 6884 }] 6885 }], () => [], { 6886 cdkVirtualForOf: [{ 6887 type: Input 6888 }], 6889 cdkVirtualForTrackBy: [{ 6890 type: Input 6891 }], 6892 cdkVirtualForTemplate: [{ 6893 type: Input 6894 }], 6895 cdkVirtualForTemplateCacheSize: [{ 6896 type: Input 6897 }] 6898 }); 6899})(); 6900var _CdkVirtualScrollableElement = class _CdkVirtualScrollableElement extends CdkVirtualScrollable { 6901 constructor() { 6902 super(); 6903 } 6904 measureBoundingClientRectWithScrollOffset(from) { 6905 return this.getElementRef().nativeElement.getBoundingClientRect()[from] - this.measureScrollOffset(from); 6906 } 6907}; 6908__publicField(_CdkVirtualScrollableElement, "\u0275fac", function CdkVirtualScrollableElement_Factory(__ngFactoryType__) { 6909 return new (__ngFactoryType__ || _CdkVirtualScrollableElement)(); 6910}); 6911__publicField(_CdkVirtualScrollableElement, "\u0275dir", /* @__PURE__ */ \u0275\u0275defineDirective({ 6912 type: _CdkVirtualScrollableElement, 6913 selectors: [["", "cdkVirtualScrollingElement", ""]], 6914 hostAttrs: [1, "cdk-virtual-scrollable"], 6915 features: [\u0275\u0275ProvidersFeature([{ 6916 provide: VIRTUAL_SCROLLABLE, 6917 useExisting: _CdkVirtualScrollableElement 6918 }]), \u0275\u0275InheritDefinitionFeature] 6919})); 6920var CdkVirtualScrollableElement = _CdkVirtualScrollableElement; 6921(() => { 6922 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(CdkVirtualScrollableElement, [{ 6923 type: Directive, 6924 args: [{ 6925 selector: "[cdkVirtualScrollingElement]", 6926 providers: [{ 6927 provide: VIRTUAL_SCROLLABLE, 6928 useExisting: CdkVirtualScrollableElement 6929 }], 6930 host: { 6931 "class": "cdk-virtual-scrollable" 6932 } 6933 }] 6934 }], () => [], null); 6935})(); 6936var _CdkVirtualScrollableWindow = class _CdkVirtualScrollableWindow extends CdkVirtualScrollable { 6937 constructor() { 6938 super(); 6939 const document2 = inject(DOCUMENT); 6940 this.elementRef = new ElementRef(document2.documentElement); 6941 this._scrollElement = document2; 6942 } 6943 measureBoundingClientRectWithScrollOffset(from) { 6944 return this.getElementRef().nativeElement.getBoundingClientRect()[from]; 6945 } 6946}; 6947__publicField(_CdkVirtualScrollableWindow, "\u0275fac", function CdkVirtualScrollableWindow_Factory(__ngFactoryType__) { 6948 return new (__ngFactoryType__ || _CdkVirtualScrollableWindow)(); 6949}); 6950__publicField(_CdkVirtualScrollableWindow, "\u0275dir", /* @__PURE__ */ \u0275\u0275defineDirective({ 6951 type: _CdkVirtualScrollableWindow, 6952 selectors: [["cdk-virtual-scroll-viewport", "scrollWindow", ""]], 6953 features: [\u0275\u0275ProvidersFeature([{ 6954 provide: VIRTUAL_SCROLLABLE, 6955 useExisting: _CdkVirtualScrollableWindow 6956 }]), \u0275\u0275InheritDefinitionFeature] 6957})); 6958var CdkVirtualScrollableWindow = _CdkVirtualScrollableWindow; 6959(() => { 6960 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(CdkVirtualScrollableWindow, [{ 6961 type: Directive, 6962 args: [{ 6963 selector: "cdk-virtual-scroll-viewport[scrollWindow]", 6964 providers: [{ 6965 provide: VIRTUAL_SCROLLABLE, 6966 useExisting: CdkVirtualScrollableWindow 6967 }] 6968 }] 6969 }], () => [], null); 6970})(); 6971var _CdkScrollableModule = class _CdkScrollableModule { 6972}; 6973__publicField(_CdkScrollableModule, "\u0275fac", function CdkScrollableModule_Factory(__ngFactoryType__) { 6974 return new (__ngFactoryType__ || _CdkScrollableModule)(); 6975}); 6976__publicField(_CdkScrollableModule, "\u0275mod", /* @__PURE__ */ \u0275\u0275defineNgModule({ 6977 type: _CdkScrollableModule 6978})); 6979__publicField(_CdkScrollableModule, "\u0275inj", /* @__PURE__ */ \u0275\u0275defineInjector({})); 6980var CdkScrollableModule = _CdkScrollableModule; 6981(() => { 6982 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(CdkScrollableModule, [{ 6983 type: NgModule, 6984 args: [{ 6985 exports: [CdkScrollable], 6986 imports: [CdkScrollable] 6987 }] 6988 }], null, null); 6989})(); 6990var _ScrollingModule = class _ScrollingModule { 6991}; 6992__publicField(_ScrollingModule, "\u0275fac", function ScrollingModule_Factory(__ngFactoryType__) { 6993 return new (__ngFactoryType__ || _ScrollingModule)(); 6994}); 6995__publicField(_ScrollingModule, "\u0275mod", /* @__PURE__ */ \u0275\u0275defineNgModule({ 6996 type: _ScrollingModule 6997})); 6998__publicField(_ScrollingModule, "\u0275inj", /* @__PURE__ */ \u0275\u0275defineInjector({ 6999 imports: [BidiModule, CdkScrollableModule, BidiModule, CdkScrollableModule] 7000})); 7001var ScrollingModule = _ScrollingModule; 7002(() => { 7003 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(ScrollingModule, [{ 7004 type: NgModule, 7005 args: [{ 7006 imports: [BidiModule, CdkScrollableModule, CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollableWindow, CdkVirtualScrollableElement], 7007 exports: [BidiModule, CdkScrollableModule, CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport, CdkVirtualScrollableWindow, CdkVirtualScrollableElement] 7008 }] 7009 }], null, null); 7010})(); 7011
7012// src/app/pages/campus-map-v3/components/entity-tree-panel/entity-tree-panel.component.ts 7013function EntityTreePanelComponent_div_7_Conditional_1_Template(rf, ctx) { 7014 if (rf & 1) { 7015 const _r4 = \u0275\u0275getCurrentView(); 7016 \u0275\u0275elementStart(0, "button", 14); 7017 \u0275\u0275listener("click", function EntityTreePanelComponent_div_7_Conditional_1_Template_button_click_0_listener($event) { 7018 \u0275\u0275restoreView(_r4); 7019 const item_r2 = \u0275\u0275nextContext().$implicit; 7020 const ctx_r2 = \u0275\u0275nextContext(); 7021 return \u0275\u0275resetView(ctx_r2.onExpandToggle(item_r2, $event)); 7022 }); 7023 \u0275\u0275element(1, "ng-icon", 15); 7024 \u0275\u0275elementEnd(); 7025 } 7026 if (rf & 2) { 7027 const item_r2 = \u0275\u0275nextContext().$implicit; 7028 const ctx_r2 = \u0275\u0275nextContext(); 7029 \u0275\u0275property("title", item_r2.isExpanded ? "Collapse" : "Expand"); 7030 \u0275\u0275advance(); 7031 \u0275\u0275property("name", ctx_r2.getChevronIcon(item_r2)); 7032 } 7033} 7034function EntityTreePanelComponent_div_7_Conditional_2_Template(rf, ctx) { 7035 if (rf & 1) { 7036 \u0275\u0275element(0, "span", 9); 7037 } 7038} 7039function EntityTreePanelComponent_div_7_Conditional_6_Template(rf, ctx) { 7040 if (rf & 1) { 7041 \u0275\u0275elementStart(0, "span", 12); 7042 \u0275\u0275text(1); 7043 \u0275\u0275elementEnd(); 7044 } 7045 if (rf & 2) { 7046 const item_r2 = \u0275\u0275nextContext().$implicit; 7047 \u0275\u0275advance(); 7048 \u0275\u0275textInterpolate(item_r2.childCount); 7049 } 7050} 7051function EntityTreePanelComponent_div_7_Conditional_7_Template(rf, ctx) { 7052 if (rf & 1) { 7053 const _r5 = \u0275\u0275getCurrentView(); 7054 \u0275\u0275elementStart(0, "button", 16); 7055 \u0275\u0275listener("click", function EntityTreePanelComponent_div_7_Conditional_7_Template_button_click_0_listener($event) { 7056 \u0275\u0275restoreView(_r5); 7057 const item_r2 = \u0275\u0275nextContext().$implicit; 7058 const ctx_r2 = \u0275\u0275nextContext(); 7059 return \u0275\u0275resetView(ctx_r2.onMoreClick(item_r2, $event)); 7060 }); 7061 \u0275\u0275element(1, "ng-icon", 17); 7062 \u0275\u0275elementEnd(); 7063 } 7064} 7065function EntityTreePanelComponent_div_7_Template(rf, ctx) { 7066 if (rf & 1) { 7067 const _r1 = \u0275\u0275getCurrentView(); 7068 \u0275\u0275elementStart(0, "div", 7); 7069 \u0275\u0275listener("click", function EntityTreePanelComponent_div_7_Template_div_click_0_listener() { 7070 const item_r2 = \u0275\u0275restoreView(_r1).$implicit; 7071 const ctx_r2 = \u0275\u0275nextContext(); 7072 return \u0275\u0275resetView(ctx_r2.isClickable(item_r2) && ctx_r2.onNodeClick(item_r2)); 7073 })("contextmenu", function EntityTreePanelComponent_div_7_Template_div_contextmenu_0_listener($event) { 7074 const item_r2 = \u0275\u0275restoreView(_r1).$implicit; 7075 const ctx_r2 = \u0275\u0275nextContext(); 7076 return \u0275\u0275resetView(ctx_r2.hasContextMenu(item_r2) && ctx_r2.onContextMenu(item_r2, $event)); 7077 }); 7078 \u0275\u0275template(1, EntityTreePanelComponent_div_7_Conditional_1_Template, 2, 2, "button", 8)(2, EntityTreePanelComponent_div_7_Conditional_2_Template, 1, 0, "span", 9); 7079 \u0275\u0275element(3, "ng-icon", 10); 7080 \u0275\u0275elementStart(4, "span", 11); 7081 \u0275\u0275text(5); 7082 \u0275\u0275elementEnd(); 7083 \u0275\u0275template(6, EntityTreePanelComponent_div_7_Conditional_6_Template, 2, 1, "span", 12)(7, EntityTreePanelComponent_div_7_Conditional_7_Template, 2, 0, "button", 13); 7084 \u0275\u0275elementEnd(); 7085 } 7086 if (rf & 2) { 7087 const item_r2 = ctx.$implicit; 7088 const ctx_r2 = \u0275\u0275nextContext(); 7089 \u0275\u0275styleProp("padding-left", ctx_r2.getIndentPx(item_r2.depth) + 8, "px"); 7090 \u0275\u0275classProp("selected", item_r2.isSelected)("clickable", ctx_r2.isClickable(item_r2)); 7091 \u0275\u0275advance(); 7092 \u0275\u0275conditional(item_r2.hasChildren ? 1 : 2); 7093 \u0275\u0275advance(2); 7094 \u0275\u0275property("name", item_r2.icon); 7095 \u0275\u0275advance(); 7096 \u0275\u0275property("title", item_r2.name); 7097 \u0275\u0275advance(); 7098 \u0275\u0275textInterpolate(item_r2.name); 7099 \u0275\u0275advance(); 7100 \u0275\u0275conditional(item_r2.childCount && item_r2.childCount > 0 ? 6 : -1); 7101 \u0275\u0275advance();
7102 \u0275\u0275conditional(ctx_r2.hasContextMenu(item_r2) ? 7 : -1); 7103 } 7104} 7105function EntityTreePanelComponent_Conditional_8_Template(rf, ctx) { 7106 if (rf & 1) { 7107 \u0275\u0275elementStart(0, "div", 6); 7108 \u0275\u0275element(1, "ng-icon", 18); 7109 \u0275\u0275elementStart(2, "p"); 7110 \u0275\u0275text(3, "No entities found"); 7111 \u0275\u0275elementEnd()(); 7112 } 7113} 7114var _EntityTreePanelComponent = class _EntityTreePanelComponent { 7115 constructor() { 7116 this.elementRef = inject(ElementRef); 7117 this.treeState = inject(TreeStateService); 7118 this.nodeClicked = new EventEmitter(); 7119 this.nodeContextMenu = new EventEmitter(); 7120 this.addBuildingRequested = new EventEmitter(); 7121 this.trackById = (_index, item) => item.id; 7122 } 7123 ngAfterViewInit() { 7124 this.resizeObserver = new ResizeObserver(() => { 7125 var _a3; 7126 (_a3 = this.viewport) == null ? void 0 : _a3.checkViewportSize(); 7127 }); 7128 this.resizeObserver.observe(this.elementRef.nativeElement); 7129 } 7130 ngOnDestroy() { 7131 var _a3; 7132 (_a3 = this.resizeObserver) == null ? void 0 : _a3.disconnect(); 7133 } 7134 onExpandToggle(item, event) { 7135 event.stopPropagation(); 7136 this.treeState.toggle(item.id); 7137 } 7138 onNodeClick(item) { 7139 this.nodeClicked.emit(item); 7140 } 7141 onContextMenu(item, event) { 7142 event.preventDefault(); 7143 event.stopPropagation(); 7144 this.nodeContextMenu.emit({ item, event }); 7145 } 7146 onMoreClick(item, event) { 7147 event.stopPropagation(); 7148 this.nodeContextMenu.emit({ item, event }); 7149 } 7150 onAddBuilding() { 7151 this.addBuildingRequested.emit(); 7152 } 7153 getIndentPx(depth) { 7154 return depth * 20; 7155 } 7156 getChevronIcon(item) { 7157 return item.isExpanded ? "lucideChevronDown" : "lucideChevronRight"; 7158 } 7159 isClickable(item) { 7160 return item.type !== "campus" && item.type !== "outdoorNodes"; 7161 } 7162 hasContextMenu(item) { 7163 return item.type !== "campus" && item.type !== "outdoorNodes"; 7164 } 7165}; 7166_EntityTreePanelComponent.\u0275fac = function EntityTreePanelComponent_Factory(__ngFactoryType__) { 7167 return new (__ngFactoryType__ || _EntityTreePanelComponent)(); 7168}; 7169_EntityTreePanelComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _EntityTreePanelComponent, selectors: [["app-entity-tree-panel"]], viewQuery: function EntityTreePanelComponent_Query(rf, ctx) { 7170 if (rf & 1) { 7171 \u0275\u0275viewQuery(CdkVirtualScrollViewport, 5); 7172 } 7173 if (rf & 2) { 7174 let _t; 7175 \u0275\u0275queryRefresh(_t = \u0275\u0275loadQuery()) && (ctx.viewport = _t.first); 7176 } 7177}, outputs: { nodeClicked: "nodeClicked", nodeContextMenu: "nodeContextMenu", addBuildingRequested: "addBuildingRequested" }, features: [\u0275\u0275ProvidersFeature([], [ 7178 provideIcons({ 7179 lucideChevronRight, 7180 lucideChevronDown, 7181 lucideMapPin, 7182 lucideBuilding, 7183 lucideLayers, 7184 lucideDoorOpen, 7185 lucideCircle, 7186 lucideTrees, 7187 lucideEllipsisVertical, 7188 lucideFolderTree, 7189 lucidePlus 7190 }) 7191])], decls: 9, vars: 3, consts: [[1, "entity-tree-panel"], [1, "add-section"], [1, "add-button", 3, "click"], ["name", "lucidePlus", "size", "14"], ["itemSize", "36", "minBufferPx", "200", "maxBufferPx", "400", 1, "tree-viewport"], ["class", "tree-node", 3, "selected", "clickable", "padding-left", "click", "contextmenu", 4, "cdkVirtualFor", "cdkVirtualForOf", "cdkVirtualForTrackBy"], [1, "empty-state"], [1, "tree-node", 3, "click", "contextmenu"], [1, "expand-button", 3, "title"], [1, "expand-placeholder"], ["size", "16", 1, "node-icon", 3, "name"], [1, "node-name", 3, "title"], [1, "child-count"], ["title", "Actions", 1, "more-button"], [1, "expand-button", 3, "click", "title"], ["size", "14", 3, "name"], ["title", "Actions", 1, "more-button", 3, "click"], ["name", "lucideEllipsisVertical", "size", "14"], ["name", "lucideFolderTree", "size", "32"]], template: function EntityTreePanelComponent_Template(rf, ctx) { 7192 if (rf & 1) { 7193 \u0275\u0275elementStart(0, "div", 0)(1, "div", 1)(2, "button", 2); 7194 \u0275\u0275listener("click", function EntityTreePanelComponent_Template_button_click_2_listener() { 7195 return ctx.onAddBuilding(); 7196 }); 7197 \u0275\u0275element(3, "ng-icon", 3); 7198 \u0275\u0275elementStart(4, "span"); 7199 \u0275\u0275text(5, "Add Building"); 7200 \u0275\u0275elementEnd()()(); 7201 \u0275\u0275elementStart(6, "cdk-virtual-scroll-viewport", 4); 7202 \u0275\u0275template(7, EntityTreePanelComponent_div_7_Template, 8, 12, "div", 5); 7203 \u0275\u0275elementEnd(); 7204 \u0275\u0275template(8, EntityTreePanelComponent_Conditional_8_Template, 4, 0, "div", 6); 7205 \u0275\u0275elementEnd(); 7206 } 7207 if (rf & 2) { 7208 \u0275\u0275advance(7); 7209 \u0275\u0275property("cdkVirtualForOf", ctx.treeState.flattenedItems())("cdkVirtualForTrackBy", ctx.trackById); 7210 \u0275\u0275advance(); 7211 \u0275\u0275conditional(ctx.treeState.itemCount() === 0 ? 8 : -1); 7212 } 7213}, dependencies: [CommonModule, ScrollingModule, CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport, NgIcon], styles: ["\n\n.entity-tree-panel[_ngcontent-%COMP%] {\n background: var(--tool-palette-bg, rgba(255, 255, 255, 0.95));\n border-radius: 12px;\n box-shadow: var(--tool-palette-shadow, 0 4px 12px rgba(0, 0, 0, 0.15));\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light, rgba(0, 0, 0, 0.08));\n width: 280px;\n max-height: 70vh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n.add-section[_ngcontent-%COMP%] {\n padding: 8px 12px;\n border-bottom: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n flex-shrink: 0;\n}\n.add-button[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 8px 12px;\n border: 1px solid var(--ion-color-primary);\n border-radius: 6px;\n background: var(--ion-color-primary);\n color: var(--ion-color-primary-contrast);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.2s ease;\n}\n.add-button[_ngcontent-%COMP%]:hover {\n background: var(--ion-color-primary-shade);\n}\n.tree-viewport[_ngcontent-%COMP%] {\n flex: 1;\n min-height: 200px;\n max-height: 50vh;\n}\n.tree-node[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 4px;\n height: 36px;\n padding-right: 8px;\n cursor: default;\n -webkit-user-select: none;\n user-select: none;\n transition: background-color 0.15s ease;\n}\n.tree-node.clickable[_ngcontent-%COMP%] {\n cursor: pointer;\n}\n.tree-node.clickable[_ngcontent-%COMP%]:hover {\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n}\n.tree-node.selected[_ngcontent-%COMP%] {\n background: rgba(var(--ion-color-primary-rgb), 0.15);\n}
7213\n.tree-node.selected[_ngcontent-%COMP%] .node-name[_ngcontent-%COMP%] {\n color: var(--ion-color-primary);\n font-weight: 500;\n}\n.tree-node.selected[_ngcontent-%COMP%] .node-icon[_ngcontent-%COMP%] {\n color: var(--ion-color-primary);\n}\n.expand-button[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n padding: 0;\n border: none;\n background: transparent;\n border-radius: 4px;\n cursor: pointer;\n color: var(--ion-color-medium);\n transition: all 0.15s ease;\n flex-shrink: 0;\n}\n.expand-button[_ngcontent-%COMP%]:hover {\n background: rgba(0, 0, 0, 0.08);\n color: var(--ion-text-color);\n}\n.expand-placeholder[_ngcontent-%COMP%] {\n width: 20px;\n flex-shrink: 0;\n}\n.node-icon[_ngcontent-%COMP%] {\n flex-shrink: 0;\n color: var(--ion-color-medium);\n}\n.node-name[_ngcontent-%COMP%] {\n flex: 1;\n font-size: 13px;\n color: var(--ion-text-color);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.child-count[_ngcontent-%COMP%] {\n flex-shrink: 0;\n padding: 2px 6px;\n font-size: 10px;\n font-weight: 600;\n color: var(--ion-color-medium);\n background: rgba(0, 0, 0, 0.06);\n border-radius: 10px;\n}\n.more-button[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n padding: 0;\n border: none;\n background: transparent;\n border-radius: 4px;\n cursor: pointer;\n color: var(--ion-color-medium);\n opacity: 0;\n transition: all 0.15s ease;\n flex-shrink: 0;\n}\n.tree-node[_ngcontent-%COMP%]:hover .more-button[_ngcontent-%COMP%] {\n opacity: 1;\n}\n.more-button[_ngcontent-%COMP%]:hover {\n background: rgba(0, 0, 0, 0.08);\n color: var(--ion-text-color);\n}\n.empty-state[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 32px 16px;\n color: var(--ion-color-medium);\n text-align: center;\n}\n.empty-state[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n opacity: 0.5;\n margin-bottom: 8px;\n}\n.empty-state[_ngcontent-%COMP%] p[_ngcontent-%COMP%] {\n margin: 0;\n font-size: 13px;\n}"], changeDetection: 0 }); 7214var EntityTreePanelComponent = _EntityTreePanelComponent; 7215(() => { 7216 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(EntityTreePanelComponent, [{ 7217 type: Component, 7218 args: [{ selector: "app-entity-tree-panel", standalone: true, imports: [CommonModule, ScrollingModule, NgIcon], viewProviders: [ 7219 provideIcons({ 7220 lucideChevronRight, 7221 lucideChevronDown, 7222 lucideMapPin, 7223 lucideBuilding, 7224 lucideLayers, 7225 lucideDoorOpen, 7226 lucideCircle, 7227 lucideTrees, 7228 lucideEllipsisVertical, 7229 lucideFolderTree, 7230 lucidePlus 7231 }) 7232 ], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="entity-tree-panel"> 7233 <!-- Add Building Button --> 7234 <div class="add-section"> 7235 <button class="add-button" (click)="onAddBuilding()"> 7236 <ng-icon name="lucidePlus" size="14" /> 7237 <span>Add Building</span> 7238 </button> 7239 </div> 7240 7241 <!-- Tree Viewport with CDK Virtual Scroll --> 7242 <cdk-virtual-scroll-viewport 7243 itemSize="36" 7244 minBufferPx="200" 7245 maxBufferPx="400" 7246 class="tree-viewport"> 7247 <div 7248 *cdkVirtualFor="let item of treeState.flattenedItems(); trackBy: trackById" 7249 class="tree-node" 7250 [class.selected]="item.isSelected" 7251 [class.clickable]="isClickable(item)" 7252 [style.padding-left.px]="getIndentPx(item.depth) + 8" 7253 (click)="isClickable(item) && onNodeClick(item)" 7254 (contextmenu)="hasContextMenu(item) && onContextMenu(item, $event)"> 7255 7256 <!-- Expand/Collapse Chevron --> 7257 @if (item.hasChildren) { 7258 <button 7259 class="expand-button" 7260 (click)="onExpandToggle(item, $event)" 7261 [title]="item.isExpanded ? 'Collapse' : 'Expand'"> 7262 <ng-icon [name]="getChevronIcon(item)" size="14" /> 7263 </button> 7264 } @else { 7265 <span class="expand-placeholder"></span> 7266 } 7267 7268 <!-- Entity Icon --> 7269 <ng-icon [name]="item.icon" size="16" class="node-icon" /> 7270 7271 <!-- Name -->
7272 <span class="node-name" [title]="item.name">{{ item.name }}</span> 7273 7274 <!-- Child Count Badge --> 7275 @if (item.childCount && item.childCount > 0) { 7276 <span class="child-count">{{ item.childCount }}</span> 7277 } 7278 7279 <!-- Context Menu Button --> 7280 @if (hasContextMenu(item)) { 7281 <button 7282 class="more-button" 7283 (click)="onMoreClick(item, $event)" 7284 title="Actions"> 7285 <ng-icon name="lucideEllipsisVertical" size="14" /> 7286 </button> 7287 } 7288 </div> 7289 </cdk-virtual-scroll-viewport> 7290 7291 <!-- Empty State --> 7292 @if (treeState.itemCount() === 0) { 7293 <div class="empty-state"> 7294 <ng-icon name="lucideFolderTree" size="32" /> 7295 <p>No entities found</p> 7296 </div> 7297 } 7298</div> 7299`, styles: ["/* src/app/pages/campus-map-v3/components/entity-tree-panel/entity-tree-panel.component.scss */\n.entity-tree-panel {\n background: var(--tool-palette-bg, rgba(255, 255, 255, 0.95));\n border-radius: 12px;\n box-shadow: var(--tool-palette-shadow, 0 4px 12px rgba(0, 0, 0, 0.15));\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light, rgba(0, 0, 0, 0.08));\n width: 280px;\n max-height: 70vh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n.add-section {\n padding: 8px 12px;\n border-bottom: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n flex-shrink: 0;\n}\n.add-button {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 8px 12px;\n border: 1px solid var(--ion-color-primary);\n border-radius: 6px;\n background: var(--ion-color-primary);\n color: var(--ion-color-primary-contrast);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.2s ease;\n}\n.add-button:hover {\n background: var(--ion-color-primary-shade);\n}\n.tree-viewport {\n flex: 1;\n min-height: 200px;\n max-height: 50vh;\n}\n.tree-node {\n display: flex;\n align-items: center;\n gap: 4px;\n height: 36px;\n padding-right: 8px;\n cursor: default;\n -webkit-user-select: none;\n user-select: none;\n transition: background-color 0.15s ease;\n}\n.tree-node.clickable {\n cursor: pointer;\n}\n.tree-node.clickable:hover {\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n}\n.tree-node.selected {\n background: rgba(var(--ion-color-primary-rgb), 0.15);\n}\n.tree-node.selected .node-name {\n color: var(--ion-color-primary);\n font-weight: 500;\n}\n.tree-node.selected .node-icon {\n color: var(--ion-color-primary);\n}\n.expand-button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n padding: 0;\n border: none;\n background: transparent;\n border-radius: 4px;\n cursor: pointer;\n color: var(--ion-color-medium);\n transition: all 0.15s ease;\n flex-shrink: 0;\n}\n.expand-button:hover {\n background: rgba(0, 0, 0, 0.08);\n color: var(--ion-text-color);\n}\n.expand-placeholder {\n width: 20px;\n flex-shrink: 0;\n}\n.node-icon {\n flex-shrink: 0;\n color: var(--ion-color-medium);\n}\n.node-name {\n flex: 1;\n font-size: 13px;\n color: var(--ion-text-color);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.child-count {\n flex-shrink: 0;\n padding: 2px 6px;\n font-size: 10px;\n font-weight: 600;\n color: var(--ion-color-medium);\n background: rgba(0, 0, 0, 0.06);\n border-radius: 10px;\n}\n.more-button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n padding: 0;\n border: none;\n background: transparent;\n border-radius: 4px;\n cursor: pointer;\n color: var(--ion-color-medium);\n opacity: 0;\n transition: all 0.15s ease;\n flex-shrink: 0;\n}\n.tree-node:hover .more-button {\n opacity: 1;\n}\n.more-button:hover {\n background: rgba(0, 0, 0, 0.08);\n color: var(--ion-text-color);\n}\n.empty-state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 32px 16px;\n color: var(--ion-color-medium);\n text-align: center;\n}\n.empty-state ng-icon {\n opacity: 0.5;\n margin-bottom: 8px;\n}\n.empty-state p {\n margin: 0;\n font-size: 13px;\n}\n"] }] 7300 }], null, { viewport: [{ 7301 type: ViewChild, 7302 args: [CdkVirtualScrollViewport] 7303 }], nodeClicked: [{ 7304 type: Output 7305 }], nodeContextMenu: [{ 7306 type: Output 7307 }], addBuildingRequested: [{ 7308 type: Output 7309 }] }); 7310})(); 7311(() => { 7312 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(EntityTreePanelComponent, { className: "EntityTreePanelComponent", filePath: "src/app/pages/campus-map-v3/components/entity-tree-panel/entity-tree-panel.component.ts", lineNumber: 54 }); 7313})(); 7314
7315// src/app/pages/campus-map-v3/components/entity-sidebar/entity-sidebar.component.ts 7316function EntitySidebarComponent_Conditional_0_Template(rf, ctx) { 7317 if (rf & 1) { 7318 const _r1 = \u0275\u0275getCurrentView(); 7319 \u0275\u0275elementStart(0, "div", 4); 7320 \u0275\u0275listener("click", function EntitySidebarComponent_Conditional_0_Template_div_click_0_listener() { 7321 \u0275\u0275restoreView(_r1); 7322 const ctx_r1 = \u0275\u0275nextContext(); 7323 return \u0275\u0275resetView(ctx_r1.onBackdropClick()); 7324 }); 7325 \u0275\u0275elementEnd(); 7326 } 7327} 7328function EntitySidebarComponent_Conditional_2_Conditional_13_Template(rf, ctx) { 7329 if (rf & 1) { 7330 const _r4 = \u0275\u0275getCurrentView(); 7331 \u0275\u0275elementStart(0, "app-tool-button", 19); 7332 \u0275\u0275listener("clicked", function EntitySidebarComponent_Conditional_2_Conditional_13_Template_app_tool_button_clicked_0_listener() { 7333 \u0275\u0275restoreView(_r4); 7334 const ctx_r1 = \u0275\u0275nextContext(2); 7335 return \u0275\u0275resetView(ctx_r1.outdoorNodesAlwaysToggled.emit(!ctx_r1.showOutdoorNodesAlways)); 7336 }); 7337 \u0275\u0275elementEnd(); 7338 } 7339 if (rf & 2) { 7340 const ctx_r1 = \u0275\u0275nextContext(2); 7341 \u0275\u0275property("active", ctx_r1.showOutdoorNodesAlways); 7342 } 7343} 7344function EntitySidebarComponent_Conditional_2_Template(rf, ctx) { 7345 if (rf & 1) { 7346 const _r3 = \u0275\u0275getCurrentView(); 7347 \u0275\u0275elementStart(0, "header", 5); 7348 \u0275\u0275element(1, "ng-icon", 6); 7349 \u0275\u0275elementStart(2, "span"); 7350 \u0275\u0275text(3, "Entity Navigator"); 7351 \u0275\u0275elementEnd()(); 7352 \u0275\u0275elementStart(4, "div", 7)(5, "app-tool-button", 8); 7353 \u0275\u0275listener("clicked", function EntitySidebarComponent_Conditional_2_Template_app_tool_button_clicked_5_listener() { 7354 \u0275\u0275restoreView(_r3); 7355 const ctx_r1 = \u0275\u0275nextContext(); 7356 return \u0275\u0275resetView(ctx_r1.groundReferenceToggled.emit(!ctx_r1.showGroundReference)); 7357 }); 7358 \u0275\u0275elementEnd(); 7359 \u0275\u0275elementStart(6, "app-tool-button", 9); 7360 \u0275\u0275listener("clicked", function EntitySidebarComponent_Conditional_2_Template_app_tool_button_clicked_6_listener() { 7361 \u0275\u0275restoreView(_r3); 7362 const ctx_r1 = \u0275\u0275nextContext(); 7363 return \u0275\u0275resetView(ctx_r1.centerOnSelectionToggled.emit(!ctx_r1.centerOnSelection)); 7364 }); 7365 \u0275\u0275elementEnd(); 7366 \u0275\u0275elementStart(7, "app-tool-button", 10); 7367 \u0275\u0275listener("clicked", function EntitySidebarComponent_Conditional_2_Template_app_tool_button_clicked_7_listener() { 7368 \u0275\u0275restoreView(_r3); 7369 const ctx_r1 = \u0275\u0275nextContext(); 7370 return \u0275\u0275resetView(ctx_r1.buildingsToggled.emit(!ctx_r1.showBuildings)); 7371 }); 7372 \u0275\u0275elementEnd(); 7373 \u0275\u0275elementStart(8, "app-tool-button", 11); 7374 \u0275\u0275listener("clicked", function EntitySidebarComponent_Conditional_2_Template_app_tool_button_clicked_8_listener() { 7375 \u0275\u0275restoreView(_r3); 7376 const ctx_r1 = \u0275\u0275nextContext(); 7377 return \u0275\u0275resetView(ctx_r1.roomsToggled.emit(!ctx_r1.showRooms)); 7378 }); 7379 \u0275\u0275elementEnd(); 7380 \u0275\u0275elementStart(9, "app-tool-button", 12); 7381 \u0275\u0275listener("clicked", function EntitySidebarComponent_Conditional_2_Template_app_tool_button_clicked_9_listener() { 7382 \u0275\u0275restoreView(_r3); 7383 const ctx_r1 = \u0275\u0275nextContext(); 7384 return \u0275\u0275resetView(ctx_r1.roomLabelsToggled.emit(!ctx_r1.showRoomLabels)); 7385 }); 7386 \u0275\u0275elementEnd(); 7387 \u0275\u0275elementStart(10, "app-tool-button", 13); 7388 \u0275\u0275listener("clicked", function EntitySidebarComponent_Conditional_2_Template_app_tool_button_clicked_10_listener() { 7389 \u0275\u0275restoreView(_r3); 7390 const ctx_r1 = \u0275\u0275nextContext(); 7391 return \u0275\u0275resetView(ctx_r1.outlinesOnlyToggled.emit(!ctx_r1.showOutlinesOnly)); 7392 }); 7393 \u0275\u0275elementEnd(); 7394 \u0275\u0275elementStart(11, "app-tool-button", 14); 7395 \u0275\u0275listener("clicked", function EntitySidebarComponent_Conditional_2_Template_app_tool_button_clicked_11_listener() { 7396 \u0275\u0275restoreView(_r3); 7397 const ctx_r1 = \u0275\u0275nextContext();
7398 return \u0275\u0275resetView(ctx_r1.nodesToggled.emit(!ctx_r1.showNodes)); 7399 }); 7400 \u0275\u0275elementEnd(); 7401 \u0275\u0275elementStart(12, "app-tool-button", 15); 7402 \u0275\u0275listener("clicked", function EntitySidebarComponent_Conditional_2_Template_app_tool_button_clicked_12_listener() { 7403 \u0275\u0275restoreView(_r3); 7404 const ctx_r1 = \u0275\u0275nextContext(); 7405 return \u0275\u0275resetView(ctx_r1.floorBackgroundsToggled.emit(!ctx_r1.showFloorBackgrounds)); 7406 }); 7407 \u0275\u0275elementEnd(); 7408 \u0275\u0275template(13, EntitySidebarComponent_Conditional_2_Conditional_13_Template, 1, 1, "app-tool-button", 16); 7409 \u0275\u0275elementEnd(); 7410 \u0275\u0275elementStart(14, "div", 17)(15, "app-entity-tree-panel", 18); 7411 \u0275\u0275listener("nodeClicked", function EntitySidebarComponent_Conditional_2_Template_app_entity_tree_panel_nodeClicked_15_listener($event) { 7412 \u0275\u0275restoreView(_r3); 7413 const ctx_r1 = \u0275\u0275nextContext(); 7414 return \u0275\u0275resetView(ctx_r1.nodeClicked.emit($event)); 7415 })("nodeContextMenu", function EntitySidebarComponent_Conditional_2_Template_app_entity_tree_panel_nodeContextMenu_15_listener($event) { 7416 \u0275\u0275restoreView(_r3); 7417 const ctx_r1 = \u0275\u0275nextContext(); 7418 return \u0275\u0275resetView(ctx_r1.nodeContextMenu.emit($event)); 7419 })("addBuildingRequested", function EntitySidebarComponent_Conditional_2_Template_app_entity_tree_panel_addBuildingRequested_15_listener() { 7420 \u0275\u0275restoreView(_r3); 7421 const ctx_r1 = \u0275\u0275nextContext(); 7422 return \u0275\u0275resetView(ctx_r1.addBuildingRequested.emit()); 7423 }); 7424 \u0275\u0275elementEnd()(); 7425 } 7426 if (rf & 2) { 7427 const ctx_r1 = \u0275\u0275nextContext(); 7428 \u0275\u0275advance(5); 7429 \u0275\u0275property("active", ctx_r1.showGroundReference); 7430 \u0275\u0275advance(); 7431 \u0275\u0275property("active", ctx_r1.centerOnSelection); 7432 \u0275\u0275advance(); 7433 \u0275\u0275property("active", ctx_r1.showBuildings); 7434 \u0275\u0275advance(); 7435 \u0275\u0275property("active", ctx_r1.showRooms); 7436 \u0275\u0275advance(); 7437 \u0275\u0275property("active", ctx_r1.showRoomLabels); 7438 \u0275\u0275advance(); 7439 \u0275\u0275property("active", ctx_r1.showOutlinesOnly); 7440 \u0275\u0275advance(); 7441 \u0275\u0275property("active", ctx_r1.showNodes); 7442 \u0275\u0275advance(); 7443 \u0275\u0275property("active", ctx_r1.showFloorBackgrounds); 7444 \u0275\u0275advance(); 7445 \u0275\u0275conditional(ctx_r1.showNodes ? 13 : -1); 7446 } 7447} 7448var _EntitySidebarComponent = class _EntitySidebarComponent { 7449 constructor() { 7450 this.showBuildings = true; 7451 this.showRooms = true; 7452 this.showNodes = false; 7453 this.showGroundReference = true; 7454 this.centerOnSelection = true; 7455 this.showOutdoorNodesAlways = false; 7456 this.showRoomLabels = true; 7457 this.showOutlinesOnly = false; 7458 this.showFloorBackgrounds = false; 7459 this.collapsed = false; 7460 this.isMobile = false; 7461 this.buildingsToggled = new EventEmitter(); 7462 this.roomsToggled = new EventEmitter(); 7463 this.nodesToggled = new EventEmitter(); 7464 this.groundReferenceToggled = new EventEmitter(); 7465 this.centerOnSelectionToggled = new EventEmitter(); 7466 this.outdoorNodesAlwaysToggled = new EventEmitter(); 7467 this.roomLabelsToggled = new EventEmitter(); 7468 this.outlinesOnlyToggled = new EventEmitter(); 7469 this.floorBackgroundsToggled = new EventEmitter(); 7470 this.collapsedChange = new EventEmitter(); 7471 this.nodeClicked = new EventEmitter(); 7472 this.nodeContextMenu = new EventEmitter(); 7473 this.addBuildingRequested = new EventEmitter(); 7474 } 7475 toggleCollapse() { 7476 this.collapsed = !this.collapsed; 7477 this.collapsedChange.emit(this.collapsed); 7478 } 7479 onBackdropClick() { 7480 if (!this.collapsed) { 7481 this.collapsed = true; 7482 this.collapsedChange.emit(true); 7483 } 7484 } 7485 getCollapseIcon() { 7486 return this.collapsed ? "lucideChevronRight" : "lucideChevronLeft"; 7487 } 7488 getCollapseTitle() { 7489 return this.collapsed ? "Expand sidebar" : "Collapse sidebar"; 7490 } 7491}; 7492_EntitySidebarComponent.\u0275fac = function EntitySidebarComponent_Factory(__ngFactoryType__) { 7493 return new (__ngFactoryType__ || _EntitySidebarComponent)(); 7494}; 7495_EntitySidebarComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _EntitySidebarComponent, selectors: [["app-entity-sidebar"]], inputs: { showBuildings: "showBuildings", showRooms: "showRooms", showNodes: "showNodes", showGroundReference: "showGroundReference", centerOnSelection: "centerOnSelection", showOutdoorNodesAlways: "showOutdoorNodesAlways", showRoomLabels: "showRoomLabels", showOutlinesOnl
7495y: "showOutlinesOnly", showFloorBackgrounds: "showFloorBackgrounds", collapsed: "collapsed", isMobile: "isMobile" }, outputs: { buildingsToggled: "buildingsToggled", roomsToggled: "roomsToggled", nodesToggled: "nodesToggled", groundReferenceToggled: "groundReferenceToggled", centerOnSelectionToggled: "centerOnSelectionToggled", outdoorNodesAlwaysToggled: "outdoorNodesAlwaysToggled", roomLabelsToggled: "roomLabelsToggled", outlinesOnlyToggled: "outlinesOnlyToggled", floorBackgroundsToggled: "floorBackgroundsToggled", collapsedChange: "collapsedChange", nodeClicked: "nodeClicked", nodeContextMenu: "nodeContextMenu", addBuildingRequested: "addBuildingRequested" }, features: [\u0275\u0275ProvidersFeature([], [ 7496 provideIcons({ 7497 lucideChevronLeft, 7498 lucideChevronRight, 7499 lucideBuilding2, 7500 lucideSquare, 7501 lucideCircleDot, 7502 lucideLayers, 7503 lucideFolderTree, 7504 lucideLocate, 7505 lucideSun, 7506 lucideTag, 7507 lucideScan, 7508 lucideImage 7509 }) 7510])], decls: 5, vars: 8, consts: [[1, "sidebar-backdrop"], [1, "entity-sidebar"], [1, "collapse-handle", 3, "click", "title"], ["size", "16", 3, "name"], [1, "sidebar-backdrop", 3, "click"], [1, "sidebar-title"], ["name", "lucideFolderTree", "size", "16"], [1, "filter-toggles"], ["icon", "lucideLayers", "hotkey", "G", "title", "Ground Reference", "size", "compact", 3, "clicked", "active"], ["icon", "lucideLocate", "hotkey", "O", "title", "Center on Selection", "size", "compact", 3, "clicked", "active"], ["icon", "lucideBuilding2", "hotkey", "B", "title", "Show Buildings", "size", "compact", 3, "clicked", "active"], ["icon", "lucideSquare", "hotkey", "R", "title", "Show Rooms", "size", "compact", 3, "clicked", "active"], ["icon", "lucideTag", "hotkey", "L", "title", "Show Room Labels", "size", "compact", 3, "clicked", "active"], ["icon", "lucideScan", "hotkey", "W", "title", "Wireframe Mode", "size", "compact", 3, "clicked", "active"], ["icon", "lucideCircleDot", "hotkey", "N", "title", "Show Nodes", "size", "compact", 3, "clicked", "active"], ["icon", "lucideImage", "hotkey", "I", "title", "Floor Backgrounds", "size", "compact", 3, "clicked", "active"], ["icon", "lucideSun", "hotkey", "U", "title", "Always Show Outdoor Nodes", "size", "compact", 3, "active"], [1, "sidebar-content"], [3, "nodeClicked", "nodeContextMenu", "addBuildingRequested"], ["icon", "lucideSun", "hotkey", "U", "title", "Always Show Outdoor Nodes", "size", "compact", 3, "clicked", "active"]], template: function EntitySidebarComponent_Template(rf, ctx) { 7511 if (rf & 1) { 7512 \u0275\u0275template(0, EntitySidebarComponent_Conditional_0_Template, 1, 0, "div", 0); 7513 \u0275\u0275elementStart(1, "aside", 1); 7514 \u0275\u0275template(2, EntitySidebarComponent_Conditional_2_Template, 16, 9); 7515 \u0275\u0275elementStart(3, "button", 2); 7516 \u0275\u0275listener("click", function EntitySidebarComponent_Template_button_click_3_listener() { 7517 return ctx.toggleCollapse(); 7518 }); 7519 \u0275\u0275element(4, "ng-icon", 3); 7520 \u0275\u0275elementEnd()(); 7521 } 7522 if (rf & 2) { 7523 \u0275\u0275conditional(ctx.isMobile && !ctx.collapsed ? 0 : -1); 7524 \u0275\u0275advance(); 7525 \u0275\u0275classProp("collapsed", ctx.collapsed)("mobile", ctx.isMobile); 7526 \u0275\u0275advance(); 7527 \u0275\u0275conditional(!ctx.collapsed ? 2 : -1); 7528 \u0275\u0275advance(); 7529 \u0275\u0275property("title", ctx.getCollapseTitle()); 7530 \u0275\u0275advance(); 7531 \u0275\u0275property("name", ctx.getCollapseIcon()); 7532 } 7533}, dependencies: [NgIcon, ToolButtonComponent, EntityTreePanelComponent], styles: ["\n\n.sidebar-backdrop[_ngcontent-%COMP%] {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.4);\n z-index: 150;\n animation: _ngcontent-%COMP%_fadeIn 0.2s ease-out;\n}\n@keyframes _ngcontent-%COMP%_fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n.entity-sidebar[_ngcontent-%COMP%] {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n width: 280px;\n background: var(--tool-palette-bg, rgba(255, 255, 255, 0.95));\n border-right: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n display: flex;\n flex-direction: column;\n z-index: 50;\n transition: width 0.25s ease-out;\n}\n.entity-sidebar.collapsed[_ngcontent-%COMP%] {\n width: 0;
7533\n border-right: none;\n}\n.entity-sidebar.collapsed[_ngcontent-%COMP%] .sidebar-title[_ngcontent-%COMP%], \n.entity-sidebar.collapsed[_ngcontent-%COMP%] .filter-toggles[_ngcontent-%COMP%], \n.entity-sidebar.collapsed[_ngcontent-%COMP%] .sidebar-content[_ngcontent-%COMP%] {\n opacity: 0;\n visibility: hidden;\n}\n.entity-sidebar.collapsed[_ngcontent-%COMP%] .collapse-handle[_ngcontent-%COMP%] {\n left: 0;\n}\n.sidebar-title[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 16px;\n border-bottom: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n flex-shrink: 0;\n font-weight: 600;\n font-size: 14px;\n color: var(--ion-text-color);\n}\n.sidebar-title[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n color: var(--ion-color-primary);\n}\n.filter-toggles[_ngcontent-%COMP%] {\n display: flex;\n gap: 4px;\n justify-content: center;\n flex-wrap: wrap;\n padding: 6px 8px;\n border-bottom: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n flex-shrink: 0;\n}\n.filter-toggles[_ngcontent-%COMP%] .filter-divider[_ngcontent-%COMP%] {\n width: 1px;\n height: 16px;\n background: var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n margin: 4px 4px;\n flex-shrink: 0;\n}\n.filter-toggles[_ngcontent-%COMP%] .tool-palette-button {\n width: 24px;\n height: 24px;\n border-radius: 4px;\n background: transparent;\n border: 1px solid transparent;\n}\n.filter-toggles[_ngcontent-%COMP%] .tool-palette-button:hover {\n background: rgba(0, 0, 0, 0.05);\n}\n.filter-toggles[_ngcontent-%COMP%] .tool-palette-button.active {\n background: transparent;\n border: 1px solid rgba(0, 0, 0, 0.35);\n color: var(--ion-text-color);\n}\n.filter-toggles[_ngcontent-%COMP%] .tool-palette-hotkey {\n display: none;\n}\n.sidebar-content[_ngcontent-%COMP%] {\n flex: 1;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n.sidebar-content[_ngcontent-%COMP%] app-entity-tree-panel {\n flex: 1;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n.sidebar-content[_ngcontent-%COMP%] .entity-tree-panel {\n width: 100%;\n max-height: none;\n border-radius: 0;\n box-shadow: none;\n border: none;\n flex: 1;\n display: flex;\n flex-direction: column;\n}\n.sidebar-content[_ngcontent-%COMP%] .tree-viewport {\n max-height: none;\n min-height: 0;\n flex: 1;\n}\n.collapse-handle[_ngcontent-%COMP%] {\n position: absolute;\n left: calc(100% - 1px);\n top: 50%;\n transform: translateY(-50%);\n width: 14px;\n height: 48px;\n padding: 0;\n background: var(--tool-palette-bg, rgba(255, 255, 255, 0.95));\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n border-left: none;\n border-radius: 0 6px 6px 0;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--ion-color-medium);\n transition:\n color 0.2s ease,\n box-shadow 0.2s ease,\n left 0.25s ease-out;\n z-index: 1;\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n}\n.collapse-handle[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0;\n padding: 0;\n}\n.collapse-handle[_ngcontent-%COMP%]:hover {\n color: var(--ion-color-primary);\n box-shadow: 2px 0 8px rgba(var(--ion-color-primary-rgb), 0.25);\n}\n.collapse-handle[_ngcontent-%COMP%]:active {\n transform: translateY(-50%) scale(0.95);\n}\n.entity-sidebar.mobile[_ngcontent-%COMP%] {\n z-index: 200;\n box-shadow: 4px 0 16px rgba(0, 0, 0, 0.15);\n}\n.entity-sidebar.mobile[_ngcontent-%COMP%]:not(.collapsed) {\n width: 100%;\n max-width: 320px;\n}\n@media (max-width: 767px) {\n .filter-toggles[_ngcontent-%COMP%] .tool-palette-button {\n width: 28px;\n height: 28px;\n }\n .collapse-handle[_ngcontent-%COMP%] {\n width: 18px;\n height: 56px;\n }\n}"], changeDetection: 0 }); 7534var EntitySidebarComponent = _EntitySidebarComponent; 7535(() => { 7536 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(EntitySidebarComponent, [{ 7537 type: Component, 7538 args: [{ selector: "app-entity-sidebar", standalone: true, imports: [NgIcon, ToolButtonComponent, EntityTreePanelComponent], viewProviders: [ 7539 provideIcons({ 7540 lucideChevronLeft, 7541 lucideChevronRight, 7542 lucideBuilding2, 7543 lucideSquare, 7544 lucideCircleDot, 7545 lucideLayers, 7546 lucideFolderTree, 7547 lucideLocate, 7548 lucideSun, 7549 lucideTag, 7550 lucideScan, 7551 lucideImage 7552 }) 7553 ], changeDetection: ChangeDetectionStrategy.OnPush, template: '<!-- Mobile backdrop (shown when sidebar expanded on mobile) -->\n@if (isMobile && !collapsed) {\n <div class="sidebar-backdrop" (click)="onBackdropClick()"></div>\n}\n\n<aside class="entity-sidebar" [class.collapsed]="collapsed" [class.mobile]="isMobile">\n <!-- Sidebar Content (only visible when expanded) -->\n @if (!collapsed) {\n <!-- Title Header -->\n <header class="sidebar-title">\n <ng-icon name="lucideFolderTree" size="16" />
7553\n <span>Entity Navigator</span>\n </header>\n\n <!-- Filter Toggles -->\n <div class="filter-toggles">\n <app-tool-button\n icon="lucideLayers"\n hotkey="G"\n title="Ground Reference"\n size="compact"\n [active]="showGroundReference"\n (clicked)="groundReferenceToggled.emit(!showGroundReference)"\n />\n <app-tool-button\n icon="lucideLocate"\n hotkey="O"\n title="Center on Selection"\n size="compact"\n [active]="centerOnSelection"\n (clicked)="centerOnSelectionToggled.emit(!centerOnSelection)"\n />\n <app-tool-button\n icon="lucideBuilding2"\n hotkey="B"\n title="Show Buildings"\n size="compact"\n [active]="showBuildings"\n (clicked)="buildingsToggled.emit(!showBuildings)"\n />\n <app-tool-button\n icon="lucideSquare"\n hotkey="R"\n title="Show Rooms"\n size="compact"\n [active]="showRooms"\n (clicked)="roomsToggled.emit(!showRooms)"\n />\n <app-tool-button\n icon="lucideTag"\n hotkey="L"\n title="Show Room Labels"\n size="compact"\n [active]="showRoomLabels"\n (clicked)="roomLabelsToggled.emit(!showRoomLabels)"\n />\n <app-tool-button\n icon="lucideScan"\n hotkey="W"\n title="Wireframe Mode"\n size="compact"\n [active]="showOutlinesOnly"\n (clicked)="outlinesOnlyToggled.emit(!showOutlinesOnly)"\n />\n <app-tool-button\n icon="lucideCircleDot"\n hotkey="N"\n title="Show Nodes"\n size="compact"\n [active]="showNodes"\n (clicked)="nodesToggled.emit(!showNodes)"\n />\n <app-tool-button\n icon="lucideImage"\n hotkey="I"\n title="Floor Backgrounds"\n size="compact"\n [active]="showFloorBackgrounds"\n (clicked)="floorBackgroundsToggled.emit(!showFloorBackgrounds)"\n />\n @if (showNodes) {\n <app-tool-button\n icon="lucideSun"\n hotkey="U"\n title="Always Show Outdoor Nodes"\n size="compact"\n [active]="showOutdoorNodesAlways"\n (clicked)="outdoorNodesAlwaysToggled.emit(!showOutdoorNodesAlways)"\n />\n }\n </div>\n\n <!-- Entity Tree Panel Content -->\n <div class="sidebar-content">\n <app-entity-tree-panel\n (nodeClicked)="nodeClicked.emit($event)"\n (nodeContextMenu)="nodeContextMenu.emit($event)"\n (addBuildingRequested)="addBuildingRequested.emit()"\n />\n </div>\n }\n\n <!-- Collapse/Expand Handle (always visible) -->\n <button\n class="collapse-handle"\n [title]="getCollapseTitle()"\n (click)="toggleCollapse()">\n <ng-icon [name]="getCollapseIcon()" size="16" />\n </button>\n</aside>\n', styles: ["/* src/app/pages/campus-map-v3/components/entity-sidebar/entity-sidebar.component.scss */\n.sidebar-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.4);\n z-index: 150;\n animation: fadeIn 0.2s ease-out;\n}\n@keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n.entity-sidebar {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n width: 280px;\n background: var(--tool-palette-bg, rgba(255, 255, 255, 0.95));\n border-right: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n display: flex;\n flex-direction: column;\n z-index: 50;\n transition: width 0.25s ease-out;\n}\n.entity-sidebar.collapsed {\n width: 0;\n border-right: none;\n}\n.entity-sidebar.collapsed .sidebar-title,\n.entity-sidebar.collapsed .filter-toggles,\n.entity-sidebar.collapsed .sidebar-content {\n opacity: 0;\n visibility: hidden;\n}\n.entity-sidebar.collapsed .collapse-handle {\n left: 0;\n}\n.sidebar-title {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 16px;\n border-bottom: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n flex-shrink: 0;\n font-weight: 600;\n font-size: 14px;\n color: var(--ion-text-color);\n}\n.sidebar-title ng-icon {\n color: var(--ion-color-primary);\n}\n.filter-toggles {\n display: flex;\n gap: 4px;\n justify-content: center;\n flex-wrap: wrap;\n padding: 6px 8px;\n border-bottom: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n flex-shrink: 0;\n}\n.filter-toggles .filter-divider {\n width: 1px;\n height: 16px;\n background: var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n margin: 4px 4px;\n flex-shrink: 0;\n}\n.filter-toggles ::ng-deep .tool-palette-button {\n width: 24px;\n height: 24px;\n border-radius: 4px;\n background: transparent;\n border: 1px solid transparent;\n}\n.filter-toggles ::ng-deep .tool-palette-button:hover {\n background: rgba(0, 0, 0, 0.05);\n}\n.filter-toggles ::ng-deep .tool-palette-button.active {\n background: transparent;\n border: 1px solid rgba(0, 0, 0, 0.35);\n color: var(--ion-text-color);\n}\n.filter-toggles ::ng-deep .tool-palette-hotkey {\n display: none;\n}\n.sidebar-content {\n flex: 1;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n.sidebar-content ::ng-deep app-entity-tree-panel {\n flex: 1;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n.sidebar-content ::ng-deep .entity-tree-panel {\n width: 100%;\n max-height: none;\n border-radius: 0;\n box-shadow: none;\n border: none;\n flex: 1;\n display: flex;\n flex-direction: column;\n}\n.sidebar-content ::ng-deep .tree-viewport {\n max-height: none;\n min-height: 0;\n flex: 1;\n}\n.collapse-handle {\n position: absolute;\n left: calc(100% - 1px);\n top: 50%;\n transform: translateY(-50%);\n width: 14px;\n height: 48px;\n padding: 0;\n background: var(--tool-palette-bg, rgba(255, 255, 255, 0.95));\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n border-left: none;\n border-radius: 0 6px 6px 0;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;
7553\n color: var(--ion-color-medium);\n transition:\n color 0.2s ease,\n box-shadow 0.2s ease,\n left 0.25s ease-out;\n z-index: 1;\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n}\n.collapse-handle ng-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0;\n padding: 0;\n}\n.collapse-handle:hover {\n color: var(--ion-color-primary);\n box-shadow: 2px 0 8px rgba(var(--ion-color-primary-rgb), 0.25);\n}\n.collapse-handle:active {\n transform: translateY(-50%) scale(0.95);\n}\n.entity-sidebar.mobile {\n z-index: 200;\n box-shadow: 4px 0 16px rgba(0, 0, 0, 0.15);\n}\n.entity-sidebar.mobile:not(.collapsed) {\n width: 100%;\n max-width: 320px;\n}\n@media (max-width: 767px) {\n .filter-toggles ::ng-deep .tool-palette-button {\n width: 28px;\n height: 28px;\n }\n .collapse-handle {\n width: 18px;\n height: 56px;\n }\n}\n"] }] 7554 }], null, { showBuildings: [{ 7555 type: Input 7556 }], showRooms: [{ 7557 type: Input 7558 }], showNodes: [{ 7559 type: Input 7560 }], showGroundReference: [{ 7561 type: Input 7562 }], centerOnSelection: [{ 7563 type: Input 7564 }], showOutdoorNodesAlways: [{ 7565 type: Input 7566 }], showRoomLabels: [{ 7567 type: Input 7568 }], showOutlinesOnly: [{ 7569 type: Input 7570 }], showFloorBackgrounds: [{ 7571 type: Input 7572 }], collapsed: [{ 7573 type: Input 7574 }], isMobile: [{ 7575 type: Input 7576 }], buildingsToggled: [{ 7577 type: Output 7578 }], roomsToggled: [{ 7579 type: Output 7580 }], nodesToggled: [{ 7581 type: Output 7582 }], groundReferenceToggled: [{ 7583 type: Output 7584 }], centerOnSelectionToggled: [{ 7585 type: Output 7586 }], outdoorNodesAlwaysToggled: [{ 7587 type: Output 7588 }], roomLabelsToggled: [{ 7589 type: Output 7590 }], outlinesOnlyToggled: [{ 7591 type: Output 7592 }], floorBackgroundsToggled: [{ 7593 type: Output 7594 }], collapsedChange: [{ 7595 type: Output 7596 }], nodeClicked: [{ 7597 type: Output 7598 }], nodeContextMenu: [{ 7599 type: Output 7600 }], addBuildingRequested: [{ 7601 type: Output 7602 }] }); 7603})(); 7604(() => { 7605 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(EntitySidebarComponent, { className: "EntitySidebarComponent", filePath: "src/app/pages/campus-map-v3/components/entity-sidebar/entity-sidebar.component.ts", lineNumber: 51 }); 7606})(); 7607
7608// src/app/common/map-renderer-v2/layers/fbx-model-layer.ts 7609var EDITOR_MODEL_ADAPTER = { 7610 normalizeModel(model) { 7611 model.rotation.x = MathUtils.degToRad(-90); 7612 model.updateMatrixWorld(true); 7613 const center = new Box3().setFromObject(model).getCenter(new Vector3()); 7614 model.traverse((child) => { 7615 if (child instanceof Mesh) { 7616 child.castShadow = true; 7617 child.receiveShadow = true; 7618 } 7619 }); 7620 model.position.set(-center.x, -center.y, -center.z); 7621 model.updateMatrixWorld(true); 7622 return new Box3().setFromObject(model); 7623 }, 7624 applyTransform(root, transform) { 7625 root.rotation.set(MathUtils.degToRad(transform.rotationX), MathUtils.degToRad(transform.rotationY), MathUtils.degToRad(transform.rotationZ)); 7626 const scale = transform.scale; 7627 root.scale.set(scale, scale, scale); 7628 root.position.set(transform.offsetX, transform.offsetY, transform.offsetZ); 7629 root.updateMatrixWorld(true); 7630 } 7631}; 7632var FbxModelLayer = class _FbxModelLayer { 7633 constructor(context) { 7634 this.context = context; 7635 this.name = "fbx-model-layer"; 7636 this.loadCallbacks = /* @__PURE__ */ new Set(); 7637 this.modelConfig = null; 7638 this.instances = /* @__PURE__ */ new Map(); 7639 this.instanceStates = /* @__PURE__ */ new Map(); 7640 this.activeModelId = null; 7641 this.cacheService = null; 7642 this.cacheCleanups = /* @__PURE__ */ new Map(); 7643 this.lastConfiguredUrls = /* @__PURE__ */ new Map(); 7644 this.appliedConfigs = /* @__PURE__ */ new Map(); 7645 this.requestSigs = /* @__PURE__ */ new Map(); 7646 this.directionalLight = null; 7647 this.group = new Group(); 7648 this.group.name = this.name; 7649 this.group.visible = false; 7650 } 7651 setModelConfig(config) { 7652 var _a3, _b2, _c, _d, _e, _f, _g; 7653 this.modelConfig = config; 7654 const desired = (_a3 = config == null ? void 0 : config.models) != null ? _a3 : []; 7655 const desiredIds = new Set(desired.map((model) => model.id)); 7656 for (const [id, instance] of [...this.instances]) { 7657 if (!desiredIds.has(id)) { 7658 instance.dispose(); 7659 this.instances.delete(id); 7660 this.instanceStates.delete(id); 7661 this.releaseCacheCleanup(id); 7662 } 7663 } 7664 for (const model of desired) { 7665 const instance = this.instances.get(model.id); 7666 if (!instance || !instance.isLoaded()) 7667 continue; 7668 if (this.lastConfiguredUrls.get(model.id) !== ((_b2 = model.modelUrl) != null ? _b2 : null)) 7669 continue; 7670 if (this.requestSigs.get(model.id) === _FbxModelLayer.signatureOf(model)) { 7671 instance.update(this.withCacheSources(model), this.group); 7672 } else if (((_c = this.instanceStates.get(model.id)) == null ? void 0 : _c.state) !== "loading") { 7673 void this.loadInstance(model).catch(() => void 0); 7674 } 7675 } 7676 for (const id of [...this.lastConfiguredUrls.keys()]) { 7677 if (!desiredIds.has(id)) { 7678 this.lastConfiguredUrls.delete(id); 7679 this.appliedConfigs.delete(id); 7680 this.requestSigs.delete(id); 7681 } 7682 } 7683 if (this.activeModelId && !desiredIds.has(this.activeModelId)) { 7684 this.activeModelId = (_e = (_d = desired[0]) == null ? void 0 : _d.id) != null ? _e : null; 7685 } 7686 if (!this.activeModelId) { 7687 this.activeModelId = (_g = (_f = desired[0]) == null ? void 0 : _f.id) != null ? _g : null; 7688 } 7689 this.syncDirectionalLightIntensity(); 7690 } 7691 getModelConfig() { 7692 return this.modelConfig; 7693 } 7694 getModelIds() { 7695 return [...this.instances.keys()]; 7696 } 7697 /** Select the model that transform/gamma operations apply to by default. */ 7698 setActiveModel(modelId) { 7699 if (modelId && !this.instances.has(modelId)) 7700 return; 7701 this.activeModelId = modelId; 7702 this.syncDirectionalLightIntensity(); 7703 } 7704 getActiveModelId() { 7705 return this.activeModelId; 7706 } 7707 setCacheService(service) { 7708 this.cacheService = service; 7709 } 7710 load() { 7711 return __async(this, null, function* () { 7712 var _a3, _b2, _c, _d; 7713 const desired = (_b2 = (_a3 = this.modelConfig) == null ? void 0 : _a3.models) != null ? _b2 : []; 7714 const pending = desired.filter((model) => { 7715 var _a4, _b3; 7716 const instance = this.instances.get(model.id); 7717 if (((_a4 = this.instanceStates.get(model.id)) == null ? void 0 : _a4.
7717state) === "loading") 7718 return false; 7719 if (!instance || !instance.isLoaded()) 7720 return true; 7721 return this.lastConfiguredUrls.get(model.id) !== ((_b3 = model.modelUrl) != null ? _b3 : null) || this.requestSigs.get(model.id) !== _FbxModelLayer.signatureOf(model); 7722 }); 7723 if (!desired.length) { 7724 this.notifyCallbacks("error", void 0, "No 3D model configured for this campus"); 7725 throw new Error("No 3D model configured for this campus"); 7726 } 7727 if (!pending.length) { 7728 this.notifyCallbacks("loaded", 100); 7729 return; 7730 } 7731 this.notifyCallbacks("loading", 0); 7732 const loads = pending.map((model) => this.loadInstance(model)); 7733 yield Promise.allSettled(loads); 7734 this.context.markDirty(); 7735 const failed = pending.find((model) => { 7736 var _a4; 7737 return ((_a4 = this.instanceStates.get(model.id)) == null ? void 0 : _a4.state) === "error"; 7738 }); 7739 if (failed) { 7740 const message = (_d = (_c = this.instanceStates.get(failed.id)) == null ? void 0 : _c.error) != null ? _d : "Unknown error loading FBX model"; 7741 this.notifyCallbacks("error", void 0, message); 7742 throw new Error(message); 7743 } 7744 this.notifyCallbacks("loaded", 100); 7745 }); 7746 } 7747 loadInstance(model) { 7748 return __async(this, null, function* () { 7749 var _a3, _b2; 7750 let effective = model; 7751 if (this.cacheService && ((_a3 = model.cacheRequest) == null ? void 0 : _a3.modelUploadId)) { 7752 const cached = yield this.loadFromCache(model, model.cacheRequest); 7753 if (cached) { 7754 effective = __spreadProps(__spreadValues({}, model), { 7755 modelUrl: cached.modelSource, 7756 textures: cached.textureSources 7757 }); 7758 this.cacheCleanups.set(model.id, cached.cleanup); 7759 } 7760 } 7761 const instance = this.getOrCreateInstance(model.id); 7762 this.instanceStates.set(model.id, { state: "loading" }); 7763 yield instance.update(effective, this.group); 7764 this.lastConfiguredUrls.set(model.id, (_b2 = model.modelUrl) != null ? _b2 : null); 7765 this.appliedConfigs.set(model.id, effective); 7766 this.requestSigs.set(model.id, _FbxModelLayer.signatureOf(model)); 7767 }); 7768 } 7769 /** Identity of the underlying uploads (FBX + named texture upload ids). */ 7770 static signatureOf(model) { 7771 var _a3, _b2, _c; 7772 const request = model.cacheRequest; 7773 if (!request) { 7774 return `url:${(_a3 = model.modelUrl) != null ? _a3 : ""}`; 7775 } 7776 const textures = Object.keys((_b2 = request.textureUploads) != null ? _b2 : {}).sort().map((name) => { 7777 var _a4; 7778 return `${name}=${(_a4 = request.textureUploads[name]) != null ? _a4 : ""}`; 7779 }).join(";"); 7780 return `m:${request.modelUploadId}@${(_c = request.version) != null ? _c : 0}|${textures}`; 7781 } 7782 loadFromCache(model, request) { 7783 return __async(this, null, function* () { 7784 if (!this.cacheService) 7785 return null; 7786 try { 7787 return yield this.cacheService.getModelWithFallback(request, (phase, progress) => { 7788 if (phase === "model") { 7789 this.notifyCallbacks("loading", Math.round(progress * 0.8)); 7790 } else if (phase === "texture") { 7791 this.notifyCallbacks("loading", 80 + Math.round(progress * 0.2)); 7792 } 7793 }); 7794 } catch { 7795 return null; 7796 } 7797 }); 7798 } 7799 getBounds() { 7800 const union = new Box3(); 7801 let hasAny = false; 7802 for (const instance of this.instances.values()) { 7803 const bounds = instance.getBounds(); 7804 if (!bounds || bounds.isEmpty()) 7805 continue; 7806 if (hasAny) { 7807 union.union(bounds); 7808 } else { 7809 union.copy(bounds); 7810 hasAny = true; 7811 } 7812 } 7813 if (!hasAny) { 7814 return this.instances.size ? { minX: -1e3, maxX: 1e3, minY: -1e3, maxY: 1e3 } : null; 7815 } 7816 if (!Number.isFinite(union.min.x) || !Number.isFinite(union.max.x)) { 7817 return { minX: -1e3, maxX: 1e3, minY: -1e3, maxY: 1e3 }; 7818 } 7819 return { 7820 minX: union.min.x, 7821 maxX: union.max.x, 7822 minY: union.min.y, 7823 maxY: union.max.y 7824 }; 7825 } 7826 get3DBounds(modelId) { 7827 var _a3, _b2; 7828 const box = (_b2 = (_a3 = this.instanceFor(modelId)) == null ? void 0 : _a3.getWorldBox()) != null ? _b2 : null; 7829 return box ? { min: box.min.clone(), max: box.max.clone() } : null; 7830 } 7831 get3DCenter(modelId) { 7832 var _a3, _b2; 7833 const box = (_b2 = (_a3 = this.instanceFor(modelId)) == null ? void 0 : _a3.getWorldBox()) != null ? _b2 : null; 7834 return box ? box.getCenter(new Vector3()) : null; 7835 } 7836 show() { 7837 this.group.visible = true; 7838 this.context.markDirty(); 7839 } 7840 hide() { 7841 this.group.visible = false;
7842 this.context.markDirty(); 7843 } 7844 isVisible() { 7845 return this.group.visible; 7846 } 7847 setTransform(transform, modelId) { 7848 var _a3; 7849 (_a3 = this.instanceFor(modelId)) == null ? void 0 : _a3.setTransform(transform); 7850 this.context.markDirty(); 7851 } 7852 getTransform(modelId) { 7853 var _a3, _b2; 7854 return (_b2 = (_a3 = this.instanceFor(modelId)) == null ? void 0 : _a3.getTransform()) != null ? _b2 : __spreadValues({}, DEFAULT_MODEL_3D_TRANSFORM); 7855 } 7856 resetTransform(modelId) { 7857 var _a3; 7858 (_a3 = this.instanceFor(modelId)) == null ? void 0 : _a3.setTransform(__spreadValues({}, DEFAULT_MODEL_3D_TRANSFORM)); 7859 } 7860 setGamma(gamma, modelId) { 7861 var _a3; 7862 const clamped = Math.max(0.5, Math.min(5, gamma)); 7863 (_a3 = this.instanceFor(modelId)) == null ? void 0 : _a3.setTransform({ gamma: clamped }); 7864 this.syncDirectionalLightIntensity(); 7865 } 7866 getGamma(modelId) { 7867 var _a3, _b2; 7868 return (_b2 = (_a3 = this.instanceFor(modelId)) == null ? void 0 : _a3.getTransform().gamma) != null ? _b2 : 1; 7869 } 7870 setDirectionalLight(light) { 7871 this.directionalLight = light; 7872 this.syncDirectionalLightIntensity(); 7873 } 7874 isModelLoaded(modelId) { 7875 const instance = this.instanceFor(modelId); 7876 return instance ? instance.isLoaded() : this.getModelIds().some((id) => { 7877 var _a3; 7878 return (_a3 = this.instances.get(id)) == null ? void 0 : _a3.isLoaded(); 7879 }); 7880 } 7881 isModelLoading(modelId) { 7882 var _a3; 7883 if (modelId != null) { 7884 return ((_a3 = this.instanceStates.get(modelId)) == null ? void 0 : _a3.state) === "loading"; 7885 } 7886 for (const state of this.instanceStates.values()) { 7887 if (state.state === "loading") 7888 return true; 7889 } 7890 return false; 7891 } 7892 onLoad(callback) { 7893 this.loadCallbacks.add(callback); 7894 return () => this.loadCallbacks.delete(callback); 7895 } 7896 dispose() { 7897 for (const instance of this.instances.values()) { 7898 instance.dispose(); 7899 } 7900 this.instances.clear(); 7901 this.instanceStates.clear(); 7902 this.lastConfiguredUrls.clear(); 7903 this.appliedConfigs.clear(); 7904 this.requestSigs.clear(); 7905 for (const cleanup of this.cacheCleanups.values()) { 7906 cleanup(); 7907 } 7908 this.cacheCleanups.clear(); 7909 this.loadCallbacks.clear(); 7910 this.group.clear(); 7911 } 7912 getOrCreateInstance(id) { 7913 const existing = this.instances.get(id); 7914 if (existing) 7915 return existing; 7916 const instance = new ModelInstance(id, EDITOR_MODEL_ADAPTER, { 7917 notifyLoad: (event) => { 7918 if (event.state === "loading" || event.state === "loaded" || event.state === "error" || event.state === "idle") { 7919 this.instanceStates.set(id, event); 7920 } 7921 }, 7922 markDirty: () => this.context.markDirty() 7923 }, {}); 7924 this.instances.set(id, instance); 7925 return instance; 7926 } 7927 instanceFor(modelId) { 7928 var _a3, _b2, _c; 7929 const id = (_b2 = (_a3 = modelId != null ? modelId : this.activeModelId) != null ? _a3 : this.getModelIds()[0]) != null ? _b2 : null; 7930 if (!id) 7931 return null; 7932 return (_c = this.instances.get(id)) != null ? _c : null; 7933 } 7934 withCacheSources(model) { 7935 const applied = this.appliedConfigs.get(model.id); 7936 if (!applied || applied.modelUrl == null) { 7937 return model; 7938 } 7939 return __spreadProps(__spreadValues({}, model), { modelUrl: applied.modelUrl, textures: applied.textures }); 7940 } 7941 releaseCacheCleanup(modelId) { 7942 var _a3; 7943 (_a3 = this.cacheCleanups.get(modelId)) == null ? void 0 : _a3(); 7944 this.cacheCleanups.delete(modelId); 7945 } 7946 syncDirectionalLightIntensity() { 7947 var _a3, _b2; 7948 if (!this.directionalLight) 7949 return; 7950 const gamma = (_b2 = (_a3 = this.instanceFor(this.activeModelId)) == null ? void 0 : _a3.getTransform().gamma) != null ? _b2 : 1; 7951 this.directionalLight.intensity = 0.8 * gamma; 7952 } 7953 notifyCallbacks(state, progress, error) { 7954 for (const callback of this.loadCallbacks) { 7955 callback(state, progress, error); 7956 } 7957 } 7958}; 7959
7960// src/app/pages/campus-map-v3/utils/ruler-measurement-controller.ts 7961var RulerMeasurementController = class { 7962 constructor(config) { 7963 this.raycaster = new Raycaster(); 7964 this.pointer = new Vector2(); 7965 this.isActive = false; 7966 this.isMeasuring = false; 7967 this.startPoint = null; 7968 this.endPoint = null; 7969 this.measurementLine = null; 7970 this.startMarker = null; 7971 this.endMarker = null; 7972 this.MIN_DISTANCE_THRESHOLD = 0.1; 7973 this.config = config; 7974 this.lineMaterial = new LineBasicMaterial({ 7975 color: 16737792, 7976 // Orange 7977 linewidth: 2, 7978 depthTest: false 7979 }); 7980 this.markerMaterial = new MeshBasicMaterial({ 7981 color: 16737792, 7982 // Orange 7983 depthTest: false 7984 }); 7985 this.markerGeometry = new SphereGeometry(8, 16, 16); 7986 this.boundMouseDown = this.handleMouseDown.bind(this); 7987 this.boundMouseMove = this.handleMouseMove.bind(this); 7988 this.boundMouseUp = this.handleMouseUp.bind(this); 7989 this.boundKeyDown = this.handleKeyDown.bind(this); 7990 } 7991 /** 7992 * Activate ruler measurement mode. 7993 */ 7994 activate() { 7995 if (this.isActive) 7996 return; 7997 this.isActive = true; 7998 this.config.canvas.style.cursor = "crosshair"; 7999 this.attachListeners(); 8000 } 8001 /** 8002 * Deactivate ruler measurement mode. 8003 */ 8004 deactivate() { 8005 if (!this.isActive) 8006 return; 8007 this.isActive = false; 8008 this.config.canvas.style.cursor = ""; 8009 this.detachListeners(); 8010 this.clearMeasurement(); 8011 } 8012 /** 8013 * Check if ruler mode is currently active. 8014 */ 8015 isActiveState() { 8016 return this.isActive; 8017 } 8018 /** 8019 * Get current measurement state. 8020 */ 8021 getState() { 8022 var _a3, _b2, _c, _d; 8023 return { 8024 isActive: this.isActive, 8025 isMeasuring: this.isMeasuring, 8026 startPoint: (_b2 = (_a3 = this.startPoint) == null ? void 0 : _a3.clone()) != null ? _b2 : null, 8027 endPoint: (_d = (_c = this.endPoint) == null ? void 0 : _c.clone()) != null ? _d : null, 8028 modelDistance: this.calculateDistance() 8029 }; 8030 } 8031 /** 8032 * Clear measurement visuals without deactivating. 8033 */ 8034 clearMeasurement() { 8035 if (this.measurementLine) { 8036 this.config.overlayGroup.remove(this.measurementLine); 8037 this.measurementLine.geometry.dispose(); 8038 this.measurementLine = null; 8039 } 8040 if (this.startMarker) { 8041 this.config.overlayGroup.remove(this.startMarker); 8042 this.startMarker = null; 8043 } 8044 if (this.endMarker) { 8045 this.config.overlayGroup.remove(this.endMarker); 8046 this.endMarker = null; 8047 } 8048 this.startPoint = null; 8049 this.endPoint = null; 8050 this.isMeasuring = false; 8051 this.config.onDirty(); 8052 } 8053 /** 8054 * Clean up resources. 8055 */ 8056 dispose() { 8057 this.deactivate(); 8058 this.lineMaterial.dispose(); 8059 this.markerMaterial.dispose(); 8060 this.markerGeometry.dispose(); 8061 } 8062 // ========================================================================= 8063 // Event Listeners 8064 // ========================================================================= 8065 attachListeners() { 8066 this.config.canvas.addEventListener("mousedown", this.boundMouseDown); 8067 window.addEventListener("mousemove", this.boundMouseMove); 8068 window.addEventListener("mouseup", this.boundMouseUp); 8069 window.addEventListener("keydown", this.boundKeyDown); 8070 } 8071 detachListeners() { 8072 this.config.canvas.removeEventListener("mousedown", this.boundMouseDown); 8073 window.removeEventListener("mousemove", this.boundMouseMove); 8074 window.removeEventListener("mouseup", this.boundMouseUp); 8075 window.removeEventListener("keydown", this.boundKeyDown); 8076 } 8077 // ========================================================================= 8078 // Mouse Handlers 8079 // ========================================================================= 8080 handleMouseDown(e) { 8081 if (e.button !== 0) 8082 return; 8083 const hitPoint = this.raycastModel(e.clientX, e.clientY); 8084 if (!hitPoint) 8085 return; 8086 this.clearMeasurement(); 8087 this.isMeasuring = true; 8088 this.startPoint = hitPoint; 8089 this.endPoint = hitPoint.clone(); 8090 this.createVisuals(); 8091 this.config.onDirty(); 8092 } 8093 handleMouseMove(e) { 8094 if (!this.isMeasuring || !this.startPoint) 8095 return; 8096 const hitPoint = this.raycastModel(e.clientX, e.clientY); 8097 if (hitPoint) { 8098 this.endPoint = hitPoint; 8099 this.updateVisuals(); 8100 this.config.onDirty(); 8101 } 8102 } 8103 handleMouseUp(_e) { 8104 if (!this.isMeasuring || !this.startPoint || !this.endPoint) 8105 return; 8106 this.isMeasuring = false;
8107 const distance = this.calculateDistance(); 8108 if (distance > this.MIN_DISTANCE_THRESHOLD) { 8109 this.config.onMeasurementComplete(distance); 8110 } else { 8111 this.clearMeasurement(); 8112 } 8113 } 8114 handleKeyDown(e) { 8115 if (isEditableKeyboardEvent(e)) 8116 return; 8117 if (e.key === "Escape") { 8118 e.preventDefault(); 8119 e.stopImmediatePropagation(); 8120 this.clearMeasurement(); 8121 this.config.onMeasurementCancel(); 8122 } 8123 } 8124 // ========================================================================= 8125 // Raycasting 8126 // ========================================================================= 8127 /** 8128 * Get the world position for a screen click by intersecting with a ground plane. 8129 * Uses a horizontal plane at z=0 since the camera looks down -Z axis. 8130 */ 8131 raycastModel(clientX, clientY) { 8132 const rect = this.config.canvas.getBoundingClientRect(); 8133 this.pointer.x = (clientX - rect.left) / rect.width * 2 - 1; 8134 this.pointer.y = -((clientY - rect.top) / rect.height) * 2 + 1; 8135 this.raycaster.setFromCamera(this.pointer, this.config.camera); 8136 const groundPlane = new Plane(new Vector3(0, 0, 1), 0); 8137 const hitPoint = new Vector3(); 8138 const intersected = this.raycaster.ray.intersectPlane(groundPlane, hitPoint); 8139 return intersected ? hitPoint : null; 8140 } 8141 // ========================================================================= 8142 // Distance Calculation 8143 // ========================================================================= 8144 calculateDistance() { 8145 if (!this.startPoint || !this.endPoint) 8146 return 0; 8147 return this.startPoint.distanceTo(this.endPoint); 8148 } 8149 // ========================================================================= 8150 // Visual Feedback 8151 // ========================================================================= 8152 createVisuals() { 8153 if (!this.startPoint) 8154 return; 8155 const markerScale = 1 / this.config.camera.zoom; 8156 const geometry = new BufferGeometry(); 8157 const positions = new Float32Array(6); 8158 geometry.setAttribute("position", new BufferAttribute(positions, 3)); 8159 this.measurementLine = new Line(geometry, this.lineMaterial); 8160 this.measurementLine.renderOrder = 999; 8161 this.measurementLine.frustumCulled = false; 8162 this.config.overlayGroup.add(this.measurementLine); 8163 this.startMarker = new Mesh(this.markerGeometry, this.markerMaterial); 8164 this.startMarker.renderOrder = 999; 8165 this.startMarker.scale.setScalar(markerScale); 8166 this.startMarker.position.copy(this.startPoint); 8167 this.config.overlayGroup.add(this.startMarker); 8168 this.endMarker = new Mesh(this.markerGeometry, this.markerMaterial); 8169 this.endMarker.renderOrder = 999; 8170 this.endMarker.scale.setScalar(markerScale); 8171 this.config.overlayGroup.add(this.endMarker); 8172 this.updateVisuals(); 8173 } 8174 updateVisuals() { 8175 if (!this.startPoint || !this.endPoint) 8176 return; 8177 if (this.measurementLine) { 8178 const positions = this.measurementLine.geometry.attributes["position"]; 8179 positions.setXYZ(0, this.startPoint.x, this.startPoint.y, this.startPoint.z); 8180 positions.setXYZ(1, this.endPoint.x, this.endPoint.y, this.endPoint.z); 8181 positions.needsUpdate = true; 8182 } 8183 if (this.startMarker) { 8184 this.startMarker.position.copy(this.startPoint); 8185 } 8186 if (this.endMarker) { 8187 this.endMarker.position.copy(this.endPoint); 8188 } 8189 } 8190}; 8191
8192// src/app/pages/campus-map-v3/commands/scale-model-command.ts 8193var ScaleModelCommand = class extends BaseEditorCommand { 8194 constructor(config) { 8195 super(); 8196 this.fbxModelLayer = config.fbxModelLayer; 8197 this.modelId = config.modelId; 8198 this.previousScale = config.previousScale; 8199 this.newScale = config.newScale; 8200 this.onVisualUpdate = config.onVisualUpdate; 8201 this.onTransformChange = config.onTransformChange; 8202 const scalePercent = Math.round(this.newScale * 100); 8203 this.description = `Set model scale to ${scalePercent}%`; 8204 } 8205 execute() { 8206 this.applyScale(this.newScale); 8207 } 8208 undo() { 8209 this.applyScale(this.previousScale); 8210 } 8211 applyScale(scale) { 8212 this.fbxModelLayer.setTransform({ scale }, this.modelId); 8213 this.onTransformChange({ scale }, this.modelId); 8214 this.onVisualUpdate(); 8215 } 8216}; 8217
8218// src/app/pages/campus-map-v3/commands/update-model-transform-command.ts 8219var UpdateModelTransformCommand = class extends BaseEditorCommand { 8220 constructor(config) { 8221 super(); 8222 this.modelId = config.modelId; 8223 this.previousTransform = __spreadValues({}, config.previousTransform); 8224 this.newTransform = __spreadValues({}, config.newTransform); 8225 this.onApplyTransform = config.onApplyTransform; 8226 this.description = this.buildDescription(config.changedField); 8227 } 8228 execute() { 8229 this.onApplyTransform(this.newTransform, this.modelId); 8230 } 8231 undo() { 8232 this.onApplyTransform(this.previousTransform, this.modelId); 8233 } 8234 buildDescription(changedField) { 8235 if (!changedField) { 8236 return "Update 3D model transform"; 8237 } 8238 const prev = this.previousTransform[changedField]; 8239 const next = this.newTransform[changedField]; 8240 switch (changedField) { 8241 case "offsetX": 8242 case "offsetY": 8243 case "offsetZ": 8244 return `Set ${changedField} to ${this.formatNumber(next)}`; 8245 case "rotationX": 8246 case "rotationY": 8247 case "rotationZ": 8248 return `Set ${changedField} to ${this.formatNumber(next)}\xB0`; 8249 case "scale": 8250 return `Set scale to ${Math.round(next * 100)}%`; 8251 default: 8252 return `Update ${changedField}`; 8253 } 8254 } 8255 formatNumber(value) { 8256 return Number.isInteger(value) ? value.toString() : value.toFixed(1); 8257 } 8258}; 8259
8260// src/app/pages/campus-map-v3/utils/model-3d-controller.ts 8261var Model3DController = class { 8262 constructor(config) { 8263 this.config = config; 8264 this.fbxModelLayer = null; 8265 this.rulerController = null; 8266 this._committedTransforms = /* @__PURE__ */ new Map(); 8267 this._campusId = null; 8268 this.measurementOverlayGroup = new Group(); 8269 this.measurementOverlayGroup.name = "measurement-overlay"; 8270 this.model3DWrapperGroup = new Group(); 8271 this.model3DWrapperGroup.name = "model-3d-wrapper"; 8272 this._saveCallback = this.persistTransform.bind(this); 8273 this.config.historyService.registerSaveCallback(this._saveCallback); 8274 } 8275 /** 8276 * Initialize the controller with Three.js scene objects. 8277 * Creates FbxModelLayer and adds groups to scene. 8278 */ 8279 initialize(scene, camera, renderer) { 8280 var _a3; 8281 this.initFbxModelLayer(scene, camera, renderer); 8282 scene.add(this.measurementOverlayGroup); 8283 (_a3 = this.fbxModelLayer) == null ? void 0 : _a3.setDirectionalLight(this.config.mapScene.getDirectionalLight()); 8284 } 8285 /** 8286 * Get the FBX model group for scene integration. 8287 */ 8288 get group() { 8289 var _a3, _b2; 8290 return (_b2 = (_a3 = this.fbxModelLayer) == null ? void 0 : _a3.group) != null ? _b2 : new Group(); 8291 } 8292 /** 8293 * Get the measurement overlay group for scene integration. 8294 */ 8295 get measurementGroup() { 8296 return this.measurementOverlayGroup; 8297 } 8298 /** 8299 * Get the wrapper group for gesture system integration. 8300 * This group receives view rotation while fbxModelLayer.group receives model rotation. 8301 */ 8302 get wrapperGroup() { 8303 return this.model3DWrapperGroup; 8304 } 8305 /** 8306 * Load 3D model info from the entity database and configure the layer with 8307 * every model of the campus. 8308 */ 8309 loadModelInfo(campusId) { 8310 return __async(this, null, function* () { 8311 this._campusId = campusId; 8312 this._committedTransforms.clear(); 8313 yield this.config.model3DService.loadModelInfo(campusId); 8314 this.applyModelConfig(); 8315 this.config.onLoadStateChange(); 8316 }); 8317 } 8318 /** 8319 * Re-read the campus model entities (e.g. after campusModelsChanged$) and 8320 * rebuild the layer configuration. Committed (undoable) edits win over the 8321 * persisted entity values so an unsaved slider change is not lost on refresh. 8322 */ 8323 refreshModelConfig() { 8324 this.config.model3DService.refreshModels(); 8325 this.applyModelConfig(); 8326 this.config.onLoadStateChange(); 8327 } 8328 /** 8329 * Select the model the transform panel edits. Keeps Model3DService and 8330 * FbxModelLayer in sync and reloads slider values from that model. 8331 */ 8332 selectModel(modelId) { 8333 if (this.config.model3DService.getActiveModelId() === modelId) { 8334 return; 8335 } 8336 this.config.model3DService.setActiveModel(modelId); 8337 if (this.fbxModelLayer) { 8338 this.fbxModelLayer.setActiveModel(modelId); 8339 const committed = this._committedTransforms.get(modelId); 8340 if (committed) { 8341 this.fbxModelLayer.setTransform(committed, modelId); 8342 this.fbxModelLayer.setGamma(committed.gamma, modelId); 8343 } 8344 } 8345 this.config.onLoadStateChange(); 8346 } 8347 /** 8348 * Toggle 3D model visibility. 8349 */ 8350 toggle3DModel(visible) { 8351 this.config.model3DService.toggle3DModel(visible); 8352 if (!this.fbxModelLayer) 8353 return; 8354 const directionalLight = this.config.mapScene.getDirectionalLight(); 8355 if (visible) { 8356 directionalLight.intensity = 1.2; 8357 directionalLight.visible = true; 8358 this.fbxModelLayer.setDirectionalLight(directionalLight); 8359 if (!this.fbxModelLayer.isModelLoaded() && !this.fbxModelLayer.isModelLoading()) { 8360 this.fbxModelLayer.load().catch(() => { 8361 this.config.model3DService.setLoadError(); 8362 this.config.onLoadStateChange(); 8363 }); 8364 } 8365 this.fbxModelLayer.show(); 8366 } else { 8367 directionalLight.intensity = 0.8; 8368 this.fbxModelLayer.hide(); 8369 } 8370 this.config.onDirty(); 8371 this.config.onLoadStateChange(); 8372 } 8373 /** 8374 * Toggle transform settings panel visibility. 8375 */ 8376 toggleSettingsPanel() { 8377 this.config.model3DService.toggleTransformPanel(); 8378 this.config.onLoadStateChange(); 8379 } 8380 /** 8381 * Update the active model's transform with real-time preview (no command created). 8382 */ 8383 updateTransform(partial) { 8384 this.config.model3DService.updateTransform(partial); 8385 const modelId = this.activeModelId(); 8386 if (this.fbxModelLayer) { 8387 this.fbxModelLayer.setTransform(this.config.model3DService.getTransform(), modelId); 8388 if (partial.gamma !== void 0) { 8389 this.fbxModelLayer.setGamma(partial.gamma, modelId); 8390 } 8391 } 8392 this.config.onLoadStateChange(); 8393 } 8394 /** 8395 * Update the active model's gamma/brightness with real-time preview. 8396 */ 8397 updateGamma(gamma) { 8398 this.config.model3DService.updateGamma(gamma); 8399 if (this.fbxModelLayer) { 8400 this.fbxModelLayer.setGamma(gamma, this.activeModelId()); 8401 } 8402 this.config.onLoadStateChange(); 8403 } 8404 /** 8405 * Commit the active model's current transform as an undoable command. 8406 * Called when user finishes editing (blur, enter, preset click). 8407 * @param changedField Optional field that was changed (for description) 8408 */ 8409 commitTransformChange(changedField) { 8410 const modelId = this.activeModelId(); 8411 if (!modelId) 8412 return; 8413 const currentTransform = this.config.model3DService.getTransform(); 8414 const committed = this.committedTransformFor(modelId); 8415 if (this.transformsEqual(committed, currentTransform)) { 8416 return; 8417 } 8418 const command = new UpdateModelTransformCommand({ 8419 modelId, 8420 previousTransform: committed, 8421 newTransform: currentTransform, 8422 changedField, 8423 onApplyTransform: (transform, targetId) => this.applyTransform(transform, targetId) 8424 }); 8425 this._committedTransforms.set(modelId, __spreadValues({}, currentTransform)); 8426 this.config.historyService.executeCommand(command); 8427 } 8428 /** 8429 * Save the active model's transform settings to backend. 8430 * @deprecated Use unified save flow via EditorHistoryService.saveChanges() instead. 8431 */ 8432 saveTransform(campusId) { 8433 return __async(this, null, function* () {
8434 try { 8435 const success = yield this.config.model3DService.saveTransform(campusId); 8436 return success; 8437 } finally { 8438 this.config.onLoadStateChange(); 8439 } 8440 }); 8441 } 8442 /** 8443 * Reset the active model's transform to default values with undo support. 8444 */ 8445 resetTransform() { 8446 const modelId = this.activeModelId(); 8447 if (!modelId || !this.fbxModelLayer) 8448 return; 8449 const currentTransform = this.config.model3DService.getTransform(); 8450 if (this.transformsEqual(currentTransform, DEFAULT_MODEL_3D_TRANSFORM)) { 8451 return; 8452 } 8453 const command = new UpdateModelTransformCommand({ 8454 modelId, 8455 previousTransform: this.committedTransformFor(modelId), 8456 newTransform: __spreadValues({}, DEFAULT_MODEL_3D_TRANSFORM), 8457 onApplyTransform: (transform, targetId) => this.applyTransform(transform, targetId) 8458 }); 8459 this._committedTransforms.set(modelId, __spreadValues({}, DEFAULT_MODEL_3D_TRANSFORM)); 8460 this.config.historyService.executeCommand(command); 8461 } 8462 /** 8463 * Close the transform panel. 8464 */ 8465 closePanel() { 8466 this.config.model3DService.closeTransformPanel(); 8467 this.config.onLoadStateChange(); 8468 } 8469 /** 8470 * Center and fit the ACTIVE model to match the given bounds with undo support. 8471 * Combines scaleToBounds and centerToBounds in a single undoable operation. 8472 * @param campusBounds The bounds to fit into (e.g. campus ground patches) 8473 * @param paddingFactor Padding as a ratio (0.1 = 10% padding, fit to 90% of bounds) 8474 */ 8475 centerFitToCampus(campusBounds, paddingFactor = 0.1) { 8476 if (!this.fbxModelLayer) 8477 return; 8478 const modelBounds = this.getActiveModelBounds(); 8479 if (!modelBounds) 8480 return; 8481 const boundsWidth = campusBounds.maxX - campusBounds.minX; 8482 const boundsHeight = campusBounds.maxY - campusBounds.minY; 8483 const boundsCenterX = (campusBounds.minX + campusBounds.maxX) / 2; 8484 const boundsCenterY = (campusBounds.minY + campusBounds.maxY) / 2; 8485 const modelWidth = modelBounds.maxX - modelBounds.minX; 8486 const modelHeight = modelBounds.maxY - modelBounds.minY; 8487 const modelCenterX = (modelBounds.minX + modelBounds.maxX) / 2; 8488 const modelCenterY = (modelBounds.minY + modelBounds.maxY) / 2; 8489 const availableWidth = boundsWidth * (1 - paddingFactor); 8490 const availableHeight = boundsHeight * (1 - paddingFactor); 8491 const scale = Math.min(availableWidth / modelWidth, availableHeight / modelHeight); 8492 const offsetX = boundsCenterX - modelCenterX * scale; 8493 const offsetY = boundsCenterY - modelCenterY * scale; 8494 const currentTransform = this.config.model3DService.getTransform(); 8495 this.executeTransformCommand(__spreadProps(__spreadValues({}, currentTransform), { 8496 offsetX, 8497 offsetY, 8498 offsetZ: 0, 8499 scale 8500 })); 8501 } 8502 /** 8503 * Center the ACTIVE model to the given bounds without changing scale. 8504 * @param bounds Target bounds to center within 8505 */ 8506 centerToBounds(bounds) { 8507 if (!this.fbxModelLayer) 8508 return; 8509 const modelBounds = this.getActiveModelBounds(); 8510 if (!modelBounds) 8511 return; 8512 const boundsCenterX = (bounds.minX + bounds.maxX) / 2; 8513 const boundsCenterY = (bounds.minY + bounds.maxY) / 2; 8514 const currentTransform = this.config.model3DService.getTransform(); 8515 const modelCenterX = (modelBounds.minX + modelBounds.maxX) / 2; 8516 const modelCenterY = (modelBounds.minY + modelBounds.maxY) / 2; 8517 const offsetX = boundsCenterX - modelCenterX * currentTransform.scale; 8518 const offsetY = boundsCenterY - modelCenterY * currentTransform.scale; 8519 this.executeTransformCommand(__spreadProps(__spreadValues({}, currentTransform), { 8520 offsetX, 8521 offsetY 8522 })); 8523 } 8524 /** 8525 * Scale the ACTIVE model to fit the given bounds without changing center position. 8526 * @param bounds Target bounds to fit within 8527 * @param paddingFactor Padding as a ratio (0.1 = 10% padding) 8528 */ 8529 scaleToBounds(bounds, paddingFactor = 0.1) { 8530 if (!this.fbxModelLayer) 8531 return; 8532 const modelBounds = this.getActiveModelBounds(); 8533 if (!modelBounds) 8534 return; 8535 const boundsWidth = bounds.maxX - bounds.minX; 8536 const boundsHeight = bounds.maxY - bounds.minY; 8537 const modelWidth = modelBounds.maxX - modelBounds.minX; 8538 const modelHeight = modelBounds.maxY - modelBounds.minY; 8539 const availableWidth = boundsWidth * (1 - paddingFactor); 8540 const availableHeight = boundsHeight * (1 - paddingFactor); 8541 const scale = Math.min(availableWidth / modelWidth, availableHeight / modelHeight); 8542 const currentTransform = this.config.model3DService.getTransform(); 8543 this.executeTransformCommand(__spreadProps(__spreadValues({}, currentTransform), { 8544 scale 8545 })); 8546 } 8547 /** 8548 * Execute a transform change on the ACTIVE model as an undoable command. 8549 */ 8550 executeTransformCommand(newTransform) { 8551 const modelId = this.activeModelId(); 8552 if (!modelId) 8553 return; 8554 const command = new UpdateModelTransformCommand({ 8555 modelId, 8556 previousTransform: this.committedTransformFor(modelId), 8557 newTransform, 8558 onApplyTransform: (transform, targetId) => this.applyTransform(transform, targetId) 8559 }); 8560 this._committedTransforms.set(modelId, __spreadValues({}, newTransform)); 8561 this.config.historyService.executeCommand(command); 8562 } 8563 /** 8564 * Toggle ruler measurement tool. Measuring works against the union of all 8565 * models (the layer group); the resulting scale applies to the active model. 8566 */ 8567 toggleRuler(active) { 8568 this.config.model3DService.toggleRuler(active); 8569 if (!this.rulerController && active && this.fbxModelLayer) { 8570 this.initRulerController(); 8571 } 8572 if (this.rulerController) { 8573 if (active) { 8574 this.config.setCameraEnabled(false); 8575 this.rulerController.activate(); 8576 } else { 8577 this.config.setCameraEnabled(true); 8578 this.rulerController.deactivate(); 8579 } 8580 } 8581 this.config.onLoadStateChange(); 8582 } 8583 /** 8584 * Clean up all resources. 8585 */ 8586 dispose() { 8587 var _a3, _b2; 8588 this.config.historyService.unregisterSaveCallback(this._saveCallback); 8589 (_a3 = this.rulerController) == null ? void 0 : _a3.dispose(); 8590 this.rulerController = null; 8591 (_b2 = this.fbxModelLayer) == null ? void 0 : _b2.dispose(); 8592 this.fbxModelLayer = null; 8593 this._committedTransforms.clear(); 8594 } 8595 // ========================================================================= 8596 // Private Methods 8597 // ========================================================================= 8598 /** Current active model id from the service (single source of truth). */ 8599 activeModelId() { 8600 return this.config.model3DService.getActiveModelId(); 8601 } 8602 /** Committed (undo baseline) transform for a model. */ 8603 committedTransformFor(modelId) { 8604 var _a3; 8605 return (_a3 = this._committedTransforms.get(modelId)) != null ? _a3 : this.config.model3DService.getTransform(); 8606 } 8607 /** 8608 * Push the service's model list into the layer, keeping the active model in 8609 * sync and preferring committed (undoable) edits over persisted values. 8610 */ 8611 applyModelConfig() { 8612 var _a3, _b2, _c, _d; 8613 if (!this.fbxModelLayer) 8614 return; 8615 const modelConfig = this.config.model3DService.buildModel3DConfig(); 8616 this.fbxModelLayer.setModelConfig(modelConfig); 8617 const activeId = this.activeModelId(); 8618 this.fbxModelLayer.setActiveModel(activeId); 8619 for (const model of (_a3 = modelConfig == null ? void 0 : modelConfig.models) != null ? _a3 : []) { 8620 const committed = this._committedTransforms.get(model.id); 8621 if (committed) { 8622 this.fbxModelLayer.setTransform(committed, model.id); 8623 } else { 8624 this.fbxModelLayer.setTransform((_b2 = model.transform) != null ? _b2 : {}, model.id); 8625 if (!this._committedTransforms.has(model.id)) { 8626 this._committedTransforms.set(model.id, __spreadValues(__spreadValues({}, DEFAULT_MODEL_3D_TRANSFORM), (_c = model.transform) != null ? _c : {})); 8627 } 8628 } 8629 } 8630 const desiredIds = new Set(((_d = modelConfig == null ? void 0 : modelConfig.models) != null ? _d : []).map((model) => model.id)); 8631 for (const id of [...this._committedTransforms.keys()]) { 8632 if (!desiredIds.has(id)) { 8633 this._committedTransforms.delete(id); 8634 } 8635 } 8636 } 8637 /** 8638 * 2D (map plane) bounds of the ACTIVE model, or null when unavailable. 8639 * Uses the per-model 3D world box so scale-to-fit only considers the model 8640 * being edited (the union bounds are used by the ruler instead). 8641 */ 8642 getActiveModelBounds() { 8643 if (!this.fbxModelLayer) 8644 return null; 8645 const modelId = this.activeModelId(); 8646 if (!modelId) 8647 return null; 8648 const box = this.fbxModelLayer.get3DBounds(modelId); 8649 if (!box) 8650 return null; 8651 return { 8652 minX: box.min.x, 8653 maxX: box.max.x, 8654 minY: box.min.y, 8655 maxY: box.max.y 8656 }; 8657 } 8658 /** 8659 * Initialize the FBX model layer for the campus' 3D models. 8660 */ 8661 initFbxModelLayer(scene, camera, renderer) { 8662 this.fbxModelLayer = new FbxModelLayer({ 8663 scene, 8664 renderer, 8665 camera, 8666 orthographicCamera: camera, 8667 perspectiveCamera: camera, 8668 markDirty: () => this.config.onDirty() 8669 }); 8670 this.fbxModelLayer.setCacheService(this.config.modelCacheService); 8671 this.fbxModelLayer.onLoad((state, progress, error) => { 8672 this.config.ngZone.run(() => { 8673 if (state === "loading") { 8674 this.config.model3DService.setLoadingState(true, progress != null ? progress : 0); 8675 } else if (state === "loaded") { 8676 this.config.model3DService.setLoadComplete(); 8677 } else if (state === "error") { 8678 this.config.model3DService.setLoadError(); 8679 } 8680 this.config.onLoadStateChange(); 8681 }); 8682 }); 8683 this.model3DWrapperGroup.position.z = -10; 8684 this.model3DWrapperGroup.add(this.fbxModelLayer.group); 8685 scene.add(this.model3DWrapperGroup); 8686 } 8687 /** 8688 * Initialize the ruler measurement controller. The raycast runs against the 8689 * whole layer group so measurements work on any model. 8690 */ 8691 initRulerController() { 8692 if (!this.fbxModelLayer) 8693 return; 8694 this.rulerController = new RulerMeasurementController({ 8695 canvas: this.config.mapScene.getRenderer().domElement, 8696 camera: this.config.mapScene.getCamera(), 8697 modelGroup: this.fbxModelLayer.group, 8698 overlayGroup: this.measurementOverlayGroup, 8699 onMeasurementComplete: (distance) => { 8700 this.config.ngZone.run(() => this.showScaleInputModal(distance)); 8701 }, 8702 onMeasurementCancel: () => { 8703 this.config.ngZone.run(() => { 8704 this.config.onLoadStateChange(); 8705 }); 8706 }, 8707 onDirty: () => this.config.onDirty() 8708 }); 8709 } 8710 /** 8711 * Show scale input modal after measurement. 8712 */ 8713 showScaleInputModal(modelDistance) { 8714 return __async(this, null, function* () { 8715 const alert = yield this.config.alertController.create({ 8716 header: "Set Real-World Length", 8717 message: `Measured distance: ${modelDistance.toFixed(1)} model units`, 8718 inputs: [ 8719 { 8720 name: "realLength", 8721 type: "number", 8722 placeholder: "Length in centimeters", 8723 min: 1, 8724 attributes: { 8725 inputmode: "numeric" 8726 } 8727 } 8728 ], 8729 buttons: [ 8730 { 8731 text: "Cancel", 8732 role: "cancel", 8733 handler: () => { 8734 var _a3; 8735 (_a3 = this.rulerController) == null ? void 0 : _a3.clearMeasurement(); 8736 } 8737 }, 8738 { 8739 text: "Apply", 8740 handler: (data) => { 8741 const realLengthCm = parseFloat(data.realLength); 8742 if (isNaN(realLengthCm) || realLengthCm <= 0) { 8743 return false;
8744 } 8745 this.applyScaleFromMeasurement(modelDistance, realLengthCm); 8746 return true; 8747 } 8748 } 8749 ] 8750 }); 8751 yield alert.present(); 8752 }); 8753 } 8754 /** 8755 * Calculate and apply scale from measurement to the ACTIVE model. 8756 */ 8757 applyScaleFromMeasurement(modelDistance, realLengthCm) { 8758 var _a3; 8759 if (!this.fbxModelLayer) 8760 return; 8761 const modelId = this.activeModelId(); 8762 if (!modelId) 8763 return; 8764 const newScale = this.config.model3DService.calculateScaleFromMeasurement(modelDistance, realLengthCm); 8765 const previousScale = this.config.model3DService.setScale(newScale); 8766 const command = new ScaleModelCommand({ 8767 fbxModelLayer: this.fbxModelLayer, 8768 modelId, 8769 previousScale, 8770 newScale, 8771 onVisualUpdate: () => this.config.onDirty(), 8772 onTransformChange: (transform, targetId) => { 8773 this.config.model3DService.updateTransform(transform); 8774 if (targetId) { 8775 const current = this._committedTransforms.get(targetId); 8776 if (current) { 8777 this._committedTransforms.set(targetId, __spreadValues(__spreadValues({}, current), transform)); 8778 } 8779 } 8780 this.config.onLoadStateChange(); 8781 } 8782 }); 8783 this.config.historyService.executeCommand(command); 8784 (_a3 = this.rulerController) == null ? void 0 : _a3.clearMeasurement(); 8785 this.toggleRuler(false); 8786 } 8787 /** 8788 * Apply a transform to a specific model on the layer and, when it is the 8789 * active model, to the service signals too (used by commands). 8790 */ 8791 applyTransform(transform, modelId) { 8792 if (this.fbxModelLayer) { 8793 this.fbxModelLayer.setTransform(transform, modelId); 8794 this.fbxModelLayer.setGamma(transform.gamma, modelId); 8795 } 8796 if (modelId === this.activeModelId()) { 8797 this.config.model3DService.updateTransform(transform); 8798 } 8799 if (modelId) { 8800 this._committedTransforms.set(modelId, __spreadValues({}, transform)); 8801 } 8802 this.config.onDirty(); 8803 this.config.onLoadStateChange(); 8804 } 8805 /** 8806 * Check if two transforms are equal. 8807 */ 8808 transformsEqual(a, b) { 8809 return a.offsetX === b.offsetX && a.offsetY === b.offsetY && a.offsetZ === b.offsetZ && a.rotationX === b.rotationX && a.rotationY === b.rotationY && a.rotationZ === b.rotationZ && a.scale === b.scale && a.gamma === b.gamma; 8810 } 8811 /** 8812 * Persist the active model's transform to backend (called during save flow). 8813 * A `false` result means there was nothing to save (no active model or a 8814 * concurrent save) - same tolerated case as before multi-model support; 8815 * unexpected failures re-throw to indicate save failure. 8816 */ 8817 persistTransform() { 8818 return __async(this, null, function* () { 8819 if (!this._campusId) 8820 return; 8821 try { 8822 yield this.config.model3DService.saveTransform(this._campusId); 8823 } catch (error) { 8824 throw error; 8825 } 8826 }); 8827 } 8828}; 8829
8830// src/app/pages/campus-map-v3/utils/tree-action-controller.ts 8831var TreeActionController = class { 8832 constructor(config) { 8833 this.config = config; 8834 } 8835 // ========================================================================= 8836 // Public Methods 8837 // ========================================================================= 8838 /** 8839 * Handle tree node click - select entity on map. 8840 */ 8841 handleNodeClick(item) { 8842 switch (item.type) { 8843 case "building": 8844 this.config.selectionService.select({ id: item.id, type: "building", name: item.name }, "replace"); 8845 this.config.onCenterOnEntity(item.id); 8846 break; 8847 case "floor": 8848 if (item.level !== void 0) { 8849 this.config.onLevelSelected(item.level); 8850 } 8851 this.config.selectionService.select({ id: item.id, type: "floor", name: item.name }, "replace"); 8852 this.config.onCenterOnEntity(item.id); 8853 break; 8854 case "room": 8855 this.selectRoomOnMap(item.id); 8856 break; 8857 case "node": 8858 this.selectNodeOnMap(item.id); 8859 break; 8860 } 8861 this.config.viewState.autoCollapseSidebarOnMobile(); 8862 } 8863 /** 8864 * Show context menu for tree node. 8865 */ 8866 showContextMenu(item) { 8867 return __async(this, null, function* () { 8868 const buttons = []; 8869 if (item.type === "building") { 8870 buttons.push({ 8871 text: "Edit Building", 8872 handler: () => this.config.onOpenBuildingPanel(item.id) 8873 }); 8874 } else if (item.type === "floor") { 8875 buttons.push({ 8876 text: "Go to Level", 8877 handler: () => { 8878 if (item.level !== void 0) 8879 this.config.onLevelSelected(item.level); 8880 } 8881 }); 8882 buttons.push({ 8883 text: "Edit Floor", 8884 handler: () => this.config.onOpenFloorPanel(item.id) 8885 }); 8886 buttons.push({ 8887 text: "Add Room", 8888 handler: () => this.createRoomForFloor(item.id) 8889 }); 8890 } else if (item.type === "room") { 8891 buttons.push({ 8892 text: "Edit Room", 8893 handler: () => { 8894 this.selectRoomOnMap(item.id); 8895 this.config.onOpenRoomPanel(item.id, false); 8896 } 8897 }); 8898 } else if (item.type === "node") { 8899 buttons.push({ 8900 text: "Edit Node", 8901 handler: () => { 8902 this.selectNodeOnMap(item.id); 8903 this.config.onOpenNodePanel(item.id, false); 8904 } 8905 }); 8906 } 8907 buttons.push({ text: "Cancel", role: "cancel" }); 8908 const alert = yield this.config.alertController.create({ 8909 header: item.name, 8910 buttons 8911 }); 8912 yield alert.present(); 8913 }); 8914 } 8915 /** 8916 * Create a new building model for the current campus. 8917 * Returns the model for display in the edit panel. 8918 */ 8919 createBuildingModel() { 8920 const campus = this.config.campusData.getCampus(); 8921 if (!campus) 8922 return null; 8923 const newBuilding = Object.assign(new Building(), { 8924 id: crypto.randomUUID(), 8925 campusId: campus.entity.id, 8926 name: null, 8927 address: null, 8928 isOutdoorArea: false, 8929 initialRotation: 0, 8930 isDeleted: false, 8931 isChanged: true, 8932 version: 0 8933 }); 8934 const tempModel = new BuildingModel(newBuilding); 8935 tempModel.campus = campus; 8936 return tempModel; 8937 } 8938 /** 8939 * Create a new floor model for a building. 8940 * Returns the model for display in the edit panel. 8941 */ 8942 createFloorForBuilding(buildingId) { 8943 var _a3; 8944 const buildingModel = this.config.database.Models.get(buildingId); 8945 if (!buildingModel || buildingModel.type !== EntityType.BUILDING) 8946 return null; 8947 const building = buildingModel; 8948 const existingLevels = building.floors.filter((f) => !f.entity.isDeleted).map((f) => { 8949 var _a4; 8950 return (_a4 = f.entity.level) != null ? _a4 : 1; 8951 }); 8952 const suggestedLevel = existingLevels.length > 0 ? Math.max(...existingLevels) + 1 : 1; 8953 const newFloor = Object.assign(new Floor(), { 8954 id: crypto.randomUUID(), 8955 campusId: (_a3 = building.campus) == null ? void 0 : _a3.entity.id, 8956 buildingId, 8957 name: null, 8958 level: suggestedLevel, 8959 isDeleted: false, 8960 isChanged: true, 8961 version: 0 8962 }); 8963 const tempModel = new FloorModel(newFloor); 8964 tempModel.building = building; 8965 tempModel.campus = building.campus; 8966 return tempModel; 8967 } 8968 /** 8969 * Create a new room model for a floor. 8970 * Returns the model for display in the edit panel. 8971 */ 8972 createRoomForFloor(floorId) { 8973 var _a3, _b2; 8974 const floorModel = this.config.database.Models.get(floorId); 8975 if (!floorModel || floorModel.type !== EntityType.FLOOR) 8976 return; 8977 const floor = floorModel; 8978 const newRoom = Object.assign(new Room(), { 8979 id: crypto.randomUUID(), 8980 campusId: (_a3 = floor.campus) == null ? void 0 : _a3.entity.id, 8981 buildingId: (_b2 = floor.building) == null ? void 0 : _b2.entity.id, 8982 floorId, 8983 name: null, 8984 number: null, 8985 description: null, 8986 roomType: RoomType.Regular, 8987 hideLabel: false, 8988 isDeleted: false, 8989 isChanged: true, 8990 version: 0 8991 }); 8992 const tempModel = new RoomModel(newRoom); 8993 tempModel.floor = floor; 8994 tempModel.building = floor.building; 8995 tempModel.campus = floor.campus;
8996 this.config.entityEdit.openNewRoomPanel(tempModel); 8997 } 8998 // ========================================================================= 8999 // Private Helpers 9000 // ========================================================================= 9001 /** 9002 * Select a room on the map, switching to its floor level if needed. 9003 */ 9004 selectRoomOnMap(roomId) { 9005 var _a3; 9006 const model = this.config.database.Models.get(roomId); 9007 if (!model || model.type !== EntityType.ROOM) 9008 return; 9009 const roomModel = model; 9010 const floor = roomModel.floor; 9011 if ((floor == null ? void 0 : floor.entity.level) !== void 0 && floor.entity.level !== null) { 9012 this.config.onLevelSelected(floor.entity.level); 9013 } 9014 this.config.selectionService.select({ id: roomId, type: "room", name: (_a3 = roomModel.entity.name) != null ? _a3 : void 0 }, "replace"); 9015 this.config.onCenterOnEntity(roomId); 9016 } 9017 /** 9018 * Select a node on the map, switching to its floor level if needed. 9019 */ 9020 selectNodeOnMap(nodeId) { 9021 var _a3; 9022 const model = this.config.database.Models.get(nodeId); 9023 if (!model || model.type !== EntityType.NODE) 9024 return; 9025 const nodeModel = model; 9026 const floor = nodeModel.floor; 9027 if ((floor == null ? void 0 : floor.entity.level) !== void 0 && floor.entity.level !== null) { 9028 this.config.onLevelSelected(floor.entity.level); 9029 } else if (nodeModel.entity.type === NodeType.Outdoor) { 9030 this.config.onLevelSelected(1); 9031 } 9032 this.config.selectionService.select({ id: nodeId, type: "node", name: (_a3 = nodeModel.entity.tagName) != null ? _a3 : void 0 }, "replace"); 9033 this.config.onCenterOnEntity(nodeId); 9034 } 9035}; 9036
9037// src/app/pages/campus-map-v3/utils/edit-panel-controller.ts 9038var EditPanelController = class { 9039 constructor(config) { 9040 this.config = config; 9041 } 9042 // ========================================================================= 9043 // Node Edit Panel 9044 // ========================================================================= 9045 /** 9046 * Handle node property change from edit panel (live preview). 9047 */ 9048 handleNodeChange(changes) { 9049 var _a3, _b2, _c, _d, _e, _f, _g, _h; 9050 const { entityEdit, onMoveNodeMesh, onMarkForCheck } = this.config; 9051 const editingNode = entityEdit.editingNode(); 9052 if (!editingNode) 9053 return; 9054 entityEdit.recordChange(changes); 9055 if (changes.locationX !== void 0 || changes.locationY !== void 0) { 9056 const entity = editingNode.entity; 9057 const pendingChanges = entityEdit.getPendingChanges(); 9058 const newX = (_c = (_b2 = (_a3 = changes.locationX) != null ? _a3 : pendingChanges["locationX"]) != null ? _b2 : entity.locationX) != null ? _c : 0; 9059 const newY = (_f = (_e = (_d = changes.locationY) != null ? _d : pendingChanges["locationY"]) != null ? _e : entity.locationY) != null ? _f : 0; 9060 const oldX = (_g = entity.locationX) != null ? _g : 0; 9061 const oldY = (_h = entity.locationY) != null ? _h : 0; 9062 onMoveNodeMesh(editingNode.entity.id, newX - oldX, newY - oldY); 9063 } 9064 onMarkForCheck(); 9065 } 9066 /** 9067 * Handle apply button from node edit panel. 9068 */ 9069 handleNodeApply() { 9070 this.config.entityEdit.apply(); 9071 this.config.onMarkForCheck(); 9072 } 9073 /** 9074 * Handle cancel button from node edit panel. 9075 */ 9076 handleNodeCancel() { 9077 const { entityEdit, onRefreshView, onMarkForCheck } = this.config; 9078 if (entityEdit.editingNode() && entityEdit.hasPendingChanges()) { 9079 onRefreshView(); 9080 } 9081 entityEdit.cancel(); 9082 onMarkForCheck(); 9083 } 9084 // ========================================================================= 9085 // Floor Edit Panel 9086 // ========================================================================= 9087 /** 9088 * Handle floor property change from edit panel. 9089 */ 9090 handleFloorChange(changes) { 9091 this.config.entityEdit.recordChange(changes); 9092 this.config.onMarkForCheck(); 9093 } 9094 /** 9095 * Handle apply button from floor edit panel. 9096 */ 9097 handleFloorApply() { 9098 this.config.entityEdit.apply(); 9099 this.config.onMarkForCheck(); 9100 } 9101 /** 9102 * Handle cancel button from floor edit panel. 9103 */ 9104 handleFloorCancel() { 9105 this.config.entityEdit.cancel(); 9106 this.config.onMarkForCheck(); 9107 } 9108 // ========================================================================= 9109 // Building Edit Panel 9110 // ========================================================================= 9111 /** 9112 * Handle building property change from edit panel. 9113 */ 9114 handleBuildingChange(changes) { 9115 this.config.entityEdit.recordChange(changes); 9116 this.config.onMarkForCheck(); 9117 } 9118 /** 9119 * Handle apply button from building edit panel. 9120 */ 9121 handleBuildingApply() { 9122 this.config.entityEdit.apply(); 9123 this.config.onMarkForCheck(); 9124 } 9125 /** 9126 * Handle cancel button from building edit panel. 9127 */ 9128 handleBuildingCancel() { 9129 this.config.entityEdit.cancel(); 9130 this.config.onMarkForCheck(); 9131 } 9132 // ========================================================================= 9133 // Room Edit Panel 9134 // ========================================================================= 9135 /** 9136 * Handle room property change from edit panel. 9137 */ 9138 handleRoomChange(changes) { 9139 this.config.entityEdit.recordChange(changes); 9140 this.config.onMarkForCheck(); 9141 } 9142 /** 9143 * Handle apply button from room edit panel. 9144 */ 9145 handleRoomApply() { 9146 this.config.entityEdit.apply(); 9147 this.config.onMarkForCheck(); 9148 } 9149 /** 9150 * Handle cancel button from room edit panel. 9151 */ 9152 handleRoomCancel() { 9153 this.config.entityEdit.cancel(); 9154 this.config.onMarkForCheck(); 9155 } 9156 // ========================================================================= 9157 // Ground Patch Edit Panel 9158 // ========================================================================= 9159 /** 9160 * Handle ground patch property change from edit panel. 9161 */
9162 handleGroundPatchChange(changes) { 9163 this.config.entityEdit.recordChange(changes); 9164 this.config.onMarkForCheck(); 9165 } 9166 /** 9167 * Handle apply button from ground patch edit panel. 9168 */ 9169 handleGroundPatchApply() { 9170 this.config.entityEdit.apply(); 9171 this.config.onMarkForCheck(); 9172 } 9173 /** 9174 * Handle cancel button from ground patch edit panel. 9175 */ 9176 handleGroundPatchCancel() { 9177 this.config.entityEdit.cancel(); 9178 this.config.onMarkForCheck(); 9179 } 9180 // ========================================================================= 9181 // Person Management (Room Panel) 9182 // ========================================================================= 9183 /** 9184 * Handle assigning an existing person to the room. 9185 */ 9186 handlePersonAssigned(personId) { 9187 const { entityEdit, database, onMarkForCheck } = this.config; 9188 const room = entityEdit.editingRoom(); 9189 if (!room) 9190 return; 9191 const personRoom = Object.assign(new PersonRoom(), { 9192 id: crypto.randomUUID(), 9193 personId, 9194 roomId: room.entity.id, 9195 isDeleted: false, 9196 isChanged: true, 9197 version: 0 9198 }); 9199 database.addEntity(personRoom); 9200 database.fixModelRelations(room); 9201 onMarkForCheck(); 9202 } 9203 /** 9204 * Handle removing a person assignment from the room. 9205 */ 9206 handlePersonRemoved(personRoomId) { 9207 const { entityEdit, database, onMarkForCheck } = this.config; 9208 const model = database.Models.get(personRoomId); 9209 if (model) { 9210 database.deleteEntity(model.entity); 9211 } 9212 const room = entityEdit.editingRoom(); 9213 if (room) { 9214 database.fixModelRelations(room); 9215 } 9216 onMarkForCheck(); 9217 } 9218 /** 9219 * Handle creating a new person and assigning to the room. 9220 */ 9221 handlePersonCreated(name) { 9222 const { entityEdit, database, campusData, onMarkForCheck } = this.config; 9223 const room = entityEdit.editingRoom(); 9224 const campus = campusData.getCampus(); 9225 if (!room || !campus) 9226 return; 9227 const person = Object.assign(new Person(), { 9228 id: crypto.randomUUID(), 9229 campusId: campus.entity.id, 9230 name, 9231 isDeleted: false, 9232 isChanged: true, 9233 version: 0 9234 }); 9235 database.addEntity(person); 9236 const personRoom = Object.assign(new PersonRoom(), { 9237 id: crypto.randomUUID(), 9238 personId: person.id, 9239 roomId: room.entity.id, 9240 isDeleted: false, 9241 isChanged: true, 9242 version: 0 9243 }); 9244 database.addEntity(personRoom); 9245 database.fixModelRelations(room); 9246 onMarkForCheck(); 9247 } 9248}; 9249
9250// src/app/pages/campus-map-v3/utils/gesture-controller.ts 9251var GestureController = class { 9252 constructor(config) { 9253 this.sceneRotation = 0; 9254 this.isRotating = false; 9255 this.lastMouseX = 0; 9256 this.rotationCenter = { x: 0, y: 0 }; 9257 this.lastTouchDistance = 0; 9258 this.lastTouchAngle = 0; 9259 this.lastTouchMidpoint = { x: 0, y: 0 }; 9260 this.canvas = config.canvas; 9261 this.camera = config.camera; 9262 this.controls = config.controls; 9263 this.groups = config.groups; 9264 this.onDirty = config.onDirty; 9265 this.onRotate = config.onRotate; 9266 this.boundContextMenu = (e) => e.preventDefault(); 9267 this.boundMouseDown = this.handleMouseDown.bind(this); 9268 this.boundMouseMove = this.handleMouseMove.bind(this); 9269 this.boundMouseUp = this.handleMouseUp.bind(this); 9270 this.boundMouseLeave = () => { 9271 this.isRotating = false; 9272 }; 9273 this.boundTouchStart = this.handleTouchStart.bind(this); 9274 this.boundTouchMove = this.handleTouchMove.bind(this); 9275 this.attach(); 9276 } 9277 attach() { 9278 this.canvas.addEventListener("contextmenu", this.boundContextMenu); 9279 this.canvas.addEventListener("mousedown", this.boundMouseDown); 9280 this.canvas.addEventListener("mousemove", this.boundMouseMove); 9281 this.canvas.addEventListener("mouseup", this.boundMouseUp); 9282 this.canvas.addEventListener("mouseleave", this.boundMouseLeave); 9283 this.canvas.addEventListener("touchstart", this.boundTouchStart, { passive: false }); 9284 this.canvas.addEventListener("touchmove", this.boundTouchMove, { passive: false }); 9285 } 9286 dispose() { 9287 this.canvas.removeEventListener("contextmenu", this.boundContextMenu); 9288 this.canvas.removeEventListener("mousedown", this.boundMouseDown); 9289 this.canvas.removeEventListener("mousemove", this.boundMouseMove); 9290 this.canvas.removeEventListener("mouseup", this.boundMouseUp); 9291 this.canvas.removeEventListener("mouseleave", this.boundMouseLeave); 9292 this.canvas.removeEventListener("touchstart", this.boundTouchStart); 9293 this.canvas.removeEventListener("touchmove", this.boundTouchMove); 9294 } 9295 // === Desktop: Right-click rotation === 9296 handleMouseDown(e) { 9297 if (e.button === 2) { 9298 this.isRotating = true; 9299 this.lastMouseX = e.clientX; 9300 this.rotationCenter = this.screenToWorld(e.clientX, e.clientY); 9301 } 9302 } 9303 handleMouseMove(e) { 9304 if (this.isRotating) { 9305 const deltaX = e.clientX - this.lastMouseX; 9306 const deltaAngle = deltaX * 5e-3; 9307 this.rotateAroundPoint(deltaAngle, this.rotationCenter.x, this.rotationCenter.y); 9308 this.lastMouseX = e.clientX; 9309 } 9310 } 9311 handleMouseUp(e) { 9312 if (e.button === 2) { 9313 this.isRotating = false; 9314 } 9315 } 9316 // === Mobile: 2-finger pan + zoom + rotate === 9317 handleTouchStart(e) { 9318 if (e.touches.length === 2) { 9319 e.preventDefault(); 9320 const t0 = e.touches[0]; 9321 const t1 = e.touches[1]; 9322 this.lastTouchDistance = this.getTouchDistance(t0, t1); 9323 this.lastTouchAngle = this.getTouchAngle(t0, t1); 9324 this.lastTouchMidpoint = this.getTouchMidpoint(t0, t1); 9325 } 9326 } 9327 handleTouchMove(e) { 9328 if (e.touches.length === 2) { 9329 e.preventDefault(); 9330 const t0 = e.touches[0]; 9331 const t1 = e.touches[1]; 9332 const currentDistance = this.getTouchDistance(t0, t1); 9333 const currentAngle = this.getTouchAngle(t0, t1); 9334 const currentMidpoint = this.getTouchMidpoint(t0, t1); 9335 if (this.lastTouchDistance > 0) { 9336 const scale = currentDistance / this.lastTouchDistance; 9337 const currentZoom = this.controls.camera.zoom; 9338 this.controls.zoomTo(currentZoom * scale, false); 9339 } 9340 const angleDelta = currentAngle - this.lastTouchAngle; 9341 if (Math.abs(angleDelta) > 1e-3) { 9342 const worldMidpoint = this.screenToWorld(currentMidpoint.x, currentMidpoint.y); 9343 this.rotateAroundPoint(angleDelta, worldMidpoint.x, worldMidpoint.y); 9344 } 9345 const panX = currentMidpoint.x - this.lastTouchMidpoint.x; 9346 const panY = currentMidpoint.y - this.lastTouchMidpoint.y; 9347 const panSpeed = 1 / this.camera.zoom; 9348 this.camera.position.x -= panX * panSpeed; 9349 this.camera.position.y += panY * panSpeed; 9350 this.lastTouchDistance = currentDistance; 9351 this.lastTouchAngle = currentAngle; 9352 this.lastTouchMidpoint = currentMidpoint; 9353 this.onDirty(); 9354 } 9355 } 9356 // === Helpers === 9357 getTouchDistance(t0, t1) { 9358 const dx = t1.clientX - t0.clientX; 9359 const dy = t1.clientY - t0.clientY; 9360 return Math.sqrt(dx * dx + dy * dy); 9361 } 9362 getTouchAngle(t0, t1) { 9363 return Math.atan2(t1.clientY - t0.clientY, t1.clientX - t0.clientX); 9364 } 9365 getTouchMidpoint(t0, t1) { 9366 return { 9367 x: (t0.clientX + t1.clientX) / 2, 9368 y: (t0.clientY + t1.clientY) / 2 9369 }; 9370 } 9371 screenToWorld(screenX, screenY) { 9372 const rect = this.canvas.getBoundingClientRect(); 9373 const ndcX = (screenX - rect.left) / rect.width * 2 - 1; 9374 const ndcY = -((screenY - rect.top) / rect.height) * 2 + 1; 9375 const halfWidth = (this.camera.right - this.camera.left) / 2; 9376 const halfHeight = (this.camera.top - this.camera.bottom) / 2; 9377 return { 9378 x: this.camera.position.x + ndcX * halfWidth / this.camera.zoom, 9379 y: this.camera.position.y + ndcY * halfHeight / this.camera.zoom 9380 }; 9381 }
9382 rotateAroundPoint(deltaAngle, centerX, centerY) { 9383 var _a3; 9384 const cos = Math.cos(deltaAngle); 9385 const sin = Math.sin(deltaAngle); 9386 for (const group of this.groups) { 9387 const dx = group.position.x - centerX; 9388 const dy = group.position.y - centerY; 9389 group.position.x = centerX + dx * cos - dy * sin; 9390 group.position.y = centerY + dx * sin + dy * cos; 9391 } 9392 this.sceneRotation += deltaAngle; 9393 for (const group of this.groups) { 9394 group.rotation.z = this.sceneRotation; 9395 } 9396 (_a3 = this.onRotate) == null ? void 0 : _a3.call(this, this.sceneRotation); 9397 this.onDirty(); 9398 } 9399}; 9400
9401// src/app/pages/campus-map-v3/utils/selection-controller.ts 9402var SelectionController = class { 9403 constructor(config) { 9404 this.raycaster = new Raycaster(); 9405 this.pointer = new Vector2(); 9406 this.tapState = null; 9407 this.longPressTimer = null; 9408 this.longPressTriggered = false; 9409 this.longPressClientX = 0; 9410 this.longPressClientY = 0; 9411 this.TAP_DISTANCE_THRESHOLD = 10; 9412 this.TAP_TIME_THRESHOLD = 300; 9413 this.LONG_PRESS_DURATION = 500; 9414 this.canvas = config.canvas; 9415 this.camera = config.camera; 9416 this.selectableGroups = config.selectableGroups; 9417 this.onSelect = config.onSelect; 9418 this.onDirty = config.onDirty; 9419 this.boundMouseDown = this.handleMouseDown.bind(this); 9420 this.boundMouseUp = this.handleMouseUp.bind(this); 9421 this.boundMouseMove = this.handleMouseMove.bind(this); 9422 this.boundTouchStart = this.handleTouchStart.bind(this); 9423 this.boundTouchEnd = this.handleTouchEnd.bind(this); 9424 this.boundTouchMove = this.handleTouchMove.bind(this); 9425 this.attach(); 9426 } 9427 attach() { 9428 this.canvas.addEventListener("mousedown", this.boundMouseDown); 9429 this.canvas.addEventListener("mouseup", this.boundMouseUp); 9430 this.canvas.addEventListener("mousemove", this.boundMouseMove); 9431 this.canvas.addEventListener("touchstart", this.boundTouchStart, { passive: false }); 9432 this.canvas.addEventListener("touchend", this.boundTouchEnd, { passive: false }); 9433 this.canvas.addEventListener("touchmove", this.boundTouchMove, { passive: false }); 9434 } 9435 dispose() { 9436 this.clearLongPressTimer(); 9437 this.canvas.removeEventListener("mousedown", this.boundMouseDown); 9438 this.canvas.removeEventListener("mouseup", this.boundMouseUp); 9439 this.canvas.removeEventListener("mousemove", this.boundMouseMove); 9440 this.canvas.removeEventListener("touchstart", this.boundTouchStart); 9441 this.canvas.removeEventListener("touchend", this.boundTouchEnd); 9442 this.canvas.removeEventListener("touchmove", this.boundTouchMove); 9443 } 9444 // ========================================================================= 9445 // Mouse Event Handlers 9446 // ========================================================================= 9447 handleMouseDown(e) { 9448 if (e.button !== 0) 9449 return; 9450 this.tapState = { 9451 startX: e.clientX, 9452 startY: e.clientY, 9453 startTime: Date.now() 9454 }; 9455 } 9456 handleMouseUp(e) { 9457 if (e.button !== 0 || !this.tapState) { 9458 this.tapState = null; 9459 return; 9460 } 9461 const dx = e.clientX - this.tapState.startX; 9462 const dy = e.clientY - this.tapState.startY; 9463 const distance = Math.sqrt(dx * dx + dy * dy); 9464 const duration = Date.now() - this.tapState.startTime; 9465 if (distance < this.TAP_DISTANCE_THRESHOLD && duration < this.TAP_TIME_THRESHOLD) { 9466 const mode = e.ctrlKey || e.metaKey ? "toggle" : "replace"; 9467 this.handleTap(e.clientX, e.clientY, mode); 9468 } 9469 this.tapState = null; 9470 } 9471 handleMouseMove(e) { 9472 if (!this.tapState) 9473 return; 9474 const dx = e.clientX - this.tapState.startX; 9475 const dy = e.clientY - this.tapState.startY; 9476 const distance = Math.sqrt(dx * dx + dy * dy); 9477 if (distance > this.TAP_DISTANCE_THRESHOLD) { 9478 this.tapState = null; 9479 } 9480 } 9481 // ========================================================================= 9482 // Touch Event Handlers 9483 // ========================================================================= 9484 handleTouchStart(e) { 9485 if (e.touches.length !== 1) { 9486 this.clearLongPressTimer(); 9487 this.tapState = null; 9488 return; 9489 } 9490 const touch = e.touches[0]; 9491 this.tapState = { 9492 startX: touch.clientX, 9493 startY: touch.clientY, 9494 startTime: Date.now() 9495 }; 9496 this.longPressTriggered = false; 9497 this.longPressClientX = touch.clientX; 9498 this.longPressClientY = touch.clientY; 9499 this.startLongPressTimer(); 9500 } 9501 handleTouchEnd(e) { 9502 this.clearLongPressTimer(); 9503 if (!this.tapState || e.changedTouches.length !== 1) { 9504 this.tapState = null; 9505 return; 9506 } 9507 if (this.longPressTriggered) { 9508 this.tapState = null; 9509 return; 9510 } 9511 const touch = e.changedTouches[0]; 9512 const dx = touch.clientX - this.tapState.startX; 9513 const dy = touch.clientY - this.tapState.startY; 9514 const distance = Math.sqrt(dx * dx + dy * dy); 9515 const duration = Date.now() - this.tapState.startTime; 9516 if (distance < this.TAP_DISTANCE_THRESHOLD && duration < this.TAP_TIME_THRESHOLD) { 9517 this.handleTap(touch.clientX, touch.clientY, "replace"); 9518 } 9519 this.tapState = null; 9520 } 9521 handleTouchMove(e) { 9522 if (!this.tapState || e.touches.length !== 1) { 9523 this.clearLongPressTimer(); 9524 this.tapState = null; 9525 return; 9526 } 9527 const touch = e.touches[0]; 9528 const dx = touch.clientX - this.tapState.startX; 9529 const dy = touch.clientY - this.tapState.startY; 9530 const distance = Math.sqrt(dx * dx + dy * dy); 9531 if (distance > this.TAP_DISTANCE_THRESHOLD) { 9532 this.clearLongPressTimer(); 9533 this.tapState = null; 9534 } 9535 } 9536 // ========================================================================= 9537 // Long-Press Timer 9538 // ========================================================================= 9539 startLongPressTimer() { 9540 this.longPressTimer = setTimeout(() => { 9541 this.longPressTriggered = true; 9542 this.handleTap(this.longPressClientX, this.longPressClientY, "toggle"); 9543 if (navigator.vibrate) { 9544 navigator.vibrate(50); 9545 } 9546 }, this.LONG_PRESS_DURATION); 9547 } 9548 clearLongPressTimer() { 9549 if (this.longPressTimer !== null) { 9550 clearTimeout(this.longPressTimer); 9551 this.longPressTimer = null; 9552 } 9553 } 9554 // ========================================================================= 9555 // Tap Handling & Raycasting 9556 // ========================================================================= 9557 handleTap(clientX, clientY, mode) { 9558 const entity = this.raycast(clientX, clientY); 9559 this.onSelect(entity, mode); 9560 } 9561 raycast(clientX, clientY) { 9562 const rect = this.canvas.getBoundingClientRect();
9563 this.pointer.x = (clientX - rect.left) / rect.width * 2 - 1; 9564 this.pointer.y = -((clientY - rect.top) / rect.height) * 2 + 1; 9565 this.raycaster.setFromCamera(this.pointer, this.camera); 9566 const allObjects = []; 9567 for (const group of this.selectableGroups) { 9568 if (group.visible) { 9569 allObjects.push(...group.children); 9570 } 9571 } 9572 const intersects = this.raycaster.intersectObjects(allObjects, true); 9573 if (intersects.length > 0) { 9574 for (const intersection of intersects) { 9575 const entity = this.resolveEntity(intersection); 9576 if (entity) { 9577 return entity; 9578 } 9579 } 9580 } 9581 return null; 9582 } 9583 resolveEntity(intersection) { 9584 let object = intersection.object; 9585 while (object) { 9586 const userData = object.userData; 9587 if (userData) { 9588 if (userData["entityType"] && userData["entityId"]) { 9589 return { 9590 id: userData["entityId"], 9591 type: userData["entityType"], 9592 name: userData["entityName"] 9593 }; 9594 } 9595 if (userData["roomId"]) { 9596 return { 9597 id: userData["roomId"], 9598 type: "room", 9599 name: userData["roomName"] 9600 }; 9601 } 9602 if (userData["nodeId"]) { 9603 return { 9604 id: userData["nodeId"], 9605 type: "node", 9606 name: userData["nodeName"] 9607 }; 9608 } 9609 if (userData["floorId"]) { 9610 return { 9611 id: userData["floorId"], 9612 type: "floor", 9613 name: userData["floorName"] 9614 }; 9615 } 9616 if (userData["patchId"]) { 9617 return { 9618 id: userData["patchId"], 9619 type: "groundPatch", 9620 name: void 0 9621 }; 9622 } 9623 if (userData["connectionId"]) { 9624 return { 9625 id: userData["connectionId"], 9626 type: "nodeConnection", 9627 name: void 0 9628 }; 9629 } 9630 } 9631 object = object.parent; 9632 } 9633 return null; 9634 } 9635}; 9636
9637// src/app/pages/campus-map-v3/utils/coordinate-utils.ts 9638function getSceneTransform(groups) { 9639 if (groups.length > 0) { 9640 const group = groups[0]; 9641 return { 9642 rotation: group.rotation.z, 9643 position: { x: group.position.x, y: group.position.y } 9644 }; 9645 } 9646 return { rotation: 0, position: { x: 0, y: 0 } }; 9647} 9648function screenToWorld(clientX, clientY, canvas, camera) { 9649 const rect = canvas.getBoundingClientRect(); 9650 const ndcX = (clientX - rect.left) / rect.width * 2 - 1; 9651 const ndcY = -((clientY - rect.top) / rect.height) * 2 + 1; 9652 const halfWidth = (camera.right - camera.left) / 2 / camera.zoom; 9653 const halfHeight = (camera.top - camera.bottom) / 2 / camera.zoom; 9654 const worldX = camera.position.x + ndcX * halfWidth; 9655 const worldY = camera.position.y + ndcY * halfHeight; 9656 return { x: worldX, y: worldY }; 9657} 9658function worldToScreen(worldX, worldY, canvas, camera) { 9659 const rect = canvas.getBoundingClientRect(); 9660 const halfWidth = (camera.right - camera.left) / 2 / camera.zoom; 9661 const halfHeight = (camera.top - camera.bottom) / 2 / camera.zoom; 9662 const ndcX = (worldX - camera.position.x) / halfWidth; 9663 const ndcY = (worldY - camera.position.y) / halfHeight; 9664 const screenX = (ndcX + 1) / 2 * rect.width; 9665 const screenY = (-ndcY + 1) / 2 * rect.height; 9666 return { x: screenX, y: screenY }; 9667} 9668function worldToLocal(worldPoint, transform) { 9669 const { rotation, position } = transform; 9670 if (rotation === 0 && position.x === 0 && position.y === 0) { 9671 return { x: worldPoint.x, y: worldPoint.y }; 9672 } 9673 const dx = worldPoint.x - position.x; 9674 const dy = worldPoint.y - position.y; 9675 const cos = Math.cos(-rotation); 9676 const sin = Math.sin(-rotation); 9677 const localX = dx * cos - dy * sin; 9678 const localY = dx * sin + dy * cos; 9679 return { x: localX, y: localY }; 9680} 9681function localToWorld(localPoint, transform) { 9682 const { rotation, position } = transform; 9683 if (rotation === 0 && position.x === 0 && position.y === 0) { 9684 return { x: localPoint.x, y: localPoint.y }; 9685 } 9686 const cos = Math.cos(rotation); 9687 const sin = Math.sin(rotation); 9688 const rotatedX = localPoint.x * cos - localPoint.y * sin; 9689 const rotatedY = localPoint.x * sin + localPoint.y * cos; 9690 const worldX = rotatedX + position.x; 9691 const worldY = rotatedY + position.y; 9692 return { x: worldX, y: worldY }; 9693} 9694function screenToLocal(clientX, clientY, canvas, camera, groups) { 9695 const worldPoint = screenToWorld(clientX, clientY, canvas, camera); 9696 const transform = getSceneTransform(groups); 9697 const localPoint = worldToLocal(worldPoint, transform); 9698 return { 9699 x: Math.round(localPoint.x), 9700 y: Math.round(localPoint.y) 9701 }; 9702} 9703
9704// src/app/pages/campus-map-v3/utils/marquee-selection-controller.ts 9705var MarqueeSelectionController = class { 9706 constructor(config) { 9707 this.enabled = true; 9708 this.isDragging = false; 9709 this.dragStart = null; 9710 this.currentRect = null; 9711 this.MIN_DRAG_DISTANCE = 5; 9712 this.canvas = config.canvas; 9713 this.camera = config.camera; 9714 this.selectableGroups = config.selectableGroups; 9715 this.onMarqueeStart = config.onMarqueeStart; 9716 this.onMarqueeEnd = config.onMarqueeEnd; 9717 this.onMarqueeUpdate = config.onMarqueeUpdate; 9718 this.onMarqueeComplete = config.onMarqueeComplete; 9719 this.isEntityVisible = config.isEntityVisible; 9720 this.boundMouseDown = this.handleMouseDown.bind(this); 9721 this.boundMouseMove = this.handleMouseMove.bind(this); 9722 this.boundMouseUp = this.handleMouseUp.bind(this); 9723 this.attach(); 9724 } 9725 attach() { 9726 this.canvas.addEventListener("mousedown", this.boundMouseDown); 9727 window.addEventListener("mousemove", this.boundMouseMove); 9728 window.addEventListener("mouseup", this.boundMouseUp); 9729 } 9730 dispose() { 9731 this.canvas.removeEventListener("mousedown", this.boundMouseDown); 9732 window.removeEventListener("mousemove", this.boundMouseMove); 9733 window.removeEventListener("mouseup", this.boundMouseUp); 9734 } 9735 /** 9736 * Enable the controller to handle events. 9737 */ 9738 enable() { 9739 this.enabled = true; 9740 } 9741 /** 9742 * Disable the controller and reset any partial drag state. 9743 */ 9744 disable() { 9745 this.enabled = false; 9746 this.isDragging = false; 9747 this.dragStart = null; 9748 this.currentRect = null; 9749 } 9750 // ========================================================================= 9751 // Mouse Handlers 9752 // ========================================================================= 9753 handleMouseDown(e) { 9754 if (!this.enabled) 9755 return; 9756 if (e.button !== 0 || !e.shiftKey) 9757 return; 9758 e.preventDefault(); 9759 e.stopPropagation(); 9760 this.onMarqueeStart(); 9761 const rect = this.canvas.getBoundingClientRect(); 9762 this.dragStart = { 9763 x: e.clientX - rect.left, 9764 y: e.clientY - rect.top 9765 }; 9766 this.isDragging = false; 9767 } 9768 handleMouseMove(e) { 9769 if (!this.dragStart) 9770 return; 9771 const rect = this.canvas.getBoundingClientRect(); 9772 const currentX = e.clientX - rect.left; 9773 const currentY = e.clientY - rect.top; 9774 const dx = currentX - this.dragStart.x; 9775 const dy = currentY - this.dragStart.y; 9776 const distance = Math.sqrt(dx * dx + dy * dy); 9777 if (!this.isDragging && distance >= this.MIN_DRAG_DISTANCE) { 9778 this.isDragging = true; 9779 } 9780 if (this.isDragging) { 9781 this.currentRect = { 9782 startX: this.dragStart.x, 9783 startY: this.dragStart.y, 9784 endX: currentX, 9785 endY: currentY 9786 }; 9787 this.onMarqueeUpdate(this.screenRectToMarqueeRect(this.currentRect)); 9788 } 9789 } 9790 handleMouseUp(_e) { 9791 if (!this.dragStart) 9792 return; 9793 if (this.isDragging && this.currentRect) { 9794 const entities = this.findEntitiesInRect(this.currentRect); 9795 this.onMarqueeComplete(entities); 9796 } 9797 this.dragStart = null; 9798 this.isDragging = false; 9799 this.currentRect = null; 9800 this.onMarqueeUpdate(null); 9801 this.onMarqueeEnd(); 9802 } 9803 // ========================================================================= 9804 // Coordinate Conversion 9805 // ========================================================================= 9806 screenRectToMarqueeRect(rect) { 9807 const canvasRect = this.canvas.getBoundingClientRect(); 9808 const left = Math.min(rect.startX, rect.endX) + canvasRect.left; 9809 const top = Math.min(rect.startY, rect.endY) + canvasRect.top; 9810 const width = Math.abs(rect.endX - rect.startX); 9811 const height = Math.abs(rect.endY - rect.startY); 9812 return { left, top, width, height }; 9813 } 9814 // ========================================================================= 9815 // Entity Selection 9816 // ========================================================================= 9817 findEntitiesInRect(screenRect) { 9818 const minX = Math.min(screenRect.startX, screenRect.endX); 9819 const maxX = Math.max(screenRect.startX, screenRect.endX); 9820 const minY = Math.min(screenRect.startY, screenRect.endY); 9821 const maxY = Math.max(screenRect.startY, screenRect.endY); 9822 const entities = []; 9823 const seenIds = /* @__PURE__ */ new Set(); 9824 const worldPos = new Vector3(); 9825 for (const group of this.selectableGroups) { 9826 if (!group.visible) 9827 continue; 9828 group.traverse((obj) => { 9829 if (!(obj instanceof Mesh) && !(obj instanceof Line)) 9830 return; 9831 const entity = this.resolveEntity(obj); 9832 if (!entity || seenIds.has(entity.id)) 9833 return; 9834 if (!this.isEntityVisible(entity.type)) 9835 return; 9836 obj.getWorldPosition(worldPos); 9837 const screenPos = worldToScreen(worldPos.x, worldPos.y, this.canvas, this.camera); 9838 if (screenPos.x >= minX && screenPos.x <= maxX && screenPos.y >= minY && screenPos.y <= maxY) { 9839 entities.push(entity); 9840 seenIds.add(entity.id); 9841 } 9842 }); 9843 } 9844 return entities; 9845 } 9846 resolveEntity(obj) { 9847 let object = obj; 9848 while (object) { 9849 const userData = object.userData; 9850 if (userData) { 9851 if (userData["entityType"] && userData["entityId"]) { 9852 return { 9853 id: userData["entityId"], 9854 type: userData["entityType"], 9855 name: userData["entityName"] 9856 }; 9857 } 9858 if (userData["roomId"]) { 9859 return { 9860 id: userData["roomId"], 9861 type: "room", 9862 name: userData["roomName"] 9863 }; 9864 } 9865 if (userData["nodeId"]) { 9866 return { 9867 id: userData["nodeId"], 9868 type: "node", 9869 name: userData["nodeName"] 9870 }; 9871 } 9872 if (userData["floorId"]) { 9873 return { 9874 id: userData["floorId"], 9875 type: "floor", 9876 name: userData["floorName"] 9877 }; 9878 } 9879 if (userData["patchId"]) { 9880 return { 9881 id: userData["patchId"], 9882 type: "groundPatch", 9883 name: void 0 9884 }; 9885 } 9886 if (userData["connectionId"]) { 9887 return { 9888 id: userData["connectionId"], 9889 type: "nodeConnection", 9890 name: void 0 9891 }; 9892 } 9893 } 9894 object = object.parent; 9895 } 9896 return null; 9897 } 9898}; 9899
9900// src/app/pages/campus-map-v3/commands/move-command.ts 9901var MoveCommand = class extends BaseEditorCommand { 9902 constructor(config) { 9903 super(); 9904 this.targets = config.targets; 9905 this.deltaX = config.deltaX; 9906 this.deltaY = config.deltaY; 9907 this.database = config.database; 9908 this.onVisualUpdate = config.onVisualUpdate; 9909 if (this.targets.length === 1) { 9910 this.description = `Move ${this.formatType(this.targets[0].type)}`; 9911 } else { 9912 this.description = `Move ${this.targets.length} items`; 9913 } 9914 } 9915 execute() { 9916 for (const target of this.targets) { 9917 this.moveEntity(target, this.deltaX, this.deltaY); 9918 } 9919 this.onVisualUpdate(); 9920 } 9921 undo() { 9922 for (const target of this.targets) { 9923 this.moveEntity(target, -this.deltaX, -this.deltaY); 9924 } 9925 this.onVisualUpdate(); 9926 } 9927 moveEntity(target, dx, dy) { 9928 const model = this.database.Models.get(target.id); 9929 if (!model) 9930 return; 9931 switch (target.type) { 9932 case "node": 9933 this.movePointEntity(model, dx, dy); 9934 break; 9935 case "room": 9936 case "floor": 9937 case "groundPatch": 9938 this.movePolygonEntity(model, dx, dy); 9939 break; 9940 case "nodeConnection": 9941 break; 9942 } 9943 } 9944 movePointEntity(model, dx, dy) { 9945 var _a3, _b2; 9946 const entity = model.entity; 9947 entity["locationX"] = ((_a3 = entity["locationX"]) != null ? _a3 : 0) + dx; 9948 entity["locationY"] = ((_b2 = entity["locationY"]) != null ? _b2 : 0) + dy; 9949 entity["isChanged"] = true; 9950 } 9951 movePolygonEntity(model, dx, dy) { 9952 var _a3, _b2, _c, _d; 9953 const corners = model.corners; 9954 if (!corners || corners.length === 0) 9955 return; 9956 for (const corner of corners) { 9957 const cornerEntity = corner.entity; 9958 cornerEntity["locationX"] = ((_a3 = cornerEntity["locationX"]) != null ? _a3 : 0) + dx; 9959 cornerEntity["locationY"] = ((_b2 = cornerEntity["locationY"]) != null ? _b2 : 0) + dy; 9960 cornerEntity["isChanged"] = true; 9961 } 9962 const entity = model.entity; 9963 if (entity["locationX"] !== void 0 && entity["locationY"] !== void 0) { 9964 entity["locationX"] = ((_c = entity["locationX"]) != null ? _c : 0) + dx; 9965 entity["locationY"] = ((_d = entity["locationY"]) != null ? _d : 0) + dy; 9966 } 9967 entity["isChanged"] = true; 9968 } 9969}; 9970
9971// src/app/pages/campus-map-v3/utils/drag-move-controller.ts 9972var DragMoveController = class { 9973 constructor(config) { 9974 this.raycaster = new Raycaster(); 9975 this.pointer = new Vector2(); 9976 this.enabled = true; 9977 this.potentialDrag = false; 9978 this.isDragging = false; 9979 this.dragStartScreenX = 0; 9980 this.dragStartScreenY = 0; 9981 this.dragStartWorldX = 0; 9982 this.dragStartWorldY = 0; 9983 this.lastDeltaX = 0; 9984 this.lastDeltaY = 0; 9985 this.draggedEntities = []; 9986 this.attachedEntities = []; 9987 this.DRAG_THRESHOLD = 5; 9988 this.canvas = config.canvas; 9989 this.camera = config.camera; 9990 this.selectableGroups = config.selectableGroups; 9991 this.selectionService = config.selectionService; 9992 this.historyService = config.historyService; 9993 this.database = config.database; 9994 this.levelRenderer = config.levelRenderer; 9995 this.groundPatchRenderer = config.groundPatchRenderer; 9996 this.onDirty = config.onDirty; 9997 this.onDragStart = config.onDragStart; 9998 this.onDragEnd = config.onDragEnd; 9999 this.onVisualUpdate = config.onVisualUpdate; 10000 this.boundMouseDown = this.handleMouseDown.bind(this); 10001 this.boundMouseMove = this.handleMouseMove.bind(this); 10002 this.boundMouseUp = this.handleMouseUp.bind(this); 10003 this.attach(); 10004 } 10005 attach() { 10006 this.canvas.addEventListener("mousedown", this.boundMouseDown); 10007 window.addEventListener("mousemove", this.boundMouseMove); 10008 window.addEventListener("mouseup", this.boundMouseUp); 10009 } 10010 dispose() { 10011 this.canvas.removeEventListener("mousedown", this.boundMouseDown); 10012 window.removeEventListener("mousemove", this.boundMouseMove); 10013 window.removeEventListener("mouseup", this.boundMouseUp); 10014 } 10015 /** 10016 * Enable the controller to handle events. 10017 */ 10018 enable() { 10019 this.enabled = true; 10020 } 10021 /** 10022 * Disable the controller and reset any partial drag state. 10023 */ 10024 disable() { 10025 this.enabled = false; 10026 this.potentialDrag = false; 10027 this.isDragging = false; 10028 } 10029 // ========================================================================= 10030 // Mouse Handlers 10031 // ========================================================================= 10032 handleMouseDown(e) { 10033 if (!this.enabled) 10034 return; 10035 if (e.button !== 0 || e.shiftKey || e.ctrlKey || e.metaKey) 10036 return; 10037 if (!this.selectionService.hasSelection()) 10038 return; 10039 const clickedEntity = this.raycastEntity(e.clientX, e.clientY); 10040 if (!clickedEntity) 10041 return; 10042 if (!this.selectionService.isSelected(clickedEntity.id)) 10043 return; 10044 this.potentialDrag = true; 10045 this.onDragStart(); 10046 this.dragStartScreenX = e.clientX; 10047 this.dragStartScreenY = e.clientY; 10048 const worldStart = this.screenToWorld(e.clientX, e.clientY); 10049 this.dragStartWorldX = worldStart.x; 10050 this.dragStartWorldY = worldStart.y; 10051 } 10052 handleMouseMove(e) { 10053 if (!this.potentialDrag && !this.isDragging) 10054 return; 10055 const dx = e.clientX - this.dragStartScreenX; 10056 const dy = e.clientY - this.dragStartScreenY; 10057 const distance = Math.sqrt(dx * dx + dy * dy); 10058 if (!this.isDragging && distance >= this.DRAG_THRESHOLD) { 10059 this.startDrag(); 10060 } 10061 if (this.isDragging) { 10062 this.updateDrag(e.clientX, e.clientY); 10063 } 10064 } 10065 handleMouseUp(_e) { 10066 if (this.isDragging) { 10067 this.endDrag(); 10068 } else if (this.potentialDrag) { 10069 this.onDragEnd(); 10070 } 10071 this.potentialDrag = false; 10072 } 10073 // ========================================================================= 10074 // Drag Operations 10075 // ========================================================================= 10076 startDrag() { 10077 this.isDragging = true; 10078 this.draggedEntities = [...this.selectionService.selectedEntities()]; 10079 this.attachedEntities = this.collectAttachedEntities(this.draggedEntities); 10080 const allEntities = [...this.draggedEntities, ...this.attachedEntities]; 10081 for (const entity of allEntities) { 10082 if (entity.type === "groundPatch") { 10083 this.groundPatchRenderer.createGhost(entity.id); 10084 } else { 10085 this.levelRenderer.createGhost(entity.id, entity.type); 10086 } 10087 } 10088 this.lastDeltaX = 0; 10089 this.lastDeltaY = 0; 10090 } 10091 updateDrag(screenX, screenY) { 10092 const currentWorld = this.screenToWorld(screenX, screenY); 10093 let deltaX = currentWorld.x - this.dragStartWorldX; 10094 let deltaY = currentWorld.y - this.dragStartWorldY;
10095 const rotation = this.getSceneRotation(); 10096 if (rotation !== 0) { 10097 const cos = Math.cos(-rotation); 10098 const sin = Math.sin(-rotation); 10099 const rotatedDeltaX = deltaX * cos - deltaY * sin; 10100 const rotatedDeltaY = deltaX * sin + deltaY * cos; 10101 deltaX = rotatedDeltaX; 10102 deltaY = rotatedDeltaY; 10103 } 10104 const incrementalDeltaX = deltaX - this.lastDeltaX; 10105 const incrementalDeltaY = deltaY - this.lastDeltaY; 10106 const allEntities = [...this.draggedEntities, ...this.attachedEntities]; 10107 for (const entity of allEntities) { 10108 if (entity.type === "groundPatch") { 10109 this.groundPatchRenderer.moveEntityMesh(entity.id, incrementalDeltaX, incrementalDeltaY); 10110 } else { 10111 this.levelRenderer.moveEntityMesh(entity.id, entity.type, incrementalDeltaX, incrementalDeltaY); 10112 } 10113 this.applyMovementToEntity(entity, incrementalDeltaX, incrementalDeltaY); 10114 } 10115 this.lastDeltaX = deltaX; 10116 this.lastDeltaY = deltaY; 10117 this.onDirty(); 10118 } 10119 endDrag() { 10120 this.isDragging = false; 10121 this.onDragEnd(); 10122 this.levelRenderer.clearGhosts(); 10123 this.groundPatchRenderer.clearGhosts(); 10124 const allEntities = [...this.draggedEntities, ...this.attachedEntities]; 10125 if (Math.abs(this.lastDeltaX) > 0.1 || Math.abs(this.lastDeltaY) > 0.1) { 10126 for (const entity of allEntities) { 10127 if (entity.type === "groundPatch") { 10128 this.groundPatchRenderer.moveEntityMesh(entity.id, -this.lastDeltaX, -this.lastDeltaY); 10129 } else { 10130 this.levelRenderer.moveEntityMesh(entity.id, entity.type, -this.lastDeltaX, -this.lastDeltaY); 10131 } 10132 } 10133 for (const entity of allEntities) { 10134 this.applyMovementToEntity(entity, -this.lastDeltaX, -this.lastDeltaY); 10135 } 10136 const command = new MoveCommand({ 10137 targets: allEntities.map((e) => ({ 10138 id: e.id, 10139 type: e.type 10140 })), 10141 deltaX: this.lastDeltaX, 10142 deltaY: this.lastDeltaY, 10143 database: this.database, 10144 onVisualUpdate: this.onVisualUpdate 10145 }); 10146 this.historyService.executeCommand(command); 10147 } 10148 this.draggedEntities = []; 10149 this.attachedEntities = []; 10150 this.lastDeltaX = 0; 10151 this.lastDeltaY = 0; 10152 } 10153 // ========================================================================= 10154 // Helper Methods 10155 // ========================================================================= 10156 /** 10157 * Collect attached entities that should move with their parent. 10158 * For floors: collects all rooms and nodes on that floor. 10159 */ 10160 collectAttachedEntities(selectedEntities) { 10161 var _a3, _b2, _c, _d; 10162 const attached = []; 10163 const selectedIds = new Set(selectedEntities.map((e) => e.id)); 10164 for (const entity of selectedEntities) { 10165 if (entity.type === "floor") { 10166 const floorModel = this.database.Models.get(entity.id); 10167 if (floorModel && floorModel.type === EntityType.FLOOR) { 10168 const floor = floorModel; 10169 for (const room of floor.rooms) { 10170 if (room.entity.isDeleted) 10171 continue; 10172 if (room.entity.id && !selectedIds.has(room.entity.id)) { 10173 attached.push({ 10174 id: room.entity.id, 10175 type: "room", 10176 name: (_b2 = (_a3 = room.entity.name) != null ? _a3 : room.entity.number) != null ? _b2 : void 0 10177 }); 10178 selectedIds.add(room.entity.id); 10179 } 10180 } 10181 for (const node of floor.nodes) { 10182 if (node.entity.isDeleted) 10183 continue; 10184 if (node.entity.id && !selectedIds.has(node.entity.id)) { 10185 attached.push({ 10186 id: node.entity.id, 10187 type: "node", 10188 name: (_d = (_c = node.entity.tagName) != null ? _c : node.entity.type) != null ? _d : void 0 10189 }); 10190 selectedIds.add(node.entity.id); 10191 } 10192 } 10193 } 10194 } 10195 } 10196 return attached; 10197 } 10198 applyMovementToEntity(entity, dx, dy) { 10199 const model = this.database.Models.get(entity.id); 10200 if (!model) 10201 return; 10202 switch (entity.type) { 10203 case "node": 10204 this.applyMovementToPointEntity(model, dx, dy); 10205 break; 10206 case "room": 10207 case "floor": 10208 case "groundPatch": 10209 this.applyMovementToPolygonEntity(model, dx, dy); 10210 break; 10211 case "nodeConnection": 10212 break; 10213 } 10214 } 10215 applyMovementToPointEntity(model, dx, dy) { 10216 var _a3, _b2; 10217 const entity = model.entity; 10218 entity["locationX"] = ((_a3 = entity["locationX"]) != null ? _a3 : 0) + dx; 10219 entity["locationY"] = ((_b2 = entity["locationY"]) != null ? _b2 : 0) + dy; 10220 } 10221 applyMovementToPolygonEntity(model, dx, dy) { 10222 var _a3, _b2, _c, _d; 10223 const corners = model.corners; 10224 if (!corners) 10225 return; 10226 for (const corner of corners) { 10227 corner.entity["locationX"] = ((_a3 = corner.entity["locationX"]) != null ? _a3 : 0) + dx; 10228 corner.entity["locationY"] = ((_b2 = corner.entity["locationY"]) != null ? _b2 : 0) + dy; 10229 } 10230 const entity = model.entity; 10231 if (entity["locationX"] !== void 0 && entity["locationY"] !== void 0) { 10232 entity["locationX"] = ((_c = entity["locationX"]) != null ? _c : 0) + dx; 10233 entity["locationY"] = ((_d = entity["locationY"]) != null ? _d : 0) + dy; 10234 } 10235 } 10236 screenToWorld(screenX, screenY) { 10237 const rect = this.canvas.getBoundingClientRect(); 10238 const ndcX = (screenX - rect.left) / rect.width * 2 - 1; 10239 const ndcY = -((screenY - rect.top) / rect.height) * 2 + 1; 10240 const halfWidth = (this.camera.right - this.camera.left) / 2; 10241 const halfHeight = (this.camera.top - this.camera.bottom) / 2; 10242 return { 10243 x: this.camera.position.x + ndcX * halfWidth / this.camera.zoom, 10244 y: this.camera.position.y + ndcY * halfHeight / this.camera.zoom 10245 }; 10246 } 10247 /** 10248 * Get the current scene rotation from the selectable groups. 10249 * All groups share the same rotation applied by GestureController. 10250 */ 10251 getSceneRotation() { 10252 if (this.selectableGroups.length > 0) { 10253 return this.selectableGroups[0].rotation.z; 10254 } 10255 return 0; 10256 } 10257 raycastEntity(clientX, clientY) { 10258 const rect = this.canvas.getBoundingClientRect();
10259 this.pointer.x = (clientX - rect.left) / rect.width * 2 - 1; 10260 this.pointer.y = -((clientY - rect.top) / rect.height) * 2 + 1; 10261 this.raycaster.setFromCamera(this.pointer, this.camera); 10262 const allObjects = []; 10263 for (const group of this.selectableGroups) { 10264 if (group.visible) { 10265 allObjects.push(...group.children); 10266 } 10267 } 10268 const intersects = this.raycaster.intersectObjects(allObjects, true); 10269 for (const intersection of intersects) { 10270 const entity = this.resolveEntity(intersection.object); 10271 if (entity) { 10272 return entity; 10273 } 10274 } 10275 return null; 10276 } 10277 resolveEntity(object) { 10278 let current = object; 10279 while (current) { 10280 const userData = current.userData; 10281 if (userData) { 10282 if (userData["entityType"] && userData["entityId"]) { 10283 return { 10284 id: userData["entityId"], 10285 type: userData["entityType"], 10286 name: userData["entityName"] 10287 }; 10288 } 10289 if (userData["roomId"]) { 10290 return { id: userData["roomId"], type: "room" }; 10291 } 10292 if (userData["nodeId"]) { 10293 return { id: userData["nodeId"], type: "node" }; 10294 } 10295 if (userData["floorId"]) { 10296 return { id: userData["floorId"], type: "floor" }; 10297 } 10298 if (userData["patchId"]) { 10299 return { id: userData["patchId"], type: "groundPatch" }; 10300 } 10301 } 10302 current = current.parent; 10303 } 10304 return null; 10305 } 10306}; 10307
10308// src/app/pages/campus-map-v3/commands/rotate-command.ts 10309var RotateCommand = class extends BaseEditorCommand { 10310 constructor(config) { 10311 super(); 10312 this.targets = config.targets; 10313 this.angleDelta = config.angleDelta; 10314 this.pivot = config.pivot; 10315 this.database = config.database; 10316 this.onVisualUpdate = config.onVisualUpdate; 10317 const degrees = Math.round(this.angleDelta * 180 / Math.PI); 10318 if (this.targets.length === 1) { 10319 this.description = `Rotate ${this.formatType(this.targets[0].type)} ${degrees}deg`; 10320 } else { 10321 this.description = `Rotate ${this.targets.length} items ${degrees}deg`; 10322 } 10323 } 10324 execute() { 10325 for (const target of this.targets) { 10326 this.rotateEntity(target, this.angleDelta); 10327 } 10328 this.onVisualUpdate(); 10329 } 10330 undo() { 10331 for (const target of this.targets) { 10332 this.rotateEntity(target, -this.angleDelta); 10333 } 10334 this.onVisualUpdate(); 10335 } 10336 rotateEntity(target, angle) { 10337 const model = this.database.Models.get(target.id); 10338 if (!model) 10339 return; 10340 switch (target.type) { 10341 case "node": 10342 this.rotatePointEntity(model, angle); 10343 break; 10344 case "room": 10345 case "floor": 10346 case "groundPatch": 10347 this.rotatePolygonEntity(model, angle); 10348 break; 10349 case "nodeConnection": 10350 break; 10351 } 10352 } 10353 rotatePointEntity(model, angle) { 10354 var _a3, _b2; 10355 const entity = model.entity; 10356 const point = { 10357 x: (_a3 = entity["locationX"]) != null ? _a3 : 0, 10358 y: (_b2 = entity["locationY"]) != null ? _b2 : 0 10359 }; 10360 const rotated = PointUtils.rotate(point, angle, this.pivot); 10361 entity["locationX"] = Math.round(rotated.x); 10362 entity["locationY"] = Math.round(rotated.y); 10363 entity["isChanged"] = true; 10364 } 10365 rotatePolygonEntity(model, angle) { 10366 var _a3, _b2, _c, _d; 10367 const corners = model.corners; 10368 if (!corners || corners.length === 0) 10369 return; 10370 for (const corner of corners) { 10371 const cornerEntity = corner.entity; 10372 const point = { 10373 x: (_a3 = cornerEntity["locationX"]) != null ? _a3 : 0, 10374 y: (_b2 = cornerEntity["locationY"]) != null ? _b2 : 0 10375 }; 10376 const rotated = PointUtils.rotate(point, angle, this.pivot); 10377 cornerEntity["locationX"] = Math.round(rotated.x); 10378 cornerEntity["locationY"] = Math.round(rotated.y); 10379 cornerEntity["isChanged"] = true; 10380 } 10381 const entity = model.entity; 10382 if (entity["locationX"] !== void 0 && entity["locationY"] !== void 0) { 10383 const labelPoint = { 10384 x: (_c = entity["locationX"]) != null ? _c : 0, 10385 y: (_d = entity["locationY"]) != null ? _d : 0 10386 }; 10387 const rotatedLabel = PointUtils.rotate(labelPoint, angle, this.pivot); 10388 entity["locationX"] = Math.round(rotatedLabel.x); 10389 entity["locationY"] = Math.round(rotatedLabel.y); 10390 } 10391 entity["isChanged"] = true; 10392 } 10393}; 10394
10395// src/app/pages/campus-map-v3/utils/drag-rotate-controller.ts 10396var DragRotateController = class { 10397 constructor(config) { 10398 this.raycaster = new Raycaster(); 10399 this.pointer = new Vector2(); 10400 this.enabled = true; 10401 this.potentialRotate = false; 10402 this.isRotating = false; 10403 this.startAngle = 0; 10404 this.lastAngle = 0; 10405 this.totalAngle = 0; 10406 this.pivot = { x: 0, y: 0 }; 10407 this.lastScreenX = 0; 10408 this.lastScreenY = 0; 10409 this.rotatingEntities = []; 10410 this.attachedEntities = []; 10411 this.ROTATE_THRESHOLD = 5; 10412 this.isHoveredState = false; 10413 this.canvas = config.canvas; 10414 this.camera = config.camera; 10415 this.selectableGroups = config.selectableGroups; 10416 this.rotationHandleGroup = config.rotationHandleGroup; 10417 this.selectionService = config.selectionService; 10418 this.historyService = config.historyService; 10419 this.database = config.database; 10420 this.levelRenderer = config.levelRenderer; 10421 this.groundPatchRenderer = config.groundPatchRenderer; 10422 this.onDirty = config.onDirty; 10423 this.onDragStart = config.onDragStart; 10424 this.onDragEnd = config.onDragEnd; 10425 this.onVisualUpdate = config.onVisualUpdate; 10426 this.onAngleUpdate = config.onAngleUpdate; 10427 this.onHoverChange = config.onHoverChange; 10428 this.boundMouseDown = this.handleMouseDown.bind(this); 10429 this.boundMouseMove = this.handleMouseMove.bind(this); 10430 this.boundMouseUp = this.handleMouseUp.bind(this); 10431 this.attach(); 10432 } 10433 attach() { 10434 this.canvas.addEventListener("mousedown", this.boundMouseDown); 10435 window.addEventListener("mousemove", this.boundMouseMove); 10436 window.addEventListener("mouseup", this.boundMouseUp); 10437 } 10438 dispose() { 10439 this.canvas.removeEventListener("mousedown", this.boundMouseDown); 10440 window.removeEventListener("mousemove", this.boundMouseMove); 10441 window.removeEventListener("mouseup", this.boundMouseUp); 10442 } 10443 enable() { 10444 this.enabled = true; 10445 } 10446 disable() { 10447 this.enabled = false; 10448 this.potentialRotate = false; 10449 this.isRotating = false; 10450 } 10451 // ========================================================================= 10452 // Mouse Handlers 10453 // ========================================================================= 10454 handleMouseDown(e) { 10455 if (!this.enabled) 10456 return; 10457 if (e.button !== 0 || e.shiftKey || e.ctrlKey || e.metaKey) 10458 return; 10459 if (!this.selectionService.hasSelection()) 10460 return; 10461 if (!this.isClickOnRotationGrip(e.clientX, e.clientY)) 10462 return; 10463 e.preventDefault(); 10464 e.stopPropagation(); 10465 this.pivot = this.calculatePivot(this.selectionService.selectedEntities()); 10466 this.startAngle = this.calculateAngleFromMouse(e.clientX, e.clientY); 10467 this.lastAngle = this.startAngle; 10468 this.totalAngle = 0; 10469 this.lastScreenX = e.clientX; 10470 this.lastScreenY = e.clientY; 10471 this.potentialRotate = true; 10472 this.onDragStart(); 10473 } 10474 handleMouseMove(e) { 10475 if (!this.potentialRotate && !this.isRotating) { 10476 const wasHovered = this.isHoveredState; 10477 const isHovered = this.isClickOnRotationGrip(e.clientX, e.clientY); 10478 if (wasHovered !== isHovered) { 10479 this.isHoveredState = isHovered; 10480 this.onHoverChange(isHovered); 10481 this.canvas.style.cursor = isHovered ? "grab" : ""; 10482 } 10483 return; 10484 } 10485 const dx = e.clientX - this.lastScreenX; 10486 const dy = e.clientY - this.lastScreenY; 10487 const distance = Math.sqrt(dx * dx + dy * dy); 10488 if (!this.isRotating && distance >= this.ROTATE_THRESHOLD) { 10489 this.startRotation(); 10490 } 10491 if (this.isRotating) { 10492 this.updateRotation(e.clientX, e.clientY, e.shiftKey); 10493 } 10494 } 10495 handleMouseUp(_e) { 10496 if (this.isRotating) { 10497 this.endRotation(); 10498 } else if (this.potentialRotate) { 10499 this.onDragEnd(); 10500 } 10501 this.potentialRotate = false; 10502 this.canvas.style.cursor = ""; 10503 } 10504 // ========================================================================= 10505 // Rotation Operations 10506 // ========================================================================= 10507 startRotation() { 10508 this.isRotating = true; 10509 this.canvas.style.cursor = "grabbing"; 10510 this.rotatingEntities = [...this.selectionService.selectedEntities()]; 10511 this.attachedEntities = this.collectAttachedEntities(this.rotatingEntities); 10512 const allEntities = [...this.rotatingEntities, ...this.attachedEntities]; 10513 for (const entity of allEntities) { 10514 if (entity.type === "groundPatch") { 10515 this.groundPatchRenderer.createGhost(entity.id); 10516 } else { 10517 this.levelRenderer.createGhost(entity.id, entity.type); 10518 } 10519 } 10520 } 10521 updateRotation(screenX, screenY, shiftKey) { 10522 const currentAngle = this.calculateAngleFromMouse(screenX, screenY); 10523 let angleDelta = currentAngle - this.lastAngle; 10524 while (angleDelta > Math.PI) 10525 angleDelta -= 2 * Math.PI; 10526 while (angleDelta < -Math.PI) 10527 angleDelta += 2 * Math.PI; 10528 if (shiftKey) { 10529 const SNAP_ANGLE = Math.PI / 12; 10530 const snappedTotal = Math.round((this.totalAngle + angleDelta) / SNAP_ANGLE) * SNAP_ANGLE; 10531 angleDelta = snappedTotal - this.totalAngle; 10532 } 10533 if (Math.abs(angleDelta) < 1e-4) 10534 return; 10535 const allEntities = [...this.rotatingEntities, ...this.attachedEntities]; 10536 for (const entity of allEntities) { 10537 if (entity.type === "groundPatch") { 10538 this.groundPatchRenderer.rotateEntityMesh(entity.id, angleDelta, this.pivot); 10539 } else { 10540 this.levelRenderer.rotateEntityMesh(entity.id, entity.type, angleDelta, this.pivot); 10541 } 10542 } 10543 this.lastAngle = currentAngle; 10544 this.totalAngle += angleDelta; 10545 const totalDegrees = Math.round(this.totalAngle * 180 / Math.PI); 10546 this.onAngleUpdate(totalDegrees, screenX, screenY); 10547 this.onDirty(); 10548 } 10549 endRotation() { 10550 this.isRotating = false;
10551 this.onDragEnd(); 10552 this.onAngleUpdate(0, 0, 0); 10553 this.levelRenderer.clearGhosts(); 10554 this.groundPatchRenderer.clearGhosts(); 10555 const allEntities = [...this.rotatingEntities, ...this.attachedEntities]; 10556 if (Math.abs(this.totalAngle) > 1e-3) { 10557 for (const entity of allEntities) { 10558 if (entity.type === "groundPatch") { 10559 this.groundPatchRenderer.rotateEntityMesh(entity.id, -this.totalAngle, this.pivot); 10560 } else { 10561 this.levelRenderer.rotateEntityMesh(entity.id, entity.type, -this.totalAngle, this.pivot); 10562 } 10563 } 10564 const command = new RotateCommand({ 10565 targets: allEntities.map((e) => ({ 10566 id: e.id, 10567 type: e.type 10568 })), 10569 angleDelta: this.totalAngle, 10570 pivot: this.pivot, 10571 database: this.database, 10572 onVisualUpdate: this.onVisualUpdate 10573 }); 10574 this.historyService.executeCommand(command); 10575 } 10576 this.rotatingEntities = []; 10577 this.attachedEntities = []; 10578 this.totalAngle = 0; 10579 } 10580 // ========================================================================= 10581 // Helper Methods 10582 // ========================================================================= 10583 /** 10584 * Check if a click is on the rotation handle (grip or arc). 10585 */ 10586 isClickOnRotationGrip(clientX, clientY) { 10587 if (!this.rotationHandleGroup.visible) 10588 return false; 10589 const rect = this.canvas.getBoundingClientRect(); 10590 this.pointer.x = (clientX - rect.left) / rect.width * 2 - 1; 10591 this.pointer.y = -((clientY - rect.top) / rect.height) * 2 + 1; 10592 this.raycaster.setFromCamera(this.pointer, this.camera); 10593 this.raycaster.params.Line = { threshold: 15 }; 10594 const intersects = this.raycaster.intersectObjects(this.rotationHandleGroup.children, true); 10595 for (const intersection of intersects) { 10596 if (intersection.object.userData["isRotationGrip"] || intersection.object.userData["isRotationHandle"]) { 10597 return true; 10598 } 10599 } 10600 return false; 10601 } 10602 /** 10603 * Calculate rotation angle from mouse position relative to pivot. 10604 */ 10605 calculateAngleFromMouse(clientX, clientY) { 10606 const worldPos = this.screenToWorld(clientX, clientY); 10607 const rotation = this.getSceneRotation(); 10608 let dx = worldPos.x - this.pivot.x; 10609 let dy = worldPos.y - this.pivot.y; 10610 if (rotation !== 0) { 10611 const cos = Math.cos(-rotation); 10612 const sin = Math.sin(-rotation); 10613 const rotatedDx = dx * cos - dy * sin; 10614 const rotatedDy = dx * sin + dy * cos; 10615 dx = rotatedDx; 10616 dy = rotatedDy; 10617 } 10618 return Math.atan2(dy, dx); 10619 } 10620 /** 10621 * Calculate pivot point from selected entities. 10622 * Uses centroid of all selected entity points/corners. 10623 */ 10624 calculatePivot(entities) { 10625 var _a3, _b2, _c, _d; 10626 const allPoints = []; 10627 for (const entity of entities) { 10628 const model = this.database.Models.get(entity.id); 10629 if (!model) 10630 continue; 10631 if (entity.type === "node") { 10632 const nodeEntity = model.entity; 10633 allPoints.push({ 10634 x: (_a3 = nodeEntity["locationX"]) != null ? _a3 : 0, 10635 y: (_b2 = nodeEntity["locationY"]) != null ? _b2 : 0 10636 }); 10637 } else if (entity.type === "room" || entity.type === "floor" || entity.type === "groundPatch") { 10638 const corners = model.corners; 10639 if (corners) { 10640 for (const corner of corners) { 10641 allPoints.push({ 10642 x: (_c = corner.entity["locationX"]) != null ? _c : 0, 10643 y: (_d = corner.entity["locationY"]) != null ? _d : 0 10644 }); 10645 } 10646 } 10647 } 10648 } 10649 if (allPoints.length === 0) { 10650 return { x: 0, y: 0 }; 10651 } 10652 if (allPoints.length >= 3) { 10653 return PointUtils.polygonCentroid(allPoints); 10654 } 10655 const sumX = allPoints.reduce((sum, p) => sum + p.x, 0); 10656 const sumY = allPoints.reduce((sum, p) => sum + p.y, 0); 10657 return { 10658 x: sumX / allPoints.length, 10659 y: sumY / allPoints.length 10660 }; 10661 } 10662 /** 10663 * Collect attached entities that should rotate with their parent. 10664 * For floors: collects all rooms and nodes on that floor. 10665 */ 10666 collectAttachedEntities(selectedEntities) { 10667 var _a3, _b2, _c, _d; 10668 const attached = []; 10669 const selectedIds = new Set(selectedEntities.map((e) => e.id)); 10670 for (const entity of selectedEntities) { 10671 if (entity.type === "floor") { 10672 const floorModel = this.database.Models.get(entity.id); 10673 if (floorModel && floorModel.type === EntityType.FLOOR) { 10674 const floor = floorModel; 10675 for (const room of floor.rooms) { 10676 if (room.entity.isDeleted) 10677 continue; 10678 if (room.entity.id && !selectedIds.has(room.entity.id)) { 10679 attached.push({ 10680 id: room.entity.id, 10681 type: "room", 10682 name: (_b2 = (_a3 = room.entity.name) != null ? _a3 : room.entity.number) != null ? _b2 : void 0 10683 }); 10684 selectedIds.add(room.entity.id); 10685 } 10686 } 10687 for (const node of floor.nodes) { 10688 if (node.entity.isDeleted) 10689 continue; 10690 if (node.entity.id && !selectedIds.has(node.entity.id)) { 10691 attached.push({ 10692 id: node.entity.id, 10693 type: "node", 10694 name: (_d = (_c = node.entity.tagName) != null ? _c : node.entity.type) != null ? _d : void 0 10695 }); 10696 selectedIds.add(node.entity.id); 10697 } 10698 } 10699 } 10700 } 10701 } 10702 return attached; 10703 } 10704 screenToWorld(screenX, screenY) { 10705 const rect = this.canvas.getBoundingClientRect(); 10706 const ndcX = (screenX - rect.left) / rect.width * 2 - 1; 10707 const ndcY = -((screenY - rect.top) / rect.height) * 2 + 1; 10708 const halfWidth = (this.camera.right - this.camera.left) / 2; 10709 const halfHeight = (this.camera.top - this.camera.bottom) / 2; 10710 return { 10711 x: this.camera.position.x + ndcX * halfWidth / this.camera.zoom, 10712 y: this.camera.position.y + ndcY * halfHeight / this.camera.zoom 10713 }; 10714 } 10715 getSceneRotation() { 10716 if (this.selectableGroups.length > 0) { 10717 return this.selectableGroups[0].rotation.z; 10718 } 10719 return 0; 10720 } 10721}; 10722
10723// src/app/pages/campus-map-v3/utils/node-placement-controller.ts 10724var NodePlacementController = class { 10725 constructor(config) { 10726 this.active = false; 10727 this.originalCursor = ""; 10728 this.canvas = config.canvas; 10729 this.camera = config.camera; 10730 this.rotatableGroups = config.rotatableGroups; 10731 this.onPlacement = config.onPlacement; 10732 this.onCancel = config.onCancel; 10733 this.onDirty = config.onDirty; 10734 this.boundClick = this.handleClick.bind(this); 10735 this.boundTouchEnd = this.handleTouchEnd.bind(this); 10736 this.boundKeyDown = this.handleKeyDown.bind(this); 10737 } 10738 get isActive() { 10739 return this.active; 10740 } 10741 activate() { 10742 if (this.active) 10743 return; 10744 this.active = true; 10745 this.originalCursor = this.canvas.style.cursor; 10746 this.canvas.style.cursor = "crosshair"; 10747 this.canvas.addEventListener("click", this.boundClick); 10748 this.canvas.addEventListener("touchend", this.boundTouchEnd, { passive: false }); 10749 window.addEventListener("keydown", this.boundKeyDown); 10750 this.onDirty(); 10751 } 10752 deactivate() { 10753 if (!this.active) 10754 return; 10755 this.active = false; 10756 this.canvas.style.cursor = this.originalCursor; 10757 this.canvas.removeEventListener("click", this.boundClick); 10758 this.canvas.removeEventListener("touchend", this.boundTouchEnd); 10759 window.removeEventListener("keydown", this.boundKeyDown); 10760 this.onDirty(); 10761 } 10762 dispose() { 10763 this.deactivate(); 10764 } 10765 handleClick(e) { 10766 if (e.button !== 0) 10767 return; 10768 e.preventDefault(); 10769 e.stopPropagation(); 10770 const localPos = screenToLocal(e.clientX, e.clientY, this.canvas, this.camera, this.rotatableGroups); 10771 this.onPlacement(localPos.x, localPos.y); 10772 } 10773 handleTouchEnd(e) { 10774 if (e.changedTouches.length !== 1) 10775 return; 10776 e.preventDefault(); 10777 const touch = e.changedTouches[0]; 10778 const localPos = screenToLocal(touch.clientX, touch.clientY, this.canvas, this.camera, this.rotatableGroups); 10779 this.onPlacement(localPos.x, localPos.y); 10780 } 10781 handleKeyDown(e) { 10782 if (isEditableKeyboardEvent(e)) 10783 return; 10784 if (e.key === "Escape") { 10785 e.preventDefault(); 10786 e.stopImmediatePropagation(); 10787 this.onCancel(); 10788 } 10789 } 10790}; 10791
10792// src/app/pages/campus-map-v3/utils/node-connection-controller.ts 10793var NodeConnectionController = class { 10794 constructor(config) { 10795 this.active = false; 10796 this.sourceNodeId = null; 10797 this.sourceX = 0; 10798 this.sourceY = 0; 10799 this.raycaster = new Raycaster(); 10800 this.pointer = new Vector2(); 10801 this.originalCursor = ""; 10802 this.canvas = config.canvas; 10803 this.camera = config.camera; 10804 this.rotatableGroups = config.rotatableGroups; 10805 this.selectableGroups = config.selectableGroups; 10806 this.levelRenderer = config.levelRenderer; 10807 this.database = config.database; 10808 this.onConnectionCreate = config.onConnectionCreate; 10809 this.onCancel = config.onCancel; 10810 this.onDirty = config.onDirty; 10811 this.boundMouseMove = this.handleMouseMove.bind(this); 10812 this.boundClick = this.handleClick.bind(this); 10813 this.boundTouchMove = this.handleTouchMove.bind(this); 10814 this.boundTouchEnd = this.handleTouchEnd.bind(this); 10815 this.boundKeyDown = this.handleKeyDown.bind(this); 10816 } 10817 get isActive() { 10818 return this.active; 10819 } 10820 /** 10821 * Activate connect mode with a source node. 10822 * @param sourceNodeId The ID of the node to connect from 10823 */ 10824 activate(sourceNodeId) { 10825 var _a3, _b2; 10826 if (this.active) 10827 return; 10828 const sourceModel = this.database.Models.get(sourceNodeId); 10829 if (!sourceModel || sourceModel.type !== EntityType.NODE) { 10830 return; 10831 } 10832 this.active = true; 10833 this.sourceNodeId = sourceNodeId; 10834 this.sourceX = (_a3 = sourceModel.entity.locationX) != null ? _a3 : 0; 10835 this.sourceY = (_b2 = sourceModel.entity.locationY) != null ? _b2 : 0; 10836 this.originalCursor = this.canvas.style.cursor; 10837 this.canvas.style.cursor = "crosshair"; 10838 this.canvas.addEventListener("mousemove", this.boundMouseMove); 10839 this.canvas.addEventListener("click", this.boundClick); 10840 this.canvas.addEventListener("touchmove", this.boundTouchMove, { passive: false }); 10841 this.canvas.addEventListener("touchend", this.boundTouchEnd, { passive: false }); 10842 window.addEventListener("keydown", this.boundKeyDown); 10843 this.onDirty(); 10844 } 10845 deactivate() { 10846 if (!this.active) 10847 return; 10848 this.active = false; 10849 this.sourceNodeId = null; 10850 this.canvas.style.cursor = this.originalCursor; 10851 this.levelRenderer.clearPendingConnection(); 10852 this.canvas.removeEventListener("mousemove", this.boundMouseMove); 10853 this.canvas.removeEventListener("click", this.boundClick); 10854 this.canvas.removeEventListener("touchmove", this.boundTouchMove); 10855 this.canvas.removeEventListener("touchend", this.boundTouchEnd); 10856 window.removeEventListener("keydown", this.boundKeyDown); 10857 this.onDirty(); 10858 } 10859 dispose() { 10860 this.deactivate(); 10861 } 10862 handleMouseMove(e) { 10863 this.updateGhostLine(e.clientX, e.clientY); 10864 } 10865 handleTouchMove(e) { 10866 if (e.touches.length === 1) { 10867 const touch = e.touches[0]; 10868 this.updateGhostLine(touch.clientX, touch.clientY); 10869 } 10870 } 10871 updateGhostLine(clientX, clientY) { 10872 const localPos = screenToLocal(clientX, clientY, this.canvas, this.camera, this.rotatableGroups); 10873 const targetNodeId = this.raycastNode(clientX, clientY); 10874 const isValidTarget = targetNodeId !== null && targetNodeId !== this.sourceNodeId && !this.connectionExists(this.sourceNodeId, targetNodeId); 10875 this.levelRenderer.renderPendingConnection(this.sourceX, this.sourceY, localPos.x, localPos.y, isValidTarget); 10876 } 10877 handleClick(e) { 10878 if (e.button !== 0) 10879 return; 10880 e.preventDefault(); 10881 e.stopPropagation(); 10882 this.tryConnect(e.clientX, e.clientY); 10883 } 10884 handleTouchEnd(e) { 10885 if (e.changedTouches.length !== 1) 10886 return; 10887 e.preventDefault(); 10888 const touch = e.changedTouches[0]; 10889 this.tryConnect(touch.clientX, touch.clientY); 10890 } 10891 tryConnect(clientX, clientY) { 10892 const targetNodeId = this.raycastNode(clientX, clientY); 10893 if (!targetNodeId) { 10894 return; 10895 } 10896 if (targetNodeId === this.sourceNodeId) { 10897 return; 10898 } 10899 if (this.connectionExists(this.sourceNodeId, targetNodeId)) { 10900 return; 10901 } 10902 this.onConnectionCreate(this.sourceNodeId, targetNodeId); 10903 } 10904 handleKeyDown(e) { 10905 if (isEditableKeyboardEvent(e)) 10906 return; 10907 if (e.key === "Escape") { 10908 e.preventDefault(); 10909 e.stopImmediatePropagation(); 10910 this.onCancel(); 10911 } 10912 } 10913 /**
10914 * Raycast to find a node at the given screen position. 10915 * @returns The node ID if found, null otherwise 10916 */ 10917 raycastNode(clientX, clientY) { 10918 const rect = this.canvas.getBoundingClientRect(); 10919 this.pointer.x = (clientX - rect.left) / rect.width * 2 - 1; 10920 this.pointer.y = -((clientY - rect.top) / rect.height) * 2 + 1; 10921 this.raycaster.setFromCamera(this.pointer, this.camera); 10922 const allObjects = []; 10923 for (const group of this.selectableGroups) { 10924 if (group.visible) { 10925 allObjects.push(...group.children); 10926 } 10927 } 10928 const intersects = this.raycaster.intersectObjects(allObjects, true); 10929 for (const intersection of intersects) { 10930 let object = intersection.object; 10931 while (object) { 10932 const userData = object.userData; 10933 if (userData) { 10934 if (userData["entityType"] === "node" && userData["entityId"]) { 10935 return userData["entityId"]; 10936 } 10937 if (userData["nodeId"]) { 10938 return userData["nodeId"]; 10939 } 10940 } 10941 object = object.parent; 10942 } 10943 } 10944 return null; 10945 } 10946 /** 10947 * Check if a connection already exists between two nodes. 10948 */ 10949 connectionExists(nodeAId, nodeBId) { 10950 for (const model of this.database.Models.values()) { 10951 if (model.type === EntityType.NODE_CONNECTION) { 10952 const conn = model.entity; 10953 if (conn.nodeAId === nodeAId && conn.nodeBId === nodeBId || conn.nodeAId === nodeBId && conn.nodeBId === nodeAId) { 10954 return true; 10955 } 10956 } 10957 } 10958 return false; 10959 } 10960}; 10961
10962// src/app/pages/campus-map-v3/utils/room-polygon-draw-controller.ts 10963var CLOSE_THRESHOLD = 40; 10964var RoomPolygonDrawController = class { 10965 constructor(config) { 10966 this.active = false; 10967 this.originalCursor = ""; 10968 this.canvas = config.canvas; 10969 this.camera = config.camera; 10970 this.rotatableGroups = config.rotatableGroups; 10971 this.levelRenderer = config.levelRenderer; 10972 this.shapeEditState = config.shapeEditState; 10973 this.onPolygonComplete = config.onPolygonComplete; 10974 this.onCancel = config.onCancel; 10975 this.onDirty = config.onDirty; 10976 this.boundClick = this.handleClick.bind(this); 10977 this.boundMouseMove = this.handleMouseMove.bind(this); 10978 this.boundTouchEnd = this.handleTouchEnd.bind(this); 10979 this.boundKeyDown = this.handleKeyDown.bind(this); 10980 } 10981 get isActive() { 10982 return this.active; 10983 } 10984 /** 10985 * Activate draw mode. 10986 */ 10987 activate() { 10988 if (this.active) 10989 return; 10990 this.active = true; 10991 this.originalCursor = this.canvas.style.cursor; 10992 this.canvas.style.cursor = "crosshair"; 10993 this.shapeEditState.enterDrawMode(); 10994 this.canvas.addEventListener("click", this.boundClick); 10995 this.canvas.addEventListener("mousemove", this.boundMouseMove); 10996 this.canvas.addEventListener("touchend", this.boundTouchEnd, { passive: false }); 10997 window.addEventListener("keydown", this.boundKeyDown); 10998 this.onDirty(); 10999 } 11000 /** 11001 * Deactivate draw mode and clean up. 11002 */ 11003 deactivate() { 11004 if (!this.active) 11005 return; 11006 this.active = false; 11007 this.canvas.style.cursor = this.originalCursor; 11008 this.levelRenderer.clearPolygonPreview(); 11009 this.shapeEditState.exitMode(); 11010 this.canvas.removeEventListener("click", this.boundClick); 11011 this.canvas.removeEventListener("mousemove", this.boundMouseMove); 11012 this.canvas.removeEventListener("touchend", this.boundTouchEnd); 11013 window.removeEventListener("keydown", this.boundKeyDown); 11014 this.onDirty(); 11015 } 11016 /** 11017 * Dispose controller and clean up all resources. 11018 */ 11019 dispose() { 11020 this.deactivate(); 11021 } 11022 /** 11023 * Handle mouse click - add vertex or complete polygon. 11024 */ 11025 handleClick(e) { 11026 if (e.button !== 0) 11027 return; 11028 e.preventDefault(); 11029 e.stopPropagation(); 11030 const localPos = this.screenToLocal(e.clientX, e.clientY); 11031 this.handlePlacement(localPos); 11032 } 11033 /** 11034 * Handle mouse move - update cursor position for preview. 11035 */ 11036 handleMouseMove(e) { 11037 const localPos = this.screenToLocal(e.clientX, e.clientY); 11038 this.shapeEditState.setCursorPosition(localPos); 11039 this.updatePreview(); 11040 } 11041 /** 11042 * Handle touch end - add vertex or complete polygon. 11043 */ 11044 handleTouchEnd(e) { 11045 if (e.changedTouches.length !== 1) 11046 return; 11047 e.preventDefault(); 11048 const touch = e.changedTouches[0]; 11049 const localPos = this.screenToLocal(touch.clientX, touch.clientY); 11050 this.handlePlacement(localPos); 11051 } 11052 /** 11053 * Handle keyboard shortcuts. 11054 */ 11055 handleKeyDown(e) { 11056 if (isEditableKeyboardEvent(e)) 11057 return; 11058 if (e.key === "Escape") { 11059 e.preventDefault(); 11060 e.stopImmediatePropagation(); 11061 this.onCancel(); 11062 } else if (e.key === "Backspace") { 11063 e.preventDefault(); 11064 e.stopImmediatePropagation(); 11065 this.removeLastVertex(); 11066 } else if (e.key === "Enter") { 11067 e.preventDefault(); 11068 e.stopImmediatePropagation(); 11069 this.tryComplete(); 11070 } 11071 } 11072 /** 11073 * Handle vertex placement at the given local position. 11074 */ 11075 handlePlacement(localPos) { 11076 const vertices = this.shapeEditState.drawingVertices(); 11077 const canClose = this.shapeEditState.canClosePolygon(); 11078 if (canClose && vertices.length >= 3) { 11079 const firstVertex = vertices[0]; 11080 const distance = this.getDistance(localPos, firstVertex); 11081 if (distance <= CLOSE_THRESHOLD) { 11082 this.completePolygon(); 11083 return; 11084 } 11085 } 11086 if (vertices.length === 0) { 11087 const floorId = this.levelRenderer.getFloorAtPosition(localPos.x, localPos.y); 11088 if (!floorId) { 11089 return; 11090 } 11091 this.shapeEditState.setTargetFloor(floorId); 11092 } 11093 this.shapeEditState.addDrawingVertex(localPos); 11094 this.updatePreview(); 11095 } 11096 /** 11097 * Remove the last placed vertex. 11098 */ 11099 removeLastVertex() { 11100 if (this.shapeEditState.removeLastDrawingVertex()) { 11101 this.updatePreview(); 11102 } 11103 } 11104 /** 11105 * Try to complete the polygon (if enough vertices). 11106 */ 11107 tryComplete() { 11108 if (this.shapeEditState.canClosePolygon()) { 11109 this.completePolygon(); 11110 } 11111 } 11112 /** 11113 * Complete the polygon and invoke callback. 11114 */ 11115 completePolygon() { 11116 const vertices = this.shapeEditState.getDrawingVerticesCopy(); 11117 const floorId = this.shapeEditState.targetFloorId(); 11118 if (vertices.length >= 3 && floorId) { 11119 this.levelRenderer.clearPolygonPreview(); 11120 this.onPolygonComplete(vertices, floorId); 11121 } 11122 } 11123 /** 11124 * Update the polygon preview rendering. 11125 */ 11126 updatePreview() { 11127 const vertices = this.shapeEditState.drawingVertices(); 11128 const cursor = this.shapeEditState.cursorPosition(); 11129 const canClose = this.checkCanClose(cursor); 11130 this.levelRenderer.renderPolygonDrawPreview(vertices, cursor, canClose); 11131 this.onDirty(); 11132 } 11133 /** 11134 * Check if cursor is close enough to first vertex to close. 11135 */ 11136 checkCanClose(cursor) { 11137 if (!cursor) 11138 return false;
11139 const vertices = this.shapeEditState.drawingVertices(); 11140 if (vertices.length < 3) 11141 return false; 11142 const firstVertex = vertices[0]; 11143 const distance = this.getDistance(cursor, firstVertex); 11144 return distance <= CLOSE_THRESHOLD; 11145 } 11146 /** 11147 * Convert screen coordinates to local (group) coordinates. 11148 */ 11149 screenToLocal(clientX, clientY) { 11150 return screenToLocal(clientX, clientY, this.canvas, this.camera, this.rotatableGroups); 11151 } 11152 /** 11153 * Calculate distance between two points. 11154 */ 11155 getDistance(a, b) { 11156 const dx = a.x - b.x; 11157 const dy = a.y - b.y; 11158 return Math.sqrt(dx * dx + dy * dy); 11159 } 11160}; 11161
11162// src/app/pages/campus-map-v3/utils/floor-outline-draw-controller.ts 11163var CLOSE_THRESHOLD2 = 40; 11164var FloorOutlineDrawController = class { 11165 constructor(config) { 11166 this.active = false; 11167 this.activationData = null; 11168 this.originalCursor = ""; 11169 this.canvas = config.canvas; 11170 this.camera = config.camera; 11171 this.rotatableGroups = config.rotatableGroups; 11172 this.levelRenderer = config.levelRenderer; 11173 this.shapeEditState = config.shapeEditState; 11174 this.onPolygonComplete = config.onPolygonComplete; 11175 this.onCancel = config.onCancel; 11176 this.onDirty = config.onDirty; 11177 this.boundClick = this.handleClick.bind(this); 11178 this.boundMouseMove = this.handleMouseMove.bind(this); 11179 this.boundTouchEnd = this.handleTouchEnd.bind(this); 11180 this.boundKeyDown = this.handleKeyDown.bind(this); 11181 } 11182 get isActive() { 11183 return this.active; 11184 } 11185 /** 11186 * Activate draw mode for creating a new floor with outline. 11187 * @param floorData Data for the new floor to be created 11188 */ 11189 activateForNewFloor(floorData) { 11190 this.activateInternal({ mode: "create", data: floorData }); 11191 } 11192 /** 11193 * Activate draw mode for adding outline to an existing floor. 11194 * @param floorId The existing floor to draw outline for 11195 */ 11196 activate(floorId) { 11197 this.activateInternal({ mode: "edit", floorId }); 11198 } 11199 /** 11200 * Internal activation with typed data. 11201 */ 11202 activateInternal(data) { 11203 if (this.active) 11204 return; 11205 this.active = true; 11206 this.activationData = data; 11207 this.originalCursor = this.canvas.style.cursor; 11208 this.canvas.style.cursor = "crosshair"; 11209 this.shapeEditState.enterDrawMode(); 11210 const targetId = data.mode === "edit" ? data.floorId : "new-floor-pending"; 11211 this.shapeEditState.setTargetFloor(targetId); 11212 this.canvas.addEventListener("click", this.boundClick); 11213 this.canvas.addEventListener("mousemove", this.boundMouseMove); 11214 this.canvas.addEventListener("touchend", this.boundTouchEnd, { passive: false }); 11215 window.addEventListener("keydown", this.boundKeyDown); 11216 this.onDirty(); 11217 } 11218 /** 11219 * Deactivate draw mode and clean up. 11220 */ 11221 deactivate() { 11222 if (!this.active) 11223 return; 11224 this.active = false; 11225 this.activationData = null; 11226 this.canvas.style.cursor = this.originalCursor; 11227 this.levelRenderer.clearPolygonPreview(); 11228 this.shapeEditState.exitMode(); 11229 this.canvas.removeEventListener("click", this.boundClick); 11230 this.canvas.removeEventListener("mousemove", this.boundMouseMove); 11231 this.canvas.removeEventListener("touchend", this.boundTouchEnd); 11232 window.removeEventListener("keydown", this.boundKeyDown); 11233 this.onDirty(); 11234 } 11235 /** 11236 * Dispose controller and clean up all resources. 11237 */ 11238 dispose() { 11239 this.deactivate(); 11240 } 11241 /** 11242 * Handle mouse click - add vertex or complete polygon. 11243 */ 11244 handleClick(e) { 11245 if (e.button !== 0) 11246 return; 11247 e.preventDefault(); 11248 e.stopPropagation(); 11249 const localPos = this.screenToLocal(e.clientX, e.clientY); 11250 this.handlePlacement(localPos); 11251 } 11252 /** 11253 * Handle mouse move - update cursor position for preview. 11254 */ 11255 handleMouseMove(e) { 11256 const localPos = this.screenToLocal(e.clientX, e.clientY); 11257 this.shapeEditState.setCursorPosition(localPos); 11258 this.updatePreview(); 11259 } 11260 /** 11261 * Handle touch end - add vertex or complete polygon. 11262 */ 11263 handleTouchEnd(e) { 11264 if (e.changedTouches.length !== 1) 11265 return; 11266 e.preventDefault(); 11267 const touch = e.changedTouches[0]; 11268 const localPos = this.screenToLocal(touch.clientX, touch.clientY); 11269 this.handlePlacement(localPos); 11270 } 11271 /** 11272 * Handle keyboard shortcuts. 11273 */ 11274 handleKeyDown(e) { 11275 if (isEditableKeyboardEvent(e)) 11276 return; 11277 if (e.key === "Escape") { 11278 e.preventDefault(); 11279 e.stopImmediatePropagation(); 11280 this.onCancel(); 11281 } else if (e.key === "Backspace") { 11282 e.preventDefault(); 11283 e.stopImmediatePropagation(); 11284 this.removeLastVertex(); 11285 } else if (e.key === "Enter") { 11286 e.preventDefault(); 11287 e.stopImmediatePropagation(); 11288 this.tryComplete(); 11289 } 11290 } 11291 /** 11292 * Handle vertex placement at the given local position. 11293 */ 11294 handlePlacement(localPos) { 11295 const vertices = this.shapeEditState.drawingVertices(); 11296 const canClose = this.shapeEditState.canClosePolygon(); 11297 if (canClose && vertices.length >= 3) { 11298 const firstVertex = vertices[0]; 11299 const distance = this.getDistance(localPos, firstVertex); 11300 if (distance <= CLOSE_THRESHOLD2) { 11301 this.completePolygon(); 11302 return; 11303 } 11304 } 11305 this.shapeEditState.addDrawingVertex(localPos); 11306 this.updatePreview(); 11307 } 11308 /** 11309 * Remove the last placed vertex. 11310 */ 11311 removeLastVertex() { 11312 if (this.shapeEditState.removeLastDrawingVertex()) { 11313 this.updatePreview(); 11314 } 11315 } 11316 /** 11317 * Try to complete the polygon (if enough vertices). 11318 */ 11319 tryComplete() { 11320 if (this.shapeEditState.canClosePolygon()) { 11321 this.completePolygon(); 11322 } 11323 } 11324 /** 11325 * Complete the polygon and invoke callback. 11326 */ 11327 completePolygon() { 11328 const vertices = this.shapeEditState.getDrawingVerticesCopy(); 11329 if (vertices.length >= 3 && this.activationData) { 11330 this.levelRenderer.clearPolygonPreview(); 11331 const completionData = this.activationData.mode === "create" ? { mode: "create", data: this.activationData.data, vertices } : { mode: "edit", floorId: this.activationData.floorId, vertices }; 11332 this.onPolygonComplete(completionData); 11333 } 11334 } 11335 /** 11336 * Update the polygon preview rendering. 11337 */ 11338 updatePreview() { 11339 const vertices = this.shapeEditState.drawingVertices(); 11340 const cursor = this.shapeEditState.cursorPosition(); 11341 const canClose = this.checkCanClose(cursor); 11342 this.levelRenderer.renderPolygonDrawPreview(vertices, cursor, canClose); 11343 this.onDirty(); 11344 } 11345 /** 11346 * Check if cursor is close enough to first vertex to close. 11347 */ 11348 checkCanClose(cursor) { 11349 if (!cursor) 11350 return false;
11351 const vertices = this.shapeEditState.drawingVertices(); 11352 if (vertices.length < 3) 11353 return false; 11354 const firstVertex = vertices[0]; 11355 const distance = this.getDistance(cursor, firstVertex); 11356 return distance <= CLOSE_THRESHOLD2; 11357 } 11358 /** 11359 * Convert screen coordinates to local (group) coordinates. 11360 */ 11361 screenToLocal(clientX, clientY) { 11362 return screenToLocal(clientX, clientY, this.canvas, this.camera, this.rotatableGroups); 11363 } 11364 /** 11365 * Calculate distance between two points. 11366 */ 11367 getDistance(a, b) { 11368 const dx = a.x - b.x; 11369 const dy = a.y - b.y; 11370 return Math.sqrt(dx * dx + dy * dy); 11371 } 11372}; 11373
11374// src/app/pages/campus-map-v3/utils/ground-patch-polygon-draw-controller.ts 11375var CLOSE_THRESHOLD3 = 40; 11376var GroundPatchPolygonDrawController = class { 11377 constructor(config) { 11378 this.active = false; 11379 this.targetCampusId = null; 11380 this.originalCursor = ""; 11381 this.canvas = config.canvas; 11382 this.camera = config.camera; 11383 this.rotatableGroups = config.rotatableGroups; 11384 this.levelRenderer = config.levelRenderer; 11385 this.shapeEditState = config.shapeEditState; 11386 this.onPolygonComplete = config.onPolygonComplete; 11387 this.onCancel = config.onCancel; 11388 this.onDirty = config.onDirty; 11389 this.boundClick = this.handleClick.bind(this); 11390 this.boundMouseMove = this.handleMouseMove.bind(this); 11391 this.boundTouchEnd = this.handleTouchEnd.bind(this); 11392 this.boundKeyDown = this.handleKeyDown.bind(this); 11393 } 11394 get isActive() { 11395 return this.active; 11396 } 11397 /** 11398 * Activate draw mode for a specific campus. 11399 * @param campusId The campus to create ground patch in 11400 */ 11401 activate(campusId) { 11402 if (this.active) 11403 return; 11404 this.active = true; 11405 this.targetCampusId = campusId; 11406 this.originalCursor = this.canvas.style.cursor; 11407 this.canvas.style.cursor = "crosshair"; 11408 this.shapeEditState.enterDrawMode(); 11409 this.canvas.addEventListener("click", this.boundClick); 11410 this.canvas.addEventListener("mousemove", this.boundMouseMove); 11411 this.canvas.addEventListener("touchend", this.boundTouchEnd, { passive: false }); 11412 window.addEventListener("keydown", this.boundKeyDown); 11413 this.onDirty(); 11414 } 11415 /** 11416 * Deactivate draw mode and clean up. 11417 */ 11418 deactivate() { 11419 if (!this.active) 11420 return; 11421 this.active = false; 11422 this.targetCampusId = null; 11423 this.canvas.style.cursor = this.originalCursor; 11424 this.levelRenderer.clearPolygonPreview(); 11425 this.shapeEditState.exitMode(); 11426 this.canvas.removeEventListener("click", this.boundClick); 11427 this.canvas.removeEventListener("mousemove", this.boundMouseMove); 11428 this.canvas.removeEventListener("touchend", this.boundTouchEnd); 11429 window.removeEventListener("keydown", this.boundKeyDown); 11430 this.onDirty(); 11431 } 11432 /** 11433 * Dispose controller and clean up all resources. 11434 */ 11435 dispose() { 11436 this.deactivate(); 11437 } 11438 /** 11439 * Handle mouse click - add vertex or complete polygon. 11440 */ 11441 handleClick(e) { 11442 if (e.button !== 0) 11443 return; 11444 e.preventDefault(); 11445 e.stopPropagation(); 11446 const localPos = this.screenToLocal(e.clientX, e.clientY); 11447 this.handlePlacement(localPos); 11448 } 11449 /** 11450 * Handle mouse move - update cursor position for preview. 11451 */ 11452 handleMouseMove(e) { 11453 const localPos = this.screenToLocal(e.clientX, e.clientY); 11454 this.shapeEditState.setCursorPosition(localPos); 11455 this.updatePreview(); 11456 } 11457 /** 11458 * Handle touch end - add vertex or complete polygon. 11459 */ 11460 handleTouchEnd(e) { 11461 if (e.changedTouches.length !== 1) 11462 return; 11463 e.preventDefault(); 11464 const touch = e.changedTouches[0]; 11465 const localPos = this.screenToLocal(touch.clientX, touch.clientY); 11466 this.handlePlacement(localPos); 11467 } 11468 /** 11469 * Handle keyboard shortcuts. 11470 */ 11471 handleKeyDown(e) { 11472 if (isEditableKeyboardEvent(e)) 11473 return; 11474 if (e.key === "Escape") { 11475 e.preventDefault(); 11476 e.stopImmediatePropagation(); 11477 this.onCancel(); 11478 } else if (e.key === "Backspace") { 11479 e.preventDefault(); 11480 e.stopImmediatePropagation(); 11481 this.removeLastVertex(); 11482 } else if (e.key === "Enter") { 11483 e.preventDefault(); 11484 e.stopImmediatePropagation(); 11485 this.tryComplete(); 11486 } 11487 } 11488 /** 11489 * Handle vertex placement at the given local position. 11490 */ 11491 handlePlacement(localPos) { 11492 const vertices = this.shapeEditState.drawingVertices(); 11493 const canClose = this.shapeEditState.canClosePolygon(); 11494 if (canClose && vertices.length >= 3) { 11495 const firstVertex = vertices[0]; 11496 const distance = this.getDistance(localPos, firstVertex); 11497 if (distance <= CLOSE_THRESHOLD3) { 11498 this.completePolygon(); 11499 return; 11500 } 11501 } 11502 this.shapeEditState.addDrawingVertex(localPos); 11503 this.updatePreview(); 11504 } 11505 /** 11506 * Remove the last placed vertex. 11507 */ 11508 removeLastVertex() { 11509 if (this.shapeEditState.removeLastDrawingVertex()) { 11510 this.updatePreview(); 11511 } 11512 } 11513 /** 11514 * Try to complete the polygon (if enough vertices). 11515 */ 11516 tryComplete() { 11517 if (this.shapeEditState.canClosePolygon()) { 11518 this.completePolygon(); 11519 } 11520 } 11521 /** 11522 * Complete the polygon and invoke callback. 11523 */ 11524 completePolygon() { 11525 const vertices = this.shapeEditState.getDrawingVerticesCopy(); 11526 if (vertices.length >= 3 && this.targetCampusId) { 11527 this.levelRenderer.clearPolygonPreview(); 11528 this.onPolygonComplete(vertices, this.targetCampusId); 11529 } 11530 } 11531 /** 11532 * Update the polygon preview rendering. 11533 */ 11534 updatePreview() { 11535 const vertices = this.shapeEditState.drawingVertices(); 11536 const cursor = this.shapeEditState.cursorPosition(); 11537 const canClose = this.checkCanClose(cursor); 11538 this.levelRenderer.renderPolygonDrawPreview(vertices, cursor, canClose); 11539 this.onDirty(); 11540 } 11541 /** 11542 * Check if cursor is close enough to first vertex to close. 11543 */ 11544 checkCanClose(cursor) { 11545 if (!cursor) 11546 return false;
11547 const vertices = this.shapeEditState.drawingVertices(); 11548 if (vertices.length < 3) 11549 return false; 11550 const firstVertex = vertices[0]; 11551 const distance = this.getDistance(cursor, firstVertex); 11552 return distance <= CLOSE_THRESHOLD3; 11553 } 11554 /** 11555 * Convert screen coordinates to local (group) coordinates. 11556 */ 11557 screenToLocal(clientX, clientY) { 11558 return screenToLocal(clientX, clientY, this.canvas, this.camera, this.rotatableGroups); 11559 } 11560 /** 11561 * Calculate distance between two points. 11562 */ 11563 getDistance(a, b) { 11564 const dx = a.x - b.x; 11565 const dy = a.y - b.y; 11566 return Math.sqrt(dx * dx + dy * dy); 11567 } 11568}; 11569
11570// src/app/pages/campus-map-v3/utils/room-wall-geometry.ts 11571var MIN_WALL_LENGTH_CM = 1; 11572var EPSILON = 0.01; 11573function getRoomWallGeometry(room, startCornerId, endCornerId) { 11574 var _a3; 11575 const corners = getActiveCorners(room); 11576 const edgeIndex = corners.findIndex((corner) => corner.id === startCornerId); 11577 if (edgeIndex < 0 || corners.length < 3) 11578 return null; 11579 const start = corners[edgeIndex]; 11580 const end = corners[(edgeIndex + 1) % corners.length]; 11581 if (endCornerId && end.id !== endCornerId) 11582 return null; 11583 const dx = end.x - start.x; 11584 const dy = end.y - start.y; 11585 const lengthCm = Math.hypot(dx, dy); 11586 if (lengthCm < EPSILON) 11587 return null; 11588 const midpoint = { 11589 x: (start.x + end.x) / 2, 11590 y: (start.y + end.y) / 2 11591 }; 11592 const unit = { x: dx / lengthCm, y: dy / lengthCm }; 11593 return { 11594 roomId: (_a3 = room.entity.id) != null ? _a3 : "", 11595 edgeIndex, 11596 startCornerId: start.id, 11597 endCornerId: end.id, 11598 start: { x: start.x, y: start.y }, 11599 end: { x: end.x, y: end.y }, 11600 midpoint, 11601 unit, 11602 lengthCm, 11603 minimumLengthCm: calculateMinimumLengthForDoors(room, edgeIndex, midpoint, unit) 11604 }; 11605} 11606function calculateSymmetricWallResize(wall, targetLengthCm) { 11607 const halfLength = targetLengthCm / 2; 11608 return { 11609 start: { 11610 x: wall.midpoint.x - wall.unit.x * halfLength, 11611 y: wall.midpoint.y - wall.unit.y * halfLength 11612 }, 11613 end: { 11614 x: wall.midpoint.x + wall.unit.x * halfLength, 11615 y: wall.midpoint.y + wall.unit.y * halfLength 11616 } 11617 }; 11618} 11619function validateRoomWallResize(room, wall, targetLengthCm) { 11620 const resized = calculateSymmetricWallResize(wall, targetLengthCm); 11621 const minimumLengthCm = Math.max(MIN_WALL_LENGTH_CM, wall.minimumLengthCm); 11622 if (!Number.isFinite(targetLengthCm) || targetLengthCm < MIN_WALL_LENGTH_CM) { 11623 return invalid(resized, minimumLengthCm, "minimum"); 11624 } 11625 if (targetLengthCm + EPSILON < wall.minimumLengthCm) { 11626 return invalid(resized, minimumLengthCm, "doors"); 11627 } 11628 const original = getActiveCorners(room); 11629 const points = original.map((corner) => { 11630 if (corner.id === wall.startCornerId) 11631 return resized.start; 11632 if (corner.id === wall.endCornerId) 11633 return resized.end; 11634 return { x: corner.x, y: corner.y }; 11635 }); 11636 if (points.some((point, index) => PointUtils.distance(point, points[(index + 1) % points.length]) < MIN_WALL_LENGTH_CM)) { 11637 return invalid(resized, minimumLengthCm, "degenerate"); 11638 } 11639 const originalArea = PointUtils.polygonSignedArea(original); 11640 const resizedArea = PointUtils.polygonSignedArea(points); 11641 if (Math.abs(resizedArea) < EPSILON || Math.sign(originalArea) !== 0 && Math.sign(resizedArea) !== Math.sign(originalArea)) { 11642 return invalid(resized, minimumLengthCm, "degenerate"); 11643 } 11644 if (hasSelfIntersection(points)) { 11645 return invalid(resized, minimumLengthCm, "selfIntersection"); 11646 } 11647 return __spreadProps(__spreadValues({ 11648 valid: true 11649 }, resized), { 11650 minimumLengthCm, 11651 error: null 11652 }); 11653} 11654function getActiveCorners(room) { 11655 return room.corners.filter((corner) => !corner.entity.isDeleted && !!corner.entity.id).sort((a, b) => { 11656 var _a3, _b2; 11657 return ((_a3 = a.entity.position) != null ? _a3 : 0) - ((_b2 = b.entity.position) != null ? _b2 : 0); 11658 }).map((corner) => { 11659 var _a3, _b2; 11660 return { 11661 id: corner.entity.id, 11662 x: (_a3 = corner.entity.locationX) != null ? _a3 : 0, 11663 y: (_b2 = corner.entity.locationY) != null ? _b2 : 0 11664 }; 11665 }); 11666} 11667function calculateMinimumLengthForDoors(room, selectedEdgeIndex, midpoint, unit) { 11668 var _a3, _b2, _c; 11669 const corners = getActiveCorners(room); 11670 let requiredHalfLength = 0; 11671 for (const door of room.doors.filter((candidate) => !candidate.entity.isDeleted)) { 11672 const doorPoint = { 11673 x: (_a3 = door.entity.locationX) != null ? _a3 : 0, 11674 y: (_b2 = door.entity.locationY) != null ? _b2 : 0 11675 }; 11676 if (findNearestEdgeIndex(corners, doorPoint) !== selectedEdgeIndex) 11677 continue; 11678 const alongWall = Math.abs((doorPoint.x - midpoint.x) * unit.x + (doorPoint.y - midpoint.y) * unit.y); 11679 requiredHalfLength = Math.max(requiredHalfLength, alongWall + Math.max((_c = door.entity.width) != null ? _c : 0, 0) / 2); 11680 } 11681 return Math.max(MIN_WALL_LENGTH_CM, Math.ceil(requiredHalfLength * 2 - EPSILON)); 11682} 11683function findNearestEdgeIndex(corners, point) { 11684 let bestIndex = -1; 11685 let bestDistance = Number.POSITIVE_INFINITY; 11686 for (let index = 0; index < corners.length; index++) { 11687 const distance = pointToSegmentDistance(point, corners[index], corners[(index + 1) % corners.length]); 11688 if (distance < bestDistance - EPSILON) { 11689 bestDistance = distance; 11690 bestIndex = index; 11691 } 11692 } 11693 return bestIndex; 11694} 11695function pointToSegmentDistance(point, start, end) { 11696 const dx = end.x - start.x; 11697 const dy = end.y - start.y; 11698 const lengthSquared = dx * dx + dy * dy; 11699 if (lengthSquared < EPSILON) 11700 return PointUtils.distance(point, start); 11701 const t = Math.max(0, Math.min(1, ((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSquared)); 11702 return Math.hypot(point.x - (start.x + t * dx), point.y - (start.y + t * dy)); 11703} 11704function hasSelfIntersection(points) { 11705 for (let first = 0; first < points.length; first++) { 11706 const firstNext = (first + 1) % points.length; 11707 for (let second = first + 1; second < points.length; second++) { 11708 const secondNext = (second + 1) % points.length; 11709 if (first === second || firstNext === second || secondNext === first) 11710 continue; 11711 if (PointUtils.lineSegmentIntersection(points[first], points[firstNext], points[second], points[secondNext])) { 11712 return true; 11713 } 11714 } 11715 } 11716 return false;
11717} 11718function invalid(resized, minimumLengthCm, error) { 11719 return __spreadProps(__spreadValues({ valid: false }, resized), { minimumLengthCm, error }); 11720} 11721
11722// src/app/pages/campus-map-v3/utils/polygon-shape-edit-controller.ts 11723var CORNER_HIT_RADIUS = 35; 11724var MIDPOINT_HIT_RADIUS = 25; 11725var DRAG_THRESHOLD = 5; 11726var PolygonShapeEditController = class { 11727 constructor(config) { 11728 this.active = false; 11729 this.activeEntityId = null; 11730 this.isDragging = false; 11731 this.draggedCornerId = null; 11732 this.dragStartScreen = null; 11733 this.dragStartWorld = null; 11734 this.cornerStartPosition = null; 11735 this.selectedWallBase = null; 11736 this.wallPreviewLengthCm = null; 11737 this.wallPreviewInvalid = false; 11738 this.isWallResizing = false; 11739 this.wallResizeHandle = null; 11740 this.originalCursor = ""; 11741 this.cameraToggleCallback = null; 11742 this.zoomUnsubscribe = null; 11743 this.canvas = config.canvas; 11744 this.camera = config.camera; 11745 this.rotatableGroups = config.rotatableGroups; 11746 this.levelRenderer = config.levelRenderer; 11747 this.database = config.database; 11748 this.historyService = config.historyService; 11749 this.shapeEditState = config.shapeEditState; 11750 this.adapter = config.adapter; 11751 this.commandFactory = config.commandFactory; 11752 this.onExit = config.onExit; 11753 this.onDirty = config.onDirty; 11754 this.onVisualUpdate = config.onVisualUpdate; 11755 this.subscribeZoomChange = config.onZoomChange; 11756 this.boundMouseDown = this.handleMouseDown.bind(this); 11757 this.boundMouseMove = this.handleMouseMove.bind(this); 11758 this.boundMouseUp = this.handleMouseUp.bind(this); 11759 this.boundKeyDown = this.handleKeyDown.bind(this); 11760 } 11761 get isActive() { 11762 return this.active; 11763 } 11764 /** 11765 * Set callback for enabling/disabling camera during corner drag. 11766 */ 11767 setCameraToggleCallback(callback) { 11768 this.cameraToggleCallback = callback; 11769 } 11770 /** 11771 * Activate edit mode for an entity. 11772 */ 11773 activate(entityId) { 11774 if (this.active) { 11775 this.deactivate(); 11776 } 11777 this.active = true; 11778 this.activeEntityId = entityId; 11779 this.originalCursor = this.canvas.style.cursor; 11780 this.canvas.style.cursor = "default"; 11781 this.shapeEditState.enterEditMode(entityId, this.adapter.entityTypeName); 11782 this.renderHandles(); 11783 if (this.subscribeZoomChange) { 11784 this.zoomUnsubscribe = this.subscribeZoomChange(() => { 11785 this.renderHandles(); 11786 this.refreshSelectedEdgeOverlay(true); 11787 }); 11788 } 11789 this.canvas.addEventListener("mousedown", this.boundMouseDown); 11790 this.canvas.addEventListener("mousemove", this.boundMouseMove); 11791 this.canvas.addEventListener("mouseup", this.boundMouseUp); 11792 window.addEventListener("keydown", this.boundKeyDown); 11793 this.onDirty(); 11794 } 11795 /** 11796 * Deactivate edit mode and clean up. 11797 */ 11798 deactivate() { 11799 if (!this.active) 11800 return; 11801 this.active = false; 11802 this.activeEntityId = null; 11803 this.canvas.style.cursor = this.originalCursor; 11804 this.levelRenderer.clearCornerHandles(); 11805 this.levelRenderer.clearEdgeMidpoints(); 11806 this.levelRenderer.clearRoomWallDimension(); 11807 this.shapeEditState.exitMode(); 11808 this.resetDragState(); 11809 this.selectedWallBase = null; 11810 this.wallPreviewLengthCm = null; 11811 this.wallPreviewInvalid = false; 11812 this.isWallResizing = false; 11813 this.wallResizeHandle = null; 11814 if (this.zoomUnsubscribe) { 11815 this.zoomUnsubscribe(); 11816 this.zoomUnsubscribe = null; 11817 } 11818 this.canvas.removeEventListener("mousedown", this.boundMouseDown); 11819 this.canvas.removeEventListener("mousemove", this.boundMouseMove); 11820 this.canvas.removeEventListener("mouseup", this.boundMouseUp); 11821 window.removeEventListener("keydown", this.boundKeyDown); 11822 this.onDirty(); 11823 } 11824 /** 11825 * Dispose controller. 11826 */ 11827 dispose() { 11828 this.deactivate(); 11829 } 11830 /** 11831 * Handle mouse down - start potential drag or select corner. 11832 */ 11833 handleMouseDown(e) { 11834 var _a3, _b2; 11835 if (e.button !== 0) 11836 return; 11837 const localPos = this.screenToLocal(e.clientX, e.clientY); 11838 const selectedWall = this.shapeEditState.selectedRoomWall(); 11839 if (selectedWall && this.supportsWallLengthEditing()) { 11840 const handle = this.hitTestSelectedWallEndpoint(localPos, selectedWall); 11841 if (handle) { 11842 e.preventDefault(); 11843 e.stopPropagation(); 11844 (_a3 = this.cameraToggleCallback) == null ? void 0 : _a3.call(this, false); 11845 this.isWallResizing = true; 11846 this.wallResizeHandle = handle; 11847 this.canvas.style.cursor = "ew-resize"; 11848 return; 11849 } 11850 } 11851 const hitCornerId = this.hitTestCorners(localPos); 11852 if (hitCornerId) { 11853 e.preventDefault(); 11854 e.stopPropagation(); 11855 this.clearSelectedRoomWall(); 11856 (_b2 = this.cameraToggleCallback) == null ? void 0 : _b2.call(this, false); 11857 this.draggedCornerId = hitCornerId; 11858 this.dragStartScreen = { x: e.clientX, y: e.clientY }; 11859 this.dragStartWorld = localPos; 11860 const corner = this.adapter.getCornerById(hitCornerId); 11861 if (corner) { 11862 this.cornerStartPosition = this.adapter.getCornerPosition(corner); 11863 } 11864 const mode = e.ctrlKey || e.metaKey ? "toggle" : "replace"; 11865 this.shapeEditState.selectCorner(hitCornerId, mode); 11866 this.renderHandles(); 11867 return; 11868 } 11869 const hitMidpoint = this.hitTestMidpoints(localPos); 11870 if (hitMidpoint) { 11871 e.preventDefault(); 11872 e.stopPropagation(); 11873 this.addCornerAtMidpoint(hitMidpoint.edgeIndex, hitMidpoint.position); 11874 return; 11875 } 11876 const hitEdge = this.hitTestEdges(localPos); 11877 if (hitEdge && this.supportsWallLengthEditing()) { 11878 e.preventDefault(); 11879 e.stopPropagation(); 11880 this.clearSelectedRoomWall(); 11881 this.selectRoomWall(hitEdge.edgeIndex); 11882 return; 11883 } 11884 this.shapeEditState.deselectAllCorners(); 11885 this.clearSelectedRoomWall(); 11886 this.renderHandles(); 11887 } 11888 /** 11889 * Handle mouse move - update hover state or drag corner. 11890 */ 11891 handleMouseMove(e) { 11892 var _a3, _b2; 11893 const localPos = this.screenToLocal(e.clientX, e.clientY); 11894 if (this.isWallResizing && this.selectedWallBase && this.wallResizeHandle) { 11895 const fromCenter = { 11896 x: localPos.x - this.selectedWallBase.midpoint.x, 11897 y: localPos.y - this.selectedWallBase.midpoint.y 11898 }; 11899 const projected = fromCenter.x * this.selectedWallBase.unit.x + fromCenter.y * this.selectedWallBase.unit.y; 11900 const targetLengthCm = Math.round(Math.abs((this.wallResizeHandle === "end" ? projected : -projected) * 2)); 11901 this.previewSelectedEdgeLength(targetLengthCm); 11902 return; 11903 } 11904 if (this.draggedCornerId && this.dragStartScreen && this.cornerStartPosition) { 11905 const dx = e.clientX - this.dragStartScreen.x; 11906 const dy = e.clientY - this.dragStartScreen.y; 11907 const distance = Math.sqrt(dx * dx + dy * dy); 11908 if (distance > DRAG_THRESHOLD || this.isDragging) { 11909 if (!this.isDragging) { 11910 this.isDragging = true; 11911 this.canvas.style.cursor = "grabbing"; 11912 } 11913 const corner = this.adapter.getCornerById(this.draggedCornerId); 11914 if (corner) { 11915 const worldDx = localPos.x - this.dragStartWorld.x; 11916 const worldDy = localPos.y - this.dragStartWorld.y; 11917 this.adapter.setCornerPosition(corner, this.cornerStartPosition.x + worldDx, this.cornerStartPosition.y + worldDy); 11918 this.renderHandles(); 11919 this.onDirty(); 11920 } 11921 } 11922 return; 11923 } 11924 const hoveredCornerId = this.hitTestCorners(localPos); 11925 let hoveredEdgeIndex = null; 11926 this.shapeEditState.setHoveredCorner(hoveredCornerId); 11927 if (hoveredCornerId) { 11928 this.canvas.style.cursor = "grab"; 11929 this.shapeEditState.setHoveredEdge(null); 11930 this.levelRenderer.clearEdgeMidpoints(); 11931 } else { 11932 const hoveredEdge = this.hitTestEdges(localPos); 11933 hoveredEdgeIndex = (_a3 = hoveredEdge == null ? void 0 : hoveredEdge.edgeIndex) != null ? _a3 : null; 11934 this.shapeEditState.setHoveredEdge((_b2 = hoveredEdge == null ? void 0 : hoveredEdge.edgeIndex) != null ? _b2 : null); 11935 if (hoveredEdge) { 11936 this.canvas.style.cursor = "crosshair"; 11937 const parent = this.adapter.getParent(this.activeEntityId); 11938 if (parent) { 11939 const midpoints = this.getMidpoints(parent); 11940 this.levelRenderer.renderEdgeMidpoints(midpoints, hoveredEdge.edgeIndex); 11941 } 11942 } else { 11943 this.canvas.style.cursor = "default"; 11944 this.levelRenderer.clearEdgeMidpoints(); 11945 } 11946 } 11947 this.renderHandles(); 11948 this.renderHoveredOrSelectedWall(hoveredEdgeIndex); 11949 } 11950 /** 11951 * Handle mouse up - complete drag operation. 11952 */ 11953 handleMouseUp(_e) { 11954 var _a3; 11955 if (this.isWallResizing) { 11956 (_a3 = this.cameraToggleCallback) == null ? void 0 : _a3.call(this, true); 11957 this.isWallResizing = false;
11958 this.wallResizeHandle = null; 11959 this.canvas.style.cursor = "default"; 11960 if (this.wallPreviewLengthCm !== null && !this.shapeEditState.wallLengthError()) { 11961 this.commitSelectedEdgeLength(); 11962 } else { 11963 this.cancelSelectedEdgeLengthPreview(); 11964 } 11965 return; 11966 } 11967 if (!this.draggedCornerId) 11968 return; 11969 if (this.isDragging && this.cornerStartPosition) { 11970 const corner = this.adapter.getCornerById(this.draggedCornerId); 11971 if (corner) { 11972 const currentPos = this.adapter.getCornerPosition(corner); 11973 const deltaX = currentPos.x - this.cornerStartPosition.x; 11974 const deltaY = currentPos.y - this.cornerStartPosition.y; 11975 this.adapter.setCornerPosition(corner, this.cornerStartPosition.x, this.cornerStartPosition.y); 11976 if (Math.abs(deltaX) > 0.01 || Math.abs(deltaY) > 0.01) { 11977 const command = this.commandFactory.createMoveCornerCommand({ 11978 cornerId: this.draggedCornerId, 11979 deltaX, 11980 deltaY, 11981 database: this.database, 11982 onVisualUpdate: () => { 11983 this.onVisualUpdate(); 11984 this.renderHandles(); 11985 } 11986 }); 11987 this.historyService.executeCommand(command); 11988 } 11989 } 11990 } 11991 this.resetDragState(); 11992 this.canvas.style.cursor = "default"; 11993 this.renderHandles(); 11994 } 11995 /** 11996 * Handle keyboard shortcuts. 11997 */ 11998 handleKeyDown(e) { 11999 if (isEditableKeyboardEvent(e)) 12000 return; 12001 if (e.key === "Escape") { 12002 e.preventDefault(); 12003 e.stopImmediatePropagation(); 12004 if (this.shapeEditState.selectedRoomWall()) { 12005 this.cancelSelectedEdgeLengthPreview(); 12006 this.clearSelectedRoomWall(); 12007 } else { 12008 this.onExit(); 12009 } 12010 } else if (e.key === "Delete" || e.key === "Backspace") { 12011 e.preventDefault(); 12012 e.stopImmediatePropagation(); 12013 this.deleteSelectedCorner(); 12014 } 12015 } 12016 /** 12017 * Delete the currently selected corner. 12018 */ 12019 deleteSelectedCorner() { 12020 const selectedIds = this.shapeEditState.getSelectedCornerIds(); 12021 if (selectedIds.length !== 1) 12022 return; 12023 const cornerId = selectedIds[0]; 12024 const command = this.commandFactory.createRemoveCornerCommand({ 12025 cornerId, 12026 database: this.database, 12027 onVisualUpdate: () => { 12028 this.onVisualUpdate(); 12029 this.renderHandles(); 12030 } 12031 }); 12032 this.historyService.executeCommand(command); 12033 this.shapeEditState.deselectAllCorners(); 12034 this.clearSelectedRoomWall(); 12035 } 12036 /** 12037 * Add a corner at the midpoint of an edge. 12038 */ 12039 addCornerAtMidpoint(edgeIndex, position) { 12040 if (!this.activeEntityId) 12041 return; 12042 const command = this.commandFactory.createAddCornerCommand({ 12043 parentId: this.activeEntityId, 12044 afterPosition: edgeIndex, 12045 location: position, 12046 database: this.database, 12047 onVisualUpdate: () => { 12048 this.onVisualUpdate(); 12049 this.renderHandles(); 12050 } 12051 }); 12052 this.historyService.executeCommand(command); 12053 this.clearSelectedRoomWall(); 12054 } 12055 // ========================================================================= 12056 // Room Wall Length Editing 12057 // ========================================================================= 12058 previewSelectedEdgeLength(targetLengthCm) { 12059 if (!this.selectedWallBase || !this.activeEntityId || !this.supportsWallLengthEditing()) 12060 return false; 12061 const room = this.adapter.getParent(this.activeEntityId); 12062 if (!room) 12063 return false; 12064 if (!Number.isFinite(targetLengthCm)) { 12065 this.wallPreviewInvalid = true; 12066 this.shapeEditState.setWallLengthError("invalid"); 12067 return false; 12068 } 12069 const result = validateRoomWallResize(room, this.selectedWallBase, targetLengthCm); 12070 if (!result.valid) { 12071 this.wallPreviewInvalid = true; 12072 this.shapeEditState.setWallLengthError(result.error); 12073 return false; 12074 } 12075 const startCorner = this.adapter.getCornerById(this.selectedWallBase.startCornerId); 12076 const endCorner = this.adapter.getCornerById(this.selectedWallBase.endCornerId); 12077 if (!startCorner || !endCorner) 12078 return false;
12079 this.adapter.setCornerPosition(startCorner, result.start.x, result.start.y); 12080 this.adapter.setCornerPosition(endCorner, result.end.x, result.end.y); 12081 this.wallPreviewLengthCm = targetLengthCm; 12082 this.wallPreviewInvalid = false; 12083 this.shapeEditState.setWallLengthError(null); 12084 this.onVisualUpdate(); 12085 this.refreshSelectedEdgeOverlay(); 12086 return true; 12087 } 12088 commitSelectedEdgeLength() { 12089 if (!this.selectedWallBase || this.wallPreviewLengthCm === null || this.wallPreviewInvalid || !this.activeEntityId || !this.commandFactory.createResizeEdgeCommand) 12090 return false; 12091 const room = this.adapter.getParent(this.activeEntityId); 12092 if (!room) 12093 return false; 12094 const result = validateRoomWallResize(room, this.selectedWallBase, this.wallPreviewLengthCm); 12095 if (!result.valid) { 12096 this.shapeEditState.setWallLengthError(result.error); 12097 return false; 12098 } 12099 const startCorner = this.adapter.getCornerById(this.selectedWallBase.startCornerId); 12100 const endCorner = this.adapter.getCornerById(this.selectedWallBase.endCornerId); 12101 if (!startCorner || !endCorner) 12102 return false; 12103 this.adapter.setCornerPosition(startCorner, this.selectedWallBase.start.x, this.selectedWallBase.start.y); 12104 this.adapter.setCornerPosition(endCorner, this.selectedWallBase.end.x, this.selectedWallBase.end.y); 12105 const targetLengthCm = this.wallPreviewLengthCm; 12106 this.wallPreviewLengthCm = null; 12107 this.wallPreviewInvalid = false; 12108 const command = this.commandFactory.createResizeEdgeCommand({ 12109 parentId: this.activeEntityId, 12110 startCornerId: this.selectedWallBase.startCornerId, 12111 endCornerId: this.selectedWallBase.endCornerId, 12112 startPosition: result.start, 12113 endPosition: result.end, 12114 targetLengthCm, 12115 database: this.database, 12116 onVisualUpdate: () => this.onVisualUpdate() 12117 }); 12118 this.historyService.executeCommand(command); 12119 const updated = getRoomWallGeometry(room, this.selectedWallBase.startCornerId, this.selectedWallBase.endCornerId); 12120 if (updated) 12121 this.selectedWallBase = updated; 12122 this.shapeEditState.setWallLengthError(null); 12123 this.refreshSelectedEdgeOverlay(); 12124 this.renderHandles(); 12125 return true; 12126 } 12127 cancelSelectedEdgeLengthPreview() { 12128 if (!this.selectedWallBase || this.wallPreviewLengthCm === null) { 12129 this.wallPreviewInvalid = false; 12130 this.shapeEditState.setWallLengthError(null); 12131 return; 12132 } 12133 const startCorner = this.adapter.getCornerById(this.selectedWallBase.startCornerId); 12134 const endCorner = this.adapter.getCornerById(this.selectedWallBase.endCornerId); 12135 if (startCorner && endCorner) { 12136 this.adapter.setCornerPosition(startCorner, this.selectedWallBase.start.x, this.selectedWallBase.start.y); 12137 this.adapter.setCornerPosition(endCorner, this.selectedWallBase.end.x, this.selectedWallBase.end.y); 12138 } 12139 this.wallPreviewLengthCm = null; 12140 this.wallPreviewInvalid = false; 12141 this.shapeEditState.setWallLengthError(null); 12142 this.onVisualUpdate(); 12143 this.refreshSelectedEdgeOverlay(); 12144 this.renderHandles(); 12145 } 12146 refreshSelectedEdgeOverlay(forceRender = false) { 12147 const selected = this.shapeEditState.selectedRoomWall(); 12148 if (!selected || !this.activeEntityId || !this.supportsWallLengthEditing()) 12149 return; 12150 const room = this.adapter.getParent(this.activeEntityId); 12151 if (!room) 12152 return; 12153 const geometry = getRoomWallGeometry(room, selected.startCornerId, selected.endCornerId); 12154 if (!geometry) { 12155 this.clearSelectedRoomWall(); 12156 return; 12157 } 12158 const state = this.toSelectedRoomWall(geometry); 12159 if (this.wallPreviewLengthCm === null && !this.isWallResizing) { 12160 this.selectedWallBase = geometry; 12161 } 12162 const geometryChanged = Math.abs(selected.lengthCm - state.lengthCm) > 0.01 || Math.abs(selected.start.x - state.start.x) > 0.01 || Math.abs(selected.start.y - state.start.y) > 0.01 || Math.abs(selected.end.x - state.end.x) > 0.01 || Math.abs(selected.end.y - state.end.y) > 0.01; 12163 if (Math.abs(selected.screenX - state.screenX) > 0.25 || Math.abs(selected.screenY - state.screenY) > 0.25 || Math.abs(selected.lengthCm - state.lengthCm) > 0.01 || selected.minimumLengthCm !== state.minimumLengthCm) { 12164 this.shapeEditState.updateSelectedRoomWall(state); 12165 } 12166 if (forceRender || geometryChanged) { 12167 this.levelRenderer.renderRoomWallDimension(geometry, true); 12168 } 12169 } 12170 selectRoomWall(edgeIndex) { 12171 if (!this.activeEntityId || !this.supportsWallLengthEditing()) 12172 return; 12173 this.cancelSelectedEdgeLengthPreview(); 12174 const room = this.adapter.getParent(this.activeEntityId); 12175 if (!room) 12176 return; 12177 const corners = this.adapter.getCorners(room); 12178 const start = corners[edgeIndex]; 12179 const end = corners[(edgeIndex + 1) % corners.length]; 12180 if (!start || !end) 12181 return; 12182 const geometry = getRoomWallGeometry(room, start.id, end.id); 12183 if (!geometry) 12184 return; 12185 this.selectedWallBase = geometry; 12186 this.wallPreviewLengthCm = null; 12187 this.wallPreviewInvalid = false;
12188 this.shapeEditState.selectRoomWall(this.toSelectedRoomWall(geometry)); 12189 this.levelRenderer.clearEdgeMidpoints(); 12190 this.levelRenderer.renderRoomWallDimension(geometry, true); 12191 this.renderHandles(); 12192 } 12193 clearSelectedRoomWall() { 12194 this.cancelSelectedEdgeLengthPreview(); 12195 this.selectedWallBase = null; 12196 this.wallPreviewLengthCm = null; 12197 this.wallPreviewInvalid = false; 12198 this.shapeEditState.clearSelectedRoomWall(); 12199 this.levelRenderer.clearRoomWallDimension(); 12200 } 12201 renderHoveredOrSelectedWall(hoveredEdgeIndex) { 12202 if (!this.supportsWallLengthEditing() || !this.activeEntityId) 12203 return; 12204 if (this.shapeEditState.selectedRoomWall()) { 12205 this.refreshSelectedEdgeOverlay(); 12206 return; 12207 } 12208 if (hoveredEdgeIndex === null) { 12209 this.levelRenderer.clearRoomWallDimension(); 12210 return; 12211 } 12212 const room = this.adapter.getParent(this.activeEntityId); 12213 if (!room) 12214 return; 12215 const corners = this.adapter.getCorners(room); 12216 const start = corners[hoveredEdgeIndex]; 12217 const end = corners[(hoveredEdgeIndex + 1) % corners.length]; 12218 if (!start || !end) 12219 return; 12220 const geometry = getRoomWallGeometry(room, start.id, end.id); 12221 if (geometry) 12222 this.levelRenderer.renderRoomWallDimension(geometry, false); 12223 } 12224 toSelectedRoomWall(geometry) { 12225 const worldMidpoint = localToWorld(geometry.midpoint, getSceneTransform(this.rotatableGroups)); 12226 const canvasPosition = worldToScreen(worldMidpoint.x, worldMidpoint.y, this.canvas, this.camera); 12227 const rect = this.canvas.getBoundingClientRect(); 12228 return __spreadProps(__spreadValues({}, geometry), { 12229 screenX: rect.left + canvasPosition.x, 12230 screenY: rect.top + canvasPosition.y 12231 }); 12232 } 12233 supportsWallLengthEditing() { 12234 return this.adapter.entityTypeName === "room" && !!this.commandFactory.createResizeEdgeCommand; 12235 } 12236 hitTestSelectedWallEndpoint(localPos, wall) { 12237 if (this.getDistance(localPos, wall.start) <= CORNER_HIT_RADIUS) 12238 return "start"; 12239 if (this.getDistance(localPos, wall.end) <= CORNER_HIT_RADIUS) 12240 return "end"; 12241 return null; 12242 } 12243 /** 12244 * Render corner handles for the active entity. 12245 */ 12246 renderHandles() { 12247 if (!this.activeEntityId) 12248 return; 12249 const parent = this.adapter.getParent(this.activeEntityId); 12250 if (!parent) 12251 return; 12252 const selectedIds = this.shapeEditState.selectedCornerIds(); 12253 const hoveredId = this.shapeEditState.hoveredCornerId(); 12254 const selectedWall = this.shapeEditState.selectedRoomWall(); 12255 const corners = this.adapter.getCorners(parent).map((c) => ({ 12256 id: c.id, 12257 x: c.x, 12258 y: c.y, 12259 selected: selectedIds.has(c.id) || (selectedWall == null ? void 0 : selectedWall.startCornerId) === c.id || (selectedWall == null ? void 0 : selectedWall.endCornerId) === c.id, 12260 hovered: c.id === hoveredId 12261 })); 12262 this.levelRenderer.renderCornerHandles(corners); 12263 this.onDirty(); 12264 } 12265 /** 12266 * Hit test against corner handles. 12267 */ 12268 hitTestCorners(localPos) { 12269 if (!this.activeEntityId) 12270 return null; 12271 const parent = this.adapter.getParent(this.activeEntityId); 12272 if (!parent) 12273 return null; 12274 const corners = this.adapter.getCorners(parent); 12275 for (const corner of corners) { 12276 const distance = this.getDistance(localPos, { x: corner.x, y: corner.y }); 12277 if (distance <= CORNER_HIT_RADIUS) { 12278 return corner.id; 12279 } 12280 } 12281 return null; 12282 } 12283 /** 12284 * Hit test against edge midpoints. 12285 */ 12286 hitTestMidpoints(localPos) { 12287 if (!this.activeEntityId) 12288 return null; 12289 const parent = this.adapter.getParent(this.activeEntityId); 12290 if (!parent) 12291 return null; 12292 const corners = this.adapter.getCorners(parent); 12293 for (let i = 0; i < corners.length; i++) { 12294 const current = corners[i]; 12295 const next = corners[(i + 1) % corners.length]; 12296 const midpoint = { 12297 x: (current.x + next.x) / 2, 12298 y: (current.y + next.y) / 2 12299 }; 12300 const distance = this.getDistance(localPos, midpoint); 12301 if (distance <= MIDPOINT_HIT_RADIUS) { 12302 return { edgeIndex: i, position: midpoint }; 12303 } 12304 } 12305 return null; 12306 } 12307 /** 12308 * Hit test against edges (for hover detection). 12309 */ 12310 hitTestEdges(localPos) { 12311 if (!this.activeEntityId) 12312 return null; 12313 const parent = this.adapter.getParent(this.activeEntityId); 12314 if (!parent) 12315 return null; 12316 const corners = this.adapter.getCorners(parent); 12317 const EDGE_HIT_DISTANCE = 30; 12318 for (let i = 0; i < corners.length; i++) { 12319 const current = corners[i]; 12320 const next = corners[(i + 1) % corners.length]; 12321 const p1 = { x: current.x, y: current.y }; 12322 const p2 = { x: next.x, y: next.y }; 12323 const distance = this.pointToLineDistance(localPos, p1, p2); 12324 if (distance <= EDGE_HIT_DISTANCE) { 12325 if (this.isPointOnSegment(localPos, p1, p2, EDGE_HIT_DISTANCE)) { 12326 return { edgeIndex: i }; 12327 } 12328 } 12329 } 12330 return null; 12331 } 12332 /** 12333 * Get midpoints for all edges. 12334 */ 12335 getMidpoints(parent) { 12336 const corners = this.adapter.getCorners(parent); 12337 const midpoints = []; 12338 for (let i = 0; i < corners.length; i++) { 12339 const current = corners[i]; 12340 const next = corners[(i + 1) % corners.length]; 12341 midpoints.push({ 12342 edgeIndex: i, 12343 x: (current.x + next.x) / 2, 12344 y: (current.y + next.y) / 2 12345 }); 12346 } 12347 return midpoints; 12348 } 12349 /** 12350 * Reset drag state. 12351 */ 12352 resetDragState() { 12353 var _a3; 12354 if (this.draggedCornerId) { 12355 (_a3 = this.cameraToggleCallback) == null ? void 0 : _a3.call(this, true); 12356 } 12357 this.isDragging = false;
12358 this.draggedCornerId = null; 12359 this.dragStartScreen = null; 12360 this.dragStartWorld = null; 12361 this.cornerStartPosition = null; 12362 } 12363 /** 12364 * Convert screen coordinates to local (world) coordinates. 12365 */ 12366 screenToLocal(clientX, clientY) { 12367 return screenToLocal(clientX, clientY, this.canvas, this.camera, this.rotatableGroups); 12368 } 12369 /** 12370 * Calculate distance between two points. 12371 */ 12372 getDistance(a, b) { 12373 const dx = a.x - b.x; 12374 const dy = a.y - b.y; 12375 return Math.sqrt(dx * dx + dy * dy); 12376 } 12377 /** 12378 * Calculate perpendicular distance from a point to a line segment. 12379 */ 12380 pointToLineDistance(point, lineStart, lineEnd) { 12381 const A = point.x - lineStart.x; 12382 const B = point.y - lineStart.y; 12383 const C = lineEnd.x - lineStart.x; 12384 const D = lineEnd.y - lineStart.y; 12385 const dot = A * C + B * D; 12386 const lenSq = C * C + D * D; 12387 if (lenSq === 0) { 12388 return this.getDistance(point, lineStart); 12389 } 12390 let t = dot / lenSq; 12391 t = Math.max(0, Math.min(1, t)); 12392 const projection = { 12393 x: lineStart.x + t * C, 12394 y: lineStart.y + t * D 12395 }; 12396 return this.getDistance(point, projection); 12397 } 12398 /** 12399 * Check if a point is roughly on a line segment (with tolerance). 12400 */ 12401 isPointOnSegment(point, lineStart, lineEnd, tolerance) { 12402 const minX = Math.min(lineStart.x, lineEnd.x) - tolerance; 12403 const maxX = Math.max(lineStart.x, lineEnd.x) + tolerance; 12404 const minY = Math.min(lineStart.y, lineEnd.y) - tolerance; 12405 const maxY = Math.max(lineStart.y, lineEnd.y) + tolerance; 12406 return point.x >= minX && point.x <= maxX && point.y >= minY && point.y <= maxY; 12407 } 12408}; 12409
12410// src/app/pages/campus-map-v3/utils/adapters/room-polygon-adapter.ts 12411var RoomPolygonAdapter = class { 12412 constructor(database) { 12413 this.database = database; 12414 this.entityTypeName = "room"; 12415 } 12416 getParent(parentId) { 12417 return this.database.Models.get(parentId); 12418 } 12419 getCorners(parent) { 12420 return parent.corners.filter((c) => !c.entity.isDeleted).sort((a, b) => { 12421 var _a3, _b2; 12422 return ((_a3 = a.entity.position) != null ? _a3 : 0) - ((_b2 = b.entity.position) != null ? _b2 : 0); 12423 }).map((c) => { 12424 var _a3, _b2, _c; 12425 return { 12426 id: c.entity.id, 12427 x: (_a3 = c.entity.locationX) != null ? _a3 : 0, 12428 y: (_b2 = c.entity.locationY) != null ? _b2 : 0, 12429 position: (_c = c.entity.position) != null ? _c : 0 12430 }; 12431 }); 12432 } 12433 getCornerById(cornerId) { 12434 return this.database.Models.get(cornerId); 12435 } 12436 getCornerPosition(corner) { 12437 var _a3, _b2; 12438 return { 12439 x: (_a3 = corner.entity.locationX) != null ? _a3 : 0, 12440 y: (_b2 = corner.entity.locationY) != null ? _b2 : 0 12441 }; 12442 } 12443 setCornerPosition(corner, x, y) { 12444 corner.entity.locationX = x; 12445 corner.entity.locationY = y; 12446 } 12447 markCornerChanged(corner) { 12448 corner.entity.isChanged = true; 12449 } 12450 getCornerCount(parent) { 12451 return parent.corners.filter((c) => !c.entity.isDeleted).length; 12452 } 12453 getParentId(corner) { 12454 var _a3; 12455 return (_a3 = corner.entity.roomId) != null ? _a3 : ""; 12456 } 12457 /** 12458 * Recalculate room centroid from all active corners. 12459 * Called after corners are moved, added, or removed. 12460 */ 12461 onCornersChanged(parent) { 12462 const corners = parent.corners.filter((c) => !c.entity.isDeleted); 12463 if (corners.length === 0) 12464 return; 12465 const x = corners.reduce((sum, c) => { 12466 var _a3; 12467 return sum + ((_a3 = c.entity.locationX) != null ? _a3 : 0); 12468 }, 0) / corners.length; 12469 const y = corners.reduce((sum, c) => { 12470 var _a3; 12471 return sum + ((_a3 = c.entity.locationY) != null ? _a3 : 0); 12472 }, 0) / corners.length; 12473 parent.entity.locationX = x; 12474 parent.entity.locationY = y; 12475 parent.entity.isChanged = true; 12476 } 12477}; 12478
12479// src/app/pages/campus-map-v3/utils/adapters/floor-polygon-adapter.ts 12480var FloorPolygonAdapter = class { 12481 constructor(database) { 12482 this.database = database; 12483 this.entityTypeName = "floor"; 12484 } 12485 getParent(parentId) { 12486 return this.database.Models.get(parentId); 12487 } 12488 getCorners(parent) { 12489 return parent.corners.filter((c) => !c.entity.isDeleted).sort((a, b) => { 12490 var _a3, _b2; 12491 return ((_a3 = a.entity.position) != null ? _a3 : 0) - ((_b2 = b.entity.position) != null ? _b2 : 0); 12492 }).map((c) => { 12493 var _a3, _b2, _c; 12494 return { 12495 id: c.entity.id, 12496 x: (_a3 = c.entity.locationX) != null ? _a3 : 0, 12497 y: (_b2 = c.entity.locationY) != null ? _b2 : 0, 12498 position: (_c = c.entity.position) != null ? _c : 0 12499 }; 12500 }); 12501 } 12502 getCornerById(cornerId) { 12503 return this.database.Models.get(cornerId); 12504 } 12505 getCornerPosition(corner) { 12506 var _a3, _b2; 12507 return { 12508 x: (_a3 = corner.entity.locationX) != null ? _a3 : 0, 12509 y: (_b2 = corner.entity.locationY) != null ? _b2 : 0 12510 }; 12511 } 12512 setCornerPosition(corner, x, y) { 12513 corner.entity.locationX = x; 12514 corner.entity.locationY = y; 12515 } 12516 markCornerChanged(corner) { 12517 corner.entity.isChanged = true; 12518 } 12519 getCornerCount(parent) { 12520 return parent.corners.filter((c) => !c.entity.isDeleted).length; 12521 } 12522 getParentId(corner) { 12523 var _a3; 12524 return (_a3 = corner.entity.floorId) != null ? _a3 : ""; 12525 } 12526 /** 12527 * No-op for floors. Floors don't have a centroid property. 12528 */ 12529 onCornersChanged(_parent) { 12530 } 12531}; 12532
12533// src/app/pages/campus-map-v3/utils/adapters/ground-patch-polygon-adapter.ts 12534var GroundPatchPolygonAdapter = class { 12535 constructor(database) { 12536 this.database = database; 12537 this.entityTypeName = "groundPatch"; 12538 } 12539 getParent(parentId) { 12540 return this.database.Models.get(parentId); 12541 } 12542 getCorners(parent) { 12543 return parent.corners.filter((c) => !c.entity.isDeleted).sort((a, b) => { 12544 var _a3, _b2; 12545 return ((_a3 = a.entity.position) != null ? _a3 : 0) - ((_b2 = b.entity.position) != null ? _b2 : 0); 12546 }).map((c) => { 12547 var _a3, _b2, _c; 12548 return { 12549 id: c.entity.id, 12550 x: (_a3 = c.entity.locationX) != null ? _a3 : 0, 12551 y: (_b2 = c.entity.locationY) != null ? _b2 : 0, 12552 position: (_c = c.entity.position) != null ? _c : 0 12553 }; 12554 }); 12555 } 12556 getCornerById(cornerId) { 12557 return this.database.Models.get(cornerId); 12558 } 12559 getCornerPosition(corner) { 12560 var _a3, _b2; 12561 return { 12562 x: (_a3 = corner.entity.locationX) != null ? _a3 : 0, 12563 y: (_b2 = corner.entity.locationY) != null ? _b2 : 0 12564 }; 12565 } 12566 setCornerPosition(corner, x, y) { 12567 corner.entity.locationX = x; 12568 corner.entity.locationY = y; 12569 } 12570 markCornerChanged(corner) { 12571 corner.entity.isChanged = true; 12572 } 12573 getCornerCount(parent) { 12574 return parent.corners.filter((c) => !c.entity.isDeleted).length; 12575 } 12576 getParentId(corner) { 12577 var _a3; 12578 return (_a3 = corner.entity.groundPatchId) != null ? _a3 : ""; 12579 } 12580 /** 12581 * No-op for ground patches. Ground patches don't have a centroid property. 12582 */ 12583 onCornersChanged(_parent) { 12584 } 12585}; 12586
12587// src/app/pages/campus-map-v3/commands/move-room-corner-command.ts 12588var MoveRoomCornerCommand = class extends BaseEditorCommand { 12589 constructor(config) { 12590 super(); 12591 this.description = "Move room corner"; 12592 this.roomId = null; 12593 this.originalCentroid = null; 12594 this.cornerId = config.cornerId; 12595 this.deltaX = config.deltaX; 12596 this.deltaY = config.deltaY; 12597 this.database = config.database; 12598 this.onVisualUpdate = config.onVisualUpdate; 12599 } 12600 execute() { 12601 var _a3, _b2, _c, _d, _e; 12602 const cornerModel = this.database.Models.get(this.cornerId); 12603 if (!cornerModel) 12604 return; 12605 cornerModel.entity.locationX = ((_a3 = cornerModel.entity.locationX) != null ? _a3 : 0) + this.deltaX; 12606 cornerModel.entity.locationY = ((_b2 = cornerModel.entity.locationY) != null ? _b2 : 0) + this.deltaY; 12607 cornerModel.entity.isChanged = true; 12608 const room = cornerModel.room; 12609 if (room) { 12610 this.roomId = (_c = room.entity.id) != null ? _c : null; 12611 if (!this.originalCentroid) { 12612 this.originalCentroid = { 12613 x: (_d = room.entity.locationX) != null ? _d : 0, 12614 y: (_e = room.entity.locationY) != null ? _e : 0 12615 }; 12616 } 12617 const newCentroid = this.calculateRoomCentroid(room); 12618 room.entity.locationX = newCentroid.x; 12619 room.entity.locationY = newCentroid.y; 12620 room.entity.isChanged = true; 12621 } 12622 this.onVisualUpdate(); 12623 } 12624 undo() { 12625 var _a3, _b2; 12626 const cornerModel = this.database.Models.get(this.cornerId); 12627 if (!cornerModel) 12628 return; 12629 cornerModel.entity.locationX = ((_a3 = cornerModel.entity.locationX) != null ? _a3 : 0) - this.deltaX; 12630 cornerModel.entity.locationY = ((_b2 = cornerModel.entity.locationY) != null ? _b2 : 0) - this.deltaY; 12631 cornerModel.entity.isChanged = true; 12632 if (this.roomId && this.originalCentroid) { 12633 const room = this.database.Models.get(this.roomId); 12634 if (room) { 12635 room.entity.locationX = this.originalCentroid.x; 12636 room.entity.locationY = this.originalCentroid.y; 12637 room.entity.isChanged = true; 12638 } 12639 } 12640 this.onVisualUpdate(); 12641 } 12642 /** 12643 * Calculate centroid from all active corners of a room. 12644 */ 12645 calculateRoomCentroid(room) { 12646 var _a3, _b2; 12647 const corners = room.corners.filter((c) => !c.entity.isDeleted); 12648 if (corners.length === 0) { 12649 return { x: (_a3 = room.entity.locationX) != null ? _a3 : 0, y: (_b2 = room.entity.locationY) != null ? _b2 : 0 }; 12650 } 12651 const x = corners.reduce((sum, c) => { 12652 var _a4; 12653 return sum + ((_a4 = c.entity.locationX) != null ? _a4 : 0); 12654 }, 0) / corners.length; 12655 const y = corners.reduce((sum, c) => { 12656 var _a4; 12657 return sum + ((_a4 = c.entity.locationY) != null ? _a4 : 0); 12658 }, 0) / corners.length; 12659 return { x, y }; 12660 } 12661}; 12662
12663// src/app/pages/campus-map-v3/commands/add-room-corner-command.ts 12664var AddRoomCornerCommand = class extends BaseEditorCommand { 12665 constructor(config) { 12666 super(); 12667 this.description = "Add room corner"; 12668 this.createdCornerId = null; 12669 this.cornerEntity = null; 12670 this.shiftedCornerPositions = /* @__PURE__ */ new Map(); 12671 this.roomId = config.roomId; 12672 this.afterPosition = config.afterPosition; 12673 this.location = config.location; 12674 this.database = config.database; 12675 this.onVisualUpdate = config.onVisualUpdate; 12676 } 12677 execute() { 12678 return __async(this, null, function* () { 12679 var _a3, _b2; 12680 const room = this.database.Models.get(this.roomId); 12681 if (!room) { 12682 return; 12683 } 12684 const newPosition = this.afterPosition + 1; 12685 for (const corner of room.corners) { 12686 const currentPos = (_a3 = corner.entity.position) != null ? _a3 : 0; 12687 if (currentPos >= newPosition) { 12688 if (!this.shiftedCornerPositions.has(corner.entity.id)) { 12689 this.shiftedCornerPositions.set(corner.entity.id, currentPos); 12690 } 12691 corner.entity.position = currentPos + 1; 12692 corner.entity.isChanged = true; 12693 } 12694 } 12695 this.cornerEntity = Object.assign(new RoomCorner(), { 12696 id: crypto.randomUUID(), 12697 campusId: (_b2 = room.campus) == null ? void 0 : _b2.entity.id, 12698 roomId: this.roomId, 12699 locationX: this.location.x, 12700 locationY: this.location.y, 12701 position: newPosition, 12702 isNextWallSolid: true, 12703 isDeleted: false, 12704 isChanged: true, 12705 version: 0 12706 }); 12707 this.createdCornerId = this.cornerEntity.id; 12708 yield this.database.addEntity(this.cornerEntity, false); 12709 const cornerModel = this.database.Models.get(this.createdCornerId); 12710 if (cornerModel) { 12711 this.database.fixModelRelations(cornerModel); 12712 } 12713 this.updateRoomCentroid(room); 12714 this.onVisualUpdate(); 12715 }); 12716 } 12717 undo() { 12718 if (!this.createdCornerId) 12719 return; 12720 const cornerModel = this.database.Models.get(this.createdCornerId); 12721 if (cornerModel) { 12722 cornerModel.entity.isDeleted = true; 12723 cornerModel.entity.isChanged = true; 12724 this.database.Models.delete(this.createdCornerId); 12725 } 12726 const room = this.database.Models.get(this.roomId); 12727 if (room) { 12728 for (const [cornerId, originalPos] of this.shiftedCornerPositions) { 12729 const corner = this.database.Models.get(cornerId); 12730 if (corner) { 12731 corner.entity.position = originalPos; 12732 corner.entity.isChanged = true; 12733 } 12734 } 12735 this.updateRoomCentroid(room); 12736 } 12737 this.onVisualUpdate(); 12738 } 12739 redo() { 12740 var _a3; 12741 if (!this.createdCornerId || !this.cornerEntity) { 12742 this.execute(); 12743 return; 12744 } 12745 const room = this.database.Models.get(this.roomId); 12746 if (!room) 12747 return; 12748 const newPosition = this.afterPosition + 1; 12749 for (const corner of room.corners) { 12750 const currentPos = (_a3 = corner.entity.position) != null ? _a3 : 0; 12751 if (currentPos >= newPosition) { 12752 corner.entity.position = currentPos + 1; 12753 corner.entity.isChanged = true; 12754 } 12755 } 12756 this.cornerEntity.isDeleted = false; 12757 this.cornerEntity.isChanged = true; 12758 const cornerModel = new RoomCornerModel(this.cornerEntity); 12759 this.database.Models.set(this.createdCornerId, cornerModel); 12760 this.database.fixModelRelations(cornerModel); 12761 this.updateRoomCentroid(room); 12762 this.onVisualUpdate(); 12763 } 12764 /** 12765 * Update room centroid from its corners. 12766 */ 12767 updateRoomCentroid(room) { 12768 const activeCorners = room.corners.filter((c) => !c.entity.isDeleted); 12769 if (activeCorners.length === 0) 12770 return; 12771 const x = activeCorners.reduce((sum, c) => { 12772 var _a3; 12773 return sum + ((_a3 = c.entity.locationX) != null ? _a3 : 0); 12774 }, 0) / activeCorners.length; 12775 const y = activeCorners.reduce((sum, c) => { 12776 var _a3; 12777 return sum + ((_a3 = c.entity.locationY) != null ? _a3 : 0); 12778 }, 0) / activeCorners.length; 12779 room.entity.locationX = x; 12780 room.entity.locationY = y; 12781 room.entity.isChanged = true; 12782 } 12783}; 12784
12785// src/app/pages/campus-map-v3/commands/remove-room-corner-command.ts 12786var RemoveRoomCornerCommand = class extends BaseEditorCommand { 12787 constructor(config) { 12788 super(); 12789 this.description = "Remove room corner"; 12790 this.roomId = null; 12791 this.cornerSnapshot = null; 12792 this.removedPosition = null; 12793 this.shiftedCornerPositions = /* @__PURE__ */ new Map(); 12794 this.wasExecuted = false; 12795 this.cornerId = config.cornerId; 12796 this.database = config.database; 12797 this.onVisualUpdate = config.onVisualUpdate; 12798 } 12799 execute() { 12800 var _a3, _b2, _c, _d; 12801 const cornerModel = this.database.Models.get(this.cornerId); 12802 if (!cornerModel || !cornerModel.room) { 12803 return; 12804 } 12805 const room = cornerModel.room; 12806 this.roomId = (_a3 = room.entity.id) != null ? _a3 : null; 12807 const activeCorners = room.corners.filter((c) => !c.entity.isDeleted); 12808 if (activeCorners.length <= 3) { 12809 return; 12810 } 12811 this.cornerSnapshot = { 12812 id: this.cornerId, 12813 type: "corner", 12814 data: JSON.parse(JSON.stringify(cornerModel.entity)), 12815 isDeleted: cornerModel.entity.isDeleted 12816 }; 12817 this.removedPosition = (_b2 = cornerModel.entity.position) != null ? _b2 : 0; 12818 for (const corner of room.corners) { 12819 const pos = (_c = corner.entity.position) != null ? _c : 0; 12820 if (pos > this.removedPosition && !corner.entity.isDeleted) { 12821 this.shiftedCornerPositions.set(corner.entity.id, pos); 12822 } 12823 } 12824 cornerModel.entity.isDeleted = true; 12825 cornerModel.entity.isChanged = true; 12826 for (const corner of room.corners) { 12827 const pos = (_d = corner.entity.position) != null ? _d : 0; 12828 if (pos > this.removedPosition && !corner.entity.isDeleted) { 12829 corner.entity.position = pos - 1; 12830 corner.entity.isChanged = true; 12831 } 12832 } 12833 this.updateRoomCentroid(room); 12834 this.wasExecuted = true; 12835 this.onVisualUpdate(); 12836 } 12837 undo() { 12838 if (!this.wasExecuted || !this.cornerSnapshot || !this.roomId) 12839 return; 12840 const cornerModel = this.database.Models.get(this.cornerId); 12841 if (cornerModel) { 12842 Object.assign(cornerModel.entity, this.cornerSnapshot.data); 12843 cornerModel.entity.isDeleted = false; 12844 cornerModel.entity.isChanged = true; 12845 } 12846 const room = this.database.Models.get(this.roomId); 12847 if (room) { 12848 for (const [cornerId, originalPos] of this.shiftedCornerPositions) { 12849 const corner = this.database.Models.get(cornerId); 12850 if (corner) { 12851 corner.entity.position = originalPos; 12852 corner.entity.isChanged = true; 12853 } 12854 } 12855 this.updateRoomCentroid(room); 12856 } 12857 this.onVisualUpdate(); 12858 } 12859 redo() { 12860 var _a3; 12861 if (!this.wasExecuted) 12862 return; 12863 const cornerModel = this.database.Models.get(this.cornerId); 12864 if (!cornerModel) 12865 return; 12866 const room = this.database.Models.get(this.roomId); 12867 if (!room) 12868 return; 12869 cornerModel.entity.isDeleted = true; 12870 cornerModel.entity.isChanged = true; 12871 for (const corner of room.corners) { 12872 const pos = (_a3 = corner.entity.position) != null ? _a3 : 0; 12873 if (pos > this.removedPosition && !corner.entity.isDeleted) { 12874 corner.entity.position = pos - 1; 12875 corner.entity.isChanged = true; 12876 } 12877 } 12878 this.updateRoomCentroid(room); 12879 this.onVisualUpdate(); 12880 } 12881 /** 12882 * Update room centroid from its corners. 12883 */ 12884 updateRoomCentroid(room) { 12885 const activeCorners = room.corners.filter((c) => !c.entity.isDeleted); 12886 if (activeCorners.length === 0) 12887 return; 12888 const x = activeCorners.reduce((sum, c) => { 12889 var _a3; 12890 return sum + ((_a3 = c.entity.locationX) != null ? _a3 : 0); 12891 }, 0) / activeCorners.length; 12892 const y = activeCorners.reduce((sum, c) => { 12893 var _a3; 12894 return sum + ((_a3 = c.entity.locationY) != null ? _a3 : 0); 12895 }, 0) / activeCorners.length; 12896 room.entity.locationX = x; 12897 room.entity.locationY = y; 12898 room.entity.isChanged = true; 12899 } 12900}; 12901
12902// src/app/pages/campus-map-v3/commands/move-floor-corner-command.ts 12903var MoveFloorCornerCommand = class extends BaseEditorCommand { 12904 constructor(config) { 12905 super(); 12906 this.description = "Move floor corner"; 12907 this.cornerId = config.cornerId; 12908 this.deltaX = config.deltaX; 12909 this.deltaY = config.deltaY; 12910 this.database = config.database; 12911 this.onVisualUpdate = config.onVisualUpdate; 12912 } 12913 execute() { 12914 var _a3, _b2; 12915 const cornerModel = this.database.Models.get(this.cornerId); 12916 if (!cornerModel) 12917 return; 12918 cornerModel.entity.locationX = ((_a3 = cornerModel.entity.locationX) != null ? _a3 : 0) + this.deltaX; 12919 cornerModel.entity.locationY = ((_b2 = cornerModel.entity.locationY) != null ? _b2 : 0) + this.deltaY; 12920 cornerModel.entity.isChanged = true; 12921 this.onVisualUpdate(); 12922 } 12923 undo() { 12924 var _a3, _b2; 12925 const cornerModel = this.database.Models.get(this.cornerId); 12926 if (!cornerModel) 12927 return; 12928 cornerModel.entity.locationX = ((_a3 = cornerModel.entity.locationX) != null ? _a3 : 0) - this.deltaX; 12929 cornerModel.entity.locationY = ((_b2 = cornerModel.entity.locationY) != null ? _b2 : 0) - this.deltaY; 12930 cornerModel.entity.isChanged = true; 12931 this.onVisualUpdate(); 12932 } 12933}; 12934
12935// src/app/pages/campus-map-v3/commands/add-floor-corner-command.ts 12936var AddFloorCornerCommand = class extends BaseEditorCommand { 12937 constructor(config) { 12938 super(); 12939 this.description = "Add floor corner"; 12940 this.createdCornerId = null; 12941 this.cornerEntity = null; 12942 this.shiftedCornerPositions = /* @__PURE__ */ new Map(); 12943 this.floorId = config.floorId; 12944 this.afterPosition = config.afterPosition; 12945 this.location = config.location; 12946 this.database = config.database; 12947 this.onVisualUpdate = config.onVisualUpdate; 12948 } 12949 execute() { 12950 return __async(this, null, function* () { 12951 var _a3, _b2; 12952 const floor = this.database.Models.get(this.floorId); 12953 if (!floor) { 12954 return; 12955 } 12956 const newPosition = this.afterPosition + 1; 12957 for (const corner of floor.corners) { 12958 const currentPos = (_a3 = corner.entity.position) != null ? _a3 : 0; 12959 if (currentPos >= newPosition) { 12960 if (!this.shiftedCornerPositions.has(corner.entity.id)) { 12961 this.shiftedCornerPositions.set(corner.entity.id, currentPos); 12962 } 12963 corner.entity.position = currentPos + 1; 12964 corner.entity.isChanged = true; 12965 } 12966 } 12967 this.cornerEntity = Object.assign(new FloorCorner(), { 12968 id: crypto.randomUUID(), 12969 campusId: (_b2 = floor.campus) == null ? void 0 : _b2.entity.id, 12970 floorId: this.floorId, 12971 locationX: this.location.x, 12972 locationY: this.location.y, 12973 position: newPosition, 12974 isDeleted: false, 12975 isChanged: true, 12976 version: 0 12977 }); 12978 this.createdCornerId = this.cornerEntity.id; 12979 yield this.database.addEntity(this.cornerEntity, false); 12980 const cornerModel = this.database.Models.get(this.createdCornerId); 12981 if (cornerModel) { 12982 this.database.fixModelRelations(cornerModel); 12983 } 12984 this.onVisualUpdate(); 12985 }); 12986 } 12987 undo() { 12988 if (!this.createdCornerId) 12989 return; 12990 const cornerModel = this.database.Models.get(this.createdCornerId); 12991 if (cornerModel) { 12992 cornerModel.entity.isDeleted = true; 12993 cornerModel.entity.isChanged = true; 12994 this.database.Models.delete(this.createdCornerId); 12995 } 12996 const floor = this.database.Models.get(this.floorId); 12997 if (floor) { 12998 for (const [cornerId, originalPos] of this.shiftedCornerPositions) { 12999 const corner = this.database.Models.get(cornerId); 13000 if (corner) { 13001 corner.entity.position = originalPos; 13002 corner.entity.isChanged = true; 13003 } 13004 } 13005 } 13006 this.onVisualUpdate(); 13007 } 13008 redo() { 13009 var _a3; 13010 if (!this.createdCornerId || !this.cornerEntity) { 13011 this.execute(); 13012 return; 13013 } 13014 const floor = this.database.Models.get(this.floorId); 13015 if (!floor) 13016 return; 13017 const newPosition = this.afterPosition + 1; 13018 for (const corner of floor.corners) { 13019 const currentPos = (_a3 = corner.entity.position) != null ? _a3 : 0; 13020 if (currentPos >= newPosition) { 13021 corner.entity.position = currentPos + 1; 13022 corner.entity.isChanged = true; 13023 } 13024 } 13025 this.cornerEntity.isDeleted = false; 13026 this.cornerEntity.isChanged = true; 13027 const cornerModel = new FloorCornerModel(this.cornerEntity); 13028 this.database.Models.set(this.createdCornerId, cornerModel); 13029 this.database.fixModelRelations(cornerModel); 13030 this.onVisualUpdate(); 13031 } 13032}; 13033
13034// src/app/pages/campus-map-v3/commands/remove-floor-corner-command.ts 13035var RemoveFloorCornerCommand = class extends BaseEditorCommand { 13036 constructor(config) { 13037 super(); 13038 this.description = "Remove floor corner"; 13039 this.floorId = null; 13040 this.cornerSnapshot = null; 13041 this.removedPosition = null; 13042 this.shiftedCornerPositions = /* @__PURE__ */ new Map(); 13043 this.wasExecuted = false; 13044 this.cornerId = config.cornerId; 13045 this.database = config.database; 13046 this.onVisualUpdate = config.onVisualUpdate; 13047 } 13048 execute() { 13049 var _a3, _b2, _c, _d; 13050 const cornerModel = this.database.Models.get(this.cornerId); 13051 if (!cornerModel || !cornerModel.floor) { 13052 return; 13053 } 13054 const floor = cornerModel.floor; 13055 this.floorId = (_a3 = floor.entity.id) != null ? _a3 : null; 13056 const activeCorners = floor.corners.filter((c) => !c.entity.isDeleted); 13057 if (activeCorners.length <= 3) { 13058 return; 13059 } 13060 this.cornerSnapshot = { 13061 id: this.cornerId, 13062 type: "corner", 13063 data: JSON.parse(JSON.stringify(cornerModel.entity)), 13064 isDeleted: cornerModel.entity.isDeleted 13065 }; 13066 this.removedPosition = (_b2 = cornerModel.entity.position) != null ? _b2 : 0; 13067 for (const corner of floor.corners) { 13068 const pos = (_c = corner.entity.position) != null ? _c : 0; 13069 if (pos > this.removedPosition && !corner.entity.isDeleted) { 13070 this.shiftedCornerPositions.set(corner.entity.id, pos); 13071 } 13072 } 13073 cornerModel.entity.isDeleted = true; 13074 cornerModel.entity.isChanged = true; 13075 for (const corner of floor.corners) { 13076 const pos = (_d = corner.entity.position) != null ? _d : 0; 13077 if (pos > this.removedPosition && !corner.entity.isDeleted) { 13078 corner.entity.position = pos - 1; 13079 corner.entity.isChanged = true; 13080 } 13081 } 13082 this.wasExecuted = true; 13083 this.onVisualUpdate(); 13084 } 13085 undo() { 13086 if (!this.wasExecuted || !this.cornerSnapshot || !this.floorId) 13087 return; 13088 const cornerModel = this.database.Models.get(this.cornerId); 13089 if (cornerModel) { 13090 Object.assign(cornerModel.entity, this.cornerSnapshot.data); 13091 cornerModel.entity.isDeleted = false; 13092 cornerModel.entity.isChanged = true; 13093 } 13094 const floor = this.database.Models.get(this.floorId); 13095 if (floor) { 13096 for (const [cornerId, originalPos] of this.shiftedCornerPositions) { 13097 const corner = this.database.Models.get(cornerId); 13098 if (corner) { 13099 corner.entity.position = originalPos; 13100 corner.entity.isChanged = true; 13101 } 13102 } 13103 } 13104 this.onVisualUpdate(); 13105 } 13106 redo() { 13107 var _a3; 13108 if (!this.wasExecuted) 13109 return; 13110 const cornerModel = this.database.Models.get(this.cornerId); 13111 if (!cornerModel) 13112 return; 13113 const floor = this.database.Models.get(this.floorId); 13114 if (!floor) 13115 return; 13116 cornerModel.entity.isDeleted = true; 13117 cornerModel.entity.isChanged = true; 13118 for (const corner of floor.corners) { 13119 const pos = (_a3 = corner.entity.position) != null ? _a3 : 0; 13120 if (pos > this.removedPosition && !corner.entity.isDeleted) { 13121 corner.entity.position = pos - 1; 13122 corner.entity.isChanged = true; 13123 } 13124 } 13125 this.onVisualUpdate(); 13126 } 13127}; 13128
13129// src/app/pages/campus-map-v3/commands/move-ground-patch-corner-command.ts 13130var MoveGroundPatchCornerCommand = class extends BaseEditorCommand { 13131 constructor(config) { 13132 super(); 13133 this.description = "Move ground patch corner"; 13134 this.cornerId = config.cornerId; 13135 this.deltaX = config.deltaX; 13136 this.deltaY = config.deltaY; 13137 this.database = config.database; 13138 this.onVisualUpdate = config.onVisualUpdate; 13139 } 13140 execute() { 13141 var _a3, _b2; 13142 const cornerModel = this.database.Models.get(this.cornerId); 13143 if (!cornerModel) 13144 return; 13145 cornerModel.entity.locationX = ((_a3 = cornerModel.entity.locationX) != null ? _a3 : 0) + this.deltaX; 13146 cornerModel.entity.locationY = ((_b2 = cornerModel.entity.locationY) != null ? _b2 : 0) + this.deltaY; 13147 cornerModel.entity.isChanged = true; 13148 this.onVisualUpdate(); 13149 } 13150 undo() { 13151 var _a3, _b2; 13152 const cornerModel = this.database.Models.get(this.cornerId); 13153 if (!cornerModel) 13154 return; 13155 cornerModel.entity.locationX = ((_a3 = cornerModel.entity.locationX) != null ? _a3 : 0) - this.deltaX; 13156 cornerModel.entity.locationY = ((_b2 = cornerModel.entity.locationY) != null ? _b2 : 0) - this.deltaY; 13157 cornerModel.entity.isChanged = true; 13158 this.onVisualUpdate(); 13159 } 13160}; 13161
13162// src/app/pages/campus-map-v3/commands/add-ground-patch-corner-command.ts 13163var AddGroundPatchCornerCommand = class extends BaseEditorCommand { 13164 constructor(config) { 13165 super(); 13166 this.description = "Add ground patch corner"; 13167 this.createdCornerId = null; 13168 this.cornerEntity = null; 13169 this.shiftedCornerPositions = /* @__PURE__ */ new Map(); 13170 this.groundPatchId = config.groundPatchId; 13171 this.afterPosition = config.afterPosition; 13172 this.location = config.location; 13173 this.database = config.database; 13174 this.onVisualUpdate = config.onVisualUpdate; 13175 } 13176 execute() { 13177 return __async(this, null, function* () { 13178 var _a3, _b2; 13179 const groundPatch = this.database.Models.get(this.groundPatchId); 13180 if (!groundPatch) { 13181 return; 13182 } 13183 const newPosition = this.afterPosition + 1; 13184 for (const corner of groundPatch.corners) { 13185 const currentPos = (_a3 = corner.entity.position) != null ? _a3 : 0; 13186 if (currentPos >= newPosition) { 13187 if (!this.shiftedCornerPositions.has(corner.entity.id)) { 13188 this.shiftedCornerPositions.set(corner.entity.id, currentPos); 13189 } 13190 corner.entity.position = currentPos + 1; 13191 corner.entity.isChanged = true; 13192 } 13193 } 13194 this.cornerEntity = Object.assign(new GroundPatchCorner(), { 13195 id: crypto.randomUUID(), 13196 campusId: (_b2 = groundPatch.campus) == null ? void 0 : _b2.entity.id, 13197 groundPatchId: this.groundPatchId, 13198 locationX: this.location.x, 13199 locationY: this.location.y, 13200 position: newPosition, 13201 isDeleted: false, 13202 isChanged: true, 13203 version: 0 13204 }); 13205 this.createdCornerId = this.cornerEntity.id; 13206 yield this.database.addEntity(this.cornerEntity, false); 13207 const cornerModel = this.database.Models.get(this.createdCornerId); 13208 if (cornerModel) { 13209 this.database.fixModelRelations(cornerModel); 13210 } 13211 this.onVisualUpdate(); 13212 }); 13213 } 13214 undo() { 13215 if (!this.createdCornerId) 13216 return; 13217 const cornerModel = this.database.Models.get(this.createdCornerId); 13218 if (cornerModel) { 13219 cornerModel.entity.isDeleted = true; 13220 cornerModel.entity.isChanged = true; 13221 this.database.Models.delete(this.createdCornerId); 13222 } 13223 const groundPatch = this.database.Models.get(this.groundPatchId); 13224 if (groundPatch) { 13225 for (const [cornerId, originalPos] of this.shiftedCornerPositions) { 13226 const corner = this.database.Models.get(cornerId); 13227 if (corner) { 13228 corner.entity.position = originalPos; 13229 corner.entity.isChanged = true; 13230 } 13231 } 13232 } 13233 this.onVisualUpdate(); 13234 } 13235 redo() { 13236 var _a3; 13237 if (!this.createdCornerId || !this.cornerEntity) { 13238 this.execute(); 13239 return; 13240 } 13241 const groundPatch = this.database.Models.get(this.groundPatchId); 13242 if (!groundPatch) 13243 return; 13244 const newPosition = this.afterPosition + 1; 13245 for (const corner of groundPatch.corners) { 13246 const currentPos = (_a3 = corner.entity.position) != null ? _a3 : 0; 13247 if (currentPos >= newPosition) { 13248 corner.entity.position = currentPos + 1; 13249 corner.entity.isChanged = true; 13250 } 13251 } 13252 this.cornerEntity.isDeleted = false; 13253 this.cornerEntity.isChanged = true; 13254 const cornerModel = new GroundPatchCornerModel(this.cornerEntity); 13255 this.database.Models.set(this.createdCornerId, cornerModel); 13256 this.database.fixModelRelations(cornerModel); 13257 this.onVisualUpdate(); 13258 } 13259}; 13260
13261// src/app/pages/campus-map-v3/commands/remove-ground-patch-corner-command.ts 13262var RemoveGroundPatchCornerCommand = class extends BaseEditorCommand { 13263 constructor(config) { 13264 super(); 13265 this.description = "Remove ground patch corner"; 13266 this.groundPatchId = null; 13267 this.cornerSnapshot = null; 13268 this.removedPosition = null; 13269 this.shiftedCornerPositions = /* @__PURE__ */ new Map(); 13270 this.wasExecuted = false; 13271 this.cornerId = config.cornerId; 13272 this.database = config.database; 13273 this.onVisualUpdate = config.onVisualUpdate; 13274 } 13275 execute() { 13276 var _a3, _b2, _c, _d; 13277 const cornerModel = this.database.Models.get(this.cornerId); 13278 if (!cornerModel || !cornerModel.groundPatch) { 13279 return; 13280 } 13281 const groundPatch = cornerModel.groundPatch; 13282 this.groundPatchId = (_a3 = groundPatch.entity.id) != null ? _a3 : null; 13283 const activeCorners = groundPatch.corners.filter((c) => !c.entity.isDeleted); 13284 if (activeCorners.length <= 3) { 13285 return; 13286 } 13287 this.cornerSnapshot = { 13288 id: this.cornerId, 13289 type: "corner", 13290 data: JSON.parse(JSON.stringify(cornerModel.entity)), 13291 isDeleted: cornerModel.entity.isDeleted 13292 }; 13293 this.removedPosition = (_b2 = cornerModel.entity.position) != null ? _b2 : 0; 13294 for (const corner of groundPatch.corners) { 13295 const pos = (_c = corner.entity.position) != null ? _c : 0; 13296 if (pos > this.removedPosition && !corner.entity.isDeleted) { 13297 this.shiftedCornerPositions.set(corner.entity.id, pos); 13298 } 13299 } 13300 cornerModel.entity.isDeleted = true; 13301 cornerModel.entity.isChanged = true; 13302 for (const corner of groundPatch.corners) { 13303 const pos = (_d = corner.entity.position) != null ? _d : 0; 13304 if (pos > this.removedPosition && !corner.entity.isDeleted) { 13305 corner.entity.position = pos - 1; 13306 corner.entity.isChanged = true; 13307 } 13308 } 13309 this.wasExecuted = true; 13310 this.onVisualUpdate(); 13311 } 13312 undo() { 13313 if (!this.wasExecuted || !this.cornerSnapshot || !this.groundPatchId) 13314 return; 13315 const cornerModel = this.database.Models.get(this.cornerId); 13316 if (cornerModel) { 13317 Object.assign(cornerModel.entity, this.cornerSnapshot.data); 13318 cornerModel.entity.isDeleted = false; 13319 cornerModel.entity.isChanged = true; 13320 } 13321 const groundPatch = this.database.Models.get(this.groundPatchId); 13322 if (groundPatch) { 13323 for (const [cornerId, originalPos] of this.shiftedCornerPositions) { 13324 const corner = this.database.Models.get(cornerId); 13325 if (corner) { 13326 corner.entity.position = originalPos; 13327 corner.entity.isChanged = true; 13328 } 13329 } 13330 } 13331 this.onVisualUpdate(); 13332 } 13333 redo() { 13334 var _a3; 13335 if (!this.wasExecuted) 13336 return; 13337 const cornerModel = this.database.Models.get(this.cornerId); 13338 if (!cornerModel) 13339 return; 13340 const groundPatch = this.database.Models.get(this.groundPatchId); 13341 if (!groundPatch) 13342 return; 13343 cornerModel.entity.isDeleted = true; 13344 cornerModel.entity.isChanged = true; 13345 for (const corner of groundPatch.corners) { 13346 const pos = (_a3 = corner.entity.position) != null ? _a3 : 0; 13347 if (pos > this.removedPosition && !corner.entity.isDeleted) { 13348 corner.entity.position = pos - 1; 13349 corner.entity.isChanged = true; 13350 } 13351 } 13352 this.onVisualUpdate(); 13353 } 13354}; 13355
13356// src/app/pages/campus-map-v3/commands/resize-room-wall-command.ts 13357var ResizeRoomWallCommand = class extends BaseEditorCommand { 13358 constructor(config) { 13359 super(); 13360 this.config = config; 13361 this.originalStart = null; 13362 this.originalEnd = null; 13363 this.originalCentroid = null; 13364 this.description = `Resize room wall to ${(config.targetLengthCm / 100).toFixed(2)} m`; 13365 } 13366 execute() { 13367 var _a3, _b2; 13368 const models = this.getModels(); 13369 if (!models) 13370 return; 13371 if (!this.originalStart) { 13372 this.originalStart = this.getPosition(models.startCorner); 13373 this.originalEnd = this.getPosition(models.endCorner); 13374 this.originalCentroid = { 13375 x: (_a3 = models.room.entity.locationX) != null ? _a3 : 0, 13376 y: (_b2 = models.room.entity.locationY) != null ? _b2 : 0 13377 }; 13378 } 13379 this.setPosition(models.startCorner, this.config.startPosition); 13380 this.setPosition(models.endCorner, this.config.endPosition); 13381 this.updateRoomCentroid(models.room); 13382 this.config.onVisualUpdate(); 13383 } 13384 undo() { 13385 const models = this.getModels(); 13386 if (!models || !this.originalStart || !this.originalEnd || !this.originalCentroid) 13387 return; 13388 this.setPosition(models.startCorner, this.originalStart); 13389 this.setPosition(models.endCorner, this.originalEnd); 13390 models.room.entity.locationX = this.originalCentroid.x; 13391 models.room.entity.locationY = this.originalCentroid.y; 13392 models.room.entity.isChanged = true; 13393 this.config.onVisualUpdate(); 13394 } 13395 getModels() { 13396 const room = this.config.database.Models.get(this.config.roomId); 13397 const startCorner = this.config.database.Models.get(this.config.startCornerId); 13398 const endCorner = this.config.database.Models.get(this.config.endCornerId); 13399 return room && startCorner && endCorner ? { room, startCorner, endCorner } : null; 13400 } 13401 getPosition(corner) { 13402 var _a3, _b2; 13403 return { x: (_a3 = corner.entity.locationX) != null ? _a3 : 0, y: (_b2 = corner.entity.locationY) != null ? _b2 : 0 }; 13404 } 13405 setPosition(corner, position) { 13406 corner.entity.locationX = position.x; 13407 corner.entity.locationY = position.y; 13408 corner.entity.isChanged = true; 13409 } 13410 updateRoomCentroid(room) { 13411 const corners = room.corners.filter((corner) => !corner.entity.isDeleted); 13412 if (corners.length === 0) 13413 return; 13414 room.entity.locationX = corners.reduce((sum, corner) => { 13415 var _a3; 13416 return sum + ((_a3 = corner.entity.locationX) != null ? _a3 : 0); 13417 }, 0) / corners.length; 13418 room.entity.locationY = corners.reduce((sum, corner) => { 13419 var _a3; 13420 return sum + ((_a3 = corner.entity.locationY) != null ? _a3 : 0); 13421 }, 0) / corners.length; 13422 room.entity.isChanged = true; 13423 } 13424}; 13425
13426// src/app/pages/campus-map-v3/commands/polygon-command-factory.ts 13427var RoomPolygonCommandFactory = class { 13428 createMoveCornerCommand(config) { 13429 return new MoveRoomCornerCommand(config); 13430 } 13431 createAddCornerCommand(config) { 13432 return new AddRoomCornerCommand({ 13433 roomId: config.parentId, 13434 afterPosition: config.afterPosition, 13435 location: config.location, 13436 database: config.database, 13437 onVisualUpdate: config.onVisualUpdate 13438 }); 13439 } 13440 createRemoveCornerCommand(config) { 13441 return new RemoveRoomCornerCommand(config); 13442 } 13443 createResizeEdgeCommand(config) { 13444 return new ResizeRoomWallCommand({ 13445 roomId: config.parentId, 13446 startCornerId: config.startCornerId, 13447 endCornerId: config.endCornerId, 13448 startPosition: config.startPosition, 13449 endPosition: config.endPosition, 13450 targetLengthCm: config.targetLengthCm, 13451 database: config.database, 13452 onVisualUpdate: config.onVisualUpdate 13453 }); 13454 } 13455}; 13456var FloorPolygonCommandFactory = class { 13457 createMoveCornerCommand(config) { 13458 return new MoveFloorCornerCommand(config); 13459 } 13460 createAddCornerCommand(config) { 13461 return new AddFloorCornerCommand({ 13462 floorId: config.parentId, 13463 afterPosition: config.afterPosition, 13464 location: config.location, 13465 database: config.database, 13466 onVisualUpdate: config.onVisualUpdate 13467 }); 13468 } 13469 createRemoveCornerCommand(config) { 13470 return new RemoveFloorCornerCommand(config); 13471 } 13472}; 13473var GroundPatchPolygonCommandFactory = class { 13474 createMoveCornerCommand(config) { 13475 return new MoveGroundPatchCornerCommand(config); 13476 } 13477 createAddCornerCommand(config) { 13478 return new AddGroundPatchCornerCommand({ 13479 groundPatchId: config.parentId, 13480 afterPosition: config.afterPosition, 13481 location: config.location, 13482 database: config.database, 13483 onVisualUpdate: config.onVisualUpdate 13484 }); 13485 } 13486 createRemoveCornerCommand(config) { 13487 return new RemoveGroundPatchCornerCommand(config); 13488 } 13489}; 13490
13491// src/app/pages/campus-map-v3/utils/gesture-controller-factory.ts 13492var GestureControllerFactory = class { 13493 /** 13494 * Create all gesture controllers with the given configuration. 13495 * 13496 * @param config Factory configuration with dependencies and callbacks 13497 * @returns GestureHandlers object with all controllers and disposal method 13498 */ 13499 static create(config) { 13500 const { mapScene, cameraController, levelRenderer, groundPatchRenderer, selectionService, historyService, database, shapeEditState, ngZone, callbacks } = config; 13501 const canvas = mapScene.getRenderer().domElement; 13502 const camera = mapScene.getCamera(); 13503 const controls = cameraController.instance; 13504 const markDirty = () => mapScene.markDirty(); 13505 const gestureGroups = [ 13506 groundPatchRenderer.group, 13507 groundPatchRenderer.outlinesGroup, 13508 groundPatchRenderer.highlightGroup, 13509 groundPatchRenderer.ghostGroup, 13510 levelRenderer.floorBackgroundsGroup, 13511 levelRenderer.floorsGroup, 13512 levelRenderer.nodesGroup, 13513 levelRenderer.highlightGroup, 13514 levelRenderer.ghostGroup, 13515 levelRenderer.groundReferenceGroup, 13516 levelRenderer.labelsGroup, 13517 levelRenderer.cornerHandlesGroup, 13518 levelRenderer.edgeMidpointsGroup, 13519 levelRenderer.polygonPreviewGroup, 13520 levelRenderer.roomWallDimensionsGroup, 13521 levelRenderer.rotationHandleGroup 13522 ]; 13523 if (config.model3DWrapperGroup) { 13524 gestureGroups.push(config.model3DWrapperGroup); 13525 } 13526 const selectableGroups = [ 13527 groundPatchRenderer.group, 13528 levelRenderer.floorsGroup, 13529 levelRenderer.nodesGroup 13530 ]; 13531 const rotatableGroups = [ 13532 groundPatchRenderer.group, 13533 groundPatchRenderer.outlinesGroup, 13534 levelRenderer.floorsGroup, 13535 levelRenderer.nodesGroup 13536 ]; 13537 const gesture = new GestureController({ 13538 canvas, 13539 camera, 13540 controls, 13541 groups: gestureGroups, 13542 onDirty: markDirty, 13543 onRotate: (rotation) => levelRenderer.updateLabelsRotation(rotation) 13544 }); 13545 let selection = new SelectionController({ 13546 canvas, 13547 camera, 13548 selectableGroups, 13549 onSelect: callbacks.onSelect, 13550 onDirty: markDirty 13551 }); 13552 const marquee = new MarqueeSelectionController({ 13553 canvas, 13554 camera, 13555 selectableGroups, 13556 onMarqueeStart: callbacks.onMarqueeStart, 13557 onMarqueeEnd: callbacks.onMarqueeEnd, 13558 onMarqueeUpdate: callbacks.onMarqueeUpdate, 13559 onMarqueeComplete: callbacks.onMarqueeComplete, 13560 isEntityVisible: callbacks.isEntityVisible 13561 }); 13562 const dragMove = new DragMoveController({ 13563 canvas, 13564 camera, 13565 selectableGroups, 13566 selectionService, 13567 historyService, 13568 database, 13569 levelRenderer, 13570 groundPatchRenderer, 13571 onDirty: markDirty, 13572 onDragStart: callbacks.onDragStart, 13573 onDragEnd: callbacks.onDragEnd, 13574 onVisualUpdate: callbacks.onVisualUpdate 13575 }); 13576 const dragRotate = new DragRotateController({ 13577 canvas, 13578 camera, 13579 selectableGroups, 13580 rotationHandleGroup: levelRenderer.rotationHandleGroup, 13581 selectionService, 13582 historyService, 13583 database, 13584 levelRenderer, 13585 groundPatchRenderer, 13586 onDirty: markDirty, 13587 onDragStart: callbacks.onDragStart, 13588 onDragEnd: callbacks.onDragEnd, 13589 onVisualUpdate: callbacks.onVisualUpdate, 13590 onAngleUpdate: callbacks.onRotationAngleUpdate, 13591 onHoverChange: callbacks.onRotationHoverChange 13592 }); 13593 const nodePlacement = new NodePlacementController({ 13594 canvas, 13595 camera, 13596 rotatableGroups, 13597 onPlacement: callbacks.onNodePlacement, 13598 onCancel: callbacks.onModeExit, 13599 onDirty: markDirty 13600 }); 13601 const nodeConnection = new NodeConnectionController({ 13602 canvas, 13603 camera, 13604 rotatableGroups, 13605 selectableGroups: [levelRenderer.nodesGroup], 13606 levelRenderer, 13607 database, 13608 onConnectionCreate: callbacks.onConnectionCreate, 13609 onCancel: callbacks.onModeExit, 13610 onDirty: markDirty 13611 }); 13612 const roomDraw = new RoomPolygonDrawController({ 13613 canvas, 13614 camera, 13615 rotatableGroups, 13616 levelRenderer, 13617 shapeEditState, 13618 onPolygonComplete: callbacks.onRoomPolygonComplete, 13619 onCancel: callbacks.onModeExit, 13620 onDirty: markDirty 13621 }); 13622 const roomAdapter = new RoomPolygonAdapter(database); 13623 const roomCommandFactory = new RoomPolygonCommandFactory(); 13624 const roomShapeEdit = new PolygonShapeEditController({ 13625 canvas, 13626 camera, 13627 rotatableGroups, 13628 levelRenderer, 13629 database, 13630 historyService, 13631 shapeEditState, 13632 adapter: roomAdapter, 13633 commandFactory: roomCommandFactory, 13634 onExit: callbacks.onModeExit, 13635 onDirty: markDirty, 13636 onVisualUpdate: callbacks.onVisualUpdate, 13637 onZoomChange: (handler) => mapScene.subscribeZoomChange(handler) 13638 }); 13639 const floorOutlineDraw = new FloorOutlineDrawController({ 13640 canvas, 13641 camera, 13642 rotatableGroups, 13643 levelRenderer, 13644 shapeEditState, 13645 onPolygonComplete: callbacks.onFloorOutlineComplete, 13646 onCancel: callbacks.onModeExit, 13647 onDirty: markDirty 13648 }); 13649 const floorAdapter = new FloorPolygonAdapter(database); 13650 const floorCommandFactory = new FloorPolygonCommandFactory(); 13651 const floorShapeEdit = new PolygonShapeEditController({ 13652 canvas, 13653 camera, 13654 rotatableGroups, 13655 levelRenderer, 13656 database, 13657 historyService, 13658 shapeEditState, 13659 adapter: floorAdapter, 13660 commandFactory: floorCommandFactory, 13661 onExit: callbacks.onModeExit, 13662 onDirty: markDirty, 13663 onVisualUpdate: callbacks.onVisualUpdate, 13664 onZoomChange: (handler) => mapScene.subscribeZoomChange(handler) 13665 }); 13666 const groundPatchAdapter = new GroundPatchPolygonAdapter(database); 13667 const groundPatchCommandFactory = new GroundPatchPolygonCommandFactory();
13668 const groundPatchShapeEdit = new PolygonShapeEditController({ 13669 canvas, 13670 camera, 13671 rotatableGroups, 13672 levelRenderer, 13673 database, 13674 historyService, 13675 shapeEditState, 13676 adapter: groundPatchAdapter, 13677 commandFactory: groundPatchCommandFactory, 13678 onExit: callbacks.onModeExit, 13679 onDirty: markDirty, 13680 onVisualUpdate: callbacks.onVisualUpdate, 13681 onZoomChange: (handler) => mapScene.subscribeZoomChange(handler) 13682 }); 13683 const groundPatchDraw = new GroundPatchPolygonDrawController({ 13684 canvas, 13685 camera, 13686 rotatableGroups, 13687 levelRenderer, 13688 shapeEditState, 13689 onPolygonComplete: callbacks.onGroundPatchPolygonComplete, 13690 onCancel: callbacks.onModeExit, 13691 onDirty: markDirty 13692 }); 13693 const handlers = { 13694 gesture, 13695 selection, 13696 marquee, 13697 dragMove, 13698 dragRotate, 13699 nodePlacement, 13700 nodeConnection, 13701 roomDraw, 13702 roomShapeEdit, 13703 floorOutlineDraw, 13704 floorShapeEdit, 13705 groundPatchDraw, 13706 groundPatchShapeEdit, 13707 createSelectionController: () => { 13708 if (!handlers.selection) { 13709 handlers.selection = new SelectionController({ 13710 canvas, 13711 camera, 13712 selectableGroups, 13713 onSelect: callbacks.onSelect, 13714 onDirty: markDirty 13715 }); 13716 } 13717 }, 13718 disposeSelectionController: () => { 13719 var _a3; 13720 (_a3 = handlers.selection) == null ? void 0 : _a3.dispose(); 13721 handlers.selection = null; 13722 }, 13723 disableDragControllers: () => { 13724 dragMove.disable(); 13725 dragRotate.disable(); 13726 marquee.disable(); 13727 }, 13728 enableDragControllers: () => { 13729 dragMove.enable(); 13730 dragRotate.enable(); 13731 marquee.enable(); 13732 }, 13733 setCameraToggleCallback: (callback) => { 13734 roomShapeEdit.setCameraToggleCallback(callback); 13735 floorShapeEdit.setCameraToggleCallback(callback); 13736 groundPatchShapeEdit.setCameraToggleCallback(callback); 13737 }, 13738 disposeAll: () => { 13739 var _a3; 13740 gesture.dispose(); 13741 (_a3 = handlers.selection) == null ? void 0 : _a3.dispose(); 13742 marquee.dispose(); 13743 dragMove.dispose(); 13744 dragRotate.dispose(); 13745 nodePlacement.dispose(); 13746 nodeConnection.dispose(); 13747 roomDraw.dispose(); 13748 roomShapeEdit.dispose(); 13749 floorOutlineDraw.dispose(); 13750 floorShapeEdit.dispose(); 13751 groundPatchDraw.dispose(); 13752 groundPatchShapeEdit.dispose(); 13753 } 13754 }; 13755 return handlers; 13756 } 13757}; 13758
13759// src/app/pages/campus-map-v3/components/marquee-box/marquee-box.component.ts 13760function MarqueeBoxComponent_Conditional_0_Template(rf, ctx) { 13761 if (rf & 1) { 13762 \u0275\u0275element(0, "div", 1); 13763 } 13764 if (rf & 2) { 13765 const ctx_r0 = \u0275\u0275nextContext(); 13766 \u0275\u0275styleProp("left", ctx_r0.rect.left, "px")("top", ctx_r0.rect.top, "px")("width", ctx_r0.rect.width, "px")("height", ctx_r0.rect.height, "px"); 13767 } 13768} 13769var _MarqueeBoxComponent = class _MarqueeBoxComponent { 13770 constructor() { 13771 this.rect = null; 13772 this.visible = false; 13773 } 13774}; 13775_MarqueeBoxComponent.\u0275fac = function MarqueeBoxComponent_Factory(__ngFactoryType__) { 13776 return new (__ngFactoryType__ || _MarqueeBoxComponent)(); 13777}; 13778_MarqueeBoxComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _MarqueeBoxComponent, selectors: [["app-marquee-box"]], inputs: { rect: "rect", visible: "visible" }, decls: 1, vars: 1, consts: [[1, "marquee-box", 3, "left", "top", "width", "height"], [1, "marquee-box"]], template: function MarqueeBoxComponent_Template(rf, ctx) { 13779 if (rf & 1) { 13780 \u0275\u0275template(0, MarqueeBoxComponent_Conditional_0_Template, 1, 8, "div", 0); 13781 } 13782 if (rf & 2) { 13783 \u0275\u0275conditional(ctx.visible && ctx.rect ? 0 : -1); 13784 } 13785}, styles: ["\n\n.marquee-box[_ngcontent-%COMP%] {\n position: absolute;\n pointer-events: none;\n background: rgba(0, 188, 212, 0.1);\n border: 2px dashed rgba(0, 188, 212, 0.8);\n z-index: 50;\n}"], changeDetection: 0 }); 13786var MarqueeBoxComponent = _MarqueeBoxComponent; 13787(() => { 13788 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(MarqueeBoxComponent, [{ 13789 type: Component, 13790 args: [{ selector: "app-marquee-box", standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: '@if (visible && rect) {\n <div\n class="marquee-box"\n [style.left.px]="rect.left"\n [style.top.px]="rect.top"\n [style.width.px]="rect.width"\n [style.height.px]="rect.height"\n ></div>\n}\n', styles: ["/* src/app/pages/campus-map-v3/components/marquee-box/marquee-box.component.scss */\n.marquee-box {\n position: absolute;\n pointer-events: none;\n background: rgba(0, 188, 212, 0.1);\n border: 2px dashed rgba(0, 188, 212, 0.8);\n z-index: 50;\n}\n"] }] 13791 }], null, { rect: [{ 13792 type: Input 13793 }], visible: [{ 13794 type: Input 13795 }] }); 13796})(); 13797(() => { 13798 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(MarqueeBoxComponent, { className: "MarqueeBoxComponent", filePath: "src/app/pages/campus-map-v3/components/marquee-box/marquee-box.component.ts", lineNumber: 17 }); 13799})(); 13800
13801// src/app/pages/campus-map-v3/components/rotation-indicator/rotation-indicator.component.ts 13802function RotationIndicatorComponent_Conditional_0_Template(rf, ctx) { 13803 if (rf & 1) { 13804 \u0275\u0275elementStart(0, "div", 1); 13805 \u0275\u0275text(1); 13806 \u0275\u0275elementEnd(); 13807 } 13808 if (rf & 2) { 13809 const ctx_r0 = \u0275\u0275nextContext(); 13810 \u0275\u0275styleProp("left", ctx_r0.x, "px")("top", ctx_r0.y, "px"); 13811 \u0275\u0275advance(); 13812 \u0275\u0275textInterpolate1(" ", ctx_r0.angle, "\xB0 "); 13813 } 13814} 13815var _RotationIndicatorComponent = class _RotationIndicatorComponent { 13816 constructor() { 13817 this.visible = false; 13818 this.angle = 0; 13819 this.x = 0; 13820 this.y = 0; 13821 } 13822}; 13823_RotationIndicatorComponent.\u0275fac = function RotationIndicatorComponent_Factory(__ngFactoryType__) { 13824 return new (__ngFactoryType__ || _RotationIndicatorComponent)(); 13825}; 13826_RotationIndicatorComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _RotationIndicatorComponent, selectors: [["app-rotation-indicator"]], inputs: { visible: "visible", angle: "angle", x: "x", y: "y" }, decls: 1, vars: 1, consts: [[1, "rotation-indicator", 3, "left", "top"], [1, "rotation-indicator"]], template: function RotationIndicatorComponent_Template(rf, ctx) { 13827 if (rf & 1) { 13828 \u0275\u0275template(0, RotationIndicatorComponent_Conditional_0_Template, 2, 5, "div", 0); 13829 } 13830 if (rf & 2) { 13831 \u0275\u0275conditional(ctx.visible ? 0 : -1); 13832 } 13833}, styles: ['\n\n.rotation-indicator[_ngcontent-%COMP%] {\n position: fixed;\n background: rgba(0, 0, 0, 0.8);\n color: white;\n padding: 4px 10px;\n border-radius: 4px;\n font-size: 13px;\n font-family:\n "SF Mono",\n Monaco,\n "Cascadia Code",\n monospace;\n font-weight: 500;\n pointer-events: none;\n transform: translate(-50%, -100%);\n margin-top: -12px;\n z-index: 10000;\n white-space: nowrap;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}'], changeDetection: 0 }); 13834var RotationIndicatorComponent = _RotationIndicatorComponent; 13835(() => { 13836 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(RotationIndicatorComponent, [{ 13837 type: Component, 13838 args: [{ selector: "app-rotation-indicator", standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: '@if (visible) {\n <div\n class="rotation-indicator"\n [style.left.px]="x"\n [style.top.px]="y"\n >\n {{ angle }}\xB0\n </div>\n}\n', styles: ['/* src/app/pages/campus-map-v3/components/rotation-indicator/rotation-indicator.component.scss */\n.rotation-indicator {\n position: fixed;\n background: rgba(0, 0, 0, 0.8);\n color: white;\n padding: 4px 10px;\n border-radius: 4px;\n font-size: 13px;\n font-family:\n "SF Mono",\n Monaco,\n "Cascadia Code",\n monospace;\n font-weight: 500;\n pointer-events: none;\n transform: translate(-50%, -100%);\n margin-top: -12px;\n z-index: 10000;\n white-space: nowrap;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n'] }] 13839 }], null, { visible: [{ 13840 type: Input 13841 }], angle: [{ 13842 type: Input 13843 }], x: [{ 13844 type: Input 13845 }], y: [{ 13846 type: Input 13847 }] }); 13848})(); 13849(() => { 13850 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(RotationIndicatorComponent, { className: "RotationIndicatorComponent", filePath: "src/app/pages/campus-map-v3/components/rotation-indicator/rotation-indicator.component.ts", lineNumber: 14 }); 13851})(); 13852
13853// src/app/pages/campus-map-v3/components/wall-length-inspector/wall-length-inspector.component.ts 13854var _c04 = (a0) => ({ minimum: a0 }); 13855function WallLengthInspectorComponent_Conditional_24_Template(rf, ctx) { 13856 if (rf & 1) { 13857 \u0275\u0275elementStart(0, "div", 12); 13858 \u0275\u0275text(1); 13859 \u0275\u0275pipe(2, "translate"); 13860 \u0275\u0275elementEnd(); 13861 } 13862 if (rf & 2) { 13863 const ctx_r0 = \u0275\u0275nextContext(); 13864 \u0275\u0275advance(); 13865 \u0275\u0275textInterpolate1(" ", \u0275\u0275pipeBind2(2, 1, "campusMapV3.wallLength.errors." + ctx_r0.error, \u0275\u0275pureFunction1(4, _c04, ctx_r0.minimumMetres)), " "); 13866 } 13867} 13868function WallLengthInspectorComponent_Conditional_25_Template(rf, ctx) { 13869 if (rf & 1) { 13870 \u0275\u0275elementStart(0, "div", 13); 13871 \u0275\u0275text(1); 13872 \u0275\u0275pipe(2, "translate"); 13873 \u0275\u0275elementEnd(); 13874 } 13875 if (rf & 2) { 13876 \u0275\u0275advance(); 13877 \u0275\u0275textInterpolate(\u0275\u0275pipeBind1(2, 1, "campusMapV3.wallLength.hint")); 13878 } 13879} 13880var _WallLengthInspectorComponent = class _WallLengthInspectorComponent { 13881 constructor() { 13882 this.error = null; 13883 this.lengthPreview = new EventEmitter(); 13884 this.commitRequested = new EventEmitter(); 13885 this.cancelRequested = new EventEmitter(); 13886 this.value = "0.00"; 13887 } 13888 get left() { 13889 return Math.max(150, Math.min(this.wall.screenX, window.innerWidth - 150)); 13890 } 13891 get top() { 13892 return Math.max(28, Math.min(this.wall.screenY, window.innerHeight - 28)); 13893 } 13894 get placeBelow() { 13895 return this.wall.screenY < 145; 13896 } 13897 get minimumMetres() { 13898 return (this.wall.minimumLengthCm / 100).toFixed(2); 13899 } 13900 ngOnChanges(changes) { 13901 if (changes["wall"]) { 13902 this.value = (this.wall.lengthCm / 100).toFixed(2); 13903 } 13904 } 13905 onValueChange(value) { 13906 this.value = String(value != null ? value : ""); 13907 const metres = Number(this.value); 13908 this.lengthPreview.emit(Number.isFinite(metres) && this.value.trim() !== "" ? Math.round(metres * 100) : Number.NaN); 13909 } 13910 onKeydown(event) { 13911 event.stopPropagation(); 13912 if (event.key === "Enter") { 13913 event.preventDefault(); 13914 this.emitInvalidIfEmpty(); 13915 this.commitRequested.emit(); 13916 } else if (event.key === "Escape") { 13917 event.preventDefault(); 13918 this.cancelRequested.emit(); 13919 } 13920 } 13921 onBlur() { 13922 this.emitInvalidIfEmpty(); 13923 this.commitRequested.emit(); 13924 } 13925 emitInvalidIfEmpty() { 13926 if (this.value.trim() === "") 13927 this.lengthPreview.emit(Number.NaN); 13928 } 13929}; 13930_WallLengthInspectorComponent.\u0275fac = function WallLengthInspectorComponent_Factory(__ngFactoryType__) { 13931 return new (__ngFactoryType__ || _WallLengthInspectorComponent)(); 13932}; 13933_WallLengthInspectorComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _WallLengthInspectorComponent, selectors: [["app-wall-length-inspector"]], inputs: { wall: "wall", error: "error" }, outputs: { lengthPreview: "lengthPreview", commitRequested: "commitRequested", cancelRequested: "cancelRequested" }, features: [\u0275\u0275ProvidersFeature([], [provideIcons({ lucideCheck, lucideLockKeyhole, lucideX })]), \u0275\u0275NgOnChangesFeature], decls: 26, vars: 29, consts: [["data-testid", "wall-length-inspector", 1, "wall-length-inspector", 3, "mousedown", "click"], [1, "inspector-heading"], [1, "center-anchor"], ["name", "lucideLockKeyhole", "size", "13"], [1, "length-row"], ["for", "wall-length-metres", 1, "sr-only"], [1, "input-shell"], ["id", "wall-length-metres", "data-testid", "wall-length-input", "type", "number", "inputmode", "decimal", "min", "0.01", "step", "0.01", 3, "ngModelChange", "keydown", "blur", "ngModel"], ["type", "button", "data-testid", "wall-length-apply", 1, "icon-button", "confirm", 3, "mousedown", "click"], ["name", "lucideCheck", "size", "16"], ["type", "button", 1, "icon-button", 3, "mousedown", "click"], ["name", "lucideX", "size", "16"], ["role", "alert", "data-testid", "wall-length-error", 1, "error-message"], [1, "hint"]], template: function WallLengthInspectorComponent_Template(rf, ctx) { 13934 if (rf & 1) { 13935 \u0275\u0275elementStart(0, "div", 0); 13936 \u0275\u0275listener("mousedown", function WallLengthInspectorComponent_Template_div_mousedown_0_listener($event) { 13937 return $event.stopPropagation(); 13938 })("click", function WallLengthInspectorComponent_Template_div_click_0_listener($event) { 13939 return $event.stopPropagation(); 13940 }); 13941 \u0275\u0275elementStart(1, "div", 1)(2, "span"); 13942 \u0275\u0275text(3); 13943 \u0275\u0275pipe(4, "translate"); 13944 \u0275\u0275elementEnd(); 13945 \u0275\u0275elementStart(5, "span", 2); 13946 \u0275\u0275pipe(6, "translate"); 13947 \u0275\u0275element(7, "ng-icon", 3); 13948 \u0275\u0275text(8); 13949 \u0275\u0275pipe(9, "translate"); 13950 \u0275\u0275elementEnd()(); 13951 \u0275\u0275elementStart(10, "div", 4)(11, "label", 5); 13952 \u0275\u0275text(12); 13953 \u0275\u0275pipe(13, "translate"); 13954 \u0275\u0275elementEnd(); 13955 \u0275\u0275elementStart(14, "div", 6)(15, "input", 7); 13956 \u0275\u0275listener("ngModelChange", function WallLengthInspectorComponent_Template_input_ngModelChange_15_listener($event) { 13957 return ctx.onValueChange($event); 13958 })("keydown", function WallLengthInspectorComponent_Template_input_keydown_15_listener($event) { 13959 return ctx.onKeydown($event); 13960 })("blur", function WallLengthInspectorComponent_Template_input_blur_15_listener() { 13961 return ctx.onBlur(); 13962 }); 13963 \u0275\u0275elementEnd(); 13964 \u0275\u0275elementStart(16, "span"); 13965 \u0275\u0275text(17, "m"); 13966 \u0275\u0275elementEnd()(); 13967 \u0275\u0275elementStart(18, "button", 8); 13968 \u0275\u0275pipe(19, "translate"); 13969 \u0275\u0275listener("mousedown", function WallLengthInspectorComponent_Template_button_mousedown_18_listener($event) { 13970 return $event.preventDefault(); 13971 })("click", function WallLengthInspectorComponent_Template_button_click_18_listener() { 13972 return ctx.commitRequested.emit(); 13973 });
13974 \u0275\u0275element(20, "ng-icon", 9); 13975 \u0275\u0275elementEnd(); 13976 \u0275\u0275elementStart(21, "button", 10); 13977 \u0275\u0275pipe(22, "translate"); 13978 \u0275\u0275listener("mousedown", function WallLengthInspectorComponent_Template_button_mousedown_21_listener($event) { 13979 return $event.preventDefault(); 13980 })("click", function WallLengthInspectorComponent_Template_button_click_21_listener() { 13981 return ctx.cancelRequested.emit(); 13982 }); 13983 \u0275\u0275element(23, "ng-icon", 11); 13984 \u0275\u0275elementEnd()(); 13985 \u0275\u0275template(24, WallLengthInspectorComponent_Conditional_24_Template, 3, 6, "div", 12)(25, WallLengthInspectorComponent_Conditional_25_Template, 3, 3, "div", 13); 13986 \u0275\u0275elementEnd(); 13987 } 13988 if (rf & 2) { 13989 \u0275\u0275styleProp("left", ctx.left, "px")("top", ctx.top, "px"); 13990 \u0275\u0275classProp("below", ctx.placeBelow); 13991 \u0275\u0275advance(3); 13992 \u0275\u0275textInterpolate(\u0275\u0275pipeBind1(4, 17, "campusMapV3.wallLength.title")); 13993 \u0275\u0275advance(2); 13994 \u0275\u0275attribute("title", \u0275\u0275pipeBind1(6, 19, "campusMapV3.wallLength.centerAnchor")); 13995 \u0275\u0275advance(3); 13996 \u0275\u0275textInterpolate1(" ", \u0275\u0275pipeBind1(9, 21, "campusMapV3.wallLength.center"), " "); 13997 \u0275\u0275advance(4); 13998 \u0275\u0275textInterpolate(\u0275\u0275pipeBind1(13, 23, "campusMapV3.wallLength.inputLabel")); 13999 \u0275\u0275advance(2); 14000 \u0275\u0275classProp("invalid", ctx.error); 14001 \u0275\u0275advance(); 14002 \u0275\u0275property("ngModel", ctx.value); 14003 \u0275\u0275attribute("aria-invalid", !!ctx.error); 14004 \u0275\u0275advance(3); 14005 \u0275\u0275attribute("aria-label", \u0275\u0275pipeBind1(19, 25, "campusMapV3.wallLength.apply")); 14006 \u0275\u0275advance(3); 14007 \u0275\u0275attribute("aria-label", \u0275\u0275pipeBind1(22, 27, "campusMapV3.wallLength.cancel")); 14008 \u0275\u0275advance(3); 14009 \u0275\u0275conditional(ctx.error ? 24 : 25); 14010 } 14011}, dependencies: [FormsModule, DefaultValueAccessor, NumberValueAccessor, NgControlStatus, MinValidator, NgModel, TranslateModule, TranslatePipe, NgIcon], styles: ['\n\n.wall-length-inspector[_ngcontent-%COMP%] {\n position: fixed;\n z-index: 10020;\n width: 270px;\n padding: 10px;\n transform: translate(-50%, calc(-100% - 26px));\n border: 1px solid rgba(249, 115, 22, 0.55);\n border-radius: 10px;\n background: rgba(255, 255, 255, 0.98);\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.2);\n color: #172033;\n}\n.wall-length-inspector.below[_ngcontent-%COMP%] {\n transform: translate(-50%, 26px);\n}\n.inspector-heading[_ngcontent-%COMP%], \n.length-row[_ngcontent-%COMP%], \n.center-anchor[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n}\n.inspector-heading[_ngcontent-%COMP%] {\n justify-content: space-between;\n margin-bottom: 7px;\n font-size: 12px;\n font-weight: 700;\n}\n.center-anchor[_ngcontent-%COMP%] {\n gap: 4px;\n color: #64748b;\n font-size: 10px;\n font-weight: 600;\n}\n.length-row[_ngcontent-%COMP%] {\n gap: 6px;\n}\n.input-shell[_ngcontent-%COMP%] {\n display: flex;\n flex: 1;\n align-items: center;\n overflow: hidden;\n border: 1px solid #cbd5e1;\n border-radius: 6px;\n background: #fff;\n}\n.input-shell[_ngcontent-%COMP%]:focus-within {\n border-color: #f97316;\n box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.15);\n}\n.input-shell.invalid[_ngcontent-%COMP%] {\n border-color: var(--ion-color-danger, #dc2626);\n}\n.input-shell[_ngcontent-%COMP%] input[_ngcontent-%COMP%] {\n min-width: 0;\n flex: 1;\n border: 0;\n padding: 7px 8px;\n outline: 0;\n background: transparent;\n color: inherit;\n font:\n 600 13px "SF Mono",\n Monaco,\n "Cascadia Code",\n monospace;\n}\n.input-shell[_ngcontent-%COMP%] span[_ngcontent-%COMP%] {\n padding-right: 8px;\n color: #64748b;\n font-size: 12px;\n}\n.icon-button[_ngcontent-%COMP%] {\n display: grid;\n width: 30px;\n height: 30px;\n flex: 0 0 30px;\n place-items: center;\n border: 0;\n border-radius: 6px;\n background: #eef2f7;\n color: #475569;\n cursor: pointer;\n}\n.icon-button.confirm[_ngcontent-%COMP%] {\n background: #f97316;\n color: white;\n}\n.hint[_ngcontent-%COMP%], \n.error-message[_ngcontent-%COMP%] {\n margin-top: 6px;\n font-size: 10px;\n line-height: 1.3;\n}\n.hint[_ngcontent-%COMP%] {\n color: #64748b;\n}\n.error-message[_ngcontent-%COMP%] {\n color: var(--ion-color-danger, #dc2626);\n}\n.sr-only[_ngcontent-%COMP%] {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n@media (prefers-color-scheme: dark) {\n .wall-length-inspector[_ngcontent-%COMP%] {\n background: rgba(24, 31, 44, 0.98);\n color: #f8fafc;\n }\n .input-shell[_ngcontent-%COMP%] {\n border-color: #475569;\n background: #111827;\n }\n .icon-button[_ngcontent-%COMP%] {\n background: #334155;\n color: #e2e8f0;\n }\n .icon-button.confirm[_ngcontent-%COMP%] {\n background: #f97316;\n color: white;\n }\n}'], changeDetection: 0 }); 14012var WallLengthInspectorComponent = _WallLengthInspectorComponent; 14013(() => { 14014 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(WallLengthInspectorComponent, [{ 14015 type: Component, 14016 args: [{ selector: "app-wall-length-inspector", standalone: true, imports: [FormsModule, TranslateModule, NgIcon], viewProviders: [provideIcons({ lucideCheck, lucideLockKeyhole, lucideX })], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div 14017 class="wall-length-inspector" 14018 [class.below]="placeBelow" 14019 [style.left.px]="left" 14020 [style.top.px]="top" 14021 data-testid="wall-length-inspector" 14022 (mousedown)="$event.stopPropagation()" 14023 (click)="$event.stopPropagation()"> 14024 <div class="inspector-heading"> 14025 <span>{{ 'campusMapV3.wallLength.title' | translate }}</span> 14026 <span class="center-anchor" [attr.title]="'campusMapV3.wallLength.centerAnchor' | translate"> 14027 <ng-icon name="lucideLockKeyhole" size="13" /> 14028 {{ 'campusMapV3.wallLength.center' | translate }} 14029 </span> 14030 </div> 14031 14032 <div class="length-row"> 14033 <label class="sr-only" for="wall-length-metres">{{ 'campusMapV3.wallLength.inputLabel' | translate }}</label> 14034 <div class="input-shell" [class.invalid]="error"> 14035 <input 14036 id="wall-length-metres" 14037 data-testid="wall-length-input" 14038 type="number" 14039 inputmode="decimal" 14040 min="0.01" 14041 step="0.01" 14042 [ngModel]="value" 14043 (ngModelChange)="onValueChange($event)" 14044 (keydown)="onKeydown($event)" 14045 (blur)="onBlur()" 14046 [attr.aria-invalid]="!!error" 14047 />
14048 <span>m</span> 14049 </div> 14050 <button type="button" class="icon-button confirm" data-testid="wall-length-apply" (mousedown)="$event.preventDefault()" (click)="commitRequested.emit()" [attr.aria-label]="'campusMapV3.wallLength.apply' | translate"> 14051 <ng-icon name="lucideCheck" size="16" /> 14052 </button> 14053 <button type="button" class="icon-button" (mousedown)="$event.preventDefault()" (click)="cancelRequested.emit()" [attr.aria-label]="'campusMapV3.wallLength.cancel' | translate"> 14054 <ng-icon name="lucideX" size="16" /> 14055 </button> 14056 </div> 14057 14058 @if (error) { 14059 <div class="error-message" role="alert" data-testid="wall-length-error"> 14060 {{ ('campusMapV3.wallLength.errors.' + error) | translate:{ minimum: minimumMetres } }} 14061 </div> 14062 } @else { 14063 <div class="hint">{{ 'campusMapV3.wallLength.hint' | translate }}</div> 14064 } 14065</div> 14066`, styles: ['/* src/app/pages/campus-map-v3/components/wall-length-inspector/wall-length-inspector.component.scss */\n.wall-length-inspector {\n position: fixed;\n z-index: 10020;\n width: 270px;\n padding: 10px;\n transform: translate(-50%, calc(-100% - 26px));\n border: 1px solid rgba(249, 115, 22, 0.55);\n border-radius: 10px;\n background: rgba(255, 255, 255, 0.98);\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.2);\n color: #172033;\n}\n.wall-length-inspector.below {\n transform: translate(-50%, 26px);\n}\n.inspector-heading,\n.length-row,\n.center-anchor {\n display: flex;\n align-items: center;\n}\n.inspector-heading {\n justify-content: space-between;\n margin-bottom: 7px;\n font-size: 12px;\n font-weight: 700;\n}\n.center-anchor {\n gap: 4px;\n color: #64748b;\n font-size: 10px;\n font-weight: 600;\n}\n.length-row {\n gap: 6px;\n}\n.input-shell {\n display: flex;\n flex: 1;\n align-items: center;\n overflow: hidden;\n border: 1px solid #cbd5e1;\n border-radius: 6px;\n background: #fff;\n}\n.input-shell:focus-within {\n border-color: #f97316;\n box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.15);\n}\n.input-shell.invalid {\n border-color: var(--ion-color-danger, #dc2626);\n}\n.input-shell input {\n min-width: 0;\n flex: 1;\n border: 0;\n padding: 7px 8px;\n outline: 0;\n background: transparent;\n color: inherit;\n font:\n 600 13px "SF Mono",\n Monaco,\n "Cascadia Code",\n monospace;\n}\n.input-shell span {\n padding-right: 8px;\n color: #64748b;\n font-size: 12px;\n}\n.icon-button {\n display: grid;\n width: 30px;\n height: 30px;\n flex: 0 0 30px;\n place-items: center;\n border: 0;\n border-radius: 6px;\n background: #eef2f7;\n color: #475569;\n cursor: pointer;\n}\n.icon-button.confirm {\n background: #f97316;\n color: white;\n}\n.hint,\n.error-message {\n margin-top: 6px;\n font-size: 10px;\n line-height: 1.3;\n}\n.hint {\n color: #64748b;\n}\n.error-message {\n color: var(--ion-color-danger, #dc2626);\n}\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n@media (prefers-color-scheme: dark) {\n .wall-length-inspector {\n background: rgba(24, 31, 44, 0.98);\n color: #f8fafc;\n }\n .input-shell {\n border-color: #475569;\n background: #111827;\n }\n .icon-button {\n background: #334155;\n color: #e2e8f0;\n }\n .icon-button.confirm {\n background: #f97316;\n color: white;\n }\n}\n'] }] 14067 }], null, { wall: [{ 14068 type: Input, 14069 args: [{ required: true }] 14070 }], error: [{ 14071 type: Input 14072 }], lengthPreview: [{ 14073 type: Output 14074 }], commitRequested: [{ 14075 type: Output 14076 }], cancelRequested: [{ 14077 type: Output 14078 }] }); 14079})(); 14080(() => { 14081 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(WallLengthInspectorComponent, { className: "WallLengthInspectorComponent", filePath: "src/app/pages/campus-map-v3/components/wall-length-inspector/wall-length-inspector.component.ts", lineNumber: 17 }); 14082})(); 14083
14084// src/app/pages/campus-map-v3/components/model-transform-panel/model-transform-panel.component.ts 14085function ModelTransformPanelComponent_For_28_Template(rf, ctx) { 14086 if (rf & 1) { 14087 const _r1 = \u0275\u0275getCurrentView(); 14088 \u0275\u0275elementStart(0, "button", 33); 14089 \u0275\u0275listener("click", function ModelTransformPanelComponent_For_28_Template_button_click_0_listener() { 14090 const preset_r2 = \u0275\u0275restoreView(_r1).$implicit; 14091 const ctx_r2 = \u0275\u0275nextContext(); 14092 return \u0275\u0275resetView(ctx_r2.setRotationPreset("y", preset_r2)); 14093 }); 14094 \u0275\u0275text(1); 14095 \u0275\u0275elementEnd(); 14096 } 14097 if (rf & 2) { 14098 const preset_r2 = ctx.$implicit; 14099 const ctx_r2 = \u0275\u0275nextContext(); 14100 \u0275\u0275classProp("active", ctx_r2.rotationY === preset_r2); 14101 \u0275\u0275advance(); 14102 \u0275\u0275textInterpolate1(" ", preset_r2, "\xB0 "); 14103 } 14104} 14105function ModelTransformPanelComponent_For_39_Template(rf, ctx) { 14106 if (rf & 1) { 14107 const _r4 = \u0275\u0275getCurrentView(); 14108 \u0275\u0275elementStart(0, "button", 33); 14109 \u0275\u0275listener("click", function ModelTransformPanelComponent_For_39_Template_button_click_0_listener() { 14110 const preset_r5 = \u0275\u0275restoreView(_r4).$implicit; 14111 const ctx_r2 = \u0275\u0275nextContext(); 14112 return \u0275\u0275resetView(ctx_r2.setScalePreset(preset_r5)); 14113 }); 14114 \u0275\u0275text(1); 14115 \u0275\u0275elementEnd(); 14116 } 14117 if (rf & 2) { 14118 const preset_r5 = ctx.$implicit; 14119 const ctx_r2 = \u0275\u0275nextContext(); 14120 \u0275\u0275classProp("active", ctx_r2.scale === preset_r5); 14121 \u0275\u0275advance(); 14122 \u0275\u0275textInterpolate1(" ", preset_r5, "% "); 14123 } 14124} 14125var _ModelTransformPanelComponent = class _ModelTransformPanelComponent { 14126 constructor() { 14127 this.transform = __spreadValues({}, DEFAULT_MODEL_3D_TRANSFORM); 14128 this.transformChanged = new EventEmitter(); 14129 this.transformCommit = new EventEmitter(); 14130 this.resetRequested = new EventEmitter(); 14131 this.closeRequested = new EventEmitter(); 14132 this.centerToViewportRequested = new EventEmitter(); 14133 this.scaleToViewportRequested = new EventEmitter(); 14134 this.centerToCampusRequested = new EventEmitter(); 14135 this.scaleToCampusRequested = new EventEmitter(); 14136 this.rotationX = 0; 14137 this.rotationY = 0; 14138 this.rotationZ = 0; 14139 this.scale = 100; 14140 this.offsetX = 0; 14141 this.offsetY = 0; 14142 this.offsetZ = 0; 14143 this.gamma = 1; 14144 this.rotationPresets = [0, 90, 180, 270]; 14145 this.scalePresets = [50, 100, 500, 2e3]; 14146 this.scalePinFormatter = (value) => `${value}%`; 14147 this.gammaPinFormatter = (value) => { 14148 if (value === 1) 14149 return "Normal"; 14150 return value.toFixed(1) + "x"; 14151 }; 14152 } 14153 ngOnChanges(changes) { 14154 if (changes["transform"]) { 14155 this.syncFromTransform(); 14156 } 14157 } 14158 syncFromTransform() { 14159 var _a3; 14160 this.rotationX = this.transform.rotationX; 14161 this.rotationY = this.transform.rotationY; 14162 this.rotationZ = this.transform.rotationZ; 14163 this.scale = Math.round(this.transform.scale * 100); 14164 this.offsetX = this.transform.offsetX; 14165 this.offsetY = this.transform.offsetY; 14166 this.offsetZ = this.transform.offsetZ; 14167 this.gamma = (_a3 = this.transform.gamma) != null ? _a3 : 1; 14168 } 14169 onRotationXChange(value) { 14170 this.rotationX = value; 14171 this.emitChange({ rotationX: value }); 14172 } 14173 onRotationYChange(value) { 14174 this.rotationY = value; 14175 this.emitChange({ rotationY: value }); 14176 } 14177 onRotationZChange(value) { 14178 this.rotationZ = value; 14179 this.emitChange({ rotationZ: value }); 14180 } 14181 onScaleChange(value) { 14182 this.scale = value; 14183 this.emitChange({ scale: value / 100 }); 14184 } 14185 onOffsetXChange(value) { 14186 this.offsetX = value; 14187 this.emitChange({ offsetX: value }); 14188 } 14189 onOffsetYChange(value) { 14190 this.offsetY = value; 14191 this.emitChange({ offsetY: value }); 14192 } 14193 onOffsetZChange(value) { 14194 this.offsetZ = value; 14195 this.emitChange({ offsetZ: value }); 14196 } 14197 onGammaChange(value) { 14198 this.gamma = value; 14199 this.emitChange({ gamma: value }); 14200 } 14201 onGammaCommit() { 14202 this.transformCommit.emit("gamma"); 14203 } 14204 // Commit handlers - called on blur/release to create undo point 14205 onRotationXCommit() { 14206 this.transformCommit.emit("rotationX"); 14207 } 14208 onRotationYCommit() { 14209 this.transformCommit.emit("rotationY"); 14210 } 14211 onRotationZCommit() { 14212 this.transformCommit.emit("rotationZ"); 14213 } 14214 onScaleCommit() { 14215 this.transformCommit.emit("scale"); 14216 } 14217 onOffsetXCommit() { 14218 this.transformCommit.emit("offsetX"); 14219 } 14220 onOffsetYCommit() { 14221 this.transformCommit.emit("offsetY"); 14222 } 14223 onOffsetZCommit() { 14224 this.transformCommit.emit("offsetZ"); 14225 } 14226 setRotationPreset(axis, value) { 14227 switch (axis) { 14228 case "x": 14229 this.onRotationXChange(value); 14230 this.onRotationXCommit(); 14231 break; 14232 case "y": 14233 this.onRotationYChange(value); 14234 this.onRotationYCommit(); 14235 break; 14236 case "z": 14237 this.onRotationZChange(value); 14238 this.onRotationZCommit(); 14239 break; 14240 } 14241 } 14242 setScalePreset(value) { 14243 this.onScaleChange(value); 14244 this.onScaleCommit(); 14245 } 14246 emitChange(partial) { 14247 this.transformChanged.emit(partial); 14248 } 14249 onReset() { 14250 this.resetRequested.emit(); 14251 } 14252 onClose() { 14253 this.closeRequested.emit(); 14254 } 14255 onCenterToViewport() { 14256 this.centerToViewportRequested.emit(); 14257 } 14258 onScaleToViewport() { 14259 this.scaleToViewportRequested.emit(); 14260 } 14261 onCenterToCampus() { 14262 this.centerToCampusRequested.emit(); 14263 } 14264 onScaleToCampus() { 14265 this.scaleToCampusRequested.emit(); 14266 } 14267}; 14268_ModelTransformPanelComponent.\u0275fac = function ModelTransformPanelComponent_Factory(__ngFactoryType__) { 14269 return new (__ngFactoryType__ || _ModelTransformPanelComponent)(); 14270}; 14271_ModelTransformPanelComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _ModelTransformPanelComponent, selectors: [["app-model-transform-panel"]], inputs: { transform: "transform" }, outputs: { transformChanged: "transformChanged", transformCommit: "transformCommit", resetRequested: "resetRequested", closeRequested: "closeRequested", centerToViewportRequested: "centerToViewportRequested", scaleToViewportRequested: "scaleToViewportRequested", centerToCampusRequested: "centerToCampusRequested", scaleToCampusRequested: "scaleToCampusRequested" }, features: [\u0275\u0275ProvidersFeature([], [ 14272 provideIcons({ 14273 lucideRotateCcw, 14274 lucideMove, 14275 lucideMaximize, 14276 lucideX, 14277 lucideUndo2, 14278 lucideTarget, 14279 lucideCrosshair, 14280 lucideFocus, 14281 lucideSun 14282 }) 14283]), \u0275\u0275NgOnChangesFeature], decls: 99, vars: 20, consts: [[1, "transform-panel"], [1, "panel-header"], [1, "panel-title"], ["title", "Close", 1, "close-button", 3, "click"], ["name", "lucideX", "size", "18"], [1, "panel-section"], [1, "section-header"], ["name", "lucideRotateCcw", "size", "16"], [1, "input-row"], [1, "input-group"], ["type", "number", "min", "-360", "max", "360", "step", "1", 3, "ngModelChange", "blur", "ngModel"], [1, "preset-row"], [1, "preset-label"], [1, "preset-button", 3, "active"], ["name", "lucideMaximize", "size", "16"], [1, "value-display"], [3, "ngModelChange", "ionBlur", "ngModel", "min", "max", "step", "pin", "pinFormatter"], ["name", "lucideSun", "size", "16"], [1, "range-labels"], [1, "range-label"], ["name", "lucideMove", "size", "16"], ["type", "number", "step", "10", 3, "ngModelChange", "blur", "ngModel"], ["name", "lucideTarget", "size", "16"], [1, "quick-actions-grid"], ["title", "Center model at current viewport center", 1, "quick-action-btn", 3, "click"], ["name", "lucideCrosshair", "size", "16"], ["title", "Scale model to fit current viewport", 1, "quick-action-btn", 3, "click"], ["name", "lucideFocus", "size", "16"], ["title", "Center model at campus center", 1, "quick-action-btn", 3, "click"], ["title", "Scale model to fit campus bounds", 1, "quick-action-btn", 3, "click"], [1, "panel-actions"], ["fill", "outline", "size", "small", "expand", "block", 3, "click"], ["name", "lucideUndo2", "slot", "start", "size", "16"], [1, "preset-button", 3, "click"]], template: function ModelTransformPanelComponent_Template(rf, ctx) { 14284 if (rf & 1) { 14285 \u0275\u0275elementStart(0, "div", 0)(1, "div", 1)(2, "span", 2); 14286 \u0275\u0275text(3, "3D Model Transform"); 14287 \u0275\u0275elementEnd(); 14288 \u0275\u0275elementStart(4, "button", 3); 14289 \u0275\u0275listener("click", function ModelTransformPanelComponent_Template_button_click_4_listener() { 14290 return ctx.onClose(); 14291 }); 14292 \u0275\u0275element(5, "ng-icon", 4); 14293 \u0275\u0275elementEnd()(); 14294 \u0275\u0275elementStart(6, "div", 5)(7, "div", 6); 14295 \u0275\u0275element(8, "ng-icon", 7); 14296 \u0275\u0275elementStart(9, "span"); 14297 \u0275\u0275text(10, "Rotation (degrees)"); 14298 \u0275\u0275elementEnd()(); 14299 \u0275\u0275elementStart(11, "div", 8)(12, "div", 9)(13, "label"); 14300 \u0275\u0275text(14, "X"); 14301 \u0275\u0275elementEnd();
14302 \u0275\u0275elementStart(15, "input", 10); 14303 \u0275\u0275listener("ngModelChange", function ModelTransformPanelComponent_Template_input_ngModelChange_15_listener($event) { 14304 return ctx.onRotationXChange($event); 14305 })("blur", function ModelTransformPanelComponent_Template_input_blur_15_listener() { 14306 return ctx.onRotationXCommit(); 14307 }); 14308 \u0275\u0275elementEnd()(); 14309 \u0275\u0275elementStart(16, "div", 9)(17, "label"); 14310 \u0275\u0275text(18, "Y"); 14311 \u0275\u0275elementEnd(); 14312 \u0275\u0275elementStart(19, "input", 10); 14313 \u0275\u0275listener("ngModelChange", function ModelTransformPanelComponent_Template_input_ngModelChange_19_listener($event) { 14314 return ctx.onRotationYChange($event); 14315 })("blur", function ModelTransformPanelComponent_Template_input_blur_19_listener() { 14316 return ctx.onRotationYCommit(); 14317 }); 14318 \u0275\u0275elementEnd()(); 14319 \u0275\u0275elementStart(20, "div", 9)(21, "label"); 14320 \u0275\u0275text(22, "Z"); 14321 \u0275\u0275elementEnd(); 14322 \u0275\u0275elementStart(23, "input", 10); 14323 \u0275\u0275listener("ngModelChange", function ModelTransformPanelComponent_Template_input_ngModelChange_23_listener($event) { 14324 return ctx.onRotationZChange($event); 14325 })("blur", function ModelTransformPanelComponent_Template_input_blur_23_listener() { 14326 return ctx.onRotationZCommit(); 14327 }); 14328 \u0275\u0275elementEnd()()(); 14329 \u0275\u0275elementStart(24, "div", 11)(25, "span", 12); 14330 \u0275\u0275text(26, "Y:"); 14331 \u0275\u0275elementEnd(); 14332 \u0275\u0275repeaterCreate(27, ModelTransformPanelComponent_For_28_Template, 2, 3, "button", 13, \u0275\u0275repeaterTrackByIdentity); 14333 \u0275\u0275elementEnd()(); 14334 \u0275\u0275elementStart(29, "div", 5)(30, "div", 6); 14335 \u0275\u0275element(31, "ng-icon", 14); 14336 \u0275\u0275elementStart(32, "span"); 14337 \u0275\u0275text(33, "Scale"); 14338 \u0275\u0275elementEnd(); 14339 \u0275\u0275elementStart(34, "span", 15); 14340 \u0275\u0275text(35); 14341 \u0275\u0275elementEnd()(); 14342 \u0275\u0275elementStart(36, "ion-range", 16); 14343 \u0275\u0275listener("ngModelChange", function ModelTransformPanelComponent_Template_ion_range_ngModelChange_36_listener($event) { 14344 return ctx.onScaleChange($event); 14345 })("ionBlur", function ModelTransformPanelComponent_Template_ion_range_ionBlur_36_listener() { 14346 return ctx.onScaleCommit(); 14347 }); 14348 \u0275\u0275elementEnd(); 14349 \u0275\u0275elementStart(37, "div", 11); 14350 \u0275\u0275repeaterCreate(38, ModelTransformPanelComponent_For_39_Template, 2, 3, "button", 13, \u0275\u0275repeaterTrackByIdentity); 14351 \u0275\u0275elementEnd()(); 14352 \u0275\u0275elementStart(40, "div", 5)(41, "div", 6); 14353 \u0275\u0275element(42, "ng-icon", 17); 14354 \u0275\u0275elementStart(43, "span"); 14355 \u0275\u0275text(44, "Brightness/Gamma"); 14356 \u0275\u0275elementEnd(); 14357 \u0275\u0275elementStart(45, "span", 15); 14358 \u0275\u0275text(46); 14359 \u0275\u0275elementEnd()(); 14360 \u0275\u0275elementStart(47, "ion-range", 16); 14361 \u0275\u0275listener("ngModelChange", function ModelTransformPanelComponent_Template_ion_range_ngModelChange_47_listener($event) { 14362 return ctx.onGammaChange($event); 14363 })("ionBlur", function ModelTransformPanelComponent_Template_ion_range_ionBlur_47_listener() { 14364 return ctx.onGammaCommit(); 14365 }); 14366 \u0275\u0275elementEnd(); 14367 \u0275\u0275elementStart(48, "div", 18)(49, "span", 19); 14368 \u0275\u0275text(50, "Dark"); 14369 \u0275\u0275elementEnd(); 14370 \u0275\u0275elementStart(51, "span", 19); 14371 \u0275\u0275text(52, "Normal"); 14372 \u0275\u0275elementEnd(); 14373 \u0275\u0275elementStart(53, "span", 19); 14374 \u0275\u0275text(54, "Bright"); 14375 \u0275\u0275elementEnd()()(); 14376 \u0275\u0275elementStart(55, "div", 5)(56, "div", 6); 14377 \u0275\u0275element(57, "ng-icon", 20); 14378 \u0275\u0275elementStart(58, "span"); 14379 \u0275\u0275text(59, "Offset (cm)"); 14380 \u0275\u0275elementEnd()(); 14381 \u0275\u0275elementStart(60, "div", 8)(61, "div", 9)(62, "label"); 14382 \u0275\u0275text(63, "X"); 14383 \u0275\u0275elementEnd(); 14384 \u0275\u0275elementStart(64, "input", 21); 14385 \u0275\u0275listener("ngModelChange", function ModelTransformPanelComponent_Template_input_ngModelChange_64_listener($event) { 14386 return ctx.onOffsetXChange($event); 14387 })("blur", function ModelTransformPanelComponent_Template_input_blur_64_listener() { 14388 return ctx.onOffsetXCommit(); 14389 }); 14390 \u0275\u0275elementEnd()(); 14391 \u0275\u0275elementStart(65, "div", 9)(66, "label"); 14392 \u0275\u0275text(67, "Y"); 14393 \u0275\u0275elementEnd(); 14394 \u0275\u0275elementStart(68, "input", 21); 14395 \u0275\u0275listener("ngModelChange", function ModelTransformPanelComponent_Template_input_ngModelChange_68_listener($event) { 14396 return ctx.onOffsetYChange($event); 14397 })("blur", function ModelTransformPanelComponent_Template_input_blur_68_listener() { 14398 return ctx.onOffsetYCommit(); 14399 }); 14400 \u0275\u0275elementEnd()(); 14401 \u0275\u0275elementStart(69, "div", 9)(70, "label"); 14402 \u0275\u0275text(71, "Z"); 14403 \u0275\u0275elementEnd(); 14404 \u0275\u0275elementStart(72, "input", 21); 14405 \u0275\u0275listener("ngModelChange", function ModelTransformPanelComponent_Template_input_ngModelChange_72_listener($event) { 14406 return ctx.onOffsetZChange($event); 14407 })("blur", function ModelTransformPanelComponent_Template_input_blur_72_listener() { 14408 return ctx.onOffsetZCommit(); 14409 }); 14410 \u0275\u0275elementEnd()()()(); 14411 \u0275\u0275elementStart(73, "div", 5)(74, "div", 6); 14412 \u0275\u0275element(75, "ng-icon", 22); 14413 \u0275\u0275elementStart(76, "span"); 14414 \u0275\u0275text(77, "Quick Actions"); 14415 \u0275\u0275elementEnd()(); 14416 \u0275\u0275elementStart(78, "div", 23)(79, "button", 24); 14417 \u0275\u0275listener("click", function ModelTransformPanelComponent_Template_button_click_79_listener() { 14418 return ctx.onCenterToViewport(); 14419 }); 14420 \u0275\u0275element(80, "ng-icon", 25); 14421 \u0275\u0275elementStart(81, "span"); 14422 \u0275\u0275text(82, "Center to Viewport"); 14423 \u0275\u0275elementEnd()(); 14424 \u0275\u0275elementStart(83, "button", 26); 14425 \u0275\u0275listener("click", function ModelTransformPanelComponent_Template_button_click_83_listener() { 14426 return ctx.onScaleToViewport(); 14427 }); 14428 \u0275\u0275element(84, "ng-icon", 27); 14429 \u0275\u0275elementStart(85, "span"); 14430 \u0275\u0275text(86, "Scale to Viewport"); 14431 \u0275\u0275elementEnd()(); 14432 \u0275\u0275elementStart(87, "button", 28); 14433 \u0275\u0275listener("click", function ModelTransformPanelComponent_Template_button_click_87_listener() { 14434 return ctx.onCenterToCampus(); 14435 }); 14436 \u0275\u0275element(88, "ng-icon", 22); 14437 \u0275\u0275elementStart(89, "span"); 14438 \u0275\u0275text(90, "Center to Campus"); 14439 \u0275\u0275elementEnd()(); 14440 \u0275\u0275elementStart(91, "button", 29); 14441 \u0275\u0275listener("click", function ModelTransformPanelComponent_Template_button_click_91_listener() { 14442 return ctx.onScaleToCampus(); 14443 }); 14444 \u0275\u0275element(92, "ng-icon", 14); 14445 \u0275\u0275elementStart(93, "span"); 14446 \u0275\u0275text(94, "Scale to Campus"); 14447 \u0275\u0275elementEnd()()()(); 14448 \u0275\u0275elementStart(95, "div", 30)(96, "ion-button", 31); 14449 \u0275\u0275listener("click", function ModelTransformPanelComponent_Template_ion_button_click_96_listener() { 14450 return ctx.onReset(); 14451 }); 14452 \u0275\u0275element(97, "ng-icon", 32); 14453 \u0275\u0275text(98, " Reset to Defaults "); 14454 \u0275\u0275elementEnd()()(); 14455 } 14456 if (rf & 2) { 14457 \u0275\u0275advance(15); 14458 \u0275\u0275property("ngModel", ctx.rotationX); 14459 \u0275\u0275advance(4); 14460 \u0275\u0275property("ngModel", ctx.rotationY); 14461 \u0275\u0275advance(4); 14462 \u0275\u0275property("ngModel", ctx.rotationZ); 14463 \u0275\u0275advance(4); 14464 \u0275\u0275repeater(ctx.rotationPresets); 14465 \u0275\u0275advance(8); 14466 \u0275\u0275textInterpolate1("", ctx.scale, "%"); 14467 \u0275\u0275advance(); 14468 \u0275\u0275property("ngModel", ctx.scale)("min", 1)("max", 1e4)("step", 1)("pin", true)("pinFormatter", ctx.scalePinFormatter); 14469 \u0275\u0275advance(2); 14470 \u0275\u0275repeater(ctx.scalePresets); 14471 \u0275\u0275advance(8); 14472 \u0275\u0275textInterpolate1("", ctx.gamma.toFixed(1), "x"); 14473 \u0275\u0275advance(); 14474 \u0275\u0275property("ngModel", ctx.gamma)("min", 0.5)("max", 5)("step", 0.1)("pin", true)("pinFormatter", ctx.gammaPinFormatter); 14475 \u0275\u0275advance(17); 14476 \u0275\u0275property("ngModel", ctx.offsetX); 14477 \u0275\u0275advance(4); 14478 \u0275\u0275property("ngModel", ctx.offsetY); 14479 \u0275\u0275advance(4); 14480 \u0275\u0275property("ngModel", ctx.offsetZ); 14481 } 14482}, dependencies: [ 14483 CommonModule, 14484 FormsModule, 14485 DefaultValueAccessor, 14486 NumberValueAccessor, 14487 NgControlStatus, 14488 MinValidator, 14489 MaxValidator, 14490 NgModel, 14491 IonButton, 14492 IonRange, 14493 NgIcon 14494], styles: ["\n\n.transform-panel[_ngcontent-%COMP%] {\n background: var(--tool-palette-bg, rgba(255, 255, 255, 0.95));\n border-radius: 12px;\n box-shadow: var(--tool-palette-shadow, 0 4px 12px rgba(0, 0, 0, 0.15));\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light, rgba(0, 0, 0, 0.08));\n min-width: 380px;\n max-width: 380px;\n overflow: hidden;\n}\n.panel-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 16px;\n border-bottom: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n}\n.panel-title[_ngcontent-%COMP%] {\n font-weight: 600;\n font-size: 14px;\n color: var(--ion-text-color);\n}\n.close-button[_ngcontent-%COMP%] {\n background: transparent;\n border: none;\n padding: 4px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n color: var(--ion-color-medium);\n transition: all 0.2s ease;\n}\n.close-button[_ngcontent-%COMP%]:hover {\n background: rgba(var(--ion-color-danger-rgb), 0.1);\n color: var(--ion-color-danger);\n}\n.panel-section[_ngcontent-%COMP%] {\n padding: 12px 16px;\n border-bottom: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n}\n.panel-section[_ngcontent-%COMP%]:last-of-type {\n border-bottom: none;\n}\n.section-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-bottom: 10px;\n font-size: 13px;\n font-weight: 500;\n color: var(--ion-color-dark);\n}\n.section-header[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n color: var(--ion-color-primary);\n}\n.section-header[_ngcontent-%COMP%] .value-display[_ngcontent-%COMP%] {\n margin-left: auto;\n font-family: monospace;\n font-size: 12px;\n color: var(--ion-color-medium);\n background: rgba(0, 0, 0, 0.05);\n padding: 2px 8px;\n border-radius: 4px;\n}\n.input-row[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 8px;\n}\n.input-group[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.input-group[_ngcontent-%COMP%] label[_ngcontent-%COMP%] {\n font-size: 11px;\n font-weight: 500;\n color: var(--ion-color-medium);\n text-transform: uppercase;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%] {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n font-family: monospace;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n transition: border-color 0.2s ease;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::-webkit-inner-spin-button, \n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::-webkit-outer-spin-button {\n opacity: 1;\n}\n.preset-row[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-top: 10px;\n flex-wrap: wrap;\n}\n.preset-label[_ngcontent-%COMP%] {\n font-size: 11px;\n font-weight: 500;\n color: var(--ion-color-medium);\n}\n.preset-button[_ngcontent-%COMP%] {\n padding: 4px 10px;\n font-size: 12px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 4px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n cursor: pointer;\n transition: all 0.2s ease;\n}\n.preset-button[_ngcontent-%COMP%]:hover {\n border-color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n}\n.preset-button.active[_ngcontent-%COMP%] {\n border-color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n color: var(--ion-color-primary);\n font-weight: 500;\n}\nion-range[_ngcontent-%COMP%] {\n --bar-height: 4px;\n --bar-border-radius: 2px;\n --knob-size: 18px;\n --bar-background: rgba(var(--ion-color-primary-rgb), 0.2);\n --bar-background-active: var(--ion-color-primary);\n --knob-background: var(--ion-color-primary);\n padding: 0;\n margin: 0;\n}\n.range-labels[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n margin-top: 6px;\n padding: 0 4px;\n}\n.range-label[_ngcontent-%COMP%] {\n font-size: 10px;\n font-weight: 500;\n color: var(--ion-color-medium);\n text-transform: uppercase;\n}\n.quick-actions-grid[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 8px;\n}\n.quick-action-btn[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 4px;\n padding: 10px 8px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 8px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n cursor: pointer;\n transition: all 0.2s ease;\n font-size: 11px;\n text-align: center;\n}\n.quick-action-btn[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n color: var(--ion-color-primary);\n}\n.quick-action-btn[_ngcontent-%COMP%]:hover {\n border-color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n}\n.quick-action-btn[_ngcontent-%COMP%]:active {\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n}\n.panel-actions[_ngcontent-%COMP%] {\n padding: 12px 16px;\n background: rgba(0, 0, 0, 0.02);\n border-top: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n}\n.panel-actions[_ngcontent-%COMP%] ion-button[_ngcontent-%COMP%] {\n margin: 0;\n --padding-start: 12px;\n --padding-end: 12px;\n font-size: 13px;\n}\n.panel-actions[_ngcontent-%COMP%] ion-button[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n margin-right: 4px;\n}"], changeDetection: 0 }); 14495var ModelTransformPanelComponent = _ModelTransformPanelComponent; 14496(() => { 14497 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(ModelTransformPanelComponent, [{ 14498 type: Component, 14499 args: [{ selector: "app-model-transform-panel", standalone: true, imports: [ 14500 CommonModule, 14501 FormsModule, 14502 IonButton, 14503 IonRange, 14504 NgIcon 14505 ], viewProviders: [ 14506 provideIcons({ 14507 lucideRotateCcw, 14508 lucideMove, 14509 lucideMaximize, 14510 lucideX, 14511 lucideUndo2, 14512 lucideTarget, 14513 lucideCrosshair, 14514 lucideFocus, 14515 lucideSun 14516 }) 14517 ], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="transform-panel"> 14518 <!-- Header --> 14519 <div class="panel-header"> 14520 <span class="panel-title">3D Model Transform</span> 14521 <button class="close-button" (click)="onClose()" title="Close"> 14522 <ng-icon name="lucideX" size="18" /> 14523 </button> 14524 </div> 14525 14526 <!-- Rotation Section --> 14527 <div class="panel-section"> 14528 <div class="section-header"> 14529 <ng-icon name="lucideRotateCcw" size="16" />
14530 <span>Rotation (degrees)</span> 14531 </div> 14532 <div class="input-row"> 14533 <div class="input-group"> 14534 <label>X</label> 14535 <input 14536 type="number" 14537 [ngModel]="rotationX" 14538 (ngModelChange)="onRotationXChange($event)" 14539 (blur)="onRotationXCommit()" 14540 min="-360" 14541 max="360" 14542 step="1" 14543 /> 14544 </div> 14545 <div class="input-group"> 14546 <label>Y</label> 14547 <input 14548 type="number" 14549 [ngModel]="rotationY" 14550 (ngModelChange)="onRotationYChange($event)" 14551 (blur)="onRotationYCommit()" 14552 min="-360" 14553 max="360" 14554 step="1" 14555 /> 14556 </div> 14557 <div class="input-group"> 14558 <label>Z</label> 14559 <input 14560 type="number" 14561 [ngModel]="rotationZ" 14562 (ngModelChange)="onRotationZChange($event)" 14563 (blur)="onRotationZCommit()" 14564 min="-360" 14565 max="360" 14566 step="1" 14567 /> 14568 </div> 14569 </div> 14570 <div class="preset-row"> 14571 <span class="preset-label">Y:</span> 14572 @for (preset of rotationPresets; track preset) { 14573 <button 14574 class="preset-button" 14575 [class.active]="rotationY === preset" 14576 (click)="setRotationPreset('y', preset)" 14577 > 14578 {{ preset }}\xB0 14579 </button> 14580 } 14581 </div> 14582 </div> 14583 14584 <!-- Scale Section --> 14585 <div class="panel-section"> 14586 <div class="section-header"> 14587 <ng-icon name="lucideMaximize" size="16" /> 14588 <span>Scale</span> 14589 <span class="value-display">{{ scale }}%</span> 14590 </div> 14591 <ion-range 14592 [ngModel]="scale" 14593 (ngModelChange)="onScaleChange($event)" 14594 (ionBlur)="onScaleCommit()" 14595 [min]="1" 14596 [max]="10000" 14597 [step]="1" 14598 [pin]="true" 14599 [pinFormatter]="scalePinFormatter" 14600 /> 14601 <div class="preset-row"> 14602 @for (preset of scalePresets; track preset) { 14603 <button 14604 class="preset-button" 14605 [class.active]="scale === preset" 14606 (click)="setScalePreset(preset)" 14607 > 14608 {{ preset }}% 14609 </button> 14610 } 14611 </div> 14612 </div> 14613 14614 <!-- Gamma/Brightness Section --> 14615 <div class="panel-section"> 14616 <div class="section-header"> 14617 <ng-icon name="lucideSun" size="16" /> 14618 <span>Brightness/Gamma</span> 14619 <span class="value-display">{{ gamma.toFixed(1) }}x</span> 14620 </div> 14621 <ion-range 14622 [ngModel]="gamma" 14623 (ngModelChange)="onGammaChange($event)" 14624 (ionBlur)="onGammaCommit()" 14625 [min]="0.5" 14626 [max]="5.0" 14627 [step]="0.1" 14628 [pin]="true" 14629 [pinFormatter]="gammaPinFormatter" 14630 /> 14631 <div class="range-labels"> 14632 <span class="range-label">Dark</span> 14633 <span class="range-label">Normal</span> 14634 <span class="range-label">Bright</span> 14635 </div> 14636 </div> 14637 14638 <!-- Offset Section --> 14639 <div class="panel-section"> 14640 <div class="section-header"> 14641 <ng-icon name="lucideMove" size="16" /> 14642 <span>Offset (cm)</span> 14643 </div> 14644 <div class="input-row"> 14645 <div class="input-group"> 14646 <label>X</label> 14647 <input 14648 type="number" 14649 [ngModel]="offsetX" 14650 (ngModelChange)="onOffsetXChange($event)" 14651 (blur)="onOffsetXCommit()" 14652 step="10" 14653 /> 14654 </div> 14655 <div class="input-group"> 14656 <label>Y</label> 14657 <input 14658 type="number" 14659 [ngModel]="offsetY" 14660 (ngModelChange)="onOffsetYChange($event)" 14661 (blur)="onOffsetYCommit()" 14662 step="10" 14663 /> 14664 </div> 14665 <div class="input-group"> 14666 <label>Z</label> 14667 <input 14668 type="number" 14669 [ngModel]="offsetZ" 14670 (ngModelChange)="onOffsetZChange($event)" 14671 (blur)="onOffsetZCommit()" 14672 step="10" 14673 /> 14674 </div> 14675 </div> 14676 </div> 14677 14678 <!-- Quick Actions Section --> 14679 <div class="panel-section"> 14680 <div class="section-header"> 14681 <ng-icon name="lucideTarget" size="16" /> 14682 <span>Quick Actions</span> 14683 </div> 14684 <div class="quick-actions-grid"> 14685 <button 14686 class="quick-action-btn" 14687 (click)="onCenterToViewport()" 14688 title="Center model at current viewport center" 14689 > 14690 <ng-icon name="lucideCrosshair" size="16" />
14691 <span>Center to Viewport</span> 14692 </button> 14693 <button 14694 class="quick-action-btn" 14695 (click)="onScaleToViewport()" 14696 title="Scale model to fit current viewport" 14697 > 14698 <ng-icon name="lucideFocus" size="16" /> 14699 <span>Scale to Viewport</span> 14700 </button> 14701 <button 14702 class="quick-action-btn" 14703 (click)="onCenterToCampus()" 14704 title="Center model at campus center" 14705 > 14706 <ng-icon name="lucideTarget" size="16" /> 14707 <span>Center to Campus</span> 14708 </button> 14709 <button 14710 class="quick-action-btn" 14711 (click)="onScaleToCampus()" 14712 title="Scale model to fit campus bounds" 14713 > 14714 <ng-icon name="lucideMaximize" size="16" /> 14715 <span>Scale to Campus</span> 14716 </button> 14717 </div> 14718 </div> 14719 14720 <!-- Actions --> 14721 <div class="panel-actions"> 14722 <ion-button fill="outline" size="small" expand="block" (click)="onReset()"> 14723 <ng-icon name="lucideUndo2" slot="start" size="16" /> 14724 Reset to Defaults 14725 </ion-button> 14726 </div> 14727</div> 14728`, styles: ["/* src/app/pages/campus-map-v3/components/model-transform-panel/model-transform-panel.component.scss */\n.transform-panel {\n background: var(--tool-palette-bg, rgba(255, 255, 255, 0.95));\n border-radius: 12px;\n box-shadow: var(--tool-palette-shadow, 0 4px 12px rgba(0, 0, 0, 0.15));\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light, rgba(0, 0, 0, 0.08));\n min-width: 380px;\n max-width: 380px;\n overflow: hidden;\n}\n.panel-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 16px;\n border-bottom: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n}\n.panel-title {\n font-weight: 600;\n font-size: 14px;\n color: var(--ion-text-color);\n}\n.close-button {\n background: transparent;\n border: none;\n padding: 4px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n color: var(--ion-color-medium);\n transition: all 0.2s ease;\n}\n.close-button:hover {\n background: rgba(var(--ion-color-danger-rgb), 0.1);\n color: var(--ion-color-danger);\n}\n.panel-section {\n padding: 12px 16px;\n border-bottom: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n}\n.panel-section:last-of-type {\n border-bottom: none;\n}\n.section-header {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-bottom: 10px;\n font-size: 13px;\n font-weight: 500;\n color: var(--ion-color-dark);\n}\n.section-header ng-icon {\n color: var(--ion-color-primary);\n}\n.section-header .value-display {\n margin-left: auto;\n font-family: monospace;\n font-size: 12px;\n color: var(--ion-color-medium);\n background: rgba(0, 0, 0, 0.05);\n padding: 2px 8px;\n border-radius: 4px;\n}\n.input-row {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 8px;\n}\n.input-group {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.input-group label {\n font-size: 11px;\n font-weight: 500;\n color: var(--ion-color-medium);\n text-transform: uppercase;\n}\n.input-group input {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n font-family: monospace;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n transition: border-color 0.2s ease;\n}\n.input-group input:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.input-group input::-webkit-inner-spin-button,\n.input-group input::-webkit-outer-spin-button {\n opacity: 1;\n}\n.preset-row {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-top: 10px;\n flex-wrap: wrap;\n}\n.preset-label {\n font-size: 11px;\n font-weight: 500;\n color: var(--ion-color-medium);\n}\n.preset-button {\n padding: 4px 10px;\n font-size: 12px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 4px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n cursor: pointer;\n transition: all 0.2s ease;\n}\n.preset-button:hover {\n border-color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n}\n.preset-button.active {\n border-color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n color: var(--ion-color-primary);\n font-weight: 500;\n}\nion-range {\n --bar-height: 4px;\n --bar-border-radius: 2px;\n --knob-size: 18px;\n --bar-background: rgba(var(--ion-color-primary-rgb), 0.2);\n --bar-background-active: var(--ion-color-primary);\n --knob-background: var(--ion-color-primary);\n padding: 0;\n margin: 0;\n}\n.range-labels {\n display: flex;\n justify-content: space-between;\n margin-top: 6px;\n padding: 0 4px;\n}\n.range-label {\n font-size: 10px;\n font-weight: 500;\n color: var(--ion-color-medium);\n text-transform: uppercase;\n}\n.quick-actions-grid {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 8px;\n}\n.quick-action-btn {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 4px;\n padding: 10px 8px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 8px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n cursor: pointer;\n transition: all 0.2s ease;\n font-size: 11px;\n text-align: center;\n}\n.quick-action-btn ng-icon {\n color: var(--ion-color-primary);\n}\n.quick-action-btn:hover {\n border-color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n}\n.quick-action-btn:active {\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n}\n.panel-actions {\n padding: 12px 16px;\n background: rgba(0, 0, 0, 0.02);\n border-top: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n}\n.panel-actions ion-button {\n margin: 0;\n --padding-start: 12px;\n --padding-end: 12px;\n font-size: 13px;\n}
14728\n.panel-actions ion-button ng-icon {\n margin-right: 4px;\n}\n"] }] 14729 }], null, { transform: [{ 14730 type: Input 14731 }], transformChanged: [{ 14732 type: Output 14733 }], transformCommit: [{ 14734 type: Output 14735 }], resetRequested: [{ 14736 type: Output 14737 }], closeRequested: [{ 14738 type: Output 14739 }], centerToViewportRequested: [{ 14740 type: Output 14741 }], scaleToViewportRequested: [{ 14742 type: Output 14743 }], centerToCampusRequested: [{ 14744 type: Output 14745 }], scaleToCampusRequested: [{ 14746 type: Output 14747 }] }); 14748})(); 14749(() => { 14750 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(ModelTransformPanelComponent, { className: "ModelTransformPanelComponent", filePath: "src/app/pages/campus-map-v3/components/model-transform-panel/model-transform-panel.component.ts", lineNumber: 55 }); 14751})(); 14752
14753// src/app/shared/components/entity-panel/entity-panel.component.ts 14754var _c05 = [[["", "panel-content", ""]], [["", "panel-info", ""]]]; 14755var _c12 = ["[panel-content]", "[panel-info]"]; 14756function EntityPanelComponent_Conditional_13_Template(rf, ctx) { 14757 if (rf & 1) { 14758 \u0275\u0275element(0, "ng-icon", 9); 14759 } 14760} 14761var _EntityPanelComponent = class _EntityPanelComponent { 14762 constructor() { 14763 this.title = "Edit"; 14764 this.applyText = "Apply"; 14765 this.applyDisabled = false; 14766 this.showApplyIcon = true; 14767 this.closeRequested = new EventEmitter(); 14768 this.cancelRequested = new EventEmitter(); 14769 this.applyRequested = new EventEmitter(); 14770 } 14771 onClose() { 14772 this.closeRequested.emit(); 14773 } 14774 onCancel() { 14775 this.cancelRequested.emit(); 14776 } 14777 onApply() { 14778 this.applyRequested.emit(); 14779 } 14780}; 14781_EntityPanelComponent.\u0275fac = function EntityPanelComponent_Factory(__ngFactoryType__) { 14782 return new (__ngFactoryType__ || _EntityPanelComponent)(); 14783}; 14784_EntityPanelComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _EntityPanelComponent, selectors: [["app-entity-panel"]], inputs: { title: "title", applyText: "applyText", applyDisabled: "applyDisabled", showApplyIcon: "showApplyIcon" }, outputs: { closeRequested: "closeRequested", cancelRequested: "cancelRequested", applyRequested: "applyRequested" }, features: [\u0275\u0275ProvidersFeature([], [provideIcons({ lucideX, lucideSave })])], ngContentSelectors: _c12, decls: 15, vars: 4, consts: [[1, "entity-edit-panel"], [1, "panel-header"], [1, "panel-title"], ["title", "Close", 1, "close-button", 3, "click"], ["name", "lucideX", "size", "18"], [1, "panel-section", "info-section"], [1, "panel-actions"], ["fill", "outline", "size", "small", 3, "click"], ["size", "small", 3, "click", "disabled"], ["name", "lucideSave", "slot", "start", "size", "16"]], template: function EntityPanelComponent_Template(rf, ctx) { 14785 if (rf & 1) { 14786 \u0275\u0275projectionDef(_c05); 14787 \u0275\u0275elementStart(0, "div", 0)(1, "div", 1)(2, "span", 2); 14788 \u0275\u0275text(3); 14789 \u0275\u0275elementEnd(); 14790 \u0275\u0275elementStart(4, "button", 3); 14791 \u0275\u0275listener("click", function EntityPanelComponent_Template_button_click_4_listener() { 14792 return ctx.onClose(); 14793 }); 14794 \u0275\u0275element(5, "ng-icon", 4); 14795 \u0275\u0275elementEnd()(); 14796 \u0275\u0275projection(6); 14797 \u0275\u0275elementStart(7, "div", 5); 14798 \u0275\u0275projection(8, 1); 14799 \u0275\u0275elementEnd(); 14800 \u0275\u0275elementStart(9, "div", 6)(10, "ion-button", 7); 14801 \u0275\u0275listener("click", function EntityPanelComponent_Template_ion_button_click_10_listener() { 14802 return ctx.onCancel(); 14803 }); 14804 \u0275\u0275text(11, " Cancel "); 14805 \u0275\u0275elementEnd(); 14806 \u0275\u0275elementStart(12, "ion-button", 8); 14807 \u0275\u0275listener("click", function EntityPanelComponent_Template_ion_button_click_12_listener() { 14808 return ctx.onApply(); 14809 }); 14810 \u0275\u0275template(13, EntityPanelComponent_Conditional_13_Template, 1, 0, "ng-icon", 9); 14811 \u0275\u0275text(14); 14812 \u0275\u0275elementEnd()()(); 14813 } 14814 if (rf & 2) { 14815 \u0275\u0275advance(3); 14816 \u0275\u0275textInterpolate(ctx.title); 14817 \u0275\u0275advance(9); 14818 \u0275\u0275property("disabled", ctx.applyDisabled); 14819 \u0275\u0275advance(); 14820 \u0275\u0275conditional(ctx.showApplyIcon ? 13 : -1); 14821 \u0275\u0275advance(); 14822 \u0275\u0275textInterpolate1(" ", ctx.applyText, " "); 14823 } 14824}, dependencies: [CommonModule, IonButton, NgIcon], styles: ["\n\n.entity-edit-panel[_ngcontent-%COMP%] {\n background: var(--tool-palette-bg, rgba(255, 255, 255, 0.95));\n border-radius: 12px;\n box-shadow: var(--tool-palette-shadow, 0 4px 12px rgba(0, 0, 0, 0.15));\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light, rgba(0, 0, 0, 0.08));\n min-width: 380px;\n max-width: 380px;\n overflow: hidden;\n}\n.panel-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 16px;\n border-bottom: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n}\n.panel-title[_ngcontent-%COMP%] {\n font-weight: 600;\n font-size: 14px;\n color: var(--ion-text-color);\n}\n.close-button[_ngcontent-%COMP%] {\n background: transparent;\n border: none;\n padding: 4px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n color: var(--ion-color-medium);\n transition: all 0.2s ease;\n}\n.close-button[_ngcontent-%COMP%]:hover {\n background: rgba(var(--ion-color-danger-rgb), 0.1);\n color: var(--ion-color-danger);\n}\n.info-section[_ngcontent-%COMP%] {\n background: rgba(0, 0, 0, 0.02);\n padding: 10px 16px;\n}
14824\n.info-section[_ngcontent-%COMP%]:empty {\n display: none;\n}\n.panel-actions[_ngcontent-%COMP%] {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 16px;\n background: rgba(0, 0, 0, 0.02);\n border-top: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n}\n.panel-actions[_ngcontent-%COMP%] ion-button[_ngcontent-%COMP%] {\n --padding-start: 12px;\n --padding-end: 12px;\n font-size: 13px;\n}\n.panel-actions[_ngcontent-%COMP%] ion-button[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n margin-right: 4px;\n}"], changeDetection: 0 }); 14825var EntityPanelComponent = _EntityPanelComponent; 14826(() => { 14827 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(EntityPanelComponent, [{ 14828 type: Component, 14829 args: [{ selector: "app-entity-panel", standalone: true, imports: [CommonModule, IonButton, NgIcon], viewProviders: [provideIcons({ lucideX, lucideSave })], changeDetection: ChangeDetectionStrategy.OnPush, template: '<div class="entity-edit-panel">\n <!-- Header -->\n <div class="panel-header">\n <span class="panel-title">{{ title }}</span>\n <button class="close-button" (click)="onClose()" title="Close">\n <ng-icon name="lucideX" size="18" />\n </button>\n </div>\n\n <!-- Content Slot (entity-specific sections) -->\n <ng-content select="[panel-content]"></ng-content>\n\n <!-- Info Slot (read-only metadata) -->\n <div class="panel-section info-section">\n <ng-content select="[panel-info]"></ng-content>\n </div>\n\n <!-- Actions Footer -->\n <div class="panel-actions">\n <ion-button fill="outline" size="small" (click)="onCancel()">\n Cancel\n </ion-button>\n <ion-button size="small" [disabled]="applyDisabled" (click)="onApply()">\n @if (showApplyIcon) {\n <ng-icon name="lucideSave" slot="start" size="16" />\n }\n {{ applyText }}\n </ion-button>\n </div>\n</div>\n', styles: ["/* src/app/shared/components/entity-panel/entity-panel.component.scss */\n.entity-edit-panel {\n background: var(--tool-palette-bg, rgba(255, 255, 255, 0.95));\n border-radius: 12px;\n box-shadow: var(--tool-palette-shadow, 0 4px 12px rgba(0, 0, 0, 0.15));\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light, rgba(0, 0, 0, 0.08));\n min-width: 380px;\n max-width: 380px;\n overflow: hidden;\n}\n.panel-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 16px;\n border-bottom: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n}\n.panel-title {\n font-weight: 600;\n font-size: 14px;\n color: var(--ion-text-color);\n}\n.close-button {\n background: transparent;\n border: none;\n padding: 4px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n color: var(--ion-color-medium);\n transition: all 0.2s ease;\n}\n.close-button:hover {\n background: rgba(var(--ion-color-danger-rgb), 0.1);\n color: var(--ion-color-danger);\n}\n.info-section {\n background: rgba(0, 0, 0, 0.02);\n padding: 10px 16px;\n}\n.info-section:empty {\n display: none;\n}\n.panel-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 16px;\n background: rgba(0, 0, 0, 0.02);\n border-top: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n}\n.panel-actions ion-button {\n --padding-start: 12px;\n --padding-end: 12px;\n font-size: 13px;\n}\n.panel-actions ion-button ng-icon {\n margin-right: 4px;\n}\n"] }] 14830 }], null, { title: [{ 14831 type: Input 14832 }], applyText: [{ 14833 type: Input 14834 }], applyDisabled: [{ 14835 type: Input 14836 }], showApplyIcon: [{ 14837 type: Input 14838 }], closeRequested: [{ 14839 type: Output 14840 }], cancelRequested: [{ 14841 type: Output 14842 }], applyRequested: [{ 14843 type: Output 14844 }] }); 14845})(); 14846(() => { 14847 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(EntityPanelComponent, { className: "EntityPanelComponent", filePath: "src/app/shared/components/entity-panel/entity-panel.component.ts", lineNumber: 50 }); 14848})(); 14849
14850// src/app/shared/components/entity-section/entity-section.component.ts 14851var _c06 = [[["", "section-icon", ""]], "*"]; 14852var _c13 = ["[section-icon]", "*"]; 14853var _EntitySectionComponent = class _EntitySectionComponent { 14854 constructor() { 14855 this.label = ""; 14856 } 14857}; 14858_EntitySectionComponent.\u0275fac = function EntitySectionComponent_Factory(__ngFactoryType__) { 14859 return new (__ngFactoryType__ || _EntitySectionComponent)(); 14860}; 14861_EntitySectionComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _EntitySectionComponent, selectors: [["app-entity-section"]], inputs: { label: "label" }, ngContentSelectors: _c13, decls: 6, vars: 1, consts: [[1, "panel-section"], [1, "section-header"]], template: function EntitySectionComponent_Template(rf, ctx) { 14862 if (rf & 1) { 14863 \u0275\u0275projectionDef(_c06); 14864 \u0275\u0275elementStart(0, "div", 0)(1, "div", 1); 14865 \u0275\u0275projection(2); 14866 \u0275\u0275elementStart(3, "span"); 14867 \u0275\u0275text(4); 14868 \u0275\u0275elementEnd()(); 14869 \u0275\u0275projection(5, 1); 14870 \u0275\u0275elementEnd(); 14871 } 14872 if (rf & 2) { 14873 \u0275\u0275advance(4); 14874 \u0275\u0275textInterpolate(ctx.label); 14875 } 14876}, dependencies: [CommonModule], styles: ["\n\n.panel-section[_ngcontent-%COMP%] {\n padding: 12px 16px;\n border-bottom: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n}\n.panel-section[_ngcontent-%COMP%]:last-of-type {\n border-bottom: none;\n}\n.section-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-bottom: 10px;\n font-size: 13px;\n font-weight: 500;\n color: var(--ion-color-dark);\n}\n.section-header[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n color: var(--ion-color-primary);\n}"], changeDetection: 0 }); 14877var EntitySectionComponent = _EntitySectionComponent; 14878(() => { 14879 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(EntitySectionComponent, [{ 14880 type: Component, 14881 args: [{ selector: "app-entity-section", standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: '<div class="panel-section">\n <div class="section-header">\n <ng-content select="[section-icon]"></ng-content>\n <span>{{ label }}</span>\n </div>\n <ng-content></ng-content>\n</div>\n', styles: ["/* src/app/shared/components/entity-section/entity-section.component.scss */\n.panel-section {\n padding: 12px 16px;\n border-bottom: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n}\n.panel-section:last-of-type {\n border-bottom: none;\n}\n.section-header {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-bottom: 10px;\n font-size: 13px;\n font-weight: 500;\n color: var(--ion-color-dark);\n}\n.section-header ng-icon {\n color: var(--ion-color-primary);\n}\n"] }] 14882 }], null, { label: [{ 14883 type: Input 14884 }] }); 14885})(); 14886(() => { 14887 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(EntitySectionComponent, { className: "EntitySectionComponent", filePath: "src/app/shared/components/entity-section/entity-section.component.ts", lineNumber: 29 }); 14888})(); 14889
14890// src/app/shared/components/entity-row/entity-row.component.ts 14891function EntityRowComponent_Conditional_0_Template(rf, ctx) { 14892 if (rf & 1) { 14893 \u0275\u0275elementStart(0, "div", 0)(1, "span", 1); 14894 \u0275\u0275text(2); 14895 \u0275\u0275elementEnd(); 14896 \u0275\u0275elementStart(3, "span", 2); 14897 \u0275\u0275text(4); 14898 \u0275\u0275elementEnd()(); 14899 } 14900 if (rf & 2) { 14901 const ctx_r0 = \u0275\u0275nextContext(); 14902 \u0275\u0275advance(2); 14903 \u0275\u0275textInterpolate1("", ctx_r0.label, ":"); 14904 \u0275\u0275advance(); 14905 \u0275\u0275classProp("mono", ctx_r0.mono); 14906 \u0275\u0275advance(); 14907 \u0275\u0275textInterpolate(ctx_r0.displayValue); 14908 } 14909} 14910var _EntityRowComponent = class _EntityRowComponent { 14911 constructor() { 14912 this.label = ""; 14913 this.value = ""; 14914 this.truncate = 0; 14915 this.mono = false; 14916 } 14917 get displayValue() { 14918 var _a3; 14919 const val = (_a3 = this.value) != null ? _a3 : ""; 14920 if (this.truncate > 0 && val.length > this.truncate) { 14921 return val.substring(0, this.truncate) + "..."; 14922 } 14923 return val; 14924 } 14925}; 14926_EntityRowComponent.\u0275fac = function EntityRowComponent_Factory(__ngFactoryType__) { 14927 return new (__ngFactoryType__ || _EntityRowComponent)(); 14928}; 14929_EntityRowComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _EntityRowComponent, selectors: [["app-entity-row"]], inputs: { label: "label", value: "value", truncate: "truncate", mono: "mono" }, decls: 1, vars: 1, consts: [[1, "info-row"], [1, "info-label"], [1, "info-value"]], template: function EntityRowComponent_Template(rf, ctx) { 14930 if (rf & 1) { 14931 \u0275\u0275template(0, EntityRowComponent_Conditional_0_Template, 5, 4, "div", 0); 14932 } 14933 if (rf & 2) { 14934 \u0275\u0275conditional(ctx.value ? 0 : -1); 14935 } 14936}, dependencies: [CommonModule], styles: ["\n\n.info-row[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n}\n.info-row[_ngcontent-%COMP%] + .info-row[_ngcontent-%COMP%] {\n margin-top: 4px;\n}\n.info-label[_ngcontent-%COMP%] {\n color: var(--ion-color-medium);\n min-width: 60px;\n}\n.info-value[_ngcontent-%COMP%] {\n color: var(--ion-text-color);\n font-weight: 500;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.info-value.mono[_ngcontent-%COMP%] {\n font-family: monospace;\n font-size: 11px;\n background: rgba(0, 0, 0, 0.05);\n padding: 2px 6px;\n border-radius: 3px;\n}"], changeDetection: 0 }); 14937var EntityRowComponent = _EntityRowComponent; 14938(() => { 14939 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(EntityRowComponent, [{ 14940 type: Component, 14941 args: [{ selector: "app-entity-row", standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: '@if (value) {\n <div class="info-row">\n <span class="info-label">{{ label }}:</span>\n <span class="info-value" [class.mono]="mono">{{ displayValue }}</span>\n </div>\n}\n', styles: ["/* src/app/shared/components/entity-row/entity-row.component.scss */\n.info-row {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n}\n.info-row + .info-row {\n margin-top: 4px;\n}\n.info-label {\n color: var(--ion-color-medium);\n min-width: 60px;\n}\n.info-value {\n color: var(--ion-text-color);\n font-weight: 500;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.info-value.mono {\n font-family: monospace;\n font-size: 11px;\n background: rgba(0, 0, 0, 0.05);\n padding: 2px 6px;\n border-radius: 3px;\n}\n"] }] 14942 }], null, { label: [{ 14943 type: Input 14944 }], value: [{ 14945 type: Input 14946 }], truncate: [{ 14947 type: Input 14948 }], mono: [{ 14949 type: Input 14950 }] }); 14951})(); 14952(() => { 14953 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(EntityRowComponent, { className: "EntityRowComponent", filePath: "src/app/shared/components/entity-row/entity-row.component.ts", lineNumber: 26 }); 14954})(); 14955
14956// src/app/pages/campus-map-v3/components/gps-debug-modal/gps-debug-modal.component.ts 14957var _forTrack03 = ($index, $item) => $item.id; 14958function GpsDebugModalComponent_Conditional_8_Template(rf, ctx) { 14959 if (rf & 1) { 14960 \u0275\u0275elementStart(0, "app-entity-section", 7)(1, "p", 16); 14961 \u0275\u0275text(2, "2D Affine transformation: x = a\xB7lng + b\xB7lat + tx, y = c\xB7lng + d\xB7lat + ty"); 14962 \u0275\u0275elementEnd(); 14963 \u0275\u0275elementStart(3, "div", 17)(4, "div", 18)(5, "label"); 14964 \u0275\u0275text(6, "a (lng\u2192x):"); 14965 \u0275\u0275elementEnd(); 14966 \u0275\u0275elementStart(7, "span"); 14967 \u0275\u0275text(8); 14968 \u0275\u0275elementEnd()(); 14969 \u0275\u0275elementStart(9, "div", 18)(10, "label"); 14970 \u0275\u0275text(11, "b (lat\u2192x):"); 14971 \u0275\u0275elementEnd(); 14972 \u0275\u0275elementStart(12, "span"); 14973 \u0275\u0275text(13); 14974 \u0275\u0275elementEnd()(); 14975 \u0275\u0275elementStart(14, "div", 18)(15, "label"); 14976 \u0275\u0275text(16, "tx (x offset):"); 14977 \u0275\u0275elementEnd(); 14978 \u0275\u0275elementStart(17, "span"); 14979 \u0275\u0275text(18); 14980 \u0275\u0275elementEnd()(); 14981 \u0275\u0275elementStart(19, "div", 18)(20, "label"); 14982 \u0275\u0275text(21, "c (lng\u2192y):"); 14983 \u0275\u0275elementEnd(); 14984 \u0275\u0275elementStart(22, "span"); 14985 \u0275\u0275text(23); 14986 \u0275\u0275elementEnd()(); 14987 \u0275\u0275elementStart(24, "div", 18)(25, "label"); 14988 \u0275\u0275text(26, "d (lat\u2192y):"); 14989 \u0275\u0275elementEnd(); 14990 \u0275\u0275elementStart(27, "span"); 14991 \u0275\u0275text(28); 14992 \u0275\u0275elementEnd()(); 14993 \u0275\u0275elementStart(29, "div", 18)(30, "label"); 14994 \u0275\u0275text(31, "ty (y offset):"); 14995 \u0275\u0275elementEnd(); 14996 \u0275\u0275elementStart(32, "span"); 14997 \u0275\u0275text(33); 14998 \u0275\u0275elementEnd()()()(); 14999 } 15000 if (rf & 2) { 15001 const ctx_r0 = \u0275\u0275nextContext(); 15002 \u0275\u0275advance(8); 15003 \u0275\u0275textInterpolate(ctx_r0.formatNumber(ctx_r0.transformMatrix().a, 10)); 15004 \u0275\u0275advance(5); 15005 \u0275\u0275textInterpolate(ctx_r0.formatNumber(ctx_r0.transformMatrix().b, 10)); 15006 \u0275\u0275advance(5); 15007 \u0275\u0275textInterpolate(ctx_r0.formatNumber(ctx_r0.transformMatrix().tx, 2)); 15008 \u0275\u0275advance(5); 15009 \u0275\u0275textInterpolate(ctx_r0.formatNumber(ctx_r0.transformMatrix().c, 10)); 15010 \u0275\u0275advance(5); 15011 \u0275\u0275textInterpolate(ctx_r0.formatNumber(ctx_r0.transformMatrix().d, 10)); 15012 \u0275\u0275advance(5); 15013 \u0275\u0275textInterpolate(ctx_r0.formatNumber(ctx_r0.transformMatrix().ty, 2)); 15014 } 15015} 15016function GpsDebugModalComponent_Conditional_9_Conditional_12_Template(rf, ctx) { 15017 if (rf & 1) { 15018 \u0275\u0275elementStart(0, "div", 20)(1, "label"); 15019 \u0275\u0275text(2, "Points Filtered (Outliers):"); 15020 \u0275\u0275elementEnd(); 15021 \u0275\u0275elementStart(3, "span", 22); 15022 \u0275\u0275text(4); 15023 \u0275\u0275elementEnd()(); 15024 } 15025 if (rf & 2) { 15026 const ctx_r0 = \u0275\u0275nextContext(2); 15027 \u0275\u0275advance(4); 15028 \u0275\u0275textInterpolate(ctx_r0.transformQuality().outlierCount); 15029 } 15030} 15031function GpsDebugModalComponent_Conditional_9_Template(rf, ctx) { 15032 if (rf & 1) { 15033 \u0275\u0275elementStart(0, "app-entity-section", 8)(1, "div", 19)(2, "div", 20)(3, "label"); 15034 \u0275\u0275text(4, "Total Reference Points:"); 15035 \u0275\u0275elementEnd(); 15036 \u0275\u0275elementStart(5, "span"); 15037 \u0275\u0275text(6); 15038 \u0275\u0275elementEnd()(); 15039 \u0275\u0275elementStart(7, "div", 20)(8, "label"); 15040 \u0275\u0275text(9, "Points Used (Inliers):"); 15041 \u0275\u0275elementEnd(); 15042 \u0275\u0275elementStart(10, "span", 21); 15043 \u0275\u0275text(11); 15044 \u0275\u0275elementEnd()(); 15045 \u0275\u0275template(12, GpsDebugModalComponent_Conditional_9_Conditional_12_Template, 5, 1, "div", 20); 15046 \u0275\u0275elementStart(13, "div", 20)(14, "label"); 15047 \u0275\u0275text(15, "Max Error (Inliers):"); 15048 \u0275\u0275elementEnd(); 15049 \u0275\u0275elementStart(16, "span"); 15050 \u0275\u0275text(17); 15051 \u0275\u0275elementEnd()(); 15052 \u0275\u0275elementStart(18, "div", 20)(19, "label"); 15053 \u0275\u0275text(20, "Mean Squared Error:"); 15054 \u0275\u0275elementEnd(); 15055 \u0275\u0275elementStart(21, "span"); 15056 \u0275\u0275text(22); 15057 \u0275\u0275elementEnd()(); 15058 \u0275\u0275elementStart(23, "div", 20)(24, "label"); 15059 \u0275\u0275text(25, "Rotation Angle:"); 15060 \u0275\u0275elementEnd(); 15061 \u0275\u0275elementStart(26, "span"); 15062 \u0275\u0275text(27); 15063 \u0275\u0275elementEnd()(); 15064 \u0275\u0275elementStart(28, "div", 20)(29, "label"); 15065 \u0275\u0275text(30, "Scale X:"); 15066 \u0275\u0275elementEnd(); 15067 \u0275\u0275elementStart(31, "span"); 15068 \u0275\u0275text(32); 15069 \u0275\u0275elementEnd()(); 15070 \u0275\u0275elementStart(33, "div", 20)(34, "label"); 15071 \u0275\u0275text(35, "Scale Y:"); 15072 \u0275\u0275elementEnd(); 15073 \u0275\u0275elementStart(36, "span"); 15074 \u0275\u0275text(37); 15075 \u0275\u0275elementEnd()(); 15076 \u0275\u0275elementStart(38, "div", 20)(39, "label"); 15077 \u0275\u0275text(40, "Uniform Scale:"); 15078 \u0275\u0275elementEnd(); 15079 \u0275\u0275elementStart(41, "span"); 15080 \u0275\u0275text(42); 15081 \u0275\u0275elementEnd()()()(); 15082 } 15083 if (rf & 2) { 15084 const ctx_r0 = \u0275\u0275nextContext(); 15085 \u0275\u0275advance(6); 15086 \u0275\u0275textInterpolate(ctx_r0.transformQuality().referencePointCount); 15087 \u0275\u0275advance(5); 15088 \u0275\u0275textInterpolate(ctx_r0.transformQuality().inlierCount); 15089 \u0275\u0275advance(); 15090 \u0275\u0275conditional(ctx_r0.transformQuality().outlierCount > 0 ? 12 : -1); 15091 \u0275\u0275advance(5); 15092 \u0275\u0275textInterpolate2("", ctx_r0.formatNumber(ctx_r0.transformQuality().maxError, 1), " cm (", ctx_r0.formatNumber(ctx_r0.transformQuality().maxError / 100, 2), " m)"); 15093 \u0275\u0275advance(5); 15094 \u0275\u0275textInterpolate1("", ctx_r0.formatNumber(ctx_r0.transformQuality().meanSquaredError, 1), " cm\xB2"); 15095 \u0275\u0275advance(5); 15096 \u0275\u0275textInterpolate1("", ctx_r0.formatNumber(ctx_r0.transformQuality().rotationAngle, 2), "\xB0"); 15097 \u0275\u0275advance(5); 15098 \u0275\u0275textInterpolate(ctx_r0.formatNumber(ctx_r0.transformQuality().scaleX, 6)); 15099 \u0275\u0275advance(5); 15100 \u0275\u0275textInterpolate(ctx_r0.formatNumber(ctx_r0.transformQuality().scaleY, 6)); 15101 \u0275\u0275advance(5); 15102 \u0275\u0275textInterpolate(ctx_r0.transformQuality().uniformScale ? "Yes" : "No"); 15103 } 15104} 15105function GpsDebugModalComponent_Conditional_10_Template(rf, ctx) { 15106 if (rf & 1) { 15107 \u0275\u0275elementStart(0, "app-entity-section", 9)(1, "ol", 23)(2, "li"); 15108 \u0275\u0275text(3, "Select an outdoor node on the map"); 15109 \u0275\u0275elementEnd(); 15110 \u0275\u0275elementStart(4, "li"); 15111 \u0275\u0275text(5, "Click the GPS pin icon to assign GPS co
15111ordinates"); 15112 \u0275\u0275elementEnd(); 15113 \u0275\u0275elementStart(6, "li"); 15114 \u0275\u0275text(7, "Repeat for at least 2 outdoor nodes (3+ recommended for accuracy)"); 15115 \u0275\u0275elementEnd(); 15116 \u0275\u0275elementStart(8, "li"); 15117 \u0275\u0275text(9, "Nodes should be well-distributed across your outdoor area"); 15118 \u0275\u0275elementEnd()()(); 15119 } 15120} 15121function GpsDebugModalComponent_Conditional_11_Conditional_2_Template(rf, ctx) { 15122 if (rf & 1) { 15123 \u0275\u0275elementStart(0, "li"); 15124 \u0275\u0275text(1); 15125 \u0275\u0275elementEnd(); 15126 } 15127 if (rf & 2) { 15128 const ctx_r0 = \u0275\u0275nextContext(2); 15129 \u0275\u0275advance(); 15130 \u0275\u0275textInterpolate1("Only ", ctx_r0.transformQuality().referencePointCount, " reference points - add more for better accuracy"); 15131 } 15132} 15133function GpsDebugModalComponent_Conditional_11_Conditional_3_Template(rf, ctx) { 15134 if (rf & 1) { 15135 \u0275\u0275elementStart(0, "li"); 15136 \u0275\u0275text(1); 15137 \u0275\u0275elementEnd(); 15138 } 15139 if (rf & 2) { 15140 const ctx_r0 = \u0275\u0275nextContext(2); 15141 \u0275\u0275advance(); 15142 \u0275\u0275textInterpolate1("High maximum error (", ctx_r0.formatNumber(ctx_r0.transformQuality().maxError / 100, 2), "m) - verify node GPS coordinates"); 15143 } 15144} 15145function GpsDebugModalComponent_Conditional_11_Conditional_4_Template(rf, ctx) { 15146 if (rf & 1) { 15147 \u0275\u0275elementStart(0, "li"); 15148 \u0275\u0275text(1, "Non-uniform scale detected - map may be distorted"); 15149 \u0275\u0275elementEnd(); 15150 } 15151} 15152function GpsDebugModalComponent_Conditional_11_Template(rf, ctx) { 15153 if (rf & 1) { 15154 \u0275\u0275elementStart(0, "app-entity-section", 10)(1, "ul", 24); 15155 \u0275\u0275template(2, GpsDebugModalComponent_Conditional_11_Conditional_2_Template, 2, 1, "li")(3, GpsDebugModalComponent_Conditional_11_Conditional_3_Template, 2, 1, "li")(4, GpsDebugModalComponent_Conditional_11_Conditional_4_Template, 2, 0, "li"); 15156 \u0275\u0275elementEnd()(); 15157 } 15158 if (rf & 2) { 15159 const ctx_r0 = \u0275\u0275nextContext(); 15160 \u0275\u0275advance(2); 15161 \u0275\u0275conditional(ctx_r0.transformQuality().referencePointCount < 3 ? 2 : -1); 15162 \u0275\u0275advance(); 15163 \u0275\u0275conditional(ctx_r0.transformQuality().maxError > 500 ? 3 : -1); 15164 \u0275\u0275advance(); 15165 \u0275\u0275conditional(!ctx_r0.transformQuality().uniformScale ? 4 : -1); 15166 } 15167} 15168function GpsDebugModalComponent_Conditional_13_Template(rf, ctx) { 15169 if (rf & 1) { 15170 \u0275\u0275elementStart(0, "p", 12); 15171 \u0275\u0275text(1, "No nodes with GPS coordinates found."); 15172 \u0275\u0275elementEnd(); 15173 \u0275\u0275elementStart(2, "p", 16); 15174 \u0275\u0275text(3, "To create a GPS transform, assign GPS coordinates to at least 2 outdoor nodes."); 15175 \u0275\u0275elementEnd(); 15176 } 15177} 15178function GpsDebugModalComponent_Conditional_14_Template(rf, ctx) { 15179 if (rf & 1) { 15180 \u0275\u0275elementStart(0, "p", 12); 15181 \u0275\u0275text(1, "All reference points were filtered as outliers."); 15182 \u0275\u0275elementEnd(); 15183 } 15184} 15185function GpsDebugModalComponent_Conditional_15_For_2_Conditional_21_Template(rf, ctx) { 15186 if (rf & 1) { 15187 \u0275\u0275elementStart(0, "div", 33)(1, "label"); 15188 \u0275\u0275text(2, "Error:"); 15189 \u0275\u0275elementEnd(); 15190 \u0275\u0275elementStart(3, "span", 21); 15191 \u0275\u0275text(4); 15192 \u0275\u0275elementEnd()(); 15193 } 15194 if (rf & 2) { 15195 const node_r3 = \u0275\u0275nextContext().$implicit; 15196 const ctx_r0 = \u0275\u0275nextContext(2); 15197 \u0275\u0275advance(4); 15198 \u0275\u0275textInterpolate(ctx_r0.formatError(node_r3.error)); 15199 } 15200} 15201function GpsDebugModalComponent_Conditional_15_For_2_Template(rf, ctx) { 15202 if (rf & 1) { 15203 const _r2 = \u0275\u0275getCurrentView(); 15204 \u0275\u0275elementStart(0, "div", 25)(1, "div", 26)(2, "div", 27); 15205 \u0275\u0275element(3, "ng-icon", 28); 15206 \u0275\u0275elementStart(4, "strong"); 15207 \u0275\u0275text(5); 15208 \u0275\u0275elementEnd(); 15209 \u0275\u0275elementStart(6, "span", 29); 15210 \u0275\u0275text(7); 15211 \u0275\u0275elementEnd()(); 15212 \u0275\u0275elementStart(8, "ion-button", 30); 15213 \u0275\u0275listener("click", function GpsDebugModalComponent_Conditional_15_For_2_Template_ion_button_click_8_listener() { 15214 const node_r3 = \u0275\u0275restoreView(_r2).$implicit; 15215 const ctx_r0 = \u0275\u0275nextContext(2);
15216 return \u0275\u0275resetView(ctx_r0.locateNode(node_r3.id)); 15217 }); 15218 \u0275\u0275element(9, "ng-icon", 31); 15219 \u0275\u0275elementEnd()(); 15220 \u0275\u0275elementStart(10, "div", 32)(11, "div", 33)(12, "label"); 15221 \u0275\u0275text(13, "GPS:"); 15222 \u0275\u0275elementEnd(); 15223 \u0275\u0275elementStart(14, "span"); 15224 \u0275\u0275text(15); 15225 \u0275\u0275elementEnd()(); 15226 \u0275\u0275elementStart(16, "div", 33)(17, "label"); 15227 \u0275\u0275text(18, "Map:"); 15228 \u0275\u0275elementEnd(); 15229 \u0275\u0275elementStart(19, "span"); 15230 \u0275\u0275text(20); 15231 \u0275\u0275elementEnd()(); 15232 \u0275\u0275template(21, GpsDebugModalComponent_Conditional_15_For_2_Conditional_21_Template, 5, 1, "div", 33); 15233 \u0275\u0275elementEnd()(); 15234 } 15235 if (rf & 2) { 15236 const node_r3 = ctx.$implicit; 15237 const ctx_r0 = \u0275\u0275nextContext(2); 15238 \u0275\u0275advance(5); 15239 \u0275\u0275textInterpolate(node_r3.type); 15240 \u0275\u0275advance(2); 15241 \u0275\u0275textInterpolate1("ID: ", node_r3.id, ""); 15242 \u0275\u0275advance(8); 15243 \u0275\u0275textInterpolate2("", ctx_r0.formatCoordinate(node_r3.latitude), ", ", ctx_r0.formatCoordinate(node_r3.longitude), ""); 15244 \u0275\u0275advance(5); 15245 \u0275\u0275textInterpolate2("", ctx_r0.formatMapCoord(node_r3.mapX), ", ", ctx_r0.formatMapCoord(node_r3.mapY), ""); 15246 \u0275\u0275advance(); 15247 \u0275\u0275conditional(node_r3.error !== void 0 ? 21 : -1); 15248 } 15249} 15250function GpsDebugModalComponent_Conditional_15_Template(rf, ctx) { 15251 if (rf & 1) { 15252 \u0275\u0275elementStart(0, "div", 13); 15253 \u0275\u0275repeaterCreate(1, GpsDebugModalComponent_Conditional_15_For_2_Template, 22, 7, "div", 25, _forTrack03); 15254 \u0275\u0275elementEnd(); 15255 } 15256 if (rf & 2) { 15257 const ctx_r0 = \u0275\u0275nextContext(); 15258 \u0275\u0275advance(); 15259 \u0275\u0275repeater(ctx_r0.usedNodes()); 15260 } 15261} 15262function GpsDebugModalComponent_Conditional_16_For_5_Conditional_21_Template(rf, ctx) { 15263 if (rf & 1) { 15264 \u0275\u0275elementStart(0, "div", 33)(1, "label"); 15265 \u0275\u0275text(2, "Error:"); 15266 \u0275\u0275elementEnd(); 15267 \u0275\u0275elementStart(3, "span", 22); 15268 \u0275\u0275text(4); 15269 \u0275\u0275elementEnd()(); 15270 } 15271 if (rf & 2) { 15272 const node_r5 = \u0275\u0275nextContext().$implicit; 15273 const ctx_r0 = \u0275\u0275nextContext(2); 15274 \u0275\u0275advance(4); 15275 \u0275\u0275textInterpolate(ctx_r0.formatError(node_r5.error)); 15276 } 15277} 15278function GpsDebugModalComponent_Conditional_16_For_5_Template(rf, ctx) { 15279 if (rf & 1) { 15280 const _r4 = \u0275\u0275getCurrentView(); 15281 \u0275\u0275elementStart(0, "div", 34)(1, "div", 26)(2, "div", 27); 15282 \u0275\u0275element(3, "ng-icon", 35); 15283 \u0275\u0275elementStart(4, "strong"); 15284 \u0275\u0275text(5); 15285 \u0275\u0275elementEnd(); 15286 \u0275\u0275elementStart(6, "span", 29); 15287 \u0275\u0275text(7); 15288 \u0275\u0275elementEnd()(); 15289 \u0275\u0275elementStart(8, "ion-button", 30); 15290 \u0275\u0275listener("click", function GpsDebugModalComponent_Conditional_16_For_5_Template_ion_button_click_8_listener() { 15291 const node_r5 = \u0275\u0275restoreView(_r4).$implicit; 15292 const ctx_r0 = \u0275\u0275nextContext(2); 15293 return \u0275\u0275resetView(ctx_r0.locateNode(node_r5.id)); 15294 }); 15295 \u0275\u0275element(9, "ng-icon", 31); 15296 \u0275\u0275elementEnd()(); 15297 \u0275\u0275elementStart(10, "div", 32)(11, "div", 33)(12, "label"); 15298 \u0275\u0275text(13, "GPS:"); 15299 \u0275\u0275elementEnd(); 15300 \u0275\u0275elementStart(14, "span"); 15301 \u0275\u0275text(15); 15302 \u0275\u0275elementEnd()(); 15303 \u0275\u0275elementStart(16, "div", 33)(17, "label"); 15304 \u0275\u0275text(18, "Map:"); 15305 \u0275\u0275elementEnd(); 15306 \u0275\u0275elementStart(19, "span"); 15307 \u0275\u0275text(20); 15308 \u0275\u0275elementEnd()(); 15309 \u0275\u0275template(21, GpsDebugModalComponent_Conditional_16_For_5_Conditional_21_Template, 5, 1, "div", 33); 15310 \u0275\u0275elementEnd()(); 15311 } 15312 if (rf & 2) { 15313 const node_r5 = ctx.$implicit; 15314 const ctx_r0 = \u0275\u0275nextContext(2); 15315 \u0275\u0275advance(5); 15316 \u0275\u0275textInterpolate(node_r5.type); 15317 \u0275\u0275advance(2); 15318 \u0275\u0275textInterpolate1("ID: ", node_r5.id, ""); 15319 \u0275\u0275advance(8); 15320 \u0275\u0275textInterpolate2("", ctx_r0.formatCoordinate(node_r5.latitude), ", ", ctx_r0.formatCoordinate(node_r5.longitude), ""); 15321 \u0275\u0275advance(5); 15322 \u0275\u0275textInterpolate2("", ctx_r0.formatMapCoord(node_r5.mapX), ", ", ctx_r0.formatMapCoord(node_r5.mapY), ""); 15323 \u0275\u0275advance(); 15324 \u0275\u0275conditional(node_r5.error !== void 0 ? 21 : -1); 15325 } 15326} 15327function GpsDebugModalComponent_Conditional_16_Template(rf, ctx) { 15328 if (rf & 1) { 15329 \u0275\u0275elementStart(0, "app-entity-section", 11)(1, "p", 16); 15330 \u0275\u0275text(2, "These nodes were automatically excluded due to high transformation error."); 15331 \u0275\u0275elementEnd(); 15332 \u0275\u0275elementStart(3, "div", 13); 15333 \u0275\u0275repeaterCreate(4, GpsDebugModalComponent_Conditional_16_For_5_Template, 22, 7, "div", 34, _forTrack03); 15334 \u0275\u0275elementEnd()(); 15335 } 15336 if (rf & 2) { 15337 const ctx_r0 = \u0275\u0275nextContext(); 15338 \u0275\u0275propertyInterpolate1("label", "Filtered Outliers (", ctx_r0.outlierNodes().length, ")"); 15339 \u0275\u0275advance(4); 15340 \u0275\u0275repeater(ctx_r0.outlierNodes()); 15341 } 15342} 15343function GpsDebugModalComponent_Conditional_19_Conditional_1_Template(rf, ctx) { 15344 if (rf & 1) { 15345 \u0275\u0275element(0, "app-entity-row", 37); 15346 } 15347 if (rf & 2) { 15348 const ctx_r0 = \u0275\u0275nextContext(2); 15349 \u0275\u0275property("value", ctx_r0.transformQuality().outlierCount.toString()); 15350 } 15351} 15352function GpsDebugModalComponent_Conditional_19_Template(rf, ctx) { 15353 if (rf & 1) { 15354 \u0275\u0275element(0, "app-entity-row", 36); 15355 \u0275\u0275template(1, GpsDebugModalComponent_Conditional_19_Conditional_1_Template, 1, 1, "app-entity-row", 37); 15356 } 15357 if (rf & 2) { 15358 const ctx_r0 = \u0275\u0275nextContext(); 15359 \u0275\u0275property("value", ctx_r0.transformQuality().inlierCount.toString()); 15360 \u0275\u0275advance(); 15361 \u0275\u0275conditional(ctx_r0.transformQuality().outlierCount > 0 ? 1 : -1); 15362 } 15363} 15364var _GpsDebugModalComponent = class _GpsDebugModalComponent { 15365 constructor() { 15366 this.database = inject(DatabaseService); 15367 this.gpsTransformManager = inject(GPSTransformManagerService); 15368 this.alertController = inject(AlertController); 15369 this.closeRequested = new EventEmitter(); 15370 this.gpsNodeLocate = new EventEmitter(); 15371 this.gpsNodes = signal([]); 15372 this.transformQuality = signal(null); 15373 this.transformMatrix = signal(null); 15374 this.hasTransform = computed(() => this.transformMatrix() !== null); 15375 this.hasOutliers = computed(() => this.outlierNodes().length > 0); 15376 this.usedNodes = computed(() => this.gpsNodes().filter((n) => !n.isOutlier)); 15377 this.outlierNodes = computed(() => this.gpsNodes().filter((n) => n.isOutlier)); 15378 this.loadGPSDebugInfo(); 15379 } 15380 loadGPSDebugInfo() { 15381 const transform = this.gpsTransformManager.gpsTransform(); 15382 const quality = this.gpsTransformManager.transformQuality(); 15383 const referencePoints = this.gpsTransformManager.referencePoints(); 15384 this.transformQuality.set(quality); 15385 if (transform) { 15386 this.transformMatrix.set({ 15387 a: transform.a, 15388 b: transform.b, 15389 c: transform.c, 15390 d: transform.d, 15391 tx: transform.tx, 15392 ty: transform.ty 15393 }); 15394 const outlierIds = new Set((quality == null ? void 0 : quality.outlierNodeIds) || []); 15395 const campus = this.gpsTransformManager.getCampusDataService().getCampus(); 15396 if (!campus) { 15397 this.gpsNodes.set([]); 15398 return; 15399 } 15400 const nodesWithGps = campus.nodes.filter((node) => node.entity.latitude != null && node.entity.longitude != null && node.entity.locationX != null && node.entity.locationY != null).map((node) => { 15401 const nodeId = node.entity.id || "unknown"; 15402 const isOutlier = outlierIds.has(nodeId); 15403 let error; 15404 if (transform) { 15405 const predictedX = transform.a * node.entity.longitude + transform.b * node.entity.latitude + transform.tx; 15406 const predictedY = transform.c * node.entity.longitude + transform.d * node.entity.latitude + transform.ty; 15407 error = Math.sqrt(Math.pow(predictedX - node.entity.locationX, 2) + Math.pow(predictedY - node.entity.locationY, 2)); 15408 } 15409 return { 15410 id: nodeId, 15411 type: node.entity.type || "Outdoor", 15412 latitude: node.entity.latitude, 15413 longitude: node.entity.longitude, 15414 mapX: node.entity.locationX, 15415 mapY: node.entity.locationY, 15416 isOutlier, 15417 error 15418 }; 15419 }).sort((a, b) => { 15420 if (a.isOutlier !== b.isOutlier) { 15421 return a.isOutlier ? 1 : -1; 15422 } 15423 return (a.error || 0) - (b.error || 0); 15424 }); 15425 this.gpsNodes.set(nodesWithGps); 15426 } else { 15427 this.transformMatrix.set(null); 15428 this.gpsNodes.set([]); 15429 } 15430 } 15431 dismiss() { 15432 this.closeRequested.emit(); 15433 } 15434 locateNode(nodeId) { 15435 this.gpsNodeLocate.emit(nodeId); 15436 } 15437 get statusIcon() { 15438 var _a3; 15439 if (!this.hasTransform() || this.gpsNodes().length < 2) { 15440 return "lucideCircleX"; 15441 } 15442 if ((_a3 = this.transformQuality()) == null ? void 0 : _a3.isReliable) { 15443 return "lucideCircleCheck"; 15444 } 15445 return "lucideCircleAlert"; 15446 } 15447 get statusText() { 15448 var _a3; 15449 if (!this.hasTransform() || this.gpsNodes().length < 2) { 15450 return "Invalid - Not enough reference points"; 15451 } 15452 if ((_a3 = this.transformQuality()) == null ? void 0 : _a3.isReliable) { 15453 return "Valid - Transform is reliable"; 15454 } 15455 return "Warning - Transform may be unreliable"; 15456 } 15457 get statusClass() { 15458 var _a3; 15459 if (!this.hasTransform() || this.gpsNodes().length < 2) { 15460 return "status-error";
15461 } 15462 if ((_a3 = this.transformQuality()) == null ? void 0 : _a3.isReliable) { 15463 return "status-success"; 15464 } 15465 return "status-warning"; 15466 } 15467 formatNumber(value, decimals = 6) { 15468 if (value === void 0 || value === null) 15469 return "---"; 15470 return value.toFixed(decimals); 15471 } 15472 formatCoordinate(value) { 15473 return value.toFixed(6) + "\xB0"; 15474 } 15475 formatMapCoord(value) { 15476 return value.toFixed(0) + " cm"; 15477 } 15478 formatError(error) { 15479 if (error === void 0) 15480 return "---"; 15481 if (error < 100) { 15482 return error.toFixed(1) + " cm"; 15483 } else { 15484 return (error / 100).toFixed(2) + " m"; 15485 } 15486 } 15487 clearAllGpsCoordinates() { 15488 return __async(this, null, function* () { 15489 const nodesWithGps = this.gpsNodes().filter((n) => !n.isOutlier); 15490 if (nodesWithGps.length === 0) { 15491 return; 15492 } 15493 const alert = yield this.alertController.create({ 15494 header: "Clear All GPS Coordinates?", 15495 message: `This will remove GPS coordinates from all ${nodesWithGps.length} reference node${nodesWithGps.length > 1 ? "s" : ""}. This action cannot be undone. Are you sure?`, 15496 buttons: [ 15497 { text: "Cancel", role: "cancel" }, 15498 { text: "Clear All", role: "destructive", cssClass: "danger" } 15499 ] 15500 }); 15501 yield alert.present(); 15502 const { role } = yield alert.onDidDismiss(); 15503 if (role === "destructive") { 15504 for (const nodeInfo of nodesWithGps) { 15505 const model = this.database.Models.get(nodeInfo.id); 15506 if (model && model.type === EntityType.NODE) { 15507 const node = model; 15508 node.entity.latitude = null; 15509 node.entity.longitude = null; 15510 node.entity.altitude = null; 15511 node.entity.isChanged = true; 15512 } 15513 } 15514 this.gpsTransformManager.recalculateTransform(); 15515 this.dismiss(); 15516 } 15517 }); 15518 } 15519}; 15520_GpsDebugModalComponent.\u0275fac = function GpsDebugModalComponent_Factory(__ngFactoryType__) { 15521 return new (__ngFactoryType__ || _GpsDebugModalComponent)(); 15522}; 15523_GpsDebugModalComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _GpsDebugModalComponent, selectors: [["app-gps-debug-modal"]], outputs: { closeRequested: "closeRequested", gpsNodeLocate: "gpsNodeLocate" }, features: [\u0275\u0275ProvidersFeature([ 15524 provideIcons({ 15525 lucideX, 15526 lucideTrash2, 15527 lucideCircleCheck, 15528 lucideCircleX, 15529 lucideLocate 15530 }) 15531])], decls: 20, vars: 16, consts: [[3, "closeRequested", "applyRequested", "title", "applyText", "showApplyIcon", "applyDisabled"], ["panel-content", ""], ["label", "Transform Status"], [1, "status-card", 3, "ngClass"], [1, "status-header"], [1, "status-icon", 3, "name"], [1, "status-text"], ["label", "Transform Matrix"], ["label", "Quality Metrics"], ["label", "Setup Instructions"], ["label", "Reliability Warnings"], [3, "label"], [1, "empty-message"], [1, "nodes-list"], ["panel-info", ""], ["label", "Total Nodes", 3, "value"], [1, "hint-text"], [1, "matrix-grid"], [1, "matrix-row"], [1, "metrics-grid"], [1, "metric-row"], [1, "success-text"], [1, "error-text"], [1, "guidance-list"], [1, "guidance-list", "warning"], [1, "node-item", "node-used"], [1, "node-header"], [1, "node-info"], ["name", "lucideCircleCheck", 1, "node-status-icon", "success"], [1, "node-id"], ["fill", "clear", "size", "small", "title", "Locate on map", 3, "click"], ["name", "lucideLocate"], [1, "node-coords"], [1, "coord-row"], [1, "node-item", "node-outlier"], ["name", "lucideCircleX", 1, "node-status-icon", "error"], ["label", "Inliers", 3, "value"], ["label", "Outliers", 3, "value"]], template: function GpsDebugModalComponent_Template(rf, ctx) { 15532 if (rf & 1) { 15533 \u0275\u0275elementStart(0, "app-entity-panel", 0); 15534 \u0275\u0275listener("closeRequested", function GpsDebugModalComponent_Template_app_entity_panel_closeRequested_0_listener() { 15535 return ctx.dismiss(); 15536 })("applyRequested", function GpsDebugModalComponent_Template_app_entity_panel_applyRequested_0_listener() { 15537 return ctx.clearAllGpsCoordinates(); 15538 }); 15539 \u0275\u0275elementContainerStart(1, 1); 15540 \u0275\u0275elementStart(2, "app-entity-section", 2)(3, "div", 3)(4, "div", 4); 15541 \u0275\u0275element(5, "ng-icon", 5); 15542 \u0275\u0275elementStart(6, "span", 6); 15543 \u0275\u0275text(7); 15544 \u0275\u0275elementEnd()()()(); 15545 \u0275\u0275template(8, GpsDebugModalComponent_Conditional_8_Template, 34, 6, "app-entity-section", 7)(9, GpsDebugModalComponent_Conditional_9_Template, 43, 10, "app-entity-section", 8)(10, GpsDebugModalComponent_Conditional_10_Template, 10, 0, "app-entity-section", 9)(11, GpsDebugModalComponent_Conditional_11_Template, 5, 3, "app-entity-section", 10); 15546 \u0275\u0275elementStart(12, "app-entity-section", 11); 15547 \u0275\u0275template(13, GpsDebugModalComponent_Conditional_13_Template, 4, 0)(14, GpsDebugModalComponent_Conditional_14_Template, 2, 0, "p", 12)(15, GpsDebugModalComponent_Conditional_15_Template, 3, 0, "div", 13); 15548 \u0275\u0275elementEnd(); 15549 \u0275\u0275template(16, GpsDebugModalComponent_Conditional_16_Template, 6, 2, "app-entity-section", 11); 15550 \u0275\u0275elementContainerEnd(); 15551 \u0275\u0275elementContainerStart(17, 14); 15552 \u0275\u0275element(18, "app-entity-row", 15); 15553 \u0275\u0275template(19, GpsDebugModalComponent_Conditional_19_Template, 2, 2); 15554 \u0275\u0275elementContainerEnd(); 15555 \u0275\u0275elementEnd(); 15556 } 15557 if (rf & 2) { 15558 \u0275\u0275property("title", "GPS Transform Debug")("applyText", "Clear All GPS")("showApplyIcon", false)("applyDisabled", ctx.gpsNodes().length === 0); 15559 \u0275\u0275advance(3); 15560 \u0275\u0275property("ngClass", ctx.statusClass); 15561 \u0275\u0275advance(2); 15562 \u0275\u0275property("name", ctx.statusIcon); 15563 \u0275\u0275advance(2); 15564 \u0275\u0275textInterpolate(ctx.statusText); 15565 \u0275\u0275advance(); 15566 \u0275\u0275conditional(ctx.hasTransform() && ctx.transformMatrix() ? 8 : -1); 15567 \u0275\u0275advance(); 15568 \u0275\u0275conditional(ctx.hasTransform() && ctx.transformQuality() ? 9 : -1); 15569 \u0275\u0275advance(); 15570 \u0275\u0275conditional(!ctx.hasTransform() || ctx.gpsNodes().length < 2 ? 10 : ctx.transformQuality() && !ctx.transformQuality().isReliable ? 11 : -1); 15571 \u0275\u0275advance(2); 15572 \u0275\u0275propertyInterpolate1("label", "GPS Reference Nodes - Used (", ctx.usedNodes().length, ")"); 15573 \u0275\u0275advance(); 15574 \u0275\u0275conditional(ctx.gpsNodes().length === 0 ? 13 : ctx.usedNodes().length === 0 ? 14 : 15); 15575 \u0275\u0275advance(3); 15576 \u0275\u0275conditional(ctx.hasOutliers() ? 16 : -1); 15577 \u0275\u0275advance(2); 15578 \u0275\u0275property("value", ctx.gpsNodes().length.toString()); 15579 \u0275\u0275advance(); 15580 \u0275\u0275conditional(ctx.hasTransform() && ctx.transformQuality() ? 19 : -1); 15581 } 15582}, dependencies: [ 15583 CommonModule, 15584 NgClass, 15585 FormsModule, 15586 IonButton, 15587 NgIcon, 15588 EntityPanelComponent, 15589 EntitySectionComponent, 15590 EntityRowComponent 15591], styles: ["\n\n.gps-debug-container[_ngcontent-%COMP%] {\n background: var(--tool-palette-bg, rgba(255, 255, 255, 0.95));\n border-radius: 12px;\n box-shadow: var(--tool-palette-shadow, 0 4px 12px rgba(0, 0, 0, 0.15));\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light, rgba(0, 0, 0, 0.08));\n min-width: 380px;\n max-width: 380px;\n overflow: hidden;\n}\n.status-card[_ngcontent-%COMP%] {\n padding: 16px;\n border-radius: 8px;\n margin-bottom: 12px;\n border: 2px solid;\n}\n.status-card.status-success[_ngcontent-%COMP%] {\n background-color: rgba(16, 185, 129, 0.1);\n border-color: #10B981;\n color: #059669;\n}\n.status-card.status-warning[_ngcontent-%COMP%] {\n background-color: rgba(245, 158, 11, 0.1);\n border-color: #F59E0B;\n color: #D97706;\n}\n.status-card.status-error[_ngcontent-%COMP%] {\n background-color: rgba(239, 68, 68, 0.1);\n border-color: #EF4444;\n color: #DC2626;\n}\n.status-card[_ngcontent-%COMP%] .status-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 12px;\n font-weight: 600;\n font-size: 16px;\n}\n.status-card[_ngcontent-%COMP%] .status-header[_ngcontent-%COMP%] .status-icon[_ngcontent-%COMP%] {\n font-size: 24px;\n}\n.section[_ngcontent-%COMP%] {\n margin-bottom: 12px;\n}\n.section[_ngcontent-%COMP%] h3[_ngcontent-%COMP%] {\n font-size: 18px;\n font-weight: 600;\n margin-bottom: 12px;\n color: var(--ion-text-color, #333);\n}\n.section.guidance[_ngcontent-%COMP%] {\n background-color: rgba(59, 130, 246, 0.05);\n padding: 16px;\n border-radius: 8px;\n border-left: 4px solid #3B82F6;\n}\n.section.guidance.warning[_ngcontent-%COMP%] {\n background-color: rgba(245, 158, 11, 0.05);\n border-left-color: #F59E0B;\n}\n.empty-message[_ngcontent-%COMP%] {\n color: var(--ion-color-medium, #666);\n font-style: italic;\n margin-bottom: 12px;\n}\n.hint-text[_ngcontent-%COMP%] {\n font-size: 14px;\n color: var(--ion-color-medium, #666);\n margin-bottom: 12px;\n font-style: italic;\n}\n.guidance-list[_ngcontent-%COMP%] {\n margin: 0;\n padding-left: 24px;\n color: var(--ion-color-medium, #555);\n}\n.guidance-list[_ngcontent-%COMP%] li[_ngcontent-%COMP%] {\n margin-bottom: 8px;\n}\n.guidance-list.warning[_ngcontent-%COMP%] {\n color: var(--ion-color-warning, #854d0e);\n}\n.nodes-list[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n.node-item[_ngcontent-%COMP%] {\n background-color: var(--ion-color-light, #f8f8f8);\n border: 1px solid var(--ion-color-medium, #ddd);\n border-radius: 8px;\n padding: 12px;\n}\n.node-item.node-used[_ngcontent-%COMP%] {\n border-left: 4px solid #10B981;\n background-color: rgba(16, 185, 129, 0.02);\n}\n.node-item.node-outlier[_ngcontent-%COMP%] {\n border-left: 4px solid #EF4444;\n background-color: rgba(239, 68, 68, 0.02);\n}\n.node-item[_ngcontent-%COMP%] .node-header[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n}\n.node-item[_ngcontent-%COMP%] .node-header[_ngcontent-%COMP%] .node-info[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n.node-item[_ngcontent-%COMP%] .node-header[_ngcontent-%COMP%] .node-info[_ngcontent-%COMP%] .node-status-icon[_ngcontent-%COMP%] {\n font-size: 18px;\n}\n.node-item[_ngcontent-%COMP%] .node-header[_ngcontent-%COMP%] .node-info[_ngcontent-%COMP%] .node-status-icon.success[_ngcontent-%COMP%] {\n color: #10B981;\n}\n.node-item[_ngcontent-%COMP%] .node-header[_ngcontent-%COMP%] .node-info[_ngcontent-%COMP%] .node-status-icon.error[_ngcontent-%COMP%] {\n color: #EF4444;\n}\n.node-item[_ngcontent-%COMP%] .node-header[_ngcontent-%COMP%] .node-info[_ngcontent-%COMP%] strong[_ngcontent-%COMP%] {\n color: var(--ion-text-color, #333);\n font-size: 15px;\n}\n.node-item[_ngcontent-%COMP%] .node-header[_ngcontent-%COMP%] .node-info[_ngcontent-%COMP%] .node-id[_ngcontent-%COMP%] {\n font-size: 12px;\n color: var(--ion-color-medium, #666);\n font-family: monospace;\n}\n.node-item[_ngcontent-%COMP%] .node-header[_ngcontent-%COMP%] ion-button[_ngcontent-%COMP%] {\n margin: 0;\n --padding-start: 8px;\n --padding-end: 8px;\n}\n.node-item[_ngcontent-%COMP%] .node-coords[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n font-size: 13px;\n}\n.node-item[_ngcontent-%COMP%] .node-coords[_ngcontent-%COMP%] .coord-row[_ngcontent-%COMP%] {\n display: flex;\n gap: 8px;\n}\n.node-item[_ngcontent-%COMP%] .node-coords[_ngcontent-%COMP%] .coord-row[_ngcontent-%COMP%] label[_ngcontent-%COMP%] {\n font-weight: 600;\n color: var(--ion-color-medium, #555);\n min-width: 50px;\n}\n.node-item[_ngcontent-%COMP%] .node-coords[_ngcontent-%COMP%] .coord-row[_ngcontent-%COMP%] span[_ngcontent-%COMP%] {\n color: var(--ion-text-color, #333);\n font-family: monospace;\n}\n.matrix-grid[_ngcontent-%COMP%], \n.metrics-grid[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 12px;\n background-color: var(--ion-color-light, #f8f8f8);\n padding: 16px;\n border-radius: 8px;\n border: 1px solid var(--ion-color-medium, #ddd);\n}\n.matrix-row[_ngcontent-%COMP%], \n.metric-row[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.matrix-row[_ngcontent-%COMP%] label[_ngcontent-%COMP%], \n.metric-row[_ngcontent-%COMP%] label[_ngcontent-%COMP%] {\n font-size: 13px;\n font-weight: 600;\n color: var(--ion-color-medium, #555);\n}\n.matrix-row[_ngcontent-%COMP%] span[_ngcontent-%COMP%], \n.metric-row[_ngcontent-%COMP%] span[_ngcontent-%COMP%] {\n font-size: 14px;\n color: var(--ion-text-color, #333);\n font-family: monospace;\n}\nion-header[_ngcontent-%COMP%] ion-toolbar[_ngcontent-%COMP%] ion-buttons[_ngcontent-%COMP%] ion-button[_ngcontent-%COMP%] {\n margin: 0;\n}\nion-header[_ngcontent-%COMP%] ion-toolbar[_ngcontent-%COMP%] ion-buttons[_ngcontent-%COMP%] ion-button[color=danger][_ngcontent-%COMP%] {\n --color: #dc2626;\n --color-hover: #b91c1c;\n}\n.success-text[_ngcontent-%COMP%] {\n color: #10B981 !important;\n font-weight: 600;\n}\n.error-text[_ngcontent-%COMP%] {\n color: #EF4444 !important;\n font-weight: 600;\n}"], changeDetection: 0 }); 15592var GpsDebugModalComponent = _GpsDebugModalComponent; 15593(() => { 15594 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(GpsDebugModalComponent, [{ 15595 type: Component, 15596 args: [{ selector: "app-gps-debug-modal", standalone: true, imports: [ 15597 CommonModule, 15598 FormsModule, 15599 IonButton, 15600 NgIcon, 15601 EntityPanelComponent, 15602 EntitySectionComponent, 15603 EntityRowComponent 15604 ], providers: [ 15605 provideIcons({ 15606 lucideX, 15607 lucideTrash2, 15608 lucideCircleCheck, 15609 lucideCircleX, 15610 lucideLocate 15611 }) 15612 ], changeDetection: ChangeDetectionStrategy.OnPush, template: `<app-entity-panel 15613 [title]="'GPS Transform Debug'" 15614 [applyText]="'Clear All GPS'" 15615 [showApplyIcon]="false" 15616 [applyDisabled]="gpsNodes().length === 0" 15617 (closeRequested)="dismiss()" 15618 (applyRequested)="clearAllGpsCoordinates()"> 15619 15620 <ng-container panel-content> 15621 <!-- Status Section --> 15622 <app-entity-section label="Transform Status"> 15623 <div class="status-card" [ngClass]="statusClass"> 15624 <div class="status-header"> 15625 <ng-icon [name]="statusIcon" class="status-icon"></ng-icon>
15626 <span class="status-text">{{ statusText }}</span> 15627 </div> 15628 </div> 15629 </app-entity-section> 15630 15631 <!-- Transform Matrix --> 15632 @if (hasTransform() && transformMatrix()) { 15633 <app-entity-section label="Transform Matrix"> 15634 <p class="hint-text">2D Affine transformation: x = a\xB7lng + b\xB7lat + tx, y = c\xB7lng + d\xB7lat + ty</p> 15635 <div class="matrix-grid"> 15636 <div class="matrix-row"> 15637 <label>a (lng\u2192x):</label> 15638 <span>{{ formatNumber(transformMatrix()!.a, 10) }}</span> 15639 </div> 15640 <div class="matrix-row"> 15641 <label>b (lat\u2192x):</label> 15642 <span>{{ formatNumber(transformMatrix()!.b, 10) }}</span> 15643 </div> 15644 <div class="matrix-row"> 15645 <label>tx (x offset):</label> 15646 <span>{{ formatNumber(transformMatrix()!.tx, 2) }}</span> 15647 </div> 15648 <div class="matrix-row"> 15649 <label>c (lng\u2192y):</label> 15650 <span>{{ formatNumber(transformMatrix()!.c, 10) }}</span> 15651 </div> 15652 <div class="matrix-row"> 15653 <label>d (lat\u2192y):</label> 15654 <span>{{ formatNumber(transformMatrix()!.d, 10) }}</span> 15655 </div> 15656 <div class="matrix-row"> 15657 <label>ty (y offset):</label> 15658 <span>{{ formatNumber(transformMatrix()!.ty, 2) }}</span> 15659 </div> 15660 </div> 15661 </app-entity-section> 15662 } 15663 15664 <!-- Quality Metrics --> 15665 @if (hasTransform() && transformQuality()) { 15666 <app-entity-section label="Quality Metrics"> 15667 <div class="metrics-grid"> 15668 <div class="metric-row"> 15669 <label>Total Reference Points:</label> 15670 <span>{{ transformQuality()!.referencePointCount }}</span> 15671 </div> 15672 <div class="metric-row"> 15673 <label>Points Used (Inliers):</label> 15674 <span class="success-text">{{ transformQuality()!.inlierCount }}</span> 15675 </div> 15676 @if (transformQuality()!.outlierCount > 0) { 15677 <div class="metric-row"> 15678 <label>Points Filtered (Outliers):</label> 15679 <span class="error-text">{{ transformQuality()!.outlierCount }}</span> 15680 </div> 15681 } 15682 <div class="metric-row"> 15683 <label>Max Error (Inliers):</label> 15684 <span>{{ formatNumber(transformQuality()!.maxError, 1) }} cm ({{ formatNumber(transformQuality()!.maxError / 100, 2) }} m)</span> 15685 </div> 15686 <div class="metric-row"> 15687 <label>Mean Squared Error:</label> 15688 <span>{{ formatNumber(transformQuality()!.meanSquaredError, 1) }} cm\xB2</span> 15689 </div> 15690 <div class="metric-row"> 15691 <label>Rotation Angle:</label> 15692 <span>{{ formatNumber(transformQuality()!.rotationAngle, 2) }}\xB0</span> 15693 </div> 15694 <div class="metric-row"> 15695 <label>Scale X:</label> 15696 <span>{{ formatNumber(transformQuality()!.scaleX, 6) }}</span> 15697 </div> 15698 <div class="metric-row"> 15699 <label>Scale Y:</label> 15700 <span>{{ formatNumber(transformQuality()!.scaleY, 6) }}</span> 15701 </div> 15702 <div class="metric-row"> 15703 <label>Uniform Scale:</label> 15704 <span>{{ transformQuality()!.uniformScale ? 'Yes' : 'No' }}</span> 15705 </div> 15706 </div> 15707 </app-entity-section> 15708 } 15709 15710 <!-- Guidance --> 15711 @if (!hasTransform() || gpsNodes().length < 2) { 15712 <app-entity-section label="Setup Instructions"> 15713 <ol class="guidance-list"> 15714 <li>Select an outdoor node on the map</li> 15715 <li>Click the GPS pin icon to assign GPS coordinates</li> 15716 <li>Repeat for at least 2 outdoor nodes (3+ recommended for accuracy)</li> 15717 <li>Nodes should be well-distributed across your outdoor area</li> 15718 </ol> 15719 </app-entity-section> 15720 } @else if (transformQuality() && !transformQuality()!.isReliable) { 15721 <app-entity-section label="Reliability Warnings"> 15722 <ul class="guidance-list warning"> 15723 @if (transformQuality()!.referencePointCount < 3) { 15724 <li>Only {{ transformQuality()!.referencePointCount }} reference points - add more for better accuracy</li> 15725 } 15726 @if (transformQuality()!.maxError > 500) { 15727 <li>High maximum error ({{ formatNumber(transformQuality()!.maxError / 100, 2) }}m) - verify node GPS coordinates</li> 15728 } 15729 @if (!transformQuality()!.uniformScale) { 15730 <li>Non-uniform scale detected - map may be distorted</li> 15731 } 15732 </ul> 15733 </app-entity-section> 15734 } 15735 15736 <!-- GPS Reference Nodes - Used --> 15737 <app-entity-section label="GPS Reference Nodes - Used ({{ usedNodes().length }})"> 15738 @if (gpsNodes().length === 0) { 15739 <p class="empty-message">No nodes with GPS coordinates found.</p> 15740 <p class="hint-text">
15740To create a GPS transform, assign GPS coordinates to at least 2 outdoor nodes.</p> 15741 } @else if (usedNodes().length === 0) { 15742 <p class="empty-message">All reference points were filtered as outliers.</p> 15743 } @else { 15744 <div class="nodes-list"> 15745 @for (node of usedNodes(); track node.id) { 15746 <div class="node-item node-used"> 15747 <div class="node-header"> 15748 <div class="node-info"> 15749 <ng-icon name="lucideCircleCheck" class="node-status-icon success"></ng-icon> 15750 <strong>{{ node.type }}</strong> 15751 <span class="node-id">ID: {{ node.id }}</span> 15752 </div> 15753 <ion-button 15754 fill="clear" 15755 size="small" 15756 (click)="locateNode(node.id)" 15757 title="Locate on map"> 15758 <ng-icon name="lucideLocate"></ng-icon> 15759 </ion-button> 15760 </div> 15761 <div class="node-coords"> 15762 <div class="coord-row"> 15763 <label>GPS:</label> 15764 <span>{{ formatCoordinate(node.latitude) }}, {{ formatCoordinate(node.longitude) }}</span> 15765 </div> 15766 <div class="coord-row"> 15767 <label>Map:</label> 15768 <span>{{ formatMapCoord(node.mapX) }}, {{ formatMapCoord(node.mapY) }}</span> 15769 </div> 15770 @if (node.error !== undefined) { 15771 <div class="coord-row"> 15772 <label>Error:</label> 15773 <span class="success-text">{{ formatError(node.error) }}</span> 15774 </div> 15775 } 15776 </div> 15777 </div> 15778 } 15779 </div> 15780 } 15781 </app-entity-section> 15782 15783 <!-- GPS Reference Nodes - Filtered Outliers --> 15784 @if (hasOutliers()) { 15785 <app-entity-section label="Filtered Outliers ({{ outlierNodes().length }})"> 15786 <p class="hint-text">These nodes were automatically excluded due to high transformation error.</p> 15787 <div class="nodes-list"> 15788 @for (node of outlierNodes(); track node.id) { 15789 <div class="node-item node-outlier"> 15790 <div class="node-header"> 15791 <div class="node-info"> 15792 <ng-icon name="lucideCircleX" class="node-status-icon error"></ng-icon> 15793 <strong>{{ node.type }}</strong> 15794 <span class="node-id">ID: {{ node.id }}</span> 15795 </div> 15796 <ion-button 15797 fill="clear" 15798 size="small" 15799 (click)="locateNode(node.id)" 15800 title="Locate on map"> 15801 <ng-icon name="lucideLocate"></ng-icon> 15802 </ion-button> 15803 </div> 15804 <div class="node-coords"> 15805 <div class="coord-row"> 15806 <label>GPS:</label> 15807 <span>{{ formatCoordinate(node.latitude) }}, {{ formatCoordinate(node.longitude) }}</span> 15808 </div> 15809 <div class="coord-row"> 15810 <label>Map:</label> 15811 <span>{{ formatMapCoord(node.mapX) }}, {{ formatMapCoord(node.mapY) }}</span> 15812 </div> 15813 @if (node.error !== undefined) { 15814 <div class="coord-row"> 15815 <label>Error:</label> 15816 <span class="error-text">{{ formatError(node.error) }}</span> 15817 </div> 15818 } 15819 </div> 15820 </div> 15821 } 15822 </div> 15823 </app-entity-section> 15824 } 15825 </ng-container> 15826 15827 <ng-container panel-info> 15828 <app-entity-row label="Total Nodes" [value]="gpsNodes().length.toString()" /> 15829 @if (hasTransform() && transformQuality()) { 15830 <app-entity-row label="Inliers" [value]="transformQuality()!.inlierCount.toString()" /> 15831 @if (transformQuality()!.outlierCount > 0) { 15832 <app-entity-row label="Outliers" [value]="transformQuality()!.outlierCount.toString()" /> 15833 } 15834 } 15835 </ng-container> 15836 15837</app-entity-panel> 15838`, styles: ["/* src/app/pages/campus-map-v3/components/gps-debug-modal/gps-debug-modal.component.scss */\n.gps-debug-container {\n background: var(--tool-palette-bg, rgba(255, 255, 255, 0.95));\n border-radius: 12px;\n box-shadow: var(--tool-palette-shadow, 0 4px 12px rgba(0, 0, 0, 0.15));\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light, rgba(0, 0, 0, 0.08));\n min-width: 380px;\n max-width: 380px;\n overflow: hidden;\n}\n.status-card {\n padding: 16px;\n border-radius: 8px;\n margin-bottom: 12px;\n border: 2px solid;\n}\n.status-card.status-success {\n background-color: rgba(16, 185, 129, 0.1);\n border-color: #10B981;\n color: #059669;\n}\n.status-card.status-warning {\n background-color: rgba(245, 158, 11, 0.1);\n border-color: #F59E0B;\n color: #D97706;\n}\n.status-card.status-error {\n background-color: rgba(239, 68, 68, 0.1);\n border-color: #EF4444;\n color: #DC2626;\n}\n.status-card .status-header {\n display: flex;\n align-items: center;\n gap: 12px;\n font-weight: 600;\n font-size: 16px;\n}\n.status-card .status-header .status-icon {\n font-size: 24px;\n}\n.section {\n margin-bottom: 12px;\n}\n.section h3 {\n font-size: 18px;\n font-weight: 600;\n margin-bottom: 12px;\n color: var(--ion-text-color, #333);\n}\n.section.guidance {\n background-color: rgba(59, 130, 246, 0.05);\n padding: 16px;\n border-radius: 8px;\n border-left: 4px solid #3B82F6;\n}\n.section.guidance.warning {\n background-color: rgba(245, 158, 11, 0.05);\n border-left-color: #F59E0B;\n}\n.empty-message {\n color: var(--ion-color-medium, #666);\n font-style: italic;\n margin-bottom: 12px;\n}\n.hint-text {\n font-size: 14px;\n color: var(--ion-color-medium, #666);\n margin-bottom: 12px;\n font-style: italic;\n}\n.guidance-list {\n margin: 0;\n padding-left: 24px;\n color: var(--ion-color-medium, #555);\n}\n.guidance-list li {\n margin-bottom: 8px;\n}\n.guidance-list.warning {\n color: var(--ion-color-warning, #854d0e);\n}\n.nodes-list {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n.node-item {\n background-color: var(--ion-color-light, #f8f8f8);\n border: 1px solid var(--ion-color-medium, #ddd);\n border-radius: 8px;\n padding: 12px;\n}\n.node-item.node-used {\n border-left: 4px solid #10B981;\n background-color: rgba(16, 185, 129, 0.02);\n}\n.node-item.node-outlier {\n border-left: 4px solid #EF4444;\n background-color: rgba(239, 68, 68, 0.02);\n}\n.node-item .node-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n}\n.node-item .node-header .node-info {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n.node-item .node-header .node-info .node-status-icon {\n font-size: 18px;\n}\n.node-item .node-header .node-info .node-status-icon.success {\n color: #10B981;\n}\n.node-item .node-header .node-info .node-status-icon.error {\n color: #EF4444;\n}\n.node-item .node-header .node-info strong {\n color: var(--ion-text-color, #333);\n font-size: 15px;\n}\n.node-item .node-header .node-info .node-id {\n font-size: 12px;\n color: var(--ion-color-medium, #666);\n font-family: monospace;\n}\n.node-item .node-header ion-button {\n margin: 0;\n --padding-start: 8px;\n --padding-end: 8px;\n}\n.node-item .node-coords {\n display: flex;\n flex-direction: column;\n gap: 4px;\n font-size: 13px;\n}\n.node-item .node-coords .coord-row {\n display: flex;\n gap: 8px;\n}\n.node-item .node-coords .coord-row label {\n font-weight: 600;\n color: var(--ion-color-medium, #555);\n min-width: 50px;\n}\n.node-item .node-coords .coord-row span {\n color: var(--ion-text-color, #333);\n font-family: monospace;\n}
15838\n.matrix-grid,\n.metrics-grid {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 12px;\n background-color: var(--ion-color-light, #f8f8f8);\n padding: 16px;\n border-radius: 8px;\n border: 1px solid var(--ion-color-medium, #ddd);\n}\n.matrix-row,\n.metric-row {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.matrix-row label,\n.metric-row label {\n font-size: 13px;\n font-weight: 600;\n color: var(--ion-color-medium, #555);\n}\n.matrix-row span,\n.metric-row span {\n font-size: 14px;\n color: var(--ion-text-color, #333);\n font-family: monospace;\n}\nion-header ion-toolbar ion-buttons ion-button {\n margin: 0;\n}\nion-header ion-toolbar ion-buttons ion-button[color=danger] {\n --color: #dc2626;\n --color-hover: #b91c1c;\n}\n.success-text {\n color: #10B981 !important;\n font-weight: 600;\n}\n.error-text {\n color: #EF4444 !important;\n font-weight: 600;\n}\n"] }] 15839 }], () => [], { closeRequested: [{ 15840 type: Output 15841 }], gpsNodeLocate: [{ 15842 type: Output 15843 }] }); 15844})(); 15845(() => { 15846 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(GpsDebugModalComponent, { className: "GpsDebugModalComponent", filePath: "src/app/pages/campus-map-v3/components/gps-debug-modal/gps-debug-modal.component.ts", lineNumber: 63 }); 15847})(); 15848
15849// src/app/pages/campus-map-v3/commands/delete-command.ts 15850var DeleteCommand = class extends BaseEditorCommand { 15851 constructor(config) { 15852 super(); 15853 this.snapshots = /* @__PURE__ */ new Map(); 15854 this.targets = config.targets; 15855 this.database = config.database; 15856 this.onVisualUpdate = config.onVisualUpdate; 15857 if (this.targets.length === 1) { 15858 const target = this.targets[0]; 15859 this.description = target.name ? `Delete "${target.name}"` : `Delete ${this.formatType(target.type)}`; 15860 } else { 15861 this.description = `Delete ${this.targets.length} items`; 15862 } 15863 } 15864 execute() { 15865 if (this.snapshots.size === 0) { 15866 for (const target of this.targets) { 15867 const snapshots = this.captureEntitySnapshots(target.id, target.type); 15868 this.snapshots.set(target.id, snapshots); 15869 } 15870 } 15871 for (const target of this.targets) { 15872 this.softDeleteEntity(target.id); 15873 } 15874 this.onVisualUpdate(); 15875 } 15876 undo() { 15877 for (const [, snapshots] of this.snapshots) { 15878 for (const snapshot of snapshots) { 15879 const model = this.database.Models.get(snapshot.id); 15880 if (model) { 15881 Object.assign(model.entity, snapshot.data); 15882 model.entity.isDeleted = false; 15883 model.entity.isChanged = true; 15884 } 15885 } 15886 } 15887 this.onVisualUpdate(); 15888 } 15889 captureEntitySnapshots(id, type) { 15890 const snapshots = []; 15891 const model = this.database.Models.get(id); 15892 if (!model) 15893 return snapshots; 15894 snapshots.push({ 15895 id: model.entity.id, 15896 type, 15897 data: JSON.parse(JSON.stringify(model.entity)), 15898 isDeleted: model.entity.isDeleted 15899 }); 15900 if (type === "room" || type === "floor" || type === "groundPatch") { 15901 const corners = model.corners || []; 15902 for (const corner of corners) { 15903 snapshots.push({ 15904 id: corner.entity.id, 15905 type: "corner", 15906 data: JSON.parse(JSON.stringify(corner.entity)), 15907 isDeleted: corner.entity.isDeleted 15908 }); 15909 } 15910 } 15911 return snapshots; 15912 } 15913 softDeleteEntity(id) { 15914 const model = this.database.Models.get(id); 15915 if (!model) 15916 return; 15917 model.entity.isDeleted = true; 15918 model.entity.isChanged = true; 15919 const corners = model.corners; 15920 if (corners) { 15921 for (const corner of corners) { 15922 corner.entity.isDeleted = true; 15923 corner.entity.isChanged = true; 15924 } 15925 } 15926 } 15927}; 15928
15929// src/app/pages/campus-map-v3/commands/create-node-command.ts 15930var CreateNodeCommand = class extends BaseEditorCommand { 15931 constructor(config) { 15932 var _a3; 15933 super(); 15934 this.createdNodeId = null; 15935 this.node = config.node; 15936 this.database = config.database; 15937 this.onVisualUpdate = config.onVisualUpdate; 15938 this.onNodeCreated = config.onNodeCreated; 15939 const typeName = (_a3 = this.node.type) != null ? _a3 : NodeType.Regular; 15940 this.description = `Create ${typeName.toLowerCase()} node`; 15941 } 15942 execute() { 15943 return __async(this, null, function* () { 15944 if (!this.node.id) { 15945 this.node.id = crypto.randomUUID(); 15946 } 15947 this.node.isDeleted = false; 15948 this.node.isChanged = true; 15949 this.node.version = 0; 15950 const model = yield this.database.addEntity(this.node, false); 15951 this.createdNodeId = this.node.id; 15952 this.database.fixModelRelations(model); 15953 this.onVisualUpdate(); 15954 if (this.onNodeCreated && this.createdNodeId) { 15955 this.onNodeCreated(this.createdNodeId); 15956 } 15957 }); 15958 } 15959 undo() { 15960 if (!this.createdNodeId) 15961 return; 15962 const model = this.database.Models.get(this.createdNodeId); 15963 if (!model) 15964 return; 15965 model.entity.isDeleted = true; 15966 model.entity.isChanged = true; 15967 this.database.Models.delete(this.createdNodeId); 15968 this.onVisualUpdate(); 15969 } 15970 redo() { 15971 if (!this.createdNodeId) { 15972 this.execute(); 15973 return; 15974 } 15975 const nodeModel = new NodeModel(this.node); 15976 this.node.isDeleted = false; 15977 this.node.isChanged = true; 15978 this.database.Models.set(this.createdNodeId, nodeModel); 15979 this.database.fixModelRelations(nodeModel); 15980 this.onVisualUpdate(); 15981 if (this.onNodeCreated) { 15982 this.onNodeCreated(this.createdNodeId); 15983 } 15984 } 15985}; 15986
15987// src/app/pages/campus-map-v3/commands/create-node-connection-command.ts 15988var CreateNodeConnectionCommand = class extends BaseEditorCommand { 15989 constructor(config) { 15990 super(); 15991 this.connection = null; 15992 this.createdConnectionId = null; 15993 this.nodeAId = config.nodeAId; 15994 this.nodeBId = config.nodeBId; 15995 this.database = config.database; 15996 this.onVisualUpdate = config.onVisualUpdate; 15997 this.description = "Create node connection"; 15998 } 15999 execute() { 16000 return __async(this, null, function* () { 16001 var _a3, _b2, _c, _d; 16002 const nodeA = this.database.Models.get(this.nodeAId); 16003 const nodeB = this.database.Models.get(this.nodeBId); 16004 if (!nodeA || !nodeB || nodeA.type !== EntityType.NODE || nodeB.type !== EntityType.NODE) { 16005 return; 16006 } 16007 const nodeAEntity = nodeA.entity; 16008 const nodeBEntity = nodeB.entity; 16009 const dx = ((_a3 = nodeBEntity.locationX) != null ? _a3 : 0) - ((_b2 = nodeAEntity.locationX) != null ? _b2 : 0); 16010 const dy = ((_c = nodeBEntity.locationY) != null ? _c : 0) - ((_d = nodeAEntity.locationY) != null ? _d : 0); 16011 const length = Math.sqrt(dx * dx + dy * dy); 16012 let floorId = null; 16013 if (nodeAEntity.floorId && nodeAEntity.floorId === nodeBEntity.floorId) { 16014 floorId = nodeAEntity.floorId; 16015 } 16016 this.connection = Object.assign(new NodeConnection(), { 16017 id: crypto.randomUUID(), 16018 campusId: nodeAEntity.campusId, 16019 nodeAId: this.nodeAId, 16020 nodeBId: this.nodeBId, 16021 floorId, 16022 length, 16023 lengthAdjusted: length, 16024 coefficient: 1, 16025 isDeleted: false, 16026 isChanged: true, 16027 version: 0 16028 }); 16029 this.createdConnectionId = this.connection.id; 16030 const model = yield this.database.addEntity(this.connection, false); 16031 this.database.fixModelRelations(model); 16032 this.onVisualUpdate(); 16033 }); 16034 } 16035 undo() { 16036 if (!this.createdConnectionId) 16037 return; 16038 const model = this.database.Models.get(this.createdConnectionId); 16039 if (!model) 16040 return; 16041 model.entity.isDeleted = true; 16042 model.entity.isChanged = true; 16043 this.database.Models.delete(this.createdConnectionId); 16044 this.onVisualUpdate(); 16045 } 16046 redo() { 16047 if (!this.createdConnectionId || !this.connection) { 16048 this.execute(); 16049 return; 16050 } 16051 const connectionModel = new NodeConnectionModel(this.connection); 16052 this.connection.isDeleted = false; 16053 this.connection.isChanged = true; 16054 this.database.Models.set(this.createdConnectionId, connectionModel); 16055 this.database.fixModelRelations(connectionModel); 16056 this.onVisualUpdate(); 16057 } 16058}; 16059
16060// src/app/pages/campus-map-v3/commands/update-nodes-building-command.ts 16061var UpdateNodesBuildingCommand = class extends BaseEditorCommand { 16062 constructor(config) { 16063 super(); 16064 this.previousValues = []; 16065 this.nodeIds = [...new Set(config.nodeIds)]; 16066 this.buildingId = config.buildingId; 16067 this.database = config.database; 16068 this.onVisualUpdate = config.onVisualUpdate; 16069 this.description = `Assign ${this.nodeIds.length} node${this.nodeIds.length === 1 ? "" : "s"} to building`; 16070 } 16071 execute() { 16072 if (this.previousValues.length === 0) { 16073 this.previousValues = this.getExistingNodes().map(({ nodeId, entity }) => ({ 16074 nodeId, 16075 buildingId: entity.buildingId 16076 })); 16077 } 16078 for (const { entity } of this.getExistingNodes()) { 16079 entity.buildingId = this.buildingId; 16080 entity.isChanged = true; 16081 } 16082 this.onVisualUpdate(); 16083 } 16084 undo() { 16085 for (const previous of this.previousValues) { 16086 const model = this.database.Models.get(previous.nodeId); 16087 if (!model) 16088 continue; 16089 const entity = model.entity; 16090 entity.buildingId = previous.buildingId; 16091 entity.isChanged = true; 16092 } 16093 this.onVisualUpdate(); 16094 } 16095 getExistingNodes() { 16096 return this.nodeIds.map((nodeId) => { 16097 const model = this.database.Models.get(nodeId); 16098 return model ? { nodeId, entity: model.entity } : null; 16099 }).filter((item) => !!item); 16100 } 16101}; 16102
16103// src/app/pages/campus-map-v3/commands/update-nodes-campus-entrance-command.ts 16104var UpdateNodesCampusEntranceCommand = class extends BaseEditorCommand { 16105 constructor(config) { 16106 super(); 16107 this.previousValues = []; 16108 this.nodeIds = [...new Set(config.nodeIds)]; 16109 this.isCampusEntrance = config.isCampusEntrance; 16110 this.database = config.database; 16111 this.onVisualUpdate = config.onVisualUpdate; 16112 const count = this.nodeIds.length; 16113 this.description = this.isCampusEntrance ? count === 1 ? "Mark node as a campus entrance" : `Mark ${count} nodes as campus entrances` : count === 1 ? "Remove campus entrance flag from node" : `Remove campus entrance flag from ${count} nodes`; 16114 } 16115 execute() { 16116 if (this.previousValues.length === 0) { 16117 this.previousValues = this.getExistingNodes().map(({ nodeId, entity }) => ({ 16118 nodeId, 16119 isCampusEntrance: entity.isCampusEntrance 16120 })); 16121 } 16122 for (const { entity } of this.getExistingNodes()) { 16123 entity.isCampusEntrance = this.isCampusEntrance; 16124 entity.isChanged = true; 16125 } 16126 this.onVisualUpdate(); 16127 } 16128 undo() { 16129 for (const previous of this.previousValues) { 16130 const model = this.database.Models.get(previous.nodeId); 16131 if (!model) 16132 continue; 16133 const entity = model.entity; 16134 entity.isCampusEntrance = previous.isCampusEntrance; 16135 entity.isChanged = true; 16136 } 16137 this.onVisualUpdate(); 16138 } 16139 getExistingNodes() { 16140 return this.nodeIds.map((nodeId) => { 16141 const model = this.database.Models.get(nodeId); 16142 return model ? { nodeId, entity: model.entity } : null; 16143 }).filter((item) => !!item); 16144 } 16145}; 16146
16147// src/app/pages/campus-map-v3/services/shape-edit-state.service.ts 16148var _ShapeEditStateService = class _ShapeEditStateService { 16149 constructor() { 16150 this._mode = signal("none"); 16151 this._activeEntityId = signal(null); 16152 this._activeEntityType = signal(null); 16153 this._targetFloorId = signal(null); 16154 this._selectedCornerIds = signal(/* @__PURE__ */ new Set()); 16155 this._hoveredCornerId = signal(null); 16156 this._hoveredEdgeIndex = signal(null); 16157 this._selectedRoomWall = signal(null); 16158 this._wallLengthError = signal(null); 16159 this._drawingVertices = signal([]); 16160 this._cursorPosition = signal(null); 16161 this.mode = this._mode.asReadonly(); 16162 this.activeEntityId = this._activeEntityId.asReadonly(); 16163 this.activeEntityType = this._activeEntityType.asReadonly(); 16164 this.targetFloorId = this._targetFloorId.asReadonly(); 16165 this.activeRoomId = this._activeEntityId.asReadonly(); 16166 this.selectedCornerIds = this._selectedCornerIds.asReadonly(); 16167 this.hoveredCornerId = this._hoveredCornerId.asReadonly(); 16168 this.hoveredEdgeIndex = this._hoveredEdgeIndex.asReadonly(); 16169 this.selectedRoomWall = this._selectedRoomWall.asReadonly(); 16170 this.wallLengthError = this._wallLengthError.asReadonly(); 16171 this.drawingVertices = this._drawingVertices.asReadonly(); 16172 this.cursorPosition = this._cursorPosition.asReadonly(); 16173 this.isDrawing = computed(() => this._mode() === "draw"); 16174 this.isEditing = computed(() => this._mode() === "edit"); 16175 this.isActive = computed(() => this._mode() !== "none"); 16176 this.vertexCount = computed(() => this._drawingVertices().length); 16177 this.canClosePolygon = computed(() => this._drawingVertices().length >= 3); 16178 this.hasSelectedCorners = computed(() => this._selectedCornerIds().size > 0); 16179 } 16180 // ========================================================================= 16181 // Mode Management 16182 // ========================================================================= 16183 /** 16184 * Enter draw mode to create a new polygon. 16185 * Floor will be auto-detected from first click position. 16186 */ 16187 enterDrawMode() { 16188 this.exitMode(); 16189 this._mode.set("draw"); 16190 } 16191 /** 16192 * Enter edit mode to modify an existing entity's shape. 16193 * @param entityId The entity ID to edit 16194 * @param entityType The type of entity ('room' or 'floor') 16195 */ 16196 enterEditMode(entityId, entityType = "room") { 16197 this.exitMode(); 16198 this._mode.set("edit"); 16199 this._activeEntityId.set(entityId); 16200 this._activeEntityType.set(entityType); 16201 } 16202 /** 16203 * Exit current mode and reset all state. 16204 */ 16205 exitMode() { 16206 this._mode.set("none"); 16207 this._activeEntityId.set(null); 16208 this._activeEntityType.set(null); 16209 this._targetFloorId.set(null); 16210 this._selectedCornerIds.set(/* @__PURE__ */ new Set()); 16211 this._hoveredCornerId.set(null); 16212 this._hoveredEdgeIndex.set(null); 16213 this._selectedRoomWall.set(null); 16214 this._wallLengthError.set(null); 16215 this._drawingVertices.set([]); 16216 this._cursorPosition.set(null); 16217 } 16218 // ========================================================================= 16219 // Drawing Operations 16220 // ========================================================================= 16221 /** 16222 * Set the target floor for the polygon being drawn. 16223 * Called after detecting floor from first click position. 16224 */ 16225 setTargetFloor(floorId) { 16226 this._targetFloorId.set(floorId); 16227 } 16228 /** 16229 * Add a vertex to the polygon being drawn. 16230 */ 16231 addDrawingVertex(point) { 16232 this._drawingVertices.update((vertices) => [...vertices, point]); 16233 } 16234 /** 16235 * Remove the last vertex from the polygon being drawn. 16236 * @returns true if a vertex was removed, false if empty 16237 */ 16238 removeLastDrawingVertex() { 16239 const vertices = this._drawingVertices(); 16240 if (vertices.length === 0) 16241 return false; 16242 this._drawingVertices.set(vertices.slice(0, -1)); 16243 if (vertices.length === 1) { 16244 this._targetFloorId.set(null); 16245 } 16246 return true; 16247 } 16248 /** 16249 * Clear all drawing vertices and reset drawing state. 16250 */ 16251 clearDrawingVertices() { 16252 this._drawingVertices.set([]); 16253 this._targetFloorId.set(null); 16254 this._cursorPosition.set(null); 16255 } 16256 /** 16257 * Update cursor position for rubber-band preview. 16258 */ 16259 setCursorPosition(point) { 16260 this._cursorPosition.set(point); 16261 } 16262 /** 16263 * Get the first vertex for close detection. 16264 */ 16265 getFirstVertex() { 16266 const vertices = this._drawingVertices(); 16267 return vertices.length > 0 ? vertices[0] : null; 16268 } 16269 /** 16270 * Get a copy of all drawing vertices. 16271 */ 16272 getDrawingVerticesCopy() { 16273 return [...this._drawingVertices()]; 16274 } 16275 // ========================================================================= 16276 // Edit Mode Corner Selection 16277 // ========================================================================= 16278 /** 16279 * Select a corner in edit mode. 16280 * @param cornerId Corner entity ID 16281 * @param mode 'replace' clears other selections, 'toggle' adds/removes 16282 */ 16283 selectCorner(cornerId, mode) { 16284 if (mode === "replace") { 16285 this._selectedCornerIds.set(/* @__PURE__ */ new Set([cornerId])); 16286 } else { 16287 this._selectedCornerIds.update((set) => { 16288 const newSet = new Set(set); 16289 if (newSet.has(cornerId)) { 16290 newSet.delete(cornerId); 16291 } else { 16292 newSet.add(cornerId); 16293 } 16294 return newSet; 16295 }); 16296 } 16297 } 16298 /**
16299 * Deselect all corners. 16300 */ 16301 deselectAllCorners() { 16302 this._selectedCornerIds.set(/* @__PURE__ */ new Set()); 16303 } 16304 /** 16305 * Check if a corner is selected. 16306 */ 16307 isCornerSelected(cornerId) { 16308 return this._selectedCornerIds().has(cornerId); 16309 } 16310 /** 16311 * Get array of selected corner IDs. 16312 */ 16313 getSelectedCornerIds() { 16314 return Array.from(this._selectedCornerIds()); 16315 } 16316 // ========================================================================= 16317 // Edit Mode Hover State 16318 // ========================================================================= 16319 /** 16320 * Set the currently hovered corner. 16321 */ 16322 setHoveredCorner(cornerId) { 16323 this._hoveredCornerId.set(cornerId); 16324 } 16325 /** 16326 * Set the currently hovered edge (for midpoint display). 16327 * @param edgeIndex Index of the edge (0 = between corner 0 and 1) 16328 */ 16329 setHoveredEdge(edgeIndex) { 16330 this._hoveredEdgeIndex.set(edgeIndex); 16331 } 16332 selectRoomWall(wall) { 16333 this._selectedRoomWall.set(wall); 16334 this._wallLengthError.set(null); 16335 this._selectedCornerIds.set(/* @__PURE__ */ new Set()); 16336 } 16337 updateSelectedRoomWall(wall) { 16338 this._selectedRoomWall.set(wall); 16339 } 16340 clearSelectedRoomWall() { 16341 this._selectedRoomWall.set(null); 16342 this._wallLengthError.set(null); 16343 } 16344 setWallLengthError(error) { 16345 this._wallLengthError.set(error); 16346 } 16347}; 16348_ShapeEditStateService.\u0275fac = function ShapeEditStateService_Factory(__ngFactoryType__) { 16349 return new (__ngFactoryType__ || _ShapeEditStateService)(); 16350}; 16351_ShapeEditStateService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _ShapeEditStateService, factory: _ShapeEditStateService.\u0275fac }); 16352var ShapeEditStateService = _ShapeEditStateService; 16353(() => { 16354 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(ShapeEditStateService, [{ 16355 type: Injectable 16356 }], null, null); 16357})(); 16358
16359// src/app/pages/campus-map-v3/commands/create-room-polygon-command.ts 16360var CreateRoomPolygonCommand = class extends BaseEditorCommand { 16361 constructor(config) { 16362 super(); 16363 this.description = "Create room polygon"; 16364 this.createdRoomId = null; 16365 this.createdCornerIds = []; 16366 this.roomEntity = null; 16367 this.cornerEntities = []; 16368 this.vertices = config.vertices; 16369 this.floorId = config.floorId; 16370 this.database = config.database; 16371 this.onVisualUpdate = config.onVisualUpdate; 16372 this.onRoomCreated = config.onRoomCreated; 16373 } 16374 execute() { 16375 return __async(this, null, function* () { 16376 var _a3, _b2; 16377 const floor = this.database.Models.get(this.floorId); 16378 if (!floor) { 16379 return; 16380 } 16381 const centroid = this.calculateCentroid(this.vertices); 16382 this.roomEntity = Object.assign(new Room(), { 16383 id: crypto.randomUUID(), 16384 campusId: (_a3 = floor.campus) == null ? void 0 : _a3.entity.id, 16385 buildingId: (_b2 = floor.building) == null ? void 0 : _b2.entity.id, 16386 floorId: this.floorId, 16387 locationX: centroid.x, 16388 locationY: centroid.y, 16389 roomType: RoomType.Regular, 16390 hideLabel: false, 16391 isDeleted: false, 16392 isChanged: true, 16393 version: 0 16394 }); 16395 this.createdRoomId = this.roomEntity.id; 16396 this.cornerEntities = this.vertices.map((v, i) => { 16397 var _a4; 16398 return Object.assign(new RoomCorner(), { 16399 id: crypto.randomUUID(), 16400 campusId: (_a4 = floor.campus) == null ? void 0 : _a4.entity.id, 16401 roomId: this.createdRoomId, 16402 locationX: v.x, 16403 locationY: v.y, 16404 position: i, 16405 isNextWallSolid: true, 16406 isDeleted: false, 16407 isChanged: true, 16408 version: 0 16409 }); 16410 }); 16411 this.createdCornerIds = this.cornerEntities.map((c) => c.id); 16412 const roomModel = yield this.database.addEntity(this.roomEntity, false); 16413 for (const corner of this.cornerEntities) { 16414 yield this.database.addEntity(corner, false); 16415 } 16416 this.database.fixModelRelations(roomModel); 16417 for (const cornerId of this.createdCornerIds) { 16418 const cornerModel = this.database.Models.get(cornerId); 16419 if (cornerModel) { 16420 this.database.fixModelRelations(cornerModel); 16421 } 16422 } 16423 this.onVisualUpdate(); 16424 if (this.onRoomCreated && this.createdRoomId) { 16425 this.onRoomCreated(this.createdRoomId); 16426 } 16427 }); 16428 } 16429 undo() { 16430 if (!this.createdRoomId) 16431 return; 16432 for (const cornerId of this.createdCornerIds) { 16433 const cornerModel = this.database.Models.get(cornerId); 16434 if (cornerModel) { 16435 cornerModel.entity.isDeleted = true; 16436 cornerModel.entity.isChanged = true; 16437 this.database.Models.delete(cornerId); 16438 } 16439 } 16440 const roomModel = this.database.Models.get(this.createdRoomId); 16441 if (roomModel) { 16442 roomModel.entity.isDeleted = true; 16443 roomModel.entity.isChanged = true; 16444 this.database.Models.delete(this.createdRoomId); 16445 } 16446 this.onVisualUpdate(); 16447 } 16448 redo() { 16449 if (!this.createdRoomId || !this.roomEntity) { 16450 this.execute(); 16451 return; 16452 } 16453 this.roomEntity.isDeleted = false; 16454 this.roomEntity.isChanged = true; 16455 const roomModel = new RoomModel(this.roomEntity); 16456 this.database.Models.set(this.createdRoomId, roomModel); 16457 for (let i = 0; i < this.cornerEntities.length; i++) { 16458 const corner = this.cornerEntities[i]; 16459 const cornerId = this.createdCornerIds[i]; 16460 corner.isDeleted = false; 16461 corner.isChanged = true; 16462 const cornerModel = new RoomCornerModel(corner); 16463 this.database.Models.set(cornerId, cornerModel); 16464 } 16465 this.database.fixModelRelations(roomModel); 16466 for (const cornerId of this.createdCornerIds) { 16467 const cornerModel = this.database.Models.get(cornerId); 16468 if (cornerModel) { 16469 this.database.fixModelRelations(cornerModel); 16470 } 16471 } 16472 this.onVisualUpdate(); 16473 if (this.onRoomCreated) { 16474 this.onRoomCreated(this.createdRoomId); 16475 } 16476 } 16477 /** 16478 * Calculate centroid (geometric center) from vertices. 16479 */ 16480 calculateCentroid(vertices) { 16481 const x = vertices.reduce((sum, v) => sum + v.x, 0) / vertices.length; 16482 const y = vertices.reduce((sum, v) => sum + v.y, 0) / vertices.length; 16483 return { x, y }; 16484 } 16485}; 16486
16487// src/app/pages/campus-map-v3/commands/create-floor-outline-command.ts 16488var CreateFloorOutlineCommand = class extends BaseEditorCommand { 16489 constructor(config) { 16490 super(); 16491 this.description = "Create floor outline"; 16492 this.createdCornerIds = []; 16493 this.cornerEntities = []; 16494 this.replacedCornerSnapshots = []; 16495 this.vertices = config.vertices; 16496 this.floorId = config.floorId; 16497 this.database = config.database; 16498 this.onVisualUpdate = config.onVisualUpdate; 16499 this.onOutlineCreated = config.onOutlineCreated; 16500 } 16501 execute() { 16502 return __async(this, null, function* () { 16503 const floor = this.database.Models.get(this.floorId); 16504 if (!floor) { 16505 return; 16506 } 16507 const existingCorners = floor.corners.filter((c) => !c.entity.isDeleted); 16508 if (existingCorners.length > 0 && this.replacedCornerSnapshots.length === 0) { 16509 for (const corner of existingCorners) { 16510 this.replacedCornerSnapshots.push({ 16511 id: corner.entity.id, 16512 type: "corner", 16513 data: JSON.parse(JSON.stringify(corner.entity)) 16514 }); 16515 } 16516 } 16517 for (const corner of existingCorners) { 16518 corner.entity.isDeleted = true; 16519 corner.entity.isChanged = true; 16520 this.database.Models.delete(corner.entity.id); 16521 } 16522 this.cornerEntities = this.vertices.map((v, i) => { 16523 var _a3; 16524 return Object.assign(new FloorCorner(), { 16525 id: crypto.randomUUID(), 16526 campusId: (_a3 = floor.campus) == null ? void 0 : _a3.entity.id, 16527 floorId: this.floorId, 16528 locationX: v.x, 16529 locationY: v.y, 16530 position: i, 16531 isDeleted: false, 16532 isChanged: true, 16533 version: 0 16534 }); 16535 }); 16536 this.createdCornerIds = this.cornerEntities.map((c) => c.id); 16537 for (const corner of this.cornerEntities) { 16538 yield this.database.addEntity(corner, false); 16539 } 16540 for (const cornerId of this.createdCornerIds) { 16541 const cornerModel = this.database.Models.get(cornerId); 16542 if (cornerModel) { 16543 this.database.fixModelRelations(cornerModel); 16544 } 16545 } 16546 this.onVisualUpdate(); 16547 if (this.onOutlineCreated) { 16548 this.onOutlineCreated(this.floorId); 16549 } 16550 }); 16551 } 16552 undo() { 16553 for (const cornerId of this.createdCornerIds) { 16554 const cornerModel = this.database.Models.get(cornerId); 16555 if (cornerModel) { 16556 cornerModel.entity.isDeleted = true; 16557 cornerModel.entity.isChanged = true; 16558 this.database.Models.delete(cornerId); 16559 } 16560 } 16561 for (const snapshot of this.replacedCornerSnapshots) { 16562 const restoredCorner = Object.assign(new FloorCorner(), snapshot.data); 16563 restoredCorner.isDeleted = false; 16564 restoredCorner.isChanged = true; 16565 const cornerModel = new FloorCornerModel(restoredCorner); 16566 this.database.Models.set(snapshot.id, cornerModel); 16567 this.database.fixModelRelations(cornerModel); 16568 } 16569 this.onVisualUpdate(); 16570 } 16571 redo() { 16572 if (this.cornerEntities.length === 0) { 16573 this.execute(); 16574 return; 16575 } 16576 for (const snapshot of this.replacedCornerSnapshots) { 16577 const cornerModel = this.database.Models.get(snapshot.id); 16578 if (cornerModel) { 16579 cornerModel.entity.isDeleted = true; 16580 cornerModel.entity.isChanged = true; 16581 this.database.Models.delete(snapshot.id); 16582 } 16583 } 16584 for (let i = 0; i < this.cornerEntities.length; i++) { 16585 const corner = this.cornerEntities[i]; 16586 const cornerId = this.createdCornerIds[i]; 16587 corner.isDeleted = false; 16588 corner.isChanged = true; 16589 const cornerModel = new FloorCornerModel(corner); 16590 this.database.Models.set(cornerId, cornerModel); 16591 } 16592 for (const cornerId of this.createdCornerIds) { 16593 const cornerModel = this.database.Models.get(cornerId); 16594 if (cornerModel) { 16595 this.database.fixModelRelations(cornerModel); 16596 } 16597 } 16598 this.onVisualUpdate(); 16599 if (this.onOutlineCreated) { 16600 this.onOutlineCreated(this.floorId); 16601 } 16602 } 16603}; 16604
16605// src/app/pages/campus-map-v3/commands/create-floor-with-outline-command.ts 16606var CreateFloorWithOutlineCommand = class extends BaseEditorCommand { 16607 constructor(config) { 16608 super(); 16609 this.createdFloorId = null; 16610 this.createdCornerIds = []; 16611 this.floorEntity = null; 16612 this.cornerEntities = []; 16613 this.floorData = config.floorData; 16614 this.vertices = config.vertices; 16615 this.database = config.database; 16616 this.onVisualUpdate = config.onVisualUpdate; 16617 this.onFloorCreated = config.onFloorCreated; 16618 this.description = `Create floor "${this.floorData.name}" with outline`; 16619 } 16620 execute() { 16621 return __async(this, null, function* () { 16622 if (!this.createdFloorId) { 16623 this.createdFloorId = crypto.randomUUID(); 16624 } 16625 this.floorEntity = Object.assign(new Floor(), { 16626 id: this.createdFloorId, 16627 name: this.floorData.name, 16628 buildingId: this.floorData.buildingId, 16629 campusId: this.floorData.campusId, 16630 level: this.floorData.level, 16631 isDeleted: false, 16632 isChanged: true, 16633 version: 0 16634 }); 16635 yield this.database.addEntity(this.floorEntity, false); 16636 const floorModel = this.database.Models.get(this.createdFloorId); 16637 if (floorModel) { 16638 this.database.fixModelRelations(floorModel); 16639 } 16640 this.cornerEntities = this.vertices.map((v, i) => Object.assign(new FloorCorner(), { 16641 id: crypto.randomUUID(), 16642 campusId: this.floorData.campusId, 16643 floorId: this.createdFloorId, 16644 locationX: v.x, 16645 locationY: v.y, 16646 position: i, 16647 isDeleted: false, 16648 isChanged: true, 16649 version: 0 16650 })); 16651 this.createdCornerIds = this.cornerEntities.map((c) => c.id); 16652 for (const corner of this.cornerEntities) { 16653 yield this.database.addEntity(corner, false); 16654 } 16655 for (const cornerId of this.createdCornerIds) { 16656 const cornerModel = this.database.Models.get(cornerId); 16657 if (cornerModel) { 16658 this.database.fixModelRelations(cornerModel); 16659 } 16660 } 16661 this.onVisualUpdate(); 16662 if (this.onFloorCreated && this.createdFloorId) { 16663 this.onFloorCreated(this.createdFloorId); 16664 } 16665 }); 16666 } 16667 undo() { 16668 for (const cornerId of this.createdCornerIds) { 16669 const cornerModel = this.database.Models.get(cornerId); 16670 if (cornerModel) { 16671 cornerModel.entity.isDeleted = true; 16672 cornerModel.entity.isChanged = true; 16673 this.database.Models.delete(cornerId); 16674 } 16675 } 16676 if (this.createdFloorId) { 16677 const floorModel = this.database.Models.get(this.createdFloorId); 16678 if (floorModel) { 16679 floorModel.entity.isDeleted = true; 16680 floorModel.entity.isChanged = true; 16681 this.database.Models.delete(this.createdFloorId); 16682 } 16683 } 16684 this.onVisualUpdate(); 16685 } 16686 redo() { 16687 if (!this.floorEntity || this.cornerEntities.length === 0) { 16688 this.execute(); 16689 return; 16690 } 16691 this.floorEntity.isDeleted = false; 16692 this.floorEntity.isChanged = true; 16693 const floorModel = new FloorModel(this.floorEntity); 16694 this.database.Models.set(this.createdFloorId, floorModel); 16695 this.database.fixModelRelations(floorModel); 16696 for (let i = 0; i < this.cornerEntities.length; i++) { 16697 const corner = this.cornerEntities[i]; 16698 const cornerId = this.createdCornerIds[i]; 16699 corner.isDeleted = false; 16700 corner.isChanged = true; 16701 const cornerModel = new FloorCornerModel(corner); 16702 this.database.Models.set(cornerId, cornerModel); 16703 this.database.fixModelRelations(cornerModel); 16704 } 16705 this.onVisualUpdate(); 16706 if (this.onFloorCreated && this.createdFloorId) { 16707 this.onFloorCreated(this.createdFloorId); 16708 } 16709 } 16710}; 16711
16712// src/app/pages/campus-map-v3/commands/create-ground-patch-polygon-command.ts 16713var CreateGroundPatchPolygonCommand = class extends BaseEditorCommand { 16714 constructor(config) { 16715 super(); 16716 this.description = "Create ground patch polygon"; 16717 this.createdGroundPatchId = null; 16718 this.createdCornerIds = []; 16719 this.groundPatchEntity = null; 16720 this.cornerEntities = []; 16721 this.vertices = config.vertices; 16722 this.campusId = config.campusId; 16723 this.database = config.database; 16724 this.onVisualUpdate = config.onVisualUpdate; 16725 this.onGroundPatchCreated = config.onGroundPatchCreated; 16726 } 16727 execute() { 16728 return __async(this, null, function* () { 16729 this.groundPatchEntity = Object.assign(new GroundPatch(), { 16730 id: crypto.randomUUID(), 16731 campusId: this.campusId, 16732 type: GroundPatchType.Grass, 16733 isDeleted: false, 16734 isChanged: true, 16735 version: 0 16736 }); 16737 this.createdGroundPatchId = this.groundPatchEntity.id; 16738 this.cornerEntities = this.vertices.map((v, i) => Object.assign(new GroundPatchCorner(), { 16739 id: crypto.randomUUID(), 16740 groundPatchId: this.createdGroundPatchId, 16741 locationX: v.x, 16742 locationY: v.y, 16743 position: i, 16744 isDeleted: false, 16745 isChanged: true, 16746 version: 0 16747 })); 16748 this.createdCornerIds = this.cornerEntities.map((c) => c.id); 16749 const groundPatchModel = yield this.database.addEntity(this.groundPatchEntity, false); 16750 for (const corner of this.cornerEntities) { 16751 yield this.database.addEntity(corner, false); 16752 } 16753 this.database.fixModelRelations(groundPatchModel); 16754 for (const cornerId of this.createdCornerIds) { 16755 const cornerModel = this.database.Models.get(cornerId); 16756 if (cornerModel) { 16757 this.database.fixModelRelations(cornerModel); 16758 } 16759 } 16760 this.onVisualUpdate(); 16761 if (this.onGroundPatchCreated && this.createdGroundPatchId) { 16762 this.onGroundPatchCreated(this.createdGroundPatchId); 16763 } 16764 }); 16765 } 16766 undo() { 16767 if (!this.createdGroundPatchId) 16768 return; 16769 for (const cornerId of this.createdCornerIds) { 16770 const cornerModel = this.database.Models.get(cornerId); 16771 if (cornerModel) { 16772 cornerModel.entity.isDeleted = true; 16773 cornerModel.entity.isChanged = true; 16774 this.database.Models.delete(cornerId); 16775 } 16776 } 16777 const groundPatchModel = this.database.Models.get(this.createdGroundPatchId); 16778 if (groundPatchModel) { 16779 groundPatchModel.entity.isDeleted = true; 16780 groundPatchModel.entity.isChanged = true; 16781 this.database.Models.delete(this.createdGroundPatchId); 16782 } 16783 this.onVisualUpdate(); 16784 } 16785 redo() { 16786 if (!this.createdGroundPatchId || !this.groundPatchEntity) { 16787 this.execute(); 16788 return; 16789 } 16790 this.groundPatchEntity.isDeleted = false; 16791 this.groundPatchEntity.isChanged = true; 16792 const groundPatchModel = new GroundPatchModel(this.groundPatchEntity); 16793 this.database.Models.set(this.createdGroundPatchId, groundPatchModel); 16794 for (let i = 0; i < this.cornerEntities.length; i++) { 16795 const corner = this.cornerEntities[i]; 16796 const cornerId = this.createdCornerIds[i]; 16797 corner.isDeleted = false; 16798 corner.isChanged = true; 16799 const cornerModel = new GroundPatchCornerModel(corner); 16800 this.database.Models.set(cornerId, cornerModel); 16801 } 16802 this.database.fixModelRelations(groundPatchModel); 16803 for (const cornerId of this.createdCornerIds) { 16804 const cornerModel = this.database.Models.get(cornerId); 16805 if (cornerModel) { 16806 this.database.fixModelRelations(cornerModel); 16807 } 16808 } 16809 this.onVisualUpdate(); 16810 if (this.onGroundPatchCreated) { 16811 this.onGroundPatchCreated(this.createdGroundPatchId); 16812 } 16813 } 16814}; 16815
16816// src/app/pages/campus-map-v3/components/node-edit-panel/node-edit-panel.component.ts 16817var _forTrack04 = ($index, $item) => $item.type; 16818var _forTrack1 = ($index, $item) => $item.entity.id; 16819function NodeEditPanelComponent_For_6_Template(rf, ctx) { 16820 if (rf & 1) { 16821 const _r1 = \u0275\u0275getCurrentView(); 16822 \u0275\u0275elementStart(0, "button", 25); 16823 \u0275\u0275listener("click", function NodeEditPanelComponent_For_6_Template_button_click_0_listener() { 16824 const item_r2 = \u0275\u0275restoreView(_r1).$implicit; 16825 const ctx_r2 = \u0275\u0275nextContext(); 16826 return \u0275\u0275resetView(ctx_r2.onTypeChange(item_r2.type)); 16827 }); 16828 \u0275\u0275element(1, "ng-icon", 26); 16829 \u0275\u0275elementStart(2, "span", 27); 16830 \u0275\u0275text(3); 16831 \u0275\u0275elementEnd()(); 16832 } 16833 if (rf & 2) { 16834 const item_r2 = ctx.$implicit; 16835 const ctx_r2 = \u0275\u0275nextContext(); 16836 \u0275\u0275classProp("active", ctx_r2.nodeType === item_r2.type); 16837 \u0275\u0275property("title", item_r2.label); 16838 \u0275\u0275advance(); 16839 \u0275\u0275property("name", item_r2.icon); 16840 \u0275\u0275advance(2); 16841 \u0275\u0275textInterpolate(item_r2.label); 16842 } 16843} 16844function NodeEditPanelComponent_For_27_Template(rf, ctx) { 16845 if (rf & 1) { 16846 \u0275\u0275elementStart(0, "option", 15); 16847 \u0275\u0275text(1); 16848 \u0275\u0275elementEnd(); 16849 } 16850 if (rf & 2) { 16851 const building_r4 = ctx.$implicit; 16852 \u0275\u0275property("ngValue", building_r4.entity.id); 16853 \u0275\u0275advance(); 16854 \u0275\u0275textInterpolate(building_r4.entity.name || "Unnamed Building"); 16855 } 16856} 16857function NodeEditPanelComponent_Conditional_39_Conditional_16_Template(rf, ctx) { 16858 if (rf & 1) { 16859 \u0275\u0275elementStart(0, "span", 36); 16860 \u0275\u0275text(1); 16861 \u0275\u0275elementEnd(); 16862 } 16863 if (rf & 2) { 16864 const ctx_r2 = \u0275\u0275nextContext(2); 16865 \u0275\u0275advance(); 16866 \u0275\u0275textInterpolate(ctx_r2.latitudeError); 16867 } 16868} 16869function NodeEditPanelComponent_Conditional_39_Conditional_21_Template(rf, ctx) { 16870 if (rf & 1) { 16871 \u0275\u0275elementStart(0, "span", 36); 16872 \u0275\u0275text(1); 16873 \u0275\u0275elementEnd(); 16874 } 16875 if (rf & 2) { 16876 const ctx_r2 = \u0275\u0275nextContext(2); 16877 \u0275\u0275advance(); 16878 \u0275\u0275textInterpolate(ctx_r2.longitudeError); 16879 } 16880} 16881function NodeEditPanelComponent_Conditional_39_Conditional_26_Template(rf, ctx) { 16882 if (rf & 1) { 16883 \u0275\u0275elementStart(0, "span", 36); 16884 \u0275\u0275text(1); 16885 \u0275\u0275elementEnd(); 16886 } 16887 if (rf & 2) { 16888 const ctx_r2 = \u0275\u0275nextContext(2); 16889 \u0275\u0275advance(); 16890 \u0275\u0275textInterpolate(ctx_r2.altitudeError); 16891 } 16892} 16893function NodeEditPanelComponent_Conditional_39_Conditional_30_Template(rf, ctx) { 16894 if (rf & 1) { 16895 \u0275\u0275elementStart(0, "span", 41); 16896 \u0275\u0275text(1); 16897 \u0275\u0275elementEnd(); 16898 } 16899 if (rf & 2) { 16900 const ctx_r2 = \u0275\u0275nextContext(2); 16901 \u0275\u0275advance(); 16902 \u0275\u0275textInterpolate(ctx_r2.gpsError); 16903 } 16904} 16905function NodeEditPanelComponent_Conditional_39_Template(rf, ctx) { 16906 if (rf & 1) { 16907 const _r5 = \u0275\u0275getCurrentView(); 16908 \u0275\u0275elementStart(0, "app-entity-section", 28); 16909 \u0275\u0275element(1, "ng-icon", 29); 16910 \u0275\u0275elementStart(2, "label", 30)(3, "span")(4, "strong"); 16911 \u0275\u0275text(5, "Campus entrance"); 16912 \u0275\u0275elementEnd(); 16913 \u0275\u0275elementStart(6, "small"); 16914 \u0275\u0275text(7, "Allow remote navigation to enter the campus here"); 16915 \u0275\u0275elementEnd()(); 16916 \u0275\u0275elementStart(8, "input", 31); 16917 \u0275\u0275listener("ngModelChange", function NodeEditPanelComponent_Conditional_39_Template_input_ngModelChange_8_listener($event) { 16918 \u0275\u0275restoreView(_r5); 16919 const ctx_r2 = \u0275\u0275nextContext(); 16920 return \u0275\u0275resetView(ctx_r2.onCampusEntranceChange($event)); 16921 }); 16922 \u0275\u0275elementEnd()()(); 16923 \u0275\u0275elementStart(9, "app-entity-section", 32); 16924 \u0275\u0275element(10, "ng-icon", 33); 16925 \u0275\u0275elementStart(11, "div", 34)(12, "div", 9)(13, "label"); 16926 \u0275\u0275text(14, "Lat"); 16927 \u0275\u0275elementEnd(); 16928 \u0275\u0275elementStart(15, "input", 35); 16929 \u0275\u0275listener("ngModelChange", function NodeEditPanelComponent_Conditional_39_Template_input_ngModelChange_15_listener($event) { 16930 \u0275\u0275restoreView(_r5); 16931 const ctx_r2 = \u0275\u0275nextContext();
16932 return \u0275\u0275resetView(ctx_r2.onLatitudeChange($event)); 16933 }); 16934 \u0275\u0275elementEnd(); 16935 \u0275\u0275template(16, NodeEditPanelComponent_Conditional_39_Conditional_16_Template, 2, 1, "span", 36); 16936 \u0275\u0275elementEnd(); 16937 \u0275\u0275elementStart(17, "div", 9)(18, "label"); 16938 \u0275\u0275text(19, "Lng"); 16939 \u0275\u0275elementEnd(); 16940 \u0275\u0275elementStart(20, "input", 37); 16941 \u0275\u0275listener("ngModelChange", function NodeEditPanelComponent_Conditional_39_Template_input_ngModelChange_20_listener($event) { 16942 \u0275\u0275restoreView(_r5); 16943 const ctx_r2 = \u0275\u0275nextContext(); 16944 return \u0275\u0275resetView(ctx_r2.onLongitudeChange($event)); 16945 }); 16946 \u0275\u0275elementEnd(); 16947 \u0275\u0275template(21, NodeEditPanelComponent_Conditional_39_Conditional_21_Template, 2, 1, "span", 36); 16948 \u0275\u0275elementEnd(); 16949 \u0275\u0275elementStart(22, "div", 9)(23, "label"); 16950 \u0275\u0275text(24, "Alt"); 16951 \u0275\u0275elementEnd(); 16952 \u0275\u0275elementStart(25, "input", 38); 16953 \u0275\u0275listener("ngModelChange", function NodeEditPanelComponent_Conditional_39_Template_input_ngModelChange_25_listener($event) { 16954 \u0275\u0275restoreView(_r5); 16955 const ctx_r2 = \u0275\u0275nextContext(); 16956 return \u0275\u0275resetView(ctx_r2.onAltitudeChange($event)); 16957 }); 16958 \u0275\u0275elementEnd(); 16959 \u0275\u0275template(26, NodeEditPanelComponent_Conditional_39_Conditional_26_Template, 2, 1, "span", 36); 16960 \u0275\u0275elementEnd()(); 16961 \u0275\u0275elementStart(27, "button", 39); 16962 \u0275\u0275listener("click", function NodeEditPanelComponent_Conditional_39_Template_button_click_27_listener() { 16963 \u0275\u0275restoreView(_r5); 16964 const ctx_r2 = \u0275\u0275nextContext(); 16965 return \u0275\u0275resetView(ctx_r2.captureGpsFromDevice()); 16966 }); 16967 \u0275\u0275element(28, "ng-icon", 40); 16968 \u0275\u0275text(29); 16969 \u0275\u0275elementEnd(); 16970 \u0275\u0275template(30, NodeEditPanelComponent_Conditional_39_Conditional_30_Template, 2, 1, "span", 41); 16971 \u0275\u0275elementEnd(); 16972 } 16973 if (rf & 2) { 16974 const ctx_r2 = \u0275\u0275nextContext(); 16975 \u0275\u0275advance(8); 16976 \u0275\u0275property("ngModel", ctx_r2.isCampusEntrance); 16977 \u0275\u0275advance(4); 16978 \u0275\u0275classProp("has-error", ctx_r2.latitudeError); 16979 \u0275\u0275advance(3); 16980 \u0275\u0275property("ngModel", ctx_r2.latitude); 16981 \u0275\u0275advance(); 16982 \u0275\u0275conditional(ctx_r2.latitudeError ? 16 : -1); 16983 \u0275\u0275advance(); 16984 \u0275\u0275classProp("has-error", ctx_r2.longitudeError); 16985 \u0275\u0275advance(3); 16986 \u0275\u0275property("ngModel", ctx_r2.longitude); 16987 \u0275\u0275advance(); 16988 \u0275\u0275conditional(ctx_r2.longitudeError ? 21 : -1); 16989 \u0275\u0275advance(); 16990 \u0275\u0275classProp("has-error", ctx_r2.altitudeError); 16991 \u0275\u0275advance(3); 16992 \u0275\u0275property("ngModel", ctx_r2.altitude); 16993 \u0275\u0275advance(); 16994 \u0275\u0275conditional(ctx_r2.altitudeError ? 26 : -1); 16995 \u0275\u0275advance(); 16996 \u0275\u0275property("disabled", ctx_r2.isCapturing); 16997 \u0275\u0275advance(2); 16998 \u0275\u0275textInterpolate1(" ", ctx_r2.isCapturing ? "Capturing..." : "Capture from Device", " "); 16999 \u0275\u0275advance(); 17000 \u0275\u0275conditional(ctx_r2.gpsError ? 30 : -1); 17001 } 17002} 17003var _NodeEditPanelComponent = class _NodeEditPanelComponent { 17004 constructor() { 17005 this.node = null; 17006 this.floorName = ""; 17007 this.isNew = false; 17008 this.buildings = []; 17009 this.nodeChanged = new EventEmitter(); 17010 this.applyRequested = new EventEmitter(); 17011 this.cancelRequested = new EventEmitter(); 17012 this.closeRequested = new EventEmitter(); 17013 this.locationX = 0; 17014 this.locationY = 0; 17015 this.nodeType = NodeType.Regular; 17016 this.tagName = ""; 17017 this.tagDeviceId = ""; 17018 this.latitude = null; 17019 this.longitude = null; 17020 this.altitude = null; 17021 this.buildingId = null; 17022 this.isCampusEntrance = false; 17023 this.isCapturing = false; 17024 this.gpsError = null; 17025 this.nodeTypes = [ 17026 { type: NodeType.Regular, label: "Regular", icon: "lucideMapPin" }, 17027 { type: NodeType.Door, label: "Door", icon: "lucideDoorOpen" }, 17028 { type: NodeType.Stairs, label: "Stairs", icon: "lucideArrowUpFromLine" }, 17029 { type: NodeType.Elevator, label: "Elevator", icon: "lucideArrowDownUp" }, 17030 { type: NodeType.Outdoor, label: "Outdoor", icon: "lucideTrees" } 17031 ]; 17032 } 17033 get nodeId() { 17034 var _a3, _b2; 17035 return (_b2 = (_a3 = this.node) == null ? void 0 : _a3.entity.id) != null ? _b2 : ""; 17036 } 17037 get isOutdoor() { 17038 return this.nodeType === NodeType.Outdoor; 17039 } 17040 get panelTitle() { 17041 return this.isNew ? "New Node" : "Edit Node"; 17042 } 17043 get buildingName() { 17044 var _a3, _b2; 17045 if (!this.buildingId) 17046 return "No building"; 17047 return (_b2 = (_a3 = this.buildings.find((building) => building.entity.id === this.buildingId)) == null ? void 0 : _a3.entity.name) != null ? _b2 : "Unknown Bu
17047ilding"; 17048 } 17049 // GPS validation getters 17050 get latitudeError() { 17051 if (this.latitude === null || this.latitude === void 0) 17052 return null; 17053 return this.latitude < -90 || this.latitude > 90 ? "Must be -90 to 90" : null; 17054 } 17055 get longitudeError() { 17056 if (this.longitude === null || this.longitude === void 0) 17057 return null; 17058 return this.longitude < -180 || this.longitude > 180 ? "Must be -180 to 180" : null; 17059 } 17060 get altitudeError() { 17061 if (this.altitude === null || this.altitude === void 0) 17062 return null; 17063 return this.altitude < -500 || this.altitude > 1e4 ? "Must be -500 to 10000m" : null; 17064 } 17065 ngOnChanges(changes) { 17066 if (changes["node"] && this.node) { 17067 this.syncFromNode(); 17068 } 17069 } 17070 syncFromNode() { 17071 var _a3, _b2, _c, _d, _e, _f, _g, _h, _i; 17072 if (!this.node) 17073 return; 17074 const entity = this.node.entity; 17075 this.locationX = Math.round((_a3 = entity.locationX) != null ? _a3 : 0); 17076 this.locationY = Math.round((_b2 = entity.locationY) != null ? _b2 : 0); 17077 this.nodeType = (_c = entity.type) != null ? _c : NodeType.Regular; 17078 this.tagName = (_d = entity.tagName) != null ? _d : ""; 17079 this.tagDeviceId = (_e = entity.tagDeviceId) != null ? _e : ""; 17080 this.latitude = (_f = entity.latitude) != null ? _f : null; 17081 this.longitude = (_g = entity.longitude) != null ? _g : null; 17082 this.altitude = (_h = entity.altitude) != null ? _h : null; 17083 this.buildingId = (_i = entity.buildingId) != null ? _i : null; 17084 this.isCampusEntrance = entity.isCampusEntrance === true; 17085 } 17086 onLocationXChange(value) { 17087 this.locationX = value; 17088 this.emitChange({ locationX: value }); 17089 } 17090 onLocationYChange(value) { 17091 this.locationY = value; 17092 this.emitChange({ locationY: value }); 17093 } 17094 onTypeChange(type) { 17095 this.nodeType = type; 17096 if (type !== NodeType.Outdoor && this.isCampusEntrance) { 17097 this.isCampusEntrance = false; 17098 this.emitChange({ type, isCampusEntrance: false }); 17099 return; 17100 } 17101 this.emitChange({ type }); 17102 } 17103 onCampusEntranceChange(value) { 17104 this.isCampusEntrance = value; 17105 this.emitChange({ isCampusEntrance: value }); 17106 } 17107 onTagNameChange(value) { 17108 this.tagName = value; 17109 this.emitChange({ tagName: value || null }); 17110 } 17111 onTagDeviceIdChange(value) { 17112 this.tagDeviceId = value; 17113 this.emitChange({ tagDeviceId: value || null }); 17114 } 17115 onLatitudeChange(value) { 17116 this.latitude = value; 17117 this.emitChange({ latitude: value }); 17118 } 17119 onLongitudeChange(value) { 17120 this.longitude = value; 17121 this.emitChange({ longitude: value }); 17122 } 17123 onAltitudeChange(value) { 17124 this.altitude = value; 17125 this.emitChange({ altitude: value }); 17126 } 17127 onBuildingChange(value) { 17128 this.buildingId = value || null; 17129 this.emitChange({ buildingId: this.buildingId }); 17130 } 17131 captureGpsFromDevice() { 17132 return __async(this, null, function* () { 17133 var _a3; 17134 this.isCapturing = true; 17135 this.gpsError = null; 17136 try { 17137 const permission = yield Geolocation.checkPermissions(); 17138 if (permission.location !== "granted") { 17139 const requested = yield Geolocation.requestPermissions(); 17140 if (requested.location !== "granted") { 17141 this.gpsError = "Location permission denied"; 17142 return; 17143 } 17144 } 17145 const position = yield Geolocation.getCurrentPosition({ 17146 enableHighAccuracy: true, 17147 timeout: 1e4 17148 }); 17149 this.latitude = position.coords.latitude; 17150 this.longitude = position.coords.longitude; 17151 this.altitude = (_a3 = position.coords.altitude) != null ? _a3 : null; 17152 this.emitChange({ 17153 latitude: this.latitude, 17154 longitude: this.longitude, 17155 altitude: this.altitude 17156 }); 17157 } catch (error) { 17158 const message = error instanceof Error ? error.message : "GPS capture failed"; 17159 this.gpsError = message; 17160 } finally { 17161 this.isCapturing = false; 17162 } 17163 }); 17164 } 17165 emitChange(partial) { 17166 this.nodeChanged.emit(partial); 17167 } 17168 onApply() { 17169 this.applyRequested.emit(); 17170 } 17171 onCancel() { 17172 this.cancelRequested.emit(); 17173 } 17174 onClose() { 17175 this.closeRequested.emit(); 17176 } 17177}; 17178_NodeEditPanelComponent.\u0275fac = function NodeEditPanelComponent_Factory(__ngFactoryType__) { 17179 return new (__ngFactoryType__ || _NodeEditPanelComponent)(); 17180}; 17181_NodeEditPanelComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _NodeEditPanelComponent, selectors: [["app-node-edit-panel"]], inputs: { node: "node", floorName: "floorName", isNew: "isNew", buildings: "buildings" }, outputs: { nodeChanged: "nodeChanged", applyRequested: "applyRequested", cancelRequested: "cancelRequested", closeRequested: "closeRequested" }, features: [\u0275\u0275ProvidersFeature([], [ 17182 provideIcons({ 17183 lucideMapPin, 17184 lucideMove, 17185 lucideRadio, 17186 lucideNavigation, 17187 lucideDoorOpen, 17188 lucideArrowUpFromLine, 17189 lucideArrowDownUp, 17190 lucideTrees, 17191 lucideCrosshair, 17192 lucideBuilding2 17193 }) 17194]), \u0275\u0275NgOnChangesFeature], decls: 44, vars: 12, consts: [[3, "closeRequested", "cancelRequested", "applyRequested", "title"], ["panel-content", ""], ["label", "Type"], ["section-icon", "", "name", "lucideMapPin", "size", "16"], [1, "type-grid"], [1, "type-button", 3, "active", "title"], ["label", "Position (cm)"], ["section-icon", "", "name", "lucideMove", "size", "16"], [1, "input-row", "two-cols"], [1, "input-group"], ["type", "number", "step", "10", 3, "ngModelChange", "ngModel"], ["label", "Building"], ["section-icon", "", "name", "lucideBuilding2", "size", "16"], [1, "input-group", "full"], [3, "ngModelChange", "ngModel"], [3, "ngValue"], ["label", "Beacon (optional)"], ["section-icon", "", "name", "lucideRadio", "size", "16"], [1, "input-stack"], ["type", "text", "placeholder", "e.g., Hallway-A", 3, "ngModelChange", "ngModel"], ["type", "text", "placeholder", "e.g., AA:BB:CC:DD:EE:FF", 3, "ngModelChange", "ngModel"], ["panel-info", ""], ["label", "Floor", 3, "value"], ["label", "Building", 3, "value"], ["label", "ID", 3, "value", "mono"], [1, "type-button", 3, "click", "title"], ["size", "18", 3, "name"], [1, "type-label"], ["label", "Campus Entry"], ["section-icon", "", "name", "lucideDoorOpen", "size", "16"], [1, "toggle-row"], ["type", "checkbox", 3, "ngModelChange", "ngModel"], ["label", "GPS Coordinates"], ["section-icon", "", "name", "lucideNavigation", "size", "16"], [1, "input-row", "three-cols"], ["type", "number", "step", "0.0001", "min", "-90", "max", "90", 3, "ngModelChange", "ngModel"], [1, "error-hint"], ["type", "number", "step", "0.0001", "min", "-180", "max", "180", 3, "ngModelChange", "ngModel"], ["type", "number", "step", "0.1", 3, "ngModelChange", "ngModel"], ["type", "button", 1, "capture-button", 3, "click", "disabled"], ["name", "lucideCrosshair", "size", "14"], [1, "gps-error"]], template: function NodeEditPanelComponent_Template(rf, ctx) { 17195 if (rf & 1) { 17196 \u0275\u0275elementStart(0, "app-entity-panel", 0); 17197 \u0275\u0275listener("closeRequested", function NodeEditPanelComponent_Template_app_entity_panel_closeRequested_0_listener() { 17198 return ctx.onClose(); 17199 })("cancelRequested", function NodeEditPanelComponent_Template_app_entity_panel_cancelRequested_0_listener() { 17200 return ctx.onCancel(); 17201 })("applyRequested", function NodeEditPanelComponent_Template_app_entity_panel_applyRequested_0_listener() { 17202 return ctx.onApply(); 17203 }); 17204 \u0275\u0275elementContainerStart(1, 1); 17205 \u0275\u0275elementStart(2, "app-entity-section", 2); 17206 \u0275\u0275element(3, "ng-icon", 3); 17207 \u0275\u0275elementStart(4, "div", 4); 17208 \u0275\u0275repeaterCreate(5, NodeEditPanelComponent_For_6_Template, 4, 5, "button", 5, _forTrack04); 17209 \u0275\u0275elementEnd()(); 17210 \u0275\u0275elementStart(7, "app-entity-section", 6); 17211 \u0275\u0275element(8, "ng-icon", 7); 17212 \u0275\u0275elementStart(9, "div", 8)(10, "div", 9)(11, "label"); 17213 \u0275\u0275text(12, "X"); 17214 \u0275\u0275elementEnd();
17215 \u0275\u0275elementStart(13, "input", 10); 17216 \u0275\u0275listener("ngModelChange", function NodeEditPanelComponent_Template_input_ngModelChange_13_listener($event) { 17217 return ctx.onLocationXChange($event); 17218 }); 17219 \u0275\u0275elementEnd()(); 17220 \u0275\u0275elementStart(14, "div", 9)(15, "label"); 17221 \u0275\u0275text(16, "Y"); 17222 \u0275\u0275elementEnd(); 17223 \u0275\u0275elementStart(17, "input", 10); 17224 \u0275\u0275listener("ngModelChange", function NodeEditPanelComponent_Template_input_ngModelChange_17_listener($event) { 17225 return ctx.onLocationYChange($event); 17226 }); 17227 \u0275\u0275elementEnd()()()(); 17228 \u0275\u0275elementStart(18, "app-entity-section", 11); 17229 \u0275\u0275element(19, "ng-icon", 12); 17230 \u0275\u0275elementStart(20, "div", 13)(21, "label"); 17231 \u0275\u0275text(22, "BUILDING"); 17232 \u0275\u0275elementEnd(); 17233 \u0275\u0275elementStart(23, "select", 14); 17234 \u0275\u0275listener("ngModelChange", function NodeEditPanelComponent_Template_select_ngModelChange_23_listener($event) { 17235 return ctx.onBuildingChange($event); 17236 }); 17237 \u0275\u0275elementStart(24, "option", 15); 17238 \u0275\u0275text(25, "No building"); 17239 \u0275\u0275elementEnd(); 17240 \u0275\u0275repeaterCreate(26, NodeEditPanelComponent_For_27_Template, 2, 2, "option", 15, _forTrack1); 17241 \u0275\u0275elementEnd()()(); 17242 \u0275\u0275elementStart(28, "app-entity-section", 16); 17243 \u0275\u0275element(29, "ng-icon", 17); 17244 \u0275\u0275elementStart(30, "div", 18)(31, "div", 13)(32, "label"); 17245 \u0275\u0275text(33, "Tag Name"); 17246 \u0275\u0275elementEnd(); 17247 \u0275\u0275elementStart(34, "input", 19); 17248 \u0275\u0275listener("ngModelChange", function NodeEditPanelComponent_Template_input_ngModelChange_34_listener($event) { 17249 return ctx.onTagNameChange($event); 17250 }); 17251 \u0275\u0275elementEnd()(); 17252 \u0275\u0275elementStart(35, "div", 13)(36, "label"); 17253 \u0275\u0275text(37, "Device ID"); 17254 \u0275\u0275elementEnd(); 17255 \u0275\u0275elementStart(38, "input", 20); 17256 \u0275\u0275listener("ngModelChange", function NodeEditPanelComponent_Template_input_ngModelChange_38_listener($event) { 17257 return ctx.onTagDeviceIdChange($event); 17258 }); 17259 \u0275\u0275elementEnd()()()(); 17260 \u0275\u0275template(39, NodeEditPanelComponent_Conditional_39_Template, 31, 16); 17261 \u0275\u0275elementContainerEnd(); 17262 \u0275\u0275elementContainerStart(40, 21); 17263 \u0275\u0275element(41, "app-entity-row", 22)(42, "app-entity-row", 23)(43, "app-entity-row", 24); 17264 \u0275\u0275elementContainerEnd(); 17265 \u0275\u0275elementEnd(); 17266 } 17267 if (rf & 2) { 17268 \u0275\u0275property("title", ctx.panelTitle); 17269 \u0275\u0275advance(5); 17270 \u0275\u0275repeater(ctx.nodeTypes); 17271 \u0275\u0275advance(8); 17272 \u0275\u0275property("ngModel", ctx.locationX); 17273 \u0275\u0275advance(4); 17274 \u0275\u0275property("ngModel", ctx.locationY); 17275 \u0275\u0275advance(6); 17276 \u0275\u0275property("ngModel", ctx.buildingId); 17277 \u0275\u0275advance(); 17278 \u0275\u0275property("ngValue", null); 17279 \u0275\u0275advance(2); 17280 \u0275\u0275repeater(ctx.buildings); 17281 \u0275\u0275advance(8); 17282 \u0275\u0275property("ngModel", ctx.tagName); 17283 \u0275\u0275advance(4); 17284 \u0275\u0275property("ngModel", ctx.tagDeviceId); 17285 \u0275\u0275advance(); 17286 \u0275\u0275conditional(ctx.isOutdoor ? 39 : -1); 17287 \u0275\u0275advance(2); 17288 \u0275\u0275property("value", ctx.floorName); 17289 \u0275\u0275advance(); 17290 \u0275\u0275property("value", ctx.buildingName); 17291 \u0275\u0275advance(); 17292 \u0275\u0275property("value", ctx.nodeId)("mono", true); 17293 } 17294}, dependencies: [ 17295 CommonModule, 17296 FormsModule, 17297 NgSelectOption, 17298 \u0275NgSelectMultipleOption, 17299 DefaultValueAccessor, 17300 NumberValueAccessor, 17301 CheckboxControlValueAccessor, 17302 SelectControlValueAccessor, 17303 NgControlStatus, 17304 MinValidator, 17305 MaxValidator, 17306 NgModel, 17307 NgIcon, 17308 EntityPanelComponent, 17309 EntitySectionComponent, 17310 EntityRowComponent 17311], styles: ["\n\n.type-grid[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 6px;\n}\n.type-button[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 4px;\n padding: 8px 4px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n cursor: pointer;\n transition: all 0.2s ease;\n}\n.type-button[_ngcontent-%COMP%]:hover {\n border-color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n}\n.type-button.active[_ngcontent-%COMP%] {\n border-color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n color: var(--ion-color-primary);\n}\n.type-button.active[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n color: var(--ion-color-primary);\n}\n.type-button[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n color: var(--ion-color-medium);\n}\n.type-button[_ngcontent-%COMP%] .type-label[_ngcontent-%COMP%] {\n font-size: 10px;\n font-weight: 500;\n}\n.input-row[_ngcontent-%COMP%] {\n display: grid;\n gap: 8px;\n}\n.input-row.two-cols[_ngcontent-%COMP%] {\n grid-template-columns: repeat(2, 1fr);\n}\n.input-row.three-cols[_ngcontent-%COMP%] {\n grid-template-columns: repeat(3, 1fr);\n}\n.input-stack[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.input-group[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.input-group.full[_ngcontent-%COMP%] {\n width: 100%;\n}\n.input-group[_ngcontent-%COMP%] label[_ngcontent-%COMP%] {\n font-size: 11px;\n font-weight: 500;\n color: var(--ion-color-medium);\n text-transform: uppercase;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%] {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n transition: border-color 0.2s ease;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.input-group[_ngcontent-%COMP%] input[type=number][_ngcontent-%COMP%] {\n font-family: monospace;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::placeholder {\n color: var(--ion-color-medium-tint);\n font-style: italic;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::-webkit-inner-spin-button, \n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::-webkit-outer-spin-button {\n opacity: 1;\n}\n.input-group[_ngcontent-%COMP%] select[_ngcontent-%COMP%] {\n width: 100%;\n min-height: 34px;\n padding: 6px 8px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n font-size: 13px;\n}\n.capture-button[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n width: 100%;\n margin-top: 8px;\n padding: 8px 12px;\n font-size: 12px;\n font-weight: 500;\n color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.08);\n border: 1px solid rgba(var(--ion-color-primary-rgb), 0.2);\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.2s ease;\n}\n.capture-button[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: rgba(var(--ion-color-primary-rgb), 0.15);\n border-color: var(--ion-color-primary);\n}\n.capture-button[_ngcontent-%COMP%]:disabled {\n opacity: 0.6;
17311\n cursor: not-allowed;\n}\n.input-group.has-error[_ngcontent-%COMP%] input[_ngcontent-%COMP%] {\n border-color: var(--ion-color-danger);\n background: rgba(var(--ion-color-danger-rgb), 0.05);\n}\n.error-hint[_ngcontent-%COMP%] {\n font-size: 10px;\n color: var(--ion-color-danger);\n margin-top: 2px;\n}\n.gps-error[_ngcontent-%COMP%] {\n display: block;\n font-size: 11px;\n color: var(--ion-color-danger);\n margin-top: 6px;\n text-align: center;\n}\n.toggle-row[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n cursor: pointer;\n}\n.toggle-row[_ngcontent-%COMP%] span[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n.toggle-row[_ngcontent-%COMP%] strong[_ngcontent-%COMP%] {\n font-size: 12px;\n font-weight: 600;\n}\n.toggle-row[_ngcontent-%COMP%] small[_ngcontent-%COMP%] {\n color: var(--ion-color-medium);\n font-size: 10px;\n line-height: 1.35;\n}\n.toggle-row[_ngcontent-%COMP%] input[_ngcontent-%COMP%] {\n width: 18px;\n height: 18px;\n flex: 0 0 auto;\n accent-color: var(--ion-color-primary);\n}"], changeDetection: 0 }); 17312var NodeEditPanelComponent = _NodeEditPanelComponent; 17313(() => { 17314 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(NodeEditPanelComponent, [{ 17315 type: Component, 17316 args: [{ selector: "app-node-edit-panel", standalone: true, imports: [ 17317 CommonModule, 17318 FormsModule, 17319 NgIcon, 17320 EntityPanelComponent, 17321 EntitySectionComponent, 17322 EntityRowComponent 17323 ], viewProviders: [ 17324 provideIcons({ 17325 lucideMapPin, 17326 lucideMove, 17327 lucideRadio, 17328 lucideNavigation, 17329 lucideDoorOpen, 17330 lucideArrowUpFromLine, 17331 lucideArrowDownUp, 17332 lucideTrees, 17333 lucideCrosshair, 17334 lucideBuilding2 17335 }) 17336 ], changeDetection: ChangeDetectionStrategy.OnPush, template: `<app-entity-panel 17337 [title]="panelTitle" 17338 (closeRequested)="onClose()" 17339 (cancelRequested)="onCancel()" 17340 (applyRequested)="onApply()"> 17341 17342 <ng-container panel-content> 17343 <!-- Type Section --> 17344 <app-entity-section label="Type"> 17345 <ng-icon section-icon name="lucideMapPin" size="16" /> 17346 <div class="type-grid"> 17347 @for (item of nodeTypes; track item.type) { 17348 <button 17349 class="type-button" 17350 [class.active]="nodeType === item.type" 17351 [title]="item.label" 17352 (click)="onTypeChange(item.type)" 17353 > 17354 <ng-icon [name]="item.icon" size="18" /> 17355 <span class="type-label">{{ item.label }}</span> 17356 </button> 17357 } 17358 </div> 17359 </app-entity-section> 17360 17361 <!-- Position Section --> 17362 <app-entity-section label="Position (cm)"> 17363 <ng-icon section-icon name="lucideMove" size="16" /> 17364 <div class="input-row two-cols"> 17365 <div class="input-group"> 17366 <label>X</label> 17367 <input 17368 type="number" 17369 [ngModel]="locationX" 17370 (ngModelChange)="onLocationXChange($event)" 17371 step="10" 17372 /> 17373 </div> 17374 <div class="input-group"> 17375 <label>Y</label> 17376 <input 17377 type="number" 17378 [ngModel]="locationY" 17379 (ngModelChange)="onLocationYChange($event)" 17380 step="10" 17381 /> 17382 </div> 17383 </div> 17384 </app-entity-section> 17385 17386 <!-- Building Section --> 17387 <app-entity-section label="Building"> 17388 <ng-icon section-icon name="lucideBuilding2" size="16" /> 17389 <div class="input-group full"> 17390 <label>BUILDING</label> 17391 <select 17392 [ngModel]="buildingId" 17393 (ngModelChange)="onBuildingChange($event)" 17394 > 17395 <option [ngValue]="null">No building</option> 17396 @for (building of buildings; track building.entity.id) { 17397 <option [ngValue]="building.entity.id">{{ building.entity.name || 'Unnamed Building' }}</option> 17398 } 17399 </select> 17400 </div> 17401 </app-entity-section> 17402 17403 <!-- Beacon Section --> 17404 <app-entity-section label="Beacon (optional)"> 17405 <ng-icon section-icon name="lucideRadio" size="16" /> 17406 <div class="input-stack"> 17407 <div class="input-group full"> 17408 <label>Tag Name</label> 17409 <input 17410 type="text" 17411 [ngModel]="tagName" 17412 (ngModelChange)="onTagNameChange($event)" 17413 placeholder="e.g., Hallway-A" 17414 /> 17415 </div> 17416 <div class="input-group full"> 17417 <label>Device ID</label> 17418 <input 17419 type="text" 17420 [ngModel]="tagDeviceId" 17421 (ngModelChange)="onTagDeviceIdChange($event)" 17422 placeholder="e.g., AA:BB:CC:DD:EE:FF" 17423 /> 17424 </div> 17425 </div> 17426 </app-entity-section> 17427 17428 <!-- GPS Section (Outdoor only) --> 17429 @if (isOutdoor) { 17430 <app-entity-section label="Campus Entry"> 17431 <ng-icon section-icon name="lucideDoorOpen" size="16" /> 17432 <label class="toggle-row">
17433 <span> 17434 <strong>Campus entrance</strong> 17435 <small>Allow remote navigation to enter the campus here</small> 17436 </span> 17437 <input 17438 type="checkbox" 17439 [ngModel]="isCampusEntrance" 17440 (ngModelChange)="onCampusEntranceChange($event)" 17441 /> 17442 </label> 17443 </app-entity-section> 17444 17445 <app-entity-section label="GPS Coordinates"> 17446 <ng-icon section-icon name="lucideNavigation" size="16" /> 17447 <div class="input-row three-cols"> 17448 <div class="input-group" [class.has-error]="latitudeError"> 17449 <label>Lat</label> 17450 <input 17451 type="number" 17452 [ngModel]="latitude" 17453 (ngModelChange)="onLatitudeChange($event)" 17454 step="0.0001" 17455 min="-90" 17456 max="90" 17457 /> 17458 @if (latitudeError) { 17459 <span class="error-hint">{{ latitudeError }}</span> 17460 } 17461 </div> 17462 <div class="input-group" [class.has-error]="longitudeError"> 17463 <label>Lng</label> 17464 <input 17465 type="number" 17466 [ngModel]="longitude" 17467 (ngModelChange)="onLongitudeChange($event)" 17468 step="0.0001" 17469 min="-180" 17470 max="180" 17471 /> 17472 @if (longitudeError) { 17473 <span class="error-hint">{{ longitudeError }}</span> 17474 } 17475 </div> 17476 <div class="input-group" [class.has-error]="altitudeError"> 17477 <label>Alt</label> 17478 <input 17479 type="number" 17480 [ngModel]="altitude" 17481 (ngModelChange)="onAltitudeChange($event)" 17482 step="0.1" 17483 /> 17484 @if (altitudeError) { 17485 <span class="error-hint">{{ altitudeError }}</span> 17486 } 17487 </div> 17488 </div> 17489 <button 17490 type="button" 17491 class="capture-button" 17492 [disabled]="isCapturing" 17493 (click)="captureGpsFromDevice()" 17494 > 17495 <ng-icon name="lucideCrosshair" size="14" /> 17496 {{ isCapturing ? 'Capturing...' : 'Capture from Device' }} 17497 </button> 17498 @if (gpsError) { 17499 <span class="gps-error">{{ gpsError }}</span> 17500 } 17501 </app-entity-section> 17502 } 17503 </ng-container> 17504 17505 <ng-container panel-info> 17506 <app-entity-row label="Floor" [value]="floorName" /> 17507 <app-entity-row label="Building" [value]="buildingName" /> 17508 <app-entity-row label="ID" [value]="nodeId" [mono]="true" /> 17509 </ng-container> 17510 17511</app-entity-panel> 17512`, styles: ["/* src/app/pages/campus-map-v3/components/node-edit-panel/node-edit-panel.component.scss */\n.type-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 6px;\n}\n.type-button {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 4px;\n padding: 8px 4px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n cursor: pointer;\n transition: all 0.2s ease;\n}\n.type-button:hover {\n border-color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n}\n.type-button.active {\n border-color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n color: var(--ion-color-primary);\n}\n.type-button.active ng-icon {\n color: var(--ion-color-primary);\n}\n.type-button ng-icon {\n color: var(--ion-color-medium);\n}\n.type-button .type-label {\n font-size: 10px;\n font-weight: 500;\n}\n.input-row {\n display: grid;\n gap: 8px;\n}\n.input-row.two-cols {\n grid-template-columns: repeat(2, 1fr);\n}\n.input-row.three-cols {\n grid-template-columns: repeat(3, 1fr);\n}\n.input-stack {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.input-group {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.input-group.full {\n width: 100%;\n}\n.input-group label {\n font-size: 11px;\n font-weight: 500;\n color: var(--ion-color-medium);\n text-transform: uppercase;\n}\n.input-group input {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n transition: border-color 0.2s ease;\n}\n.input-group input:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.input-group input[type=number] {\n font-family: monospace;\n}\n.input-group input::placeholder {\n color: var(--ion-color-medium-tint);\n font-style: italic;\n}\n.input-group input::-webkit-inner-spin-button,\n.input-group input::-webkit-outer-spin-button {\n opacity: 1;\n}\n.input-group select {\n width: 100%;\n min-height: 34px;\n padding: 6px 8px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n font-size: 13px;\n}\n.capture-button {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n width: 100%;\n margin-top: 8px;\n padding: 8px 12px;\n font-size: 12px;\n font-weight: 500;\n color: var(--ion-color-primary);\n background: rgba(var(--ion-color-primary-rgb), 0.08);\n border: 1px solid rgba(var(--ion-color-primary-rgb), 0.2);\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.2s ease;\n}\n.capture-button:hover:not(:disabled) {\n background: rgba(var(--ion-color-primary-rgb), 0.15);\n border-color: var(--ion-color-primary);\n}\n.capture-button:disabled {\n opacity: 0.6;
17512\n cursor: not-allowed;\n}\n.input-group.has-error input {\n border-color: var(--ion-color-danger);\n background: rgba(var(--ion-color-danger-rgb), 0.05);\n}\n.error-hint {\n font-size: 10px;\n color: var(--ion-color-danger);\n margin-top: 2px;\n}\n.gps-error {\n display: block;\n font-size: 11px;\n color: var(--ion-color-danger);\n margin-top: 6px;\n text-align: center;\n}\n.toggle-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n cursor: pointer;\n}\n.toggle-row span {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n.toggle-row strong {\n font-size: 12px;\n font-weight: 600;\n}\n.toggle-row small {\n color: var(--ion-color-medium);\n font-size: 10px;\n line-height: 1.35;\n}\n.toggle-row input {\n width: 18px;\n height: 18px;\n flex: 0 0 auto;\n accent-color: var(--ion-color-primary);\n}\n"] }] 17513 }], null, { node: [{ 17514 type: Input 17515 }], floorName: [{ 17516 type: Input 17517 }], isNew: [{ 17518 type: Input 17519 }], buildings: [{ 17520 type: Input 17521 }], nodeChanged: [{ 17522 type: Output 17523 }], applyRequested: [{ 17524 type: Output 17525 }], cancelRequested: [{ 17526 type: Output 17527 }], closeRequested: [{ 17528 type: Output 17529 }] }); 17530})(); 17531(() => { 17532 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(NodeEditPanelComponent, { className: "NodeEditPanelComponent", filePath: "src/app/pages/campus-map-v3/components/node-edit-panel/node-edit-panel.component.ts", lineNumber: 75 }); 17533})(); 17534
17535// src/app/pages/campus-map-v3/components/floor-edit-panel/floor-edit-panel.component.ts 17536function FloorEditPanelComponent_Conditional_20_Template(rf, ctx) { 17537 if (rf & 1) { 17538 \u0275\u0275text(0); 17539 } 17540 if (rf & 2) { 17541 const ctx_r0 = \u0275\u0275nextContext(); 17542 \u0275\u0275textInterpolate1(" The floor has ", ctx_r0.cornerCount, " outline corners. "); 17543 } 17544} 17545function FloorEditPanelComponent_Conditional_21_Template(rf, ctx) { 17546 if (rf & 1) { 17547 \u0275\u0275text(0, " No outline defined. Draw an outline to enable dimensions. "); 17548 } 17549} 17550function FloorEditPanelComponent_Conditional_23_Template(rf, ctx) { 17551 if (rf & 1) { 17552 const _r2 = \u0275\u0275getCurrentView(); 17553 \u0275\u0275elementStart(0, "button", 26); 17554 \u0275\u0275listener("click", function FloorEditPanelComponent_Conditional_23_Template_button_click_0_listener() { 17555 \u0275\u0275restoreView(_r2); 17556 const ctx_r0 = \u0275\u0275nextContext(); 17557 return \u0275\u0275resetView(ctx_r0.onEditOutline()); 17558 }); 17559 \u0275\u0275element(1, "ng-icon", 27); 17560 \u0275\u0275text(2, " Edit Outline "); 17561 \u0275\u0275elementEnd(); 17562 \u0275\u0275elementStart(3, "button", 28); 17563 \u0275\u0275listener("click", function FloorEditPanelComponent_Conditional_23_Template_button_click_3_listener() { 17564 \u0275\u0275restoreView(_r2); 17565 const ctx_r0 = \u0275\u0275nextContext(); 17566 return \u0275\u0275resetView(ctx_r0.onDrawOutline()); 17567 }); 17568 \u0275\u0275element(4, "ng-icon", 29); 17569 \u0275\u0275text(5, " Redraw "); 17570 \u0275\u0275elementEnd(); 17571 } 17572} 17573function FloorEditPanelComponent_Conditional_24_Template(rf, ctx) { 17574 if (rf & 1) { 17575 const _r3 = \u0275\u0275getCurrentView(); 17576 \u0275\u0275elementStart(0, "button", 30); 17577 \u0275\u0275listener("click", function FloorEditPanelComponent_Conditional_24_Template_button_click_0_listener() { 17578 \u0275\u0275restoreView(_r3); 17579 const ctx_r0 = \u0275\u0275nextContext(); 17580 return \u0275\u0275resetView(ctx_r0.onDrawOutline()); 17581 }); 17582 \u0275\u0275element(1, "ng-icon", 29); 17583 \u0275\u0275text(2, " Draw Outline "); 17584 \u0275\u0275elementEnd(); 17585 } 17586} 17587function FloorEditPanelComponent_Conditional_25_Template(rf, ctx) { 17588 if (rf & 1) { 17589 \u0275\u0275elementStart(0, "app-entity-section", 17); 17590 \u0275\u0275element(1, "ng-icon", 31); 17591 \u0275\u0275elementStart(2, "div", 32)(3, "div", 33)(4, "span", 34); 17592 \u0275\u0275text(5, "Rooms"); 17593 \u0275\u0275elementEnd(); 17594 \u0275\u0275elementStart(6, "span", 35); 17595 \u0275\u0275text(7); 17596 \u0275\u0275elementEnd()(); 17597 \u0275\u0275elementStart(8, "div", 33)(9, "span", 34); 17598 \u0275\u0275text(10, "Nodes"); 17599 \u0275\u0275elementEnd(); 17600 \u0275\u0275elementStart(11, "span", 35); 17601 \u0275\u0275text(12); 17602 \u0275\u0275elementEnd()(); 17603 \u0275\u0275elementStart(13, "div", 33)(14, "span", 34); 17604 \u0275\u0275text(15, "Connections"); 17605 \u0275\u0275elementEnd(); 17606 \u0275\u0275elementStart(16, "span", 35); 17607 \u0275\u0275text(17); 17608 \u0275\u0275elementEnd()(); 17609 \u0275\u0275elementStart(18, "div", 33)(19, "span", 34); 17610 \u0275\u0275text(20, "Corners"); 17611 \u0275\u0275elementEnd(); 17612 \u0275\u0275elementStart(21, "span", 35); 17613 \u0275\u0275text(22); 17614 \u0275\u0275elementEnd()()()(); 17615 } 17616 if (rf & 2) { 17617 const ctx_r0 = \u0275\u0275nextContext(); 17618 \u0275\u0275advance(7); 17619 \u0275\u0275textInterpolate(ctx_r0.roomCount); 17620 \u0275\u0275advance(5); 17621 \u0275\u0275textInterpolate(ctx_r0.nodeCount); 17622 \u0275\u0275advance(5);
17623 \u0275\u0275textInterpolate(ctx_r0.connectionCount); 17624 \u0275\u0275advance(5); 17625 \u0275\u0275textInterpolate(ctx_r0.cornerCount); 17626 } 17627} 17628function FloorEditPanelComponent_Conditional_26_Template(rf, ctx) { 17629 if (rf & 1) { 17630 \u0275\u0275elementStart(0, "app-entity-section", 18); 17631 \u0275\u0275element(1, "ng-icon", 36); 17632 \u0275\u0275elementStart(2, "div", 37)(3, "div", 38)(4, "span", 39); 17633 \u0275\u0275text(5, "Width"); 17634 \u0275\u0275elementEnd(); 17635 \u0275\u0275elementStart(6, "span", 40); 17636 \u0275\u0275text(7); 17637 \u0275\u0275elementEnd()(); 17638 \u0275\u0275elementStart(8, "div", 38)(9, "span", 39); 17639 \u0275\u0275text(10, "Height"); 17640 \u0275\u0275elementEnd(); 17641 \u0275\u0275elementStart(11, "span", 40); 17642 \u0275\u0275text(12); 17643 \u0275\u0275elementEnd()()(); 17644 \u0275\u0275elementStart(13, "div", 41)(14, "span", 42); 17645 \u0275\u0275text(15, "Area"); 17646 \u0275\u0275elementEnd(); 17647 \u0275\u0275elementStart(16, "span", 43); 17648 \u0275\u0275text(17); 17649 \u0275\u0275pipe(18, "number"); 17650 \u0275\u0275elementEnd()()(); 17651 } 17652 if (rf & 2) { 17653 const ctx_r0 = \u0275\u0275nextContext(); 17654 \u0275\u0275advance(7); 17655 \u0275\u0275textInterpolate1("", ctx_r0.dimensions.width, " m"); 17656 \u0275\u0275advance(5); 17657 \u0275\u0275textInterpolate1("", ctx_r0.dimensions.height, " m"); 17658 \u0275\u0275advance(5); 17659 \u0275\u0275textInterpolate1("~", \u0275\u0275pipeBind1(18, 3, ctx_r0.dimensions.area), " m\xB2"); 17660 } 17661} 17662function FloorEditPanelComponent_Conditional_32_Template(rf, ctx) { 17663 if (rf & 1) { 17664 \u0275\u0275element(0, "app-entity-row", 24); 17665 } 17666 if (rf & 2) { 17667 const ctx_r0 = \u0275\u0275nextContext(); 17668 \u0275\u0275property("value", ctx_r0.floorId)("mono", true); 17669 } 17670} 17671var _FloorEditPanelComponent = class _FloorEditPanelComponent { 17672 constructor() { 17673 this.floor = null; 17674 this.isNew = false; 17675 this.floorChanged = new EventEmitter(); 17676 this.applyRequested = new EventEmitter(); 17677 this.cancelRequested = new EventEmitter(); 17678 this.closeRequested = new EventEmitter(); 17679 this.drawOutlineRequested = new EventEmitter(); 17680 this.editOutlineRequested = new EventEmitter(); 17681 this.name = null; 17682 this.level = null; 17683 } 17684 // Dynamic panel text based on create/edit mode 17685 get panelTitle() { 17686 return this.isNew ? "New Floor" : "Edit Floor"; 17687 } 17688 get applyButtonText() { 17689 return this.isNew ? "Create" : "Apply"; 17690 } 17691 ngOnChanges(changes) { 17692 if (changes["floor"] && this.floor) { 17693 this.syncFromFloor(); 17694 } 17695 } 17696 /** 17697 * Sync local form values from floor model. 17698 */ 17699 syncFromFloor() { 17700 var _a3, _b2; 17701 if (!this.floor) 17702 return; 17703 this.name = (_a3 = this.floor.entity.name) != null ? _a3 : null; 17704 this.level = (_b2 = this.floor.entity.level) != null ? _b2 : null; 17705 } 17706 // ========================================================================= 17707 // Computed Read-Only Statistics 17708 // ========================================================================= 17709 get roomCount() { 17710 var _a3, _b2, _c; 17711 return (_c = (_b2 = (_a3 = this.floor) == null ? void 0 : _a3.rooms) == null ? void 0 : _b2.length) != null ? _c : 0; 17712 } 17713 get nodeCount() { 17714 var _a3, _b2, _c; 17715 return (_c = (_b2 = (_a3 = this.floor) == null ? void 0 : _a3.nodes) == null ? void 0 : _b2.length) != null ? _c : 0; 17716 } 17717 get connectionCount() { 17718 var _a3, _b2, _c; 17719 return (_c = (_b2 = (_a3 = this.floor) == null ? void 0 : _a3.nodeConnections) == null ? void 0 : _b2.length) != null ? _c : 0; 17720 } 17721 get cornerCount() { 17722 var _a3, _b2, _c, _d; 17723 return (_d = (_c = (_b2 = (_a3 = this.floor) == null ? void 0 : _a3.corners) == null ? void 0 : _b2.filter((c) => !c.entity.isDeleted)) == null ? void 0 : _c.length) != null ? _d : 0; 17724 } 17725 /** 17726 * Whether the floor has an existing outline (at least 3 corners). 17727 */ 17728 get hasOutline() { 17729 return this.cornerCount >= 3; 17730 } 17731 /** 17732 * Calculate floor dimensions from outline bounds. 17733 * Returns width, height in meters and area in m². 17734 */ 17735 get dimensions() { 17736 if (!this.floor) 17737 return null; 17738 const bounds = this.floor.getOutlineBounds(); 17739 if (!bounds) 17740 return null; 17741 const widthM = bounds.width / 100; 17742 const heightM = bounds.height / 100; 17743 const areaM2 = widthM * heightM; 17744 return { 17745 width: Math.round(widthM * 10) / 10, 17746 height: Math.round(heightM * 10) / 10, 17747 area: Math.round(areaM2) 17748 }; 17749 } 17750 get buildingName() { 17751 var _a3, _b2, _c; 17752 return (_c = (_b2 = (_a3 = this.floor) == null ? void 0 : _a3.building) == null ? void 0 : _b2.entity.name) != null ? _c : "Unknown Bu
17752ilding"; 17753 } 17754 get campusName() { 17755 var _a3, _b2, _c; 17756 return (_c = (_b2 = (_a3 = this.floor) == null ? void 0 : _a3.campus) == null ? void 0 : _b2.entity.name) != null ? _c : "Unknown Campus"; 17757 } 17758 get floorId() { 17759 var _a3, _b2; 17760 return (_b2 = (_a3 = this.floor) == null ? void 0 : _a3.entity.id) != null ? _b2 : ""; 17761 } 17762 // ========================================================================= 17763 // Event Handlers 17764 // ========================================================================= 17765 onNameChange(value) { 17766 this.name = value || null; 17767 this.floorChanged.emit({ name: this.name }); 17768 } 17769 onLevelChange(value) { 17770 this.level = value; 17771 this.floorChanged.emit({ level: this.level }); 17772 } 17773 onApply() { 17774 this.applyRequested.emit(); 17775 } 17776 onCancel() { 17777 this.cancelRequested.emit(); 17778 } 17779 onClose() { 17780 this.closeRequested.emit(); 17781 } 17782 onDrawOutline() { 17783 this.drawOutlineRequested.emit(); 17784 } 17785 onEditOutline() { 17786 this.editOutlineRequested.emit(); 17787 } 17788}; 17789_FloorEditPanelComponent.\u0275fac = function FloorEditPanelComponent_Factory(__ngFactoryType__) { 17790 return new (__ngFactoryType__ || _FloorEditPanelComponent)(); 17791}; 17792_FloorEditPanelComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _FloorEditPanelComponent, selectors: [["app-floor-edit-panel"]], inputs: { floor: "floor", isNew: "isNew" }, outputs: { floorChanged: "floorChanged", applyRequested: "applyRequested", cancelRequested: "cancelRequested", closeRequested: "closeRequested", drawOutlineRequested: "drawOutlineRequested", editOutlineRequested: "editOutlineRequested" }, features: [\u0275\u0275ProvidersFeature([], [ 17793 provideIcons({ 17794 lucideBuilding2, 17795 lucideChartBar, 17796 lucideRuler, 17797 lucideMapPin, 17798 lucidePencil, 17799 lucidePenTool 17800 }) 17801]), \u0275\u0275NgOnChangesFeature], decls: 34, vars: 11, consts: [[3, "closeRequested", "cancelRequested", "applyRequested", "title", "applyText"], ["panel-content", ""], ["label", "Floor Details"], ["section-icon", "", "name", "lucideBuilding2", "size", "16"], [1, "input-stack"], [1, "input-group", "full"], ["type", "text", "placeholder", "e.g., Ground Floor", 3, "ngModelChange", "ngModel"], [1, "input-group-row"], [1, "input-group"], ["type", "number", "step", "1", 3, "ngModelChange", "ngModel"], [1, "level-hint"], ["label", "Floor Outline"], ["section-icon", "", "name", "lucidePenTool", "size", "16"], [1, "outline-section"], [1, "outline-description"], [1, "outline-actions"], [1, "outline-button", "primary"], ["label", "Statistics"], ["label", "Dimensions"], ["label", "Location"], ["section-icon", "", "name", "lucideMapPin", "size", "16"], [1, "location-info"], ["label", "Building", 3, "value"], ["label", "Campus", 3, "value"], ["label", "ID", 3, "value", "mono"], ["panel-info", ""], [1, "outline-button", 3, "click"], ["name", "lucidePencil", "size", "14"], [1, "outline-button", "secondary", 3, "click"], ["name", "lucidePenTool", "size", "14"], [1, "outline-button", "primary", 3, "click"], ["section-icon", "", "name", "lucideChartBar", "size", "16"], [1, "stats-grid"], [1, "stat-item"], [1, "stat-label"], [1, "stat-value"], ["section-icon", "", "name", "lucideRuler", "size", "16"], [1, "dimensions-grid"], [1, "dimension-item"], [1, "dimension-label"], [1, "dimension-value"], [1, "area-row"], [1, "area-label"], [1, "area-value"]], template: function FloorEditPanelComponent_Template(rf, ctx) { 17802 if (rf & 1) { 17803 \u0275\u0275elementStart(0, "app-entity-panel", 0); 17804 \u0275\u0275listener("closeRequested", function FloorEditPanelComponent_Template_app_entity_panel_closeRequested_0_listener() { 17805 return ctx.onClose(); 17806 })("cancelRequested", function FloorEditPanelComponent_Template_app_entity_panel_cancelRequested_0_listener() { 17807 return ctx.onCancel(); 17808 })("applyRequested", function FloorEditPanelComponent_Template_app_entity_panel_applyRequested_0_listener() { 17809 return ctx.onApply(); 17810 }); 17811 \u0275\u0275elementContainerStart(1, 1); 17812 \u0275\u0275elementStart(2, "app-entity-section", 2); 17813 \u0275\u0275element(3, "ng-icon", 3); 17814 \u0275\u0275elementStart(4, "div", 4)(5, "div", 5)(6, "label"); 17815 \u0275\u0275text(7, "NAME"); 17816 \u0275\u0275elementEnd(); 17817 \u0275\u0275elementStart(8, "input", 6); 17818 \u0275\u0275listener("ngModelChange", function FloorEditPanelComponent_Template_input_ngModelChange_8_listener($event) { 17819 return ctx.onNameChange($event); 17820 }); 17821 \u0275\u0275elementEnd()(); 17822 \u0275\u0275elementStart(9, "div", 7)(10, "div", 8)(11, "label"); 17823 \u0275\u0275text(12, "LEVEL"); 17824 \u0275\u0275elementEnd(); 17825 \u0275\u0275elementStart(13, "input", 9); 17826 \u0275\u0275listener("ngModelChange", function FloorEditPanelComponent_Template_input_ngModelChange_13_listener($event) { 17827 return ctx.onLevelChange($event); 17828 }); 17829 \u0275\u0275elementEnd()(); 17830 \u0275\u0275elementStart(14, "span", 10); 17831 \u0275\u0275text(15, "1=Ground, -1=B1"); 17832 \u0275\u0275elementEnd()()()(); 17833 \u0275\u0275elementStart(16, "app-entity-section", 11); 17834 \u0275\u0275element(17, "ng-icon", 12); 17835 \u0275\u0275elementStart(18, "div", 13)(19, "p", 14); 17836 \u0275\u0275template(20, FloorEditPanelComponent_Conditional_20_Template, 1, 1)(21, FloorEditPanelComponent_Conditional_21_Template, 1, 0); 17837 \u0275\u0275elementEnd(); 17838 \u0275\u0275elementStart(22, "div", 15); 17839 \u0275\u0275template(23, FloorEditPanelComponent_Conditional_23_Template, 6, 0)(24, FloorEditPanelComponent_Conditional_24_Template, 3, 0, "button", 16); 17840 \u0275\u0275elementEnd()()(); 17841 \u0275\u0275template(25, FloorEditPanelComponent_Conditional_25_Template, 23, 4, "app-entity-section", 17)(26, FloorEditPanelComponent_Conditional_26_Template, 19, 5, "app-entity-section", 18); 17842 \u0275\u0275elementStart(27, "app-entity-section", 19); 17843 \u0275\u0275element(28, "ng-icon", 20); 17844 \u0275\u0275elementStart(29, "div", 21); 17845 \u0275\u0275element(30, "app-entity-row", 22)(31, "app-entity-row", 23); 17846 \u0275\u0275template(32, FloorEditPanelComponent_Conditional_32_Template, 1, 2, "app-entity-row", 24); 17847 \u0275\u0275elementEnd()(); 17848 \u0275\u0275elementContainerEnd(); 17849 \u0275\u0275elementContainer(33, 25); 17850 \u0275\u0275elementEnd(); 17851 } 17852 if (rf & 2) { 17853 \u0275\u0275property("title", ctx.panelTitle)("applyText", ctx.applyButtonText); 17854 \u0275\u0275advance(8); 17855 \u0275\u0275property("ngModel", ctx.name); 17856 \u0275\u0275advance(5); 17857 \u0275\u0275property("ngModel", ctx.level); 17858 \u0275\u0275advance(7); 17859 \u0275\u0275conditional(ctx.hasOutline ? 20 : 21); 17860 \u0275\u0275advance(3); 17861 \u0275\u0275conditional(ctx.hasOutline ? 23 : 24); 17862 \u0275\u0275advance(2); 17863 \u0275\u0275conditional(!ctx.isNew ? 25 : -1); 17864 \u0275\u0275advance(); 17865 \u0275\u0275conditional(ctx.dimensions ? 26 : -1); 17866 \u0275\u0275advance(4); 17867 \u0275\u0275property("value", ctx.buildingName); 17868 \u0275\u0275advance(); 17869 \u0275\u0275property("value", ctx.campusName); 17870 \u0275\u0275advance(); 17871 \u0275\u0275conditional(!ctx.isNew ? 32 : -1); 17872 } 17873}, dependencies: [ 17874 CommonModule, 17875 DecimalPipe, 17876 FormsModule, 17877 DefaultValueAccessor, 17878 NumberValueAccessor, 17879 NgControlStatus, 17880 NgModel, 17881 NgIcon, 17882 EntityPanelComponent, 17883 EntitySectionComponent, 17884 EntityRowComponent 17885], styles: ["\n\n.input-stack[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n gap: 10px;\n}\n.input-group-row[_ngcontent-%COMP%] {\n display: flex;\n align-items: flex-end;\n gap: 10px;\n}\n.input-group-row[_ngcontent-%COMP%] .input-group[_ngcontent-%COMP%] {\n flex: 0 0 auto;\n width: 80px;\n}\n.level-hint[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--ion-color-medium);\n padding-bottom: 8px;\n white-space: nowrap;\n}\n.input-group[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.input-group.full[_ngcontent-%COMP%] {\n width: 100%;\n}\n.input-group[_ngcontent-%COMP%] label[_ngcontent-%COMP%] {\n font-size: 11px;\n font-weight: 500;\n color: var(--ion-color-medium);\n text-transform: uppercase;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%] {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n transition: border-color 0.2s ease;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.input-group[_ngcontent-%COMP%] input[type=number][_ngcontent-%COMP%] {\n font-family: monospace;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::placeholder {\n color: var(--ion-color-medium-tint);\n font-style: italic;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::-webkit-inner-spin-button, \n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::-webkit-outer-spin-button {\n opacity: 1;\n}\n.stats-grid[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 8px;\n}\n.stat-item[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 6px 8px;\n background: rgba(0, 0, 0, 0.02);\n border-radius: 4px;\n}\n.stat-label[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--ion-color-medium);\n}\n.stat-value[_ngcontent-%COMP%] {\n font-size: 13px;\n font-weight: 600;\n color: var(--ion-text-color);\n font-family: monospace;\n}\n.dimensions-grid[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 8px;\n margin-bottom: 8px;\n}\n.dimension-item[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 6px 8px;\n background: rgba(0, 0, 0, 0.02);\n border-radius: 4px;\n}\n.dimension-label[_ngcontent-%COMP%] {\n font-size: 10px;\n color: var(--ion-color-medium);\n text-transform: uppercase;\n}\n.dimension-value[_ngcontent-%COMP%] {\n font-size: 13px;\n font-weight: 500;\n color: var(--ion-text-color);\n font-family: monospace;\n}\n.area-row[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 6px 8px;\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n border-radius: 4px;\n}\n.area-label[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--ion-color-medium);\n}\n.area-value[_ngcontent-%COMP%] {\n font-size: 13px;\n font-weight: 600;\n color: var(--ion-color-primary);\n font-family: monospace;\n}\n.outline-section[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n.outline-description[_ngcontent-%COMP%] {\n font-size: 12px;\n color: var(--ion-color-medium);\n margin: 0;\n line-height: 1.4;\n}\n.outline-actions[_ngcontent-%COMP%] {\n display: flex;\n gap: 8px;\n}\n.outline-button[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 12px;\n font-size: 12px;\n font-weight: 500;\n border: 1px solid var(--ion-color-medium-shade);\n border-radius: 6px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n cursor: pointer;\n transition: all 0.15s ease;\n}\n.outline-button[_ngcontent-%COMP%]:hover {\n background: var(--ion-color-light);\n border-color: var(--ion-color-primary);\n}\n.outline-button.primary[_ngcontent-%COMP%] {\n background: var(--ion-color-primary);\n border-color: var(--ion-color-primary);\n color: var(--ion-color-primary-contrast);\n}\n.outline-button.primary[_ngcontent-%COMP%]:hover {\n background: var(--ion-color-primary-shade);\n}\n.outline-button.secondary[_ngcontent-%COMP%] {\n font-size: 11px;\n padding: 6px 10px;\n color: var(--ion-color-medium);\n}\n.outline-button[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n flex-shrink: 0;\n}"], changeDetection: 0 }); 17886var FloorEditPanelComponent = _FloorEditPanelComponent; 17887(() => { 17888 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(FloorEditPanelComponent, [{ 17889 type: Component, 17890 args: [{ selector: "app-floor-edit-panel", standalone: true, imports: [ 17891 CommonModule, 17892 FormsModule, 17893 NgIcon, 17894 EntityPanelComponent, 17895 EntitySectionComponent, 17896 EntityRowComponent 17897 ], viewProviders: [ 17898 provideIcons({ 17899 lucideBuilding2, 17900 lucideChartBar, 17901 lucideRuler, 17902 lucideMapPin, 17903 lucidePencil, 17904 lucidePenTool 17905 }) 17906 ], changeDetection: ChangeDetectionStrategy.OnPush, template: `<app-entity-panel 17907 [title]="panelTitle" 17908 [applyText]="applyButtonText" 17909 (closeRequested)="onClose()" 17910 (cancelRequested)="onCancel()" 17911 (applyRequested)="onApply()"> 17912 17913 <ng-container panel-content> 17914 <!-- Floor Details Section --> 17915 <app-entity-section label="Floor Details"> 17916 <ng-icon section-icon name="lucideBuilding2" size="16" /> 17917 <div class="input-stack"> 17918 <div class="input-group full"> 17919 <label>NAME</label> 17920 <input 17921 type="text" 17922 [ngModel]="name" 17923 (ngModelChange)="onNameChange($event)" 17924 placeholder="e.g., Ground Floor" 17925 /> 17926 </div> 17927 <div class="input-group-row"> 17928 <div class="input-group"> 17929 <label>LEVEL</label> 17930 <input 17931 type="number" 17932 [ngModel]="level" 17933 (ngModelChange)="onLevelChange($event)" 17934 step="1" 17935 /> 17936 </div>
17937 <span class="level-hint">1=Ground, -1=B1</span> 17938 </div> 17939 </div> 17940 </app-entity-section> 17941 17942 <!-- Floor Outline Section --> 17943 <app-entity-section label="Floor Outline"> 17944 <ng-icon section-icon name="lucidePenTool" size="16" /> 17945 <div class="outline-section"> 17946 <p class="outline-description"> 17947 @if (hasOutline) { 17948 The floor has {{ cornerCount }} outline corners. 17949 } @else { 17950 No outline defined. Draw an outline to enable dimensions. 17951 } 17952 </p> 17953 <div class="outline-actions"> 17954 @if (hasOutline) { 17955 <button class="outline-button" (click)="onEditOutline()"> 17956 <ng-icon name="lucidePencil" size="14" /> 17957 Edit Outline 17958 </button> 17959 <button class="outline-button secondary" (click)="onDrawOutline()"> 17960 <ng-icon name="lucidePenTool" size="14" /> 17961 Redraw 17962 </button> 17963 } @else { 17964 <button class="outline-button primary" (click)="onDrawOutline()"> 17965 <ng-icon name="lucidePenTool" size="14" /> 17966 Draw Outline 17967 </button> 17968 } 17969 </div> 17970 </div> 17971 </app-entity-section> 17972 17973 <!-- Statistics Section (hidden during creation) --> 17974 @if (!isNew) { 17975 <app-entity-section label="Statistics"> 17976 <ng-icon section-icon name="lucideChartBar" size="16" /> 17977 <div class="stats-grid"> 17978 <div class="stat-item"> 17979 <span class="stat-label">Rooms</span> 17980 <span class="stat-value">{{ roomCount }}</span> 17981 </div> 17982 <div class="stat-item"> 17983 <span class="stat-label">Nodes</span> 17984 <span class="stat-value">{{ nodeCount }}</span> 17985 </div> 17986 <div class="stat-item"> 17987 <span class="stat-label">Connections</span> 17988 <span class="stat-value">{{ connectionCount }}</span> 17989 </div> 17990 <div class="stat-item"> 17991 <span class="stat-label">Corners</span> 17992 <span class="stat-value">{{ cornerCount }}</span> 17993 </div> 17994 </div> 17995 </app-entity-section> 17996 } 17997 17998 <!-- Dimensions Section --> 17999 @if (dimensions) { 18000 <app-entity-section label="Dimensions"> 18001 <ng-icon section-icon name="lucideRuler" size="16" /> 18002 <div class="dimensions-grid"> 18003 <div class="dimension-item"> 18004 <span class="dimension-label">Width</span> 18005 <span class="dimension-value">{{ dimensions.width }} m</span> 18006 </div> 18007 <div class="dimension-item"> 18008 <span class="dimension-label">Height</span> 18009 <span class="dimension-value">{{ dimensions.height }} m</span> 18010 </div> 18011 </div> 18012 <div class="area-row"> 18013 <span class="area-label">Area</span> 18014 <span class="area-value">~{{ dimensions.area | number }} m\xB2</span> 18015 </div> 18016 </app-entity-section> 18017 } 18018 18019 <!-- Location Info Section --> 18020 <app-entity-section label="Location"> 18021 <ng-icon section-icon name="lucideMapPin" size="16" /> 18022 <div class="location-info"> 18023 <app-entity-row label="Building" [value]="buildingName" /> 18024 <app-entity-row label="Campus" [value]="campusName" /> 18025 @if (!isNew) { 18026 <app-entity-row label="ID" [value]="floorId" [mono]="true" /> 18027 } 18028 </div> 18029 </app-entity-section> 18030 </ng-container> 18031 18032 <!-- Empty panel-info slot since we're using a custom Location section --> 18033 <ng-container panel-info></ng-container> 18034 18035</app-entity-panel> 18036`, styles: ["/* src/app/pages/campus-map-v3/components/floor-edit-panel/floor-edit-panel.component.scss */\n.input-stack {\n display: flex;\n flex-direction: column;\n gap: 8px;\n gap: 10px;\n}\n.input-group-row {\n display: flex;\n align-items: flex-end;\n gap: 10px;\n}\n.input-group-row .input-group {\n flex: 0 0 auto;\n width: 80px;\n}\n.level-hint {\n font-size: 11px;\n color: var(--ion-color-medium);\n padding-bottom: 8px;\n white-space: nowrap;\n}\n.input-group {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.input-group.full {\n width: 100%;\n}\n.input-group label {\n font-size: 11px;\n font-weight: 500;\n color: var(--ion-color-medium);\n text-transform: uppercase;\n}\n.input-group input {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n transition: border-color 0.2s ease;\n}\n.input-group input:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.input-group input[type=number] {\n font-family: monospace;\n}\n.input-group input::placeholder {\n color: var(--ion-color-medium-tint);\n font-style: italic;\n}\n.input-group input::-webkit-inner-spin-button,\n.input-group input::-webkit-outer-spin-button {\n opacity: 1;\n}\n.stats-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 8px;\n}\n.stat-item {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 6px 8px;\n background: rgba(0, 0, 0, 0.02);\n border-radius: 4px;\n}\n.stat-label {\n font-size: 11px;\n color: var(--ion-color-medium);\n}\n.stat-value {\n font-size: 13px;\n font-weight: 600;\n color: var(--ion-text-color);\n font-family: monospace;\n}\n.dimensions-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 8px;\n margin-bottom: 8px;\n}\n.dimension-item {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 6px 8px;\n background: rgba(0, 0, 0, 0.02);\n border-radius: 4px;\n}\n.dimension-label {\n font-size: 10px;\n color: var(--ion-color-medium);\n text-transform: uppercase;\n}\n.dimension-value {\n font-size: 13px;\n font-weight: 500;\n color: var(--ion-text-color);\n font-family: monospace;\n}\n.area-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 6px 8px;\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n border-radius: 4px;\n}\n.area-label {\n font-size: 11px;\n color: var(--ion-color-medium);\n}\n.area-value {\n font-size: 13px;\n font-weight: 600;\n color: var(--ion-color-primary);\n font-family: monospace;\n}\n.outline-section {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n.outline-description {\n font-size: 12px;\n color: var(--ion-color-medium);\n margin: 0;\n line-height: 1.4;\n}\n.outline-actions {\n display: flex;\n gap: 8px;\n}\n.outline-button {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 12px;\n font-size: 12px;\n font-weight: 500;\n border: 1px solid var(--ion-color-medium-shade);\n border-radius: 6px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n cursor: pointer;\n transition: all 0.15s ease;\n}\n.outline-button:hover {\n background: var(--ion-color-light);\n border-color: var(--ion-color-primary);\n}\n.outline-button.primary {\n background: var(--ion-color-primary);\n border-color: var(--ion-color-primary);\n color: var(--ion-color-primary-contrast);\n}\n.outline-button.primary:hover {\n background: var(--ion-color-primary-shade);\n}\n.outline-button.secondary {\n font-size: 11px;\n padding: 6px 10px;\n color: var(--ion-color-medium);\n}\n.outline-button ng-icon {\n flex-shrink: 0;\n}\n"] }] 18037 }], null, { floor: [{ 18038 type: Input 18039 }], isNew: [{ 18040 type: Input 18041 }], floorChanged: [{ 18042 type: Output 18043 }], applyRequested: [{ 18044 type: Output 18045 }], cancelRequested: [{ 18046 type: Output 18047 }], closeRequested: [{ 18048 type: Output 18049 }], drawOutlineRequested: [{ 18050 type: Output 18051 }], editOutlineRequested: [{ 18052 type: Output 18053 }] }); 18054})(); 18055(() => { 18056 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(FloorEditPanelComponent, { className: "FloorEditPanelComponent", filePath: "src/app/pages/campus-map-v3/components/floor-edit-panel/floor-edit-panel.component.ts", lineNumber: 59 }); 18057})(); 18058
18059// src/app/pages/campus-map-v3/components/building-edit-panel/building-edit-panel.component.ts 18060function BuildingEditPanelComponent_Conditional_13_Template(rf, ctx) { 18061 if (rf & 1) { 18062 const _r1 = \u0275\u0275getCurrentView(); 18063 \u0275\u0275elementStart(0, "app-entity-section", 8); 18064 \u0275\u0275element(1, "ng-icon", 17); 18065 \u0275\u0275elementStart(2, "div", 18); 18066 \u0275\u0275element(3, "app-entity-row", 19); 18067 \u0275\u0275elementStart(4, "div", 20)(5, "button", 21); 18068 \u0275\u0275listener("click", function BuildingEditPanelComponent_Conditional_13_Template_button_click_5_listener() { 18069 \u0275\u0275restoreView(_r1); 18070 const ctx_r1 = \u0275\u0275nextContext(); 18071 return \u0275\u0275resetView(ctx_r1.onPickDefaultDestination()); 18072 }); 18073 \u0275\u0275element(6, "ng-icon", 22); 18074 \u0275\u0275elementStart(7, "span"); 18075 \u0275\u0275text(8, "Pick on map"); 18076 \u0275\u0275elementEnd()(); 18077 \u0275\u0275elementStart(9, "button", 23); 18078 \u0275\u0275listener("click", function BuildingEditPanelComponent_Conditional_13_Template_button_click_9_listener() { 18079 \u0275\u0275restoreView(_r1); 18080 const ctx_r1 = \u0275\u0275nextContext(); 18081 return \u0275\u0275resetView(ctx_r1.onClearDefaultDestination()); 18082 }); 18083 \u0275\u0275element(10, "ng-icon", 24); 18084 \u0275\u0275elementStart(11, "span"); 18085 \u0275\u0275text(12, "Clear"); 18086 \u0275\u0275elementEnd()()()()(); 18087 } 18088 if (rf & 2) { 18089 const ctx_r1 = \u0275\u0275nextContext(); 18090 \u0275\u0275advance(3); 18091 \u0275\u0275property("value", ctx_r1.currentDefaultDestinationLabel); 18092 \u0275\u0275advance(6); 18093 \u0275\u0275property("disabled", !ctx_r1.canClearDefaultDestination); 18094 } 18095} 18096function BuildingEditPanelComponent_Conditional_14_Template(rf, ctx) { 18097 if (rf & 1) { 18098 const _r3 = \u0275\u0275getCurrentView(); 18099 \u0275\u0275elementStart(0, "app-entity-section", 9); 18100 \u0275\u0275element(1, "ng-icon", 25); 18101 \u0275\u0275elementStart(2, "div", 26)(3, "div", 27); 18102 \u0275\u0275element(4, "app-entity-row", 28)(5, "app-entity-row", 29); 18103 \u0275\u0275elementEnd(); 18104 \u0275\u0275elementStart(6, "button", 30); 18105 \u0275\u0275listener("click", function BuildingEditPanelComponent_Conditional_14_Template_button_click_6_listener() { 18106 \u0275\u0275restoreView(_r3); 18107 const ctx_r1 = \u0275\u0275nextContext(); 18108 return \u0275\u0275resetView(ctx_r1.onAssignNodes()); 18109 }); 18110 \u0275\u0275element(7, "ng-icon", 31); 18111 \u0275\u0275elementStart(8, "span"); 18112 \u0275\u0275text(9, "Assign on map"); 18113 \u0275\u0275elementEnd()()()(); 18114 } 18115 if (rf & 2) { 18116 const ctx_r1 = \u0275\u0275nextContext(); 18117 \u0275\u0275advance(4); 18118 \u0275\u0275property("value", ctx_r1.assignedNodeCount.toString()); 18119 \u0275\u0275advance(); 18120 \u0275\u0275property("value", ctx_r1.eligibleNodeCount.toString()); 18121 } 18122} 18123function BuildingEditPanelComponent_Conditional_15_Template(rf, ctx) { 18124 if (rf & 1) { 18125 \u0275\u0275elementStart(0, "app-entity-section", 10); 18126 \u0275\u0275element(1, "ng-icon", 32); 18127 \u0275\u0275elementStart(2, "div", 33)(3, "div", 34)(4, "span", 35); 18128 \u0275\u0275text(5, "Floors"); 18129 \u0275\u0275elementEnd(); 18130 \u0275\u0275elementStart(6, "span", 36); 18131 \u0275\u0275text(7); 18132 \u0275\u0275elementEnd()(); 18133 \u0275\u0275elementStart(8, "div", 34)(9, "span", 35); 18134 \u0275\u0275text(10, "Rooms"); 18135 \u0275\u0275elementEnd(); 18136 \u0275\u0275elementStart(11, "span", 36); 18137 \u0275\u0275text(12); 18138 \u0275\u0275elementEnd()(); 18139 \u0275\u0275elementStart(13, "div", 34)(14, "span", 35); 18140 \u0275\u0275text(15, "Nodes"); 18141 \u0275\u0275elementEnd(); 18142 \u0275\u0275elementStart(16, "span", 36); 18143 \u0275\u0275text(17); 18144 \u0275\u0275elementEnd()(); 18145 \u0275\u0275elementStart(18, "div", 34)(19, "span", 35); 18146 \u0275\u0275text(20, "Stairwells"); 18147 \u0275\u0275elementEnd(); 18148 \u0275\u0275elementStart(21, "span", 36); 18149 \u0275\u0275text(22); 18150 \u0275\u0275elementEnd()()()(); 18151 } 18152 if (rf & 2) { 18153 const ctx_r1 = \u0275\u0275nextContext(); 18154 \u0275\u0275advance(7); 18155 \u0275\u0275textInterpolate(ctx_r1.floorCount); 18156 \u0275\u0275advance(5); 18157 \u0275\u0275textInterpolate(ctx_r1.roomCount); 18158 \u0275\u0275advance(5); 18159 \u0275\u0275textInterpolate(ctx_r1.nodeCount); 18160 \u0275\u0275advance(5); 18161 \u0275\u0275textInterpolate(ctx_r1.stairwellCount); 18162 } 18163} 18164function BuildingEditPanelComponent_Conditional_20_Template(rf, ctx) { 18165 if (rf & 1) { 18166 \u0275\u0275element(0, "app-entity-row", 15); 18167 } 18168 if (rf & 2) { 18169 const ctx_r1 = \u0275\u0275nextContext(); 18170 \u0275\u0275property("value", ctx_r1.buildingId)("mono", true); 18171 } 18172} 18173var _BuildingEditPanelComponent = class _BuildingEditPanelComponent { 18174 constructor() { 18175 this.building = null; 18176 this.isNew = false;
18177 this.assignedNodeCount = 0; 18178 this.eligibleNodeCount = 0; 18179 this.buildingChanged = new EventEmitter(); 18180 this.pickDefaultDestinationRequested = new EventEmitter(); 18181 this.clearDefaultDestinationRequested = new EventEmitter(); 18182 this.assignNodesRequested = new EventEmitter(); 18183 this.applyRequested = new EventEmitter(); 18184 this.cancelRequested = new EventEmitter(); 18185 this.closeRequested = new EventEmitter(); 18186 this.name = null; 18187 this.address = null; 18188 } 18189 ngOnChanges(changes) { 18190 if (changes["building"] && this.building) { 18191 this.syncFromBuilding(); 18192 } 18193 } 18194 /** 18195 * Sync local form value from building model. 18196 */ 18197 syncFromBuilding() { 18198 var _a3, _b2; 18199 if (!this.building) 18200 return; 18201 this.name = (_a3 = this.building.entity.name) != null ? _a3 : null; 18202 this.address = (_b2 = this.building.entity.address) != null ? _b2 : null; 18203 } 18204 // ========================================================================= 18205 // Computed Read-Only Statistics 18206 // ========================================================================= 18207 get floorCount() { 18208 var _a3, _b2, _c; 18209 return (_c = (_b2 = (_a3 = this.building) == null ? void 0 : _a3.floors) == null ? void 0 : _b2.length) != null ? _c : 0; 18210 } 18211 get roomCount() { 18212 var _a3, _b2, _c; 18213 return (_c = (_b2 = (_a3 = this.building) == null ? void 0 : _a3.rooms) == null ? void 0 : _b2.length) != null ? _c : 0; 18214 } 18215 get nodeCount() { 18216 return this.assignedNodeCount; 18217 } 18218 get stairwellCount() { 18219 var _a3, _b2, _c; 18220 return (_c = (_b2 = (_a3 = this.building) == null ? void 0 : _a3.stairwells) == null ? void 0 : _b2.length) != null ? _c : 0; 18221 } 18222 get campusName() { 18223 var _a3, _b2, _c; 18224 return (_c = (_b2 = (_a3 = this.building) == null ? void 0 : _a3.campus) == null ? void 0 : _b2.entity.name) != null ? _c : "Unknown Campus"; 18225 } 18226 get buildingId() { 18227 var _a3, _b2; 18228 return (_b2 = (_a3 = this.building) == null ? void 0 : _a3.entity.id) != null ? _b2 : ""; 18229 } 18230 get currentDefaultDestinationLabel() { 18231 var _a3, _b2, _c, _d; 18232 const node = this.currentDefaultDestinationNode; 18233 if (!node) 18234 return "Automatic"; 18235 const type = (_a3 = node.entity.type) != null ? _a3 : NodeType.Regular; 18236 const floorName = (_b2 = node.floor) == null ? void 0 : _b2.entity.name; 18237 const id = (_d = (_c = node.entity.id) == null ? void 0 : _c.slice(0, 8)) != null ? _d : "node"; 18238 if (type === NodeType.Outdoor) { 18239 return `Outdoor node ${id}`; 18240 } 18241 return `${type} node ${id}${floorName ? ` - ${floorName}` : ""}`; 18242 } 18243 get canClearDefaultDestination() { 18244 var _a3; 18245 return !!((_a3 = this.building) == null ? void 0 : _a3.entity.defaultDestinationNodeId); 18246 } 18247 get panelTitle() { 18248 return this.isNew ? "New Building" : "Edit Building"; 18249 } 18250 get applyButtonText() { 18251 return this.isNew ? "Create" : "Apply"; 18252 } 18253 // ========================================================================= 18254 // Event Handlers 18255 // ========================================================================= 18256 onNameChange(value) { 18257 this.name = value || null; 18258 this.buildingChanged.emit({ name: this.name }); 18259 } 18260 onAddressChange(value) { 18261 this.address = value || null; 18262 this.buildingChanged.emit({ address: this.address }); 18263 } 18264 onPickDefaultDestination() { 18265 this.pickDefaultDestinationRequested.emit(); 18266 } 18267 onClearDefaultDestination() { 18268 this.clearDefaultDestinationRequested.emit(); 18269 } 18270 onAssignNodes() { 18271 this.assignNodesRequested.emit(); 18272 } 18273 onApply() { 18274 this.applyRequested.emit(); 18275 } 18276 onCancel() { 18277 this.cancelRequested.emit(); 18278 } 18279 onClose() { 18280 this.closeRequested.emit(); 18281 } 18282 get currentDefaultDestinationNode() { 18283 var _a3, _b2, _c, _d, _e, _f; 18284 const nodeId = (_a3 = this.building) == null ? void 0 : _a3.entity.defaultDestinationNodeId; 18285 if (!nodeId || !this.building) 18286 return null; 18287 for (const floor of (_b2 = this.building.floors) != null ? _b2 : []) { 18288 const node = (_c = floor.nodes) == null ? void 0 : _c.find((candidate) => candidate.entity.id === nodeId); 18289 if (node) 18290 return node; 18291 } 18292 return (_f = (_e = (_d = this.building.campus) == null ? void 0 : _d.nodes) == null ? void 0 : _e.find((candidate) => candidate.entity.id === nodeId)) != null ? _f : null; 18293 } 18294}; 18295_BuildingEditPanelComponent.\u0275fac = function BuildingEditPanelComponent_Factory(__ngFactoryType__) { 18296 return new (__ngFactoryType__ || _BuildingEditPanelComponent)(); 18297}; 18298_BuildingEditPanelComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _BuildingEditPanelComponent, selectors: [["app-building-edit-panel"]], inputs: { building: "building", isNew: "isNew", assignedNodeCount: "assignedNodeCount", eligibleNodeCount: "eligibleNodeCount" }, outputs: { buildingChanged: "buildingChanged", pickDefaultDestinationRequested: "pickDefaultDestinationRequested", clearDefaultDestinationRequested: "clearDefaultDestinationRequested", assignNodesRequested: "assignNodesRequested", applyRequested: "applyRequested", cancelRequested: "cancelRequested", closeRequested: "closeRequested" }, features: [\u0275\u0275ProvidersFeature([], [ 18299 provideIcons({ 18300 lucideBuilding2, 18301 lucideChartBar, 18302 lucideCircleX, 18303 lucideCrosshair, 18304 lucideMapPin, 18305 lucideNetwork 18306 }) 18307]), \u0275\u0275NgOnChangesFeature], decls: 22, vars: 9, consts: [[3, "closeRequested", "cancelRequested", "applyRequested", "title", "applyText"], ["panel-content", ""], ["label", "Building Details"], ["section-icon", "", "name", "lucideBuilding2", "size", "16"], [1, "input-stack"], [1, "input-group", "full"], ["type", "text", "placeholder", "e.g., Main Building", 3, "ngModelChange", "ngModel"], ["type", "text", "placeholder", "e.g., 123 Clinic Ave", 3, "ngModelChange", "ngModel"], ["label", "Destination"], ["label", "Nodes"], ["label", "Statistics"], ["label", "Location"], ["section-icon", "", "name", "lucideMapPin", "size", "16"], [1, "location-info"], ["label", "Campus", 3, "value"], ["label", "ID", 3, "value", "mono"], ["panel-info", ""], ["section-icon", "", "name", "lucideCrosshair", "size", "16"], [1, "destination-stack"], ["label", "Default", 3, "value"], [1, "destination-actions"], ["type", "button", 1, "panel-action-button", "primary", 3, "click"], ["name", "lucideMapPin", "size", "14"], ["type", "button", 1, "panel-action-button", 3, "click", "disabled"], ["name", "lucideCircleX", "size", "14"], ["section-icon", "", "name", "lucideNetwork", "size", "16"], [1, "node-assignment-stack"], [1, "node-assignment-counts"], ["label", "Assigned", 3, "value"], ["label", "Eligible visible", 3, "value"], ["type", "button", 1, "panel-action-button", "primary", "full-width", 3, "click"], ["name", "lucideNetwork", "size", "14"], ["section-icon", "", "name", "lucideChartBar", "size", "16"], [1, "stats-grid"], [1, "stat-item"], [1, "stat-label"], [1, "stat-value"]], template: function BuildingEditPanelComponent_Template(rf, ctx) { 18308 if (rf & 1) { 18309 \u0275\u0275elementStart(0, "app-entity-panel", 0); 18310 \u0275\u0275listener("closeRequested", function BuildingEditPanelComponent_Template_app_entity_panel_closeRequested_0_listener() { 18311 return ctx.onClose(); 18312 })("cancelRequested", function BuildingEditPanelComponent_Template_app_entity_panel_cancelRequested_0_listener() { 18313 return ctx.onCancel(); 18314 })("applyRequested", function BuildingEditPanelComponent_Template_app_entity_panel_applyRequested_0_listener() { 18315 return ctx.onApply(); 18316 }); 18317 \u0275\u0275elementContainerStart(1, 1); 18318 \u0275\u0275elementStart(2, "app-entity-section", 2); 18319 \u0275\u0275element(3, "ng-icon", 3); 18320 \u0275\u0275elementStart(4, "div", 4)(5, "div", 5)(6, "label"); 18321 \u0275\u0275text(7, "NAME"); 18322 \u0275\u0275elementEnd(); 18323 \u0275\u0275elementStart(8, "input", 6); 18324 \u0275\u0275listener("ngModelChange", function BuildingEditPanelComponent_Template_input_ngModelChange_8_listener($event) { 18325 return ctx.onNameChange($event); 18326 }); 18327 \u0275\u0275elementEnd()(); 18328 \u0275\u0275elementStart(9, "div", 5)(10, "label"); 18329 \u0275\u0275text(11, "ADDRESS"); 18330 \u0275\u0275elementEnd(); 18331 \u0275\u0275elementStart(12, "input", 7); 18332 \u0275\u0275listener("ngModelChange", function BuildingEditPanelComponent_Template_input_ngModelChange_12_listener($event) { 18333 return ctx.onAddressChange($event); 18334 }); 18335 \u0275\u0275elementEnd()()()(); 18336 \u0275\u0275template(13, BuildingEditPanelComponent_Conditional_13_Template, 13, 2, "app-entity-section", 8)(14, BuildingEditPanelComponent_Conditional_14_Template, 10, 2, "app-entity-section", 9)(15, BuildingEditPanelComponent_Conditional_15_Template, 23, 4, "app-entity-section", 10); 18337 \u0275\u0275elementStart(16, "app-entity-section", 11); 18338 \u0275\u0275element(17, "ng-icon", 12); 18339 \u0275\u0275elementStart(18, "div", 13); 18340 \u0275\u0275element(19, "app-entity-row", 14); 18341 \u0275\u0275template(20, BuildingEditPanelComponent_Conditional_20_Template, 1, 2, "app-entity-row", 15); 18342 \u0275\u0275elementEnd()(); 18343 \u0275\u0275elementContainerEnd(); 18344 \u0275\u0275elementContainer(21, 16); 18345 \u0275\u0275elementEnd(); 18346 } 18347 if (rf & 2) { 18348 \u0275\u0275property("title", ctx.panelTitle)("applyText", ctx.applyButtonText); 18349 \u0275\u0275advance(8); 18350 \u0275\u0275property("ngModel", ctx.name); 18351 \u0275\u0275advance(4); 18352 \u0275\u0275property("ngModel", ctx.address); 18353 \u0275\u0275advance(); 18354 \u0275\u0275conditional(!ctx.isNew ? 13 : -1); 18355 \u0275\u0275advance(); 18356 \u0275\u0275conditional(!ctx.isNew ? 14 : -1); 18357 \u0275\u0275advance();
18358 \u0275\u0275conditional(!ctx.isNew ? 15 : -1); 18359 \u0275\u0275advance(4); 18360 \u0275\u0275property("value", ctx.campusName); 18361 \u0275\u0275advance(); 18362 \u0275\u0275conditional(!ctx.isNew ? 20 : -1); 18363 } 18364}, dependencies: [ 18365 CommonModule, 18366 FormsModule, 18367 DefaultValueAccessor, 18368 NgControlStatus, 18369 NgModel, 18370 NgIcon, 18371 EntityPanelComponent, 18372 EntitySectionComponent, 18373 EntityRowComponent 18374], styles: ["\n\n.input-stack[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n gap: 10px;\n}\n.input-group[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.input-group.full[_ngcontent-%COMP%] {\n width: 100%;\n}\n.input-group[_ngcontent-%COMP%] label[_ngcontent-%COMP%] {\n font-size: 11px;\n font-weight: 500;\n color: var(--ion-color-medium);\n text-transform: uppercase;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%] {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n transition: border-color 0.2s ease;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.input-group[_ngcontent-%COMP%] input[type=number][_ngcontent-%COMP%] {\n font-family: monospace;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::placeholder {\n color: var(--ion-color-medium-tint);\n font-style: italic;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::-webkit-inner-spin-button, \n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::-webkit-outer-spin-button {\n opacity: 1;\n}\n.destination-stack[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n.destination-actions[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 8px;\n}\n.node-assignment-stack[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n.node-assignment-counts[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n}\n.panel-action-button[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n min-height: 32px;\n padding: 6px 10px;\n border: 1px solid var(--ion-color-step-200, #d7d8da);\n border-radius: 6px;\n background: var(--ion-background-color, #fff);\n color: var(--ion-text-color);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n.panel-action-button.primary[_ngcontent-%COMP%] {\n border-color: var(--ion-color-primary);\n color: var(--ion-color-primary);\n}\n.panel-action-button.full-width[_ngcontent-%COMP%] {\n width: 100%;\n}\n.panel-action-button[_ngcontent-%COMP%]:disabled {\n opacity: 0.45;\n cursor: default;\n}\n.stats-grid[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 8px;\n}\n.stat-item[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 6px 8px;\n background: rgba(0, 0, 0, 0.02);\n border-radius: 4px;\n}\n.stat-label[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--ion-color-medium);\n}\n.stat-value[_ngcontent-%COMP%] {\n font-size: 13px;\n font-weight: 600;\n color: var(--ion-text-color);\n font-family: monospace;\n}"], changeDetection: 0 }); 18375var BuildingEditPanelComponent = _BuildingEditPanelComponent; 18376(() => { 18377 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(BuildingEditPanelComponent, [{ 18378 type: Component, 18379 args: [{ selector: "app-building-edit-panel", standalone: true, imports: [ 18380 CommonModule, 18381 FormsModule, 18382 NgIcon, 18383 EntityPanelComponent, 18384 EntitySectionComponent, 18385 EntityRowComponent 18386 ], viewProviders: [ 18387 provideIcons({ 18388 lucideBuilding2, 18389 lucideChartBar, 18390 lucideCircleX, 18391 lucideCrosshair, 18392 lucideMapPin, 18393 lucideNetwork 18394 }) 18395 ], changeDetection: ChangeDetectionStrategy.OnPush, template: '<app-entity-panel\n [title]="panelTitle"\n [applyText]="applyButtonText"\n (closeRequested)="onClose()"\n (cancelRequested)="onCancel()"\n (applyRequested)="onApply()">\n\n <ng-container panel-content>\n <!-- Building Details Section -->\n <app-entity-section label="Building Details">\n <ng-icon section-icon name="lucideBuilding2" size="16" />\n <div class="input-stack">\n <div class="input-group full">\n <label>NAME</label>\n <input\n type="text"\n [ngModel]="name"\n (ngModelChange)="onNameChange($event)"\n placeholder="e.g., Main Building"\n />\n </div>\n <div class="input-group full">\n <label>ADDRESS</label>\n <input\n type="text"\n [ngModel]="address"\
18395n (ngModelChange)="onAddressChange($event)"\n placeholder="e.g., 123 Clinic Ave"\n />\n </div>\n </div>\n </app-entity-section>\n\n @if (!isNew) {\n <app-entity-section label="Destination">\n <ng-icon section-icon name="lucideCrosshair" size="16" />\n <div class="destination-stack">\n <app-entity-row label="Default" [value]="currentDefaultDestinationLabel" />\n <div class="destination-actions">\n <button type="button" class="panel-action-button primary" (click)="onPickDefaultDestination()">\n <ng-icon name="lucideMapPin" size="14" />\n <span>Pick on map</span>\n </button>\n <button\n type="button"\n class="panel-action-button"\n [disabled]="!canClearDefaultDestination"\n (click)="onClearDefaultDestination()">\n <ng-icon name="lucideCircleX" size="14" />\n <span>Clear</span>\n </button>\n </div>\n </div>\n </app-entity-section>\n }\n\n @if (!isNew) {\n <app-entity-section label="Nodes">\n <ng-icon section-icon name="lucideNetwork" size="16" />\n <div class="node-assignment-stack">\n <div class="node-assignment-counts">\n <app-entity-row label="Assigned" [value]="assignedNodeCount.toString()" />\n <app-entity-row label="Eligible visible" [value]="eligibleNodeCount.toString()" />\n </div>\n <button type="button" class="panel-action-button primary full-width" (click)="onAssignNodes()">\n <ng-icon name="lucideNetwork" size="14" />\n <span>Assign on map</span>\n </button>\n </div>\n </app-entity-section>\n }\n\n <!-- Statistics Section (only in edit mode) -->\n @if (!isNew) {\n <app-entity-section label="Statistics">\n <ng-icon section-icon name="lucideChartBar" size="16" />\n <div class="stats-grid">\n <div class="stat-item">\n <span class="stat-label">Floors</span>\n <span class="stat-value">{{ floorCount }}</span>\n </div>\n <div class="stat-item">\n <span class="stat-label">Rooms</span>\n <span class="stat-value">{{ roomCount }}</span>\n </div>\n <div class="stat-item">\n <span class="stat-label">Nodes</span>\n <span class="stat-value">{{ nodeCount }}</span>\n </div>\n <div class="stat-item">\n <span class="stat-label">Stairwells</span>\n <span class="stat-value">{{ stairwellCount }}</span>\n </div>\n </div>\n </app-entity-section>\n }\n\n <!-- Location Info Section -->\n <app-entity-section label="Location">\n <ng-icon section-icon name="lucideMapPin" size="16" />\n <div class="location-info">\n <app-entity-row label="Campus" [value]="campusName" />\n @if (!isNew) {\n <app-entity-row label="ID" [value]="buildingId" [mono]="true" />\n }\n </div>\n </app-entity-section>\n </ng-container>\n\n <!-- Empty panel-info slot -->\n <ng-container panel-info></ng-container>\n\n</app-entity-panel>\n', styles: ["/* src/app/pages/campus-map-v3/components/building-edit-panel/building-edit-panel.component.scss */\n.input-stack {\n display: flex;\n flex-direction: column;\n gap: 8px;\n gap: 10px;\n}\n.input-group {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.input-group.full {\n width: 100%;\n}\n.input-group label {\n font-size: 11px;\n font-weight: 500;\n color: var(--ion-color-medium);\n text-transform: uppercase;\n}\n.input-group input {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n transition: border-color 0.2s ease;\n}\n.input-group input:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.input-group input[type=number] {\n font-family: monospace;\n}\n.input-group input::placeholder {\n color: var(--ion-color-medium-tint);\n font-style: italic;\n}\n.input-group input::-webkit-inner-spin-button,\n.input-group input::-webkit-outer-spin-button {\n opacity: 1;\n}\n.destination-stack {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n.destination-actions {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 8px;\n}\n.node-assignment-stack {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}
18395\n.node-assignment-counts {\n display: flex;\n flex-direction: column;\n}\n.panel-action-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n min-height: 32px;\n padding: 6px 10px;\n border: 1px solid var(--ion-color-step-200, #d7d8da);\n border-radius: 6px;\n background: var(--ion-background-color, #fff);\n color: var(--ion-text-color);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n.panel-action-button.primary {\n border-color: var(--ion-color-primary);\n color: var(--ion-color-primary);\n}\n.panel-action-button.full-width {\n width: 100%;\n}\n.panel-action-button:disabled {\n opacity: 0.45;\n cursor: default;\n}\n.stats-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 8px;\n}\n.stat-item {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 6px 8px;\n background: rgba(0, 0, 0, 0.02);\n border-radius: 4px;\n}\n.stat-label {\n font-size: 11px;\n color: var(--ion-color-medium);\n}\n.stat-value {\n font-size: 13px;\n font-weight: 600;\n color: var(--ion-text-color);\n font-family: monospace;\n}\n"] }] 18396 }], null, { building: [{ 18397 type: Input 18398 }], isNew: [{ 18399 type: Input 18400 }], assignedNodeCount: [{ 18401 type: Input 18402 }], eligibleNodeCount: [{ 18403 type: Input 18404 }], buildingChanged: [{ 18405 type: Output 18406 }], pickDefaultDestinationRequested: [{ 18407 type: Output 18408 }], clearDefaultDestinationRequested: [{ 18409 type: Output 18410 }], assignNodesRequested: [{ 18411 type: Output 18412 }], applyRequested: [{ 18413 type: Output 18414 }], cancelRequested: [{ 18415 type: Output 18416 }], closeRequested: [{ 18417 type: Output 18418 }] }); 18419})(); 18420(() => { 18421 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(BuildingEditPanelComponent, { className: "BuildingEditPanelComponent", filePath: "src/app/pages/campus-map-v3/components/building-edit-panel/building-edit-panel.component.ts", lineNumber: 62 }); 18422})(); 18423
18424// src/app/pages/campus-map-v3/components/room-edit-panel/room-edit-panel.component.ts 18425var _forTrack05 = ($index, $item) => $item.value; 18426var _forTrack12 = ($index, $item) => $item.entity.id; 18427function RoomEditPanelComponent_For_26_Template(rf, ctx) { 18428 if (rf & 1) { 18429 \u0275\u0275elementStart(0, "option", 13); 18430 \u0275\u0275text(1); 18431 \u0275\u0275elementEnd(); 18432 } 18433 if (rf & 2) { 18434 const option_r1 = ctx.$implicit; 18435 \u0275\u0275property("value", option_r1.value); 18436 \u0275\u0275advance(); 18437 \u0275\u0275textInterpolate(option_r1.label); 18438 } 18439} 18440function RoomEditPanelComponent_Conditional_32_Conditional_18_Template(rf, ctx) { 18441 if (rf & 1) { 18442 \u0275\u0275elementStart(0, "div", 30)(1, "span", 31); 18443 \u0275\u0275text(2, "Area"); 18444 \u0275\u0275elementEnd(); 18445 \u0275\u0275elementStart(3, "span", 32); 18446 \u0275\u0275text(4); 18447 \u0275\u0275elementEnd()(); 18448 } 18449 if (rf & 2) { 18450 const ctx_r1 = \u0275\u0275nextContext(2); 18451 \u0275\u0275advance(4); 18452 \u0275\u0275textInterpolate1("~", ctx_r1.area, " m\xB2"); 18453 } 18454} 18455function RoomEditPanelComponent_Conditional_32_Template(rf, ctx) { 18456 if (rf & 1) { 18457 \u0275\u0275elementStart(0, "app-entity-section", 18); 18458 \u0275\u0275element(1, "ng-icon", 28); 18459 \u0275\u0275elementStart(2, "div", 29)(3, "div", 30)(4, "span", 31); 18460 \u0275\u0275text(5, "Doors"); 18461 \u0275\u0275elementEnd(); 18462 \u0275\u0275elementStart(6, "span", 32); 18463 \u0275\u0275text(7); 18464 \u0275\u0275elementEnd()(); 18465 \u0275\u0275elementStart(8, "div", 30)(9, "span", 31); 18466 \u0275\u0275text(10, "Corners"); 18467 \u0275\u0275elementEnd(); 18468 \u0275\u0275elementStart(11, "span", 32); 18469 \u0275\u0275text(12); 18470 \u0275\u0275elementEnd()(); 18471 \u0275\u0275elementStart(13, "div", 30)(14, "span", 31); 18472 \u0275\u0275text(15, "People"); 18473 \u0275\u0275elementEnd(); 18474 \u0275\u0275elementStart(16, "span", 32); 18475 \u0275\u0275text(17); 18476 \u0275\u0275elementEnd()(); 18477 \u0275\u0275template(18, RoomEditPanelComponent_Conditional_32_Conditional_18_Template, 5, 1, "div", 30); 18478 \u0275\u0275elementEnd()(); 18479 } 18480 if (rf & 2) { 18481 const ctx_r1 = \u0275\u0275nextContext(); 18482 \u0275\u0275advance(7); 18483 \u0275\u0275textInterpolate(ctx_r1.doorCount); 18484 \u0275\u0275advance(5); 18485 \u0275\u0275textInterpolate(ctx_r1.cornerCount); 18486 \u0275\u0275advance(5); 18487 \u0275\u0275textInterpolate(ctx_r1.personCount); 18488 \u0275\u0275advance(); 18489 \u0275\u0275conditional(ctx_r1.area !== null ? 18 : -1); 18490 } 18491} 18492function RoomEditPanelComponent_Conditional_33_Conditional_3_For_2_Template(rf, ctx) { 18493 if (rf & 1) { 18494 const _r4 = \u0275\u0275getCurrentView(); 18495 \u0275\u0275elementStart(0, "div", 40)(1, "span", 41); 18496 \u0275\u0275text(2); 18497 \u0275\u0275elementEnd(); 18498 \u0275\u0275elementStart(3, "button", 42); 18499 \u0275\u0275listener("click", function RoomEditPanelComponent_Conditional_33_Conditional_3_For_2_Template_button_click_3_listener() { 18500 const pr_r5 = \u0275\u0275restoreView(_r4).$implicit; 18501 const ctx_r1 = \u0275\u0275nextContext(3); 18502 return \u0275\u0275resetView(ctx_r1.onRemovePerson(pr_r5)); 18503 }); 18504 \u0275\u0275element(4, "ng-icon", 43); 18505 \u0275\u0275elementEnd()(); 18506 } 18507 if (rf & 2) { 18508 let tmp_12_0; 18509 const pr_r5 = ctx.$implicit; 18510 \u0275\u0275advance(2); 18511 \u0275\u0275textInterpolate((tmp_12_0 = pr_r5.person == null ? null : pr_r5.person.entity == null ? null : pr_r5.person.entity.name) !== null && tmp_12_0 !== void 0 ? tmp_12_0 : "Unknown"); 18512 } 18513} 18514function RoomEditPanelComponent_Conditional_33_Conditional_3_Template(rf, ctx) { 18515 if (rf & 1) { 18516 \u0275\u0275elementStart(0, "div", 35); 18517 \u0275\u0275repeaterCreate(1, RoomEditPanelComponent_Conditional_33_Conditional_3_For_2_Template, 5, 1, "div", 40, _forTrack12); 18518 \u0275\u0275elementEnd(); 18519 } 18520 if (rf & 2) { 18521 const ctx_r1 = \u0275\u0275nextContext(2); 18522 \u0275\u0275advance(); 18523 \u0275\u0275repeater(ctx_r1.assignedPersons); 18524 } 18525} 18526function RoomEditPanelComponent_Conditional_33_Conditional_4_Template(rf, ctx) { 18527 if (rf & 1) { 18528 \u0275\u0275elementStart(0, "div", 36); 18529 \u0275\u0275text(1, "No people assigned"); 18530 \u0275\u0275elementEnd(); 18531 } 18532} 18533function RoomEditPanelComponent_Conditional_33_Template(rf, ctx) { 18534 if (rf & 1) { 18535 const _r3 = \u0275\u0275getCurrentView(); 18536 \u0275\u0275elementStart(0, "app-entity-section", 19); 18537 \u0275\u0275element(1, "ng-icon", 33); 18538 \u0275\u0275elementStart(2, "div", 34); 18539 \u0275\u0275template(3, RoomEditPanelComponent_Conditional_33_Conditional_3_Template, 3, 0, "div", 35)(4, RoomEditPanelComponent_Conditional_33_Conditional_4_Template, 2, 0, "div", 36); 18540 \u0275\u0275elementStart(5, "div", 37)(6, "button", 38); 18541 \u0275\u0275listener("click", function RoomEditPanelComponent_Conditional_33_Template_button_click_6_listener() { 18542 \u0275\u0275restoreView(_r3); 18543 const ctx_r1 = \u0275\u0275nextContext();
18544 return \u0275\u0275resetView(ctx_r1.openPersonManagementModal()); 18545 }); 18546 \u0275\u0275element(7, "ng-icon", 39); 18547 \u0275\u0275text(8, " Manage People "); 18548 \u0275\u0275elementEnd()()()(); 18549 } 18550 if (rf & 2) { 18551 const ctx_r1 = \u0275\u0275nextContext(); 18552 \u0275\u0275advance(3); 18553 \u0275\u0275conditional(ctx_r1.assignedPersons.length > 0 ? 3 : 4); 18554 } 18555} 18556function RoomEditPanelComponent_Conditional_40_Template(rf, ctx) { 18557 if (rf & 1) { 18558 \u0275\u0275element(0, "app-entity-row", 26); 18559 } 18560 if (rf & 2) { 18561 const ctx_r1 = \u0275\u0275nextContext(); 18562 \u0275\u0275property("value", ctx_r1.roomId)("mono", true); 18563 } 18564} 18565var _RoomEditPanelComponent = class _RoomEditPanelComponent { 18566 constructor(modalController) { 18567 this.modalController = modalController; 18568 this.room = null; 18569 this.isNew = false; 18570 this.allPersons = []; 18571 this.roomChanged = new EventEmitter(); 18572 this.applyRequested = new EventEmitter(); 18573 this.cancelRequested = new EventEmitter(); 18574 this.closeRequested = new EventEmitter(); 18575 this.personAssigned = new EventEmitter(); 18576 this.personRemoved = new EventEmitter(); 18577 this.personCreated = new EventEmitter(); 18578 this.number = null; 18579 this.name = null; 18580 this.description = null; 18581 this.roomType = RoomType.Regular; 18582 this.hideLabel = false; 18583 this.roomTypeOptions = [ 18584 { value: RoomType.Regular, label: "Regular", icon: "lucideDoorOpen" }, 18585 { value: RoomType.Office, label: "Office", icon: "lucideBriefcase" }, 18586 { value: RoomType.MeetingRoom, label: "Meeting Room", icon: "lucideUsers" }, 18587 { value: RoomType.Restroom, label: "Restroom", icon: "lucideShowerHead" }, 18588 { value: RoomType.Elevator, label: "Elevator", icon: "lucideArrowUpDown" }, 18589 { value: RoomType.Stairs, label: "Stairs", icon: "lucideArrowUpFromLine" }, 18590 { value: RoomType.Cafeteria, label: "Cafeteria", icon: "lucideUtensils" }, 18591 { value: RoomType.Emergency, label: "Emergency", icon: "lucideTriangleAlert" }, 18592 { value: RoomType.Information, label: "Information", icon: "lucideInfo" } 18593 ]; 18594 } 18595 /** 18596 * Get the icon for the currently selected room type. 18597 */ 18598 get currentRoomTypeIcon() { 18599 var _a3; 18600 const option = this.roomTypeOptions.find((o) => o.value === this.roomType); 18601 return (_a3 = option == null ? void 0 : option.icon) != null ? _a3 : "lucideDoorOpen"; 18602 } 18603 /** 18604 * Get persons available for assignment (not already assigned to this room). 18605 */ 18606 get availablePersons() { 18607 var _a3, _b2, _c; 18608 const assignedIds = new Set((_c = (_b2 = (_a3 = this.room) == null ? void 0 : _a3.personRooms) == null ? void 0 : _b2.filter((pr) => !pr.entity.isDeleted).map((pr) => pr.entity.personId)) != null ? _c : []); 18609 return this.allPersons.filter((p) => !p.entity.isDeleted && !assignedIds.has(p.entity.id)); 18610 } 18611 /** 18612 * Get currently assigned persons (non-deleted). 18613 */ 18614 get assignedPersons() { 18615 var _a3, _b2, _c; 18616 return (_c = (_b2 = (_a3 = this.room) == null ? void 0 : _a3.personRooms) == null ? void 0 : _b2.filter((pr) => !pr.entity.isDeleted)) != null ? _c : []; 18617 } 18618 // ========================================================================= 18619 // Lifecycle Hooks 18620 // ========================================================================= 18621 ngOnChanges(changes) { 18622 if (changes["room"] && this.room) { 18623 this.syncFromRoom(); 18624 } 18625 } 18626 /** 18627 * Sync local form values from room model. 18628 */ 18629 syncFromRoom() { 18630 var _a3, _b2, _c, _d, _e; 18631 if (!this.room) 18632 return; 18633 this.number = (_a3 = this.room.entity.number) != null ? _a3 : null; 18634 this.name = (_b2 = this.room.entity.name) != null ? _b2 : null; 18635 this.description = (_c = this.room.entity.description) != null ? _c : null; 18636 this.roomType = (_d = this.room.entity.roomType) != null ? _d : RoomType.Regular; 18637 this.hideLabel = (_e = this.room.entity.hideLabel) != null ? _e : false; 18638 } 18639 // ========================================================================= 18640 // Computed Read-Only Statistics 18641 // ========================================================================= 18642 get doorCount() { 18643 var _a3, _b2, _c; 18644 return (_c = (_b2 = (_a3 = this.room) == null ? void 0 : _a3.doors) == null ? void 0 : _b2.length) != null ? _c : 0; 18645 } 18646 get cornerCount() { 18647 var _a3, _b2, _c; 18648 return (_c = (_b2 = (_a3 = this.room) == null ? void 0 : _a3.corners) == null ? void 0 : _b2.length) != null ? _c : 0; 18649 } 18650 get personCount() { 18651 return this.assignedPersons.length; 18652 } 18653 /** 18654 * Calculate approximate area from room corners. 18655 * Returns area in square meters or null if no corners. 18656 */ 18657 get area() { 18658 var _a3, _b2, _c, _d, _e; 18659 const corners = (_a3 = this.room) == null ? void 0 : _a3.corners; 18660 if (!corners || corners.length < 3) 18661 return null; 18662 let sum = 0; 18663 for (let i = 0; i < corners.length; i++) { 18664 const curr = corners[i]; 18665 const next = corners[(i + 1) % corners.length]; 18666 const x1 = (_b2 = curr.entity.locationX) != null ? _b2 : 0; 18667 const y1 = (_c = curr.entity.locationY) != null ? _c : 0; 18668 const x2 = (_d = next.entity.locationX) != null ? _d : 0; 18669 const y2 = (_e = next.entity.locationY) != null ? _e : 0; 18670 sum += x1 * y2 - x2 * y1; 18671 } 18672 const areaCm2 = Math.abs(sum) / 2; 18673 const areaM2 = areaCm2 / 1e4; 18674 return Math.round(areaM2); 18675 } 18676 get buildingName() { 18677 var _a3, _b2, _c; 18678 return (_c = (_b2 = (_a3 = this.room) == null ? void 0 : _a3.building) == null ? void 0 : _b2.entity.name) != null ? _c : "Unknown Bu
18678ilding"; 18679 } 18680 get floorName() { 18681 var _a3, _b2; 18682 const floor = (_a3 = this.room) == null ? void 0 : _a3.floor; 18683 if (!floor) 18684 return "Unknown Floor"; 18685 const levelDisplay = floor.entity.level === 1 ? "G" : `${floor.entity.level}`; 18686 return (_b2 = floor.entity.name) != null ? _b2 : `Level ${levelDisplay}`; 18687 } 18688 get campusName() { 18689 var _a3, _b2, _c; 18690 return (_c = (_b2 = (_a3 = this.room) == null ? void 0 : _a3.campus) == null ? void 0 : _b2.entity.name) != null ? _c : "Unknown Campus"; 18691 } 18692 get roomId() { 18693 var _a3, _b2; 18694 return (_b2 = (_a3 = this.room) == null ? void 0 : _a3.entity.id) != null ? _b2 : ""; 18695 } 18696 get panelTitle() { 18697 return this.isNew ? "New Room" : "Edit Room"; 18698 } 18699 get applyButtonText() { 18700 return this.isNew ? "Create" : "Apply"; 18701 } 18702 // ========================================================================= 18703 // Event Handlers 18704 // ========================================================================= 18705 onNumberChange(value) { 18706 this.number = value || null; 18707 this.roomChanged.emit({ number: this.number }); 18708 } 18709 onNameChange(value) { 18710 this.name = value || null; 18711 this.roomChanged.emit({ name: this.name }); 18712 } 18713 onDescriptionChange(value) { 18714 this.description = value || null; 18715 this.roomChanged.emit({ description: this.description }); 18716 } 18717 onRoomTypeChange(value) { 18718 this.roomType = value; 18719 this.roomChanged.emit({ roomType: this.roomType }); 18720 } 18721 onHideLabelChange(value) { 18722 this.hideLabel = value; 18723 this.roomChanged.emit({ hideLabel: this.hideLabel }); 18724 } 18725 // ========================================================================= 18726 // Person Management 18727 // ========================================================================= 18728 /** 18729 * Open the person management modal to search/create persons. 18730 */ 18731 openPersonManagementModal() { 18732 return __async(this, null, function* () { 18733 var _a3, _b2; 18734 if (!((_a3 = this.room) == null ? void 0 : _a3.campus) || !((_b2 = this.room) == null ? void 0 : _b2.building)) { 18735 return; 18736 } 18737 const modal = yield this.modalController.create({ 18738 component: PersonManagementModal, 18739 componentProps: { 18740 availablePersons: this.availablePersons, 18741 campus: this.room.campus, 18742 building: this.room.building 18743 }, 18744 cssClass: "person-management-modal" 18745 }); 18746 yield modal.present(); 18747 const { data, role } = yield modal.onWillDismiss(); 18748 if (role === "success" && data) { 18749 if (data.action === "selected" || data.action === "created") { 18750 this.personAssigned.emit({ personId: data.person.entity.id }); 18751 } 18752 } 18753 }); 18754 } 18755 /** 18756 * Remove a person assignment from this room. 18757 */ 18758 onRemovePerson(personRoom) { 18759 this.personRemoved.emit({ personRoomId: personRoom.entity.id }); 18760 } 18761 onApply() { 18762 this.applyRequested.emit(); 18763 } 18764 onCancel() { 18765 this.cancelRequested.emit(); 18766 } 18767 onClose() { 18768 this.closeRequested.emit(); 18769 } 18770}; 18771_RoomEditPanelComponent.\u0275fac = function RoomEditPanelComponent_Factory(__ngFactoryType__) { 18772 return new (__ngFactoryType__ || _RoomEditPanelComponent)(\u0275\u0275directiveInject(ModalController)); 18773}; 18774_RoomEditPanelComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _RoomEditPanelComponent, selectors: [["app-room-edit-panel"]], inputs: { room: "room", isNew: "isNew", allPersons: "allPersons" }, outputs: { roomChanged: "roomChanged", applyRequested: "applyRequested", cancelRequested: "cancelRequested", closeRequested: "closeRequested", personAssigned: "personAssigned", personRemoved: "personRemoved", personCreated: "personCreated" }, features: [\u0275\u0275ProvidersFeature([], [ 18775 provideIcons({ 18776 lucideBuilding2, 18777 lucideChartBar, 18778 lucideMapPin, 18779 lucideSettings, 18780 lucideDoorOpen, 18781 lucideBriefcase, 18782 lucideUsers, 18783 lucideArrowUpDown, 18784 lucideUtensils, 18785 lucideTriangleAlert, 18786 lucideInfo, 18787 lucideArrowUpFromLine, 18788 lucideShowerHead, 18789 lucideUserPlus, 18790 lucideX 18791 }) 18792]), \u0275\u0275NgOnChangesFeature], decls: 42, vars: 14, consts: [[3, "closeRequested", "cancelRequested", "applyRequested", "title", "applyText"], ["panel-content", ""], ["label", "Room Details"], ["section-icon", "", "name", "lucideBuilding2", "size", "16"], [1, "input-stack"], [1, "input-group", "full"], ["type", "text", "placeholder", "e.g., 101", 3, "ngModelChange", "ngModel"], ["type", "text", "placeholder", "e.g., Conference Room A", 3, "ngModelChange", "ngModel"], ["placeholder", "Additional room details...", "rows", "2", 3, "ngModelChange", "ngModel"], ["label", "Type & Display"], ["section-icon", "", "name", "lucideSettings", "size", "16"], [1, "select-wrapper"], [3, "ngModelChange", "ngModel"], [3, "value"], ["size", "16", 1, "select-icon", 3, "name"], [1, "toggle-row"], ["type", "checkbox", 3, "ngModelChange", "ngModel"], [1, "toggle-label"], ["label", "Statistics"], ["label", "People"], ["label", "Location"], ["section-icon", "", "name", "lucideMapPin", "size", "16"], [1, "location-info"], ["label", "Building", 3, "value"], ["label", "Floor", 3, "value"], ["label", "Campus", 3, "value"], ["label", "ID", 3, "value", "mono"], ["panel-info", ""], ["section-icon", "", "name", "lucideChartBar", "size", "16"], [1, "stats-grid"], [1, "stat-item"], [1, "stat-label"], [1, "stat-value"], ["section-icon", "", "name", "lucideUsers", "size", "16"], [1, "persons-section"], [1, "assigned-persons-list"], [1, "empty-state"], [1, "person-actions"], ["type", "button", 1, "action-btn", 3, "click"], ["name", "lucideUserPlus", "size", "14"], [1, "person-item"], [1, "person-name"], ["type", "button", "title", "Remove person", 1, "remove-btn", 3, "click"], ["name", "lucideX", "size", "14"]], template: function RoomEditPanelComponent_Template(rf, ctx) { 18793 if (rf & 1) { 18794 \u0275\u0275elementStart(0, "app-entity-panel", 0); 18795 \u0275\u0275listener("closeRequested", function RoomEditPanelComponent_Template_app_entity_panel_closeRequested_0_listener() { 18796 return ctx.onClose(); 18797 })("cancelRequested", function RoomEditPanelComponent_Template_app_entity_panel_cancelRequested_0_listener() { 18798 return ctx.onCancel(); 18799 })("applyRequested", function RoomEditPanelComponent_Template_app_entity_panel_applyRequested_0_listener() { 18800 return ctx.onApply(); 18801 }); 18802 \u0275\u0275elementContainerStart(1, 1); 18803 \u0275\u0275elementStart(2, "app-entity-section", 2); 18804 \u0275\u0275element(3, "ng-icon", 3); 18805 \u0275\u0275elementStart(4, "div", 4)(5, "div", 5)(6, "label"); 18806 \u0275\u0275text(7, "NUMBER"); 18807 \u0275\u0275elementEnd(); 18808 \u0275\u0275elementStart(8, "input", 6); 18809 \u0275\u0275listener("ngModelChange", function RoomEditPanelComponent_Template_input_ngModelChange_8_listener($event) { 18810 return ctx.onNumberChange($event); 18811 }); 18812 \u0275\u0275elementEnd()(); 18813 \u0275\u0275elementStart(9, "div", 5)(10, "label"); 18814 \u0275\u0275text(11, "NAME"); 18815 \u0275\u0275elementEnd(); 18816 \u0275\u0275elementStart(12, "input", 7); 18817 \u0275\u0275listener("ngModelChange", function RoomEditPanelComponent_Template_input_ngModelChange_12_listener($event) { 18818 return ctx.onNameChange($event); 18819 }); 18820 \u0275\u0275elementEnd()(); 18821 \u0275\u0275elementStart(13, "div", 5)(14, "label"); 18822 \u0275\u0275text(15, "DESCRIPTION"); 18823 \u0275\u0275elementEnd(); 18824 \u0275\u0275elementStart(16, "textarea", 8); 18825 \u0275\u0275listener("ngModelChange", function RoomEditPanelComponent_Template_textarea_ngModelChange_16_listener($event) { 18826 return ctx.onDescriptionChange($event); 18827 }); 18828 \u0275\u0275elementEnd()()()(); 18829 \u0275\u0275elementStart(17, "app-entity-section", 9); 18830 \u0275\u0275element(18, "ng-icon", 10); 18831 \u0275\u0275elementStart(19, "div", 4)(20, "div", 5)(21, "label"); 18832 \u0275\u0275text(22, "ROOM TYPE"); 18833 \u0275\u0275elementEnd(); 18834 \u0275\u0275elementStart(23, "div", 11)(24, "select", 12); 18835 \u0275\u0275listener("ngModelChange", function RoomEditPanelComponent_Template_select_ngModelChange_24_listener($event) { 18836 return ctx.onRoomTypeChange($event); 18837 }); 18838 \u0275\u0275repeaterCreate(25, RoomEditPanelComponent_For_26_Template, 2, 2, "option", 13, _forTrack05); 18839 \u0275\u0275elementEnd(); 18840 \u0275\u0275element(27, "ng-icon", 14); 18841 \u0275\u0275elementEnd()();
18842 \u0275\u0275elementStart(28, "label", 15)(29, "input", 16); 18843 \u0275\u0275listener("ngModelChange", function RoomEditPanelComponent_Template_input_ngModelChange_29_listener($event) { 18844 return ctx.onHideLabelChange($event); 18845 }); 18846 \u0275\u0275elementEnd(); 18847 \u0275\u0275elementStart(30, "span", 17); 18848 \u0275\u0275text(31, "Hide label in navigation"); 18849 \u0275\u0275elementEnd()()()(); 18850 \u0275\u0275template(32, RoomEditPanelComponent_Conditional_32_Template, 19, 4, "app-entity-section", 18)(33, RoomEditPanelComponent_Conditional_33_Template, 9, 1, "app-entity-section", 19); 18851 \u0275\u0275elementStart(34, "app-entity-section", 20); 18852 \u0275\u0275element(35, "ng-icon", 21); 18853 \u0275\u0275elementStart(36, "div", 22); 18854 \u0275\u0275element(37, "app-entity-row", 23)(38, "app-entity-row", 24)(39, "app-entity-row", 25); 18855 \u0275\u0275template(40, RoomEditPanelComponent_Conditional_40_Template, 1, 2, "app-entity-row", 26); 18856 \u0275\u0275elementEnd()(); 18857 \u0275\u0275elementContainerEnd(); 18858 \u0275\u0275elementContainer(41, 27); 18859 \u0275\u0275elementEnd(); 18860 } 18861 if (rf & 2) { 18862 \u0275\u0275property("title", ctx.panelTitle)("applyText", ctx.applyButtonText); 18863 \u0275\u0275advance(8); 18864 \u0275\u0275property("ngModel", ctx.number); 18865 \u0275\u0275advance(4); 18866 \u0275\u0275property("ngModel", ctx.name); 18867 \u0275\u0275advance(4); 18868 \u0275\u0275property("ngModel", ctx.description); 18869 \u0275\u0275advance(8); 18870 \u0275\u0275property("ngModel", ctx.roomType); 18871 \u0275\u0275advance(); 18872 \u0275\u0275repeater(ctx.roomTypeOptions); 18873 \u0275\u0275advance(2); 18874 \u0275\u0275property("name", ctx.currentRoomTypeIcon); 18875 \u0275\u0275advance(2); 18876 \u0275\u0275property("ngModel", ctx.hideLabel); 18877 \u0275\u0275advance(3); 18878 \u0275\u0275conditional(!ctx.isNew ? 32 : -1); 18879 \u0275\u0275advance(); 18880 \u0275\u0275conditional(!ctx.isNew ? 33 : -1); 18881 \u0275\u0275advance(4); 18882 \u0275\u0275property("value", ctx.buildingName); 18883 \u0275\u0275advance(); 18884 \u0275\u0275property("value", ctx.floorName); 18885 \u0275\u0275advance(); 18886 \u0275\u0275property("value", ctx.campusName); 18887 \u0275\u0275advance(); 18888 \u0275\u0275conditional(!ctx.isNew ? 40 : -1); 18889 } 18890}, dependencies: [ 18891 CommonModule, 18892 FormsModule, 18893 NgSelectOption, 18894 \u0275NgSelectMultipleOption, 18895 DefaultValueAccessor, 18896 CheckboxControlValueAccessor, 18897 SelectControlValueAccessor, 18898 NgControlStatus, 18899 NgModel, 18900 NgIcon, 18901 EntityPanelComponent, 18902 EntitySectionComponent, 18903 EntityRowComponent 18904], styles: ["\n\n.input-stack[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n gap: 10px;\n}\n.input-group[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.input-group.full[_ngcontent-%COMP%] {\n width: 100%;\n}\n.input-group[_ngcontent-%COMP%] label[_ngcontent-%COMP%] {\n font-size: 11px;\n font-weight: 500;\n color: var(--ion-color-medium);\n text-transform: uppercase;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%] {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n transition: border-color 0.2s ease;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.input-group[_ngcontent-%COMP%] input[type=number][_ngcontent-%COMP%] {\n font-family: monospace;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::placeholder {\n color: var(--ion-color-medium-tint);\n font-style: italic;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::-webkit-inner-spin-button, \n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::-webkit-outer-spin-button {\n opacity: 1;\n}\n.input-group[_ngcontent-%COMP%] textarea[_ngcontent-%COMP%] {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n font-family: inherit;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n resize: vertical;\n min-height: 50px;\n transition: border-color 0.2s ease;\n}\n.input-group[_ngcontent-%COMP%] textarea[_ngcontent-%COMP%]:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.input-group[_ngcontent-%COMP%] textarea[_ngcontent-%COMP%]::placeholder {\n color: var(--ion-color-medium-tint);\n font-style: italic;\n}\n.select-wrapper[_ngcontent-%COMP%] {\n position: relative;\n display: flex;\n align-items: center;\n}\n.select-wrapper[_ngcontent-%COMP%] select[_ngcontent-%COMP%] {\n width: 100%;\n padding: 6px 8px;\n padding-left: 32px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n cursor: pointer;\n -webkit-appearance: auto;\n -moz-appearance: auto;\n appearance: auto;\n transition: border-color 0.2s ease;\n}\n.select-wrapper[_ngcontent-%COMP%] select[_ngcontent-%COMP%]:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.select-wrapper[_ngcontent-%COMP%] .select-icon[_ngcontent-%COMP%] {\n position: absolute;\n left: 8px;\n pointer-events: none;\n color: var(--ion-color-primary);\n}\n.toggle-row[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n padding: 4px 0;\n}\n.toggle-row[_ngcontent-%COMP%] input[type=checkbox][_ngcontent-%COMP%] {\n width: 16px;\n height: 16px;\n cursor: pointer;\n accent-color: var(--ion-color-primary);\n}\n.toggle-row[_ngcontent-%COMP%] .toggle-label[_ngcontent-%COMP%] {\n font-size: 13px;\n color: var(--ion-text-color);\n -webkit-user-select: none;\n user-select: none;\n}\n.stats-grid[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 8px;\n}\n.stat-item[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 6px 8px;\n background: rgba(0, 0, 0, 0.02);\n border-radius: 4px;\n}\n.stat-label[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--ion-color-medium);\n}\n.stat-value[_ngcontent-%COMP%] {\n font-size: 13px;\n font-weight: 600;\n color: var(--ion-text-color);\n font-family: monospace;\n}\n.persons-section[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.assigned-persons-list[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.person-item[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 8px;\n background: rgba(0, 0, 0, 0.02);\n border-radius: 4px;\n}
18904\n.person-item[_ngcontent-%COMP%] .person-name[_ngcontent-%COMP%] {\n font-size: 13px;\n color: var(--ion-text-color);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n flex: 1;\n min-width: 0;\n}\n.person-item[_ngcontent-%COMP%] .remove-btn[_ngcontent-%COMP%] {\n background: transparent;\n border: none;\n padding: 4px;\n cursor: pointer;\n color: var(--ion-color-medium);\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: all 0.2s ease;\n flex-shrink: 0;\n}\n.person-item[_ngcontent-%COMP%] .remove-btn[_ngcontent-%COMP%]:hover {\n color: var(--ion-color-danger);\n background: rgba(var(--ion-color-danger-rgb), 0.1);\n}\n.empty-state[_ngcontent-%COMP%] {\n font-size: 12px;\n color: var(--ion-color-medium);\n padding: 8px;\n text-align: center;\n font-style: italic;\n}\n.person-actions[_ngcontent-%COMP%] {\n display: flex;\n gap: 8px;\n margin-top: 4px;\n}\n.action-btn[_ngcontent-%COMP%] {\n flex: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 6px 10px;\n border: 1px solid var(--ion-color-primary);\n border-radius: 6px;\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n color: var(--ion-color-primary);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.2s ease;\n}\n.action-btn[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: rgba(var(--ion-color-primary-rgb), 0.2);\n}\n.action-btn[_ngcontent-%COMP%]:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}"], changeDetection: 0 }); 18905var RoomEditPanelComponent = _RoomEditPanelComponent; 18906(() => { 18907 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(RoomEditPanelComponent, [{ 18908 type: Component, 18909 args: [{ selector: "app-room-edit-panel", standalone: true, imports: [ 18910 CommonModule, 18911 FormsModule, 18912 NgIcon, 18913 EntityPanelComponent, 18914 EntitySectionComponent, 18915 EntityRowComponent 18916 ], viewProviders: [ 18917 provideIcons({ 18918 lucideBuilding2, 18919 lucideChartBar, 18920 lucideMapPin, 18921 lucideSettings, 18922 lucideDoorOpen, 18923 lucideBriefcase, 18924 lucideUsers, 18925 lucideArrowUpDown, 18926 lucideUtensils, 18927 lucideTriangleAlert, 18928 lucideInfo, 18929 lucideArrowUpFromLine, 18930 lucideShowerHead, 18931 lucideUserPlus, 18932 lucideX 18933 }) 18934 ], changeDetection: ChangeDetectionStrategy.OnPush, template: `<app-entity-panel 18935 [title]="panelTitle" 18936 [applyText]="applyButtonText" 18937 (closeRequested)="onClose()" 18938 (cancelRequested)="onCancel()" 18939 (applyRequested)="onApply()"> 18940 18941 <ng-container panel-content> 18942 <!-- Room Details Section --> 18943 <app-entity-section label="Room Details"> 18944 <ng-icon section-icon name="lucideBuilding2" size="16" /> 18945 <div class="input-stack"> 18946 <div class="input-group full"> 18947 <label>NUMBER</label> 18948 <input 18949 type="text" 18950 [ngModel]="number" 18951 (ngModelChange)="onNumberChange($event)" 18952 placeholder="e.g., 101" 18953 /> 18954 </div> 18955 <div class="input-group full"> 18956 <label>NAME</label> 18957 <input 18958 type="text" 18959 [ngModel]="name" 18960 (ngModelChange)="onNameChange($event)" 18961 placeholder="e.g., Conference Room A" 18962 /> 18963 </div> 18964 <div class="input-group full"> 18965 <label>DESCRIPTION</label> 18966 <textarea 18967 [ngModel]="description" 18968 (ngModelChange)="onDescriptionChange($event)" 18969 placeholder="Additional room details..." 18970 rows="2" 18971 ></textarea> 18972 </div> 18973 </div> 18974 </app-entity-section> 18975 18976 <!-- Type & Display Section --> 18977 <app-entity-section label="Type & Display"> 18978 <ng-icon section-icon name="lucideSettings" size="16" /> 18979 <div class="input-stack"> 18980 <div class="input-group full"> 18981 <label>ROOM TYPE</label> 18982 <div class="select-wrapper"> 18983 <select 18984 [ngModel]="roomType" 18985 (ngModelChange)="onRoomTypeChange($event)" 18986 > 18987 @for (option of roomTypeOptions; track option.value) { 18988 <option [value]="option.value">{{ option.label }}</option> 18989 } 18990 </select> 18991 <ng-icon 18992 class="select-icon" 18993 [name]="currentRoomTypeIcon" 18994 size="16" 18995 /> 18996 </div> 18997 </div> 18998 <label class="toggle-row"> 18999 <input 19000 type="checkbox" 19001 [ngModel]="hideLabel" 19002 (ngModelChange)="onHideLabelChange($event)" 19003 />
19004 <span class="toggle-label">Hide label in navigation</span> 19005 </label> 19006 </div> 19007 </app-entity-section> 19008 19009 <!-- Statistics Section (edit mode only) --> 19010 @if (!isNew) { 19011 <app-entity-section label="Statistics"> 19012 <ng-icon section-icon name="lucideChartBar" size="16" /> 19013 <div class="stats-grid"> 19014 <div class="stat-item"> 19015 <span class="stat-label">Doors</span> 19016 <span class="stat-value">{{ doorCount }}</span> 19017 </div> 19018 <div class="stat-item"> 19019 <span class="stat-label">Corners</span> 19020 <span class="stat-value">{{ cornerCount }}</span> 19021 </div> 19022 <div class="stat-item"> 19023 <span class="stat-label">People</span> 19024 <span class="stat-value">{{ personCount }}</span> 19025 </div> 19026 @if (area !== null) { 19027 <div class="stat-item"> 19028 <span class="stat-label">Area</span> 19029 <span class="stat-value">~{{ area }} m\xB2</span> 19030 </div> 19031 } 19032 </div> 19033 </app-entity-section> 19034 } 19035 19036 <!-- People Section (edit mode only) --> 19037 @if (!isNew) { 19038 <app-entity-section label="People"> 19039 <ng-icon section-icon name="lucideUsers" size="16" /> 19040 19041 <div class="persons-section"> 19042 <!-- Assigned persons list --> 19043 @if (assignedPersons.length > 0) { 19044 <div class="assigned-persons-list"> 19045 @for (pr of assignedPersons; track pr.entity.id) { 19046 <div class="person-item"> 19047 <span class="person-name">{{ pr.person?.entity?.name ?? 'Unknown' }}</span> 19048 <button 19049 class="remove-btn" 19050 type="button" 19051 title="Remove person" 19052 (click)="onRemovePerson(pr)"> 19053 <ng-icon name="lucideX" size="14" /> 19054 </button> 19055 </div> 19056 } 19057 </div> 19058 } @else { 19059 <div class="empty-state">No people assigned</div> 19060 } 19061 19062 <!-- Single action button to open modal --> 19063 <div class="person-actions"> 19064 <button 19065 class="action-btn" 19066 type="button" 19067 (click)="openPersonManagementModal()"> 19068 <ng-icon name="lucideUserPlus" size="14" /> 19069 Manage People 19070 </button> 19071 </div> 19072 </div> 19073 </app-entity-section> 19074 } 19075 19076 <!-- Location Info Section --> 19077 <app-entity-section label="Location"> 19078 <ng-icon section-icon name="lucideMapPin" size="16" /> 19079 <div class="location-info"> 19080 <app-entity-row label="Building" [value]="buildingName" /> 19081 <app-entity-row label="Floor" [value]="floorName" /> 19082 <app-entity-row label="Campus" [value]="campusName" /> 19083 @if (!isNew) { 19084 <app-entity-row label="ID" [value]="roomId" [mono]="true" /> 19085 } 19086 </div> 19087 </app-entity-section> 19088 </ng-container> 19089 19090 <!-- Empty panel-info slot --> 19091 <ng-container panel-info></ng-container> 19092 19093</app-entity-panel> 19094`, styles: ["/* src/app/pages/campus-map-v3/components/room-edit-panel/room-edit-panel.component.scss */\n.input-stack {\n display: flex;\n flex-direction: column;\n gap: 8px;\n gap: 10px;\n}\n.input-group {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.input-group.full {\n width: 100%;\n}\n.input-group label {\n font-size: 11px;\n font-weight: 500;\n color: var(--ion-color-medium);\n text-transform: uppercase;\n}\n.input-group input {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n transition: border-color 0.2s ease;\n}\n.input-group input:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.input-group input[type=number] {\n font-family: monospace;\n}\n.input-group input::placeholder {\n color: var(--ion-color-medium-tint);\n font-style: italic;\n}\n.input-group input::-webkit-inner-spin-button,\n.input-group input::-webkit-outer-spin-button {\n opacity: 1;\n}\n.input-group textarea {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n font-family: inherit;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n resize: vertical;\n min-height: 50px;\n transition: border-color 0.2s ease;\n}\n.input-group textarea:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.input-group textarea::placeholder {\n color: var(--ion-color-medium-tint);\n font-style: italic;\n}\n.select-wrapper {\n position: relative;\n display: flex;\n align-items: center;\n}\n.select-wrapper select {\n width: 100%;\n padding: 6px 8px;\n padding-left: 32px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n cursor: pointer;\n -webkit-appearance: auto;\n -moz-appearance: auto;\n appearance: auto;\n transition: border-color 0.2s ease;\n}\n.select-wrapper select:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.select-wrapper .select-icon {\n position: absolute;\n left: 8px;\n pointer-events: none;\n color: var(--ion-color-primary);\n}\n.toggle-row {\n display: flex;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n padding: 4px 0;\n}\n.toggle-row input[type=checkbox] {\n width: 16px;\n height: 16px;\n cursor: pointer;\n accent-color: var(--ion-color-primary);\n}\n.toggle-row .toggle-label {\n font-size: 13px;\n color: var(--ion-text-color);\n -webkit-user-select: none;\n user-select: none;\n}\n.stats-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 8px;\n}\n.stat-item {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 6px 8px;\n background: rgba(0, 0, 0, 0.02);\n border-radius: 4px;\n}\n.stat-label {\n font-size: 11px;\n color: var(--ion-color-medium);\n}\n.stat-value {\n font-size: 13px;\n font-weight: 600;\n color: var(--ion-text-color);\n font-family: monospace;\n}\n.persons-section {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.assigned-persons-list {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.person-item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 8px;\n background: rgba(0, 0, 0, 0.02);\n border-radius: 4px;\n}\n.person-item .person-name {\n font-size: 13px;\n color: var(--ion-text-color);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n flex: 1;\n min-width: 0;\n}\n.person-item .remove-btn {\n background: transparent;\n border: none;\n padding: 4px;\n cursor: pointer;
19094\n color: var(--ion-color-medium);\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: all 0.2s ease;\n flex-shrink: 0;\n}\n.person-item .remove-btn:hover {\n color: var(--ion-color-danger);\n background: rgba(var(--ion-color-danger-rgb), 0.1);\n}\n.empty-state {\n font-size: 12px;\n color: var(--ion-color-medium);\n padding: 8px;\n text-align: center;\n font-style: italic;\n}\n.person-actions {\n display: flex;\n gap: 8px;\n margin-top: 4px;\n}\n.action-btn {\n flex: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 6px 10px;\n border: 1px solid var(--ion-color-primary);\n border-radius: 6px;\n background: rgba(var(--ion-color-primary-rgb), 0.1);\n color: var(--ion-color-primary);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.2s ease;\n}\n.action-btn:hover:not(:disabled) {\n background: rgba(var(--ion-color-primary-rgb), 0.2);\n}\n.action-btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n"] }] 19095 }], () => [{ type: ModalController }], { room: [{ 19096 type: Input 19097 }], isNew: [{ 19098 type: Input 19099 }], allPersons: [{ 19100 type: Input 19101 }], roomChanged: [{ 19102 type: Output 19103 }], applyRequested: [{ 19104 type: Output 19105 }], cancelRequested: [{ 19106 type: Output 19107 }], closeRequested: [{ 19108 type: Output 19109 }], personAssigned: [{ 19110 type: Output 19111 }], personRemoved: [{ 19112 type: Output 19113 }], personCreated: [{ 19114 type: Output 19115 }] }); 19116})(); 19117(() => { 19118 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(RoomEditPanelComponent, { className: "RoomEditPanelComponent", filePath: "src/app/pages/campus-map-v3/components/room-edit-panel/room-edit-panel.component.ts", lineNumber: 85 }); 19119})(); 19120
19121// src/app/pages/campus-map-v3/components/ground-patch-edit-panel/ground-patch-edit-panel.component.ts 19122var _forTrack06 = ($index, $item) => $item.value; 19123function GroundPatchEditPanelComponent_For_11_Template(rf, ctx) { 19124 if (rf & 1) { 19125 \u0275\u0275elementStart(0, "option", 8); 19126 \u0275\u0275text(1); 19127 \u0275\u0275elementEnd(); 19128 } 19129 if (rf & 2) { 19130 const option_r1 = ctx.$implicit; 19131 \u0275\u0275property("value", option_r1.value); 19132 \u0275\u0275advance(); 19133 \u0275\u0275textInterpolate(option_r1.label); 19134 } 19135} 19136function GroundPatchEditPanelComponent_Conditional_13_Conditional_4_Template(rf, ctx) { 19137 if (rf & 1) { 19138 \u0275\u0275text(0); 19139 } 19140 if (rf & 2) { 19141 const ctx_r1 = \u0275\u0275nextContext(2); 19142 \u0275\u0275textInterpolate1(" The ground patch has ", ctx_r1.cornerCount, " outline corners. "); 19143 } 19144} 19145function GroundPatchEditPanelComponent_Conditional_13_Conditional_5_Template(rf, ctx) { 19146 if (rf & 1) { 19147 \u0275\u0275text(0, " No outline defined. "); 19148 } 19149} 19150function GroundPatchEditPanelComponent_Conditional_13_Conditional_6_Template(rf, ctx) { 19151 if (rf & 1) { 19152 const _r3 = \u0275\u0275getCurrentView(); 19153 \u0275\u0275elementStart(0, "div", 21)(1, "button", 22); 19154 \u0275\u0275listener("click", function GroundPatchEditPanelComponent_Conditional_13_Conditional_6_Template_button_click_1_listener() { 19155 \u0275\u0275restoreView(_r3); 19156 const ctx_r1 = \u0275\u0275nextContext(2); 19157 return \u0275\u0275resetView(ctx_r1.onEditOutline()); 19158 }); 19159 \u0275\u0275element(2, "ng-icon", 23); 19160 \u0275\u0275text(3, " Edit Outline "); 19161 \u0275\u0275elementEnd()(); 19162 } 19163} 19164function GroundPatchEditPanelComponent_Conditional_13_Template(rf, ctx) { 19165 if (rf & 1) { 19166 \u0275\u0275elementStart(0, "app-entity-section", 10); 19167 \u0275\u0275element(1, "ng-icon", 18); 19168 \u0275\u0275elementStart(2, "div", 19)(3, "p", 20); 19169 \u0275\u0275template(4, GroundPatchEditPanelComponent_Conditional_13_Conditional_4_Template, 1, 1)(5, GroundPatchEditPanelComponent_Conditional_13_Conditional_5_Template, 1, 0); 19170 \u0275\u0275elementEnd(); 19171 \u0275\u0275template(6, GroundPatchEditPanelComponent_Conditional_13_Conditional_6_Template, 4, 0, "div", 21); 19172 \u0275\u0275elementEnd()(); 19173 } 19174 if (rf & 2) { 19175 const ctx_r1 = \u0275\u0275nextContext(); 19176 \u0275\u0275advance(4); 19177 \u0275\u0275conditional(ctx_r1.hasOutline ? 4 : 5); 19178 \u0275\u0275advance(2); 19179 \u0275\u0275conditional(ctx_r1.hasOutline ? 6 : -1); 19180 } 19181} 19182function GroundPatchEditPanelComponent_Conditional_14_Conditional_8_Template(rf, ctx) { 19183 if (rf & 1) { 19184 \u0275\u0275elementStart(0, "div", 26)(1, "span", 27); 19185 \u0275\u0275text(2, "Area"); 19186 \u0275\u0275elementEnd(); 19187 \u0275\u0275elementStart(3, "span", 28); 19188 \u0275\u0275text(4); 19189 \u0275\u0275elementEnd()(); 19190 } 19191 if (rf & 2) { 19192 const ctx_r1 = \u0275\u0275nextContext(2); 19193 \u0275\u0275advance(4); 19194 \u0275\u0275textInterpolate1("~", ctx_r1.area, " m\xB2"); 19195 } 19196} 19197function GroundPatchEditPanelComponent_Conditional_14_Template(rf, ctx) { 19198 if (rf & 1) { 19199 \u0275\u0275elementStart(0, "app-entity-section", 11); 19200 \u0275\u0275element(1, "ng-icon", 24); 19201 \u0275\u0275elementStart(2, "div", 25)(3, "div", 26)(4, "span", 27); 19202 \u0275\u0275text(5, "Corners"); 19203 \u0275\u0275elementEnd(); 19204 \u0275\u0275elementStart(6, "span", 28); 19205 \u0275\u0275text(7); 19206 \u0275\u0275elementEnd()(); 19207 \u0275\u0275template(8, GroundPatchEditPanelComponent_Conditional_14_Conditional_8_Template, 5, 1, "div", 26); 19208 \u0275\u0275elementEnd()(); 19209 } 19210 if (rf & 2) { 19211 const ctx_r1 = \u0275\u0275nextContext(); 19212 \u0275\u0275advance(7); 19213 \u0275\u0275textInterpolate(ctx_r1.cornerCount); 19214 \u0275\u0275advance(); 19215 \u0275\u0275conditional(ctx_r1.area !== null ? 8 : -1); 19216 } 19217} 19218function GroundPatchEditPanelComponent_Conditional_19_Template(rf, ctx) { 19219 if (rf & 1) { 19220 \u0275\u0275element(0, "app-entity-row", 16); 19221 } 19222 if (rf & 2) { 19223 const ctx_r1 = \u0275\u0275nextContext(); 19224 \u0275\u0275property("value", ctx_r1.groundPatchId)("mono", true); 19225 } 19226} 19227var _GroundPatchEditPanelComponent = class _GroundPatchEditPanelComponent { 19228 constructor() { 19229 this.groundPatch = null; 19230 this.isNew = false;
19231 this.groundPatchChanged = new EventEmitter(); 19232 this.applyRequested = new EventEmitter(); 19233 this.cancelRequested = new EventEmitter(); 19234 this.closeRequested = new EventEmitter(); 19235 this.editOutlineRequested = new EventEmitter(); 19236 this.groundPatchType = GroundPatchType.Grass; 19237 this.groundPatchTypeOptions = [ 19238 { value: GroundPatchType.Grass, label: "Grass", icon: "lucideTreeDeciduous" }, 19239 { value: GroundPatchType.Gravel, label: "Gravel", icon: "lucideCircleDot" }, 19240 { value: GroundPatchType.Concrete, label: "Concrete", icon: "lucideBox" }, 19241 { value: GroundPatchType.Asphalt, label: "Asphalt", icon: "lucideSquare" }, 19242 { value: GroundPatchType.Dirt, label: "Dirt", icon: "lucideMountain" }, 19243 { value: GroundPatchType.Sand, label: "Sand", icon: "lucideWaves" }, 19244 { value: GroundPatchType.Pavement, label: "Pavement", icon: "lucideGrid3x3" } 19245 ]; 19246 } 19247 /** 19248 * Get the icon for the currently selected ground patch type. 19249 */ 19250 get currentGroundPatchTypeIcon() { 19251 var _a3; 19252 const option = this.groundPatchTypeOptions.find((o) => o.value === this.groundPatchType); 19253 return (_a3 = option == null ? void 0 : option.icon) != null ? _a3 : "lucideTreeDeciduous"; 19254 } 19255 // ========================================================================= 19256 // Lifecycle Hooks 19257 // ========================================================================= 19258 ngOnChanges(changes) { 19259 if (changes["groundPatch"] && this.groundPatch) { 19260 this.syncFromGroundPatch(); 19261 } 19262 } 19263 /** 19264 * Sync local form values from ground patch model. 19265 */ 19266 syncFromGroundPatch() { 19267 var _a3; 19268 if (!this.groundPatch) 19269 return; 19270 this.groundPatchType = (_a3 = this.groundPatch.entity.type) != null ? _a3 : GroundPatchType.Grass; 19271 } 19272 // ========================================================================= 19273 // Computed Read-Only Statistics 19274 // ========================================================================= 19275 get cornerCount() { 19276 var _a3, _b2, _c, _d; 19277 return (_d = (_c = (_b2 = (_a3 = this.groundPatch) == null ? void 0 : _a3.corners) == null ? void 0 : _b2.filter((c) => !c.entity.isDeleted)) == null ? void 0 : _c.length) != null ? _d : 0; 19278 } 19279 /** 19280 * Whether the ground patch has an existing outline (at least 3 corners). 19281 */ 19282 get hasOutline() { 19283 return this.cornerCount >= 3; 19284 } 19285 /** 19286 * Calculate approximate area from ground patch corners. 19287 * Returns area in square meters or null if no corners. 19288 */ 19289 get area() { 19290 var _a3, _b2, _c, _d, _e, _f; 19291 const corners = (_b2 = (_a3 = this.groundPatch) == null ? void 0 : _a3.corners) == null ? void 0 : _b2.filter((c) => !c.entity.isDeleted); 19292 if (!corners || corners.length < 3) 19293 return null; 19294 const sortedCorners = [...corners].sort((a, b) => { 19295 var _a4, _b3; 19296 return ((_a4 = a.entity.position) != null ? _a4 : 0) - ((_b3 = b.entity.position) != null ? _b3 : 0); 19297 }); 19298 let sum = 0; 19299 for (let i = 0; i < sortedCorners.length; i++) { 19300 const curr = sortedCorners[i]; 19301 const next = sortedCorners[(i + 1) % sortedCorners.length]; 19302 const x1 = (_c = curr.entity.locationX) != null ? _c : 0; 19303 const y1 = (_d = curr.entity.locationY) != null ? _d : 0; 19304 const x2 = (_e = next.entity.locationX) != null ? _e : 0; 19305 const y2 = (_f = next.entity.locationY) != null ? _f : 0; 19306 sum += x1 * y2 - x2 * y1; 19307 } 19308 const areaCm2 = Math.abs(sum) / 2; 19309 const areaM2 = areaCm2 / 1e4; 19310 return Math.round(areaM2); 19311 } 19312 get campusName() { 19313 var _a3, _b2, _c; 19314 return (_c = (_b2 = (_a3 = this.groundPatch) == null ? void 0 : _a3.campus) == null ? void 0 : _b2.entity.name) != null ? _c : "Unknown Campus"; 19315 } 19316 get groundPatchId() { 19317 var _a3, _b2; 19318 return (_b2 = (_a3 = this.groundPatch) == null ? void 0 : _a3.entity.id) != null ? _b2 : ""; 19319 } 19320 get panelTitle() { 19321 return this.isNew ? "New Ground Patch" : "Edit Ground Patch"; 19322 } 19323 get applyButtonText() { 19324 return this.isNew ? "Create" : "Apply"; 19325 } 19326 // ========================================================================= 19327 // Event Handlers 19328 // ========================================================================= 19329 onGroundPatchTypeChange(value) { 19330 this.groundPatchType = value; 19331 this.groundPatchChanged.emit({ type: this.groundPatchType }); 19332 } 19333 onApply() { 19334 this.applyRequested.emit(); 19335 } 19336 onCancel() { 19337 this.cancelRequested.emit(); 19338 } 19339 onClose() { 19340 this.closeRequested.emit(); 19341 } 19342 onEditOutline() { 19343 this.editOutlineRequested.emit(); 19344 } 19345}; 19346_GroundPatchEditPanelComponent.\u0275fac = function GroundPatchEditPanelComponent_Factory(__ngFactoryType__) { 19347 return new (__ngFactoryType__ || _GroundPatchEditPanelComponent)(); 19348}; 19349_GroundPatchEditPanelComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _GroundPatchEditPanelComponent, selectors: [["app-groun
19349d-patch-edit-panel"]], inputs: { groundPatch: "groundPatch", isNew: "isNew" }, outputs: { groundPatchChanged: "groundPatchChanged", applyRequested: "applyRequested", cancelRequested: "cancelRequested", closeRequested: "closeRequested", editOutlineRequested: "editOutlineRequested" }, features: [\u0275\u0275ProvidersFeature([], [ 19350 provideIcons({ 19351 lucideChartBar, 19352 lucideMapPin, 19353 lucidePencil, 19354 lucidePenTool, 19355 lucideLayers, 19356 lucideTreeDeciduous, 19357 lucideCircleDot, 19358 lucideBox, 19359 lucideSquare, 19360 lucideMountain, 19361 lucideWaves, 19362 lucideGrid3x3 19363 }) 19364]), \u0275\u0275NgOnChangesFeature], decls: 21, vars: 8, consts: [[3, "closeRequested", "cancelRequested", "applyRequested", "title", "applyText"], ["panel-content", ""], ["label", "Ground Type"], ["section-icon", "", "name", "lucideLayers", "size", "16"], [1, "input-stack"], [1, "input-group", "full"], [1, "select-wrapper"], [3, "ngModelChange", "ngModel"], [3, "value"], ["size", "16", 1, "select-icon", 3, "name"], ["label", "Outline"], ["label", "Statistics"], ["label", "Location"], ["section-icon", "", "name", "lucideMapPin", "size", "16"], [1, "location-info"], ["label", "Campus", 3, "value"], ["label", "ID", 3, "value", "mono"], ["panel-info", ""], ["section-icon", "", "name", "lucidePenTool", "size", "16"], [1, "outline-section"], [1, "outline-description"], [1, "outline-actions"], [1, "outline-button", 3, "click"], ["name", "lucidePencil", "size", "14"], ["section-icon", "", "name", "lucideChartBar", "size", "16"], [1, "stats-grid"], [1, "stat-item"], [1, "stat-label"], [1, "stat-value"]], template: function GroundPatchEditPanelComponent_Template(rf, ctx) { 19365 if (rf & 1) { 19366 \u0275\u0275elementStart(0, "app-entity-panel", 0); 19367 \u0275\u0275listener("closeRequested", function GroundPatchEditPanelComponent_Template_app_entity_panel_closeRequested_0_listener() { 19368 return ctx.onClose(); 19369 })("cancelRequested", function GroundPatchEditPanelComponent_Template_app_entity_panel_cancelRequested_0_listener() { 19370 return ctx.onCancel(); 19371 })("applyRequested", function GroundPatchEditPanelComponent_Template_app_entity_panel_applyRequested_0_listener() { 19372 return ctx.onApply(); 19373 }); 19374 \u0275\u0275elementContainerStart(1, 1); 19375 \u0275\u0275elementStart(2, "app-entity-section", 2); 19376 \u0275\u0275element(3, "ng-icon", 3); 19377 \u0275\u0275elementStart(4, "div", 4)(5, "div", 5)(6, "label"); 19378 \u0275\u0275text(7, "SURFACE TYPE"); 19379 \u0275\u0275elementEnd(); 19380 \u0275\u0275elementStart(8, "div", 6)(9, "select", 7); 19381 \u0275\u0275listener("ngModelChange", function GroundPatchEditPanelComponent_Template_select_ngModelChange_9_listener($event) { 19382 return ctx.onGroundPatchTypeChange($event); 19383 }); 19384 \u0275\u0275repeaterCreate(10, GroundPatchEditPanelComponent_For_11_Template, 2, 2, "option", 8, _forTrack06); 19385 \u0275\u0275elementEnd(); 19386 \u0275\u0275element(12, "ng-icon", 9); 19387 \u0275\u0275elementEnd()()()(); 19388 \u0275\u0275template(13, GroundPatchEditPanelComponent_Conditional_13_Template, 7, 2, "app-entity-section", 10)(14, GroundPatchEditPanelComponent_Conditional_14_Template, 9, 2, "app-entity-section", 11); 19389 \u0275\u0275elementStart(15, "app-entity-section", 12); 19390 \u0275\u0275element(16, "ng-icon", 13); 19391 \u0275\u0275elementStart(17, "div", 14); 19392 \u0275\u0275element(18, "app-entity-row", 15); 19393 \u0275\u0275template(19, GroundPatchEditPanelComponent_Conditional_19_Template, 1, 2, "app-entity-row", 16); 19394 \u0275\u0275elementEnd()(); 19395 \u0275\u0275elementContainerEnd(); 19396 \u0275\u0275elementContainer(20, 17); 19397 \u0275\u0275elementEnd(); 19398 } 19399 if (rf & 2) { 19400 \u0275\u0275property("title", ctx.panelTitle)("applyText", ctx.applyButtonText); 19401 \u0275\u0275advance(9); 19402 \u0275\u0275property("ngModel", ctx.groundPatchType); 19403 \u0275\u0275advance(); 19404 \u0275\u0275repeater(ctx.groundPatchTypeOptions); 19405 \u0275\u0275advance(2); 19406 \u0275\u0275property("name", ctx.currentGroundPatchTypeIcon); 19407 \u0275\u0275advance(); 19408 \u0275\u0275conditional(!ctx.isNew ? 13 : -1); 19409 \u0275\u0275advance(); 19410 \u0275\u0275conditional(!ctx.isNew ? 14 : -1); 19411 \u0275\u0275advance(4); 19412 \u0275\u0275property("value", ctx.campusName); 19413 \u0275\u0275advance(); 19414 \u0275\u0275conditional(!ctx.isNew ? 19 : -1); 19415 } 19416}, dependencies: [ 19417 CommonModule, 19418 FormsModule, 19419 NgSelectOption, 19420 \u0275NgSelectMultipleOption, 19421 SelectControlValueAccessor, 19422 NgControlStatus, 19423 NgModel, 19424 NgIcon, 19425 EntityPanelComponent, 19426 EntitySectionComponent, 19427 EntityRowComponent 19428], styles: ["\n\n.input-stack[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n gap: 10px;\n}\n.input-group[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.input-group.full[_ngcontent-%COMP%] {\n width: 100%;\n}\n.input-group[_ngcontent-%COMP%] label[_ngcontent-%COMP%] {\n font-size: 11px;\n font-weight: 500;\n color: var(--ion-color-medium);\n text-transform: uppercase;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%] {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n transition: border-color 0.2s ease;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.input-group[_ngcontent-%COMP%] input[type=number][_ngcontent-%COMP%] {\n font-family: monospace;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::placeholder {\n color: var(--ion-color-medium-tint);\n font-style: italic;\n}\n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::-webkit-inner-spin-button, \n.input-group[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::-webkit-outer-spin-button {\n opacity: 1;\n}\n.select-wrapper[_ngcontent-%COMP%] {\n position: relative;\n display: flex;\n align-items: center;\n}\n.select-wrapper[_ngcontent-%COMP%] select[_ngcontent-%COMP%] {\n width: 100%;\n padding: 6px 8px;\n padding-left: 32px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n cursor: pointer;\n -webkit-appearance: auto;\n -moz-appearance: auto;\n appearance: auto;\n transition: border-color 0.2s ease;\n}\n.select-wrapper[_ngcontent-%COMP%] select[_ngcontent-%COMP%]:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.select-wrapper[_ngcontent-%COMP%] .select-icon[_ngcontent-%COMP%] {\n position: absolute;\n left: 8px;\n pointer-events: none;\n color: var(--ion-color-primary);\n}\n.stats-grid[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 8px;\n}\n.stat-item[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 6px 8px;\n background: rgba(0, 0, 0, 0.02);\n border-radius: 4px;\n}\n.stat-label[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--ion-color-medium);\n}\n.stat-value[_ngcontent-%COMP%] {\n font-size: 13px;\n font-weight: 600;\n color: var(--ion-text-color);\n font-family: monospace;\n}\n.outline-section[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n.outline-description[_ngcontent-%COMP%] {\n font-size: 12px;\n color: var(--ion-color-medium);\n margin: 0;\n line-height: 1.4;\n}\n.outline-actions[_ngcontent-%COMP%] {\n display: flex;\n gap: 8px;\n}\n.outline-button[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 12px;\n font-size: 12px;\n font-weight: 500;\n border: 1px solid var(--ion-color-medium-shade);\n border-radius: 6px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n cursor: pointer;\n transition: all 0.15s ease;\n}\n.outline-button[_ngcontent-%COMP%]:hover {\n background: var(--ion-color-light);\n border-color: var(--ion-color-primary);\n}\n.outline-button[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n flex-shrink: 0;\n}"], changeDetection: 0 }); 19429var GroundPatchEditPanelComponent = _GroundPatchEditPanelComponent; 19430(() => { 19431 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(GroundPatchEditPanelComponent, [{ 19432 type: Component,
19433 args: [{ selector: "app-ground-patch-edit-panel", standalone: true, imports: [ 19434 CommonModule, 19435 FormsModule, 19436 NgIcon, 19437 EntityPanelComponent, 19438 EntitySectionComponent, 19439 EntityRowComponent 19440 ], viewProviders: [ 19441 provideIcons({ 19442 lucideChartBar, 19443 lucideMapPin, 19444 lucidePencil, 19445 lucidePenTool, 19446 lucideLayers, 19447 lucideTreeDeciduous, 19448 lucideCircleDot, 19449 lucideBox, 19450 lucideSquare, 19451 lucideMountain, 19452 lucideWaves, 19453 lucideGrid3x3 19454 }) 19455 ], changeDetection: ChangeDetectionStrategy.OnPush, template: '<app-entity-panel\n [title]="panelTitle"\n [applyText]="applyButtonText"\n (closeRequested)="onClose()"\n (cancelRequested)="onCancel()"\n (applyRequested)="onApply()">\n\n <ng-container panel-content>\n <!-- Ground Type Section -->\n <app-entity-section label="Ground Type">\n <ng-icon section-icon name="lucideLayers" size="16" />\n <div class="input-stack">\n <div class="input-group full">\n <label>SURFACE TYPE</label>\n <div class="select-wrapper">\n <select\n [ngModel]="groundPatchType"\n (ngModelChange)="onGroundPatchTypeChange($event)"\n >\n @for (option of groundPatchTypeOptions; track option.value) {\n <option [value]="option.value">{{ option.label }}</option>\n }\n </select>\n <ng-icon\n class="select-icon"\n [name]="currentGroundPatchTypeIcon"\n size="16"\n />\n </div>\n </div>\n </div>\n </app-entity-section>\n\n <!-- Outline Section (edit mode only) -->\n @if (!isNew) {\n <app-entity-section label="Outline">\n <ng-icon section-icon name="lucidePenTool" size="16" />\n <div class="outline-section">\n <p class="outline-description">\n @if (hasOutline) {\n The ground patch has {{ cornerCount }} outline corners.\n } @else {\n No outline defined.\n }\n </p>\n @if (hasOutline) {\n <div class="outline-actions">\n <button class="outline-button" (click)="onEditOutline()">\n <ng-icon name="lucidePencil" size="14" />\n Edit Outline\n </button>\n </div>\n }\n </div>\n </app-entity-section>\n }\n\n <!-- Statistics Section (edit mode only) -->\n @if (!isNew) {\n <app-entity-section label="Statistics">\n <ng-icon section-icon name="lucideChartBar" size="16" />\n <div class="stats-grid">\n <div class="stat-item">\n <span class="stat-label">Corners</span>\n <span class="stat-value">{{ cornerCount }}</span>\n </div>\n @if (area !== null) {\n <div class="stat-item">\n <span class="stat-label">Area</span>\n <span class="stat-value">~{{ area }} m\xB2</span>\n </div>\n }\n </div>\n </app-entity-section>\n }\n\n <!-- Location Info Section -->\n <app-entity-section label="Location">\n <ng-icon section-icon name="lucideMapPin" size="16" />\n <div class="location-info">\n <app-entity-row label="Campus" [value]="campusName" />\n @if (!isNew) {\n <app-entity-row label="ID" [value]="groundPatchId" [mono]="true" />\n }\n </div>\n </app-entity-section>\n </ng-container>\n\n <!-- Empty panel-info slot -->\n <ng-container panel-info></ng-container>\n\n</app-entity-panel>\n', styles: ["/* src/app/pages/campus-map-v3/components/ground-patch-edit-panel/ground-patch-edit-panel.component.scss */\n.input-stack {\n display: flex;\n flex-direction: column;\n gap: 8px;\n gap: 10px;\n}\n.input-group {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.input-group.full {\n width: 100%;\n}\n.input-group label {\n font-size: 11px;\n font-weight: 500;\n color: var(--ion-color-medium);\n text-transform: uppercase;\n}\n.input-group input {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n transition: border-color 0.2s ease;\n}\n.input-group input:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.input-group input[type=number] {\n font-family: monospace;\n}\n.input-group input::placeholder {\n color: var(--ion-color-medium-tint);\n font-style: italic;\n}\n.input-group input::-webkit-inner-spin-button,\n.input-group input::-webkit-outer-spin-button {\n opacity: 1;\n}\n.select-wrapper {\n position: relative;\n display: flex;\n align-items: center;\n}\n.select-wrapper select {\n width: 100%;\n padding: 6px 8px;\n padding-left: 32px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n cursor: pointer;\n -webkit-appearance: auto;\n -moz-appearance: auto;\n appearance: auto;\n transition: border-color 0.2s ease;\n}\n.select-wrapper select:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.select-wrapper .select-icon {\n position: absolute;\n left: 8px;\n pointer-events: none;\n color: var(--ion-color-primary);\n}\n.stats-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 8px;\n}\n.stat-item {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 6px 8px;\n background: rgba(0, 0, 0, 0.02);\n border-radius: 4px;\n}\n.stat-label {\n font-size: 11px;\n color: var(--ion-color-medium);\n}\n.stat-value {\n font-size: 13px;\n font-weight: 600;\n color: var(--ion-text-color);\n font-family: monospace;\n}\n.outline-section {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n.outline-description {\n font-size: 12px;\n color: var(--ion-color-medium);\n margin: 0;\n line-height: 1.4;\n}\n.outline-actions {\n display: flex;\n gap: 8px;\n}\n.outline-button {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 12px;\n font-size: 12px;\n font-weight: 500;\n border: 1px solid var(--ion-color-medium-shade);\n border-radius: 6px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n cursor: pointer;\n transition: all 0.15s ease;\n}\n.outline-button:hover {\n background: var(--ion-color-light);\n border-color: var(--ion-color-primary);\n}\n.outline-button ng-icon {\n flex-shrink: 0;\n}\n"] }] 19456 }], null, { groundPatch: [{ 19457 type: Input 19458 }], isNew: [{ 19459 type: Input 19460 }], groundPatchChanged: [{ 19461 type: Output 19462 }], applyRequested: [{ 19463 type: Output 19464 }], cancelRequested: [{ 19465 type: Output 19466 }], closeRequested: [{ 19467 type: Output 19468 }], editOutlineRequested: [{ 19469 type: Output 19470 }] }); 19471})(); 19472(() => { 19473 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(GroundPatchEditPanelComponent, { className: "GroundPatchEditPanelComponent", filePath: "src/app/pages/campus-map-v3/components/ground-patch-edit-panel/groun
19473d-patch-edit-panel.component.ts", lineNumber: 71 }); 19474})(); 19475
19476// src/app/pages/campus-map-v3/components/building-selection-modal/building-selection-modal.component.ts 19477var _forTrack07 = ($index, $item) => $item.id; 19478function BuildingSelectionModalComponent_Conditional_12_Conditional_1_Template(rf, ctx) { 19479 if (rf & 1) { 19480 const _r1 = \u0275\u0275getCurrentView(); 19481 \u0275\u0275element(0, "ng-icon", 16); 19482 \u0275\u0275elementStart(1, "p"); 19483 \u0275\u0275text(2); 19484 \u0275\u0275elementEnd(); 19485 \u0275\u0275elementStart(3, "button", 17); 19486 \u0275\u0275listener("click", function BuildingSelectionModalComponent_Conditional_12_Conditional_1_Template_button_click_3_listener() { 19487 \u0275\u0275restoreView(_r1); 19488 const ctx_r1 = \u0275\u0275nextContext(2); 19489 ctx_r1.searchTerm = ""; 19490 return \u0275\u0275resetView(ctx_r1.onSearchChange("")); 19491 }); 19492 \u0275\u0275text(4, " Clear search "); 19493 \u0275\u0275elementEnd(); 19494 } 19495 if (rf & 2) { 19496 const ctx_r1 = \u0275\u0275nextContext(2); 19497 \u0275\u0275advance(2); 19498 \u0275\u0275textInterpolate1('No buildings match "', ctx_r1.searchTerm, '"'); 19499 } 19500} 19501function BuildingSelectionModalComponent_Conditional_12_Conditional_2_Template(rf, ctx) { 19502 if (rf & 1) { 19503 \u0275\u0275element(0, "ng-icon", 18); 19504 \u0275\u0275elementStart(1, "p"); 19505 \u0275\u0275text(2, "No buildings available"); 19506 \u0275\u0275elementEnd(); 19507 } 19508} 19509function BuildingSelectionModalComponent_Conditional_12_Template(rf, ctx) { 19510 if (rf & 1) { 19511 \u0275\u0275elementStart(0, "div", 11); 19512 \u0275\u0275template(1, BuildingSelectionModalComponent_Conditional_12_Conditional_1_Template, 5, 1)(2, BuildingSelectionModalComponent_Conditional_12_Conditional_2_Template, 3, 0); 19513 \u0275\u0275elementEnd(); 19514 } 19515 if (rf & 2) { 19516 const ctx_r1 = \u0275\u0275nextContext(); 19517 \u0275\u0275advance(); 19518 \u0275\u0275conditional(ctx_r1.searchTerm ? 1 : 2); 19519 } 19520} 19521function BuildingSelectionModalComponent_Conditional_13_For_2_Conditional_17_Template(rf, ctx) { 19522 if (rf & 1) { 19523 \u0275\u0275element(0, "ng-icon", 31); 19524 } 19525} 19526function BuildingSelectionModalComponent_Conditional_13_For_2_Conditional_18_Template(rf, ctx) { 19527 if (rf & 1) { 19528 \u0275\u0275element(0, "ng-icon", 32); 19529 } 19530} 19531function BuildingSelectionModalComponent_Conditional_13_For_2_Template(rf, ctx) { 19532 if (rf & 1) { 19533 const _r3 = \u0275\u0275getCurrentView(); 19534 \u0275\u0275elementStart(0, "button", 20); 19535 \u0275\u0275listener("click", function BuildingSelectionModalComponent_Conditional_13_For_2_Template_button_click_0_listener() { 19536 const building_r4 = \u0275\u0275restoreView(_r3).$implicit; 19537 const ctx_r1 = \u0275\u0275nextContext(2); 19538 return \u0275\u0275resetView(ctx_r1.onBuildingSelect(building_r4)); 19539 }); 19540 \u0275\u0275elementStart(1, "div", 21); 19541 \u0275\u0275element(2, "ng-icon", 22); 19542 \u0275\u0275elementEnd(); 19543 \u0275\u0275elementStart(3, "div", 23)(4, "div", 24); 19544 \u0275\u0275text(5); 19545 \u0275\u0275elementEnd(); 19546 \u0275\u0275elementStart(6, "div", 25)(7, "span", 26); 19547 \u0275\u0275element(8, "ng-icon", 27); 19548 \u0275\u0275text(9); 19549 \u0275\u0275elementEnd(); 19550 \u0275\u0275elementStart(10, "span", 26); 19551 \u0275\u0275element(11, "ng-icon", 28); 19552 \u0275\u0275text(12); 19553 \u0275\u0275elementEnd(); 19554 \u0275\u0275elementStart(13, "span", 26); 19555 \u0275\u0275element(14, "ng-icon", 29); 19556 \u0275\u0275text(15); 19557 \u0275\u0275elementEnd()()(); 19558 \u0275\u0275elementStart(16, "div", 30); 19559 \u0275\u0275template(17, BuildingSelectionModalComponent_Conditional_13_For_2_Conditional_17_Template, 1, 0, "ng-icon", 31)(18, BuildingSelectionModalComponent_Conditional_13_For_2_Conditional_18_Template, 1, 0, "ng-icon", 32); 19560 \u0275\u0275elementEnd()(); 19561 } 19562 if (rf & 2) { 19563 const building_r4 = ctx.$implicit; 19564 const ctx_r1 = \u0275\u0275nextContext(2); 19565 \u0275\u0275classProp("selected", ctx_r1.selectedBuilding === building_r4); 19566 \u0275\u0275attribute("aria-label", "Select " + building_r4.name); 19567 \u0275\u0275advance(5); 19568 \u0275\u0275textInterpolate(building_r4.name); 19569 \u0275\u0275advance(4); 19570 \u0275\u0275textInterpolate1(" ", building_r4.floorCount, " floors "); 19571 \u0275\u0275advance(3); 19572 \u0275\u0275textInterpolate1(" ", building_r4.roomCount, " rooms "); 19573 \u0275\u0275advance(3); 19574 \u0275\u0275textInterpolate1(" ", building_r4.nodeCount, " nodes "); 19575 \u0275\u0275advance(2); 19576 \u0275\u0275conditional(ctx_r1.selectedBuilding === building_r4 ? 17 : 18); 19577 } 19578} 19579function BuildingSelectionModalComponent_Conditional_13_Template(rf, ctx) { 19580 if (rf & 1) { 19581 \u0275\u0275elementStart(0, "div", 12); 19582 \u0275\u0275repeaterCreate(1, BuildingSelectionModalComponent_Conditional_13_For_2_Template, 19, 8, "button", 19, _forTrack07); 19583 \u0275\u0275elementEnd(); 19584 } 19585 if (rf & 2) { 19586 const ctx_r1 = \u0275\u0275nextContext(); 19587 \u0275\u0275advance(); 19588 \u0275\u0275repeater(ctx_r1.filteredBuildings); 19589 } 19590} 19591var _BuildingSelectionModalComponent = class _BuildingSelectionModalComponent { 19592 constructor(cdr) { 19593 this.cdr = cdr; 19594 this.buildings = []; 19595 this.title = "Select Building"; 19596 this.buildingSelected = new EventEmitter(); 19597 this.cancelRequested = new EventEmitter(); 19598 this.closeRequested = new EventEmitter(); 19599 this.searchTerm = ""; 19600 this.filteredBuildings = []; 19601 this.selectedBuilding = null; 19602 } 19603 ngOnInit() { 19604 this.filterBuildings(); 19605 } 19606 onSearchChange(value) { 19607 this.searchTerm = value || ""; 19608 this.filterBuildings(); 19609 this.cdr.markForCheck(); 19610 } 19611 filterBuildings() { 19612 if (!this.searchTerm.trim()) { 19613 this.filteredBuildings = [...this.buildings]; 19614 return; 19615 } 19616 const term = this.searchTerm.toLowerCase().trim(); 19617 this.filteredBuildings = this.buildings.filter((b) => b.name.toLowerCase().includes(term)); 19618 } 19619 onBuildingSelect(building) { 19620 this.selectedBuilding = building; 19621 this.cdr.markForCheck(); 19622 } 19623 onCreateFloor() { 19624 if (this.selectedBuilding) { 19625 this.buildingSelected.emit(this.selectedBuilding); 19626 } 19627 } 19628 onCancel() { 19629 this.cancelRequested.emit(); 19630 } 19631 onClose() { 19632 this.closeRequested.emit(); 19633 } 19634 get applyButtonText() { 19635 return "Create & Draw"; 19636 } 19637 get hasSelection() { 19638 return this.selectedBuilding !== null; 19639 } 19640}; 19641_BuildingSelectionModalComponent.\u0275fac = function BuildingSelectionModalComponent_Factory(__ngFactoryType__) { 19642 return new (__ngFactoryType__ || _BuildingSelectionModalComponent)(\u0275\u0275directiveInject(ChangeDetectorRef)); 19643}; 19644_BuildingSelectionModalComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _BuildingSelectionModalComponent, selectors: [["app-building-selection-modal"]], inputs: { buildings: "buildings", title: "title" }, outputs: { buildingSelected: "buildingSelected", cancelRequested: "cancelRequested", closeRequested: "closeRequested" }, features: [\u0275\u0275ProvidersFeature([], [ 19645 provideIcons({ 19646 lucideSearch, 19647 lucideSearchX, 19648 lucideX, 19649 lucideBuilding2, 19650 lucideLayers, 19651 lucideSquare, 19652 lucideTag, 19653 lucideChevronRight, 19654 lucideCheck 19655 }) 19656])], decls: 19, vars: 5, consts: [[3, "backdropClick"], [1, "building-selection-modal"], [1, "panel-header"], [1, "panel-title"], ["title", "Close", 1, "close-button", 3, "click"], ["name", "lucideX", "size", "18"], [1, "search-section"], [1, "search-input-wrapper"], ["name", "lucideSearch", "size", "16", 1, "search-icon"], ["type", "text", "placeholder", "Search buildings...", "autocomplete", "off", 1, "search-input", 3, "ngModelChange", "ngModel"], [1, "buildings-list"], [1, "empty-state"], [1, "buildings-scroll"], [1, "panel-actions"], [1, "action-button", "cancel", 3, "click"], [1, "action-button", "primary", 3, "click", "disabled"], ["name", "lucideSearchX", "size", "32"], [1, "clear-button", 3, "click"], ["name", "lucideBuilding2", "size", "32"], [1, "building-item", 3, "selected"], [1, "building-item", 3, "click"], [1, "building-icon"], ["name", "lucideBuilding2", "size", "18"], [1, "building-info"], [1, "building-name"], [1, "building-stats"], [1, "stat"], ["name", "lucideLayers", "size", "12"], ["name", "lucideSquare", "size", "12"], ["name", "lucideTag", "size", "12"], [1, "building-arrow"], ["name", "lucideCheck", "size", "16", 1, "checkmark"], ["name", "lucideChevronRight", "size", "16"]], template: function BuildingSelectionModalComponent_Template(rf, ctx) { 19657 if (rf & 1) { 19658 \u0275\u0275elementStart(0, "app-modal-overlay", 0); 19659 \u0275\u0275listener("backdropClick", function BuildingSelectionModalComponent_Template_app_modal_overlay_backdropClick_0_listener() { 19660 return ctx.onClose(); 19661 }); 19662 \u0275\u0275elementStart(1, "div", 1)(2, "div", 2)(3, "span", 3); 19663 \u0275\u0275text(4); 19664 \u0275\u0275elementEnd(); 19665 \u0275\u0275elementStart(5, "button", 4); 19666 \u0275\u0275listener("click", function BuildingSelectionModalComponent_Template_button_click_5_listener() { 19667 return ctx.onClose(); 19668 }); 19669 \u0275\u0275element(6, "ng-icon", 5); 19670 \u0275\u0275elementEnd()(); 19671 \u0275\u0275elementStart(7, "div", 6)(8, "div", 7); 19672 \u0275\u0275element(9, "ng-icon", 8); 19673 \u0275\u0275elementStart(10, "input", 9); 19674 \u0275\u0275twoWayListener("ngModelChange", function BuildingSelectionModalComponent_Template_input_ngModelChange_10_listener($event) { 19675 \u0275\u0275twoWayBindingSet(ctx.searchTerm, $event) || (ctx.searchTerm = $event); 19676 return $event; 19677 }); 19678 \u0275\u0275listener("ngModelChange", function BuildingSelectionModalComponent_Template_input_ngModelChange_10_listener($event) { 19679 return ctx.onSearchChange($event); 19680 }); 19681 \u0275\u0275elementEnd()()(); 19682 \u0275\u0275elementStart(11, "div", 10); 19683 \u0275\u0275template(12, BuildingSelectionModalComponent_Conditional_12_Template, 3, 1, "div", 11)(13, BuildingSelectionModalComponent_Conditional_13_Template, 3, 0, "div", 12); 19684 \u0275\u0275elementEnd(); 19685 \u0275\u0275elementStart(14, "div", 13)(15, "button", 14); 19686 \u0275\u0275listener("click", function BuildingSelectionModalComponent_Template_button_click_15_listener() { 19687 return ctx.onCancel(); 19688 }); 19689 \u0275\u0275text(16, " Cancel "); 19690 \u0275\u0275elementEnd(); 19691 \u0275\u0275elementStart(17, "button", 15); 19692 \u0275\u0275listener("click", function BuildingSelectionModalComponent_Template_button_click_17_listener() { 19693 return ctx.onCreateFloor(); 19694 }); 19695 \u0275\u0275text(18); 19696 \u0275\u0275elementEnd()()()(); 19697 } 19698 if (rf & 2) { 19699 \u0275\u0275advance(4); 19700 \u0275\u0275textInterpolate(ctx.title); 19701 \u0275\u0275advance(6); 19702 \u0275\u0275twoWayProperty("ngModel", ctx.searchTerm); 19703 \u0275\u0275advance(2); 19704 \u0275\u0275conditional(ctx.filteredBuildings.length === 0 ? 12 : 13); 19705 \u0275\u0275advance(5); 19706 \u0275\u0275property("disabled", !ctx.hasSelection); 19707 \u0275\u0275advance(); 19708 \u0275\u0275textInterpolate1(" ", ctx.applyButtonText, " "); 19709 } 19710}, dependencies: [ 19711 CommonModule, 19712 FormsModule, 19713 DefaultValueAccessor, 19714 NgControlStatus, 19715 NgModel, 19716 NgIcon, 19717 ModalOverlayComponent 19718], styles: ['@charset "UTF-8";\n\n\n\n.building-selection-modal[_ngcontent-%COMP%] {\n background: var(--tool-palette-bg, rgba(255, 255, 255, 0.95));\n border-radius: 12px;\n box-shadow: var(--tool-palette-shadow, 0 4px 12px rgba(0, 0, 0, 0.15));\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light, rgba(0, 0, 0, 0.08));\n min-width: 380px;\n max-width: 380px;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n max-height: 80vh;\n min-width: 420px;\n max-width: 500px;\n}\n.panel-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 16px;\n border-bottom: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n}\n.panel-title[_ngcontent-%COMP%] {\n font-weight: 600;\n font-size: 14px;\n color: var(--ion-text-color);\n}\n.close-button[_ngcontent-%COMP%] {\n background: transparent;\n border: none;\n padding: 4px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n color: var(--ion-color-medium);\n transition: all 0.2s ease;\n}\n.close-button[_ngcontent-%COMP%]:hover {\n background: rgba(var(--ion-color-danger-rgb), 0.1);\n color: var(--ion-color-danger);\n}\n.search-section[_ngcontent-%COMP%] {\n padding: 12px 16px;\n border-bottom: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n background: var(--ion-color-light, rgba(0, 0, 0, 0.02));\n}\n.search-section[_ngcontent-%COMP%] .search-input-wrapper[_ngcontent-%COMP%] {\n position: relative;\n display: flex;\n align-items: center;\n}\n.search-section[_ngcontent-%COMP%] .search-input-wrapper[_ngcontent-%COMP%] .search-icon[_ngcontent-%COMP%] {\n position: absolute;\n left: 12px;\n color: var(--ion-color-medium);\n pointer-events: none;\n}\n.search-section[_ngcontent-%COMP%] .search-input-wrapper[_ngcontent-%COMP%] .search-input[_ngcontent-%COMP%] {\n width: 100%;\n padding: 8px 12px 8px 36px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n transition: border-color 0.2s ease;\n}\n.search-section[_ngcontent-%COMP%] .search-input-wrapper[_ngcontent-%COMP%] .search-input[_ngcontent-%COMP%]:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.search-section[_ngcontent-%COMP%] .search-input-wrapper[_ngcontent-%COMP%] .search-input[_ngcontent-%COMP%]::placeholder {\n color: var(--ion-color-medium-tint);\n font-style: italic;\n}\n.buildings-list[_ngcontent-%COMP%] {\n flex: 1;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n min-height: 200px;\n max-height: 500px;\n}\n.buildings-list[_ngcontent-%COMP%] .empty-state[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 40px 20px;\n text-align: center;
19718\n color: var(--ion-color-medium);\n}\n.buildings-list[_ngcontent-%COMP%] .empty-state[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n margin-bottom: 12px;\n opacity: 0.5;\n}\n.buildings-list[_ngcontent-%COMP%] .empty-state[_ngcontent-%COMP%] p[_ngcontent-%COMP%] {\n margin: 0 0 16px 0;\n font-size: 14px;\n}\n.buildings-list[_ngcontent-%COMP%] .empty-state[_ngcontent-%COMP%] .clear-button[_ngcontent-%COMP%] {\n background: transparent;\n border: 1px solid var(--ion-color-primary);\n color: var(--ion-color-primary);\n padding: 6px 16px;\n border-radius: 6px;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.2s ease;\n}\n.buildings-list[_ngcontent-%COMP%] .empty-state[_ngcontent-%COMP%] .clear-button[_ngcontent-%COMP%]:hover {\n background: var(--ion-color-primary);\n color: var(--ion-color-primary-contrast);\n}\n.buildings-list[_ngcontent-%COMP%] .buildings-scroll[_ngcontent-%COMP%] {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n padding: 8px 16px;\n scrollbar-width: thin;\n scrollbar-color: var(--ion-color-medium-tint) transparent;\n}\n.buildings-list[_ngcontent-%COMP%] .buildings-scroll[_ngcontent-%COMP%]:hover {\n scrollbar-color: var(--ion-color-primary-tint) transparent;\n}\n.buildings-list[_ngcontent-%COMP%] .buildings-scroll[_ngcontent-%COMP%]::-webkit-scrollbar {\n width: 6px;\n}\n.buildings-list[_ngcontent-%COMP%] .buildings-scroll[_ngcontent-%COMP%]::-webkit-scrollbar-track {\n background: transparent;\n}\n.buildings-list[_ngcontent-%COMP%] .buildings-scroll[_ngcontent-%COMP%]::-webkit-scrollbar-thumb {\n background-color: var(--ion-color-medium-tint);\n border-radius: 3px;\n}\n.buildings-list[_ngcontent-%COMP%] .buildings-scroll[_ngcontent-%COMP%]::-webkit-scrollbar-thumb:hover {\n background-color: var(--ion-color-primary-tint);\n}\n.building-item[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 12px;\n width: 100%;\n padding: 12px 14px;\n margin-bottom: 6px;\n border: 1px solid transparent;\n border-radius: 6px;\n background: transparent;\n cursor: pointer;\n transition: all 0.15s ease;\n text-align: left;\n}\n.building-item[_ngcontent-%COMP%]:hover {\n background: var(--ion-color-light, rgba(0, 0, 0, 0.03));\n border-color: var(--ion-color-medium-tint, rgba(0, 0, 0, 0.08));\n}\n.building-item[_ngcontent-%COMP%]:active {\n background: var(--ion-color-medium-tint, rgba(0, 0, 0, 0.05));\n}\n.building-item.selected[_ngcontent-%COMP%] {\n background: rgba(0, 0, 0, 0.04);\n border-color: rgba(0, 0, 0, 0.12);\n}\n.building-item[_ngcontent-%COMP%]:last-child {\n margin-bottom: 0;\n}\n.building-item[_ngcontent-%COMP%] .building-icon[_ngcontent-%COMP%] {\n flex-shrink: 0;\n width: 36px;\n height: 36px;\n display: flex;\n align-items: center;\n justify-content: center;\n background:\n linear-gradient(\n 135deg,\n var(--ion-color-primary-tint, rgba(var(--ion-color-primary-rgb), 0.15)),\n var(--ion-color-primary-shade, rgba(var(--ion-color-primary-rgb), 0.05)));\n border-radius: 6px;\n color: white;\n}\n.building-item[_ngcontent-%COMP%] .building-info[_ngcontent-%COMP%] {\n flex: 1;\n min-width: 0;\n}\n.building-item[_ngcontent-%COMP%] .building-info[_ngcontent-%COMP%] .building-name[_ng
19718content-%COMP%] {\n font-size: 13px;\n font-weight: 600;\n color: var(--ion-text-color);\n margin-bottom: 3px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.building-item[_ngcontent-%COMP%] .building-info[_ngcontent-%COMP%] .building-stats[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n}\n.building-item[_ngcontent-%COMP%] .building-info[_ngcontent-%COMP%] .building-stats[_ngcontent-%COMP%] .stat[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 10px;\n color: var(--ion-color-medium);\n white-space: nowrap;\n}\n.building-item[_ngcontent-%COMP%] .building-info[_ngcontent-%COMP%] .building-stats[_ngcontent-%COMP%] .stat[_ngcontent-%COMP%] ng-icon[_ngcontent-%COMP%] {\n opacity: 0.7;\n}\n.building-item[_ngcontent-%COMP%] .building-info[_ngcontent-%COMP%] .building-stats[_ngcontent-%COMP%] .stat[_ngcontent-%COMP%]:not(:last-child)::after {\n content: "\\2022";\n margin-left: 10px;\n opacity: 0.3;\n}\n.building-item[_ngcontent-%COMP%] .building-arrow[_ngcontent-%COMP%] {\n flex-shrink: 0;\n color: var(--ion-color-medium);\n opacity: 0.5;\n transition: opacity 0.15s ease;\n}\n.building-item[_ngcontent-%COMP%] .building-arrow[_ngcontent-%COMP%] .checkmark[_ngcontent-%COMP%] {\n stroke-width: 3;\n opacity: 1;\n color: var(--ion-color-primary);\n}\n.building-item[_ngcontent-%COMP%]:hover .building-arrow[_ngcontent-%COMP%] {\n opacity: 0.8;\n}\n.panel-actions[_ngcontent-%COMP%] {\n display: flex;\n gap: 10px;\n padding: 14px 16px;\n border-top: 1px solid var(--ion-color-medium-tint, rgba(0, 0, 0, 0.08));\n background: var(--ion-color-light, rgba(0, 0, 0, 0.02));\n}\n.panel-actions[_ngcontent-%COMP%] .action-button[_ngcontent-%COMP%] {\n flex: 1;\n padding: 12px 20px;\n border-radius: 8px;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s ease;\n border: none;\n outline: none;\n}\n.panel-actions[_ngcontent-%COMP%] .action-button.cancel[_ngcontent-%COMP%] {\n background: transparent;\n color: var(--ion-text-color);\n border: 1px solid var(--ion-color-medium-tint, rgba(0, 0, 0, 0.2));\n}\n.panel-actions[_ngcontent-%COMP%] .action-button.cancel[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: var(--ion-color-light);\n border-color: var(--ion-color-medium);\n}\n.panel-actions[_ngcontent-%COMP%] .action-button.primary[_ngcontent-%COMP%] {\n background: var(--ion-color-primary);\n color: var(--ion-color-primary-contrast);\n}\n.panel-actions[_ngcontent-%COMP%] .action-button.primary[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: var(--ion-color-primary-shade);\n}\n.panel-actions[_ngcontent-%COMP%] .action-button.primary[_ngcontent-%COMP%]:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n.panel-actions[_ngcontent-%COMP%] .action-button[_ngcontent-%COMP%]:active:not(:disabled) {\n transform: scale(0.98);\n}\n.panel-actions[_ngcontent-%COMP%] .action-button[_ngcontent-%COMP%]:focus {\n box-shadow: 0 0 0 3px rgba(var(--ion-color-primary-rgb), 0.2);\n}\n@media (max-width: 768px) {\n .building-selection-modal[_ngcontent-%COMP%] {\n min-width: 90vw;\n max-width: 95vw;\n max-height: 90vh;\n margin: 10px;\n }\n}'], changeDetection: 0 }); 19719var BuildingSelectionModalComponent = _BuildingSelectionModalComponent; 19720(() => { 19721 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(BuildingSelectionModalComponent, [{ 19722 type: Component, 19723 args: [{ selector: "app-building-selection-modal", standalone: true, imports: [ 19724 CommonModule, 19725 FormsModule, 19726 NgIcon, 19727 ModalOverlayComponent 19728 ], viewProviders: [ 19729 provideIcons({ 19730 lucideSearch, 19731 lucideSearchX, 19732 lucideX, 19733 lucideBuilding2, 19734 lucideLayers, 19735 lucideSquare, 19736 lucideTag, 19737 lucideChevronRight, 19738 lucideCheck 19739 }) 19740 ], changeDetection: ChangeDetectionStrategy.OnPush, template: `<app-modal-overlay (backdropClick)="onClose()">
19741 <div class="building-selection-modal"> 19742 <!-- Header --> 19743 <div class="panel-header"> 19744 <span class="panel-title">{{ title }}</span> 19745 <button class="close-button" (click)="onClose()" title="Close"> 19746 <ng-icon name="lucideX" size="18" /> 19747 </button> 19748 </div> 19749 19750 <!-- Search Bar --> 19751 <div class="search-section"> 19752 <div class="search-input-wrapper"> 19753 <ng-icon name="lucideSearch" class="search-icon" size="16" /> 19754 <input 19755 type="text" 19756 [(ngModel)]="searchTerm" 19757 (ngModelChange)="onSearchChange($event)" 19758 placeholder="Search buildings..." 19759 class="search-input" 19760 autocomplete="off" 19761 /> 19762 </div> 19763 </div> 19764 19765 <!-- Building List (Scrollable) --> 19766 <div class="buildings-list"> 19767 @if (filteredBuildings.length === 0) { 19768 <div class="empty-state"> 19769 @if (searchTerm) { 19770 <ng-icon name="lucideSearchX" size="32" /> 19771 <p>No buildings match "{{ searchTerm }}"</p> 19772 <button class="clear-button" (click)="searchTerm = ''; onSearchChange('');"> 19773 Clear search 19774 </button> 19775 } @else { 19776 <ng-icon name="lucideBuilding2" size="32" /> 19777 <p>No buildings available</p> 19778 } 19779 </div> 19780 } @else { 19781 <div class="buildings-scroll"> 19782 @for (building of filteredBuildings; track building.id) { 19783 <button 19784 class="building-item" 19785 [class.selected]="selectedBuilding === building" 19786 (click)="onBuildingSelect(building)" 19787 [attr.aria-label]="'Select ' + building.name"> 19788 <div class="building-icon"> 19789 <ng-icon name="lucideBuilding2" size="18" /> 19790 </div> 19791 <div class="building-info"> 19792 <div class="building-name">{{ building.name }}</div> 19793 <div class="building-stats"> 19794 <span class="stat"> 19795 <ng-icon name="lucideLayers" size="12" /> 19796 {{ building.floorCount }} floors 19797 </span> 19798 <span class="stat"> 19799 <ng-icon name="lucideSquare" size="12" /> 19800 {{ building.roomCount }} rooms 19801 </span> 19802 <span class="stat"> 19803 <ng-icon name="lucideTag" size="12" /> 19804 {{ building.nodeCount }} nodes 19805 </span> 19806 </div> 19807 </div> 19808 <div class="building-arrow"> 19809 @if (selectedBuilding === building) { 19810 <ng-icon name="lucideCheck" size="16" class="checkmark" /> 19811 } @else { 19812 <ng-icon name="lucideChevronRight" size="16" /> 19813 } 19814 </div> 19815 </button> 19816 } 19817 </div> 19818 } 19819 </div> 19820 19821 <!-- Actions Footer --> 19822 <div class="panel-actions"> 19823 <button class="action-button cancel" (click)="onCancel()"> 19824 Cancel 19825 </button> 19826 <button class="action-button primary" [disabled]="!hasSelection" (click)="onCreateFloor()"> 19827 {{ applyButtonText }} 19828 </button> 19829 </div> 19830 </div> 19831</app-modal-overlay> 19832`, styles: ['@charset "UTF-8";\n\n/* src/app/pages/campus-map-v3/components/building-selection-modal/building-selection-modal.component.scss */\n.building-selection-modal {\n background: var(--tool-palette-bg, rgba(255, 255, 255, 0.95));\n border-radius: 12px;\n box-shadow: var(--tool-palette-shadow, 0 4px 12px rgba(0, 0, 0, 0.15));\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light, rgba(0, 0, 0, 0.08));\n min-width: 380px;\n max-width: 380px;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n max-height: 80vh;\n min-width: 420px;\n max-width: 500px;\n}\n.panel-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 16px;\n border-bottom: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n background: rgba(var(--ion-color-primary-rgb), 0.05);\n}\n.panel-title {\n font-weight: 600;\n font-size: 14px;\n color: var(--ion-text-color);\n}\n.close-button {\n background: transparent;\n border: none;\n padding: 4px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n color: var(--ion-color-medium);\n transition: all 0.2s ease;\n}\n.close-button:hover {\n background: rgba(var(--ion-color-danger-rgb), 0.1);\n color: var(--ion-color-danger);\n}\n.search-section {\n padding: 12px 16px;\n border-bottom: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.1));\n background: var(--ion-color-light, rgba(0, 0, 0, 0.02));\n}\n.search-section .search-input-wrapper {\n position: relative;\n display: flex;\n align-items: center;\n}\n.search-section .search-input-wrapper .search-icon {\n position: absolute;\n left: 12px;\n color: var(--ion-color-medium);\n pointer-events: none;\n}\n.search-section .search-input-wrapper .search-input {\n width: 100%;\n padding: 8px 12px 8px 36px;\n border: 1px solid var(--tool-palette-border, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n font-size: 13px;\n background: var(--ion-background-color);\n color: var(--ion-text-color);\n transition: border-color 0.2s ease;\n}\n.search-section .search-input-wrapper .search-input:focus {\n outline: none;\n border-color: var(--ion-color-primary);\n}\n.search-section .search-input-wrapper .search-input::placeholder {\n color: var(--ion-color-medium-tint);\n font-style: italic;\n}\n.buildings-list {\n flex: 1;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n min-height: 200px;\n max-height: 500px;\n}\n.buildings-list .empty-state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 40px 20px;\n text-align: center;
19832\n color: var(--ion-color-medium);\n}\n.buildings-list .empty-state ng-icon {\n margin-bottom: 12px;\n opacity: 0.5;\n}\n.buildings-list .empty-state p {\n margin: 0 0 16px 0;\n font-size: 14px;\n}\n.buildings-list .empty-state .clear-button {\n background: transparent;\n border: 1px solid var(--ion-color-primary);\n color: var(--ion-color-primary);\n padding: 6px 16px;\n border-radius: 6px;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.2s ease;\n}\n.buildings-list .empty-state .clear-button:hover {\n background: var(--ion-color-primary);\n color: var(--ion-color-primary-contrast);\n}\n.buildings-list .buildings-scroll {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n padding: 8px 16px;\n scrollbar-width: thin;\n scrollbar-color: var(--ion-color-medium-tint) transparent;\n}\n.buildings-list .buildings-scroll:hover {\n scrollbar-color: var(--ion-color-primary-tint) transparent;\n}\n.buildings-list .buildings-scroll::-webkit-scrollbar {\n width: 6px;\n}\n.buildings-list .buildings-scroll::-webkit-scrollbar-track {\n background: transparent;\n}\n.buildings-list .buildings-scroll::-webkit-scrollbar-thumb {\n background-color: var(--ion-color-medium-tint);\n border-radius: 3px;\n}\n.buildings-list .buildings-scroll::-webkit-scrollbar-thumb:hover {\n background-color: var(--ion-color-primary-tint);\n}\n.building-item {\n display: flex;\n align-items: center;\n gap: 12px;\n width: 100%;\n padding: 12px 14px;\n margin-bottom: 6px;\n border: 1px solid transparent;\n border-radius: 6px;\n background: transparent;\n cursor: pointer;\n transition: all 0.15s ease;\n text-align: left;\n}\n.building-item:hover {\n background: var(--ion-color-light, rgba(0, 0, 0, 0.03));\n border-color: var(--ion-color-medium-tint, rgba(0, 0, 0, 0.08));\n}\n.building-item:active {\n background: var(--ion-color-medium-tint, rgba(0, 0, 0, 0.05));\n}\n.building-item.selected {\n background: rgba(0, 0, 0, 0.04);\n border-color: rgba(0, 0, 0, 0.12);\n}\n.building-item:last-child {\n margin-bottom: 0;\n}\n.building-item .building-icon {\n flex-shrink: 0;\n width: 36px;\n height: 36px;\n display: flex;\n align-items: center;\n justify-content: center;\n background:\n linear-gradient(\n 135deg,\n var(--ion-color-primary-tint, rgba(var(--ion-color-primary-rgb), 0.15)),\n var(--ion-color-primary-shade, rgba(var(--ion-color-primary-rgb), 0.05)));\n border-radius: 6px;\n color: white;\n}\n.building-item .building-info {\n flex: 1;\n min-width: 0;\n}\n.building-item .building-info .building-name {\n font-size: 13px;\n font-weight: 600;\n color: var(--ion-text-color);\n margin-bottom: 3px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.building-item .building-info .building-stats {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n}\n.building-item .building-info .building-stats .stat {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 10px;\n color: var(--ion-color-medium);\n white-space: nowrap;\n}\n.building-item .building-info .building-stats .stat ng-icon {\n opacity: 0.7;\n}\n.building-item .building-info .building-stats .stat:not(:last-child)::after {\n content: "\\2022";\n margin-left: 10px;\n opacity: 0.3;\n}\n.building-item .building-arrow {\n flex-shrink: 0;\n color: var(--ion-color-medium);\n opacity: 0.5;\n transition: opacity 0.15s ease;\n}\n.building-item .building-arrow .checkmark {\n stroke-width: 3;\n opacity: 1;\n color: var(--ion-color-primary);\n}\n.building-item:hover .building-arrow {\n opacity: 0.8;\n}\n.panel-actions {\n display: flex;\n gap: 10px;\n padding: 14px 16px;\n border-top: 1px solid var(--ion-color-medium-tint, rgba(0, 0, 0, 0.08));\n background: var(--ion-color-light, rgba(0, 0, 0, 0.02));\n}\n.panel-actions .action-button {\n flex: 1;\n padding: 12px 20px;\n border-radius: 8px;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s ease;\n border: none;\n outline: none;\n}\n.panel-actions .action-button.cancel {\n background: transparent;\n color: var(--ion-text-color);\n border: 1px solid var(--ion-color-medium-tint, rgba(0, 0, 0, 0.2));\n}\n.panel-actions .action-button.cancel:hover:not(:disabled) {\n background: var(--ion-color-light);\n border-color: var(--ion-color-medium);\n}\n.panel-actions .action-button.primary {\n background: var(--ion-color-primary);\n color: var(--ion-color-primary-contrast);\n}\n.panel-actions .action-button.primary:hover:not(:disabled) {\n background: var(--ion-color-primary-shade);\n}\n.panel-actions .action-button.primary:disabled {\n opacity: 0.4;
19832\n cursor: not-allowed;\n}\n.panel-actions .action-button:active:not(:disabled) {\n transform: scale(0.98);\n}\n.panel-actions .action-button:focus {\n box-shadow: 0 0 0 3px rgba(var(--ion-color-primary-rgb), 0.2);\n}\n@media (max-width: 768px) {\n .building-selection-modal {\n min-width: 90vw;\n max-width: 95vw;\n max-height: 90vh;\n margin: 10px;\n }\n}\n'] }] 19833 }], () => [{ type: ChangeDetectorRef }], { buildings: [{ 19834 type: Input 19835 }], title: [{ 19836 type: Input 19837 }], buildingSelected: [{ 19838 type: Output 19839 }], cancelRequested: [{ 19840 type: Output 19841 }], closeRequested: [{ 19842 type: Output 19843 }] }); 19844})(); 19845(() => { 19846 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(BuildingSelectionModalComponent, { className: "BuildingSelectionModalComponent", filePath: "src/app/pages/campus-map-v3/components/building-selection-modal/building-selection-modal.component.ts", lineNumber: 44 }); 19847})(); 19848
19849// src/app/modals/navigation-link-share.modal.ts 19850var QRCode = __toESM(require_browser()); 19851function NavigationLinkShareModalComponent_Conditional_12_Template(rf, ctx) { 19852 if (rf & 1) { 19853 \u0275\u0275elementStart(0, "p"); 19854 \u0275\u0275text(1); 19855 \u0275\u0275elementEnd(); 19856 } 19857 if (rf & 2) { 19858 const ctx_r0 = \u0275\u0275nextContext(); 19859 \u0275\u0275advance(); 19860 \u0275\u0275textInterpolate(ctx_r0.locationText); 19861 } 19862} 19863function NavigationLinkShareModalComponent_Conditional_16_Template(rf, ctx) { 19864 if (rf & 1) { 19865 \u0275\u0275elementStart(0, "div", 7); 19866 \u0275\u0275text(1, "Creating short link..."); 19867 \u0275\u0275elementEnd(); 19868 } 19869} 19870function NavigationLinkShareModalComponent_Conditional_17_Template(rf, ctx) { 19871 if (rf & 1) { 19872 const _r2 = \u0275\u0275getCurrentView(); 19873 \u0275\u0275elementStart(0, "button", 19); 19874 \u0275\u0275listener("click", function NavigationLinkShareModalComponent_Conditional_17_Template_button_click_0_listener() { 19875 \u0275\u0275restoreView(_r2); 19876 const ctx_r0 = \u0275\u0275nextContext(); 19877 return \u0275\u0275resetView(ctx_r0.copyActiveLink()); 19878 }); 19879 \u0275\u0275elementStart(1, "span"); 19880 \u0275\u0275text(2); 19881 \u0275\u0275elementEnd(); 19882 \u0275\u0275element(3, "ng-icon", 16); 19883 \u0275\u0275elementEnd(); 19884 } 19885 if (rf & 2) { 19886 const ctx_r0 = \u0275\u0275nextContext(); 19887 \u0275\u0275advance(2); 19888 \u0275\u0275textInterpolate(ctx_r0.shortLink); 19889 } 19890} 19891function NavigationLinkShareModalComponent_Conditional_18_Template(rf, ctx) { 19892 if (rf & 1) { 19893 \u0275\u0275elementStart(0, "div", 9); 19894 \u0275\u0275text(1); 19895 \u0275\u0275elementEnd(); 19896 } 19897 if (rf & 2) { 19898 const ctx_r0 = \u0275\u0275nextContext(); 19899 \u0275\u0275advance(); 19900 \u0275\u0275textInterpolate(ctx_r0.shortLinkError); 19901 } 19902} 19903function NavigationLinkShareModalComponent_Conditional_19_Template(rf, ctx) { 19904 if (rf & 1) { 19905 \u0275\u0275elementStart(0, "section", 10); 19906 \u0275\u0275element(1, "img", 20); 19907 \u0275\u0275elementEnd(); 19908 } 19909 if (rf & 2) { 19910 const ctx_r0 = \u0275\u0275nextContext(); 19911 \u0275\u0275advance(); 19912 \u0275\u0275property("src", ctx_r0.qrCodeDataUrl, \u0275\u0275sanitizeUrl); 19913 } 19914} 19915var _NavigationLinkShareModalComponent = class _NavigationLinkShareModalComponent { 19916 constructor(modalController, deepLinkService, toastService) { 19917 this.modalController = modalController; 19918 this.deepLinkService = deepLinkService; 19919 this.toastService = toastService; 19920 this.locationText = ""; 19921 this.longLink = ""; 19922 this.shortLink = ""; 19923 this.activeLink = ""; 19924 this.qrCodeDataUrl = ""; 19925 this.isLoadingShortLink = true; 19926 this.shortLinkError = ""; 19927 } 19928 ngOnInit() { 19929 return __async(this, null, function* () { 19930 this.longLink = this.deepLinkService.generateLongNavigationUrl(this.kind, this.targetId, this.campusId); 19931 this.activeLink = this.longLink;
19932 try { 19933 const shortLink = yield this.deepLinkService.createShortLink(this.kind, this.targetId, this.campusId); 19934 this.shortLink = this.deepLinkService.generateShortNavigationUrl(shortLink.hash); 19935 this.activeLink = this.shortLink; 19936 } catch (error) { 19937 console.error("[NavigationLinkShareModal] Failed to create short link:", error); 19938 this.shortLinkError = "Short link unavailable. The long link is ready to use."; 19939 } finally { 19940 this.isLoadingShortLink = false; 19941 yield this.generateQrCode(); 19942 } 19943 }); 19944 } 19945 copyActiveLink() { 19946 return __async(this, null, function* () { 19947 yield this.copyLink(this.activeLink, "Link copied"); 19948 }); 19949 } 19950 copyLongLink() { 19951 return __async(this, null, function* () { 19952 yield this.copyLink(this.longLink, "Long link copied"); 19953 }); 19954 } 19955 shareLink() { 19956 return __async(this, null, function* () { 19957 try { 19958 const canShare = yield Share.canShare(); 19959 if (canShare.value) { 19960 yield Share.share({ 19961 title: this.titleText, 19962 text: `${this.titleText} 19963${this.activeLink}`, 19964 dialogTitle: "Share navigation link" 19965 }); 19966 } else { 19967 yield this.copyActiveLink(); 19968 } 19969 } catch (error) { 19970 console.error("[NavigationLinkShareModal] Failed to share link:", error); 19971 yield this.toastService.showError("Unable to share link"); 19972 } 19973 }); 19974 } 19975 dismiss() { 19976 this.modalController.dismiss(); 19977 } 19978 copyLink(link, message) { 19979 return __async(this, null, function* () { 19980 try { 19981 yield Clipboard.write({ string: link }); 19982 yield this.toastService.showSuccess(message); 19983 } catch { 19984 yield this.toastService.showError("Unable to copy link"); 19985 } 19986 }); 19987 } 19988 generateQrCode() { 19989 return __async(this, null, function* () { 19990 this.qrCodeDataUrl = yield QRCode.toDataURL(this.activeLink, { 19991 errorCorrectionLevel: "H", 19992 type: "image/png", 19993 width: 300, 19994 margin: 2, 19995 color: { 19996 dark: "#000000", 19997 light: "#FFFFFF" 19998 } 19999 }); 20000 }); 20001 } 20002}; 20003_NavigationLinkShareModalComponent.\u0275fac = function NavigationLinkShareModalComponent_Factory(__ngFactoryType__) { 20004 return new (__ngFactoryType__ || _NavigationLinkShareModalComponent)(\u0275\u0275directiveInject(ModalController), \u0275\u0275directiveInject(DeepLinkService), \u0275\u0275directiveInject(ToastService)); 20005}; 20006_NavigationLinkShareModalComponent.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _NavigationLinkShareModalComponent, selectors: [["app-navigation-link-share-modal"]], inputs: { kind: "kind", targetId: "targetId", campusId: "campusId", titleText: "titleText", locationText: "locationText" }, features: [\u0275\u0275ProvidersFeature([], [provideIcons({ lucideClipboard, lucideExternalLink, lucideShare2, lucideX })])], decls: 36, vars: 5, consts: [["slot", "end"], ["fill", "clear", "aria-label", "Close", 3, "click"], ["name", "lucideX", "size", "1.25em"], [1, "share-link-modal"], [1, "destination-summary"], [1, "link-section"], [1, "section-label"], [1, "link-box", "muted"], ["type", "button", 1, "link-box"], [1, "link-box", "warning"], [1, "qr-section"], [1, "long-link-section"], ["type", "button", 1, "link-box", "compact", 3, "click"], ["name", "lucideExternalLink", "size", "1em"], [1, "actions"], ["type", "button", 1, "action-button", "secondary", 3, "click"], ["name", "lucideClipboard", "size", "1em"], ["type", "button", 1, "action-button", "primary", 3, "click"], ["name", "lucideShare2", "size", "1em"], ["type", "button", 1, "link-box", 3, "click"], ["alt", "Navigation link QR code", 3, "src"]], template: function NavigationLinkShareModalComponent_Template(rf, ctx) { 20007 if (rf & 1) { 20008 \u0275\u0275elementStart(0, "ion-header")(1, "ion-toolbar")(2, "ion-title"); 20009 \u0275\u0275text(3, "Share Navigation Link"); 20010 \u0275\u0275elementEnd(); 20011 \u0275\u0275elementStart(4, "ion-buttons", 0)(5, "ion-button", 1); 20012 \u0275\u0275listener("click", function NavigationLinkShareModalComponent_Template_ion_button_click_5_listener() { 20013 return ctx.dismiss(); 20014 }); 20015 \u0275\u0275element(6, "ng-icon", 2); 20016 \u0275\u0275elementEnd()()()(); 20017 \u0275\u0275elementStart(7, "ion-content")(8, "div", 3)(9, "section", 4)(10, "h2"); 20018 \u0275\u0275text(11); 20019 \u0275\u0275elementEnd(); 20020 \u0275\u0275template(12, NavigationLinkShareModalComponent_Conditional_12_Template, 2, 1, "p"); 20021 \u0275\u0275elementEnd(); 20022 \u0275\u0275elementStart(13, "section", 5)(14, "div", 6); 20023 \u0275\u0275text(15, "Short link"); 20024 \u0275\u0275elementEnd(); 20025 \u0275\u0275template(16, NavigationLinkShareModalComponent_Conditional_16_Template, 2, 0, "div", 7)(17, NavigationLinkShareModalComponent_Conditional_17_Template, 4, 1, "button", 8)(18, NavigationLinkShareModalComponent_Conditional_18_Template, 2, 1, "div", 9); 20026 \u0275\u0275elementEnd(); 20027 \u0275\u0275template(19, NavigationLinkShareModalComponent_Conditional_19_Template, 2, 1, "section", 10); 20028 \u0275\u0275elementStart(20, "section", 11)(21, "div", 6); 20029 \u0275\u0275text(22, "Long link"); 20030 \u0275\u0275elementEnd(); 20031 \u0275\u0275elementStart(23, "button", 12); 20032 \u0275\u0275listener("click", function NavigationLinkShareModalComponent_Template_button_click_23_listener() { 20033 return ctx.copyLongLink(); 20034 }); 20035 \u0275\u0275elementStart(24, "span"); 20036 \u0275\u0275text(25); 20037 \u0275\u0275elementEnd(); 20038 \u0275\u0275element(26, "ng-icon", 13); 20039 \u0275\u0275elementEnd()(); 20040 \u0275\u0275elementStart(27, "div", 14)(28, "button", 15); 20041 \u0275\u0275listener("click", function NavigationLinkShareModalComponent_Template_button_click_28_listener() { 20042 return ctx.copyActiveLink(); 20043 }); 20044 \u0275\u0275element(29, "ng-icon", 16); 20045 \u0275\u0275elementStart(30, "span"); 20046 \u0275\u0275text(31, "Copy"); 20047 \u0275\u0275elementEnd()(); 20048 \u0275\u0275elementStart(32, "button", 17); 20049 \u0275\u0275listener("click", function NavigationLinkShareModalComponent_Template_button_click_32_listener() { 20050 return ctx.shareLink(); 20051 }); 20052 \u0275\u0275element(33, "ng-icon", 18); 20053 \u0275\u0275elementStart(34, "span"); 20054 \u0275\u0275text(35, "Share"); 20055 \u0275\u0275elementEnd()()()()(); 20056 } 20057 if (rf & 2) { 20058 \u0275\u0275advance(11); 20059 \u0275\u0275textInterpolate(ctx.titleText); 20060 \u0275\u0275advance(); 20061 \u0275\u0275conditional(ctx.locationText ? 12 : -1); 20062 \u0275\u0275advance(4); 20063 \u0275\u0275conditional(ctx.isLoadingShortLink ? 16 : ctx.shortLink ? 17 : 18); 20064 \u0275\u0275advance(3); 20065 \u0275\u0275conditional(ctx.qrCodeDataUrl ? 19 : -1); 20066 \u0275\u0275advance(6); 20067 \u0275\u0275textInterpolate(ctx.longLink); 20068 } 20069}, dependencies: [CommonModule, IonHeader, IonToolbar, IonTitle, IonButtons, IonButton, IonContent, NgIcon], styles: ["\n\n.share-link-modal[_ngcontent-%COMP%] {\n display: grid;\n gap: 18px;\n padding: 18px;\n}\n.destination-summary[_ngcontent-%COMP%] h2[_ngcontent-%COMP%] {\n margin: 0;\n color: #111827;\n font-size: 1.2rem;\n font-weight: 800;\n line-height: 1.25;\n}\n.destination-summary[_ngcontent-%COMP%] p[_ngcontent-%COMP%] {\n margin: 4px 0 0;\n color: #64748b;\n font-size: 0.92rem;\n}\n.section-label[_ngcontent-%COMP%] {\n margin-bottom: 6px;\n color: #475569;\n font-size: 0.78rem;\n font-weight: 800;\n letter-spacing: 0;\n text-transform: uppercase;\n}\n.link-box[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n width: 100%;\n min-height: 44px;\n padding: 10px 12px;\n border: 1px solid #cbd5e1;\n border-radius: 8px;\n background: #fff;
20069\n color: #0f172a;\n font-size: 0.92rem;\n line-height: 1.25;\n text-align: left;\n}\n.link-box[_ngcontent-%COMP%] span[_ngcontent-%COMP%] {\n overflow-wrap: anywhere;\n}\n.link-box.compact[_ngcontent-%COMP%] {\n font-size: 0.8rem;\n}\n.link-box.muted[_ngcontent-%COMP%] {\n color: #64748b;\n}\n.link-box.warning[_ngcontent-%COMP%] {\n border-color: #f59e0b;\n color: #92400e;\n}\n.qr-section[_ngcontent-%COMP%] {\n display: grid;\n justify-content: center;\n}\n.qr-section[_ngcontent-%COMP%] img[_ngcontent-%COMP%] {\n width: min(300px, 78vw);\n height: auto;\n border: 1px solid #e2e8f0;\n border-radius: 8px;\n}\n.actions[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 10px;\n}\n.action-button[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n min-height: 44px;\n border-radius: 8px;\n font-weight: 800;\n}\n.action-button.primary[_ngcontent-%COMP%] {\n border: 1px solid #2563eb;\n background: #2563eb;\n color: #fff;\n}\n.action-button.secondary[_ngcontent-%COMP%] {\n border: 1px solid #cbd5e1;\n background: #fff;\n color: #334155;\n}"] }); 20070var NavigationLinkShareModalComponent = _NavigationLinkShareModalComponent; 20071(() => { 20072 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(NavigationLinkShareModalComponent, [{ 20073 type: Component, 20074 args: [{ selector: "app-navigation-link-share-modal", standalone: true, imports: [CommonModule, IonHeader, IonToolbar, IonTitle, IonButtons, IonButton, IonContent, NgIcon], viewProviders: [provideIcons({ lucideClipboard, lucideExternalLink, lucideShare2, lucideX })], template: '<ion-header>\n <ion-toolbar>\n <ion-title>Share Navigation Link</ion-title>\n <ion-buttons slot="end">\n <ion-button fill="clear" (click)="dismiss()" aria-label="Close">\n <ng-icon name="lucideX" size="1.25em"></ng-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content>\n <div class="share-link-modal">\n <section class="destination-summary">\n <h2>{{ titleText }}</h2>\n @if (locationText) {\n <p>{{ locationText }}</p>\n }\n </section>\n\n <section class="link-section">\n <div class="section-label">Short link</div>\n @if (isLoadingShortLink) {\n <div class="link-box muted">Creating short link...</div>\n } @else if (shortLink) {\n <button type="button" class="link-box" (click)="copyActiveLink()">\n <span>{{ shortLink }}</span>\n <ng-icon name="lucideClipboard" size="1em"></ng-icon>\n </button>\n } @else {\n <div class="link-box warning">{{ shortLinkError }}</div>\n }\n </section>\n\n @if (qrCodeDataUrl) {\n <section class="qr-section">\n <img [src]="qrCodeDataUrl" alt="Navigation link QR code">\n </section>\n }\n\n <section class="long-link-section">\n <div class="section-label">Long link</div>\n <button type="button" class="link-box compact" (click)="copyLongLink()">\n <span>{{ longLink }}</span>\n <ng-icon name="lucideExternalLink" size="1em"></ng-icon>\n </button>\n </section>\n\n <div class="actions">\n <button type="button" class="action-button secondary" (click)="copyActiveLink()">\n <ng-icon name="lucideClipboard" size="1em"></ng-icon>\n <span>Copy</span>\n </button>\n <button type="button" class="action-button primary" (click)="shareLink()">\n <ng-icon name="lucideShare2" size="1em"></ng-icon>\n <span>Share</span>\n </button>\n </div>\n </div>\n</ion-content>\n', styles: ["/* src/app/modals/navigation-link-share.modal.scss */\n.share-link-modal {\n display: grid;\n gap: 18px;\n padding: 18px;\n}\n.destination-summary h2 {\n margin: 0;\n color: #111827;\n font-size: 1.2rem;\n font-weight: 800;\n line-height: 1.25;\n}\n.destination-summary p {\n margin: 4px 0 0;\n color: #64748b;\n font-size: 0.92rem;\n}\n.section-label {\n margin-bottom: 6px;\n color: #475569;\n font-size: 0.78rem;\n font-weight: 800;\n letter-spacing: 0;\n text-transform: uppercase;\n}\n.link-box {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n width: 100%;\n min-height: 44px;\n padding: 10px 12px;\n border: 1px solid #cbd5e1;\n border-radius: 8px;\n background: #fff;
20074\n color: #0f172a;\n font-size: 0.92rem;\n line-height: 1.25;\n text-align: left;\n}\n.link-box span {\n overflow-wrap: anywhere;\n}\n.link-box.compact {\n font-size: 0.8rem;\n}\n.link-box.muted {\n color: #64748b;\n}\n.link-box.warning {\n border-color: #f59e0b;\n color: #92400e;\n}\n.qr-section {\n display: grid;\n justify-content: center;\n}\n.qr-section img {\n width: min(300px, 78vw);\n height: auto;\n border: 1px solid #e2e8f0;\n border-radius: 8px;\n}\n.actions {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 10px;\n}\n.action-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n min-height: 44px;\n border-radius: 8px;\n font-weight: 800;\n}\n.action-button.primary {\n border: 1px solid #2563eb;\n background: #2563eb;\n color: #fff;\n}\n.action-button.secondary {\n border: 1px solid #cbd5e1;\n background: #fff;\n color: #334155;\n}\n"] }] 20075 }], () => [{ type: ModalController }, { type: DeepLinkService }, { type: ToastService }], { kind: [{ 20076 type: Input, 20077 args: [{ required: true }] 20078 }], targetId: [{ 20079 type: Input, 20080 args: [{ required: true }] 20081 }], campusId: [{ 20082 type: Input, 20083 args: [{ required: true }] 20084 }], titleText: [{ 20085 type: Input, 20086 args: [{ required: true }] 20087 }], locationText: [{ 20088 type: Input 20089 }] }); 20090})(); 20091(() => { 20092 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(NavigationLinkShareModalComponent, { className: "NavigationLinkShareModalComponent", filePath: "src/app/modals/navigation-link-share.modal.ts", lineNumber: 28 }); 20093})(); 20094
vendor: 99,928 bytes, lines 20095-22492
20095// node_modules/camera-controls/dist/camera-controls.module.js 20096var MOUSE_BUTTON = { 20097 LEFT: 1, 20098 RIGHT: 2, 20099 MIDDLE: 4 20100}; 20101var ACTION = Object.freeze({ 20102 NONE: 0, 20103 ROTATE: 1, 20104 TRUCK: 2, 20105 SCREEN_PAN: 4, 20106 OFFSET: 8, 20107 DOLLY: 16, 20108 ZOOM: 32, 20109 TOUCH_ROTATE: 64, 20110 TOUCH_TRUCK: 128, 20111 TOUCH_SCREEN_PAN: 256, 20112 TOUCH_OFFSET: 512, 20113 TOUCH_DOLLY: 1024, 20114 TOUCH_ZOOM: 2048, 20115 TOUCH_DOLLY_TRUCK: 4096, 20116 TOUCH_DOLLY_SCREEN_PAN: 8192, 20117 TOUCH_DOLLY_OFFSET: 16384, 20118 TOUCH_DOLLY_ROTATE: 32768, 20119 TOUCH_ZOOM_TRUCK: 65536, 20120 TOUCH_ZOOM_OFFSET: 131072, 20121 TOUCH_ZOOM_SCREEN_PAN: 262144, 20122 TOUCH_ZOOM_ROTATE: 524288 20123}); 20124var DOLLY_DIRECTION = { 20125 NONE: 0, 20126 IN: 1, 20127 OUT: -1 20128}; 20129function isPerspectiveCamera(camera) { 20130 return camera.isPerspectiveCamera; 20131} 20132function isOrthographicCamera(camera) { 20133 return camera.isOrthographicCamera; 20134} 20135var PI_2 = Math.PI * 2; 20136var PI_HALF = Math.PI / 2; 20137var EPSILON2 = 1e-5; 20138var DEG2RAD = Math.PI / 180; 20139function clamp(value, min, max) { 20140 return Math.max(min, Math.min(max, value)); 20141} 20142function approxZero(number, error = EPSILON2) { 20143 return Math.abs(number) < error; 20144} 20145function approxEquals(a, b, error = EPSILON2) { 20146 return approxZero(a - b, error); 20147} 20148function roundToStep(value, step) { 20149 return Math.round(value / step) * step; 20150} 20151function infinityToMaxNumber(value) { 20152 if (isFinite(value)) return value; 20153 if (value < 0) return -Number.MAX_VALUE; 20154 return Number.MAX_VALUE; 20155} 20156function maxNumberToInfinity(value) { 20157 if (Math.abs(value) < Number.MAX_VALUE) return value; 20158 return value * Infinity; 20159} 20160function smoothDamp(current, target, currentVelocityRef, smoothTime, maxSpeed = Infinity, deltaTime) { 20161 smoothTime = Math.max(1e-4, smoothTime); 20162 const omega = 2 / smoothTime; 20163 const x = omega * deltaTime; 20164 const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x); 20165 let change = current - target; 20166 const originalTo = target; 20167 const maxChange = maxSpeed * smoothTime; 20168 change = clamp(change, -maxChange, maxChange); 20169 target = current - change; 20170 const temp = (currentVelocityRef.value + omega * change) * deltaTime; 20171 currentVelocityRef.value = (currentVelocityRef.value - omega * temp) * exp; 20172 let output = target + (change + temp) * exp; 20173 if (originalTo - current > 0 === output > originalTo) { 20174 output = originalTo; 20175 currentVelocityRef.value = (output - originalTo) / deltaTime; 20176 } 20177 return output; 20178} 20179function smoothDampVec3(current, target, currentVelocityRef, smoothTime, maxSpeed = Infinity, deltaTime, out) { 20180 smoothTime = Math.max(1e-4, smoothTime); 20181 const omega = 2 / smoothTime; 20182 const x = omega * deltaTime; 20183 const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x); 20184 let targetX = target.x; 20185 let targetY = target.y; 20186 let targetZ = target.z; 20187 let changeX = current.x - targetX; 20188 let changeY = current.y - targetY; 20189 let changeZ = current.z - targetZ; 20190 const originalToX = targetX; 20191 const originalToY = targetY; 20192 const originalToZ = targetZ; 20193 const maxChange = maxSpeed * smoothTime; 20194 const maxChangeSq = maxChange * maxChange; 20195 const magnitudeSq = changeX * changeX + changeY * changeY + changeZ * changeZ; 20196 if (magnitudeSq > maxChangeSq) { 20197 const magnitude = Math.sqrt(magnitudeSq); 20198 changeX = changeX / magnitude * maxChange; 20199 changeY = changeY / magnitude * maxChange; 20200 changeZ = changeZ / magnitude * maxChange; 20201 } 20202 targetX = current.x - changeX; 20203 targetY = current.y - changeY; 20204 targetZ = current.z - changeZ; 20205 const tempX = (currentVelocityRef.x + omega * changeX) * deltaTime; 20206 const tempY = (currentVelocityRef.y + omega * changeY) * deltaTime; 20207 const tempZ = (currentVelocityRef.z + omega * changeZ) * deltaTime; 20208 currentVelocityRef.x = (currentVelocityRef.x - omega * tempX) * exp; 20209 currentVelocityRef.y = (currentVelocityRef.y - omega * tempY) * exp; 20210 currentVelocityRef.z = (currentVelocityRef.z - omega * tempZ) * exp; 20211 out.x = targetX + (changeX + tempX) * exp; 20212 out.y = targetY + (changeY + tempY) * exp; 20213 out.z = targetZ + (changeZ + tempZ) * exp; 20214 const origMinusCurrentX = originalToX - current.x; 20215 const origMinusCurrentY = originalToY - current.y; 20216 const origMinusCurrentZ = originalToZ - current.z; 20217 const outMinusOrigX = out.x - originalToX; 20218 const outMinusOrigY = out.y - originalToY; 20219 const outMinusOrigZ = out.z - originalToZ; 20220 if (origMinusCurrentX * outMinusOrigX + origMinusCurrentY * outMinusOrigY + origMinusCurrentZ * outMinusOrigZ > 0) { 20221 out.x = originalToX; 20222 out.y = originalToY; 20223 out.z = originalToZ; 20224 currentVelocityRef.x = (out.x - originalToX) / deltaTime; 20225 currentVelocityRef.y = (out.y - originalToY) / deltaTime; 20226 currentVelocityRef.z = (out.z - originalToZ) / deltaTime; 20227 } 20228 return out; 20229} 20230function extractClientCoordFromEvent(pointers, out) { 20231 out.set(0, 0); 20232 pointers.forEach((pointer) => { 20233 out.x += pointer.clientX; 20234 out.y += pointer.clientY; 20235 }); 20236 out.x /= pointers.length; 20237 out.y /= pointers.length; 20238} 20239function notSupportedInOrthographicCamera(camera, message) { 20240 if (isOrthographicCamera(camera)) { 20241 console.warn(`${message} is not supported in OrthographicCamera`); 20242 return true; 20243 } 20244 return false; 20245} 20246var EventDispatcher = class { 20247 _listeners = {}; 20248 /** 20249 * Adds the specified event listener. 20250 * @param type event name 20251 * @param listener handler function 20252 * @category Methods 20253 */ 20254 addEventListener(type, listener) { 20255 const listeners = this._listeners; 20256 if (listeners[type] === void 0) listeners[type] = []; 20257 if (listeners[type].indexOf(listener) === -1) listeners[type].push(listener); 20258 } 20259 /** 20260 * Presence of the specified event listener. 20261 * @param type event name 20262 * @param listener handler function 20263 * @category Methods 20264 */ 20265 hasEventListener(type, listener) { 20266 const listeners = this._listeners; 20267 return listeners[type] !== void 0 && listeners[type].indexOf(listener) !== -1; 20268 } 20269 /** 20270 * Removes the specified event listener 20271 * @param type event name 20272 * @param listener handler function 20273 * @category Methods 20274 */ 20275 removeEventListener(type, listener) { 20276 const listeners = this._listeners; 20277 const listenerArray = listeners[type]; 20278 if (listenerArray !== void 0) { 20279 const index = listenerArray.indexOf(listener); 20280 if (index !== -1) listenerArray.splice(index, 1); 20281 } 20282 } 20283 /** 20284 * Removes all event listeners 20285 * @param type event name 20286 * @category Methods 20287 */ 20288 removeAllEventListeners(type) { 20289 if (!type) { 20290 this._listeners = {}; 20291 return; 20292 } 20293 if (Array.isArray(this._listeners[type])) this._listeners[type].length = 0; 20294 } 20295 /** 20296 * Fire an event type. 20297 * @param event DispatcherEvent 20298 * @category Methods 20299 */ 20300 dispatchEvent(event) { 20301 const listeners = this._listeners; 20302 const listenerArray = listeners[event.type]; 20303 if (listenerArray !== void 0) { 20304 event.target = this; 20305 const array = listenerArray.slice(0); 20306 for (let i = 0, l = array.length; i < l; i++) { 20307 array[i].call(this, event); 20308 } 20309 } 20310 } 20311}; 20312var VERSION = "3.1.2"; 20313var TOUCH_DOLLY_FACTOR = 1 / 8; 20314var _a2; 20315var isMac = /Mac/.test((_a2 = globalThis == null ? void 0 : globalThis.navigator) == null ? void 0 : _a2.platform); 20316var THREE; 20317var _ORIGIN; 20318var _AXIS_Y; 20319var _AXIS_Z; 20320var _v2; 20321var _v3A; 20322var _v3B; 20323var _v3C; 20324var _cameraDirection; 20325var _xColumn; 20326var _yColumn; 20327var _zColumn; 20328var _deltaTarget; 20329var _deltaOffset; 20330var _sphericalA; 20331var _sphericalB; 20332var _box3A; 20333var _box3B; 20334var _sphere; 20335var _quaternionA; 20336var _quaternionB; 20337var _rotationMatrix; 20338var _raycaster; 20339var CameraControls = class _CameraControls extends EventDispatcher { 20340 /** 20341 * Injects THREE as the dependency. You can then proceed to use CameraControls. 20342 * 20343 * e.g 20344 * ```javascript 20345 * CameraControls.install( { THREE: THREE } ); 20346 * ``` 20347 * 20348 * Note: If you do not wish to use enter three.js to reduce file size(tree-shaking for example), make a subset to install. 20349 * 20350 * ```js 20351 * import { 20352 * Vector2, 20353 * Vector3, 20354 * Vector4, 20355 * Quaternion, 20356 * Matrix4, 20357 * Spherical, 20358 * Box3, 20359 * Sphere, 20360 * Raycaster, 20361 * MathUtils, 20362 * } from 'three'; 20363 * 20364 * const subsetOfTHREE = { 20365 * Vector2 : Vector2, 20366 * Vector3 : Vector3, 20367 * Vector4 : Vector4, 20368 * Quaternion: Quaternion, 20369 * Matrix4 : Matrix4, 20370 * Spherical : Spherical, 20371 * Box3 : Box3, 20372 * Sphere : Sphere, 20373 * Raycaster : Raycaster, 20374 * }; 20375 * CameraControls.install( { THREE: subsetOfTHREE } ); 20376 * ``` 20377 * @category Statics 20378 */ 20379 static install(libs) { 20380 THREE = libs.THREE; 20381 _ORIGIN = Object.freeze(new THREE.Vector3(0, 0, 0)); 20382 _AXIS_Y = Object.freeze(new THREE.Vector3(0, 1, 0)); 20383 _AXIS_Z = Object.freeze(new THREE.Vector3(0, 0, 1)); 20384 _v2 = new THREE.Vector2(); 20385 _v3A = new THREE.Vector3(); 20386 _v3B = new THREE.Vector3(); 20387 _v3C = new THREE.Vector3(); 20388 _cameraDirection = new THREE.Vector3(); 20389 _xColumn = new THREE.Vector3(); 20390 _yColumn = new THREE.Vector3(); 20391 _zColumn = new THREE.Vector3(); 20392 _deltaTarget = new THREE.Vector3(); 20393 _deltaOffset = new THREE.Vector3(); 20394 _sphericalA = new THREE.Spherical(); 20395 _sphericalB = new THREE.Spherical(); 20396 _box3A = new THREE.Box3(); 20397 _box3B = new THREE.Box3(); 20398 _sphere = new THREE.Sphere(); 20399 _quaternionA = new THREE.Quaternion(); 20400 _quaternionB = new THREE.Quaternion(); 20401 _rotationMatrix = new THREE.Matrix4(); 20402 _raycaster = new THREE.Raycaster(); 20403 } 20404 /** 20405 * list all ACTIONs 20406 * @category Statics 20407 */ 20408 static get ACTION() { 20409 return ACTION; 20410 } 20411 /** 20412 * Minimum vertical angle in radians. 20413 * The angle has to be between `0` and `.maxPolarAngle` inclusive. 20414 * The default value is `0`. 20415 * 20416 * e.g. 20417 * ``` 20418 * cameraControls.maxPolarAngle = 0; 20419 * ``` 20420 * @category Properties 20421 */ 20422 minPolarAngle = 0; 20423 // radians 20424 /** 20425 * Maximum vertical angle in radians. 20426 * The angle has to be between `.maxPolarAngle` and `Math.PI` inclusive. 20427 * The default value is `Math.PI`. 20428 * 20429 * e.g. 20430 * ``` 20431 * cameraControls.maxPolarAngle = Math.PI; 20432 * ``` 20433 * @category Properties 20434 */ 20435 maxPolarAngle = Math.PI; 20436 // radians 20437 /** 20438 * Minimum horizontal angle in radians. 20439 * The angle has to be less than `.maxAzimuthAngle`. 20440 * The default value is `- Infinity`. 20441 * 20442 * e.g. 20443 * ``` 20444 * cameraControls.minAzimuthAngle = - Infinity; 20445 * ``` 20446 * @category Properties 20447 */ 20448 minAzimuthAngle = -Infinity; 20449 // radians 20450 /** 20451 * Maximum horizontal angle in radians. 20452 * The angle has to be greater than `.minAzimuthAngle`. 20453 * The default value is `Infinity`. 20454 * 20455 * e.g. 20456 * ``` 20457 * cameraControls.maxAzimuthAngle = Infinity; 20458 * ``` 20459 * @category Properties 20460 */ 20461 maxAzimuthAngle = Infinity; 20462 // radians 20463 // How far you can dolly in and out ( PerspectiveCamera only ) 20464 /** 20465 * Minimum distance for dolly. The value must be higher than `0`. Default is `Number.EPSILON`. 20466 * PerspectiveCamera only. 20467 * @category Properties 20468 */ 20469 minDistance = Number.EPSILON; 20470 /** 20471 * Maximum distance for dolly. The value must be higher than `minDistance`. Default is `Infinity`. 20472 * PerspectiveCamera only. 20473 * @category Properties 20474 */ 20475 maxDistance = Infinity; 20476 /** 20477 * `true` to enable Infinity Dolly for wheel and pinch. Use this with `minDistance` and `maxDistance` 20478 * If the Dolly distance is less (or over) than the `minDistance` (or `maxDistance`), `infinityDolly` will keep the distance and pushes the target position instead. 20479 * @category Properties 20480 */ 20481 infinityDolly = false; 20482 /** 20483 * Minimum camera zoom. 20484 * @category Properties 20485 */ 20486 minZoom = 0.01; 20487 /** 20488 * Maximum camera zoom. 20489 * @category Properties 20490 */ 20491 maxZoom = Infinity; 20492 /** 20493 * Approximate time in seconds to reach the target. A smaller value will reach the target faster. 20494 * @category Properties 20495 */ 20496 smoothTime = 0.25; 20497 /** 20498 * the smoothTime while dragging 20499 * @category Properties 20500 */ 20501 draggingSmoothTime = 0.125; 20502 /** 20503 * Max transition speed in unit-per-seconds 20504 * @category Properties 20505 */ 20506 maxSpeed = Infinity; 20507 /** 20508 * Speed of azimuth (horizontal) rotation. 20509 * @category Properties 20510 */ 20511 azimuthRotateSpeed = 1; 20512 /** 20513 * Speed of polar (vertical) rotation. 20514 * @category Properties 20515 */ 20516 polarRotateSpeed = 1; 20517 /** 20518 * Speed of mouse-wheel dollying. 20519 * @category Properties 20520 */ 20521 dollySpeed = 1; 20522 /** 20523 * `true` to invert direction when dollying or zooming via drag 20524 * @category Properties 20525 */ 20526 dollyDragInverted = false; 20527 /** 20528 * Speed of drag for truck and pedestal. 20529 * @category Properties 20530 */ 20531 truckSpeed = 2; 20532 /** 20533 * `true` to enable Dolly-in to the mouse cursor coords. 20534 * @category Properties 20535 */ 20536 dollyToCursor = false; 20537 /** 20538 * @category Properties 20539 */ 20540 dragToOffset = false; 20541 /** 20542 * Friction ratio of the boundary. 20543 * @category Properties 20544 */ 20545 boundaryFriction = 0; 20546 /** 20547 * Controls how soon the `rest` event fires as the camera slows. 20548 * @category Properties 20549 */ 20550 restThreshold = 0.01; 20551 /** 20552 * An array of Meshes to collide with camera. 20553 * Be aware colliderMeshes may decrease performance. The collision test uses 4 raycasters from the camera since the near plane has 4 corners. 20554 * @category Properties 20555 */ 20556 colliderMeshes = []; 20557 // button configs 20558 /** 20559 * User's mouse input config. 20560 * 20561 * | button to assign | behavior | 20562 * | --------------------- | -------- | 20563 * | `mouseButtons.left` | `CameraControls.ACTION.ROTATE`* \| `CameraControls.ACTION.TRUCK` \| `CameraControls.ACTION.OFFSET` \| `CameraControls.ACTION.DOLLY` \| `CameraControls.ACTION.ZOOM` \| `CameraControls.ACTION.NONE` | 20564 * | `mouseButtons.right` | `CameraControls.ACTION.ROTATE` \| `CameraControls.ACTION.TRUCK`* \| `CameraControls.ACTION.OFFSET` \| `CameraControls.ACTION.DOLLY` \| `CameraControls.ACTION.ZOOM` \| `CameraControls.ACTION.NONE` | 20565 * | `mouseButtons.wheel` ¹ | `CameraControls.ACTION.ROTATE` \| `CameraControls.ACTION.TRUCK` \| `CameraControls.ACTION.OFFSET` \| `CameraControls.ACTION.DOLLY` \| `CameraControls.ACTION.ZOOM` \| `CameraControls.ACTION.NONE` | 20566 * | `mouseButtons.middle` ² | `CameraControls.ACTION.ROTATE` \| `CameraControls.ACTION.TRUCK` \| `CameraControls.ACTION.OFFSET` \| `CameraControls.ACTION.DOLLY`* \| `CameraControls.ACTION.ZOOM` \| `CameraControls.ACTION.NONE` | 20567 * 20568 * 1. Mouse wheel event for scroll "up/down" on mac "up/down/left/right" 20569 * 2. Mouse click on wheel event "button" 20570 * - \* is the default. 20571 * - The default of `mouseButtons.wheel` is: 20572 * - `DOLLY` for Perspective camera. 20573 * - `ZOOM` for Orthographic camera, and can't set `DOLLY`. 20574 * @category Properties 20575 */ 20576 mouseButtons; 20577 /** 20578 * User's touch input config. 20579 * 20580 * | fingers to assign | behavior | 20581 * | --------------------- | -------- | 20582 * | `touches.one` | `CameraControls.ACTION.TOUCH_ROTATE`* \| `CameraControls.ACTION.TOUCH_TRUCK` \| `CameraControls.ACTION.TOUCH_OFFSET` \| `CameraControls.ACTION.DOLLY` | `CameraControls.ACTION.ZOOM` | `CameraControls.ACTION.NONE` | 20583 * | `touches.two` | `ACTION.TOUCH_DOLLY_TRUCK` \| `ACTION.TOUCH_DOLLY_OFFSET` \| `ACTION.TOUCH_DOLLY_ROTATE` \| `ACTION.TOUCH_ZOOM_TRUCK` \| `ACTION.TOUCH_ZOOM_OFFSET` \| `ACTION.TOUCH_ZOOM_ROTATE` \| `ACTION.TOUCH_DOLLY` \| `ACTION.TOUCH_ZOOM` \| `CameraControls.ACTION.TOUCH_ROTATE` \| `CameraControls.ACTION.TOUCH_TRUCK` \| `CameraControls.ACTION.TOUCH_OFFSET` \| `CameraControls.ACTION.NONE` | 20584 * | `touches.three` | `ACTION.TOUCH_DOLLY_TRUCK` \| `ACTION.TOUCH_DOLLY_OFFSET` \| `ACTION.TOUCH_DOLLY_ROTATE` \| `ACTION.TOUCH_ZOOM_TRUCK` \| `ACTION.TOUCH_ZOOM_OFFSET` \| `ACTION.TOUCH_ZOOM_ROTATE` \| `CameraControls.ACTION.TOUCH_ROTATE` \| `CameraControls.ACTION.TOUCH_TRUCK` \| `CameraControls.ACTION.TOUCH_OFFSET` \| `CameraControls.ACTION.NONE` | 20585 * 20586 * - \* is the default. 20587 * - The default of `touches.two` and `touches.three` is: 20588 * - `TOUCH_DOLLY_TRUCK` for Perspective camera. 20589 * - `TOUCH_ZOOM_TRUCK` for Orthographic camera, and can't set `TOUCH_DOLLY_TRUCK` and `TOUCH_DOLLY`. 20590 * @category Properties 20591 */ 20592 touches; 20593 /** 20594 * Force cancel user dragging. 20595 * @category Methods 20596 */ 20597 // cancel will be overwritten in the constructor. 20598 cancel = () => { 20599 }; 20600 /** 20601 * Still an experimental feature. 20602 * This could change at any time. 20603 * @category Methods 20604 */ 20605 lockPointer; 20606 /** 20607 * Still an experimental feature. 20608 * This could change at any time. 20609 * @category Methods 20610 */ 20611 unlockPointer; 20612 _enabled = true; 20613 _camera; 20614 _yAxisUpSpace; 20615 _yAxisUpSpaceInverse; 20616 _state = ACTION.NONE; 20617 _domElement; 20618 _viewport = null; 20619 // the location of focus, where the object orbits around 20620 _target; 20621 _targetEnd; 20622 _focalOffset; 20623 _focalOffsetEnd; 20624 // rotation and dolly distance 20625 _spherical; 20626 _sphericalEnd; 20627 _lastDistance; 20628 _zoom; 20629 _zoomEnd; 20630 _lastZoom; 20631 // reset 20632 _cameraUp0; 20633 _target0; 20634 _position0; 20635 _zoom0; 20636 _focalOffset0; 20637 _dollyControlCoord; 20638 _changedDolly = 0; 20639 _changedZoom = 0; 20640 // collisionTest uses nearPlane. ( PerspectiveCamera only ) 20641 _nearPlaneCorners; 20642 _hasRested = true; 20643 _boundary; 20644 _boundaryEnclosesCamera = false; 20645 _needsUpdate = true; 20646 _updatedLastTime = false; 20647 _elementRect = new DOMRect(); 20648 _isDragging = false; 20649 _dragNeedsUpdate = true; 20650 _activePointers = []; 20651 _lockedPointer = null; 20652 _interactiveArea = new DOMRect(0, 0, 1, 1); 20653 // Use draggingSmoothTime over smoothTime while true. 20654 // set automatically true on user-dragging start. 20655 // set automatically false on programmable methods call. 20656 _isUserControllingRotate = false; 20657 _isUserControllingDolly = false; 20658 _isUserControllingTruck = false; 20659 _isUserControllingOffset = false; 20660 _isUserControllingZoom = false; 20661 _lastDollyDirection = DOLLY_DIRECTION.NONE; 20662 // velocities for smoothDamp 20663 _thetaVelocity = { 20664 value: 0 20665 }; 20666 _phiVelocity = { 20667 value: 0 20668 }; 20669 _radiusVelocity = { 20670 value: 0 20671 }; 20672 _targetVelocity = new THREE.Vector3(); 20673 _focalOffsetVelocity = new THREE.Vector3(); 20674 _zoomVelocity = { 20675 value: 0 20676 }; 20677 /** 20678 * @deprecated Use `cameraControls.mouseButtons.left = CameraControls.ACTION.SCREEN_PAN` instead. 20679 */ 20680 set verticalDragToForward(_) { 20681 console.warn("camera-controls: `verticalDragToForward` was removed. Use `mouseButtons.left = CameraControls.ACTION.SCREEN_PAN` instead."); 20682 } 20683 /** 20684 * Creates a `CameraControls` instance. 20685 * 20686 * Note: 20687 * You **must install** three.js before using camera-controls. see [#install](#install) 20688 * Not doing so will lead to runtime errors (`undefined` references to THREE). 20689 * 20690 * e.g. 20691 * ``` 20692 * CameraControls.install( { THREE } ); 20693 * const cameraControls = new CameraControls( camera, domElement ); 20694 * ``` 20695 * 20696 * @param camera A `THREE.PerspectiveCamera` or `THREE.OrthographicCamera` to be controlled. 20697 * @param domElement A `HTMLElement` for the draggable area, usually `renderer.domElement`. 20698 * @category Constructor 20699 */ 20700 constructor(camera, domElement) { 20701 super(); 20702 if (typeof THREE === "undefined") { 20703 console.error("camera-controls: `THREE` is undefined. You must first run `CameraControls.install( { THREE: THREE } )`. Check the docs for further information."); 20704 } 20705 this._camera = camera; 20706 this._yAxisUpSpace = new THREE.Quaternion().setFromUnitVectors(this._camera.up, _AXIS_Y); 20707 this._yAxisUpSpaceInverse = this._yAxisUpSpace.clone().invert(); 20708 this._state = ACTION.NONE; 20709 this._target = new THREE.Vector3(); 20710 this._targetEnd = this._target.clone(); 20711 this._focalOffset = new THREE.Vector3(); 20712 this._focalOffsetEnd = this._focalOffset.clone(); 20713 this._spherical = new THREE.Spherical().setFromVector3(_v3A.copy(this._camera.position).applyQuaternion(this._yAxisUpSpace)); 20714 this._sphericalEnd = this._spherical.clone(); 20715 this._lastDistance = this._spherical.radius; 20716 this._zoom = this._camera.zoom; 20717 this._zoomEnd = this._zoom; 20718 this._lastZoom = this._zoom; 20719 this._nearPlaneCorners = [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()]; 20720 this._updateNearPlaneCorners(); 20721 this._boundary = new THREE.Box3(new THREE.Vector3(-Infinity, -Infinity, -Infinity), new THREE.Vector3(Infinity, Infinity, Infinity)); 20722 this._cameraUp0 = this._camera.up.clone(); 20723 this._target0 = this._target.clone(); 20724 this._position0 = this._camera.position.clone(); 20725 this._zoom0 = this._zoom; 20726 this._focalOffset0 = this._focalOffset.clone(); 20727 this._dollyControlCoord = new THREE.Vector2(); 20728 this.mouseButtons = { 20729 left: ACTION.ROTATE, 20730 middle: ACTION.DOLLY, 20731 right: ACTION.TRUCK, 20732 wheel: isPerspectiveCamera(this._camera) ? ACTION.DOLLY : isOrthographicCamera(this._camera) ? ACTION.ZOOM : ACTION.NONE 20733 }; 20734 this.touches = { 20735 one: ACTION.TOUCH_ROTATE, 20736 two: isPerspectiveCamera(this._camera) ? ACTION.TOUCH_DOLLY_TRUCK : isOrthographicCamera(this._camera) ? ACTION.TOUCH_ZOOM_TRUCK : ACTION.NONE, 20737 three: ACTION.TOUCH_TRUCK 20738 }; 20739 const dragStartPosition = new THREE.Vector2(); 20740 const lastDragPosition = new THREE.Vector2(); 20741 const dollyStart = new THREE.Vector2(); 20742 const onPointerDown = (event) => { 20743 if (!this._enabled || !this._domElement) return; 20744 if (this._interactiveArea.left !== 0 || this._interactiveArea.top !== 0 || this._interactiveArea.width !== 1 || this._interactiveArea.height !== 1) { 20745 const elRect = this._domElement.getBoundingClientRect(); 20746 const left = event.clientX / elRect.width; 20747 const top = event.clientY / elRect.height; 20748 if (left < this._interactiveArea.left || left > this._interactiveArea.right || top < this._interactiveArea.top || top > this._interactiveArea.bottom) return; 20749 } 20750 const mouseButton = event.pointerType !== "mouse" ? null : (event.buttons & MOUSE_BUTTON.LEFT) === MOUSE_BUTTON.LEFT ? MOUSE_BUTTON.LEFT : (event.buttons & MOUSE_BUTTON.MIDDLE) === MOUSE_BUTTON.MIDDLE ? MOUSE_BUTTON.MIDDLE : (event.buttons & MOUSE_BUTTON.RIGHT) === MOUSE_BUTTON.RIGHT ? MOUSE_BUTTON.RIGHT : null; 20751 if (mouseButton !== null) { 20752 const zombiePointer = this._findPointerByMouseButton(mouseButton); 20753 zombiePointer && this._disposePointer(zombiePointer); 20754 } 20755 if ((event.buttons & MOUSE_BUTTON.LEFT) === MOUSE_BUTTON.LEFT && this._lockedPointer) return; 20756 const pointer = { 20757 pointerId: event.pointerId, 20758 clientX: event.clientX, 20759 clientY: event.clientY, 20760 deltaX: 0, 20761 deltaY: 0, 20762 mouseButton 20763 }; 20764 this._activePointers.push(pointer); 20765 this._domElement.ownerDocument.removeEventListener("pointermove", onPointerMove, { 20766 passive: false 20767 }); 20768 this._domElement.ownerDocument.removeEventListener("pointerup", onPointerUp); 20769 this._domElement.ownerDocument.addEventListener("pointermove", onPointerMove, { 20770 passive: false 20771 }); 20772 this._domElement.ownerDocument.addEventListener("pointerup", onPointerUp); 20773 this._isDragging = true; 20774 startDragging(event); 20775 }; 20776 const onPointerMove = (event) => { 20777 if (event.cancelable) event.preventDefault(); 20778 const pointerId = event.pointerId; 20779 const pointer = this._lockedPointer || this._findPointerById(pointerId); 20780 if (!pointer) return; 20781 pointer.clientX = event.clientX; 20782 pointer.clientY = event.clientY; 20783 pointer.deltaX = event.movementX; 20784 pointer.deltaY = event.movementY; 20785 this._state = 0; 20786 if (event.pointerType === "touch") { 20787 switch (this._activePointers.length) { 20788 case 1: 20789 this._state = this.touches.one; 20790 break; 20791 case 2: 20792 this._state = this.touches.two; 20793 break; 20794 case 3: 20795 this._state = this.touches.three; 20796 break; 20797 } 20798 } else { 20799 if (!this._isDragging && this._lockedPointer || this._isDragging && (event.buttons & MOUSE_BUTTON.LEFT) === MOUSE_BUTTON.LEFT) { 20800 this._state = this._state | this.mouseButtons.left; 20801 } 20802 if (this._isDragging && (event.buttons & MOUSE_BUTTON.MIDDLE) === MOUSE_BUTTON.MIDDLE) { 20803 this._state = this._state | this.mouseButtons.middle; 20804 } 20805 if (this._isDragging && (event.buttons & MOUSE_BUTTON.RIGHT) === MOUSE_BUTTON.RIGHT) { 20806 this._state = this._state | this.mouseButtons.right; 20807 } 20808 } 20809 dragging(); 20810 }; 20811 const onPointerUp = (event) => { 20812 const pointer = this._findPointerById(event.pointerId); 20813 if (pointer && pointer === this._lockedPointer) return; 20814 pointer && this._disposePointer(pointer); 20815 if (event.pointerType === "touch") { 20816 switch (this._activePointers.length) { 20817 case 0: 20818 this._state = ACTION.NONE; 20819 break; 20820 case 1: 20821 this._state = this.touches.one; 20822 break; 20823 case 2: 20824 this._state = this.touches.two; 20825 break; 20826 case 3: 20827 this._state = this.touches.three; 20828 break; 20829 } 20830 } else { 20831 this._state = ACTION.NONE; 20832 } 20833 endDragging(); 20834 }; 20835 let lastScrollTimeStamp = -1; 20836 const onMouseWheel = (event) => { 20837 if (!this._domElement) return; 20838 if (!this._enabled || this.mouseButtons.wheel === ACTION.NONE) return; 20839 if (this._interactiveArea.left !== 0 || this._interactiveArea.top !== 0 || this._interactiveArea.width !== 1 || this._interactiveArea.height !== 1) { 20840 const elRect = this._domElement.getBoundingClientRect(); 20841 const left = event.clientX / elRect.width; 20842 const top = event.clientY / elRect.height; 20843 if (left < this._interactiveArea.left || left > this._interactiveArea.right || top < this._interactiveArea.top || top > this._interactiveArea.bottom) return; 20844 } 20845 event.preventDefault(); 20846 if (this.dollyToCursor || this.mouseButtons.wheel === ACTION.ROTATE || this.mouseButtons.wheel === ACTION.TRUCK) { 20847 const now = performance.now(); 20848 if (lastScrollTimeStamp - now < 1e3) this._getClientRect(this._elementRect); 20849 lastScrollTimeStamp = now; 20850 } 20851 const deltaYFactor = isMac ? -1 : -3; 20852 const delta = event.deltaMode === 1 && !event.ctrlKey ? event.deltaY / deltaYFactor : event.deltaY / (deltaYFactor * 10); 20853 const x = this.dollyToCursor ? (event.clientX - this._elementRect.x) / this._elementRect.width * 2 - 1 : 0; 20854 const y = this.dollyToCursor ? (event.clientY - this._elementRect.y) / this._elementRect.height * -2 + 1 : 0; 20855 const controlMode = event.ctrlKey ? ACTION.ZOOM : this.mouseButtons.wheel; 20856 switch (controlMode) { 20857 case ACTION.ROTATE: { 20858 this._rotateInternal(event.deltaX, event.deltaY); 20859 this._isUserControllingRotate = true; 20860 break; 20861 } 20862 case ACTION.TRUCK: { 20863 this._truckInternal(event.deltaX, event.deltaY, false, false); 20864 this._isUserControllingTruck = true; 20865 break; 20866 } 20867 case ACTION.SCREEN_PAN: { 20868 this._truckInternal(event.deltaX, event.deltaY, false, true); 20869 this._isUserControllingTruck = true; 20870 break; 20871 } 20872 case ACTION.OFFSET: { 20873 this._truckInternal(event.deltaX, event.deltaY, true, false); 20874 this._isUserControllingOffset = true; 20875 break; 20876 } 20877 case ACTION.DOLLY: { 20878 this._dollyInternal(-delta, x, y); 20879 this._isUserControllingDolly = true; 20880 break; 20881 } 20882 case ACTION.ZOOM: { 20883 this._zoomInternal(-delta, x, y); 20884 this._isUserControllingZoom = true; 20885 break; 20886 } 20887 } 20888 this.dispatchEvent({ 20889 type: "control" 20890 }); 20891 }; 20892 const onContextMenu = (event) => { 20893 if (!this._domElement || !this._enabled) return; 20894 if (this.mouseButtons.right === _CameraControls.ACTION.NONE) { 20895 const pointerId = event instanceof PointerEvent ? event.pointerId : 0; 20896 const pointer = this._findPointerById(pointerId); 20897 pointer && this._disposePointer(pointer); 20898 this._domElement.ownerDocument.removeEventListener("pointermove", onPointerMove, { 20899 passive: false 20900 }); 20901 this._domElement.ownerDocument.removeEventListener("pointerup", onPointerUp); 20902 return; 20903 } 20904 event.preventDefault(); 20905 }; 20906 const startDragging = (event) => { 20907 if (!this._enabled) return; 20908 extractClientCoordFromEvent(this._activePointers, _v2); 20909 this._getClientRect(this._elementRect); 20910 dragStartPosition.copy(_v2); 20911 lastDragPosition.copy(_v2); 20912 const isMultiTouch = this._activePointers.length >= 2; 20913 if (isMultiTouch) { 20914 const dx = _v2.x - this._activePointers[1].clientX; 20915 const dy = _v2.y - this._activePointers[1].clientY; 20916 const distance = Math.sqrt(dx * dx + dy * dy); 20917 dollyStart.set(0, distance); 20918 const x = (this._activePointers[0].clientX + this._activePointers[1].clientX) * 0.5; 20919 const y = (this._activePointers[0].clientY + this._activePointers[1].clientY) * 0.5; 20920 lastDragPosition.set(x, y); 20921 } 20922 this._state = 0; 20923 if (!event) { 20924 if (this._lockedPointer) this._state = this._state | this.mouseButtons.left; 20925 } else if ("pointerType" in event && event.pointerType === "touch") { 20926 switch (this._activePointers.length) { 20927 case 1: 20928 this._state = this.touches.one; 20929 break; 20930 case 2: 20931 this._state = this.touches.two; 20932 break; 20933 case 3: 20934 this._state = this.touches.three; 20935 break; 20936 } 20937 } else { 20938 if (!this._lockedPointer && (event.buttons & MOUSE_BUTTON.LEFT) === MOUSE_BUTTON.LEFT) { 20939 this._state = this._state | this.mouseButtons.left; 20940 } 20941 if ((event.buttons & MOUSE_BUTTON.MIDDLE) === MOUSE_BUTTON.MIDDLE) { 20942 this._state = this._state | this.mouseButtons.middle; 20943 } 20944 if ((event.buttons & MOUSE_BUTTON.RIGHT) === MOUSE_BUTTON.RIGHT) { 20945 this._state = this._state | this.mouseButtons.right; 20946 } 20947 } 20948 if ((this._state & ACTION.ROTATE) === ACTION.ROTATE || (this._state & ACTION.TOUCH_ROTATE) === ACTION.TOUCH_ROTATE || (this._state & ACTION.TOUCH_DOLLY_ROTATE) === ACTION.TOUCH_DOLLY_ROTATE || (this._state & ACTION.TOUCH_ZOOM_ROTATE) === ACTION.TOUCH_ZOOM_ROTATE) { 20949 this._sphericalEnd.theta = this._spherical.theta; 20950 this._sphericalEnd.phi = this._spherical.phi; 20951 this._thetaVelocity.value = 0; 20952 this._phiVelocity.value = 0; 20953 } 20954 if ((this._state & ACTION.TRUCK) === ACTION.TRUCK || (this._state & ACTION.SCREEN_PAN) === ACTION.SCREEN_PAN || (this._state & ACTION.TOUCH_TRUCK) === ACTION.TOUCH_TRUCK || (this._state & ACTION.TOUCH_SCREEN_PAN) === ACTION.TOUCH_SCREEN_PAN || (this._state & ACTION.TOUCH_DOLLY_TRUCK) === ACTION.TOUCH_DOLLY_TRUCK || (this._state & ACTION.TOUCH_DOLLY_SCREEN_PAN) === ACTION.TOUCH_DOLLY_SCREEN_PAN || (this._state & ACTION.TOUCH_ZOOM_TRUCK) === ACTION.TOUCH_ZOOM_TRUCK || (this._state & ACTION.TOUCH_ZOOM_SCREEN_PAN) === ACTION.TOUCH_DOLLY_SCREEN_PAN) { 20955 this._targetEnd.copy(this._target); 20956 this._targetVelocity.set(0, 0, 0); 20957 } 20958 if ((this._state & ACTION.DOLLY) === ACTION.DOLLY || (this._state & ACTION.TOUCH_DOLLY) === ACTION.TOUCH_DOLLY || (this._state & ACTION.TOUCH_DOLLY_TRUCK) === ACTION.TOUCH_DOLLY_TRUCK || (this._state & ACTION.TOUCH_DOLLY_SCREEN_PAN) === ACTION.TOUCH_DOLLY_SCREEN_PAN || (this._state & ACTION.TOUCH_DOLLY_OFFSET) === ACTION.TOUCH_DOLLY_OFFSET || (this._state & ACTION.TOUCH_DOLLY_ROTATE) === ACTION.TOUCH_DOLLY_ROTATE) { 20959 this._sphericalEnd.radius = this._spherical.radius; 20960 this._radiusVelocity.value = 0; 20961 } 20962 if ((this._state & ACTION.ZOOM) === ACTION.ZOOM || (this._state & ACTION.TOUCH_ZOOM) === ACTION.TOUCH_ZOOM || (this._state & ACTION.TOUCH_ZOOM_TRUCK) === ACTION.TOUCH_ZOOM_TRUCK || (this._state & ACTION.TOUCH_ZOOM_SCREEN_PAN) === ACTION.TOUCH_ZOOM_SCREEN_PAN || (this._state & ACTION.TOUCH_ZOOM_OFFSET) === ACTION.TOUCH_ZOOM_OFFSET || (this._state & ACTION.TOUCH_ZOOM_ROTATE) === ACTION.TOUCH_ZOOM_ROTATE) { 20963 this._zoomEnd = this._zoom; 20964 this._zoomVelocity.value = 0; 20965 } 20966 if ((this._state & ACTION.OFFSET) === ACTION.OFFSET || (this._state & ACTION.TOUCH_OFFSET) === ACTION.TOUCH_OFFSET || (this._state & ACTION.TOUCH_DOLLY_OFFSET) === ACTION.TOUCH_DOLLY_OFFSET || (this._state & ACTION.TOUCH_ZOOM_OFFSET) === ACTION.TOUCH_ZOOM_OFFSET) { 20967 this._focalOffsetEnd.copy(this._focalOffset); 20968 this._focalOffsetVelocity.set(0, 0, 0); 20969 } 20970 this.dispatchEvent({ 20971 type: "controlstart" 20972 }); 20973 }; 20974 const dragging = () => { 20975 if (!this._enabled || !this._dragNeedsUpdate) return; 20976 this._dragNeedsUpdate = false; 20977 extractClientCoordFromEvent(this._activePointers, _v2); 20978 const isPointerLockActive = this._domElement && this._domElement.ownerDocument.pointerLockElement === this._domElement; 20979 const lockedPointer = isPointerLockActive ? this._lockedPointer || this._activePointers[0] : null; 20980 const deltaX = lockedPointer ? -lockedPointer.deltaX : lastDragPosition.x - _v2.x; 20981 const deltaY = lockedPointer ? -lockedPointer.deltaY : lastDragPosition.y - _v2.y; 20982 lastDragPosition.copy(_v2); 20983 if ((this._state & ACTION.ROTATE) === ACTION.ROTATE || (this._state & ACTION.TOUCH_ROTATE) === ACTION.TOUCH_ROTATE || (this._state & ACTION.TOUCH_DOLLY_ROTATE) === ACTION.TOUCH_DOLLY_ROTATE || (this._state & ACTION.TOUCH_ZOOM_ROTATE) === ACTION.TOUCH_ZOOM_ROTATE) { 20984 this._rotateInternal(deltaX, deltaY); 20985 this._isUserControllingRotate = true; 20986 } 20987 if ((this._state & ACTION.DOLLY) === ACTION.DOLLY || (this._state & ACTION.ZOOM) === ACTION.ZOOM) { 20988 const dollyX = this.dollyToCursor ? (dragStartPosition.x - this._elementRect.x) / this._elementRect.width * 2 - 1 : 0; 20989 const dollyY = this.dollyToCursor ? (dragStartPosition.y - this._elementRect.y) / this._elementRect.height * -2 + 1 : 0; 20990 const dollyDirection = this.dollyDragInverted ? -1 : 1; 20991 if ((this._state & ACTION.DOLLY) === ACTION.DOLLY) { 20992 this._dollyInternal(dollyDirection * deltaY * TOUCH_DOLLY_FACTOR, dollyX, dollyY); 20993 this._isUserControllingDolly = true; 20994 } else { 20995 this._zoomInternal(dollyDirection * deltaY * TOUCH_DOLLY_FACTOR, dollyX, dollyY); 20996 this._isUserControllingZoom = true; 20997 } 20998 } 20999 if ((this._state & ACTION.TOUCH_DOLLY) === ACTION.TOUCH_DOLLY || (this._state & ACTION.TOUCH_ZOOM) === ACTION.TOUCH_ZOOM || (this._state & ACTION.TOUCH_DOLLY_TRUCK) === ACTION.TOUCH_DOLLY_TRUCK || (this._state & ACTION.TOUCH_ZOOM_TRUCK) === ACTION.TOUCH_ZOOM_TRUCK || (this._state & ACTION.TOUCH_DOLLY_SCREEN_PAN) === ACTION.TOUCH_DOLLY_SCREEN_PAN || (this._state & ACTION.TOUCH_ZOOM_SCREEN_PAN) === ACTION.TOUCH_ZOOM_SCREEN_PAN || (this._state & ACTION.TOUCH_DOLLY_OFFSET) === ACTION.TOUCH_DOLLY_OFFSET || (this._state & ACTION.TOUCH_ZOOM_OFFSET) === ACTION.TOUCH_ZOOM_OFFSET || (this._state & ACTION.TOUCH_DOLLY_ROTATE) === ACTION.TOUCH_DOLLY_ROTATE || (this._state & ACTION.TOUCH_ZOOM_ROTATE) === ACTION.TOUCH_ZOOM_ROTATE) { 21000 const dx = _v2.x - this._activePointers[1].clientX; 21001 const dy = _v2.y - this._activePointers[1].clientY; 21002 const distance = Math.sqrt(dx * dx + dy * dy); 21003 const dollyDelta = dollyStart.y - distance; 21004 dollyStart.set(0, distance); 21005 const dollyX = this.dollyToCursor ? (lastDragPosition.x - this._elementRect.x) / this._elementRect.width * 2 - 1 : 0; 21006 const dollyY = this.dollyToCursor ? (lastDragPosition.y - this._elementRect.y) / this._elementRect.height * -2 + 1 : 0; 21007 if ((this._state & ACTION.TOUCH_DOLLY) === ACTION.TOUCH_DOLLY || (this._state & ACTION.TOUCH_DOLLY_ROTATE) === ACTION.TOUCH_DOLLY_ROTATE || (this._state & ACTION.TOUCH_DOLLY_TRUCK) === ACTION.TOUCH_DOLLY_TRUCK || (this._state & ACTION.TOUCH_DOLLY_SCREEN_PAN) === ACTION.TOUCH_DOLLY_SCREEN_PAN || (this._state & ACTION.TOUCH_DOLLY_OFFSET) === ACTION.TOUCH_DOLLY_OFFSET) { 21008 this._dollyInternal(dollyDelta * TOUCH_DOLLY_FACTOR, dollyX, dollyY); 21009 this._isUserControllingDolly = true; 21010 } else { 21011 this._zoomInternal(dollyDelta * TOUCH_DOLLY_FACTOR, dollyX, dollyY); 21012 this._isUserControllingZoom = true; 21013 } 21014 } 21015 if ((this._state & ACTION.TRUCK) === ACTION.TRUCK || (this._state & ACTION.TOUCH_TRUCK) === ACTION.TOUCH_TRUCK || (this._state & ACTION.TOUCH_DOLLY_TRUCK) === ACTION.TOUCH_DOLLY_TRUCK || (this._state & ACTION.TOUCH_ZOOM_TRUCK) === ACTION.TOUCH_ZOOM_TRUCK) { 21016 this._truckInternal(deltaX, deltaY, false, false); 21017 this._isUserControllingTruck = true; 21018 } 21019 if ((this._state & ACTION.SCREEN_PAN) === ACTION.SCREEN_PAN || (this._state & ACTION.TOUCH_SCREEN_PAN) === ACTION.TOUCH_SCREEN_PAN || (this._state & ACTION.TOUCH_DOLLY_SCREEN_PAN) === ACTION.TOUCH_DOLLY_SCREEN_PAN || (this._state & ACTION.TOUCH_ZOOM_SCREEN_PAN) === ACTION.TOUCH_ZOOM_SCREEN_PAN) { 21020 this._truckInternal(deltaX, deltaY, false, true); 21021 this._isUserControllingTruck = true; 21022 } 21023 if ((this._state & ACTION.OFFSET) === ACTION.OFFSET || (this._state & ACTION.TOUCH_OFFSET) === ACTION.TOUCH_OFFSET || (this._state & ACTION.TOUCH_DOLLY_OFFSET) === ACTION.TOUCH_DOLLY_OFFSET || (this._state & ACTION.TOUCH_ZOOM_OFFSET) === ACTION.TOUCH_ZOOM_OFFSET) { 21024 this._truckInternal(deltaX, deltaY, true, false); 21025 this._isUserControllingOffset = true; 21026 } 21027 this.dispatchEvent({ 21028 type: "control" 21029 }); 21030 }; 21031 const endDragging = () => { 21032 extractClientCoordFromEvent(this._activePointers, _v2); 21033 lastDragPosition.copy(_v2); 21034 this._dragNeedsUpdate = false; 21035 if (this._activePointers.length === 0 || this._activePointers.length === 1 && this._activePointers[0] === this._lockedPointer) { 21036 this._isDragging = false; 21037 } 21038 if (this._activePointers.length === 0 && this._domElement) { 21039 this._domElement.ownerDocument.removeEventListener("pointermove", onPointerMove, { 21040 passive: false 21041 }); 21042 this._domElement.ownerDocument.removeEventListener("pointerup", onPointerUp); 21043 this.dispatchEvent({ 21044 type: "controlend" 21045 }); 21046 } 21047 }; 21048 this.lockPointer = () => { 21049 if (!this._enabled || !this._domElement) return; 21050 this.cancel(); 21051 this._lockedPointer = { 21052 pointerId: -1, 21053 clientX: 0, 21054 clientY: 0, 21055 deltaX: 0, 21056 deltaY: 0, 21057 mouseButton: null 21058 }; 21059 this._activePointers.push(this._lockedPointer); 21060 this._domElement.ownerDocument.removeEventListener("pointermove", onPointerMove, { 21061 passive: false 21062 }); 21063 this._domElement.ownerDocument.removeEventListener("pointerup", onPointerUp); 21064 this._domElement.requestPointerLock(); 21065 this._domElement.ownerDocument.addEventListener("pointerlockchange", onPointerLockChange); 21066 this._domElement.ownerDocument.addEventListener("pointerlockerror", onPointerLockError); 21067 this._domElement.ownerDocument.addEventListener("pointermove", onPointerMove, { 21068 passive: false 21069 }); 21070 this._domElement.ownerDocument.addEventListener("pointerup", onPointerUp); 21071 startDragging(); 21072 }; 21073 this.unlockPointer = () => { 21074 var _a3, _b2, _c; 21075 if (this._lockedPointer !== null) { 21076 this._disposePointer(this._lockedPointer); 21077 this._lockedPointer = null; 21078 } 21079 (_a3 = this._domElement) == null ? void 0 : _a3.ownerDocument.exitPointerLock(); 21080 (_b2 = this._domElement) == null ? void 0 : _b2.ownerDocument.removeEventListener("pointerlockchange", onPointerLockChange); 21081 (_c = this._domElement) == null ? void 0 : _c.ownerDocument.removeEventListener("pointerlockerror", onPointerLockError); 21082 this.cancel(); 21083 }; 21084 const onPointerLockChange = () => { 21085 const isPointerLockActive = this._domElement && this._domElement.ownerDocument.pointerLockElement === this._domElement; 21086 if (!isPointerLockActive) this.unlockPointer(); 21087 }; 21088 const onPointerLockError = () => { 21089 this.unlockPointer(); 21090 }; 21091 this._addAllEventListeners = (domElement2) => { 21092 this._domElement = domElement2; 21093 this._domElement.style.touchAction = "none"; 21094 this._domElement.style.userSelect = "none"; 21095 this._domElement.style.webkitUserSelect = "none"; 21096 this._domElement.addEventListener("pointerdown", onPointerDown); 21097 this._domElement.addEventListener("pointercancel", onPointerUp); 21098 this._domElement.addEventListener("wheel", onMouseWheel, { 21099 passive: false 21100 }); 21101 this._domElement.addEventListener("contextmenu", onContextMenu); 21102 }; 21103 this._removeAllEventListeners = () => { 21104 if (!this._domElement) return; 21105 this._domElement.style.touchAction = ""; 21106 this._domElement.style.userSelect = ""; 21107 this._domElement.style.webkitUserSelect = ""; 21108 this._domElement.removeEventListener("pointerdown", onPointerDown); 21109 this._domElement.removeEventListener("pointercancel", onPointerUp); 21110 this._domElement.removeEventListener("wheel", onMouseWheel, { 21111 passive: false 21112 }); 21113 this._domElement.removeEventListener("contextmenu", onContextMenu); 21114 this._domElement.ownerDocument.removeEventListener("pointermove", onPointerMove, { 21115 passive: false 21116 }); 21117 this._domElement.ownerDocument.removeEventListener("pointerup", onPointerUp); 21118 this._domElement.ownerDocument.removeEventListener("pointerlockchange", onPointerLockChange); 21119 this._domElement.ownerDocument.removeEventListener("pointerlockerror", onPointerLockError); 21120 }; 21121 this.cancel = () => { 21122 if (this._state === ACTION.NONE) return; 21123 this._state = ACTION.NONE; 21124 this._activePointers.length = 0; 21125 endDragging(); 21126 }; 21127 if (domElement) this.connect(domElement); 21128 this.update(0); 21129 } 21130 /** 21131 * The camera to be controlled 21132 * @category Properties 21133 */ 21134 get camera() { 21135 return this._camera; 21136 } 21137 set camera(camera) { 21138 this._camera = camera; 21139 this.updateCameraUp(); 21140 this._camera.updateProjectionMatrix(); 21141 this._updateNearPlaneCorners(); 21142 this._needsUpdate = true; 21143 } 21144 /** 21145 * Whether or not the controls are enabled. 21146 * `false` to disable user dragging/touch-move, but all methods works. 21147 * @category Properties 21148 */ 21149 get enabled() { 21150 return this._enabled; 21151 } 21152 set enabled(enabled) { 21153 this._enabled = enabled; 21154 if (!this._domElement) return; 21155 if (enabled) { 21156 this._domElement.style.touchAction = "none"; 21157 this._domElement.style.userSelect = "none"; 21158 this._domElement.style.webkitUserSelect = "none"; 21159 } else { 21160 this.cancel(); 21161 this._domElement.style.touchAction = ""; 21162 this._domElement.style.userSelect = ""; 21163 this._domElement.style.webkitUserSelect = ""; 21164 } 21165 } 21166 /** 21167 * Returns `true` if the controls are active updating. 21168 * readonly value. 21169 * @category Properties 21170 */ 21171 get active() { 21172 return !this._hasRested; 21173 } 21174 /** 21175 * Getter for the current `ACTION`. 21176 * readonly value. 21177 * @category Properties 21178 */ 21179 get currentAction() { 21180 return this._state; 21181 } 21182 /** 21183 * get/set Current distance. 21184 * @category Properties 21185 */ 21186 get distance() { 21187 return this._spherical.radius; 21188 } 21189 set distance(distance) { 21190 if (this._spherical.radius === distance && this._sphericalEnd.radius === distance) return; 21191 this._spherical.radius = distance; 21192 this._sphericalEnd.radius = distance; 21193 this._needsUpdate = true; 21194 } 21195 // horizontal angle 21196 /** 21197 * get/set the azimuth angle (horizontal) in radians. 21198 * Every 360 degrees turn is added to `.azimuthAngle` value, which is accumulative. 21199 * @category Properties 21200 */ 21201 get azimuthAngle() { 21202 return this._spherical.theta; 21203 } 21204 set azimuthAngle(azimuthAngle) { 21205 if (this._spherical.theta === azimuthAngle && this._sphericalEnd.theta === azimuthAngle) return; 21206 this._spherical.theta = azimuthAngle; 21207 this._sphericalEnd.theta = azimuthAngle; 21208 this._needsUpdate = true; 21209 } 21210 // vertical angle 21211 /** 21212 * get/set the polar angle (vertical) in radians. 21213 * @category Properties 21214 */ 21215 get polarAngle() { 21216 return this._spherical.phi; 21217 } 21218 set polarAngle(polarAngle) { 21219 if (this._spherical.phi === polarAngle && this._sphericalEnd.phi === polarAngle) return; 21220 this._spherical.phi = polarAngle; 21221 this._sphericalEnd.phi = polarAngle; 21222 this._needsUpdate = true; 21223 } 21224 /** 21225 * Whether camera position should be enclosed in the boundary or not. 21226 * @category Properties 21227 */ 21228 get boundaryEnclosesCamera() { 21229 return this._boundaryEnclosesCamera; 21230 } 21231 set boundaryEnclosesCamera(boundaryEnclosesCamera) { 21232 this._boundaryEnclosesCamera = boundaryEnclosesCamera; 21233 this._needsUpdate = true; 21234 } 21235 /** 21236 * Set drag-start, touches and wheel enable area in the domElement. 21237 * each values are between `0` and `1` inclusive, where `0` is left/top and `1` is right/bottom of the screen. 21238 * e.g. `{ x: 0, y: 0, width: 1, height: 1 }` for entire area. 21239 * @category Properties 21240 */ 21241 set interactiveArea(interactiveArea) { 21242 this._interactiveArea.width = clamp(interactiveArea.width, 0, 1); 21243 this._interactiveArea.height = clamp(interactiveArea.height, 0, 1); 21244 this._interactiveArea.x = clamp(interactiveArea.x, 0, 1 - this._interactiveArea.width); 21245 this._interactiveArea.y = clamp(interactiveArea.y, 0, 1 - this._interactiveArea.height); 21246 } 21247 /** 21248 * Adds the specified event listener. 21249 * Applicable event types (which is `K`) are: 21250 * | Event name | Timing | 21251 * | ------------------- | ------ | 21252 * | `'controlstart'` | When the user starts to control the camera via mouse / touches. ¹ | 21253 * | `'control'` | When the user controls the camera (dragging). | 21254 * | `'controlend'` | When the user ends to control the camera. ¹ | 21255 * | `'transitionstart'` | When any kind of transition starts, either user control or using a method with `enableTransition = true` | 21256 * | `'update'` | When the camera position is updated. | 21257 * | `'wake'` | When the camera starts moving. | 21258 * | `'rest'` | When the camera movement is below `.restThreshold` ². | 21259 * | `'sleep'` | When the camera end moving. | 21260 * 21261 * 1. `mouseButtons.wheel` (Mouse wheel control) does not emit `'controlstart'` and `'controlend'`. `mouseButtons.wheel` uses scroll-event internally, and scroll-event happens intermittently. That means "start" and "end" cannot be detected. 21262 * 2. Due to damping, `sleep` will usually fire a few seconds after the camera _appears_ to have stopped moving. If you want to do something (e.g. enable UI, perform another transition) at the point when the camera has stopped, you probably want the `rest` event. This can be fine tuned using the `.restThreshold` parameter. See the [Rest and Sleep Example](https://yomotsu.github.io/camera-controls/examples/rest-and-sleep.html). 21263 * 21264 * e.g. 21265 * ``` 21266 * cameraControl.addEventListener( 'controlstart', myCallbackFunction ); 21267 * ``` 21268 * @param type event name 21269 * @param listener handler function 21270 * @category Methods 21271 */ 21272 addEventListener(type, listener) { 21273 super.addEventListener(type, listener); 21274 } 21275 /** 21276 * Removes the specified event listener 21277 * e.g. 21278 * ``` 21279 * cameraControl.addEventListener( 'controlstart', myCallbackFunction ); 21280 * ``` 21281 * @param type event name 21282 * @param listener handler function 21283 * @category Methods 21284 */ 21285 removeEventListener(type, listener) { 21286 super.removeEventListener(type, listener); 21287 } 21288 /** 21289 * Rotate azimuthal angle(horizontal) and polar angle(vertical). 21290 * Every value is added to the current value. 21291 * @param azimuthAngle Azimuth rotate angle. In radian. 21292 * @param polarAngle Polar rotate angle. In radian. 21293 * @param enableTransition Whether to move smoothly or immediately 21294 * @category Methods 21295 */ 21296 rotate(azimuthAngle, polarAngle, enableTransition = false) { 21297 return this.rotateTo(this._sphericalEnd.theta + azimuthAngle, this._sphericalEnd.phi + polarAngle, enableTransition); 21298 } 21299 /** 21300 * Rotate azimuthal angle(horizontal) to the given angle and keep the same polar angle(vertical) target. 21301 * 21302 * e.g. 21303 * ``` 21304 * cameraControls.rotateAzimuthTo( 30 * THREE.MathUtils.DEG2RAD, true ); 21305 * ``` 21306 * @param azimuthAngle Azimuth rotate angle. In radian. 21307 * @param enableTransition Whether to move smoothly or immediately 21308 * @category Methods 21309 */ 21310 rotateAzimuthTo(azimuthAngle, enableTransition = false) { 21311 return this.rotateTo(azimuthAngle, this._sphericalEnd.phi, enableTransition); 21312 } 21313 /** 21314 * Rotate polar angle(vertical) to the given angle and keep the same azimuthal angle(horizontal) target. 21315 * 21316 * e.g. 21317 * ``` 21318 * cameraControls.rotatePolarTo( 30 * THREE.MathUtils.DEG2RAD, true ); 21319 * ``` 21320 * @param polarAngle Polar rotate angle. In radian. 21321 * @param enableTransition Whether to move smoothly or immediately 21322 * @category Methods 21323 */ 21324 rotatePolarTo(polarAngle, enableTransition = false) { 21325 return this.rotateTo(this._sphericalEnd.theta, polarAngle, enableTransition); 21326 } 21327 /** 21328 * Rotate azimuthal angle(horizontal) and polar angle(vertical) to the given angle. 21329 * Camera view will rotate over the orbit pivot absolutely: 21330 * 21331 * azimuthAngle 21332 * ``` 21333 * 0º 21334 * \ 21335 * 90º -----+----- -90º 21336 * \ 21337 * 180º 21338 * ``` 21339 * | direction | angle | 21340 * | --------- | ---------------------- | 21341 * | front | 0º | 21342 * | left | 90º (`Math.PI / 2`) | 21343 * | right | -90º (`- Math.PI / 2`) | 21344 * | back | 180º (`Math.PI`) | 21345 * 21346 * polarAngle 21347 * ``` 21348 * 180º 21349 * | 21350 * 90º 21351 * | 21352 * 0º 21353 * ``` 21354 * | direction | angle | 21355 * | -------------------- | ---------------------- | 21356 * | top/sky | 180º (`Math.PI`) | 21357 * | horizontal from view | 90º (`Math.PI / 2`) | 21358 * | bottom/floor | 0º | 21359 * 21360 * @param azimuthAngle Azimuth rotate angle to. In radian. 21361 * @param polarAngle Polar rotate angle to. In radian. 21362 * @param enableTransition Whether to move smoothly or immediately 21363 * @category Methods 21364 */ 21365 rotateTo(azimuthAngle, polarAngle, enableTransition = false) { 21366 this._isUserControllingRotate = false; 21367 const theta = clamp(azimuthAngle, this.minAzimuthAngle, this.maxAzimuthAngle); 21368 const phi = clamp(polarAngle, this.minPolarAngle, this.maxPolarAngle); 21369 this._sphericalEnd.theta = theta; 21370 this._sphericalEnd.phi = phi; 21371 this._sphericalEnd.makeSafe(); 21372 this._needsUpdate = true; 21373 if (!enableTransition) { 21374 this._spherical.theta = this._sphericalEnd.theta; 21375 this._spherical.phi = this._sphericalEnd.phi; 21376 } 21377 const resolveImmediately = !enableTransition || approxEquals(this._spherical.theta, this._sphericalEnd.theta, this.restThreshold) && approxEquals(this._spherical.phi, this._sphericalEnd.phi, this.restThreshold); 21378 return this._createOnRestPromise(resolveImmediately); 21379 } 21380 /** 21381 * Dolly in/out camera position. 21382 * @param distance Distance of dollyIn. Negative number for dollyOut. 21383 * @param enableTransition Whether to move smoothly or immediately. 21384 * @category Methods 21385 */ 21386 dolly(distance, enableTransition = false) { 21387 return this.dollyTo(this._sphericalEnd.radius - distance, enableTransition); 21388 } 21389 /** 21390 * Dolly in/out camera position to given distance. 21391 * @param distance Distance of dolly. 21392 * @param enableTransition Whether to move smoothly or immediately. 21393 * @category Methods 21394 */ 21395 dollyTo(distance, enableTransition = false) { 21396 this._isUserControllingDolly = false; 21397 this._lastDollyDirection = DOLLY_DIRECTION.NONE; 21398 this._changedDolly = 0; 21399 return this._dollyToNoClamp(clamp(distance, this.minDistance, this.maxDistance), enableTransition); 21400 } 21401 _dollyToNoClamp(distance, enableTransition = false) { 21402 const lastRadius = this._sphericalEnd.radius; 21403 const hasCollider = this.colliderMeshes.length >= 1; 21404 if (hasCollider) { 21405 const maxDistanceByCollisionTest = this._collisionTest(); 21406 const isCollided = approxEquals(maxDistanceByCollisionTest, this._spherical.radius); 21407 const isDollyIn = lastRadius > distance; 21408 if (!isDollyIn && isCollided) return Promise.resolve(); 21409 this._sphericalEnd.radius = Math.min(distance, maxDistanceByCollisionTest); 21410 } else { 21411 this._sphericalEnd.radius = distance; 21412 } 21413 this._needsUpdate = true; 21414 if (!enableTransition) { 21415 this._spherical.radius = this._sphericalEnd.radius; 21416 } 21417 const resolveImmediately = !enableTransition || approxEquals(this._spherical.radius, this._sphericalEnd.radius, this.restThreshold); 21418 return this._createOnRestPromise(resolveImmediately); 21419 } 21420 /** 21421 * Dolly in, but does not change the distance between the target and the camera, and moves the target position instead. 21422 * Specify a negative value for dolly out. 21423 * @param distance Distance of dolly. 21424 * @param enableTransition Whether to move smoothly or immediately. 21425 * @category Methods 21426 */ 21427 dollyInFixed(distance, enableTransition = false) { 21428 this._targetEnd.add(this._getCameraDirection(_cameraDirection).multiplyScalar(distance)); 21429 if (!enableTransition) { 21430 this._target.copy(this._targetEnd); 21431 } 21432 const resolveImmediately = !enableTransition || approxEquals(this._target.x, this._targetEnd.x, this.restThreshold) && approxEquals(this._target.y, this._targetEnd.y, this.restThreshold) && approxEquals(this._target.z, this._targetEnd.z, this.restThreshold); 21433 return this._createOnRestPromise(resolveImmediately); 21434 } 21435 /** 21436 * Zoom in/out camera. The value is added to camera zoom. 21437 * Limits set with `.minZoom` and `.maxZoom` 21438 * @param zoomStep zoom scale 21439 * @param enableTransition Whether to move smoothly or immediately 21440 * @category Methods 21441 */ 21442 zoom(zoomStep, enableTransition = false) { 21443 return this.zoomTo(this._zoomEnd + zoomStep, enableTransition); 21444 } 21445 /** 21446 * Zoom in/out camera to given scale. The value overwrites camera zoom. 21447 * Limits set with .minZoom and .maxZoom 21448 * @param zoom 21449 * @param enableTransition 21450 * @category Methods 21451 */ 21452 zoomTo(zoom, enableTransition = false) { 21453 this._isUserControllingZoom = false; 21454 this._zoomEnd = clamp(zoom, this.minZoom, this.maxZoom); 21455 this._needsUpdate = true; 21456 if (!enableTransition) { 21457 this._zoom = this._zoomEnd; 21458 } 21459 const resolveImmediately = !enableTransition || approxEquals(this._zoom, this._zoomEnd, this.restThreshold); 21460 this._changedZoom = 0; 21461 return this._createOnRestPromise(resolveImmediately); 21462 } 21463 /** 21464 * @deprecated `pan()` has been renamed to `truck()` 21465 * @category Methods 21466 */ 21467 pan(x, y, enableTransition = false) { 21468 console.warn("`pan` has been renamed to `truck`"); 21469 return this.truck(x, y, enableTransition); 21470 } 21471 /** 21472 * Truck and pedestal camera using current azimuthal angle 21473 * @param x Horizontal translate amount 21474 * @param y Vertical translate amount 21475 * @param enableTransition Whether to move smoothly or immediately 21476 * @category Methods 21477 */ 21478 truck(x, y, enableTransition = false) { 21479 this._camera.updateMatrix(); 21480 _xColumn.setFromMatrixColumn(this._camera.matrix, 0); 21481 _yColumn.setFromMatrixColumn(this._camera.matrix, 1); 21482 _xColumn.multiplyScalar(x); 21483 _yColumn.multiplyScalar(-y); 21484 const offset = _v3A.copy(_xColumn).add(_yColumn); 21485 const to = _v3B.copy(this._targetEnd).add(offset); 21486 return this.moveTo(to.x, to.y, to.z, enableTransition); 21487 } 21488 /** 21489 * Move forward / backward. 21490 * @param distance Amount to move forward / backward. Negative value to move backward 21491 * @param enableTransition Whether to move smoothly or immediately 21492 * @category Methods 21493 */ 21494 forward(distance, enableTransition = false) { 21495 _v3A.setFromMatrixColumn(this._camera.matrix, 0); 21496 _v3A.crossVectors(this._camera.up, _v3A); 21497 _v3A.multiplyScalar(distance); 21498 const to = _v3B.copy(this._targetEnd).add(_v3A); 21499 return this.moveTo(to.x, to.y, to.z, enableTransition); 21500 } 21501 /** 21502 * Move up / down. 21503 * @param height Amount to move up / down. Negative value to move down 21504 * @param enableTransition Whether to move smoothly or immediately 21505 * @category Methods 21506 */ 21507 elevate(height, enableTransition = false) { 21508 _v3A.copy(this._camera.up).multiplyScalar(height); 21509 return this.moveTo(this._targetEnd.x + _v3A.x, this._targetEnd.y + _v3A.y, this._targetEnd.z + _v3A.z, enableTransition); 21510 } 21511 /** 21512 * Move target position to given point. 21513 * @param x x coord to move center position 21514 * @param y y coord to move center position 21515 * @param z z coord to move center position 21516 * @param enableTransition Whether to move smoothly or immediately 21517 * @category Methods 21518 */ 21519 moveTo(x, y, z, enableTransition = false) { 21520 this._isUserControllingTruck = false; 21521 const offset = _v3A.set(x, y, z).sub(this._targetEnd); 21522 this._encloseToBoundary(this._targetEnd, offset, this.boundaryFriction); 21523 this._needsUpdate = true; 21524 if (!enableTransition) { 21525 this._target.copy(this._targetEnd); 21526 } 21527 const resolveImmediately = !enableTransition || approxEquals(this._target.x, this._targetEnd.x, this.restThreshold) && approxEquals(this._target.y, this._targetEnd.y, this.restThreshold) && approxEquals(this._target.z, this._targetEnd.z, this.restThreshold); 21528 return this._createOnRestPromise(resolveImmediately); 21529 } 21530 /** 21531 * Look in the given point direction. 21532 * @param x point x. 21533 * @param y point y. 21534 * @param z point z. 21535 * @param enableTransition Whether to move smoothly or immediately. 21536 * @returns Transition end promise 21537 * @category Methods 21538 */ 21539 lookInDirectionOf(x, y, z, enableTransition = false) { 21540 const point = _v3A.set(x, y, z); 21541 const direction = point.sub(this._targetEnd).normalize(); 21542 const position = direction.multiplyScalar(-this._sphericalEnd.radius).add(this._targetEnd); 21543 return this.setPosition(position.x, position.y, position.z, enableTransition); 21544 } 21545 /** 21546 * Fit the viewport to the box or the bounding box of the object, using the nearest axis. paddings are in unit. 21547 * set `cover: true` to fill enter screen. 21548 * e.g. 21549 * ``` 21550 * cameraControls.fitToBox( myMesh ); 21551 * ``` 21552 * @param box3OrObject Axis aligned bounding box to fit the view. 21553 * @param enableTransition Whether to move smoothly or immediately. 21554 * @param options | `<object>` { cover: boolean, paddingTop: number, paddingLeft: number, paddingBottom: number, paddingRight: number } 21555 * @returns Transition end promise 21556 * @category Methods 21557 */ 21558 fitToBox(box3OrObject, enableTransition, { 21559 cover = false, 21560 paddingLeft = 0, 21561 paddingRight = 0, 21562 paddingBottom = 0, 21563 paddingTop = 0 21564 } = {}) { 21565 const promises = []; 21566 const aabb = box3OrObject.isBox3 ? _box3A.copy(box3OrObject) : _box3A.setFromObject(box3OrObject); 21567 if (aabb.isEmpty()) { 21568 console.warn("camera-controls: fitTo() cannot be used with an empty box. Aborting"); 21569 Promise.resolve(); 21570 } 21571 const theta = roundToStep(this._sphericalEnd.theta, PI_HALF); 21572 const phi = roundToStep(this._sphericalEnd.phi, PI_HALF); 21573 promises.push(this.rotateTo(theta, phi, enableTransition)); 21574 const normal = _v3A.setFromSpherical(this._sphericalEnd).normalize(); 21575 const rotation = _quaternionA.setFromUnitVectors(normal, _AXIS_Z); 21576 const viewFromPolar = approxEquals(Math.abs(normal.y), 1); 21577 if (viewFromPolar) { 21578 rotation.multiply(_quaternionB.setFromAxisAngle(_AXIS_Y, theta)); 21579 } 21580 rotation.multiply(this._yAxisUpSpaceInverse); 21581 const bb = _box3B.makeEmpty(); 21582 _v3B.copy(aabb.min).applyQuaternion(rotation); 21583 bb.expandByPoint(_v3B); 21584 _v3B.copy(aabb.min).setX(aabb.max.x).applyQuaternion(rotation); 21585 bb.expandByPoint(_v3B); 21586 _v3B.copy(aabb.min).setY(aabb.max.y).applyQuaternion(rotation); 21587 bb.expandByPoint(_v3B); 21588 _v3B.copy(aabb.max).setZ(aabb.min.z).applyQuaternion(rotation); 21589 bb.expandByPoint(_v3B); 21590 _v3B.copy(aabb.min).setZ(aabb.max.z).applyQuaternion(rotation); 21591 bb.expandByPoint(_v3B); 21592 _v3B.copy(aabb.max).setY(aabb.min.y).applyQuaternion(rotation); 21593 bb.expandByPoint(_v3B); 21594 _v3B.copy(aabb.max).setX(aabb.min.x).applyQuaternion(rotation); 21595 bb.expandByPoint(_v3B); 21596 _v3B.copy(aabb.max).applyQuaternion(rotation); 21597 bb.expandByPoint(_v3B); 21598 bb.min.x -= paddingLeft; 21599 bb.min.y -= paddingBottom; 21600 bb.max.x += paddingRight; 21601 bb.max.y += paddingTop; 21602 rotation.setFromUnitVectors(_AXIS_Z, normal); 21603 if (viewFromPolar) { 21604 rotation.premultiply(_quaternionB.invert()); 21605 } 21606 rotation.premultiply(this._yAxisUpSpace); 21607 const bbSize = bb.getSize(_v3A); 21608 const center = bb.getCenter(_v3B).applyQuaternion(rotation); 21609 if (isPerspectiveCamera(this._camera)) { 21610 const distance = this.getDistanceToFitBox(bbSize.x, bbSize.y, bbSize.z, cover); 21611 promises.push(this.moveTo(center.x, center.y, center.z, enableTransition)); 21612 promises.push(this.dollyTo(distance, enableTransition)); 21613 promises.push(this.setFocalOffset(0, 0, 0, enableTransition)); 21614 } else if (isOrthographicCamera(this._camera)) { 21615 const camera = this._camera; 21616 const width = camera.right - camera.left; 21617 const height = camera.top - camera.bottom; 21618 const zoom = cover ? Math.max(width / bbSize.x, height / bbSize.y) : Math.min(width / bbSize.x, height / bbSize.y); 21619 promises.push(this.moveTo(center.x, center.y, center.z, enableTransition)); 21620 promises.push(this.zoomTo(zoom, enableTransition)); 21621 promises.push(this.setFocalOffset(0, 0, 0, enableTransition)); 21622 } 21623 return Promise.all(promises); 21624 } 21625 /** 21626 * Fit the viewport to the sphere or the bounding sphere of the object. 21627 * @param sphereOrMesh 21628 * @param enableTransition 21629 * @category Methods 21630 */ 21631 fitToSphere(sphereOrMesh, enableTransition) { 21632 const promises = []; 21633 const isObject3D = "isObject3D" in sphereOrMesh; 21634 const boundingSphere = isObject3D ? _CameraControls.createBoundingSphere(sphereOrMesh, _sphere) : _sphere.copy(sphereOrMesh); 21635 promises.push(this.moveTo(boundingSphere.center.x, boundingSphere.center.y, boundingSphere.center.z, enableTransition)); 21636 if (isPerspectiveCamera(this._camera)) { 21637 const distanceToFit = this.getDistanceToFitSphere(boundingSphere.radius); 21638 promises.push(this.dollyTo(distanceToFit, enableTransition)); 21639 } else if (isOrthographicCamera(this._camera)) { 21640 const width = this._camera.right - this._camera.left; 21641 const height = this._camera.top - this._camera.bottom; 21642 const diameter = 2 * boundingSphere.radius; 21643 const zoom = Math.min(width / diameter, height / diameter); 21644 promises.push(this.zoomTo(zoom, enableTransition)); 21645 } 21646 promises.push(this.setFocalOffset(0, 0, 0, enableTransition)); 21647 return Promise.all(promises); 21648 } 21649 /** 21650 * Look at the `target` from the `position`. 21651 * @param positionX 21652 * @param positionY 21653 * @param positionZ 21654 * @param targetX 21655 * @param targetY 21656 * @param targetZ 21657 * @param enableTransition 21658 * @category Methods 21659 */ 21660 setLookAt(positionX, positionY, positionZ, targetX, targetY, targetZ, enableTransition = false) { 21661 this._isUserControllingRotate = false; 21662 this._isUserControllingDolly = false; 21663 this._isUserControllingTruck = false; 21664 this._lastDollyDirection = DOLLY_DIRECTION.NONE; 21665 this._changedDolly = 0; 21666 const target = _v3B.set(targetX, targetY, targetZ); 21667 const position = _v3A.set(positionX, positionY, positionZ); 21668 this._targetEnd.copy(target); 21669 this._sphericalEnd.setFromVector3(position.sub(target).applyQuaternion(this._yAxisUpSpace)); 21670 this._needsUpdate = true; 21671 if (!enableTransition) { 21672 this._target.copy(this._targetEnd); 21673 this._spherical.copy(this._sphericalEnd); 21674 } 21675 const resolveImmediately = !enableTransition || approxEquals(this._target.x, this._targetEnd.x, this.restThreshold) && approxEquals(this._target.y, this._targetEnd.y, this.restThreshold) && approxEquals(this._target.z, this._targetEnd.z, this.restThreshold) && approxEquals(this._spherical.theta, this._sphericalEnd.theta, this.restThreshold) && approxEquals(this._spherical.phi, this._sphericalEnd.phi, this.restThreshold) && approxEquals(this._spherical.radius, this._sphericalEnd.radius, this.restThreshold); 21676 return this._createOnRestPromise(resolveImmediately); 21677 } 21678 /** 21679 * Interpolates between two states. 21680 * @param stateA 21681 * @param stateB 21682 * @param t 21683 * @param enableTransition 21684 * @category Methods 21685 */ 21686 lerp(stateA, stateB, t, enableTransition = false) { 21687 this._isUserControllingRotate = false; 21688 this._isUserControllingDolly = false; 21689 this._isUserControllingTruck = false; 21690 this._lastDollyDirection = DOLLY_DIRECTION.NONE; 21691 this._changedDolly = 0; 21692 const targetA = _v3A.set(...stateA.target); 21693 if ("spherical" in stateA) { 21694 _sphericalA.set(...stateA.spherical); 21695 } else { 21696 const positionA = _v3B.set(...stateA.position); 21697 _sphericalA.setFromVector3(positionA.sub(targetA).applyQuaternion(this._yAxisUpSpace)); 21698 } 21699 const targetB = _v3C.set(...stateB.target); 21700 if ("spherical" in stateB) { 21701 _sphericalB.set(...stateB.spherical); 21702 } else { 21703 const positionB = _v3B.set(...stateB.position); 21704 _sphericalB.setFromVector3(positionB.sub(targetB).applyQuaternion(this._yAxisUpSpace)); 21705 } 21706 this._targetEnd.copy(targetA.lerp(targetB, t)); 21707 const deltaTheta = _sphericalB.theta - _sphericalA.theta; 21708 const deltaPhi = _sphericalB.phi - _sphericalA.phi; 21709 const deltaRadius = _sphericalB.radius - _sphericalA.radius; 21710 this._sphericalEnd.set(_sphericalA.radius + deltaRadius * t, _sphericalA.phi + deltaPhi * t, _sphericalA.theta + deltaTheta * t); 21711 this._needsUpdate = true; 21712 if (!enableTransition) { 21713 this._target.copy(this._targetEnd); 21714 this._spherical.copy(this._sphericalEnd); 21715 } 21716 const resolveImmediately = !enableTransition || approxEquals(this._target.x, this._targetEnd.x, this.restThreshold) && approxEquals(this._target.y, this._targetEnd.y, this.restThreshold) && approxEquals(this._target.z, this._targetEnd.z, this.restThreshold) && approxEquals(this._spherical.theta, this._sphericalEnd.theta, this.restThreshold) && approxEquals(this._spherical.phi, this._sphericalEnd.phi, this.restThreshold) && approxEquals(this._spherical.radius, this._sphericalEnd.radius, this.restThreshold); 21717 return this._createOnRestPromise(resolveImmediately); 21718 } 21719 /** 21720 * Similar to setLookAt, but it interpolates between two states. 21721 * @param positionAX 21722 * @param positionAY 21723 * @param positionAZ 21724 * @param targetAX 21725 * @param targetAY 21726 * @param targetAZ 21727 * @param positionBX 21728 * @param positionBY 21729 * @param positionBZ 21730 * @param targetBX 21731 * @param targetBY 21732 * @param targetBZ 21733 * @param t 21734 * @param enableTransition 21735 * @category Methods 21736 */ 21737 lerpLookAt(positionAX, positionAY, positionAZ, targetAX, targetAY, targetAZ, positionBX, positionBY, positionBZ, targetBX, targetBY, targetBZ, t, enableTransition = false) { 21738 return this.lerp({ 21739 position: [positionAX, positionAY, positionAZ], 21740 target: [targetAX, targetAY, targetAZ] 21741 }, { 21742 position: [positionBX, positionBY, positionBZ], 21743 target: [targetBX, targetBY, targetBZ] 21744 }, t, enableTransition); 21745 } 21746 /** 21747 * Set angle and distance by given position. 21748 * An alias of `setLookAt()`, without target change. Thus keep gazing at the current target 21749 * @param positionX 21750 * @param positionY 21751 * @param positionZ 21752 * @param enableTransition 21753 * @category Methods 21754 */ 21755 setPosition(positionX, positionY, positionZ, enableTransition = false) { 21756 return this.setLookAt(positionX, positionY, positionZ, this._targetEnd.x, this._targetEnd.y, this._targetEnd.z, enableTransition); 21757 } 21758 /** 21759 * Set the target position where gaze at. 21760 * An alias of `setLookAt()`, without position change. Thus keep the same position. 21761 * @param targetX 21762 * @param targetY 21763 * @param targetZ 21764 * @param enableTransition 21765 * @category Methods 21766 */ 21767 setTarget(targetX, targetY, targetZ, enableTransition = false) { 21768 const pos = this.getPosition(_v3A); 21769 const promise = this.setLookAt(pos.x, pos.y, pos.z, targetX, targetY, targetZ, enableTransition); 21770 this._sphericalEnd.phi = clamp(this._sphericalEnd.phi, this.minPolarAngle, this.maxPolarAngle); 21771 return promise; 21772 } 21773 /** 21774 * Set focal offset using the screen parallel coordinates. z doesn't affect in Orthographic as with Dolly. 21775 * @param x 21776 * @param y 21777 * @param z 21778 * @param enableTransition 21779 * @category Methods 21780 */ 21781 setFocalOffset(x, y, z, enableTransition = false) { 21782 this._isUserControllingOffset = false; 21783 this._focalOffsetEnd.set(x, y, z); 21784 this._needsUpdate = true; 21785 if (!enableTransition) this._focalOffset.copy(this._focalOffsetEnd); 21786 const resolveImmediately = !enableTransition || approxEquals(this._focalOffset.x, this._focalOffsetEnd.x, this.restThreshold) && approxEquals(this._focalOffset.y, this._focalOffsetEnd.y, this.restThreshold) && approxEquals(this._focalOffset.z, this._focalOffsetEnd.z, this.restThreshold); 21787 return this._createOnRestPromise(resolveImmediately); 21788 } 21789 /** 21790 * Set orbit point without moving the camera. 21791 * SHOULD NOT RUN DURING ANIMATIONS. `setOrbitPoint()` will immediately fix the positions. 21792 * @param targetX 21793 * @param targetY 21794 * @param targetZ 21795 * @category Methods 21796 */ 21797 setOrbitPoint(targetX, targetY, targetZ) { 21798 this._camera.updateMatrixWorld(); 21799 _xColumn.setFromMatrixColumn(this._camera.matrixWorldInverse, 0); 21800 _yColumn.setFromMatrixColumn(this._camera.matrixWorldInverse, 1); 21801 _zColumn.setFromMatrixColumn(this._camera.matrixWorldInverse, 2); 21802 const position = _v3A.set(targetX, targetY, targetZ); 21803 const distance = position.distanceTo(this._camera.position); 21804 const cameraToPoint = position.sub(this._camera.position); 21805 _xColumn.multiplyScalar(cameraToPoint.x); 21806 _yColumn.multiplyScalar(cameraToPoint.y); 21807 _zColumn.multiplyScalar(cameraToPoint.z); 21808 _v3A.copy(_xColumn).add(_yColumn).add(_zColumn); 21809 _v3A.z = _v3A.z + distance; 21810 this.dollyTo(distance, false); 21811 this.setFocalOffset(-_v3A.x, _v3A.y, -_v3A.z, false); 21812 this.moveTo(targetX, targetY, targetZ, false); 21813 } 21814 /** 21815 * Set the boundary box that encloses the target of the camera. box3 is in THREE.Box3 21816 * @param box3 21817 * @category Methods 21818 */ 21819 setBoundary(box3) { 21820 if (!box3) { 21821 this._boundary.min.set(-Infinity, -Infinity, -Infinity); 21822 this._boundary.max.set(Infinity, Infinity, Infinity); 21823 this._needsUpdate = true; 21824 return; 21825 } 21826 this._boundary.copy(box3); 21827 this._boundary.clampPoint(this._targetEnd, this._targetEnd); 21828 this._needsUpdate = true; 21829 } 21830 /** 21831 * Set (or unset) the current viewport. 21832 * Set this when you want to use renderer viewport and .dollyToCursor feature at the same time. 21833 * @param viewportOrX 21834 * @param y 21835 * @param width 21836 * @param height 21837 * @category Methods 21838 */ 21839 setViewport(viewportOrX, y, width, height) { 21840 if (viewportOrX === null) { 21841 this._viewport = null; 21842 return; 21843 } 21844 this._viewport = this._viewport || new THREE.Vector4(); 21845 if (typeof viewportOrX === "number") { 21846 this._viewport.set(viewportOrX, y, width, height); 21847 } else { 21848 this._viewport.copy(viewportOrX); 21849 } 21850 } 21851 /** 21852 * Calculate the distance to fit the box. 21853 * @param width box width 21854 * @param height box height 21855 * @param depth box depth 21856 * @returns distance 21857 * @category Methods 21858 */ 21859 getDistanceToFitBox(width, height, depth, cover = false) { 21860 if (notSupportedInOrthographicCamera(this._camera, "getDistanceToFitBox")) return this._spherical.radius; 21861 const boundingRectAspect = width / height; 21862 const fov = this._camera.getEffectiveFOV() * DEG2RAD; 21863 const aspect = this._camera.aspect; 21864 const heightToFit = (cover ? boundingRectAspect > aspect : boundingRectAspect < aspect) ? height : width / aspect; 21865 return heightToFit * 0.5 / Math.tan(fov * 0.5) + depth * 0.5; 21866 } 21867 /** 21868 * Calculate the distance to fit the sphere. 21869 * @param radius sphere radius 21870 * @returns distance 21871 * @category Methods 21872 */ 21873 getDistanceToFitSphere(radius) { 21874 if (notSupportedInOrthographicCamera(this._camera, "getDistanceToFitSphere")) return this._spherical.radius; 21875 const vFOV = this._camera.getEffectiveFOV() * DEG2RAD; 21876 const hFOV = Math.atan(Math.tan(vFOV * 0.5) * this._camera.aspect) * 2; 21877 const fov = 1 < this._camera.aspect ? vFOV : hFOV; 21878 return radius / Math.sin(fov * 0.5); 21879 } 21880 /** 21881 * Returns the orbit center position, where the camera looking at. 21882 * @param out The receiving Vector3 instance to copy the result 21883 * @param receiveEndValue Whether receive the transition end coords or current. default is `true` 21884 * @category Methods 21885 */ 21886 getTarget(out, receiveEndValue = true) { 21887 const _out = !!out && out.isVector3 ? out : new THREE.Vector3(); 21888 return _out.copy(receiveEndValue ? this._targetEnd : this._target); 21889 } 21890 /** 21891 * Returns the camera position. 21892 * @param out The receiving Vector3 instance to copy the result 21893 * @param receiveEndValue Whether receive the transition end coords or current. default is `true` 21894 * @category Methods 21895 */ 21896 getPosition(out, receiveEndValue = true) { 21897 const _out = !!out && out.isVector3 ? out : new THREE.Vector3(); 21898 return _out.setFromSpherical(receiveEndValue ? this._sphericalEnd : this._spherical).applyQuaternion(this._yAxisUpSpaceInverse).add(receiveEndValue ? this._targetEnd : this._target); 21899 } 21900 /** 21901 * Returns the spherical coordinates of the orbit. 21902 * @param out The receiving Spherical instance to copy the result 21903 * @param receiveEndValue Whether receive the transition end coords or current. default is `true` 21904 * @category Methods 21905 */ 21906 getSpherical(out, receiveEndValue = true) { 21907 const _out = out || new THREE.Spherical(); 21908 return _out.copy(receiveEndValue ? this._sphericalEnd : this._spherical); 21909 } 21910 /** 21911 * Returns the focal offset, which is how much the camera appears to be translated in screen parallel coordinates. 21912 * @param out The receiving Vector3 instance to copy the result 21913 * @param receiveEndValue Whether receive the transition end coords or current. default is `true` 21914 * @category Methods 21915 */ 21916 getFocalOffset(out, receiveEndValue = true) { 21917 const _out = !!out && out.isVector3 ? out : new THREE.Vector3(); 21918 return _out.copy(receiveEndValue ? this._focalOffsetEnd : this._focalOffset); 21919 } 21920 /** 21921 * Normalize camera azimuth angle (horizontal rotation) between -180 and 180 degrees. 21922 * @returns This CameraControls instance. 21923 * @category Methods 21924 */ 21925 normalizeRotations() { 21926 this._sphericalEnd.theta = (this._sphericalEnd.theta % PI_2 + PI_2) % PI_2; 21927 if (this._sphericalEnd.theta > Math.PI) this._sphericalEnd.theta -= PI_2; 21928 this._spherical.theta += PI_2 * Math.round((this._sphericalEnd.theta - this._spherical.theta) / PI_2); 21929 return this; 21930 } 21931 /** 21932 * stop all transitions. 21933 */ 21934 stop() { 21935 this._focalOffset.copy(this._focalOffsetEnd); 21936 this._target.copy(this._targetEnd); 21937 this._spherical.copy(this._sphericalEnd); 21938 this._zoom = this._zoomEnd; 21939 } 21940 /** 21941 * Reset all rotation and position to defaults. 21942 * @param enableTransition 21943 * @category Methods 21944 */ 21945 reset(enableTransition = false) { 21946 if (!approxEquals(this._camera.up.x, this._cameraUp0.x) || !approxEquals(this._camera.up.y, this._cameraUp0.y) || !approxEquals(this._camera.up.z, this._cameraUp0.z)) { 21947 this._camera.up.copy(this._cameraUp0); 21948 const position = this.getPosition(_v3A); 21949 this.updateCameraUp(); 21950 this.setPosition(position.x, position.y, position.z); 21951 } 21952 const promises = [this.setLookAt(this._position0.x, this._position0.y, this._position0.z, this._target0.x, this._target0.y, this._target0.z, enableTransition), this.setFocalOffset(this._focalOffset0.x, this._focalOffset0.y, this._focalOffset0.z, enableTransition), this.zoomTo(this._zoom0, enableTransition)]; 21953 return Promise.all(promises); 21954 } 21955 /** 21956 * Set current camera position as the default position. 21957 * @category Methods 21958 */ 21959 saveState() { 21960 this._cameraUp0.copy(this._camera.up); 21961 this.getTarget(this._target0); 21962 this.getPosition(this._position0); 21963 this._zoom0 = this._zoom; 21964 this._focalOffset0.copy(this._focalOffset); 21965 } 21966 /** 21967 * Sync camera-up direction. 21968 * When camera-up vector is changed, `.updateCameraUp()` must be called. 21969 * @category Methods 21970 */ 21971 updateCameraUp() { 21972 this._yAxisUpSpace.setFromUnitVectors(this._camera.up, _AXIS_Y); 21973 this._yAxisUpSpaceInverse.copy(this._yAxisUpSpace).invert(); 21974 } 21975 /** 21976 * Apply current camera-up direction to the camera. 21977 * The orbit system will be re-initialized with the current position. 21978 * @category Methods 21979 */ 21980 applyCameraUp() { 21981 const cameraDirection = _v3A.subVectors(this._target, this._camera.position).normalize(); 21982 const side = _v3B.crossVectors(cameraDirection, this._camera.up); 21983 this._camera.up.crossVectors(side, cameraDirection).normalize(); 21984 this._camera.updateMatrixWorld(); 21985 const position = this.getPosition(_v3A); 21986 this.updateCameraUp(); 21987 this.setPosition(position.x, position.y, position.z); 21988 } 21989 /** 21990 * Update camera position and directions. 21991 * This should be called in your tick loop every time, and returns true if re-rendering is needed. 21992 * @param delta 21993 * @returns updated 21994 * @category Methods 21995 */ 21996 update(delta) { 21997 const deltaTheta = this._sphericalEnd.theta - this._spherical.theta; 21998 const deltaPhi = this._sphericalEnd.phi - this._spherical.phi; 21999 const deltaRadius = this._sphericalEnd.radius - this._spherical.radius; 22000 const deltaTarget = _deltaTarget.subVectors(this._targetEnd, this._target); 22001 const deltaOffset = _deltaOffset.subVectors(this._focalOffsetEnd, this._focalOffset); 22002 const deltaZoom = this._zoomEnd - this._zoom; 22003 if (approxZero(deltaTheta)) { 22004 this._thetaVelocity.value = 0; 22005 this._spherical.theta = this._sphericalEnd.theta; 22006 } else { 22007 const smoothTime = this._isUserControllingRotate ? this.draggingSmoothTime : this.smoothTime; 22008 this._spherical.theta = smoothDamp(this._spherical.theta, this._sphericalEnd.theta, this._thetaVelocity, smoothTime, Infinity, delta); 22009 this._needsUpdate = true; 22010 } 22011 if (approxZero(deltaPhi)) { 22012 this._phiVelocity.value = 0; 22013 this._spherical.phi = this._sphericalEnd.phi; 22014 } else { 22015 const smoothTime = this._isUserControllingRotate ? this.draggingSmoothTime : this.smoothTime; 22016 this._spherical.phi = smoothDamp(this._spherical.phi, this._sphericalEnd.phi, this._phiVelocity, smoothTime, Infinity, delta); 22017 this._needsUpdate = true; 22018 } 22019 if (approxZero(deltaRadius)) { 22020 this._radiusVelocity.value = 0; 22021 this._spherical.radius = this._sphericalEnd.radius; 22022 } else { 22023 const smoothTime = this._isUserControllingDolly ? this.draggingSmoothTime : this.smoothTime; 22024 this._spherical.radius = smoothDamp(this._spherical.radius, this._sphericalEnd.radius, this._radiusVelocity, smoothTime, this.maxSpeed, delta); 22025 this._needsUpdate = true; 22026 } 22027 if (approxZero(deltaTarget.x) && approxZero(deltaTarget.y) && approxZero(deltaTarget.z)) { 22028 this._targetVelocity.set(0, 0, 0); 22029 this._target.copy(this._targetEnd); 22030 } else { 22031 const smoothTime = this._isUserControllingTruck ? this.draggingSmoothTime : this.smoothTime; 22032 smoothDampVec3(this._target, this._targetEnd, this._targetVelocity, smoothTime, this.maxSpeed, delta, this._target); 22033 this._needsUpdate = true; 22034 } 22035 if (approxZero(deltaOffset.x) && approxZero(deltaOffset.y) && approxZero(deltaOffset.z)) { 22036 this._focalOffsetVelocity.set(0, 0, 0); 22037 this._focalOffset.copy(this._focalOffsetEnd); 22038 } else { 22039 const smoothTime = this._isUserControllingOffset ? this.draggingSmoothTime : this.smoothTime; 22040 smoothDampVec3(this._focalOffset, this._focalOffsetEnd, this._focalOffsetVelocity, smoothTime, this.maxSpeed, delta, this._focalOffset); 22041 this._needsUpdate = true; 22042 } 22043 if (approxZero(deltaZoom)) { 22044 this._zoomVelocity.value = 0; 22045 this._zoom = this._zoomEnd; 22046 } else { 22047 const smoothTime = this._isUserControllingZoom ? this.draggingSmoothTime : this.smoothTime; 22048 this._zoom = smoothDamp(this._zoom, this._zoomEnd, this._zoomVelocity, smoothTime, Infinity, delta); 22049 } 22050 if (this.dollyToCursor) { 22051 if (isPerspectiveCamera(this._camera) && this._changedDolly !== 0) { 22052 const dollyControlAmount = this._spherical.radius - this._lastDistance; 22053 const camera = this._camera; 22054 const cameraDirection = this._getCameraDirection(_cameraDirection); 22055 const planeX = _v3A.copy(cameraDirection).cross(camera.up).normalize(); 22056 if (planeX.lengthSq() === 0) planeX.x = 1; 22057 const planeY = _v3B.crossVectors(planeX, cameraDirection); 22058 const worldToScreen2 = this._sphericalEnd.radius * Math.tan(camera.getEffectiveFOV() * DEG2RAD * 0.5); 22059 const prevRadius = this._sphericalEnd.radius - dollyControlAmount; 22060 const lerpRatio = (prevRadius - this._sphericalEnd.radius) / this._sphericalEnd.radius; 22061 const cursor = _v3C.copy(this._targetEnd).add(planeX.multiplyScalar(this._dollyControlCoord.x * worldToScreen2 * camera.aspect)).add(planeY.multiplyScalar(this._dollyControlCoord.y * worldToScreen2)); 22062 const newTargetEnd = _v3A.copy(this._targetEnd).lerp(cursor, lerpRatio); 22063 const isMin = this._lastDollyDirection === DOLLY_DIRECTION.IN && this._spherical.radius <= this.minDistance; 22064 const isMax = this._lastDollyDirection === DOLLY_DIRECTION.OUT && this.maxDistance <= this._spherical.radius; 22065 if (this.infinityDolly && (isMin || isMax)) { 22066 this._sphericalEnd.radius -= dollyControlAmount; 22067 this._spherical.radius -= dollyControlAmount; 22068 const dollyAmount = _v3B.copy(cameraDirection).multiplyScalar(-dollyControlAmount); 22069 newTargetEnd.add(dollyAmount); 22070 } 22071 this._boundary.clampPoint(newTargetEnd, newTargetEnd); 22072 const targetEndDiff = _v3B.subVectors(newTargetEnd, this._targetEnd); 22073 this._targetEnd.copy(newTargetEnd); 22074 this._target.add(targetEndDiff); 22075 this._changedDolly -= dollyControlAmount; 22076 if (approxZero(this._changedDolly)) this._changedDolly = 0; 22077 } else if (isOrthographicCamera(this._camera) && this._changedZoom !== 0) { 22078 const dollyControlAmount = this._zoom - this._lastZoom; 22079 const camera = this._camera; 22080 const worldCursorPosition = _v3A.set(this._dollyControlCoord.x, this._dollyControlCoord.y, (camera.near + camera.far) / (camera.near - camera.far)).unproject(camera); 22081 const quaternion = _v3B.set(0, 0, -1).applyQuaternion(camera.quaternion); 22082 const cursor = _v3C.copy(worldCursorPosition).add(quaternion.multiplyScalar(-worldCursorPosition.dot(camera.up))); 22083 const prevZoom = this._zoom - dollyControlAmount; 22084 const lerpRatio = -(prevZoom - this._zoom) / this._zoom; 22085 const cameraDirection = this._getCameraDirection(_cameraDirection); 22086 const prevPlaneConstant = this._targetEnd.dot(cameraDirection); 22087 const newTargetEnd = _v3A.copy(this._targetEnd).lerp(cursor, lerpRatio); 22088 const newPlaneConstant = newTargetEnd.dot(cameraDirection); 22089 const pullBack = cameraDirection.multiplyScalar(newPlaneConstant - prevPlaneConstant); 22090 newTargetEnd.sub(pullBack); 22091 this._boundary.clampPoint(newTargetEnd, newTargetEnd); 22092 const targetEndDiff = _v3B.subVectors(newTargetEnd, this._targetEnd); 22093 this._targetEnd.copy(newTargetEnd); 22094 this._target.add(targetEndDiff); 22095 this._changedZoom -= dollyControlAmount; 22096 if (approxZero(this._changedZoom)) this._changedZoom = 0; 22097 } 22098 } 22099 if (this._camera.zoom !== this._zoom) { 22100 this._camera.zoom = this._zoom; 22101 this._camera.updateProjectionMatrix(); 22102 this._updateNearPlaneCorners(); 22103 this._needsUpdate = true; 22104 } 22105 this._dragNeedsUpdate = true; 22106 const maxDistance = this._collisionTest(); 22107 this._spherical.radius = Math.min(this._spherical.radius, maxDistance); 22108 this._spherical.makeSafe(); 22109 this._camera.position.setFromSpherical(this._spherical).applyQuaternion(this._yAxisUpSpaceInverse).add(this._target); 22110 this._camera.lookAt(this._target); 22111 const affectOffset = !approxZero(this._focalOffset.x) || !approxZero(this._focalOffset.y) || !approxZero(this._focalOffset.z); 22112 if (affectOffset) { 22113 this._camera.matrix.compose(this._camera.position, this._camera.quaternion, this._camera.scale); 22114 _xColumn.setFromMatrixColumn(this._camera.matrix, 0); 22115 _yColumn.setFromMatrixColumn(this._camera.matrix, 1); 22116 _zColumn.setFromMatrixColumn(this._camera.matrix, 2); 22117 _xColumn.multiplyScalar(this._focalOffset.x); 22118 _yColumn.multiplyScalar(-this._focalOffset.y); 22119 _zColumn.multiplyScalar(this._focalOffset.z); 22120 _v3A.copy(_xColumn).add(_yColumn).add(_zColumn); 22121 this._camera.position.add(_v3A); 22122 this._camera.updateMatrixWorld(); 22123 } 22124 if (this._boundaryEnclosesCamera) { 22125 this._encloseToBoundary(this._camera.position.copy(this._target), _v3A.setFromSpherical(this._spherical).applyQuaternion(this._yAxisUpSpaceInverse), 1); 22126 } 22127 const updated = this._needsUpdate; 22128 if (updated && !this._updatedLastTime) { 22129 this._hasRested = false; 22130 this.dispatchEvent({ 22131 type: "wake" 22132 }); 22133 this.dispatchEvent({ 22134 type: "update" 22135 }); 22136 } else if (updated) { 22137 this.dispatchEvent({ 22138 type: "update" 22139 }); 22140 if (approxZero(deltaTheta, this.restThreshold) && approxZero(deltaPhi, this.restThreshold) && approxZero(deltaRadius, this.restThreshold) && approxZero(deltaTarget.x, this.restThreshold) && approxZero(deltaTarget.y, this.restThreshold) && approxZero(deltaTarget.z, this.restThreshold) && approxZero(deltaOffset.x, this.restThreshold) && approxZero(deltaOffset.y, this.restThreshold) && approxZero(deltaOffset.z, this.restThreshold) && approxZero(deltaZoom, this.restThreshold) && !this._hasRested) { 22141 this._hasRested = true; 22142 this.dispatchEvent({ 22143 type: "rest" 22144 }); 22145 } 22146 } else if (!updated && this._updatedLastTime) { 22147 this.dispatchEvent({ 22148 type: "sleep" 22149 }); 22150 } 22151 this._lastDistance = this._spherical.radius; 22152 this._lastZoom = this._zoom; 22153 this._updatedLastTime = updated; 22154 this._needsUpdate = false; 22155 return updated; 22156 } 22157 /** 22158 * Get all state in JSON string 22159 * @category Methods 22160 */ 22161 toJSON() { 22162 return JSON.stringify({ 22163 enabled: this._enabled, 22164 minDistance: this.minDistance, 22165 maxDistance: infinityToMaxNumber(this.maxDistance), 22166 minZoom: this.minZoom, 22167 maxZoom: infinityToMaxNumber(this.maxZoom), 22168 minPolarAngle: this.minPolarAngle, 22169 maxPolarAngle: infinityToMaxNumber(this.maxPolarAngle), 22170 minAzimuthAngle: infinityToMaxNumber(this.minAzimuthAngle), 22171 maxAzimuthAngle: infinityToMaxNumber(this.maxAzimuthAngle), 22172 smoothTime: this.smoothTime, 22173 draggingSmoothTime: this.draggingSmoothTime, 22174 dollySpeed: this.dollySpeed, 22175 truckSpeed: this.truckSpeed, 22176 dollyToCursor: this.dollyToCursor, 22177 target: this._targetEnd.toArray(), 22178 position: _v3A.setFromSpherical(this._sphericalEnd).add(this._targetEnd).toArray(), 22179 zoom: this._zoomEnd, 22180 focalOffset: this._focalOffsetEnd.toArray(), 22181 target0: this._target0.toArray(), 22182 position0: this._position0.toArray(), 22183 zoom0: this._zoom0, 22184 focalOffset0: this._focalOffset0.toArray() 22185 }); 22186 } 22187 /** 22188 * Reproduce the control state with JSON. enableTransition is where anim or not in a boolean. 22189 * @param json 22190 * @param enableTransition 22191 * @category Methods 22192 */ 22193 fromJSON(json, enableTransition = false) { 22194 const obj = JSON.parse(json); 22195 this.enabled = obj.enabled; 22196 this.minDistance = obj.minDistance; 22197 this.maxDistance = maxNumberToInfinity(obj.maxDistance); 22198 this.minZoom = obj.minZoom; 22199 this.maxZoom = maxNumberToInfinity(obj.maxZoom); 22200 this.minPolarAngle = obj.minPolarAngle; 22201 this.maxPolarAngle = maxNumberToInfinity(obj.maxPolarAngle); 22202 this.minAzimuthAngle = maxNumberToInfinity(obj.minAzimuthAngle); 22203 this.maxAzimuthAngle = maxNumberToInfinity(obj.maxAzimuthAngle); 22204 this.smoothTime = obj.smoothTime; 22205 this.draggingSmoothTime = obj.draggingSmoothTime; 22206 this.dollySpeed = obj.dollySpeed; 22207 this.truckSpeed = obj.truckSpeed; 22208 this.dollyToCursor = obj.dollyToCursor; 22209 this._target0.fromArray(obj.target0); 22210 this._position0.fromArray(obj.position0); 22211 this._zoom0 = obj.zoom0; 22212 this._focalOffset0.fromArray(obj.focalOffset0); 22213 this.moveTo(obj.target[0], obj.target[1], obj.target[2], enableTransition); 22214 _sphericalA.setFromVector3(_v3A.fromArray(obj.position).sub(this._targetEnd).applyQuaternion(this._yAxisUpSpace)); 22215 this.rotateTo(_sphericalA.theta, _sphericalA.phi, enableTransition); 22216 this.dollyTo(_sphericalA.radius, enableTransition); 22217 this.zoomTo(obj.zoom, enableTransition); 22218 this.setFocalOffset(obj.focalOffset[0], obj.focalOffset[1], obj.focalOffset[2], enableTransition); 22219 this._needsUpdate = true; 22220 } 22221 /** 22222 * Attach all internal event handlers to enable drag control. 22223 * @category Methods 22224 */ 22225 connect(domElement) { 22226 if (this._domElement) { 22227 console.warn("camera-controls is already connected."); 22228 return; 22229 } 22230 domElement.setAttribute("data-camera-controls-version", VERSION); 22231 this._addAllEventListeners(domElement); 22232 this._getClientRect(this._elementRect); 22233 } 22234 /** 22235 * Detach all internal event handlers to disable drag control. 22236 */ 22237 disconnect() { 22238 this.cancel(); 22239 this._removeAllEventListeners(); 22240 if (this._domElement) { 22241 this._domElement.removeAttribute("data-camera-controls-version"); 22242 this._domElement = void 0; 22243 } 22244 } 22245 /** 22246 * Dispose the cameraControls instance itself, remove all eventListeners. 22247 * @category Methods 22248 */ 22249 dispose() { 22250 this.removeAllEventListeners(); 22251 this.disconnect(); 22252 } 22253 // it's okay to expose public though 22254 _getTargetDirection(out) { 22255 return out.setFromSpherical(this._spherical).divideScalar(this._spherical.radius).applyQuaternion(this._yAxisUpSpaceInverse); 22256 } 22257 // it's okay to expose public though 22258 _getCameraDirection(out) { 22259 return this._getTargetDirection(out).negate(); 22260 } 22261 _findPointerById(pointerId) { 22262 return this._activePointers.find((activePointer) => activePointer.pointerId === pointerId); 22263 } 22264 _findPointerByMouseButton(mouseButton) { 22265 return this._activePointers.find((activePointer) => activePointer.mouseButton === mouseButton); 22266 } 22267 _disposePointer(pointer) { 22268 this._activePointers.splice(this._activePointers.indexOf(pointer), 1); 22269 } 22270 _encloseToBoundary(position, offset, friction) { 22271 const offsetLength2 = offset.lengthSq(); 22272 if (offsetLength2 === 0) { 22273 return position; 22274 } 22275 const newTarget = _v3B.copy(offset).add(position); 22276 const clampedTarget = this._boundary.clampPoint(newTarget, _v3C); 22277 const deltaClampedTarget = clampedTarget.sub(newTarget); 22278 const deltaClampedTargetLength2 = deltaClampedTarget.lengthSq(); 22279 if (deltaClampedTargetLength2 === 0) { 22280 return position.add(offset); 22281 } else if (deltaClampedTargetLength2 === offsetLength2) { 22282 return position; 22283 } else if (friction === 0) { 22284 return position.add(offset).add(deltaClampedTarget); 22285 } else { 22286 const offsetFactor = 1 + friction * deltaClampedTargetLength2 / offset.dot(deltaClampedTarget); 22287 return position.add(_v3B.copy(offset).multiplyScalar(offsetFactor)).add(deltaClampedTarget.multiplyScalar(1 - friction)); 22288 } 22289 } 22290 _updateNearPlaneCorners() { 22291 if (isPerspectiveCamera(this._camera)) { 22292 const camera = this._camera; 22293 const near = camera.near; 22294 const fov = camera.getEffectiveFOV() * DEG2RAD; 22295 const heightHalf = Math.tan(fov * 0.5) * near; 22296 const widthHalf = heightHalf * camera.aspect; 22297 this._nearPlaneCorners[0].set(-widthHalf, -heightHalf, 0); 22298 this._nearPlaneCorners[1].set(widthHalf, -heightHalf, 0); 22299 this._nearPlaneCorners[2].set(widthHalf, heightHalf, 0); 22300 this._nearPlaneCorners[3].set(-widthHalf, heightHalf, 0); 22301 } else if (isOrthographicCamera(this._camera)) { 22302 const camera = this._camera; 22303 const zoomInv = 1 / camera.zoom; 22304 const left = camera.left * zoomInv; 22305 const right = camera.right * zoomInv; 22306 const top = camera.top * zoomInv; 22307 const bottom = camera.bottom * zoomInv; 22308 this._nearPlaneCorners[0].set(left, top, 0); 22309 this._nearPlaneCorners[1].set(right, top, 0); 22310 this._nearPlaneCorners[2].set(right, bottom, 0); 22311 this._nearPlaneCorners[3].set(left, bottom, 0); 22312 } 22313 } 22314 _truckInternal = (deltaX, deltaY, dragToOffset, screenSpacePanning) => { 22315 let truckX; 22316 let pedestalY; 22317 if (isPerspectiveCamera(this._camera)) { 22318 const offset = _v3A.copy(this._camera.position).sub(this._target); 22319 const fov = this._camera.getEffectiveFOV() * DEG2RAD; 22320 const targetDistance = offset.length() * Math.tan(fov * 0.5); 22321 truckX = this.truckSpeed * deltaX * targetDistance / this._elementRect.height; 22322 pedestalY = this.truckSpeed * deltaY * targetDistance / this._elementRect.height; 22323 } else if (isOrthographicCamera(this._camera)) { 22324 const camera = this._camera; 22325 truckX = this.truckSpeed * deltaX * (camera.right - camera.left) / camera.zoom / this._elementRect.width; 22326 pedestalY = this.truckSpeed * deltaY * (camera.top - camera.bottom) / camera.zoom / this._elementRect.height; 22327 } else { 22328 return; 22329 } 22330 if (screenSpacePanning) { 22331 dragToOffset ? this.setFocalOffset(this._focalOffsetEnd.x + truckX, this._focalOffsetEnd.y, this._focalOffsetEnd.z, true) : this.truck(truckX, 0, true); 22332 this.forward(-pedestalY, true); 22333 } else { 22334 dragToOffset ? this.setFocalOffset(this._focalOffsetEnd.x + truckX, this._focalOffsetEnd.y + pedestalY, this._focalOffsetEnd.z, true) : this.truck(truckX, pedestalY, true); 22335 } 22336 }; 22337 _rotateInternal = (deltaX, deltaY) => { 22338 const theta = PI_2 * this.azimuthRotateSpeed * deltaX / this._elementRect.height; 22339 const phi = PI_2 * this.polarRotateSpeed * deltaY / this._elementRect.height; 22340 this.rotate(theta, phi, true); 22341 }; 22342 _dollyInternal = (delta, x, y) => { 22343 const dollyScale = Math.pow(0.95, -delta * this.dollySpeed); 22344 const lastDistance = this._sphericalEnd.radius; 22345 const distance = this._sphericalEnd.radius * dollyScale; 22346 const clampedDistance = clamp(distance, this.minDistance, this.maxDistance); 22347 const overflowedDistance = clampedDistance - distance; 22348 if (this.infinityDolly && this.dollyToCursor) { 22349 this._dollyToNoClamp(distance, true); 22350 } else if (this.infinityDolly && !this.dollyToCursor) { 22351 this.dollyInFixed(overflowedDistance, true); 22352 this._dollyToNoClamp(clampedDistance, true); 22353 } else { 22354 this._dollyToNoClamp(clampedDistance, true); 22355 } 22356 if (this.dollyToCursor) { 22357 this._changedDolly += (this.infinityDolly ? distance : clampedDistance) - lastDistance; 22358 this._dollyControlCoord.set(x, y); 22359 } 22360 this._lastDollyDirection = Math.sign(-delta); 22361 }; 22362 _zoomInternal = (delta, x, y) => { 22363 const zoomScale = Math.pow(0.95, delta * this.dollySpeed); 22364 const lastZoom = this._zoom; 22365 const zoom = this._zoom * zoomScale; 22366 this.zoomTo(zoom, true); 22367 if (this.dollyToCursor) { 22368 this._changedZoom += zoom - lastZoom; 22369 this._dollyControlCoord.set(x, y); 22370 } 22371 }; 22372 // lateUpdate 22373 _collisionTest() { 22374 let distance = Infinity; 22375 const hasCollider = this.colliderMeshes.length >= 1; 22376 if (!hasCollider) return distance; 22377 if (notSupportedInOrthographicCamera(this._camera, "_collisionTest")) return distance; 22378 const rayDirection = this._getTargetDirection(_cameraDirection); 22379 _rotationMatrix.lookAt(_ORIGIN, rayDirection, this._camera.up); 22380 for (let i = 0; i < 4; i++) { 22381 const nearPlaneCorner = _v3B.copy(this._nearPlaneCorners[i]); 22382 nearPlaneCorner.applyMatrix4(_rotationMatrix); 22383 const origin = _v3C.addVectors(this._target, nearPlaneCorner); 22384 _raycaster.set(origin, rayDirection); 22385 _raycaster.far = this._spherical.radius + 1; 22386 const intersects = _raycaster.intersectObjects(this.colliderMeshes); 22387 if (intersects.length !== 0 && intersects[0].distance < distance) { 22388 distance = intersects[0].distance; 22389 } 22390 } 22391 return distance; 22392 } 22393 /** 22394 * Get its client rect and package into given `DOMRect` . 22395 */ 22396 _getClientRect(target) { 22397 if (!this._domElement) return; 22398 const rect = this._domElement.getBoundingClientRect(); 22399 target.x = rect.left; 22400 target.y = rect.top; 22401 if (this._viewport) { 22402 target.x += this._viewport.x; 22403 target.y += rect.height - this._viewport.w - this._viewport.y; 22404 target.width = this._viewport.z; 22405 target.height = this._viewport.w; 22406 } else { 22407 target.width = rect.width; 22408 target.height = rect.height; 22409 } 22410 return target; 22411 } 22412 _createOnRestPromise(resolveImmediately) { 22413 if (resolveImmediately) return Promise.resolve(); 22414 this._hasRested = false; 22415 this.dispatchEvent({ 22416 type: "transitionstart" 22417 }); 22418 return new Promise((resolve) => { 22419 const onResolve = () => { 22420 this.removeEventListener("rest", onResolve); 22421 resolve(); 22422 }; 22423 this.addEventListener("rest", onResolve); 22424 }); 22425 } 22426 // eslint-disable-next-line @typescript-eslint/no-unused-vars 22427 _addAllEventListeners(_domElement) { 22428 } 22429 _removeAllEventListeners() { 22430 } 22431 /** 22432 * backward compatible 22433 * @deprecated use smoothTime (in seconds) instead 22434 * @category Properties 22435 */ 22436 get dampingFactor() { 22437 console.warn(".dampingFactor has been deprecated. use smoothTime (in seconds) instead."); 22438 return 0; 22439 } 22440 /** 22441 * backward compatible 22442 * @deprecated use smoothTime (in seconds) instead 22443 * @category Properties 22444 */ 22445 set dampingFactor(_) { 22446 console.warn(".dampingFactor has been deprecated. use smoothTime (in seconds) instead."); 22447 } 22448 /** 22449 * backward compatible 22450 * @deprecated use draggingSmoothTime (in seconds) instead 22451 * @category Properties 22452 */ 22453 get draggingDampingFactor() { 22454 console.warn(".draggingDampingFactor has been deprecated. use draggingSmoothTime (in seconds) instead."); 22455 return 0; 22456 } 22457 /** 22458 * backward compatible 22459 * @deprecated use draggingSmoothTime (in seconds) instead 22460 * @category Properties 22461 */ 22462 set draggingDampingFactor(_) { 22463 console.warn(".draggingDampingFactor has been deprecated. use draggingSmoothTime (in seconds) instead."); 22464 } 22465 static createBoundingSphere(object3d, out = new THREE.Sphere()) { 22466 const boundingSphere = out; 22467 const center = boundingSphere.center; 22468 _box3A.makeEmpty(); 22469 object3d.traverseVisible((object) => { 22470 if (!object.isMesh) return; 22471 _box3A.expandByObject(object); 22472 }); 22473 _box3A.getCenter(center); 22474 let maxRadiusSq = 0; 22475 object3d.traverseVisible((object) => { 22476 if (!object.isMesh) return; 22477 const mesh = object; 22478 if (!mesh.geometry) return; 22479 const geometry = mesh.geometry.clone(); 22480 geometry.applyMatrix4(mesh.matrixWorld); 22481 const bufferGeometry = geometry; 22482 const position = bufferGeometry.attributes.position; 22483 for (let i = 0, l = position.count; i < l; i++) { 22484 _v3A.fromBufferAttribute(position, i); 22485 maxRadiusSq = Math.max(maxRadiusSq, center.distanceToSquared(_v3A)); 22486 } 22487 }); 22488 boundingSphere.radius = Math.sqrt(maxRadiusSq); 22489 return boundingSphere; 22490 } 22491}; 22492
22493// src/app/pages/campus-map-v3/utils/map-camera-controller.ts 22494CameraControls.install({ THREE: three_module_exports }); 22495var MapCameraController = class { 22496 constructor(config) { 22497 this.camera = config.camera; 22498 this.onDirty = config.onDirty; 22499 this.controls = new CameraControls(config.camera, config.renderer.domElement); 22500 this.configure(); 22501 } 22502 /** 22503 * Get the underlying CameraControls instance for advanced usage. 22504 */ 22505 get instance() { 22506 return this.controls; 22507 } 22508 configure() { 22509 this.controls.minZoom = 0.01; 22510 this.controls.maxZoom = 10; 22511 this.controls.dollyToCursor = true; 22512 this.controls.infinityDolly = false; 22513 this.controls.dollySpeed = 3; 22514 this.controls.smoothTime = 0.15; 22515 this.controls.draggingSmoothTime = 0.1; 22516 this.controls.mouseButtons.left = CameraControls.ACTION.TRUCK; 22517 this.controls.mouseButtons.middle = CameraControls.ACTION.ZOOM; 22518 this.controls.mouseButtons.right = CameraControls.ACTION.NONE; 22519 this.controls.mouseButtons.wheel = CameraControls.ACTION.ZOOM; 22520 this.controls.touches.one = CameraControls.ACTION.TOUCH_TRUCK; 22521 this.controls.touches.two = CameraControls.ACTION.NONE; 22522 this.controls.touches.three = CameraControls.ACTION.NONE; 22523 this.controls.polarAngle = 0; 22524 this.controls.minPolarAngle = 0; 22525 this.controls.maxPolarAngle = 0; 22526 this.controls.setLookAt(0, 0, 100, 0, 0, 0, false); 22527 this.controls.zoomTo(0.1, false); 22528 this.controls.addEventListener("update", () => { 22529 this.onDirty(); 22530 }); 22531 } 22532 /** 22533 * Update controls (call each frame). 22534 * @returns true if camera was updated 22535 */ 22536 update(delta) { 22537 return this.controls.update(delta); 22538 } 22539 /** 22540 * Fit camera to show the given bounds with optional padding. 22541 * @param bounds The area to fit 22542 * @param padding Padding as a ratio (0.1 = 10% padding) 22543 */ 22544 fitBounds(bounds, padding = 0.05) { 22545 const boundsWidth = bounds.maxX - bounds.minX; 22546 const boundsHeight = bounds.maxY - bounds.minY; 22547 const centerX = (bounds.minX + bounds.maxX) / 2; 22548 const centerY = (bounds.minY + bounds.maxY) / 2; 22549 const width = boundsWidth * (1 + padding * 2); 22550 const height = boundsHeight * (1 + padding * 2); 22551 if (width <= 0 || height <= 0) 22552 return; 22553 const aspect = (this.camera.right - this.camera.left) / Math.abs(this.camera.top - this.camera.bottom); 22554 const baseFrustumSize = 1e3; 22555 const zoomX = Math.sqrt(baseFrustumSize * aspect / width); 22556 const zoomY = Math.sqrt(baseFrustumSize / height); 22557 const zoom = Math.min(zoomX, zoomY); 22558 const clampedZoom = Math.max(0.01, Math.min(10, zoom)); 22559 this.controls.setLookAt(centerX, centerY, 100, centerX, centerY, 0, false); 22560 this.controls.zoomTo(clampedZoom, false); 22561 } 22562 /** 22563 * Pan camera to center on a world position without changing zoom. 22564 * @param x World X coordinate 22565 * @param y World Y coordinate 22566 * @param animate Whether to animate the transition (default: true) 22567 */ 22568 centerOn(x, y, animate = true) { 22569 const currentZoom = this.controls.camera.zoom; 22570 this.controls.setLookAt(x, y, 100, x, y, 0, animate); 22571 this.controls.zoomTo(currentZoom, animate); 22572 } 22573 /** 22574 * Dispose of resources. 22575 */ 22576 dispose() { 22577 this.controls.dispose(); 22578 } 22579}; 22580
22581// src/app/pages/campus-map-v3/utils/polygon-shape.ts 22582function createShapeFromCorners(coords) { 22583 if (coords.length < 3) 22584 return null; 22585 const centroid = { 22586 x: coords.reduce((sum, c) => sum + c.x, 0) / coords.length, 22587 y: coords.reduce((sum, c) => sum + c.y, 0) / coords.length 22588 }; 22589 const localCoords = coords.map((c) => ({ 22590 x: c.x - centroid.x, 22591 y: c.y - centroid.y 22592 })); 22593 let signedArea = 0; 22594 for (let i = 0; i < localCoords.length; i++) { 22595 const j = (i + 1) % localCoords.length; 22596 signedArea += localCoords[i].x * localCoords[j].y; 22597 signedArea -= localCoords[j].x * localCoords[i].y; 22598 } 22599 signedArea /= 2; 22600 const orderedCoords = signedArea > 0 ? [...localCoords].reverse() : localCoords; 22601 const shape = new Shape(); 22602 shape.moveTo(orderedCoords[0].x, orderedCoords[0].y); 22603 for (let i = 1; i < orderedCoords.length; i++) { 22604 shape.lineTo(orderedCoords[i].x, orderedCoords[i].y); 22605 } 22606 shape.closePath(); 22607 return { shape, centroid }; 22608} 22609function createPolygonOutline(coords, material, options) { 22610 if (coords.length < 3) 22611 return null; 22612 const points = coords.map((c) => new Vector3(c.x, c.y, options.zLayer)); 22613 points.push(points[0].clone()); 22614 const geometry = new BufferGeometry().setFromPoints(points); 22615 const line = new Line(geometry, material); 22616 if (options.dashed) { 22617 line.computeLineDistances(); 22618 } 22619 return line; 22620} 22621
22622// src/app/pages/campus-map-v3/utils/ground-patch-renderer.ts 22623var Z_SELECTION_HIGHLIGHT = 0; 22624var Z_GHOST = 0.5; 22625var GroundPatchRenderer = class { 22626 constructor(config) { 22627 this.highlightMeshes = /* @__PURE__ */ new Map(); 22628 this.ghostMeshes = /* @__PURE__ */ new Map(); 22629 this.materials = config.materials; 22630 this.onDirty = config.onDirty; 22631 this.group = new Group(); 22632 this.group.name = "ground-patches"; 22633 this.outlinesGroup = new Group(); 22634 this.outlinesGroup.name = "ground-patch-outlines"; 22635 this.highlightGroup = new Group(); 22636 this.highlightGroup.name = "ground-patch-highlights"; 22637 this.ghostGroup = new Group(); 22638 this.ghostGroup.name = "ground-patch-ghosts"; 22639 } 22640 /** 22641 * Render all ground patches and return their combined bounds. 22642 * @param patches Array of ground patch models to render 22643 * @returns Combined bounds of all patches, or null if no valid patches 22644 */ 22645 render(patches) { 22646 this.clear(); 22647 for (const patch of patches) { 22648 const mesh = this.createMesh(patch); 22649 if (mesh) { 22650 this.group.add(mesh); 22651 const outline = this.createOutline(patch); 22652 if (outline) { 22653 this.outlinesGroup.add(outline); 22654 } 22655 } 22656 } 22657 const bounds = this.calculateBounds(patches); 22658 this.onDirty(); 22659 return bounds; 22660 } 22661 /** 22662 * Clear all ground patch geometry. 22663 */ 22664 clear() { 22665 while (this.group.children.length > 0) { 22666 const child = this.group.children[0]; 22667 this.group.remove(child); 22668 if (child instanceof Mesh) { 22669 child.geometry.dispose(); 22670 } 22671 } 22672 while (this.outlinesGroup.children.length > 0) { 22673 const child = this.outlinesGroup.children[0]; 22674 this.outlinesGroup.remove(child); 22675 if (child instanceof Line) { 22676 child.geometry.dispose(); 22677 } 22678 } 22679 } 22680 // ========================================================================= 22681 // Selection Highlighting 22682 // ========================================================================= 22683 /** 22684 * Highlight a ground patch with a selection outline. 22685 * @param id Entity ID 22686 */ 22687 highlightEntity(id) { 22688 if (this.highlightMeshes.has(id)) 22689 return; 22690 let sourceMesh = null; 22691 this.group.traverse((obj) => { 22692 if (obj instanceof Mesh && obj.userData["entityId"] === id) { 22693 sourceMesh = obj; 22694 } 22695 });
22696 if (!sourceMesh) 22697 return; 22698 const highlight = this.createHighlight(sourceMesh); 22699 if (highlight) { 22700 highlight.name = `highlight-${id}`; 22701 highlight.userData["highlightFor"] = id; 22702 this.highlightGroup.add(highlight); 22703 this.highlightMeshes.set(id, highlight); 22704 this.onDirty(); 22705 } 22706 } 22707 /** 22708 * Remove highlight from a ground patch. 22709 * @param id Entity ID 22710 */ 22711 unhighlightEntity(id) { 22712 const highlight = this.highlightMeshes.get(id); 22713 if (!highlight) 22714 return; 22715 this.highlightGroup.remove(highlight); 22716 highlight.geometry.dispose(); 22717 this.highlightMeshes.delete(id); 22718 this.onDirty(); 22719 } 22720 /** 22721 * Clear all selection highlights. 22722 */ 22723 clearHighlights() { 22724 for (const [, highlight] of this.highlightMeshes) { 22725 this.highlightGroup.remove(highlight); 22726 highlight.geometry.dispose(); 22727 } 22728 this.highlightMeshes.clear(); 22729 this.onDirty(); 22730 } 22731 /** 22732 * Check if an entity is highlighted. 22733 */ 22734 isHighlighted(id) { 22735 return this.highlightMeshes.has(id); 22736 } 22737 /** 22738 * Create a highlight outline for a ground patch mesh. 22739 * Uses stored outline coordinates (world space) for accurate shape tracing. 22740 */ 22741 createHighlight(source) { 22742 const coords = source.userData["outlineCoords"]; 22743 if (!coords || coords.length < 3) 22744 return null; 22745 const material = this.materials.getSelectionOutlineMaterial(); 22746 return createPolygonOutline(coords, material, { 22747 zLayer: Z_SELECTION_HIGHLIGHT 22748 }); 22749 } 22750 // ========================================================================= 22751 // Ghost System (Drag Preview) 22752 // ========================================================================= 22753 /** 22754 * Create a ghost outline at entity's current position. 22755 * Used during drag to show original position. 22756 */ 22757 createGhost(id) { 22758 if (this.ghostMeshes.has(id)) 22759 return; 22760 const source = this.findEntityMesh(id); 22761 if (!source) 22762 return; 22763 const ghost = this.createGhostOutline(source); 22764 if (ghost) { 22765 ghost.name = `ghost-${id}`; 22766 ghost.userData["ghostFor"] = id; 22767 this.ghostGroup.add(ghost); 22768 this.ghostMeshes.set(id, ghost); 22769 this.onDirty(); 22770 } 22771 } 22772 /** 22773 * Remove ghost for an entity. 22774 */ 22775 removeGhost(id) { 22776 const ghost = this.ghostMeshes.get(id); 22777 if (!ghost) 22778 return; 22779 this.ghostGroup.remove(ghost); 22780 ghost.geometry.dispose(); 22781 this.ghostMeshes.delete(id); 22782 this.onDirty(); 22783 } 22784 /** 22785 * Clear all ghosts. 22786 */ 22787 clearGhosts() { 22788 for (const [, ghost] of this.ghostMeshes) { 22789 this.ghostGroup.remove(ghost); 22790 ghost.geometry.dispose(); 22791 } 22792 this.ghostMeshes.clear(); 22793 this.onDirty(); 22794 } 22795 /** 22796 * Create a dashed outline ghost for a ground patch. 22797 * Uses stored outline coordinates for accurate shape tracing. 22798 */ 22799 createGhostOutline(source) { 22800 const coords = source.userData["outlineCoords"]; 22801 if (!coords || coords.length < 3) 22802 return null; 22803 const material = this.materials.getGhostOutlineMaterial(); 22804 return createPolygonOutline(coords, material, { 22805 zLayer: Z_GHOST, 22806 dashed: true 22807 }); 22808 } 22809 // ========================================================================= 22810 // Mesh Movement (Drag Preview) 22811 // ========================================================================= 22812 /** 22813 * Find mesh by entity ID. 22814 */ 22815 findEntityMesh(id) { 22816 let found = null; 22817 this.group.traverse((obj) => { 22818 if (found) 22819 return; 22820 if (obj instanceof Mesh && obj.userData["entityId"] === id) { 22821 found = obj; 22822 } 22823 }); 22824 return found; 22825 } 22826 /** 22827 * Move an entity's mesh by delta for drag preview. 22828 * Does NOT modify entity data - visual only. 22829 */ 22830 moveEntityMesh(id, dx, dy) { 22831 const mesh = this.findEntityMesh(id); 22832 if (mesh) { 22833 mesh.position.x += dx; 22834 mesh.position.y += dy; 22835 } 22836 const highlight = this.highlightMeshes.get(id); 22837 if (highlight) { 22838 highlight.position.x += dx; 22839 highlight.position.y += dy; 22840 } 22841 this.onDirty(); 22842 } 22843 /** 22844 * Get current mesh position for an entity. 22845 */
22846 getEntityMeshPosition(id) { 22847 const mesh = this.findEntityMesh(id); 22848 if (!mesh) 22849 return null; 22850 return { x: mesh.position.x, y: mesh.position.y }; 22851 } 22852 /** 22853 * Rotate an entity's mesh around a pivot for rotation preview. 22854 * Does NOT modify entity data - visual only. 22855 */ 22856 rotateEntityMesh(id, angleDelta, pivot) { 22857 const mesh = this.findEntityMesh(id); 22858 if (mesh) { 22859 this.rotateMeshAroundPivot(mesh, angleDelta, pivot); 22860 } 22861 const highlight = this.highlightMeshes.get(id); 22862 if (highlight) { 22863 this.rotateMeshAroundPivot(highlight, angleDelta, pivot); 22864 } 22865 this.onDirty(); 22866 } 22867 /** 22868 * Rotate a mesh around a pivot point. 22869 */ 22870 rotateMeshAroundPivot(mesh, angleDelta, pivot) { 22871 const currentPos = { x: mesh.position.x, y: mesh.position.y }; 22872 const rotated = PointUtils.rotate(currentPos, angleDelta, pivot); 22873 mesh.position.x = rotated.x; 22874 mesh.position.y = rotated.y; 22875 mesh.rotation.z += angleDelta; 22876 } 22877 // ========================================================================= 22878 // Lifecycle 22879 // ========================================================================= 22880 /** 22881 * Dispose of all resources. 22882 */ 22883 dispose() { 22884 this.clear(); 22885 this.clearHighlights(); 22886 this.clearGhosts(); 22887 } 22888 /** 22889 * Create a mesh for a single ground patch. 22890 */ 22891 createMesh(patch) { 22892 const corners = patch.corners.filter((c) => !c.entity.isDeleted); 22893 if (corners.length < 3) 22894 return null; 22895 const coords = corners.map((c) => ({ 22896 x: c.entity.locationX || 0, 22897 y: c.entity.locationY || 0 22898 })); 22899 const shapeResult = createShapeFromCorners(coords); 22900 if (!shapeResult) 22901 return null; 22902 const { shape, centroid } = shapeResult; 22903 const geometry = new ShapeGeometry(shape); 22904 const patchType = patch.entity.type || "Grass"; 22905 const material = this.materials.getGroundPatchMaterial(patchType); 22906 const mesh = new Mesh(geometry, material); 22907 mesh.name = `ground-patch-${patch.entity.id}`; 22908 mesh.position.set(centroid.x, centroid.y, -1); 22909 mesh.userData["entityType"] = "groundPatch"; 22910 mesh.userData["entityId"] = patch.entity.id; 22911 mesh.userData["entityName"] = patchType; 22912 mesh.userData["patchId"] = patch.entity.id; 22913 mesh.userData["outlineCoords"] = coords; 22914 return mesh; 22915 } 22916 /** 22917 * Create an outline for a single ground patch (for wireframe mode). 22918 */ 22919 createOutline(patch) { 22920 const corners = patch.corners.filter((c) => !c.entity.isDeleted); 22921 if (corners.length < 3) 22922 return null; 22923 const coords = corners.map((c) => ({ 22924 x: c.entity.locationX || 0, 22925 y: c.entity.locationY || 0 22926 })); 22927 const material = this.materials.getGroundPatchOutlineMaterial(); 22928 const outline = createPolygonOutline(coords, material, { 22929 zLayer: -0.9 22930 // Slightly above fill at z=-1 22931 }); 22932 if (outline) { 22933 outline.name = `ground-patch-outline-${patch.entity.id}`; 22934 outline.userData["entityId"] = patch.entity.id; 22935 } 22936 return outline; 22937 } 22938 /** 22939 * Show/hide fill meshes for wireframe mode. 22940 */ 22941 setFillsVisible(visible) { 22942 this.group.traverse((obj) => { 22943 if (obj.name.startsWith("ground-patch-")) { 22944 obj.visible = visible; 22945 } 22946 }); 22947 this.onDirty(); 22948 } 22949 /** 22950 * Calculate combined bounds of all patches. 22951 */ 22952 calculateBounds(patches) { 22953 if (patches.length === 0) 22954 return null; 22955 let minX = Infinity; 22956 let maxX = -Infinity; 22957 let minY = Infinity; 22958 let maxY = -Infinity; 22959 for (const patch of patches) { 22960 for (const corner of patch.corners) { 22961 if (corner.entity.isDeleted) 22962 continue; 22963 const x = corner.entity.locationX || 0; 22964 const y = corner.entity.locationY || 0; 22965 minX = Math.min(minX, x); 22966 maxX = Math.max(maxX, x); 22967 minY = Math.min(minY, y); 22968 maxY = Math.max(maxY, y); 22969 } 22970 } 22971 if (!isFinite(minX) || !isFinite(maxX)) 22972 return null; 22973 return { minX, maxX, minY, maxY }; 22974 } 22975}; 22976
22977// src/app/pages/campus-map-v3/utils/renderers/z-layers.ts 22978var Z_LAYERS = { 22979 GROUND_REFERENCE: -1.5, 22980 FLOOR_BACKGROUND: -1, 22981 FLOOR_FILL: -0.5, 22982 ROOM_FILL: 1, 22983 ROOM_OUTLINE: 1.5, 22984 ROOM_LABEL: 2, 22985 FLOOR_OUTLINE: 2.5, 22986 CONNECTION_LINE: 3, 22987 NODE: 4, 22988 SELECTION_HIGHLIGHT: 5, 22989 DESTINATION_HIGHLIGHT: 5.5, 22990 GHOST: 6, 22991 POLYGON_PREVIEW: 7, 22992 MIDPOINT_HANDLES: 7.5, 22993 CORNER_HANDLES: 8, 22994 WALL_LENGTH: 8.5, 22995 ROTATION_HANDLE: 9, 22996 ROTATION_PIVOT: 9.5 22997}; 22998var NODE_RADIUS = 30; 22999
23000// src/app/pages/campus-map-v3/utils/renderers/floor-room-renderer.ts 23001var FloorRoomRenderer = class { 23002 constructor(config) { 23003 this.materials = config.materials; 23004 this.onDirty = config.onDirty; 23005 this.floorsGroup = new Group(); 23006 this.floorsGroup.name = "building-floors"; 23007 this.groundReferenceGroup = new Group(); 23008 this.groundReferenceGroup.name = "ground-reference"; 23009 } 23010 // ========================================================================= 23011 // Floor/Room Rendering 23012 // ========================================================================= 23013 /** 23014 * Render all floors. 23015 * @param floors Array of floor models to render 23016 */ 23017 renderFloors(floors) { 23018 this.clearFloors(); 23019 for (const floor of floors) { 23020 const floorGroup = this.renderFloor(floor); 23021 if (floorGroup) { 23022 this.floorsGroup.add(floorGroup); 23023 } 23024 } 23025 this.onDirty(); 23026 } 23027 /** 23028 * Clear all floor/room geometry. 23029 */ 23030 clearFloors() { 23031 while (this.floorsGroup.children.length > 0) { 23032 const child = this.floorsGroup.children[0]; 23033 this.floorsGroup.remove(child); 23034 child.traverse((obj) => { 23035 if (obj instanceof Mesh || obj instanceof Line) { 23036 obj.geometry.dispose(); 23037 } 23038 }); 23039 } 23040 } 23041 // ========================================================================= 23042 // Visibility Controls 23043 // ========================================================================= 23044 /** 23045 * Show/hide floor outlines (building footprints). 23046 */ 23047 setBuildingsVisible(visible) { 23048 this.floorsGroup.traverse((obj) => { 23049 if (obj.name.startsWith("floor-outline-")) { 23050 obj.visible = visible; 23051 } 23052 }); 23053 this.onDirty(); 23054 } 23055 /** 23056 * Check if buildings (floor outlines) are visible. 23057 */ 23058 get buildingsVisible() { 23059 let visible = true; 23060 this.floorsGroup.traverse((obj) => { 23061 if (obj.name.startsWith("floor-outline-")) { 23062 visible = obj.visible; 23063 } 23064 }); 23065 return visible; 23066 } 23067 /** 23068 * Show/hide rooms (fill + outline). 23069 */ 23070 setRoomsVisible(visible) { 23071 this.floorsGroup.traverse((obj) => { 23072 if (obj.name.startsWith("room-fill-") || obj.name.startsWith("room-outline-")) { 23073 obj.visible = visible; 23074 } 23075 }); 23076 this.onDirty(); 23077 } 23078 /** 23079 * Check if rooms are visible. 23080 */ 23081 get roomsVisible() { 23082 let visible = true; 23083 this.floorsGroup.traverse((obj) => { 23084 if (obj.name.startsWith("room-fill-")) { 23085 visible = obj.visible; 23086 } 23087 }); 23088 return visible; 23089 } 23090 /** 23091 * Show/hide fill meshes (floors and rooms) for wireframe mode. 23092 */ 23093 setFillsVisible(visible) { 23094 this.floorsGroup.traverse((obj) => { 23095 if (obj.name.startsWith("floor-fill-") || obj.name.startsWith("room-fill-")) { 23096 obj.visible = visible; 23097 } 23098 }); 23099 this.onDirty(); 23100 } 23101 // ========================================================================= 23102 // Ground Reference (Floor Outlines from Ground Level) 23103 // ========================================================================= 23104 /** 23105 * Render ground floor outlines as a reference layer. 23106 * Shows building footprints at 50% opacity when viewing upper floors. 23107 * @param floors Array of ground floor models to render as reference 23108 */ 23109 renderGroundReference(floors) { 23110 this.clearGroundReference(); 23111 for (const floor of floors) { 23112 const outline = this.createGroundReferenceOutline(floor); 23113 if (outline) { 23114 this.groundReferenceGroup.add(outline); 23115 } 23116 } 23117 this.onDirty(); 23118 } 23119 /** 23120 * Clear all ground reference geometry. 23121 */ 23122 clearGroundReference() { 23123 while (this.groundReferenceGroup.children.length > 0) { 23124 const child = this.groundReferenceGroup.children[0]; 23125 this.groundReferenceGroup.remove(child); 23126 if (child instanceof Line) { 23127 child.geometry.dispose(); 23128 } 23129 } 23130 this.onDirty(); 23131 } 23132 /** 23133 * Set visibility of the ground reference layer. 23134 */ 23135 setGroundReferenceVisible(visible) { 23136 this.groundReferenceGroup.visible = visible; 23137 this.onDirty(); 23138 } 23139 // ========================================================================= 23140 // Floor Detection (for node placement) 23141 // ========================================================================= 23142 /** 23143 * Find the floor at a given world position. 23144 * Returns the floor ID if the point is inside a floor polygon, null otherwise. 23145 */ 23146 getFloorAtPosition(localX, localY) { 23147 let foundFloorId = null; 23148 this.floorsGroup.traverse((obj) => { 23149 if (foundFloorId) 23150 return; 23151 if (obj instanceof Mesh && obj.name.startsWith("floor-fill-")) { 23152 const coords = obj.userData["outlineCoords"]; 23153 const floorId = obj.userData["entityId"]; 23154 if (coords && floorId && this.isPointInPolygon(localX, localY, coords)) { 23155 foundFloorId = floorId; 23156 } 23157 } 23158 }); 23159 return foundFloorId; 23160 } 23161 /** 23162 * Find a mesh by entity ID (for floors and rooms). 23163 */ 23164 findEntityMesh(id) { 23165 let found = null; 23166 this.floorsGroup.traverse((obj) => { 23167 if (found) 23168 return; 23169 if ((obj instanceof Mesh || obj instanceof Line) && obj.userData["entityId"] === id) { 23170 found = obj; 23171 } 23172 }); 23173 return found; 23174 } 23175 // ========================================================================= 23176 // Lifecycle 23177 // ========================================================================= 23178 /** 23179 * Dispose of all resources. 23180 */ 23181 dispose() { 23182 this.clearFloors(); 23183 this.clearGroundReference(); 23184 } 23185 // ========================================================================= 23186 // Private: Floor Rendering 23187 // ========================================================================= 23188 /** 23189 * Render a single floor with its rooms. 23190 */ 23191 renderFloor(floor) { 23192 const group = new Group(); 23193 group.position.x = 0; 23194 group.position.y = 0; 23195 group.name = `building-floor-${floor.entity.id}`; 23196 this.renderFloorOutline(floor, group); 23197 if (floor.rooms && floor.rooms.length > 0) { 23198 for (const room of floor.rooms) { 23199 if (room.entity.isDeleted) 23200 continue; 23201 this.renderRoom(room, group); 23202 } 23203 } 23204 return group; 23205 } 23206 /** 23207 * Render floor outline polygon. 23208 */ 23209 renderFloorOutline(floor, group) { 23210 var _a3; 23211 const corners = [...floor.corners].filter((c) => !c.entity.isDeleted).sort((a, b) => { 23212 var _a4, _b2;
23213 return ((_a4 = a.entity.position) != null ? _a4 : 0) - ((_b2 = b.entity.position) != null ? _b2 : 0); 23214 }); 23215 if (corners.length < 3) 23216 return; 23217 const coords = corners.map((c) => ({ 23218 x: c.entity.locationX || 0, 23219 y: c.entity.locationY || 0 23220 })); 23221 const shapeResult = createShapeFromCorners(coords); 23222 if (!shapeResult) 23223 return; 23224 const { shape, centroid } = shapeResult; 23225 const geometry = new ShapeGeometry(shape, 1); 23226 const material = this.materials.getFloorFillMaterial(); 23227 const mesh = new Mesh(geometry, material); 23228 mesh.position.set(centroid.x, centroid.y, Z_LAYERS.FLOOR_FILL); 23229 mesh.name = `floor-fill-${floor.entity.id}`; 23230 mesh.userData["entityType"] = "floor"; 23231 mesh.userData["entityId"] = floor.entity.id; 23232 const buildingName = (_a3 = floor.building) == null ? void 0 : _a3.entity.name; 23233 const floorLabel = floor.entity.name || (floor.entity.level === 1 ? "Level G" : `Level ${floor.entity.level}`); 23234 mesh.userData["entityName"] = buildingName ? `${floorLabel}, ${buildingName}` : floorLabel; 23235 mesh.userData["floorId"] = floor.entity.id; 23236 mesh.userData["outlineCoords"] = coords; 23237 group.add(mesh); 23238 const outlinePoints = coords.map((c) => new Vector3(c.x, c.y, Z_LAYERS.FLOOR_OUTLINE)); 23239 outlinePoints.push(outlinePoints[0].clone()); 23240 const outlineGeometry = new BufferGeometry().setFromPoints(outlinePoints); 23241 const outlineMaterial = this.materials.getFloorOutlineMaterial(); 23242 const outline = new Line(outlineGeometry, outlineMaterial); 23243 outline.name = `floor-outline-${floor.entity.id}`; 23244 group.add(outline); 23245 } 23246 /** 23247 * Render a room with type-based color and outline. 23248 */ 23249 renderRoom(room, group) { 23250 var _a3, _b2; 23251 const corners = [...room.corners].filter((c) => !c.entity.isDeleted).sort((a, b) => { 23252 var _a4, _b3; 23253 return ((_a4 = a.entity.position) != null ? _a4 : 0) - ((_b3 = b.entity.position) != null ? _b3 : 0); 23254 }); 23255 if (corners.length < 3) 23256 return; 23257 const coords = corners.map((c) => ({ 23258 x: c.entity.locationX || 0, 23259 y: c.entity.locationY || 0 23260 })); 23261 const shapeResult = createShapeFromCorners(coords); 23262 if (!shapeResult) 23263 return; 23264 const { shape, centroid } = shapeResult; 23265 const geometry = new ShapeGeometry(shape, 1); 23266 const material = this.materials.getRoomFillMaterial((_a3 = room.entity.roomType) != null ? _a3 : "Regular"); 23267 const mesh = new Mesh(geometry, material); 23268 mesh.position.set(centroid.x, centroid.y, Z_LAYERS.ROOM_FILL); 23269 mesh.name = `room-fill-${room.entity.id}`; 23270 mesh.userData["entityType"] = "room"; 23271 mesh.userData["entityId"] = room.entity.id; 23272 mesh.userData["entityName"] = (_b2 = room.entity.name) != null ? _b2 : room.entity.number; 23273 mesh.userData["roomId"] = room.entity.id; 23274 mesh.userData["outlineCoords"] = coords; 23275 group.add(mesh); 23276 const outlinePoints = coords.map((c) => new Vector3(c.x, c.y, Z_LAYERS.ROOM_OUTLINE)); 23277 outlinePoints.push(outlinePoints[0].clone()); 23278 const outlineGeometry = new BufferGeometry().setFromPoints(outlinePoints); 23279 const outlineMaterial = this.materials.getRoomOutlineMaterial(); 23280 const outline = new Line(outlineGeometry, outlineMaterial); 23281 outline.name = `room-outline-${room.entity.id}`; 23282 group.add(outline); 23283 } 23284 /** 23285 * Create a floor outline for the ground reference layer. 23286 */ 23287 createGroundReferenceOutline(floor) { 23288 const corners = [...floor.corners].filter((c) => !c.entity.isDeleted).sort((a, b) => { 23289 var _a3, _b2; 23290 return ((_a3 = a.entity.position) != null ? _a3 : 0) - ((_b2 = b.entity.position) != null ? _b2 : 0); 23291 }); 23292 if (corners.length < 3) 23293 return null; 23294 const coords = corners.map((c) => ({ 23295 x: c.entity.locationX || 0, 23296 y: c.entity.locationY || 0 23297 })); 23298 const points = coords.map((c) => new Vector3(c.x, c.y, Z_LAYERS.GROUND_REFERENCE)); 23299 points.push(points[0].clone()); 23300 const geometry = new BufferGeometry().setFromPoints(points); 23301 const material = this.materials.getGroundReferenceOutlineMaterial(); 23302 const line = new Line(geometry, material); 23303 line.name = `ground-ref-${floor.entity.id}`; 23304 return line; 23305 } 23306 /** 23307 * Check if a point is inside a polygon using ray casting algorithm. 23308 * @param x Point X coordinate 23309 * @param y Point Y coordinate 23310 * @param polygon Array of polygon vertices 23311 */ 23312 isPointInPolygon(x, y, polygon) { 23313 let inside = false;
23314 const n = polygon.length; 23315 for (let i = 0, j = n - 1; i < n; j = i++) { 23316 const xi = polygon[i].x; 23317 const yi = polygon[i].y; 23318 const xj = polygon[j].x; 23319 const yj = polygon[j].y; 23320 const intersect = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi; 23321 if (intersect) { 23322 inside = !inside; 23323 } 23324 } 23325 return inside; 23326 } 23327}; 23328
23329// src/app/pages/campus-map-v3/utils/renderers/node-connection-renderer.ts 23330var NODE_TARGET_DIAMETER_PX = 16; 23331var FRUSTUM_BASE = 1e3; 23332var NODE_MAX_SCALE = 10; 23333var NodeConnectionRenderer = class { 23334 constructor(config) { 23335 this.pendingConnectionLine = null; 23336 this.nodeMeshes = /* @__PURE__ */ new Map(); 23337 this.nodeScales = /* @__PURE__ */ new Map(); 23338 this.materials = config.materials; 23339 this.onDirty = config.onDirty; 23340 this.getCameraZoom = config.getCameraZoom; 23341 this.nodesGroup = new Group(); 23342 this.nodesGroup.name = "nodes"; 23343 this.nodesGroup.visible = false; 23344 } 23345 // ========================================================================= 23346 // Node/Connection Rendering 23347 // ========================================================================= 23348 /** 23349 * Render nodes and their connections. 23350 * @param nodes Array of node models to render 23351 * @param connections Array of connection models (will be filtered to only show connections between provided nodes) 23352 */ 23353 renderNodes(nodes, connections) { 23354 this.clearNodes(); 23355 const nodeMap = new Map(nodes.filter((n) => n.entity.id !== void 0).map((n) => [n.entity.id, n])); 23356 const visibleConnections = connections.filter((conn) => { 23357 const nodeA = conn.entity.nodeAId ? nodeMap.get(conn.entity.nodeAId) : void 0; 23358 const nodeB = conn.entity.nodeBId ? nodeMap.get(conn.entity.nodeBId) : void 0; 23359 return nodeA && nodeB; 23360 }); 23361 this.renderConnections(visibleConnections, nodeMap); 23362 for (const node of nodes) { 23363 this.renderNode(node); 23364 } 23365 this.onDirty(); 23366 } 23367 /** 23368 * Clear all node/connection geometry. 23369 */ 23370 clearNodes() { 23371 while (this.nodesGroup.children.length > 0) { 23372 const child = this.nodesGroup.children[0]; 23373 this.nodesGroup.remove(child); 23374 if (child instanceof Mesh || child instanceof Line) { 23375 child.geometry.dispose(); 23376 } 23377 } 23378 this.nodeMeshes.clear(); 23379 } 23380 /** 23381 * Update node scales to maintain constant screen-space size. 23382 * Called when camera zoom changes. 23383 * @param canvasHeight Canvas height in CSS pixels 23384 */ 23385 updateNodesScreenSpaceScale(canvasHeight) { 23386 const zoom = this.getCameraZoom(); 23387 const scale = this.calculateScreenSpaceScale(zoom, canvasHeight); 23388 for (const [entityId, mesh] of this.nodeMeshes) { 23389 mesh.scale.set(scale, scale, 1); 23390 this.nodeScales.set(entityId, scale); 23391 } 23392 } 23393 /** 23394 * Get current scales for all node meshes. 23395 * Used by highlight renderer to maintain proportional sizing. 23396 * @returns Map of entity ID to scale factor 23397 */ 23398 getNodeScales() { 23399 return this.nodeScales; 23400 } 23401 /** 23402 * Update a single node's scale. 23403 * @param mesh Node mesh to update 23404 */ 23405 updateNodeScale(mesh) { 23406 const zoom = this.getCameraZoom(); 23407 const canvasHeight = 800; 23408 const scale = this.calculateScreenSpaceScale(zoom, canvasHeight); 23409 mesh.scale.set(scale, scale, 1); 23410 } 23411 /** 23412 * Calculate the scale factor needed to maintain constant pixel size. 23413 * @param zoom Current camera zoom 23414 * @param canvasHeight Canvas height in CSS pixels 23415 * @returns Scale factor to apply to node mesh 23416 */ 23417 calculateScreenSpaceScale(zoom, canvasHeight) { 23418 const currentPixelDiameter = NODE_RADIUS * 2 * zoom * canvasHeight / FRUSTUM_BASE; 23419 if (currentPixelDiameter === 0) 23420 return 1; 23421 const scale = NODE_TARGET_DIAMETER_PX / currentPixelDiameter; 23422 return Math.min(scale, NODE_MAX_SCALE); 23423 } 23424 /** 23425 * Show/hide nodes layer. 23426 */ 23427 setNodesVisible(visible) { 23428 this.nodesGroup.visible = visible; 23429 this.onDirty(); 23430 } 23431 /** 23432 * Check if nodes are visible. 23433 */ 23434 get nodesVisible() { 23435 return this.nodesGroup.visible; 23436 } 23437 // ========================================================================= 23438 // Pending Connection Line (Connect Mode Preview) 23439 // ========================================================================= 23440 /** 23441 * Render a pending connection line from source position to cursor. 23442 * Used in connect mode to show preview of connection being created. 23443 * @param validTarget If true, renders with green "valid target" color 23444 */ 23445 renderPendingConnection(fromX, fromY, toX, toY, validTarget = false) { 23446 this.clearPendingConnection(); 23447 const points = [ 23448 new Vector3(fromX, fromY, Z_LAYERS.GHOST), 23449 new Vector3(toX, toY, Z_LAYERS.GHOST) 23450 ]; 23451 const geometry = new BufferGeometry().setFromPoints(points); 23452 const material = validTarget ? this.materials.getValidConnectionMaterial() : this.materials.getGhostOutlineMaterial(); 23453 this.pendingConnectionLine = new Line(geometry, material); 23454 this.pendingConnectionLine.name = "pending-connection"; 23455 if (!validTarget) { 23456 this.pendingConnectionLine.computeLineDistances(); 23457 } 23458 this.nodesGroup.add(this.pendingConnectionLine); 23459 this.onDirty(); 23460 } 23461 /** 23462 * Clear the pending connection preview line. 23463 */ 23464 clearPendingConnection() { 23465 if (this.pendingConnectionLine) { 23466 this.nodesGroup.remove(this.pendingConnectionLine); 23467 this.pendingConnectionLine.geometry.dispose(); 23468 this.pendingConnectionLine = null; 23469 this.onDirty(); 23470 } 23471 } 23472 /** 23473 * Find a mesh/line by entity ID (for nodes and connections). 23474 */ 23475 findEntityMesh(id) { 23476 let found = null; 23477 this.nodesGroup.traverse((obj) => { 23478 if (found) 23479 return; 23480 if ((obj instanceof Mesh || obj instanceof Line) && obj.userData["entityId"] === id) { 23481 found = obj; 23482 } 23483 }); 23484 return found; 23485 } 23486 // ========================================================================= 23487 // Lifecycle 23488 // ========================================================================= 23489 /** 23490 * Dispose of all resources. 23491 */ 23492 dispose() { 23493 this.clearNodes(); 23494 this.clearPendingConnection(); 23495 } 23496 // ========================================================================= 23497 // Private: Node Rendering 23498 // ========================================================================= 23499 /** 23500 * Render a single node as a perfect circle using SDF shader. 23501 */ 23502 renderNode(node) { 23503 var _a3, _b2, _c, _d; 23504 const x = (_a3 = node.entity.locationX) != null ? _a3 : 0; 23505 const y = (_b2 = node.entity.locationY) != null ? _b2 : 0; 23506 const nodeType = (_c = node.entity.type) != null ? _c : "Regular"; 23507 const size = NODE_RADIUS * 2; 23508 const geometry = new PlaneGeometry(size, size); 23509 const material = this.materials.getNodeCircleMaterial(nodeType, node.isCampusEntrance); 23510 const mesh = new Mesh(geometry, material); 23511 mesh.position.set(x, y, Z_LAYERS.NODE); 23512 mesh.name = `node-${node.entity.id}`; 23513 mesh.userData["entityType"] = "node"; 23514 mesh.userData["entityId"] = node.entity.id; 23515 mesh.userData["entityName"] = (_d = node.entity.tagName) != null ? _d : nodeType; 23516 mesh.userData["nodeId"] = node.entity.id; 23517 if (node.entity.id) { 23518 this.nodeMeshes.set(node.entity.id, mesh); 23519 } 23520 this.nodesGroup.add(mesh); 23521 } 23522 /** 23523 * Render connections between nodes as lines. 23524 */ 23525 renderConnections(connections, nodeMap) { 23526 var _a3, _b2, _c, _d; 23527 const lineMaterial = this.materials.getConnectionMaterial(); 23528 for (const conn of connections) { 23529 const nodeA = conn.entity.nodeAId ? nodeMap.get(conn.entity.nodeAId) : void 0; 23530 const nodeB = conn.entity.nodeBId ? nodeMap.get(conn.entity.nodeBId) : void 0; 23531 if (!nodeA || !nodeB) 23532 continue; 23533 const points = [ 23534 new Vector3((_a3 = nodeA.entity.locationX) != null ? _a3 : 0, (_b2 = nodeA.entity.locationY) != null ? _b2 : 0, Z_LAYERS.CONNECTION_LINE), 23535 new Vector3((_c = nodeB.entity.locationX) != null ? _c : 0, (_d = nodeB.entity.locationY) != null ? _d : 0, Z_LAYERS.CONNECTION_LINE) 23536 ]; 23537 const geometry = new BufferGeometry().setFromPoints(points); 23538 const line = new Line(geometry, lineMaterial); 23539 line.name = `connection-${conn.entity.id}`; 23540 line.userData["entityType"] = "nodeConnection"; 23541 line.userData["entityId"] = conn.entity.id; 23542 line.userData["connectionId"] = conn.entity.id; 23543 this.nodesGroup.add(line); 23544 } 23545 } 23546}; 23547
23548// src/app/pages/campus-map-v3/utils/room-label-utils.ts 23549var MIN_ROOM_AREA_FOR_LABEL = 1e4; 23550function calculateOBB(coords) { 23551 if (coords.length < 3) 23552 return null; 23553 let signedArea = 0; 23554 let cx = 0; 23555 let cy = 0; 23556 for (let i = 0; i < coords.length; i++) { 23557 const j = (i + 1) % coords.length; 23558 const cross = coords[i].x * coords[j].y - coords[j].x * coords[i].y; 23559 signedArea += cross; 23560 cx += (coords[i].x + coords[j].x) * cross; 23561 cy += (coords[i].y + coords[j].y) * cross; 23562 } 23563 signedArea /= 2; 23564 const area = Math.abs(signedArea); 23565 if (area < 1) 23566 return null; 23567 const centroid = { 23568 x: cx / (6 * signedArea), 23569 y: cy / (6 * signedArea) 23570 }; 23571 let cxx = 0, cxy = 0, cyy = 0; 23572 for (const p of coords) { 23573 const dx = p.x - centroid.x; 23574 const dy = p.y - centroid.y; 23575 cxx += dx * dx; 23576 cxy += dx * dy; 23577 cyy += dy * dy; 23578 } 23579 cxx /= coords.length; 23580 cxy /= coords.length; 23581 cyy /= coords.length; 23582 const trace = cxx + cyy; 23583 const det = cxx * cyy - cxy * cxy; 23584 const discriminant = Math.sqrt(Math.max(0, trace * trace / 4 - det)); 23585 const lambda1 = trace / 2 + discriminant; 23586 const lambda2 = trace / 2 - discriminant; 23587 let principalAxis; 23588 if (Math.abs(cxy) < 1e-10) { 23589 principalAxis = cxx >= cyy ? { x: 1, y: 0 } : { x: 0, y: 1 }; 23590 } else { 23591 const vx = lambda1 - cyy; 23592 const vy = cxy; 23593 const len = Math.sqrt(vx * vx + vy * vy); 23594 principalAxis = { x: vx / len, y: vy / len }; 23595 } 23596 const secondaryAxis = { x: -principalAxis.y, y: principalAxis.x }; 23597 let minPrimary = Infinity, maxPrimary = -Infinity; 23598 let minSecondary = Infinity, maxSecondary = -Infinity; 23599 for (const p of coords) { 23600 const dx = p.x - centroid.x; 23601 const dy = p.y - centroid.y; 23602 const projPrimary = dx * principalAxis.x + dy * principalAxis.y; 23603 const projSecondary = dx * secondaryAxis.x + dy * secondaryAxis.y; 23604 minPrimary = Math.min(minPrimary, projPrimary); 23605 maxPrimary = Math.max(maxPrimary, projPrimary); 23606 minSecondary = Math.min(minSecondary, projSecondary); 23607 maxSecondary = Math.max(maxSecondary, projSecondary); 23608 } 23609 const width = maxPrimary - minPrimary; 23610 const height = maxSecondary - minSecondary; 23611 let rotation = Math.atan2(principalAxis.y, principalAxis.x); 23612 if (rotation > Math.PI / 2) { 23613 rotation -= Math.PI; 23614 } else if (rotation < -Math.PI / 2) { 23615 rotation += Math.PI; 23616 } 23617 return { 23618 center: centroid, 23619 width, 23620 height, 23621 rotation, 23622 area 23623 }; 23624} 23625function getRoomTypeIcon(roomType) { 23626 switch (roomType) { 23627 case "Restroom": 23628 return "\u{1F6BB}"; 23629 case "Elevator": 23630 return "\u{1F6D7}"; 23631 case "Stairs": 23632 return "\u{1FA9C}"; 23633 case "Emergency": 23634 return "\u{1F6A8}"; 23635 case "Information": 23636 return "\u2139\uFE0F"; 23637 case "Cafeteria": 23638 return "\u{1F37D}\uFE0F"; 23639 default: 23640 return null; 23641 } 23642} 23643function shouldShowLabel(obb) { 23644 return obb.area >= MIN_ROOM_AREA_FOR_LABEL; 23645} 23646var FRUSTUM_BASE2 = 1e3; 23647var LABEL_DENSITY_THRESHOLDS = { 23648 /** Minimum px/cm to show room numbers (medium detail) */ 23649 SHOW_NUMBER: 0.32, 23650 /** Minimum px/cm to show room names (full detail) */ 23651 SHOW_NAME: 0.96 23652}; 23653var LABEL_SCALE = { 23654 /** Zoom level at which labels appear at base size */ 23655 REFERENCE_ZOOM: 1, 23656 /** Minimum scale factor (prevents illegibly small labels) */ 23657 MIN_SCALE: 0.7, 23658 /** Maximum scale factor (prevents oversized labels) */ 23659 MAX_SCALE: 2.5 23660}; 23661function getLabelDetailLevel(zoom, canvasHeight) { 23662 const pixelsPerCm = zoom * canvasHeight / FRUSTUM_BASE2; 23663 if (pixelsPerCm < LABEL_DENSITY_THRESHOLDS.SHOW_NUMBER) { 23664 return "icons"; 23665 } else if (pixelsPerCm < LABEL_DENSITY_THRESHOLDS.SHOW_NAME) { 23666 return "numbers"; 23667 } 23668 return "full"; 23669} 23670function getLabelScale(zoom) { 23671 const linearScale = zoom / LABEL_SCALE.REFERENCE_ZOOM; 23672 return Math.max(LABEL_SCALE.MIN_SCALE, Math.min(LABEL_SCALE.MAX_SCALE, linearScale)); 23673} 23674function createRoomLabelElement(config) { 23675 var _a3, _b2, _c; 23676 const container = document.createElement("div"); 23677 container.className = "room-label"; 23678 container.style.cssText = ` 23679 display: flex; 23680 flex-direction: column; 23681 align-items: center; 23682 justify-content: center; 23683 pointer-events: none; 23684 text-align: center; 23685 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 23686 transform: translate(-50%, -50%) rotate(${config.rotation}deg); 23687 transform-origin: center center; 23688 user-select: none; 23689 white-space: nowrap; 23690 `; 23691 const iconEl = document.createElement("div"); 23692 iconEl.className = "room-label-icon"; 23693 iconEl.textContent = (_a3 = config.icon) != null ? _a3 : ""; 23694 iconEl.style.cssText = `
23695 font-size: 14px; 23696 line-height: 1; 23697 display: ${config.icon ? "block" : "none"}; 23698 `; 23699 container.appendChild(iconEl); 23700 const numberEl = document.createElement("div"); 23701 numberEl.className = "room-label-number"; 23702 numberEl.textContent = (_b2 = config.number) != null ? _b2 : ""; 23703 numberEl.style.cssText = ` 23704 font-size: 12px; 23705 font-weight: 600; 23706 color: #333; 23707 line-height: 1.2; 23708 display: ${config.number ? "block" : "none"}; 23709 `; 23710 container.appendChild(numberEl); 23711 const nameEl = document.createElement("div"); 23712 nameEl.className = "room-label-name"; 23713 const name = (_c = config.name) != null ? _c : ""; 23714 if (name.length > 25) { 23715 const mid = Math.floor(name.length / 2); 23716 let splitIndex = name.lastIndexOf(" ", mid); 23717 if (splitIndex === -1 || splitIndex < 10) { 23718 splitIndex = name.indexOf(" ", mid); 23719 } 23720 if (splitIndex !== -1 && splitIndex > 5 && splitIndex < name.length - 5) { 23721 const line1 = name.substring(0, splitIndex); 23722 const line2 = name.substring(splitIndex + 1); 23723 nameEl.innerHTML = `${line1}<br>${line2}`; 23724 } else { 23725 nameEl.textContent = name; 23726 } 23727 } else { 23728 nameEl.textContent = name; 23729 } 23730 nameEl.style.cssText = ` 23731 font-size: 11px; 23732 font-weight: 400; 23733 color: #555; 23734 line-height: 1.2; 23735 max-width: ${config.maxWidth}px; 23736 overflow: hidden; 23737 text-overflow: ellipsis; 23738 white-space: ${name.length > 25 ? "normal" : "nowrap"}; 23739 display: ${config.name ? "block" : "none"}; 23740 `; 23741 container.appendChild(nameEl); 23742 return container; 23743} 23744function updateLabel(element, level, scale, rotationDeg) { 23745 const iconEl = element.querySelector(".room-label-icon"); 23746 const numberEl = element.querySelector(".room-label-number"); 23747 const nameEl = element.querySelector(".room-label-name"); 23748 if (iconEl && iconEl.textContent) { 23749 iconEl.style.display = "block"; 23750 } 23751 if (numberEl && numberEl.textContent) { 23752 numberEl.style.display = level === "numbers" || level === "full" ? "block" : "none"; 23753 } 23754 if (nameEl && nameEl.textContent) { 23755 nameEl.style.display = level === "full" ? "block" : "none"; 23756 } 23757 element.style.transform = `translate(-50%, -50%) rotate(${rotationDeg}deg) scale(${scale})`; 23758} 23759
23760// src/app/pages/campus-map-v3/utils/renderers/label-renderer.ts 23761var LabelRenderer = class { 23762 constructor(config) { 23763 this.buildingLabelDataBuilder = new BuildingLabelDataBuilder(); 23764 this.labelObjects = /* @__PURE__ */ new Map(); 23765 this.labelBaseRotations = /* @__PURE__ */ new Map(); 23766 this.currentLabelDetailLevel = null; 23767 this.currentLabelScale = null; 23768 this.currentSceneRotation = 0; 23769 this.onDirty = config.onDirty; 23770 this.labelsGroup = new Group(); 23771 this.labelsGroup.name = "room-labels"; 23772 } 23773 // ========================================================================= 23774 // Room Labels 23775 // ========================================================================= 23776 /** 23777 * Render labels for all rooms in the given floors. 23778 * Labels are centered, rotated to align with room orientation, and fitted to room size. 23779 * @param floors Array of floor models containing rooms 23780 */ 23781 renderRoomLabels(floors) { 23782 this.clearLabels(); 23783 for (const floor of floors) { 23784 if (!floor.rooms) 23785 continue; 23786 for (const room of floor.rooms) { 23787 if (room.entity.isDeleted || room.entity.hideLabel) 23788 continue; 23789 this.renderRoomLabel(room); 23790 } 23791 } 23792 for (const label of this.buildingLabelDataBuilder.build(floors)) { 23793 this.renderBuildingLabel(label); 23794 } 23795 this.onDirty(); 23796 } 23797 /** 23798 * Show/hide room labels. 23799 */ 23800 setLabelsVisible(visible) { 23801 this.labelsGroup.visible = visible; 23802 this.onDirty(); 23803 } 23804 /** 23805 * Check if labels are visible. 23806 */ 23807 get labelsVisible() { 23808 return this.labelsGroup.visible; 23809 } 23810 /** 23811 * Clear all room labels. 23812 */ 23813 clearLabels() { 23814 for (const label of this.labelObjects.values()) { 23815 this.labelsGroup.remove(label); 23816 } 23817 this.labelObjects.clear(); 23818 this.labelBaseRotations.clear(); 23819 this.currentLabelDetailLevel = null; 23820 this.currentLabelScale = null; 23821 } 23822 /** 23823 * Update label rotations to counter-rotate against scene rotation. 23824 * Call this whenever the scene/view rotation changes. 23825 * @param sceneRotation Current scene rotation in radians 23826 */ 23827 updateLabelsRotation(sceneRotation) { 23828 var _a3, _b2, _c; 23829 this.currentSceneRotation = sceneRotation; 23830 const scale = (_a3 = this.currentLabelScale) != null ? _a3 : 1; 23831 const level = (_b2 = this.currentLabelDetailLevel) != null ? _b2 : "full"; 23832 for (const [id, label] of this.labelObjects) { 23833 const baseRotation = (_c = this.labelBaseRotations.get(id)) != null ? _c : 0; 23834 const effectiveRotation = (baseRotation - sceneRotation) * (180 / Math.PI); 23835 const element = label.element; 23836 updateLabel(element, level, scale, effectiveRotation); 23837 } 23838 this.onDirty(); 23839 } 23840 /** 23841 * Update label detail level and scale based on camera zoom and viewport size. 23842 * Uses pixel-density normalization for consistent behavior across screen sizes. 23843 * - Zoomed out: icons only (smaller scale) 23844 * - Medium zoom: icons + numbers 23845 * - Close zoom: icons + numbers + names (larger scale) 23846 * @param zoom Current camera zoom level 23847 * @param canvasHeight Canvas height in CSS pixels 23848 */ 23849 updateLabelsDetailLevel(zoom, canvasHeight) { 23850 var _a3; 23851 const newLevel = getLabelDetailLevel(zoom, canvasHeight); 23852 const newScale = getLabelScale(zoom); 23853 if (newLevel === this.currentLabelDetailLevel && newScale === this.currentLabelScale) { 23854 return; 23855 } 23856 this.currentLabelDetailLevel = newLevel; 23857 this.currentLabelScale = newScale; 23858 for (const [id, label] of this.labelObjects) { 23859 const baseRotation = (_a3 = this.labelBaseRotations.get(id)) != null ? _a3 : 0; 23860 const effectiveRotation = (baseRotation - this.currentSceneRotation) * (180 / Math.PI); 23861 const element = label.element; 23862 updateLabel(element, newLevel, newScale, effectiveRotation); 23863 } 23864 } 23865 // ========================================================================= 23866 // Lifecycle 23867 // ========================================================================= 23868 /** 23869 * Dispose of all resources. 23870 */ 23871 dispose() { 23872 this.clearLabels(); 23873 } 23874 // ========================================================================= 23875 // Private: Label Rendering 23876 // ========================================================================= 23877 /** 23878 * Render a single room label using CSS2DObject. 23879 */ 23880 renderRoomLabel(room) { 23881 var _a3, _b2; 23882 const corners = [...room.corners].filter((c) => !c.entity.isDeleted).sort((a, b) => { 23883 var _a4, _b3;
23884 return ((_a4 = a.entity.position) != null ? _a4 : 0) - ((_b3 = b.entity.position) != null ? _b3 : 0); 23885 }); 23886 const coords = corners.map((c) => { 23887 var _a4, _b3; 23888 return { 23889 x: (_a4 = c.entity.locationX) != null ? _a4 : 0, 23890 y: (_b3 = c.entity.locationY) != null ? _b3 : 0 23891 }; 23892 }); 23893 if (coords.length < 3) 23894 return; 23895 const obb = calculateOBB(coords); 23896 if (!obb) 23897 return; 23898 if (!shouldShowLabel(obb)) 23899 return; 23900 const config = { 23901 number: (_a3 = room.entity.number) != null ? _a3 : null, 23902 name: (_b2 = room.entity.name) != null ? _b2 : null, 23903 icon: getRoomTypeIcon(room.entity.roomType), 23904 maxWidth: obb.width * 0.85, 23905 // Leave 15% margin 23906 rotation: obb.rotation * (180 / Math.PI) 23907 // Convert to degrees 23908 }; 23909 if (!config.number && !config.name && !config.icon) 23910 return; 23911 const element = createRoomLabelElement(config); 23912 const label = new CSS2DObject(element); 23913 label.position.set(obb.center.x, obb.center.y, Z_LAYERS.ROOM_LABEL); 23914 label.name = `room-label-${room.entity.id}`; 23915 this.labelsGroup.add(label); 23916 const key = this.getLabelKey("room", room.entity.id); 23917 this.labelObjects.set(key, label); 23918 this.labelBaseRotations.set(key, obb.rotation); 23919 } 23920 /** 23921 * Render a single building label using CSS2DObject. 23922 */ 23923 renderBuildingLabel(labelData) { 23924 var _a3, _b2; 23925 const buildingName = (_b2 = (_a3 = labelData.number) != null ? _a3 : labelData.name) != null ? _b2 : null; 23926 if (!buildingName) 23927 return; 23928 const maxWidth = (labelData.bounds.maxX - labelData.bounds.minX) * 0.85; 23929 if (!Number.isFinite(maxWidth) || maxWidth <= 0) 23930 return; 23931 const config = { 23932 number: buildingName, 23933 name: null, 23934 icon: null, 23935 maxWidth, 23936 rotation: 0 23937 }; 23938 const element = createRoomLabelElement(config); 23939 const label = new CSS2DObject(element); 23940 label.position.set(labelData.center.x, labelData.center.y, Z_LAYERS.ROOM_LABEL); 23941 label.name = `building-label-${labelData.id}`; 23942 this.labelsGroup.add(label); 23943 const key = this.getLabelKey("building", labelData.id); 23944 this.labelObjects.set(key, label); 23945 this.labelBaseRotations.set(key, 0); 23946 } 23947 getLabelKey(kind, id) { 23948 return `${kind}:${id}`; 23949 } 23950}; 23951
23952// src/app/pages/campus-map-v3/utils/renderers/shape-edit-renderer.ts 23953var MAX_HANDLE_SCALE = 3; 23954var ShapeEditRenderer = class { 23955 constructor(config) { 23956 this.materials = config.materials; 23957 this.onDirty = config.onDirty; 23958 this.getCameraZoom = config.getCameraZoom; 23959 this.cornerHandlesGroup = new Group(); 23960 this.cornerHandlesGroup.name = "corner-handles"; 23961 this.edgeMidpointsGroup = new Group(); 23962 this.edgeMidpointsGroup.name = "edge-midpoints"; 23963 this.polygonPreviewGroup = new Group(); 23964 this.polygonPreviewGroup.name = "polygon-preview"; 23965 this.roomWallDimensionsGroup = new Group(); 23966 this.roomWallDimensionsGroup.name = "room-wall-dimensions"; 23967 } 23968 renderRoomWallDimension(wall, selected) { 23969 this.clearRoomWallDimension(); 23970 const color = selected ? 16347926 : 561586; 23971 const lineMaterial = new LineBasicMaterial({ color, depthTest: false }); 23972 const lineGeometry = new BufferGeometry().setFromPoints([ 23973 new Vector3(wall.start.x, wall.start.y, Z_LAYERS.WALL_LENGTH), 23974 new Vector3(wall.end.x, wall.end.y, Z_LAYERS.WALL_LENGTH) 23975 ]); 23976 this.roomWallDimensionsGroup.add(new Line(lineGeometry, lineMaterial)); 23977 const label = document.createElement("div"); 23978 label.className = `room-wall-dimension-label${selected ? " selected" : ""}`; 23979 label.textContent = `${(wall.lengthCm / 100).toFixed(2)} m`; 23980 const labelObject = new CSS2DObject(label); 23981 labelObject.position.set(wall.midpoint.x, wall.midpoint.y, Z_LAYERS.WALL_LENGTH + 0.1); 23982 this.roomWallDimensionsGroup.add(labelObject); 23983 if (selected) { 23984 const scale = Math.min(1 / this.getCameraZoom(), MAX_HANDLE_SCALE); 23985 const endpointMaterial = new MeshBasicMaterial({ color, transparent: true, opacity: 0.9, depthTest: false }); 23986 for (const point of [wall.start, wall.end]) { 23987 const handle = new Mesh(new RingGeometry(24, 34, 24), endpointMaterial.clone()); 23988 handle.position.set(point.x, point.y, Z_LAYERS.WALL_LENGTH + 0.2); 23989 handle.scale.set(scale, scale, 1); 23990 this.roomWallDimensionsGroup.add(handle); 23991 } 23992 endpointMaterial.dispose(); 23993 const center = new Mesh(new RingGeometry(7, 13, 20), new MeshBasicMaterial({ color: 16777215, depthTest: false })); 23994 center.position.set(wall.midpoint.x, wall.midpoint.y, Z_LAYERS.WALL_LENGTH + 0.2); 23995 center.scale.set(scale, scale, 1); 23996 this.roomWallDimensionsGroup.add(center); 23997 } 23998 this.onDirty(); 23999 } 24000 clearRoomWallDimension() { 24001 while (this.roomWallDimensionsGroup.children.length > 0) { 24002 const child = this.roomWallDimensionsGroup.children[0]; 24003 this.roomWallDimensionsGroup.remove(child); 24004 if (child instanceof Mesh || child instanceof Line) { 24005 child.geometry.dispose(); 24006 if (Array.isArray(child.material)) 24007 child.material.forEach((material) => material.dispose()); 24008 else 24009 child.material.dispose(); 24010 } else if (child instanceof CSS2DObject) { 24011 child.element.remove(); 24012 } 24013 } 24014 this.onDirty(); 24015 } 24016 // ========================================================================= 24017 // Corner Handles 24018 // ========================================================================= 24019 /** 24020 * Render corner handles for shape editing mode. 24021 * @param corners Array of corner data with position and selection state 24022 */ 24023 renderCornerHandles(corners) { 24024 this.clearCornerHandles(); 24025 const handleSize = 24; 24026 const zoom = this.getCameraZoom(); 24027 const scale = Math.min(1 / zoom, MAX_HANDLE_SCALE); 24028 for (const corner of corners) { 24029 const geometry = new PlaneGeometry(handleSize * 2, handleSize * 2); 24030 const material = corner.selected ? this.materials.getCornerHandleSelectedMaterial() : this.materials.getCornerHandleMaterial(); 24031 const mesh = new Mesh(geometry, material);
24032 mesh.position.set(corner.x, corner.y, Z_LAYERS.CORNER_HANDLES); 24033 mesh.scale.set(scale, scale, 1); 24034 mesh.name = `corner-handle-${corner.id}`; 24035 mesh.userData["cornerId"] = corner.id; 24036 mesh.userData["entityType"] = "cornerHandle"; 24037 this.cornerHandlesGroup.add(mesh); 24038 const outlineMaterial = corner.hovered ? this.materials.getCornerHandleHoveredOutlineMaterial() : this.materials.getCornerHandleOutlineMaterial(); 24039 const outlinePoints = [ 24040 new Vector3(-handleSize, -handleSize, 0), 24041 new Vector3(handleSize, -handleSize, 0), 24042 new Vector3(handleSize, handleSize, 0), 24043 new Vector3(-handleSize, handleSize, 0), 24044 new Vector3(-handleSize, -handleSize, 0) 24045 // Close loop 24046 ]; 24047 const outlineGeometry = new BufferGeometry().setFromPoints(outlinePoints); 24048 const outline = new Line(outlineGeometry, outlineMaterial); 24049 outline.position.set(corner.x, corner.y, Z_LAYERS.CORNER_HANDLES + 0.1); 24050 outline.scale.set(scale, scale, 1); 24051 outline.name = `corner-handle-outline-${corner.id}`; 24052 this.cornerHandlesGroup.add(outline); 24053 } 24054 this.onDirty(); 24055 } 24056 /** 24057 * Clear all corner handles. 24058 */ 24059 clearCornerHandles() { 24060 while (this.cornerHandlesGroup.children.length > 0) { 24061 const child = this.cornerHandlesGroup.children[0]; 24062 this.cornerHandlesGroup.remove(child); 24063 if (child instanceof Mesh || child instanceof Line) { 24064 child.geometry.dispose(); 24065 } 24066 } 24067 } 24068 // ========================================================================= 24069 // Edge Midpoints 24070 // ========================================================================= 24071 /** 24072 * Render edge midpoint handles for adding corners. 24073 * Only renders the hovered midpoint for cleaner UI. 24074 * @param midpoints Array of midpoint positions 24075 * @param hoveredIndex Index of the currently hovered edge (null = none) 24076 */ 24077 renderEdgeMidpoints(midpoints, hoveredIndex) { 24078 this.clearEdgeMidpoints(); 24079 if (hoveredIndex === null) 24080 return; 24081 const hoveredMidpoint = midpoints.find((mp) => mp.edgeIndex === hoveredIndex); 24082 if (!hoveredMidpoint) 24083 return; 24084 const radius = 15; 24085 const zoom = this.getCameraZoom(); 24086 const scale = Math.min(1 / zoom, MAX_HANDLE_SCALE); 24087 const geometry = new CircleGeometry(radius, 16); 24088 const material = this.materials.getMidpointHandleMaterial(); 24089 const mesh = new Mesh(geometry, material); 24090 mesh.position.set(hoveredMidpoint.x, hoveredMidpoint.y, Z_LAYERS.MIDPOINT_HANDLES); 24091 mesh.scale.set(scale, scale, 1); 24092 mesh.name = `midpoint-handle-${hoveredMidpoint.edgeIndex}`; 24093 mesh.userData["edgeIndex"] = hoveredMidpoint.edgeIndex; 24094 mesh.userData["entityType"] = "midpointHandle"; 24095 this.edgeMidpointsGroup.add(mesh); 24096 const plusSize = radius * 0.6; 24097 const plusPoints1 = [ 24098 new Vector3(-plusSize, 0, 0.1), 24099 new Vector3(plusSize, 0, 0.1) 24100 ]; 24101 const plusPoints2 = [ 24102 new Vector3(0, -plusSize, 0.1), 24103 new Vector3(0, plusSize, 0.1) 24104 ]; 24105 const plusGeom1 = new BufferGeometry().setFromPoints(plusPoints1); 24106 const plusGeom2 = new BufferGeometry().setFromPoints(plusPoints2); 24107 const plusMaterial = this.materials.getCornerHandleOutlineMaterial(); 24108 const plusLine1 = new Line(plusGeom1, plusMaterial); 24109 plusLine1.position.set(hoveredMidpoint.x, hoveredMidpoint.y, Z_LAYERS.MIDPOINT_HANDLES); 24110 plusLine1.scale.set(scale, scale, 1); 24111 const plusLine2 = new Line(plusGeom2, plusMaterial); 24112 plusLine2.position.set(hoveredMidpoint.x, hoveredMidpoint.y, Z_LAYERS.MIDPOINT_HANDLES); 24113 plusLine2.scale.set(scale, scale, 1); 24114 this.edgeMidpointsGroup.add(plusLine1); 24115 this.edgeMidpointsGroup.add(plusLine2); 24116 this.onDirty(); 24117 } 24118 /** 24119 * Clear all edge midpoint handles. 24120 */ 24121 clearEdgeMidpoints() { 24122 while (this.edgeMidpointsGroup.children.length > 0) { 24123 const child = this.edgeMidpointsGroup.children[0]; 24124 this.edgeMidpointsGroup.remove(child); 24125 if (child instanceof Mesh || child instanceof Line) { 24126 child.geometry.dispose(); 24127 } 24128 } 24129 } 24130 // ========================================================================= 24131 // Polygon Preview 24132 // ========================================================================= 24133 /** 24134 * Render polygon drawing preview. 24135 * Shows placed vertices, connecting edges, and rubber-band line to cursor. 24136 * @param vertices Array of placed vertex positions 24137 * @param cursorPosition Current cursor position (for rubber-band line) 24138 * @param canClose Whether clicking would close the polygon (near first vertex) 24139 */ 24140 renderPolygonDrawPreview(vertices, cursorPosition, canClose) { 24141 this.clearPolygonPreview(); 24142 if (vertices.length === 0) 24143 return; 24144 const vertexRadius = 12; 24145 for (let i = 0; i < vertices.length; i++) { 24146 const v = vertices[i]; 24147 const geometry = new CircleGeometry(vertexRadius, 16); 24148 const material = this.materials.getVertexMarkerMaterial(); 24149 const mesh = new Mesh(geometry, material); 24150 mesh.position.set(v.x, v.y, Z_LAYERS.POLYGON_PREVIEW); 24151 mesh.name = `preview-vertex-${i}`; 24152 this.polygonPreviewGroup.add(mesh); 24153 } 24154 if (vertices.length >= 2) { 24155 const edgePoints = vertices.map((v) => new Vector3(v.x, v.y, Z_LAYERS.POLYGON_PREVIEW)); 24156 const edgeGeometry = new BufferGeometry().setFromPoints(edgePoints); 24157 const edgeMaterial = this.materials.getPolygonPreviewSolidMaterial(); 24158 const edgeLine = new Line(edgeGeometry, edgeMaterial); 24159 edgeLine.name = "preview-edges"; 24160 this.polygonPreviewGroup.add(edgeLine); 24161 } 24162 if (cursorPosition && vertices.length > 0) { 24163 const lastVertex = vertices[vertices.length - 1]; 24164 const rubberPoints = [ 24165 new Vector3(lastVertex.x, lastVertex.y, Z_LAYERS.POLYGON_PREVIEW), 24166 new Vector3(cursorPosition.x, cursorPosition.y, Z_LAYERS.POLYGON_PREVIEW) 24167 ]; 24168 const rubberGeometry = new BufferGeometry().setFromPoints(rubberPoints); 24169 const rubberMaterial = this.materials.getPolygonPreviewMaterial(); 24170 const rubberLine = new Line(rubberGeometry, rubberMaterial); 24171 rubberLine.computeLineDistances(); 24172 rubberLine.name = "preview-rubber-band"; 24173 this.polygonPreviewGroup.add(rubberLine); 24174 if (canClose && vertices.length >= 3) { 24175 const closePoints = [ 24176 new Vector3(cursorPosition.x, cursorPosition.y, Z_LAYERS.POLYGON_PREVIEW), 24177 new Vector3(vertices[0].x, vertices[0].y, Z_LAYERS.POLYGON_PREVIEW) 24178 ]; 24179 const closeGeometry = new BufferGeometry().setFromPoints(closePoints); 24180 const closeMaterial = this.materials.getPolygonPreviewMaterial(); 24181 const closeLine = new Line(closeGeometry, closeMaterial); 24182 closeLine.computeLineDistances(); 24183 closeLine.name = "preview-close-line"; 24184 this.polygonPreviewGroup.add(closeLine); 24185 const closeRing = new RingGeometry(vertexRadius + 8, vertexRadius + 14, 24); 24186 const closeRingMaterial = this.materials.getCloseIndicatorMaterial(); 24187 const closeRingMesh = new Mesh(closeRing, closeRingMaterial); 24188 closeRingMesh.position.set(vertices[0].x, vertices[0].y, Z_LAYERS.POLYGON_PREVIEW + 0.1); 24189 closeRingMesh.name = "preview-close-indicator"; 24190 this.polygonPreviewGroup.add(closeRingMesh); 24191 } 24192 } 24193 this.onDirty(); 24194 } 24195 /** 24196 * Clear polygon drawing preview. 24197 */ 24198 clearPolygonPreview() { 24199 while (this.polygonPreviewGroup.children.length > 0) { 24200 const child = this.polygonPreviewGroup.children[0]; 24201 this.polygonPreviewGroup.remove(child); 24202 if (child instanceof Mesh || child instanceof Line) { 24203 child.geometry.dispose(); 24204 } 24205 } 24206 } 24207 /** 24208 * Calculate edge midpoints for a room's corners. 24209 * @param corners Sorted array of corner positions 24210 * @returns Array of midpoints with edge indices 24211 */ 24212 calculateEdgeMidpoints(corners) { 24213 const midpoints = []; 24214 for (let i = 0; i < corners.length; i++) { 24215 const current = corners[i]; 24216 const next = corners[(i + 1) % corners.length]; 24217 midpoints.push({ 24218 edgeIndex: i, 24219 x: (current.x + next.x) / 2, 24220 y: (current.y + next.y) / 2 24221 }); 24222 } 24223 return midpoints; 24224 } 24225 // ========================================================================= 24226 // Lifecycle 24227 // ========================================================================= 24228 /** 24229 * Dispose of all resources. 24230 */ 24231 dispose() { 24232 this.clearCornerHandles(); 24233 this.clearEdgeMidpoints(); 24234 this.clearPolygonPreview(); 24235 this.clearRoomWallDimension(); 24236 } 24237}; 24238
24239// src/app/pages/campus-map-v3/utils/renderers/selection-highlight-renderer.ts 24240var HIGHLIGHT_TO_NODE_RATIO = 1.2; 24241var DESTINATION_HIGHLIGHT_TO_NODE_RATIO = 1.65; 24242var SelectionHighlightRenderer = class { 24243 constructor(config) { 24244 this.highlightMeshes = /* @__PURE__ */ new Map(); 24245 this.highlightScales = /* @__PURE__ */ new Map(); 24246 this.destinationHighlightMeshes = /* @__PURE__ */ new Map(); 24247 this.destinationHighlightScales = /* @__PURE__ */ new Map(); 24248 this.ghostMeshes = /* @__PURE__ */ new Map(); 24249 this.materials = config.materials; 24250 this.onDirty = config.onDirty; 24251 this.findEntityMesh = config.findEntityMesh; 24252 this.getCameraZoom = config.getCameraZoom; 24253 this.highlightGroup = new Group(); 24254 this.highlightGroup.name = "selection-highlights"; 24255 this.ghostGroup = new Group(); 24256 this.ghostGroup.name = "drag-ghosts"; 24257 } 24258 // ========================================================================= 24259 // Selection Highlighting 24260 // ========================================================================= 24261 /** 24262 * Highlight an entity with a selection outline. 24263 * @param id Entity ID 24264 * @param type Entity type (used to find the mesh) 24265 */ 24266 highlightEntity(id, type) { 24267 if (this.highlightMeshes.has(id)) 24268 return; 24269 const source = this.findEntityMesh(id, type); 24270 if (!source) 24271 return; 24272 let highlight = null; 24273 let initialScale = 1; 24274 if (type === "node") { 24275 const result = this.createNodeHighlight(source); 24276 highlight = result.mesh; 24277 initialScale = result.scale; 24278 } else if (type === "nodeConnection") { 24279 highlight = this.createConnectionHighlight(source); 24280 } else { 24281 highlight = this.createPolygonHighlight(source); 24282 } 24283 if (highlight) { 24284 highlight.name = `highlight-${id}`; 24285 highlight.userData["highlightFor"] = id; 24286 highlight.scale.set(initialScale, initialScale, 1); 24287 this.highlightGroup.add(highlight); 24288 this.highlightMeshes.set(id, highlight); 24289 this.highlightScales.set(id, initialScale); 24290 this.onDirty(); 24291 } 24292 } 24293 /** 24294 * Remove highlight from an entity. 24295 * @param id Entity ID 24296 */ 24297 unhighlightEntity(id) { 24298 const highlight = this.highlightMeshes.get(id); 24299 if (!highlight) 24300 return; 24301 this.highlightGroup.remove(highlight); 24302 highlight.geometry.dispose(); 24303 this.highlightMeshes.delete(id); 24304 this.highlightScales.delete(id); 24305 this.onDirty(); 24306 } 24307 /** 24308 * Clear all selection highlights. 24309 */ 24310 clearHighlights() { 24311 for (const [, highlight] of this.highlightMeshes) { 24312 this.highlightGroup.remove(highlight); 24313 highlight.geometry.dispose(); 24314 } 24315 this.highlightMeshes.clear(); 24316 this.highlightScales.clear(); 24317 this.onDirty(); 24318 } 24319 /** 24320 * Check if an entity is highlighted. 24321 */ 24322 isHighlighted(id) { 24323 return this.highlightMeshes.has(id); 24324 } 24325 /** 24326 * Update the scale of a highlight to match zoom level. 24327 * Called when camera zoom changes to maintain consistent screen-space size. 24328 * @param id Entity ID 24329 * @param nodeScale Current scale of the associated node mesh 24330 */ 24331 updateHighlightScale(id, nodeScale) { 24332 const highlight = this.highlightMeshes.get(id); 24333 if (!highlight) 24334 return; 24335 const newScale = nodeScale * HIGHLIGHT_TO_NODE_RATIO; 24336 highlight.scale.set(newScale, newScale, 1); 24337 this.highlightScales.set(id, newScale); 24338 this.onDirty(); 24339 } 24340 /** 24341 * Get the current scale for a highlight. 24342 * @param id Entity ID 24343 * @returns Current scale, or 1 if not found 24344 */ 24345 getHighlightScale(id) { 24346 var _a3; 24347 return (_a3 = this.highlightScales.get(id)) != null ? _a3 : 1; 24348 } 24349 // ========================================================================= 24350 // Resolved Destination Highlighting 24351 // ========================================================================= 24352 /** 24353 * Highlight a resolved destination node independently of selection state. 24354 */ 24355 highlightDestinationNode(id) { 24356 if (this.destinationHighlightMeshes.has(id)) 24357 return; 24358 const source = this.findEntityMesh(id, "node"); 24359 if (!source || !(source instanceof Mesh)) 24360 return; 24361 const result = this.createDestinationNodeHighlight(source); 24362 const highlight = result.mesh; 24363 highlight.name = `destination-highlight-${id}`; 24364 highlight.userData["destinationHighlightFor"] = id; 24365 highlight.scale.set(result.scale, result.scale, 1); 24366 this.highlightGroup.add(highlight); 24367 this.destinationHighlightMeshes.set(id, highlight); 24368 this.destinationHighlightScales.set(id, result.scale); 24369 this.onDirty(); 24370 } 24371 /** 24372 * Remove resolved destination highlight from a node. 24373 */ 24374 unhighlightDestinationNode(id) { 24375 const highlight = this.destinationHighlightMeshes.get(id); 24376 if (!highlight) 24377 return; 24378 this.highlightGroup.remove(highlight); 24379 this.disposeHighlightObject(highlight); 24380 this.destinationHighlightMeshes.delete(id); 24381 this.destinationHighlightScales.delete(id); 24382 this.onDirty(); 24383 } 24384 /** 24385 * Clear all resolved destination highlights. 24386 */ 24387 clearDestinationHighlights() { 24388 for (const [, highlight] of this.destinationHighlightMeshes) { 24389 this.highlightGroup.remove(highlight); 24390 this.disposeHighlightObject(highlight); 24391 } 24392 this.destinationHighlightMeshes.clear(); 24393 this.destinationHighlightScales.clear(); 24394 this.onDirty(); 24395 } 24396 /** 24397 * Check if a node has a resolved destination highlight. 24398 */ 24399 isDestinationHighlighted(id) { 24400 return this.destinationHighlightMeshes.has(id); 24401 } 24402 /** 24403 * Update a destination highlight to match zoom-responsive node scale. 24404 */ 24405 updateDestinationHighlightScale(id, nodeScale) { 24406 const highlight = this.destinationHighlightMeshes.get(id); 24407 if (!highlight) 24408 return; 24409 const newScale = nodeScale * DESTINATION_HIGHLIGHT_TO_NODE_RATIO; 24410 highlight.scale.set(newScale, newScale, 1); 24411 this.destinationHighlightScales.set(id, newScale); 24412 this.onDirty(); 24413 } 24414 // ========================================================================= 24415 // Ghost System (Drag Preview) 24416 // ========================================================================= 24417 /** 24418 * Create a ghost outline at entity's current position. 24419 * Used during drag to show original position. 24420 */ 24421 createGhost(id, type) { 24422 if (this.ghostMeshes.has(id)) 24423 return; 24424 const source = this.findEntityMesh(id, type); 24425 if (!source) 24426 return; 24427 let ghost = null; 24428 if (type === "node") { 24429 ghost = this.createNodeGhost(source); 24430 } else if (type === "nodeConnection") { 24431 ghost = this.createConnectionGhost(source); 24432 } else { 24433 ghost = this.createPolygonGhost(source); 24434 } 24435 if (ghost) { 24436 ghost.name = `ghost-${id}`; 24437 ghost.userData["ghostFor"] = id; 24438 this.ghostGroup.add(ghost); 24439 this.ghostMeshes.set(id, ghost); 24440 this.onDirty(); 24441 } 24442 } 24443 /** 24444 * Remove ghost for an entity. 24445 */ 24446 removeGhost(id) { 24447 const ghost = this.ghostMeshes.get(id); 24448 if (!ghost) 24449 return; 24450 this.ghostGroup.remove(ghost); 24451 ghost.geometry.dispose(); 24452 this.ghostMeshes.delete(id); 24453 this.onDirty(); 24454 } 24455 /** 24456 * Clear all ghosts. 24457 */ 24458 clearGhosts() { 24459 for (const [, ghost] of this.ghostMeshes) { 24460 this.ghostGroup.remove(ghost); 24461 ghost.geometry.dispose(); 24462 } 24463 this.ghostMeshes.clear(); 24464 this.onDirty(); 24465 } 24466 // ========================================================================= 24467 // Mesh Movement (Drag Preview) 24468 // ========================================================================= 24469 /** 24470 * Move an entity's highlight by delta for drag preview. 24471 * Does NOT modify entity data - visual only. 24472 */ 24473 moveEntityHighlight(id, dx, dy) { 24474 const highlight = this.highlightMeshes.get(id); 24475 if (highlight) { 24476 highlight.position.x += dx; 24477 highlight.position.y += dy; 24478 } 24479 const destinationHighlight = this.destinationHighlightMeshes.get(id); 24480 if (destinationHighlight) { 24481 destinationHighlight.position.x += dx; 24482 destinationHighlight.position.y += dy; 24483 } 24484 this.onDirty(); 24485 } 24486 /** 24487 * Get current mesh position for an entity. 24488 */
24489 getEntityMeshPosition(id, type) { 24490 const mesh = this.findEntityMesh(id, type); 24491 if (!mesh) 24492 return null; 24493 return { x: mesh.position.x, y: mesh.position.y }; 24494 } 24495 // ========================================================================= 24496 // Lifecycle 24497 // ========================================================================= 24498 /** 24499 * Dispose of all resources. 24500 */ 24501 dispose() { 24502 this.clearHighlights(); 24503 this.clearDestinationHighlights(); 24504 this.clearGhosts(); 24505 } 24506 // ========================================================================= 24507 // Private: Highlight Creation 24508 // ========================================================================= 24509 /** 24510 * Create a highlight ring for a node using SDF shader for perfect circles. 24511 * Returns mesh and the scale factor to apply (1.2x node size). 24512 */ 24513 createNodeHighlight(source) { 24514 const highlightRadius = NODE_RADIUS * HIGHLIGHT_TO_NODE_RATIO; 24515 const size = highlightRadius * 2; 24516 const geometry = new PlaneGeometry(size, size); 24517 const material = this.materials.getNodeHighlightMaterial(); 24518 const mesh = new Mesh(geometry, material); 24519 mesh.position.copy(source.position); 24520 mesh.position.z = Z_LAYERS.SELECTION_HIGHLIGHT; 24521 return { mesh, scale: 1 }; 24522 } 24523 /** 24524 * Create a larger highlight ring for a resolved destination node. 24525 */ 24526 createDestinationNodeHighlight(source) { 24527 const outerRadius = NODE_RADIUS * DESTINATION_HIGHLIGHT_TO_NODE_RATIO; 24528 const innerRadius = NODE_RADIUS * 1.45; 24529 const group = new Group(); 24530 group.position.set(source.position.x, source.position.y, 0); 24531 const outer = new Mesh(new PlaneGeometry(outerRadius * 2, outerRadius * 2), this.materials.getNodeDestinationOuterHighlightMaterial()); 24532 outer.name = "destination-highlight-outer-ring"; 24533 outer.position.z = Z_LAYERS.DESTINATION_HIGHLIGHT; 24534 const inner = new Mesh(new PlaneGeometry(innerRadius * 2, innerRadius * 2), this.materials.getNodeDestinationHighlightMaterial()); 24535 inner.name = "destination-highlight-inner-ring"; 24536 inner.position.z = Z_LAYERS.DESTINATION_HIGHLIGHT + 0.01; 24537 group.add(outer, inner); 24538 return { mesh: group, scale: 1 }; 24539 } 24540 disposeHighlightObject(object) { 24541 object.traverse((child) => { 24542 if (child instanceof Mesh || child instanceof Line) { 24543 child.geometry.dispose(); 24544 } 24545 }); 24546 } 24547 /** 24548 * Create a highlight for a connection line. 24549 */ 24550 createConnectionHighlight(source) { 24551 const geometry = source.geometry.clone(); 24552 const material = this.materials.getSelectionOutlineMaterial(); 24553 const line = new Line(geometry, material); 24554 line.position.z = Z_LAYERS.SELECTION_HIGHLIGHT; 24555 return line; 24556 } 24557 /** 24558 * Create a highlight outline for a polygon (room/floor). 24559 * Uses stored outline coordinates for accurate shape tracing. 24560 */ 24561 createPolygonHighlight(source) { 24562 const coords = source.userData["outlineCoords"]; 24563 if (coords && coords.length >= 3) { 24564 const points = coords.map((c) => new Vector3(c.x, c.y, Z_LAYERS.SELECTION_HIGHLIGHT)); 24565 points.push(points[0].clone()); 24566 const geometry = new BufferGeometry().setFromPoints(points); 24567 const material = this.materials.getSelectionOutlineMaterial(); 24568 return new Line(geometry, material); 24569 } 24570 return null; 24571 } 24572 // ========================================================================= 24573 // Private: Ghost Creation 24574 // ========================================================================= 24575 /** 24576 * Create dashed circle outline ghost for a node. 24577 */ 24578 createNodeGhost(source) { 24579 const segments = 32; 24580 const radius = NODE_RADIUS; 24581 const points = []; 24582 for (let i = 0; i <= segments; i++) { 24583 const angle = i / segments * Math.PI * 2; 24584 points.push(new Vector3(Math.cos(angle) * radius, Math.sin(angle) * radius, 0)); 24585 } 24586 const geometry = new BufferGeometry().setFromPoints(points); 24587 const material = this.materials.getGhostOutlineMaterial(); 24588 const line = new Line(geometry, material); 24589 line.computeLineDistances(); 24590 line.position.copy(source.position); 24591 line.position.z = Z_LAYERS.GHOST; 24592 return line; 24593 } 24594 /** 24595 * Create dashed line ghost for a connection. 24596 */ 24597 createConnectionGhost(source) { 24598 const geometry = source.geometry.clone(); 24599 const material = this.materials.getGhostOutlineMaterial(); 24600 const line = new Line(geometry, material); 24601 line.computeLineDistances(); 24602 line.position.z = Z_LAYERS.GHOST; 24603 return line; 24604 } 24605 /** 24606 * Create dashed polygon outline ghost for a room/floor. 24607 */ 24608 createPolygonGhost(source) { 24609 const coords = source.userData["outlineCoords"]; 24610 if (coords && coords.length >= 3) {
24611 const points = coords.map((c) => new Vector3(c.x, c.y, Z_LAYERS.GHOST)); 24612 points.push(points[0].clone()); 24613 const geometry = new BufferGeometry().setFromPoints(points); 24614 const material = this.materials.getGhostOutlineMaterial(); 24615 const line = new Line(geometry, material); 24616 line.computeLineDistances(); 24617 return line; 24618 } 24619 return null; 24620 } 24621}; 24622
24623// src/app/pages/campus-map-v3/utils/renderers/rotation-handle-renderer.ts 24624var GRIP_RADIUS = 25; 24625var ARC_SEGMENTS = 24; 24626var RotationHandleRenderer = class { 24627 constructor(config) { 24628 this.arcLine = null; 24629 this.gripMesh = null; 24630 this.gripOutline = null; 24631 this.pivotMarker = null; 24632 this.crossLineH = null; 24633 this.crossLineV = null; 24634 this.isHovered = false; 24635 this.currentPivot = { x: 0, y: 0 }; 24636 this.currentRadius = 0; 24637 this.materials = config.materials; 24638 this.onDirty = config.onDirty; 24639 this.group = new Group(); 24640 this.group.name = "rotation-handle"; 24641 } 24642 /** 24643 * Render rotation handle for a selection. 24644 * @param pivot Center point of the selection (rotation pivot) 24645 * @param radius Distance from pivot to handle (based on selection bounds) 24646 */ 24647 render(pivot, radius) { 24648 this.clear(); 24649 this.currentPivot = pivot; 24650 this.currentRadius = radius; 24651 const startAngle = Math.PI / 4; 24652 const endAngle = 3 * Math.PI / 4; 24653 const arcPoints = []; 24654 for (let i = 0; i <= ARC_SEGMENTS; i++) { 24655 const angle = startAngle + i / ARC_SEGMENTS * (endAngle - startAngle); 24656 arcPoints.push(new Vector3(pivot.x + Math.cos(angle) * radius, pivot.y + Math.sin(angle) * radius, Z_LAYERS.ROTATION_HANDLE)); 24657 } 24658 const arcGeometry = new BufferGeometry().setFromPoints(arcPoints); 24659 const arcMaterial = this.isHovered ? this.materials.getRotationHandleHoverMaterial() : this.materials.getRotationHandleMaterial(); 24660 this.arcLine = new Line(arcGeometry, arcMaterial); 24661 this.arcLine.name = "rotation-arc"; 24662 this.arcLine.userData["isRotationHandle"] = true; 24663 this.group.add(this.arcLine); 24664 const gripX = pivot.x + Math.cos(endAngle) * radius; 24665 const gripY = pivot.y + Math.sin(endAngle) * radius; 24666 const gripGeometry = new CircleGeometry(GRIP_RADIUS, 16); 24667 this.gripMesh = new Mesh(gripGeometry, this.materials.getRotationGripMaterial()); 24668 this.gripMesh.position.set(gripX, gripY, Z_LAYERS.ROTATION_HANDLE + 0.1); 24669 this.gripMesh.name = "rotation-grip"; 24670 this.gripMesh.userData["isRotationGrip"] = true; 24671 this.group.add(this.gripMesh); 24672 const outlinePoints = []; 24673 for (let i = 0; i <= 32; i++) { 24674 const angle = i / 32 * Math.PI * 2; 24675 outlinePoints.push(new Vector3(Math.cos(angle) * GRIP_RADIUS, Math.sin(angle) * GRIP_RADIUS, 0)); 24676 } 24677 const outlineGeometry = new BufferGeometry().setFromPoints(outlinePoints); 24678 this.gripOutline = new Line(outlineGeometry, this.materials.getRotationGripOutlineMaterial()); 24679 this.gripOutline.position.set(gripX, gripY, Z_LAYERS.ROTATION_HANDLE + 0.2); 24680 this.gripOutline.name = "rotation-grip-outline"; 24681 this.group.add(this.gripOutline); 24682 const pivotOuterGeometry = new CircleGeometry(18, 16); 24683 this.pivotMarker = new Mesh(pivotOuterGeometry, this.materials.getRotationPivotMaterial()); 24684 this.pivotMarker.position.set(pivot.x, pivot.y, Z_LAYERS.ROTATION_PIVOT); 24685 this.pivotMarker.name = "rotation-pivot"; 24686 this.group.add(this.pivotMarker); 24687 const crossSize = 25; 24688 const crossPoints = [ 24689 new Vector3(pivot.x - crossSize, pivot.y, Z_LAYERS.ROTATION_PIVOT + 0.1), 24690 new Vector3(pivot.x + crossSize, pivot.y, Z_LAYERS.ROTATION_PIVOT + 0.1) 24691 ]; 24692 const crossGeometryH = new BufferGeometry().setFromPoints(crossPoints); 24693 this.crossLineH = new Line(crossGeometryH, this.materials.getRotationHandleMaterial()); 24694 this.crossLineH.name = "rotation-pivot-cross-h"; 24695 this.group.add(this.crossLineH); 24696 const crossPointsV = [ 24697 new Vector3(pivot.x, pivot.y - crossSize, Z_LAYERS.ROTATION_PIVOT + 0.1), 24698 new Vector3(pivot.x, pivot.y + crossSize, Z_LAYERS.ROTATION_PIVOT + 0.1) 24699 ]; 24700 const crossGeometryV = new BufferGeometry().setFromPoints(crossPointsV); 24701 this.crossLineV = new Line(crossGeometryV, this.materials.getRotationHandleMaterial()); 24702 this.crossLineV.name = "rotation-pivot-cross-v"; 24703 this.group.add(this.crossLineV); 24704 this.onDirty(); 24705 } 24706 /** 24707 * Update handle hover state. 24708 */ 24709 setHovered(hovered) { 24710 if (this.isHovered === hovered) 24711 return; 24712 this.isHovered = hovered; 24713 if (this.arcLine && this.currentRadius > 0) { 24714 this.render(this.currentPivot, this.currentRadius); 24715 } 24716 } 24717 /** 24718 * Check if the handle is currently visible. 24719 */ 24720 isVisible() { 24721 return this.arcLine !== null; 24722 } 24723 /** 24724 * Get the current pivot point. 24725 */
24726 getPivot() { 24727 return this.currentPivot; 24728 } 24729 /** 24730 * Get the grip position for hit testing. 24731 */ 24732 getGripPosition() { 24733 if (!this.gripMesh) 24734 return null; 24735 return { 24736 x: this.gripMesh.position.x, 24737 y: this.gripMesh.position.y 24738 }; 24739 } 24740 /** 24741 * Get grip radius for hit testing. 24742 */ 24743 getGripRadius() { 24744 return GRIP_RADIUS; 24745 } 24746 /** 24747 * Hide the rotation handle. 24748 */ 24749 hide() { 24750 this.clear(); 24751 this.onDirty(); 24752 } 24753 /** 24754 * Dispose of all resources. 24755 */ 24756 dispose() { 24757 this.clear(); 24758 } 24759 clear() { 24760 if (this.arcLine) { 24761 this.group.remove(this.arcLine); 24762 this.arcLine.geometry.dispose(); 24763 this.arcLine = null; 24764 } 24765 if (this.gripMesh) { 24766 this.group.remove(this.gripMesh); 24767 this.gripMesh.geometry.dispose(); 24768 this.gripMesh = null; 24769 } 24770 if (this.gripOutline) { 24771 this.group.remove(this.gripOutline); 24772 this.gripOutline.geometry.dispose(); 24773 this.gripOutline = null; 24774 } 24775 if (this.pivotMarker) { 24776 this.group.remove(this.pivotMarker); 24777 this.pivotMarker.geometry.dispose(); 24778 this.pivotMarker = null; 24779 } 24780 if (this.crossLineH) { 24781 this.group.remove(this.crossLineH); 24782 this.crossLineH.geometry.dispose(); 24783 this.crossLineH = null; 24784 } 24785 if (this.crossLineV) { 24786 this.group.remove(this.crossLineV); 24787 this.crossLineV.geometry.dispose(); 24788 this.crossLineV = null; 24789 } 24790 this.currentRadius = 0; 24791 } 24792}; 24793
24794// src/app/pages/campus-map-v3/utils/renderers/floor-background-renderer.ts 24795var FloorBackgroundRenderer = class { 24796 constructor(config) { 24797 this.loadedTextures = /* @__PURE__ */ new Map(); 24798 this.meshes = /* @__PURE__ */ new Map(); 24799 this.getMediaUrl = config.getMediaUrl; 24800 this.onDirty = config.onDirty; 24801 this.group = new Group(); 24802 this.group.name = "floor-backgrounds"; 24803 this.group.visible = false; 24804 this.textureLoader = new TextureLoader(); 24805 } 24806 /** 24807 * Render floor backgrounds for the given floors. 24808 * Clears existing backgrounds and loads new ones. 24809 * @param floors Floors to render backgrounds for 24810 * @param selectedFloorIds Set of floor IDs to filter by. 24811 * Empty Set = no floors selected (don't render anything). 24812 * Non-empty Set = render only selected floors. 24813 */ 24814 renderFloorBackgrounds(floors, selectedFloorIds) { 24815 return __async(this, null, function* () { 24816 var _a3; 24817 this.clearFloorBackgrounds(); 24818 if (selectedFloorIds.size === 0) { 24819 return; 24820 } 24821 const floorsToRender = floors.filter((f) => f.entity.id && selectedFloorIds.has(f.entity.id)); 24822 for (const floor of floorsToRender) { 24823 const backgrounds = ((_a3 = floor.floorBackgrounds) == null ? void 0 : _a3.filter((bg) => !bg.entity.isDeleted)) || []; 24824 for (const bg of backgrounds) { 24825 yield this.renderBackground(bg); 24826 } 24827 } 24828 this.onDirty(); 24829 }); 24830 } 24831 /** 24832 * Clear all floor background meshes and dispose textures. 24833 */ 24834 clearFloorBackgrounds() { 24835 for (const mesh of this.meshes.values()) { 24836 mesh.geometry.dispose(); 24837 if (mesh.material instanceof MeshBasicMaterial) { 24838 mesh.material.dispose(); 24839 } 24840 this.group.remove(mesh); 24841 } 24842 this.meshes.clear(); 24843 for (const texture of this.loadedTextures.values()) { 24844 texture.dispose(); 24845 } 24846 this.loadedTextures.clear(); 24847 this.onDirty(); 24848 } 24849 /** 24850 * Set visibility of the floor backgrounds layer. 24851 */ 24852 setVisible(visible) { 24853 this.group.visible = visible; 24854 this.onDirty(); 24855 } 24856 get isVisible() { 24857 return this.group.visible; 24858 } 24859 /** 24860 * Render a single floor background image. 24861 * @param bg Background model to render 24862 */ 24863 renderBackground(bg) { 24864 return __async(this, null, function* () { 24865 var _a3, _b2, _c, _d; 24866 const uploadId = bg.entity.uploadId; 24867 if (!uploadId) 24868 return; 24869 const entityId = bg.entity.id; 24870 if (!entityId) 24871 return; 24872 try { 24873 const url = this.getMediaUrl(uploadId); 24874 const texture = yield this.loadTexture(url, uploadId); 24875 const image = texture.image; 24876 const imgWidth = image.width; 24877 const imgHeight = image.height; 24878 const scale = (_a3 = bg.entity.scale) != null ? _a3 : 1; 24879 const geometry = new PlaneGeometry(imgWidth * scale, imgHeight * scale); 24880 const material = new MeshBasicMaterial({ 24881 map: texture, 24882 transparent: true, 24883 side: DoubleSide, 24884 depthWrite: false 24885 }); 24886 const mesh = new Mesh(geometry, material);
24887 mesh.name = `floor-bg-${entityId}`; 24888 const x = (_b2 = bg.entity.locationX) != null ? _b2 : 0; 24889 const y = (_c = bg.entity.locationY) != null ? _c : 0; 24890 const rotation = (_d = bg.entity.rotation) != null ? _d : 0; 24891 mesh.position.set(x, y, Z_LAYERS.FLOOR_BACKGROUND); 24892 mesh.rotation.z = rotation * Math.PI / 180; 24893 mesh.userData["entityId"] = entityId; 24894 mesh.userData["entityType"] = "floorBackground"; 24895 this.meshes.set(entityId, mesh); 24896 this.group.add(mesh); 24897 } catch (error) { 24898 } 24899 }); 24900 } 24901 /** 24902 * Load a texture with caching. 24903 */ 24904 loadTexture(url, cacheKey) { 24905 const cached = this.loadedTextures.get(cacheKey); 24906 if (cached) { 24907 return Promise.resolve(cached); 24908 } 24909 return new Promise((resolve, reject) => { 24910 this.textureLoader.load(url, (texture) => { 24911 this.loadedTextures.set(cacheKey, texture); 24912 resolve(texture); 24913 }, void 0, (error) => reject(error)); 24914 }); 24915 } 24916 /** 24917 * Dispose all resources. 24918 */ 24919 dispose() { 24920 this.clearFloorBackgrounds(); 24921 } 24922}; 24923
24924// src/app/pages/campus-map-v3/utils/level-renderer.ts 24925var LevelRenderer = class { 24926 // ========================================================================= 24927 // Public Group Accessors (unchanged API) 24928 // ========================================================================= 24929 get floorsGroup() { 24930 return this.floorRoomRenderer.floorsGroup; 24931 } 24932 get nodesGroup() { 24933 return this.nodeRenderer.nodesGroup; 24934 } 24935 get highlightGroup() { 24936 return this.highlightRenderer.highlightGroup; 24937 } 24938 get ghostGroup() { 24939 return this.highlightRenderer.ghostGroup; 24940 } 24941 get groundReferenceGroup() { 24942 return this.floorRoomRenderer.groundReferenceGroup; 24943 } 24944 get labelsGroup() { 24945 return this.labelRenderer.labelsGroup; 24946 } 24947 get cornerHandlesGroup() { 24948 return this.shapeEditRenderer.cornerHandlesGroup; 24949 } 24950 get edgeMidpointsGroup() { 24951 return this.shapeEditRenderer.edgeMidpointsGroup; 24952 } 24953 get polygonPreviewGroup() { 24954 return this.shapeEditRenderer.polygonPreviewGroup; 24955 } 24956 get roomWallDimensionsGroup() { 24957 return this.shapeEditRenderer.roomWallDimensionsGroup; 24958 } 24959 get rotationHandleGroup() { 24960 return this.rotationHandleRenderer.group; 24961 } 24962 get floorBackgroundsGroup() { 24963 if (!this.floorBackgroundRenderer) { 24964 const emptyGroup = new Group(); 24965 emptyGroup.name = "floor-backgrounds-placeholder"; 24966 return emptyGroup; 24967 } 24968 return this.floorBackgroundRenderer.group; 24969 } 24970 constructor(config) { 24971 this.floorBackgroundRenderer = null; 24972 this.materials = config.materials; 24973 this.onDirty = config.onDirty; 24974 this.floorRoomRenderer = new FloorRoomRenderer({ 24975 materials: config.materials, 24976 onDirty: config.onDirty 24977 }); 24978 this.nodeRenderer = new NodeConnectionRenderer({ 24979 materials: config.materials, 24980 onDirty: config.onDirty, 24981 getCameraZoom: config.getCameraZoom 24982 }); 24983 this.labelRenderer = new LabelRenderer({ 24984 onDirty: config.onDirty 24985 }); 24986 this.shapeEditRenderer = new ShapeEditRenderer({ 24987 materials: config.materials, 24988 onDirty: config.onDirty, 24989 getCameraZoom: config.getCameraZoom 24990 }); 24991 this.highlightRenderer = new SelectionHighlightRenderer({ 24992 materials: config.materials, 24993 onDirty: config.onDirty, 24994 findEntityMesh: (id, type) => this.findEntityMesh(id, type), 24995 getCameraZoom: config.getCameraZoom 24996 }); 24997 this.rotationHandleRenderer = new RotationHandleRenderer({ 24998 materials: config.materials, 24999 onDirty: config.onDirty 25000 }); 25001 if (config.getMediaUrl) { 25002 this.floorBackgroundRenderer = new FloorBackgroundRenderer({ 25003 getMediaUrl: config.getMediaUrl, 25004 onDirty: config.onDirty 25005 }); 25006 } 25007 } 25008 // ========================================================================= 25009 // Floor/Room Rendering (delegate to FloorRoomRenderer) 25010 // ========================================================================= 25011 renderFloors(floors) { 25012 this.floorRoomRenderer.renderFloors(floors); 25013 } 25014 clearFloors() { 25015 this.floorRoomRenderer.clearFloors(); 25016 } 25017 // ========================================================================= 25018 // Floor Background Rendering (delegate to FloorBackgroundRenderer) 25019 // ========================================================================= 25020 renderFloorBackgrounds(floors, selectedFloorIds) { 25021 return __async(this, null, function* () { 25022 if (this.floorBackgroundRenderer) { 25023 yield this.floorBackgroundRenderer.renderFloorBackgrounds(floors, selectedFloorIds); 25024 } 25025 }); 25026 } 25027 clearFloorBackgrounds() { 25028 if (this.floorBackgroundRenderer) { 25029 this.floorBackgroundRenderer.clearFloorBackgrounds(); 25030 } 25031 } 25032 setFloorBackgroundsVisible(visible) { 25033 if (this.floorBackgroundRenderer) { 25034 this.floorBackgroundRenderer.setVisible(visible); 25035 } 25036 } 25037 get floorBackgroundsVisible() { 25038 var _a3, _b2; 25039 return (_b2 = (_a3 = this.floorBackgroundRenderer) == null ? void 0 : _a3.isVisible) != null ? _b2 : false; 25040 } 25041 // ========================================================================= 25042 // Node/Connection Rendering (delegate to NodeConnectionRenderer) 25043 // ========================================================================= 25044 renderNodes(nodes, connections) { 25045 this.nodeRenderer.renderNodes(nodes, connections); 25046 } 25047 clearNodes() { 25048 this.nodeRenderer.clearNodes(); 25049 } 25050 setNodesVisible(visible) { 25051 this.nodeRenderer.setNodesVisible(visible); 25052 } 25053 get nodesVisible() { 25054 return this.nodeRenderer.nodesVisible; 25055 } 25056 updateNodesScreenSpaceScale(canvasHeight) { 25057 this.nodeRenderer.updateNodesScreenSpaceScale(canvasHeight); 25058 } 25059 // ========================================================================= 25060 // Visibility Controls (delegate to FloorRoomRenderer) 25061 // ========================================================================= 25062 setBuildingsVisible(visible) { 25063 this.floorRoomRenderer.setBuildingsVisible(visible); 25064 } 25065 get buildingsVisible() { 25066 return this.floorRoomRenderer.buildingsVisible; 25067 } 25068 setRoomsVisible(visible) { 25069 this.floorRoomRenderer.setRoomsVisible(visible); 25070 } 25071 get roomsVisible() { 25072 return this.floorRoomRenderer.roomsVisible; 25073 } 25074 setFillsVisible(visible) { 25075 this.floorRoomRenderer.setFillsVisible(visible); 25076 } 25077 // ========================================================================= 25078 // Room Labels (delegate to LabelRenderer) 25079 // ========================================================================= 25080 renderRoomLabels(floors) { 25081 this.labelRenderer.renderRoomLabels(floors); 25082 } 25083 setLabelsVisible(visible) { 25084 this.labelRenderer.setLabelsVisible(visible); 25085 } 25086 get labelsVisible() { 25087 return this.labelRenderer.labelsVisible; 25088 } 25089 clearLabels() { 25090 this.labelRenderer.clearLabels(); 25091 } 25092 updateLabelsRotation(sceneRotation) { 25093 this.labelRenderer.updateLabelsRotation(sceneRotation); 25094 }
25095 updateLabelsDetailLevel(zoom, canvasHeight) { 25096 this.labelRenderer.updateLabelsDetailLevel(zoom, canvasHeight); 25097 } 25098 // ========================================================================= 25099 // Selection Highlighting (delegate to SelectionHighlightRenderer) 25100 // ========================================================================= 25101 highlightEntity(id, type) { 25102 this.highlightRenderer.highlightEntity(id, type); 25103 } 25104 unhighlightEntity(id) { 25105 this.highlightRenderer.unhighlightEntity(id); 25106 } 25107 clearHighlights() { 25108 this.highlightRenderer.clearHighlights(); 25109 } 25110 isHighlighted(id) { 25111 return this.highlightRenderer.isHighlighted(id); 25112 } 25113 highlightDestinationNode(id) { 25114 this.highlightRenderer.highlightDestinationNode(id); 25115 } 25116 unhighlightDestinationNode(id) { 25117 this.highlightRenderer.unhighlightDestinationNode(id); 25118 } 25119 clearDestinationHighlights() { 25120 this.highlightRenderer.clearDestinationHighlights(); 25121 } 25122 isDestinationHighlighted(id) { 25123 return this.highlightRenderer.isDestinationHighlighted(id); 25124 } 25125 /** 25126 * Update highlight scales to match node scales. 25127 * Called when camera zoom changes to keep highlights proportional to nodes. 25128 * @param canvasHeight Canvas height in CSS pixels 25129 */ 25130 updateHighlightsScale(canvasHeight) { 25131 for (const [entityId, scale] of this.nodeRenderer.getNodeScales()) { 25132 this.highlightRenderer.updateHighlightScale(entityId, scale); 25133 this.highlightRenderer.updateDestinationHighlightScale(entityId, scale); 25134 } 25135 } 25136 // ========================================================================= 25137 // Ghost System (delegate to SelectionHighlightRenderer) 25138 // ========================================================================= 25139 createGhost(id, type) { 25140 this.highlightRenderer.createGhost(id, type); 25141 } 25142 removeGhost(id) { 25143 this.highlightRenderer.removeGhost(id); 25144 } 25145 clearGhosts() { 25146 this.highlightRenderer.clearGhosts(); 25147 } 25148 // ========================================================================= 25149 // Ground Reference (delegate to FloorRoomRenderer) 25150 // ========================================================================= 25151 renderGroundReference(floors) { 25152 this.floorRoomRenderer.renderGroundReference(floors); 25153 } 25154 clearGroundReference() { 25155 this.floorRoomRenderer.clearGroundReference(); 25156 } 25157 setGroundReferenceVisible(visible) { 25158 this.floorRoomRenderer.setGroundReferenceVisible(visible); 25159 } 25160 // ========================================================================= 25161 // Pending Connection Line (delegate to NodeConnectionRenderer) 25162 // ========================================================================= 25163 renderPendingConnection(fromX, fromY, toX, toY, validTarget = false) { 25164 this.nodeRenderer.renderPendingConnection(fromX, fromY, toX, toY, validTarget); 25165 } 25166 clearPendingConnection() { 25167 this.nodeRenderer.clearPendingConnection(); 25168 } 25169 // ========================================================================= 25170 // Mesh Movement (coordinates between renderers) 25171 // ========================================================================= 25172 /** 25173 * Move an entity's mesh by delta for drag preview. 25174 * Does NOT modify entity data - visual only. 25175 */ 25176 moveEntityMesh(id, type, dx, dy) { 25177 let mesh = null; 25178 if (type === "node" || type === "nodeConnection") { 25179 mesh = this.nodeRenderer.findEntityMesh(id); 25180 } else { 25181 mesh = this.floorRoomRenderer.findEntityMesh(id); 25182 } 25183 if (mesh) { 25184 mesh.position.x += dx; 25185 mesh.position.y += dy; 25186 } 25187 this.highlightRenderer.moveEntityHighlight(id, dx, dy); 25188 if (type === "room" || type === "floor") { 25189 this.movePolygonOutline(id, type, dx, dy); 25190 } 25191 this.onDirty(); 25192 } 25193 /** 25194 * Move polygon outline (room/floor) by delta. 25195 */ 25196 movePolygonOutline(id, type, dx, dy) { 25197 const outlineName = type === "room" ? `room-outline-${id}` : `floor-outline-${id}`; 25198 this.floorsGroup.traverse((obj) => { 25199 if (obj instanceof Line && obj.name === outlineName) { 25200 obj.position.x += dx; 25201 obj.position.y += dy; 25202 } 25203 }); 25204 } 25205 /** 25206 * Get current mesh position for an entity. 25207 */
25208 getEntityMeshPosition(id, type) { 25209 return this.highlightRenderer.getEntityMeshPosition(id, type); 25210 } 25211 /** 25212 * Rotate an entity's mesh around a pivot for rotation preview. 25213 * Does NOT modify entity data - visual only. 25214 */ 25215 rotateEntityMesh(id, type, angleDelta, pivot) { 25216 let mesh = null; 25217 if (type === "node" || type === "nodeConnection") { 25218 mesh = this.nodeRenderer.findEntityMesh(id); 25219 } else { 25220 mesh = this.floorRoomRenderer.findEntityMesh(id); 25221 } 25222 if (mesh) { 25223 this.rotateMeshAroundPivot(mesh, angleDelta, pivot); 25224 } 25225 const highlight = this.highlightRenderer["highlightMeshes"].get(id); 25226 if (highlight) { 25227 this.rotateMeshAroundPivot(highlight, angleDelta, pivot); 25228 } 25229 if (type === "room" || type === "floor") { 25230 this.rotatePolygonOutline(id, type, angleDelta, pivot); 25231 } 25232 this.onDirty(); 25233 } 25234 /** 25235 * Rotate a mesh around a pivot point. 25236 */ 25237 rotateMeshAroundPivot(mesh, angleDelta, pivot) { 25238 const currentPos = { x: mesh.position.x, y: mesh.position.y }; 25239 const rotated = PointUtils.rotate(currentPos, angleDelta, pivot); 25240 mesh.position.x = rotated.x; 25241 mesh.position.y = rotated.y; 25242 mesh.rotation.z += angleDelta; 25243 } 25244 /** 25245 * Rotate polygon outline around pivot. 25246 */ 25247 rotatePolygonOutline(id, type, angleDelta, pivot) { 25248 const outlineName = type === "room" ? `room-outline-${id}` : `floor-outline-${id}`; 25249 this.floorsGroup.traverse((obj) => { 25250 if (obj instanceof Line && obj.name === outlineName) { 25251 this.rotateMeshAroundPivot(obj, angleDelta, pivot); 25252 } 25253 }); 25254 } 25255 // ========================================================================= 25256 // Rotation Handle (delegate to RotationHandleRenderer) 25257 // ========================================================================= 25258 /** 25259 * Render the rotation handle for a selection. 25260 * @param pivot Center point of the selection 25261 * @param radius Distance from pivot to handle 25262 */ 25263 renderRotationHandle(pivot, radius) { 25264 this.rotationHandleRenderer.render(pivot, radius); 25265 } 25266 /** 25267 * Hide the rotation handle. 25268 */ 25269 hideRotationHandle() { 25270 this.rotationHandleRenderer.hide(); 25271 } 25272 /** 25273 * Update rotation handle hover state. 25274 */ 25275 setRotationHandleHovered(hovered) { 25276 this.rotationHandleRenderer.setHovered(hovered); 25277 } 25278 /** 25279 * Check if rotation handle is visible. 25280 */ 25281 isRotationHandleVisible() { 25282 return this.rotationHandleRenderer.isVisible(); 25283 } 25284 // ========================================================================= 25285 // Floor Detection (delegate to FloorRoomRenderer) 25286 // ========================================================================= 25287 getFloorAtPosition(localX, localY) { 25288 return this.floorRoomRenderer.getFloorAtPosition(localX, localY); 25289 } 25290 // ========================================================================= 25291 // Shape Editing (delegate to ShapeEditRenderer) 25292 // ========================================================================= 25293 renderCornerHandles(corners) { 25294 this.shapeEditRenderer.renderCornerHandles(corners); 25295 } 25296 clearCornerHandles() { 25297 this.shapeEditRenderer.clearCornerHandles(); 25298 } 25299 renderEdgeMidpoints(midpoints, hoveredIndex) { 25300 this.shapeEditRenderer.renderEdgeMidpoints(midpoints, hoveredIndex); 25301 } 25302 clearEdgeMidpoints() { 25303 this.shapeEditRenderer.clearEdgeMidpoints(); 25304 } 25305 renderRoomWallDimension(wall, selected) { 25306 this.shapeEditRenderer.renderRoomWallDimension(wall, selected); 25307 } 25308 clearRoomWallDimension() { 25309 this.shapeEditRenderer.clearRoomWallDimension(); 25310 } 25311 renderPolygonDrawPreview(vertices, cursorPosition, canClose) { 25312 this.shapeEditRenderer.renderPolygonDrawPreview(vertices, cursorPosition, canClose); 25313 } 25314 clearPolygonPreview() { 25315 this.shapeEditRenderer.clearPolygonPreview(); 25316 } 25317 calculateEdgeMidpoints(corners) { 25318 return this.shapeEditRenderer.calculateEdgeMidpoints(corners); 25319 } 25320 // ========================================================================= 25321 // Entity Mesh Lookup (coordinates across renderers) 25322 // ========================================================================= 25323 /** 25324 * Find a mesh/line by entity ID and type. 25325 */ 25326 findEntityMesh(id, type) { 25327 if (type === "node" || type === "nodeConnection") {
25328 return this.nodeRenderer.findEntityMesh(id); 25329 } else { 25330 return this.floorRoomRenderer.findEntityMesh(id); 25331 } 25332 } 25333 // ========================================================================= 25334 // Lifecycle 25335 // ========================================================================= 25336 dispose() { 25337 var _a3; 25338 this.floorRoomRenderer.dispose(); 25339 this.nodeRenderer.dispose(); 25340 this.labelRenderer.dispose(); 25341 this.shapeEditRenderer.dispose(); 25342 this.highlightRenderer.dispose(); 25343 this.rotationHandleRenderer.dispose(); 25344 (_a3 = this.floorBackgroundRenderer) == null ? void 0 : _a3.dispose(); 25345 } 25346}; 25347
25348// src/app/pages/campus-map-v3/campus-map-v3.page.ts 25349var _c07 = ["canvasContainer"]; 25350var _forTrack08 = ($index, $item) => $item.id; 25351function CampusMapV3Page_Conditional_4_Template(rf, ctx) { 25352 if (rf & 1) { 25353 \u0275\u0275elementStart(0, "div", 4)(1, "div", 19)(2, "span", 20); 25354 \u0275\u0275text(3, "Loading 3D Model"); 25355 \u0275\u0275elementEnd(); 25356 \u0275\u0275elementStart(4, "div", 21); 25357 \u0275\u0275element(5, "div", 22); 25358 \u0275\u0275elementEnd(); 25359 \u0275\u0275elementStart(6, "span", 23); 25360 \u0275\u0275text(7); 25361 \u0275\u0275elementEnd()()(); 25362 } 25363 if (rf & 2) { 25364 const ctx_r1 = \u0275\u0275nextContext(); 25365 \u0275\u0275advance(5); 25366 \u0275\u0275styleProp("width", ctx_r1.model3DLoadProgress, "%"); 25367 \u0275\u0275advance(2); 25368 \u0275\u0275textInterpolate1("", ctx_r1.model3DLoadProgress, "%"); 25369 } 25370} 25371function CampusMapV3Page_Conditional_6_Template(rf, ctx) { 25372 if (rf & 1) { 25373 const _r3 = \u0275\u0275getCurrentView(); 25374 \u0275\u0275elementStart(0, "div", 24)(1, "div", 25)(2, "span", 26); 25375 \u0275\u0275text(3, "Pick default destination node"); 25376 \u0275\u0275elementEnd(); 25377 \u0275\u0275elementStart(4, "span", 27); 25378 \u0275\u0275text(5); 25379 \u0275\u0275elementEnd()(); 25380 \u0275\u0275elementStart(6, "button", 28); 25381 \u0275\u0275listener("click", function CampusMapV3Page_Conditional_6_Template_button_click_6_listener() { 25382 \u0275\u0275restoreView(_r3); 25383 const ctx_r1 = \u0275\u0275nextContext(); 25384 return \u0275\u0275resetView(ctx_r1.cancelDefaultDestinationPicker()); 25385 }); 25386 \u0275\u0275text(7, "Cancel"); 25387 \u0275\u0275elementEnd()(); 25388 } 25389 if (rf & 2) { 25390 const ctx_r1 = \u0275\u0275nextContext(); 25391 \u0275\u0275styleProp("left", ctx_r1.viewState.isMobileView() ? 15 : ctx_r1.viewState.sidebarCollapsed() ? 15 : 295, "px"); 25392 \u0275\u0275advance(5); 25393 \u0275\u0275textInterpolate1("Click a highlighted node for ", ctx_r1.defaultDestinationPickerBuilding.entity.name || "this building", "."); 25394 } 25395} 25396function CampusMapV3Page_Conditional_7_Template(rf, ctx) { 25397 if (rf & 1) { 25398 const _r4 = \u0275\u0275getCurrentView(); 25399 \u0275\u0275elementStart(0, "div", 29)(1, "div", 25)(2, "span", 26); 25400 \u0275\u0275text(3); 25401 \u0275\u0275elementEnd(); 25402 \u0275\u0275elementStart(4, "span", 27); 25403 \u0275\u0275text(5); 25404 \u0275\u0275elementEnd()(); 25405 \u0275\u0275elementStart(6, "div", 30)(7, "button", 31); 25406 \u0275\u0275listener("click", function CampusMapV3Page_Conditional_7_Template_button_click_7_listener() { 25407 \u0275\u0275restoreView(_r4); 25408 const ctx_r1 = \u0275\u0275nextContext(); 25409 return \u0275\u0275resetView(ctx_r1.applyNodeBuildingAssignment()); 25410 }); 25411 \u0275\u0275text(8, "Assign selected"); 25412 \u0275\u0275elementEnd(); 25413 \u0275\u0275elementStart(9, "button", 31); 25414 \u0275\u0275listener("click", function CampusMapV3Page_Conditional_7_Template_button_click_9_listener() { 25415 \u0275\u0275restoreView(_r4); 25416 const ctx_r1 = \u0275\u0275nextContext(); 25417 return \u0275\u0275resetView(ctx_r1.clearNodeAssignmentSelection()); 25418 }); 25419 \u0275\u0275text(10, "Clear"); 25420 \u0275\u0275elementEnd(); 25421 \u0275\u0275elementStart(11, "button", 28); 25422 \u0275\u0275listener("click", function CampusMapV3Page_Conditional_7_Template_button_click_11_listener() { 25423 \u0275\u0275restoreView(_r4); 25424 const ctx_r1 = \u0275\u0275nextContext(); 25425 return \u0275\u0275resetView(ctx_r1.cancelNodeBuildingAssignment()); 25426 }); 25427 \u0275\u0275text(12, "Cancel"); 25428 \u0275\u0275elementEnd()()(); 25429 } 25430 if (rf & 2) { 25431 const ctx_r1 = \u0275\u0275nextContext(); 25432 \u0275\u0275styleProp("left", ctx_r1.viewState.isMobileView() ? 15 : ctx_r1.viewState.sidebarCollapsed() ? 15 : 295, "px"); 25433 \u0275\u0275advance(3); 25434 \u0275\u0275textInterpolate1("Assign nodes to ", ctx_r1.nodeAssignmentBuilding.entity.name || "this building", ""); 25435 \u0275\u0275advance(2); 25436 \u0275\u0275textInterpolate1("", ctx_r1.assignmentSelectedNodeCount, " selected"); 25437 \u0275\u0275advance(2); 25438 \u0275\u0275property("disabled", ctx_r1.assignmentSelectedNodeCount === 0); 25439 \u0275\u0275advance(2); 25440 \u0275\u0275property("disabled", ctx_r1.assignmentSelectedNodeCount === 0); 25441 } 25442} 25443function CampusMapV3Page_Conditional_9_Template(rf, ctx) { 25444 if (rf & 1) { 25445 const _r5 = \u0275\u0275getCurrentView(); 25446 \u0275\u0275elementStart(0, "app-wall-length-inspector", 32); 25447 \u0275\u0275listener("lengthPreview", function CampusMapV3Page_Conditional_9_Template_app_w
25447all_length_inspector_lengthPreview_0_listener($event) { 25448 \u0275\u0275restoreView(_r5); 25449 const ctx_r1 = \u0275\u0275nextContext(); 25450 return \u0275\u0275resetView(ctx_r1.onWallLengthPreview($event)); 25451 })("commitRequested", function CampusMapV3Page_Conditional_9_Template_app_wall_length_inspector_commitRequested_0_listener() { 25452 \u0275\u0275restoreView(_r5); 25453 const ctx_r1 = \u0275\u0275nextContext(); 25454 return \u0275\u0275resetView(ctx_r1.onWallLengthCommit()); 25455 })("cancelRequested", function CampusMapV3Page_Conditional_9_Template_app_wall_length_inspector_cancelRequested_0_listener() { 25456 \u0275\u0275restoreView(_r5); 25457 const ctx_r1 = \u0275\u0275nextContext(); 25458 return \u0275\u0275resetView(ctx_r1.onWallLengthCancel()); 25459 }); 25460 \u0275\u0275elementEnd(); 25461 } 25462 if (rf & 2) { 25463 const ctx_r1 = \u0275\u0275nextContext(); 25464 \u0275\u0275property("wall", ctx)("error", ctx_r1.shapeEditState.wallLengthError()); 25465 } 25466} 25467function CampusMapV3Page_Conditional_14_Conditional_1_For_7_Template(rf, ctx) { 25468 if (rf & 1) { 25469 \u0275\u0275elementStart(0, "ion-select-option", 37); 25470 \u0275\u0275text(1); 25471 \u0275\u0275elementEnd(); 25472 } 25473 if (rf & 2) { 25474 const option_r8 = ctx.$implicit; 25475 \u0275\u0275property("value", option_r8.id); 25476 \u0275\u0275advance(); 25477 \u0275\u0275textInterpolate(option_r8.name); 25478 } 25479} 25480function CampusMapV3Page_Conditional_14_Conditional_1_Template(rf, ctx) { 25481 if (rf & 1) { 25482 const _r7 = \u0275\u0275getCurrentView(); 25483 \u0275\u0275elementStart(0, "div", 33)(1, "ion-select", 35); 25484 \u0275\u0275pipe(2, "translate"); 25485 \u0275\u0275listener("ionChange", function CampusMapV3Page_Conditional_14_Conditional_1_Template_ion_select_ionChange_1_listener($event) { 25486 \u0275\u0275restoreView(_r7); 25487 const ctx_r1 = \u0275\u0275nextContext(2); 25488 return \u0275\u0275resetView(ctx_r1.onModel3DSelected($event.detail.value)); 25489 }); 25490 \u0275\u0275elementStart(3, "ion-label", 36); 25491 \u0275\u0275text(4); 25492 \u0275\u0275pipe(5, "translate"); 25493 \u0275\u0275elementEnd(); 25494 \u0275\u0275repeaterCreate(6, CampusMapV3Page_Conditional_14_Conditional_1_For_7_Template, 2, 2, "ion-select-option", 37, _forTrack08); 25495 \u0275\u0275elementEnd()(); 25496 } 25497 if (rf & 2) { 25498 const ctx_r1 = \u0275\u0275nextContext(2); 25499 \u0275\u0275advance(); 25500 \u0275\u0275property("value", ctx_r1.activeModel3DId)("interface", "action-sheet"); 25501 \u0275\u0275attribute("aria-label", \u0275\u0275pipeBind1(2, 4, "campusMapV3.modelPanel.selectModel")); 25502 \u0275\u0275advance(3); 25503 \u0275\u0275textInterpolate(\u0275\u0275pipeBind1(5, 6, "campusMapV3.modelPanel.selectModel")); 25504 \u0275\u0275advance(2); 25505 \u0275\u0275repeater(ctx_r1.model3DOptions); 25506 } 25507} 25508function CampusMapV3Page_Conditional_14_Template(rf, ctx) { 25509 if (rf & 1) { 25510 const _r6 = \u0275\u0275getCurrentView(); 25511 \u0275\u0275elementStart(0, "app-toolbar-container", 14); 25512 \u0275\u0275template(1, CampusMapV3Page_Conditional_14_Conditional_1_Template, 8, 8, "div", 33); 25513 \u0275\u0275elementStart(2, "app-model-transform-panel", 34); 25514 \u0275\u0275listener("transformChanged", function CampusMapV3Page_Conditional_14_Template_app_model_transform_panel_transformChanged_2_listener($event) { 25515 \u0275\u0275restoreView(_r6); 25516 const ctx_r1 = \u0275\u0275nextContext(); 25517 return \u0275\u0275resetView(ctx_r1.onTransformChanged($event)); 25518 })("transformCommit", function CampusMapV3Page_Conditional_14_Template_app_model_transform_panel_transformCommit_2_listener($event) { 25519 \u0275\u0275restoreView(_r6); 25520 const ctx_r1 = \u0275\u0275nextContext(); 25521 return \u0275\u0275resetView(ctx_r1.onTransformCommit($event)); 25522 })("resetRequested", function CampusMapV3Page_Conditional_14_Template_app_model_transform_panel_resetRequested_2_listener() { 25523 \u0275\u0275restoreView(_r6); 25524 const ctx_r1 = \u0275\u0275nextContext(); 25525 return \u0275\u0275resetView(ctx_r1.onResetTransform()); 25526 })("closeRequested", function CampusMapV3Page_Conditional_14_Template_app_model_transform_panel_closeRequested_2_listener() { 25527 \u0275\u0275restoreView(_r6); 25528 const ctx_r1 = \u0275\u0275nextContext();
25529 return \u0275\u0275resetView(ctx_r1.onCloseTransformPanel()); 25530 })("centerToViewportRequested", function CampusMapV3Page_Conditional_14_Template_app_model_transform_panel_centerToViewportRequested_2_listener() { 25531 \u0275\u0275restoreView(_r6); 25532 const ctx_r1 = \u0275\u0275nextContext(); 25533 return \u0275\u0275resetView(ctx_r1.onCenterToViewport()); 25534 })("scaleToViewportRequested", function CampusMapV3Page_Conditional_14_Template_app_model_transform_panel_scaleToViewportRequested_2_listener() { 25535 \u0275\u0275restoreView(_r6); 25536 const ctx_r1 = \u0275\u0275nextContext(); 25537 return \u0275\u0275resetView(ctx_r1.onScaleToViewport()); 25538 })("centerToCampusRequested", function CampusMapV3Page_Conditional_14_Template_app_model_transform_panel_centerToCampusRequested_2_listener() { 25539 \u0275\u0275restoreView(_r6); 25540 const ctx_r1 = \u0275\u0275nextContext(); 25541 return \u0275\u0275resetView(ctx_r1.onCenterToCampus()); 25542 })("scaleToCampusRequested", function CampusMapV3Page_Conditional_14_Template_app_model_transform_panel_scaleToCampusRequested_2_listener() { 25543 \u0275\u0275restoreView(_r6); 25544 const ctx_r1 = \u0275\u0275nextContext(); 25545 return \u0275\u0275resetView(ctx_r1.onScaleToCampus()); 25546 }); 25547 \u0275\u0275elementEnd()(); 25548 } 25549 if (rf & 2) { 25550 const ctx_r1 = \u0275\u0275nextContext(); 25551 \u0275\u0275advance(); 25552 \u0275\u0275conditional(ctx_r1.model3DOptions.length > 1 ? 1 : -1); 25553 \u0275\u0275advance(); 25554 \u0275\u0275property("transform", ctx_r1.currentTransform); 25555 } 25556} 25557function CampusMapV3Page_Conditional_15_Template(rf, ctx) { 25558 if (rf & 1) { 25559 const _r9 = \u0275\u0275getCurrentView(); 25560 \u0275\u0275elementStart(0, "app-modal-overlay", 38); 25561 \u0275\u0275listener("backdropClick", function CampusMapV3Page_Conditional_15_Template_app_modal_overlay_backdropClick_0_listener() { 25562 \u0275\u0275restoreView(_r9); 25563 const ctx_r1 = \u0275\u0275nextContext(); 25564 return \u0275\u0275resetView(ctx_r1.onNodeEditPanelCancel()); 25565 }); 25566 \u0275\u0275elementStart(1, "app-node-edit-panel", 39); 25567 \u0275\u0275listener("nodeChanged", function CampusMapV3Page_Conditional_15_Template_app_node_edit_panel_nodeChanged_1_listener($event) { 25568 \u0275\u0275restoreView(_r9); 25569 const ctx_r1 = \u0275\u0275nextContext(); 25570 return \u0275\u0275resetView(ctx_r1.onNodeEditPanelChange($event)); 25571 })("applyRequested", function CampusMapV3Page_Conditional_15_Template_app_node_edit_panel_applyRequested_1_listener() { 25572 \u0275\u0275restoreView(_r9); 25573 const ctx_r1 = \u0275\u0275nextContext(); 25574 return \u0275\u0275resetView(ctx_r1.onNodeEditPanelApply()); 25575 })("cancelRequested", function CampusMapV3Page_Conditional_15_Template_app_node_edit_panel_cancelRequested_1_listener() { 25576 \u0275\u0275restoreView(_r9); 25577 const ctx_r1 = \u0275\u0275nextContext(); 25578 return \u0275\u0275resetView(ctx_r1.onNodeEditPanelCancel()); 25579 })("closeRequested", function CampusMapV3Page_Conditional_15_Template_app_node_edit_panel_closeRequested_1_listener() { 25580 \u0275\u0275restoreView(_r9); 25581 const ctx_r1 = \u0275\u0275nextContext(); 25582 return \u0275\u0275resetView(ctx_r1.onNodeEditPanelCancel()); 25583 }); 25584 \u0275\u0275elementEnd()(); 25585 } 25586 if (rf & 2) { 25587 const ctx_r1 = \u0275\u0275nextContext(); 25588 \u0275\u0275advance(); 25589 \u0275\u0275property("node", ctx_r1.entityEdit.editingNode())("floorName", ctx_r1.entityEdit.editingNodeFloorName())("isNew", ctx_r1.entityEdit.isNodeNew())("buildings", ctx_r1.getAllBuildingsInCampus()); 25590 } 25591} 25592function CampusMapV3Page_Conditional_16_Template(rf, ctx) { 25593 if (rf & 1) { 25594 const _r10 = \u0275\u0275getCurrentView(); 25595 \u0275\u0275elementStart(0, "app-modal-overlay", 38); 25596 \u0275\u0275listener("backdropClick", function CampusMapV3Page_Conditional_16_Template_app_modal_overlay_backdropClick_0_listener() { 25597 \u0275\u0275restoreView(_r10); 25598 const ctx_r1 = \u0275\u0275nextContext(); 25599 return \u0275\u0275resetView(ctx_r1.onFloorEditPanelCancel()); 25600 }); 25601 \u0275\u0275elementStart(1, "app-floor-edit-panel", 40); 25602 \u0275\u0275listener("floorChanged", function CampusMapV3Page_Conditional_16_Template_app_floor_edit_panel_floorChanged_1_listener($event) { 25603 \u0275\u0275restoreView(_r10); 25604 const ctx_r1 = \u0275\u0275nextContext();
25605 return \u0275\u0275resetView(ctx_r1.onFloorEditPanelChange($event)); 25606 })("applyRequested", function CampusMapV3Page_Conditional_16_Template_app_floor_edit_panel_applyRequested_1_listener() { 25607 \u0275\u0275restoreView(_r10); 25608 const ctx_r1 = \u0275\u0275nextContext(); 25609 return \u0275\u0275resetView(ctx_r1.onFloorEditPanelApply()); 25610 })("cancelRequested", function CampusMapV3Page_Conditional_16_Template_app_floor_edit_panel_cancelRequested_1_listener() { 25611 \u0275\u0275restoreView(_r10); 25612 const ctx_r1 = \u0275\u0275nextContext(); 25613 return \u0275\u0275resetView(ctx_r1.onFloorEditPanelCancel()); 25614 })("closeRequested", function CampusMapV3Page_Conditional_16_Template_app_floor_edit_panel_closeRequested_1_listener() { 25615 \u0275\u0275restoreView(_r10); 25616 const ctx_r1 = \u0275\u0275nextContext(); 25617 return \u0275\u0275resetView(ctx_r1.onFloorEditPanelCancel()); 25618 })("drawOutlineRequested", function CampusMapV3Page_Conditional_16_Template_app_floor_edit_panel_drawOutlineRequested_1_listener() { 25619 \u0275\u0275restoreView(_r10); 25620 const ctx_r1 = \u0275\u0275nextContext(); 25621 return \u0275\u0275resetView(ctx_r1.onFloorDrawOutline()); 25622 })("editOutlineRequested", function CampusMapV3Page_Conditional_16_Template_app_floor_edit_panel_editOutlineRequested_1_listener() { 25623 \u0275\u0275restoreView(_r10); 25624 const ctx_r1 = \u0275\u0275nextContext(); 25625 return \u0275\u0275resetView(ctx_r1.onFloorEditOutline()); 25626 }); 25627 \u0275\u0275elementEnd()(); 25628 } 25629 if (rf & 2) { 25630 const ctx_r1 = \u0275\u0275nextContext(); 25631 \u0275\u0275advance(); 25632 \u0275\u0275property("floor", ctx_r1.entityEdit.editingFloor())("isNew", ctx_r1.entityEdit.isFloorNew()); 25633 } 25634} 25635function CampusMapV3Page_Conditional_17_Template(rf, ctx) { 25636 if (rf & 1) { 25637 const _r11 = \u0275\u0275getCurrentView(); 25638 \u0275\u0275elementStart(0, "app-modal-overlay", 38); 25639 \u0275\u0275listener("backdropClick", function CampusMapV3Page_Conditional_17_Template_app_modal_overlay_backdropClick_0_listener() { 25640 \u0275\u0275restoreView(_r11); 25641 const ctx_r1 = \u0275\u0275nextContext(); 25642 return \u0275\u0275resetView(ctx_r1.onBuildingEditPanelCancel()); 25643 }); 25644 \u0275\u0275elementStart(1, "app-building-edit-panel", 41); 25645 \u0275\u0275listener("buildingChanged", function CampusMapV3Page_Conditional_17_Template_app_building_edit_panel_buildingChanged_1_listener($event) { 25646 \u0275\u0275restoreView(_r11); 25647 const ctx_r1 = \u0275\u0275nextContext(); 25648 return \u0275\u0275resetView(ctx_r1.onBuildingEditPanelChange($event)); 25649 })("pickDefaultDestinationRequested", function CampusMapV3Page_Conditional_17_Template_app_building_edit_panel_pickDefaultDestinationRequested_1_listener() { 25650 \u0275\u0275restoreView(_r11); 25651 const ctx_r1 = \u0275\u0275nextContext(); 25652 return \u0275\u0275resetView(ctx_r1.onBuildingPickDefaultDestination()); 25653 })("clearDefaultDestinationRequested", function CampusMapV3Page_Conditional_17_Template_app_building_edit_panel_clearDefaultDestinationRequested_1_listener() { 25654 \u0275\u0275restoreView(_r11); 25655 const ctx_r1 = \u0275\u0275nextContext(); 25656 return \u0275\u0275resetView(ctx_r1.onBuildingClearDefaultDestination()); 25657 })("assignNodesRequested", function CampusMapV3Page_Conditional_17_Template_app_building_edit_panel_assignNodesRequested_1_listener() { 25658 \u0275\u0275restoreView(_r11); 25659 const ctx_r1 = \u0275\u0275nextContext(); 25660 return \u0275\u0275resetView(ctx_r1.onBuildingAssignNodes()); 25661 })("applyRequested", function CampusMapV3Page_Conditional_17_Template_app_building_edit_panel_applyRequested_1_listener() { 25662 \u0275\u0275restoreView(_r11); 25663 const ctx_r1 = \u0275\u0275nextContext(); 25664 return \u0275\u0275resetView(ctx_r1.onBuildingEditPanelApply()); 25665 })("cancelRequested", function CampusMapV3Page_Conditional_17_Template_app_building_edit_panel_cancelRequested_1_listener() { 25666 \u0275\u0275restoreView(_r11); 25667 const ctx_r1 = \u0275\u0275nextContext(); 25668 return \u0275\u0275resetView(ctx_r1.onBuildingEditPanelCancel()); 25669 })("closeRequested", function CampusMapV3Page_Conditional_17_Template_app_building_edit_panel_closeRequested_1_listener() { 25670 \u0275\u0275restoreView(_r11); 25671 const ctx_r1 = \u0275\u0275nextContext();
25672 return \u0275\u0275resetView(ctx_r1.onBuildingEditPanelCancel()); 25673 }); 25674 \u0275\u0275elementEnd()(); 25675 } 25676 if (rf & 2) { 25677 const ctx_r1 = \u0275\u0275nextContext(); 25678 \u0275\u0275advance(); 25679 \u0275\u0275property("building", ctx_r1.entityEdit.editingBuilding())("isNew", ctx_r1.entityEdit.isBuildingNew())("assignedNodeCount", ctx_r1.getAssignedNodeCount(ctx_r1.entityEdit.editingBuilding()))("eligibleNodeCount", ctx_r1.getEligibleNodeCount(ctx_r1.entityEdit.editingBuilding())); 25680 } 25681} 25682function CampusMapV3Page_Conditional_18_Template(rf, ctx) { 25683 if (rf & 1) { 25684 const _r12 = \u0275\u0275getCurrentView(); 25685 \u0275\u0275elementStart(0, "app-modal-overlay", 38); 25686 \u0275\u0275listener("backdropClick", function CampusMapV3Page_Conditional_18_Template_app_modal_overlay_backdropClick_0_listener() { 25687 \u0275\u0275restoreView(_r12); 25688 const ctx_r1 = \u0275\u0275nextContext(); 25689 return \u0275\u0275resetView(ctx_r1.onRoomEditPanelCancel()); 25690 }); 25691 \u0275\u0275elementStart(1, "app-room-edit-panel", 42); 25692 \u0275\u0275listener("roomChanged", function CampusMapV3Page_Conditional_18_Template_app_room_edit_panel_roomChanged_1_listener($event) { 25693 \u0275\u0275restoreView(_r12); 25694 const ctx_r1 = \u0275\u0275nextContext(); 25695 return \u0275\u0275resetView(ctx_r1.onRoomEditPanelChange($event)); 25696 })("applyRequested", function CampusMapV3Page_Conditional_18_Template_app_room_edit_panel_applyRequested_1_listener() { 25697 \u0275\u0275restoreView(_r12); 25698 const ctx_r1 = \u0275\u0275nextContext(); 25699 return \u0275\u0275resetView(ctx_r1.onRoomEditPanelApply()); 25700 })("cancelRequested", function CampusMapV3Page_Conditional_18_Template_app_room_edit_panel_cancelRequested_1_listener() { 25701 \u0275\u0275restoreView(_r12); 25702 const ctx_r1 = \u0275\u0275nextContext(); 25703 return \u0275\u0275resetView(ctx_r1.onRoomEditPanelCancel()); 25704 })("closeRequested", function CampusMapV3Page_Conditional_18_Template_app_room_edit_panel_closeRequested_1_listener() { 25705 \u0275\u0275restoreView(_r12); 25706 const ctx_r1 = \u0275\u0275nextContext(); 25707 return \u0275\u0275resetView(ctx_r1.onRoomEditPanelCancel()); 25708 })("personAssigned", function CampusMapV3Page_Conditional_18_Template_app_room_edit_panel_personAssigned_1_listener($event) { 25709 \u0275\u0275restoreView(_r12); 25710 const ctx_r1 = \u0275\u0275nextContext(); 25711 return \u0275\u0275resetView(ctx_r1.onPersonAssigned($event)); 25712 })("personRemoved", function CampusMapV3Page_Conditional_18_Template_app_room_edit_panel_personRemoved_1_listener($event) { 25713 \u0275\u0275restoreView(_r12); 25714 const ctx_r1 = \u0275\u0275nextContext(); 25715 return \u0275\u0275resetView(ctx_r1.onPersonRemoved($event)); 25716 })("personCreated", function CampusMapV3Page_Conditional_18_Template_app_room_edit_panel_personCreated_1_listener($event) { 25717 \u0275\u0275restoreView(_r12); 25718 const ctx_r1 = \u0275\u0275nextContext(); 25719 return \u0275\u0275resetView(ctx_r1.onPersonCreated($event)); 25720 }); 25721 \u0275\u0275elementEnd()(); 25722 } 25723 if (rf & 2) { 25724 const ctx_r1 = \u0275\u0275nextContext(); 25725 \u0275\u0275advance(); 25726 \u0275\u0275property("room", ctx_r1.entityEdit.editingRoom())("isNew", ctx_r1.entityEdit.isRoomNew())("allPersons", ctx_r1.campusPersons); 25727 } 25728} 25729function CampusMapV3Page_Conditional_19_Template(rf, ctx) { 25730 if (rf & 1) { 25731 const _r13 = \u0275\u0275getCurrentView(); 25732 \u0275\u0275elementStart(0, "app-modal-overlay", 38); 25733 \u0275\u0275listener("backdropClick", function CampusMapV3Page_Conditional_19_Template_app_modal_overlay_backdropClick_0_listener() { 25734 \u0275\u0275restoreView(_r13); 25735 const ctx_r1 = \u0275\u0275nextContext(); 25736 return \u0275\u0275resetView(ctx_r1.onGroundPatchEditPanelCancel()); 25737 }); 25738 \u0275\u0275elementStart(1, "app-ground-patch-edit-panel", 43); 25739 \u0275\u0275listener("groundPatchChanged", function CampusMapV3Page_Conditional_19_Template_app_ground_patch_edit_panel_groundPatchChanged_1_listener($event) { 25740 \u0275\u0275restoreView(_r13); 25741 const ctx_r1 = \u0275\u0275nextContext();
25742 return \u0275\u0275resetView(ctx_r1.onGroundPatchEditPanelChange($event)); 25743 })("applyRequested", function CampusMapV3Page_Conditional_19_Template_app_ground_patch_edit_panel_applyRequested_1_listener() { 25744 \u0275\u0275restoreView(_r13); 25745 const ctx_r1 = \u0275\u0275nextContext(); 25746 return \u0275\u0275resetView(ctx_r1.onGroundPatchEditPanelApply()); 25747 })("cancelRequested", function CampusMapV3Page_Conditional_19_Template_app_ground_patch_edit_panel_cancelRequested_1_listener() { 25748 \u0275\u0275restoreView(_r13); 25749 const ctx_r1 = \u0275\u0275nextContext(); 25750 return \u0275\u0275resetView(ctx_r1.onGroundPatchEditPanelCancel()); 25751 })("closeRequested", function CampusMapV3Page_Conditional_19_Template_app_ground_patch_edit_panel_closeRequested_1_listener() { 25752 \u0275\u0275restoreView(_r13); 25753 const ctx_r1 = \u0275\u0275nextContext(); 25754 return \u0275\u0275resetView(ctx_r1.onGroundPatchEditPanelCancel()); 25755 })("editOutlineRequested", function CampusMapV3Page_Conditional_19_Template_app_ground_patch_edit_panel_editOutlineRequested_1_listener() { 25756 \u0275\u0275restoreView(_r13); 25757 const ctx_r1 = \u0275\u0275nextContext(); 25758 return \u0275\u0275resetView(ctx_r1.onGroundPatchEditOutline()); 25759 }); 25760 \u0275\u0275elementEnd()(); 25761 } 25762 if (rf & 2) { 25763 const ctx_r1 = \u0275\u0275nextContext(); 25764 \u0275\u0275advance(); 25765 \u0275\u0275property("groundPatch", ctx_r1.entityEdit.editingGroundPatch())("isNew", ctx_r1.entityEdit.isGroundPatchNew()); 25766 } 25767} 25768function CampusMapV3Page_Conditional_20_Template(rf, ctx) { 25769 if (rf & 1) { 25770 const _r14 = \u0275\u0275getCurrentView(); 25771 \u0275\u0275elementStart(0, "app-building-selection-modal", 44); 25772 \u0275\u0275listener("buildingSelected", function CampusMapV3Page_Conditional_20_Template_app_building_selection_modal_buildingSelected_0_listener($event) { 25773 \u0275\u0275restoreView(_r14); 25774 const ctx_r1 = \u0275\u0275nextContext(); 25775 return \u0275\u0275resetView(ctx_r1.onBuildingSelected($event)); 25776 })("cancelRequested", function CampusMapV3Page_Conditional_20_Template_app_building_selection_modal_cancelRequested_0_listener() { 25777 \u0275\u0275restoreView(_r14); 25778 const ctx_r1 = \u0275\u0275nextContext(); 25779 return \u0275\u0275resetView(ctx_r1.onBuildingSelectionCancel()); 25780 })("closeRequested", function CampusMapV3Page_Conditional_20_Template_app_building_selection_modal_closeRequested_0_listener() { 25781 \u0275\u0275restoreView(_r14); 25782 const ctx_r1 = \u0275\u0275nextContext(); 25783 return \u0275\u0275resetView(ctx_r1.onBuildingSelectionClose()); 25784 }); 25785 \u0275\u0275elementEnd(); 25786 } 25787 if (rf & 2) { 25788 const ctx_r1 = \u0275\u0275nextContext(); 25789 \u0275\u0275property("buildings", ctx_r1.getBuildingSelectionItems()); 25790 } 25791} 25792function CampusMapV3Page_Conditional_21_Template(rf, ctx) { 25793 if (rf & 1) { 25794 const _r15 = \u0275\u0275getCurrentView(); 25795 \u0275\u0275elementStart(0, "app-modal-overlay", 38); 25796 \u0275\u0275listener("backdropClick", function CampusMapV3Page_Conditional_21_Template_app_modal_overlay_backdropClick_0_listener() { 25797 \u0275\u0275restoreView(_r15); 25798 const ctx_r1 = \u0275\u0275nextContext(); 25799 return \u0275\u0275resetView(ctx_r1.onGpsDebugClosed()); 25800 }); 25801 \u0275\u0275elementStart(1, "app-gps-debug-modal", 45); 25802 \u0275\u0275listener("closeRequested", function CampusMapV3Page_Conditional_21_Template_app_gps_debug_modal_closeRequested_1_listener() { 25803 \u0275\u0275restoreView(_r15); 25804 const ctx_r1 = \u0275\u0275nextContext(); 25805 return \u0275\u0275resetView(ctx_r1.onGpsDebugClosed()); 25806 })("gpsNodeLocate", function CampusMapV3Page_Conditional_21_Template_app_gps_debug_modal_gpsNodeLocate_1_listener($event) { 25807 \u0275\u0275restoreView(_r15); 25808 const ctx_r1 = \u0275\u0275nextContext(); 25809 return \u0275\u0275resetView(ctx_r1.onGpsNodeLocate($event)); 25810 }); 25811 \u0275\u0275elementEnd()(); 25812 } 25813} 25814function CampusMapV3Page_Conditional_22_Template(rf, ctx) { 25815 if (rf & 1) { 25816 const _r16 = \u0275\u0275getCurrentView(); 25817 \u0275\u0275elementStart(0, "app-level-selector", 46); 25818 \u0275\u0275listener("levelSelected", function CampusMapV3Page_Conditional_22_Template_app_level_selector_levelSelected_0_listener($event) { 25819 \u0275\u0275restoreView(_r16); 25820 const ctx_r1 = \u0275\u0275nextContext();
25821 return \u0275\u0275resetView(ctx_r1.onLevelSelected($event)); 25822 }); 25823 \u0275\u0275elementEnd(); 25824 } 25825 if (rf & 2) { 25826 const ctx_r1 = \u0275\u0275nextContext(); 25827 \u0275\u0275property("levels", ctx_r1.viewState.availableLevels())("currentLevel", ctx_r1.viewState.selectedLevel()); 25828 } 25829} 25830var _CampusMapV3Page = class _CampusMapV3Page { 25831 // Template binding getters for 3D model state (delegating to Model3DService) 25832 get show3DModel() { 25833 return this.model3D.show3DModel(); 25834 } 25835 get has3DModel() { 25836 return this.model3D.has3DModel(); 25837 } 25838 get is3DModelLoading() { 25839 return this.model3D.isLoading(); 25840 } 25841 get model3DLoadProgress() { 25842 return this.model3D.loadProgress(); 25843 } 25844 get showTransformPanel() { 25845 return this.model3D.showTransformPanel(); 25846 } 25847 get currentTransform() { 25848 return this.model3D.currentTransform(); 25849 } 25850 get isRulerActive() { 25851 return this.model3D.isRulerActive(); 25852 } 25853 /** Models (with an FBX) available in the transform panel's model picker. */ 25854 get model3DOptions() { 25855 return this.model3D.modelOptions(); 25856 } 25857 /** Currently edited 3D model id (null when none selected). */ 25858 get activeModel3DId() { 25859 return this.model3D.activeModelId(); 25860 } 25861 // Template binding getters for editor mode state (delegating to EditorModeService) 25862 get isAddNodeMode() { 25863 return this.modeService.isAddNodeMode(); 25864 } 25865 get isConnectMode() { 25866 return this.modeService.isConnectMode(); 25867 } 25868 get isDrawingRoom() { 25869 return this.modeService.isDrawingRoom(); 25870 } 25871 get isDrawingFloorOutline() { 25872 return this.modeService.isDrawingFloorOutline(); 25873 } 25874 get isEditingShape() { 25875 return this.modeService.isEditingShape(); 25876 } 25877 get showFloorOutlineButton() { 25878 const entity = this.selectionService.primaryEntity(); 25879 return (entity == null ? void 0 : entity.type) === "floor" && this.selectionService.selectedEntities().length === 1; 25880 } 25881 get assignmentSelectedNodeCount() { 25882 if (!this.nodeAssignmentBuilding) 25883 return 0; 25884 return this.getSelectedEligibleAssignmentNodeIds().length; 25885 } 25886 get selectionToolbarActions() { 25887 const actions = this.selectionService.availableActions(); 25888 if (!this.nodeAssignmentBuilding || this.assignmentSelectedNodeCount === 0) { 25889 const outdoorNodes = this.getSelectedOutdoorNodesForBatch(); 25890 if (!outdoorNodes) { 25891 return actions; 25892 } 25893 const allMarkedAsEntrances = outdoorNodes.every((node) => node.entity.isCampusEntrance === true); 25894 return [ 25895 { 25896 id: "toggleCampusEntrance", 25897 label: allMarkedAsEntrances ? "Remove entrances" : "Mark entrances", 25898 icon: "lucideDoorOpen", 25899 supportedTypes: ["node"], 25900 multiSelect: true 25901 }, 25902 ...actions 25903 ]; 25904 } 25905 return [ 25906 { 25907 id: "assignBuilding", 25908 label: "Assign Building", 25909 icon: "lucideBuilding", 25910 supportedTypes: ["node"], 25911 multiSelect: true 25912 }, 25913 ...actions 25914 ]; 25915 } 25916 /** 25917 * Get all persons in the current campus for person assignment. 25918 */ 25919 get campusPersons() { 25920 const campus = this.campusData.getCampus(); 25921 if (!campus) 25922 return []; 25923 return Array.from(this.database.Models.values()).filter((m) => m.type === EntityType.PERSON && !m.entity.isDeleted && m.entity.campusId === campus.entity.id); 25924 } 25925 // Note: ResizeObserver moved to MapSceneService 25926 constructor(route, database, modelCacheService, ngZone, cdr) { 25927 this.route = route; 25928 this.database = database; 25929 this.modelCacheService = modelCacheService; 25930 this.ngZone = ngZone; 25931 this.cdr = cdr; 25932 this.destroy$ = new Subject(); 25933 this.gestureHandlers = null; 25934 this.marqueeRect = null; 25935 this.showMarquee = false; 25936 this.rotationIndicatorVisible = false; 25937 this.rotationIndicatorAngle = 0; 25938 this.rotationIndicatorX = 0; 25939 this.rotationIndicatorY = 0; 25940 this.showGpsDebugModal = false; 25941 this.defaultDestinationPickerBuilding = null; 25942 this.defaultDestinationPickerNodeIds = /* @__PURE__ */ new Set(); 25943 this.nodeAssignmentBuilding = null; 25944 this.nodeAssignmentEligibleNodeIds = /* @__PURE__ */ new Set(); 25945 this.model3DController = null; 25946 this.treeActionController = null; 25947 this.editPanelController = null; 25948 this.selectionService = inject(SelectionService); 25949 this.historyService = inject(EditorHistoryService); 25950 this.treeStateService = inject(TreeStateService); 25951 this.viewState = inject(ViewStateService); 25952 this.campusData = inject(CampusDataService); 25953 this.entityEdit = inject(EntityEditService); 25954 this.model3D = inject(Model3DService); 25955 this.shapeEditState = inject(ShapeEditStateService); 25956 this.modeService = inject(EditorModeService); 25957 this.mapScene = inject(MapSceneService); 25958 this.gpsTransformManager = inject(GPSTransformManagerService); 25959 this.alertController = inject(AlertController); 25960 this.menuController = inject(MenuController); 25961 this.modalController = inject(ModalController); 25962 this.unsavedChangesService = inject(UnsavedChangesService); 25963 this.apiService = inject(ApiService); 25964 this.toastService = inject(ToastService); 25965 this.navigationDestinationResolver = inject(NavigationDestinationResolverService); 25966 this.previouslySelectedIds = /* @__PURE__ */ new Set(); 25967 this.previouslyHighlightedResolvedDestinationNodeIds = /* @__PURE__ */ new Set(); 25968 this.resolvedDestinationHighlightRequestVersion = 0; 25969 this.showBuildingSelectionModal = false;
25970 this.pendingBuildingSelectionResolve = null; 25971 effect(() => { 25972 const entities = this.selectionService.selectedEntities(); 25973 this.updateSelectionHighlights(entities); 25974 this.updateFloorBackgroundsForSelection(); 25975 }); 25976 this.database.campusModelsChanged$.pipe(takeUntil(this.destroy$)).subscribe(() => { 25977 this.ngZone.run(() => { 25978 if (this.model3DController) { 25979 this.model3DController.refreshModelConfig(); 25980 } else { 25981 this.model3D.refreshModels(); 25982 } 25983 this.cdr.markForCheck(); 25984 }); 25985 }); 25986 } 25987 ngAfterViewInit() { 25988 this.unsavedChangesService.register(() => this.historyService.hasUnsavedChanges(), () => __async(this, null, function* () { 25989 yield this.historyService.discardChanges(); 25990 this.refreshView(); 25991 })); 25992 this.entityEdit.setOnVisualUpdate(() => this.refreshView()); 25993 this.waitForContainerSize().then(() => { 25994 this.ngZone.runOutsideAngular(() => { 25995 this.mapScene.initialize({ 25996 container: this.canvasContainer.nativeElement, 25997 onUpdate: (delta) => { 25998 var _a3, _b2, _c, _d, _e, _f; 25999 const controlsUpdated = (_b2 = (_a3 = this.cameraController) == null ? void 0 : _a3.update(delta)) != null ? _b2 : false; 26000 (_c = this.gestureHandlers) == null ? void 0 : _c.roomShapeEdit.refreshSelectedEdgeOverlay(); 26001 if (controlsUpdated) { 26002 const canvasHeight = this.mapScene.getCanvasHeight(); 26003 (_d = this.levelRenderer) == null ? void 0 : _d.updateLabelsDetailLevel(this.mapScene.getCamera().zoom, canvasHeight); 26004 (_e = this.levelRenderer) == null ? void 0 : _e.updateNodesScreenSpaceScale(canvasHeight); 26005 (_f = this.levelRenderer) == null ? void 0 : _f.updateHighlightsScale(canvasHeight); 26006 } 26007 return controlsUpdated; 26008 }, 26009 onZoomChange: (zoom, canvasHeight) => { 26010 var _a3, _b2, _c; 26011 (_a3 = this.levelRenderer) == null ? void 0 : _a3.updateLabelsDetailLevel(zoom, canvasHeight); 26012 (_b2 = this.levelRenderer) == null ? void 0 : _b2.updateNodesScreenSpaceScale(canvasHeight); 26013 (_c = this.levelRenderer) == null ? void 0 : _c.updateHighlightsScale(canvasHeight); 26014 } 26015 }); 26016 this.initUtilities(); 26017 this.loadDisplaySettings(); 26018 this.setupGestureHandlers(); 26019 this.setupEditorModeCallbacks(); 26020 }); 26021 this.initializeCampus(); 26022 }); 26023 } 26024 waitForContainerSize() { 26025 return new Promise((resolve) => { 26026 const checkSize = () => { 26027 var _a3; 26028 const container = (_a3 = this.canvasContainer) == null ? void 0 : _a3.nativeElement; 26029 if (container && container.clientWidth > 0 && container.clientHeight > 0) { 26030 resolve(); 26031 } else { 26032 requestAnimationFrame(checkSize); 26033 } 26034 }; 26035 checkSize(); 26036 }); 26037 } 26038 ngOnDestroy() { 26039 this.unsavedChangesService.unregister(); 26040 this.destroy$.next(); 26041 this.destroy$.complete(); 26042 this.ngZone.runOutsideAngular(() => { 26043 this.dispose(); 26044 }); 26045 } 26046 onResize() { 26047 this.ngZone.runOutsideAngular(() => { 26048 this.handleResize(); 26049 }); 26050 this.checkMobileAutoCollapse(); 26051 this.cdr.markForCheck(); 26052 } 26053 onBeforeUnload(event) { 26054 if (this.historyService.hasUnsavedChanges()) { 26055 event.preventDefault(); 26056 event.returnValue = ""; 26057 } 26058 } 26059 fitToView() { 26060 if (!this.campusData.getCampus()) 26061 return; 26062 const patches = this.campusData.getGroundPatches(); 26063 this.ngZone.runOutsideAngular(() => { 26064 const bounds = this.groundPatchRenderer.render(patches); 26065 if (bounds) { 26066 this.cameraController.fitBounds(bounds, 0.1); 26067 } 26068 }); 26069 } 26070 onMenuClicked() { 26071 this.menuController.open("start"); 26072 } 26073 // ========================================================================= 26074 // GPS Debug Modal 26075 // ========================================================================= 26076 onGpsDebugClicked() { 26077 this.showGpsDebugModal = true; 26078 this.cdr.markForCheck(); 26079 } 26080 onGpsDebugClosed() { 26081 this.showGpsDebugModal = false;
26082 this.cdr.markForCheck(); 26083 } 26084 onGpsNodeLocate(nodeId) { 26085 const model = this.database.Models.get(nodeId); 26086 if (model && model.entity.locationX != null && model.entity.locationY != null) { 26087 this.ngZone.runOutsideAngular(() => { 26088 this.cameraController.centerOn(model.entity.locationX, model.entity.locationY, true); 26089 }); 26090 this.cdr.markForCheck(); 26091 } 26092 } 26093 // ========================================================================= 26094 // Campus Loading 26095 // ========================================================================= 26096 initializeCampus() { 26097 this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => { 26098 if (params["campusId"]) { 26099 this.loadCampus(params["campusId"]); 26100 } else { 26101 this.loadFirstCampus(); 26102 } 26103 }); 26104 } 26105 loadCampus(campusId) { 26106 const campus = this.campusData.loadCampus(campusId); 26107 if (campus) { 26108 this.ngZone.runOutsideAngular(() => { 26109 this.loadGroundPatches(); 26110 }); 26111 this.treeStateService.setCampus(campus); 26112 this.gpsTransformManager.recalculateTransform(); 26113 this.loadCampusModelInfo(campusId); 26114 } 26115 } 26116 loadFirstCampus() { 26117 const campus = this.campusData.loadFirstCampus(); 26118 if (campus == null ? void 0 : campus.entity.id) { 26119 this.ngZone.runOutsideAngular(() => { 26120 this.loadGroundPatches(); 26121 }); 26122 this.treeStateService.setCampus(campus); 26123 this.loadCampusModelInfo(campus.entity.id); 26124 } 26125 } 26126 loadGroundPatches() { 26127 if (!this.campusData.getCampus()) 26128 return; 26129 const patches = this.campusData.getGroundPatches(); 26130 const bounds = this.groundPatchRenderer.render(patches); 26131 this.groundPatchRenderer.setFillsVisible(!this.viewState.showOutlinesOnly()); 26132 if (bounds) { 26133 this.cameraController.fitBounds(bounds, 0.1); 26134 } 26135 this.computeAvailableLevels(); 26136 if (this.viewState.availableLevels().length > 0) { 26137 this.viewState.initializeDefaultLevel(); 26138 this.renderLevel(this.viewState.selectedLevel()); 26139 this.updateGroundReference(); 26140 } 26141 this.ngZone.run(() => this.cdr.markForCheck()); 26142 } 26143 getGroundPatches() { 26144 return this.campusData.getGroundPatches(); 26145 } 26146 /** 26147 * Load 3D model info from API to determine if campus has a 3D model. 26148 */ 26149 loadCampusModelInfo(campusId) { 26150 return __async(this, null, function* () { 26151 var _a3; 26152 yield (_a3 = this.model3DController) == null ? void 0 : _a3.loadModelInfo(campusId); 26153 this.cdr.markForCheck(); 26154 }); 26155 } 26156 /** 26157 * Handle 3D model toggle from view toolbar. 26158 */ 26159 on3DModelToggled(visible) { 26160 var _a3; 26161 (_a3 = this.model3DController) == null ? void 0 : _a3.toggle3DModel(visible); 26162 this.cdr.markForCheck(); 26163 } 26164 /** 26165 * Handle 3D model settings button click from view toolbar. 26166 */ 26167 onModel3DSettingsClicked() { 26168 var _a3; 26169 (_a3 = this.model3DController) == null ? void 0 : _a3.toggleSettingsPanel(); 26170 this.cdr.markForCheck(); 26171 } 26172 /** 26173 * Handle model selection from the transform panel's model picker. 26174 * Switches the active model in the service + layer and reloads slider values. 26175 */ 26176 onModel3DSelected(modelId) { 26177 if (!modelId) { 26178 return; 26179 } 26180 if (this.model3DController) { 26181 this.model3DController.selectModel(modelId); 26182 } else { 26183 this.model3D.setActiveModel(modelId); 26184 } 26185 this.cdr.markForCheck(); 26186 } 26187 /** 26188 * Handle transform change from panel (real-time preview). 26189 */ 26190 onTransformChanged(partial) { 26191 var _a3, _b2; 26192 (_a3 = this.model3DController) == null ? void 0 : _a3.updateTransform(partial); 26193 if (partial.gamma !== void 0) { 26194 (_b2 = this.model3DController) == null ? void 0 : _b2.updateGamma(partial.gamma); 26195 } 26196 this.cdr.markForCheck(); 26197 } 26198 /** 26199 * Handle transform commit from panel (creates undo point). 26200 * Called when user finishes editing a field (blur, preset click). 26201 */ 26202 onTransformCommit(changedField) { 26203 var _a3; 26204 (_a3 = this.model3DController) == null ? void 0 : _a3.commitTransformChange(changedField); 26205 this.cdr.markForCheck(); 26206 } 26207 /** 26208 * Reset transform to default values. 26209 */ 26210 onResetTransform() { 26211 var _a3; 26212 (_a3 = this.model3DController) == null ? void 0 : _a3.resetTransform(); 26213 this.cdr.markForCheck(); 26214 } 26215 /** 26216 * Close the transform panel. 26217 */ 26218 onCloseTransformPanel() { 26219 var _a3; 26220 (_a3 = this.model3DController) == null ? void 0 : _a3.closePanel(); 26221 this.cdr.markForCheck(); 26222 } 26223 /** 26224 * Center the 3D model to the current viewport center. 26225 */ 26226 onCenterToViewport() { 26227 if (!this.model3DController) 26228 return; 26229 this.model3DController.centerToBounds(this.getViewportBounds()); 26230 this.cdr.markForCheck(); 26231 } 26232 /** 26233 * Scale the 3D model to fit the current viewport. 26234 */ 26235 onScaleToViewport() { 26236 if (!this.model3DController) 26237 return; 26238 this.model3DController.scaleToBounds(this.getViewportBounds()); 26239 this.cdr.markForCheck(); 26240 } 26241 /** 26242 * Center the 3D model to the campus bounds. 26243 */ 26244 onCenterToCampus() { 26245 if (!this.model3DController) 26246 return; 26247 const bounds = this.getCampusBounds(); 26248 if (!bounds) 26249 return; 26250 this.model3DController.centerToBounds(bounds); 26251 this.cdr.markForCheck(); 26252 } 26253 /** 26254 * Scale the 3D model to fit the campus bounds. 26255 */ 26256 onScaleToCampus() { 26257 if (!this.model3DController) 26258 return; 26259 const bounds = this.getCampusBounds(); 26260 if (!bounds) 26261 return; 26262 this.model3DController.scaleToBounds(bounds, 0.1); 26263 this.cdr.markForCheck(); 26264 } 26265 /** 26266 * Get the current viewport bounds from camera frustum. 26267 */ 26268 getViewportBounds() { 26269 const camera = this.mapScene.getCamera(); 26270 const halfWidth = (camera.right - camera.left) / 2 / camera.zoom; 26271 const halfHeight = Math.abs(camera.top - camera.bottom) / 2 / camera.zoom; 26272 return { 26273 minX: camera.position.x - halfWidth, 26274 maxX: camera.position.x + halfWidth, 26275 minY: camera.position.y - halfHeight, 26276 maxY: camera.position.y + halfHeight 26277 }; 26278 } 26279 /** 26280 * Get campus bounds from ground patches, or null if none exist. 26281 */ 26282 getCampusBounds() { 26283 const patches = this.campusData.getGroundPatches(); 26284 return this.groundPatchRenderer.render(patches); 26285 } 26286 // ========================================================================= 26287 // Ruler/Measurement Tool 26288 // ========================================================================= 26289 /** 26290 * Handle ruler tool toggle from view toolbar. 26291 */ 26292 onRulerToggled(active) { 26293 var _a3; 26294 (_a3 = this.model3DController) == null ? void 0 : _a3.toggleRuler(active); 26295 this.cdr.markForCheck(); 26296 } 26297 // ========================================================================= 26298 // Level Management 26299 // ========================================================================= 26300 getAllFloorsInCampus() { 26301 return this.campusData.getAllFloorsInCampus(); 26302 } 26303 computeAvailableLevels() { 26304 const sortedLevels = this.campusData.computeAvailableLevels(); 26305 this.viewState.setAvailableLevels(sortedLevels); 26306 } 26307 getFloorsAtLevel(level) { 26308 return this.campusData.getFloorsAtLevel(level); 26309 } 26310 /** 26311 * Get IDs of currently selected floor entities. 26312 * Returns empty Set if no floors selected (backgrounds won't render). 26313 * Used to filter floor backgrounds rendering. 26314 */ 26315 getSelectedFloorIds() { 26316 const selectedEntities = this.selectionService.selectedEntities(); 26317 const floorEntities = selectedEntities.filter((e) => e.type === "floor"); 26318 return new Set(floorEntities.map((e) => e.id).filter((id) => id !== void 0)); 26319 } 26320 /** 26321 * Re-render floor backgrounds when selection changes. 26322 * Only updates if floor backgrounds are currently visible. 26323 */
26324 updateFloorBackgroundsForSelection() { 26325 if (!this.viewState.showFloorBackgrounds() || !this.levelRenderer) 26326 return; 26327 this.ngZone.runOutsideAngular(() => { 26328 const floors = this.getFloorsAtLevel(this.viewState.selectedLevel()); 26329 const selectedFloorIds = this.getSelectedFloorIds(); 26330 this.levelRenderer.renderFloorBackgrounds(floors, selectedFloorIds); 26331 }); 26332 } 26333 onLevelSelected(level) { 26334 if (!this.viewState.selectLevel(level)) 26335 return; 26336 this.cdr.markForCheck(); 26337 this.ngZone.runOutsideAngular(() => { 26338 this.renderLevel(level); 26339 this.updateGroundReference(); 26340 this.refreshSelectionHighlights(); 26341 }); 26342 } 26343 onBuildingsToggled(visible) { 26344 this.viewState.toggleBuildings(visible); 26345 this.ngZone.runOutsideAngular(() => { 26346 this.levelRenderer.setBuildingsVisible(visible); 26347 }); 26348 } 26349 onRoomsToggled(visible) { 26350 this.viewState.toggleRooms(visible); 26351 this.ngZone.runOutsideAngular(() => { 26352 this.levelRenderer.setRoomsVisible(visible); 26353 }); 26354 } 26355 onNodesToggled(visible) { 26356 this.viewState.toggleNodes(visible); 26357 this.ngZone.runOutsideAngular(() => { 26358 const shouldKeepNodesVisibleForDestination = this.previouslyHighlightedResolvedDestinationNodeIds.size > 0 || this.getBuildingsForResolvedDestinationHighlight(this.selectionService.selectedEntities()).length > 0 || !!this.defaultDestinationPickerBuilding; 26359 const rendererVisible = visible || shouldKeepNodesVisibleForDestination; 26360 this.levelRenderer.setNodesVisible(rendererVisible); 26361 if (rendererVisible) { 26362 this.renderNodesForLevel(this.viewState.selectedLevel()); 26363 } else { 26364 this.levelRenderer.clearNodes(); 26365 } 26366 }); 26367 } 26368 onOutdoorNodesAlwaysToggled(visible) { 26369 this.viewState.toggleOutdoorNodesAlways(visible); 26370 if (this.viewState.showNodes()) { 26371 this.ngZone.runOutsideAngular(() => { 26372 this.renderNodesForLevel(this.viewState.selectedLevel()); 26373 }); 26374 } 26375 this.cdr.markForCheck(); 26376 } 26377 onGroundReferenceToggled(visible) { 26378 this.viewState.toggleGroundReference(visible); 26379 this.ngZone.runOutsideAngular(() => { 26380 this.updateGroundReference(); 26381 }); 26382 this.cdr.markForCheck(); 26383 } 26384 onRoomLabelsToggled(visible) { 26385 this.viewState.toggleRoomLabels(visible); 26386 this.ngZone.runOutsideAngular(() => { 26387 this.levelRenderer.setLabelsVisible(visible); 26388 if (visible) { 26389 const floors = this.getFloorsAtLevel(this.viewState.selectedLevel()); 26390 this.levelRenderer.renderRoomLabels(floors); 26391 const canvasHeight = this.mapScene.getCanvasHeight(); 26392 this.levelRenderer.updateLabelsDetailLevel(this.mapScene.getCamera().zoom, canvasHeight); 26393 } else { 26394 this.levelRenderer.clearLabels(); 26395 } 26396 }); 26397 this.cdr.markForCheck(); 26398 } 26399 onOutlinesOnlyToggled(enabled) { 26400 this.viewState.toggleOutlinesOnly(enabled); 26401 const fillsVisible = !enabled; 26402 this.ngZone.runOutsideAngular(() => { 26403 this.levelRenderer.setFillsVisible(fillsVisible); 26404 this.groundPatchRenderer.setFillsVisible(fillsVisible); 26405 }); 26406 this.cdr.markForCheck(); 26407 } 26408 onFloorBackgroundsToggled(visible) { 26409 this.viewState.toggleFloorBackgrounds(visible); 26410 this.ngZone.runOutsideAngular(() => { 26411 this.levelRenderer.setFloorBackgroundsVisible(visible); 26412 if (visible) { 26413 const floors = this.getFloorsAtLevel(this.viewState.selectedLevel()); 26414 const selectedFloorIds = this.getSelectedFloorIds(); 26415 this.levelRenderer.renderFloorBackgrounds(floors, selectedFloorIds); 26416 } else { 26417 this.levelRenderer.clearFloorBackgrounds(); 26418 } 26419 }); 26420 this.cdr.markForCheck(); 26421 } 26422 /** 26423 * Update ground reference visibility based on current level and toggle state. 26424 * Shows ground floor outlines when viewing levels above ground. 26425 */ 26426 updateGroundReference() { 26427 if (this.viewState.shouldShowGroundReference()) { 26428 const groundFloors = this.getFloorsAtLevel(1); 26429 this.levelRenderer.renderGroundReference(groundFloors); 26430 } else { 26431 this.levelRenderer.clearGroundReference(); 26432 } 26433 } 26434 // ========================================================================= 26435 // Rendering 26436 // ========================================================================= 26437 renderLevel(level) { 26438 const floors = this.getFloorsAtLevel(level); 26439 this.levelRenderer.renderFloors(floors); 26440 this.levelRenderer.setBuildingsVisible(this.viewState.showBuildings()); 26441 this.levelRenderer.setRoomsVisible(this.viewState.showRooms()); 26442 this.levelRenderer.setFillsVisible(!this.viewState.showOutlinesOnl
26442y()); 26443 if (this.viewState.showFloorBackgrounds()) { 26444 const selectedFloorIds = this.getSelectedFloorIds(); 26445 this.levelRenderer.renderFloorBackgrounds(floors, selectedFloorIds); 26446 } else { 26447 this.levelRenderer.clearFloorBackgrounds(); 26448 } 26449 if (this.viewState.showRoomLabels()) { 26450 this.levelRenderer.renderRoomLabels(floors); 26451 const canvasHeight = this.mapScene.getCanvasHeight(); 26452 this.levelRenderer.updateLabelsDetailLevel(this.mapScene.getCamera().zoom, canvasHeight); 26453 } else { 26454 this.levelRenderer.clearLabels(); 26455 } 26456 const shouldShowResolvedDestinationNodes = this.previouslyHighlightedResolvedDestinationNodeIds.size > 0 || this.getBuildingsForResolvedDestinationHighlight(this.selectionService.selectedEntities()).length > 0; 26457 const shouldShowAssignmentNodes = !!this.nodeAssignmentBuilding; 26458 if (this.viewState.showNodes() || shouldShowResolvedDestinationNodes || shouldShowAssignmentNodes) { 26459 this.renderNodesForLevel(level); 26460 this.levelRenderer.setNodesVisible(true); 26461 } else { 26462 this.levelRenderer.clearNodes(); 26463 this.levelRenderer.setNodesVisible(false); 26464 } 26465 } 26466 loadDisplaySettings() { 26467 this.viewState.loadFromSettings(); 26468 this.levelRenderer.setBuildingsVisible(this.viewState.showBuildings()); 26469 this.levelRenderer.setRoomsVisible(this.viewState.showRooms()); 26470 this.levelRenderer.setNodesVisible(this.viewState.showNodes()); 26471 this.levelRenderer.setLabelsVisible(this.viewState.showRoomLabels()); 26472 this.levelRenderer.setFloorBackgroundsVisible(this.viewState.showFloorBackgrounds()); 26473 } 26474 renderNodesForLevel(level) { 26475 if (!this.campusData.getCampus()) 26476 return; 26477 const floorIds = new Set(this.getFloorsAtLevel(level).map((f) => f.entity.id).filter((id) => id !== void 0)); 26478 const outdoorFloorIds = new Set(this.getAllFloorsInCampus().filter((f) => f.entity.level === void 0 || f.entity.level === null).map((f) => f.entity.id).filter((id) => id !== void 0)); 26479 const isGroundLevel = level === 1; 26480 const showOutdoor = isGroundLevel || this.viewState.showOutdoorNodesAlways(); 26481 const campus = this.campusData.getCampus(); 26482 const campusId = campus == null ? void 0 : campus.entity.id; 26483 const nodes = Array.from(this.database.Models.values()).filter((m) => m.type === EntityType.NODE && !m.entity.isDeleted).map((m) => m).filter((node) => { 26484 if (campusId && node.entity.campusId !== campusId) { 26485 return false; 26486 } 26487 if (!node.entity.floorId) { 26488 return showOutdoor; 26489 } 26490 if (floorIds.has(node.entity.floorId)) 26491 return true; 26492 if (showOutdoor && outdoorFloorIds.has(node.entity.floorId)) 26493 return true; 26494 return false; 26495 }); 26496 const nodeIdsInCampus = new Set(nodes.map((n) => n.entity.id).filter((id) => id !== void 0)); 26497 const connections = Array.from(this.database.Models.values()).filter((m) => m.type === EntityType.NODE_CONNECTION && !m.entity.isDeleted).map((m) => m).filter((connection) => { 26498 return connection.entity.nodeAId && connection.entity.nodeBId && nodeIdsInCampus.has(connection.entity.nodeAId) && nodeIdsInCampus.has(connection.entity.nodeBId); 26499 }); 26500 this.levelRenderer.renderNodes(nodes, connections); 26501 const canvasHeight = this.mapScene.getCanvasHeight(); 26502 this.levelRenderer.updateNodesScreenSpaceScale(canvasHeight); 26503 this.applyDefaultDestinationPickerHighlights(); 26504 this.applyNodeAssignmentHighlights(); 26505 } 26506 // ========================================================================= 26507 // Three.js Setup 26508 // ========================================================================= 26509 initUtilities() { 26510 const scene = this.mapScene.getScene(); 26511 const camera = this.mapScene.getCamera(); 26512 const renderer = this.mapScene.getRenderer(); 26513 const materials = this.mapScene.getMaterials(); 26514 this.cameraController = new MapCameraController({ 26515 camera, 26516 renderer, 26517 onDirty: () => this.mapScene.markDirty() 26518 }); 26519 this.groundPatchRenderer = new GroundPatchRenderer({ 26520 materials, 26521 onDirty: () => this.mapScene.markDirty() 26522 }); 26523 scene.add(this.groundPatchRenderer.group); 26524 scene.add(this.groundPatchRenderer.outlinesGroup); 26525 this.levelRenderer = new LevelRenderer({ 26526 materials, 26527 onDirty: () => this.mapScene.markDirty(), 26528 getMediaUrl: (id) => this.apiService.getMediaUrl(id), 26529 getCameraZoom: () => this.mapScene.getCamera().zoom 26530 });
26531 scene.add(this.levelRenderer.floorBackgroundsGroup); 26532 scene.add(this.levelRenderer.floorsGroup); 26533 scene.add(this.levelRenderer.nodesGroup); 26534 scene.add(this.levelRenderer.highlightGroup); 26535 scene.add(this.levelRenderer.ghostGroup); 26536 scene.add(this.levelRenderer.groundReferenceGroup); 26537 scene.add(this.levelRenderer.labelsGroup); 26538 scene.add(this.levelRenderer.cornerHandlesGroup); 26539 scene.add(this.levelRenderer.edgeMidpointsGroup); 26540 scene.add(this.levelRenderer.polygonPreviewGroup); 26541 scene.add(this.levelRenderer.roomWallDimensionsGroup); 26542 scene.add(this.levelRenderer.rotationHandleGroup); 26543 scene.add(this.groundPatchRenderer.highlightGroup); 26544 scene.add(this.groundPatchRenderer.ghostGroup); 26545 this.model3DController = new Model3DController({ 26546 model3DService: this.model3D, 26547 mapScene: this.mapScene, 26548 historyService: this.historyService, 26549 modelCacheService: this.modelCacheService, 26550 alertController: this.alertController, 26551 ngZone: this.ngZone, 26552 onLoadStateChange: () => this.cdr.markForCheck(), 26553 onDirty: () => this.mapScene.markDirty(), 26554 setCameraEnabled: (enabled) => { 26555 this.cameraController.instance.enabled = enabled; 26556 } 26557 }); 26558 this.model3DController.initialize(scene, camera, renderer); 26559 this.treeActionController = new TreeActionController({ 26560 database: this.database, 26561 campusData: this.campusData, 26562 selectionService: this.selectionService, 26563 viewState: this.viewState, 26564 entityEdit: this.entityEdit, 26565 alertController: this.alertController, 26566 onLevelSelected: (level) => this.onLevelSelected(level), 26567 onCenterOnEntity: (id) => this.maybeCenterOnEntity(id), 26568 onOpenBuildingPanel: (id) => this.openBuildingEditPanel(id), 26569 onOpenFloorPanel: (id) => this.openFloorEditPanel(id), 26570 onOpenRoomPanel: (id, isNew) => this.openRoomEditPanel(id, isNew), 26571 onOpenNodePanel: (id, isNew) => this.openNodeEditPanel(id, isNew) 26572 }); 26573 this.editPanelController = new EditPanelController({ 26574 entityEdit: this.entityEdit, 26575 database: this.database, 26576 campusData: this.campusData, 26577 onMoveNodeMesh: (nodeId, deltaX, deltaY) => { 26578 this.levelRenderer.moveEntityMesh(nodeId, "node", deltaX, deltaY); 26579 }, 26580 onRefreshView: () => this.refreshView(), 26581 onMarkForCheck: () => this.cdr.markForCheck() 26582 }); 26583 } 26584 setupGestureHandlers() { 26585 var _a3; 26586 this.gestureHandlers = GestureControllerFactory.create({ 26587 mapScene: this.mapScene, 26588 cameraController: this.cameraController, 26589 levelRenderer: this.levelRenderer, 26590 groundPatchRenderer: this.groundPatchRenderer, 26591 selectionService: this.selectionService, 26592 historyService: this.historyService, 26593 database: this.database, 26594 shapeEditState: this.shapeEditState, 26595 ngZone: this.ngZone, 26596 model3DWrapperGroup: (_a3 = this.model3DController) == null ? void 0 : _a3.wrapperGroup, 26597 callbacks: { 26598 onSelect: (entity, mode) => this.handleSelection(entity, mode), 26599 onMarqueeStart: () => this.handleMarqueeStart(), 26600 onMarqueeEnd: () => this.handleMarqueeEnd(), 26601 onMarqueeUpdate: (rect) => this.handleMarqueeUpdate(rect), 26602 onMarqueeComplete: (entities) => this.handleMarqueeComplete(entities), 26603 onDragStart: () => this.handleDragStart(), 26604 onDragEnd: () => this.handleDragEnd(), 26605 onRotationAngleUpdate: (angle, x, y) => this.handleRotationAngleUpdate(angle, x, y), 26606 onRotationHoverChange: (hovered) => this.handleRotationHoverChange(hovered), 26607 onNodePlacement: (x, y) => this.handleNodePlacement(x, y), 26608 onConnectionCreate: (nodeAId, nodeBId) => this.handleConnectionCreate(nodeAId, nodeBId), 26609 onRoomPolygonComplete: (vertices, floorId) => this.handleRoomPolygonComplete(vertices, floorId), 26610 onFloorOutlineComplete: (completionData) => this.handleFloorOutlineComplete(completionData), 26611 onGroundPatchPolygonComplete: (vertices, campusId) => this.handleGroundPatchPolygonComplete(vertices, campusId), 26612 onModeExit: () => this.ngZone.run(() => { 26613 this.modeService.exitCurrentMode(); 26614 this.cdr.markForCheck(); 26615 }), 26616 onVisualUpdate: () => this.refreshView(), 26617 isEntityVisible: (type) => this.isEntityTypeVisible(type) 26618 } 26619 }); 26620 this.gestureHandlers.setCameraToggleCallback((enabled) => { 26621 this.cameraController.instance.enabled = enabled; 26622 }); 26623 } 26624 /** 26625 * Set up callbacks for EditorModeService. 26626 * Called after gesture handlers are initialized. 26627 */ 26628 setupEditorModeCallbacks() { 26629 const callbacks = { 26630 setCameraControlEnabled: (enabled) => { 26631 this.cameraController.instance.enabled = enabled; 26632 }, 26633 disposeSelectionController: () => { 26634 var _a3; 26635 (_a3 = this.gestureHandlers) == null ? void 0 : _a3.disposeSelectionController(); 26636 }, 26637 createSelectionController: () => { 26638 var _a3; 26639 (_a3 = this.gestureHandlers) == null ? void 0 : _a3.createSelectionController(); 26640 }, 26641 disableDragControllers: () => { 26642 var _a3; 26643 (_a3 = this.gestureHandlers) == null ? void 0 : _a3.disableDragControllers(); 26644 }, 26645 enableDragControllers: () => { 26646 var _a3; 26647 (_a3 = this.gestureHandlers) == null ? void 0 : _a3.enableDragControllers(); 26648 }, 26649 deselectAll: () => { 26650 this.selectionService.deselectAll(); 26651 }, 26652 getController: (mode) => { 26653 var _a3, _b2, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o; 26654 switch (mode) { 26655 case EditorMode.AddNode: 26656 return (_b2 = (_a3 = this.gestureHandlers) == null ? void 0 : _a3.nodePlacement) != null ? _b2 : null; 26657 case EditorMode.Connect: 26658 return (_d = (_c = this.gestureHandlers) == null ? void 0 : _c.nodeConnection) != null ? _d : null; 26659 case EditorMode.DrawRoom: 26660 return (_f = (_e = this.gestureHandlers) == null ? void 0 : _e.roomDraw) != null ? _f : null; 26661 case EditorMode.DrawFloorOutline: { 26662 const pendingData = this.modeService.pendingFloorData(); 26663 const controller = (_g = this.gestureHandlers) == null ? void 0 : _g.floorOutlineDraw; 26664 if (!controller) 26665 return null; 26666 return { 26667 activate: () => { 26668 if (pendingData) { 26669 controller.activateForNewFloor(pendingData); 26670 } else { 26671 const floorId = this.modeService.drawingFloorId(); 26672 if (floorId) 26673 controller.activate(floorId); 26674 } 26675 }, 26676 deactivate: () => controller.deactivate() 26677 }; 26678 } 26679 case EditorMode.DrawGroundPatch: 26680 return (_i = (_h = this.gestureHandlers) == null ? void 0 : _h.groundPatchDraw) != null ? _i : null; 26681 case EditorMode.EditShape: 26682 const entityType = this.modeService.editingEntityType(); 26683 if (entityType === "floor") { 26684 return (_k = (_j = this.gestureHandlers) == null ? void 0 : _j.floorShapeEdit) != null ? _k : null; 26685 } else if (entityType === "groundPatch") { 26686 return (_m = (_l = this.gestureHandlers) == null ? void 0 : _l.groundPatchSh
26686apeEdit) != null ? _m : null; 26687 } 26688 return (_o = (_n = this.gestureHandlers) == null ? void 0 : _n.roomShapeEdit) != null ? _o : null; 26689 default: 26690 return null; 26691 } 26692 }, 26693 refreshView: () => this.refreshView(), 26694 clearCornerHandles: () => this.levelRenderer.clearCornerHandles(), 26695 clearEdgeMidpoints: () => this.levelRenderer.clearEdgeMidpoints() 26696 }; 26697 this.modeService.setCallbacks(callbacks); 26698 } 26699 // ========================================================================= 26700 // Resize Handling (delegated to MapSceneService) 26701 // ========================================================================= 26702 /** 26703 * Trigger resize handling in the scene service. 26704 * Called by window resize handler and sidebar toggle. 26705 */ 26706 handleResize() { 26707 this.mapScene.handleResize(); 26708 } 26709 // ========================================================================= 26710 // Selection Handling 26711 // ========================================================================= 26712 onKeyDown(event) { 26713 if (isEditableKeyboardEvent(event)) 26714 return; 26715 if (event.ctrlKey || event.metaKey) { 26716 if (event.key === "z" && !event.shiftKey) { 26717 event.preventDefault(); 26718 this.historyService.undo(); 26719 this.refreshView(); 26720 this.cdr.markForCheck(); 26721 return; 26722 } else if (event.key === "y" || event.key === "z" && event.shiftKey) { 26723 event.preventDefault(); 26724 this.historyService.redo(); 26725 this.refreshView(); 26726 this.cdr.markForCheck(); 26727 return; 26728 } else if (event.key === "s") { 26729 event.preventDefault(); 26730 this.saveChanges(); 26731 return; 26732 } 26733 } 26734 if (event.key === "Escape") { 26735 event.preventDefault(); 26736 if (this.nodeAssignmentBuilding) { 26737 this.cancelNodeBuildingAssignment(); 26738 return; 26739 } 26740 if (this.defaultDestinationPickerBuilding) { 26741 this.cancelDefaultDestinationPicker(); 26742 return; 26743 } 26744 if (this.entityEdit.activePanel()) { 26745 this.entityEdit.cancel(); 26746 this.cdr.markForCheck(); 26747 return; 26748 } 26749 if (this.modeService.isAnyModeActive()) { 26750 this.modeService.exitCurrentMode(); 26751 this.cdr.markForCheck(); 26752 return; 26753 } 26754 this.selectionService.deselectAll(); 26755 this.cdr.markForCheck(); 26756 } else if ((event.key === "Delete" || event.key === "Backspace") && !this.entityEdit.activePanel() && this.selectionService.hasSelection()) { 26757 event.preventDefault(); 26758 this.deleteSelection(); 26759 } else if (event.key.toLowerCase() === "a" && this.viewState.showNodes() && !this.entityEdit.showNodeEditPanel() && !this.isConnectMode) { 26760 event.preventDefault(); 26761 this.onAddNodeClicked(); 26762 } else if (event.key.toLowerCase() === "c" && this.viewState.showNodes() && !this.entityEdit.showNodeEditPanel()) { 26763 event.preventDefault(); 26764 this.startConnectMode(); 26765 } else if (event.key.toLowerCase() === "e" && !this.entityEdit.activePanel() && this.selectionService.hasSelection()) { 26766 const entity = this.selectionService.primaryEntity(); 26767 if ((entity == null ? void 0 : entity.type) === "node" || (entity == null ? void 0 : entity.type) === "floor" || (entity == null ? void 0 : entity.type) === "room" || (entity == null ? void 0 : entity.type) === "building") { 26768 event.preventDefault(); 26769 this.editSelectedEntity(); 26770 } 26771 } else if (event.key.toLowerCase() === "i" && !this.entityEdit.activePanel() && this.selectionService.hasSelection()) { 26772 const entity = this.selectionService.primaryEntity(); 26773 if ((entity == null ? void 0 : entity.type) === "nodeConnection") { 26774 event.preventDefault(); 26775 this.viewSelectedEntityDetails(); 26776 } 26777 } else if (event.key.toLowerCase() === "s" && !this.entityEdit.activePanel()) { 26778 const entity = this.selectionService.primaryEntity(); 26779 if (((entity == null ? void 0 : entity.type) === "room" || (entity == null ? void 0 : entity.type) === "floor" || (entity == null ? void 0 : entity.type) === "groundPatch") && this.selectionService.selectedEntities().length === 1) { 26780 event.preventDefault(); 26781 this.editSelectedEntityShape(); 26782 } 26783 }
26783 else if (event.key.toLowerCase() === "d" && this.showFloorOutlineButton && !this.entityEdit.activePanel()) { 26784 event.preventDefault(); 26785 this.onDrawFloorOutlineFromToolbar(); 26786 } 26787 } 26788 /** 26789 * Handle entity selection from SelectionController. 26790 */ 26791 handleSelection(entity, mode) { 26792 this.ngZone.run(() => { 26793 if (this.nodeAssignmentBuilding) { 26794 this.handleNodeAssignmentSelection(entity); 26795 return; 26796 } 26797 if (this.defaultDestinationPickerBuilding) { 26798 if ((entity == null ? void 0 : entity.type) === "node") { 26799 this.selectDefaultDestinationNode(entity.id); 26800 } else if (entity) { 26801 void this.toastService.showWarning("Pick a highlighted node for the building destination.", 3e3); 26802 } 26803 return; 26804 } 26805 if (entity) { 26806 this.selectionService.select(entity, mode); 26807 } else if (mode === "replace") { 26808 this.selectionService.deselectAll(); 26809 } 26810 this.cdr.markForCheck(); 26811 }); 26812 } 26813 // ========================================================================= 26814 // Marquee Selection 26815 // ========================================================================= 26816 /** 26817 * Disable camera panning when marquee starts. 26818 */ 26819 handleMarqueeStart() { 26820 this.cameraController.instance.enabled = false; 26821 } 26822 /** 26823 * Re-enable camera panning when marquee ends. 26824 */ 26825 handleMarqueeEnd() { 26826 this.cameraController.instance.enabled = true; 26827 } 26828 /** 26829 * Handle marquee rectangle update (for visual feedback). 26830 */ 26831 handleMarqueeUpdate(rect) { 26832 this.ngZone.run(() => { 26833 this.marqueeRect = rect; 26834 this.showMarquee = rect !== null; 26835 this.cdr.markForCheck(); 26836 }); 26837 } 26838 /** 26839 * Handle marquee selection completion. 26840 */ 26841 handleMarqueeComplete(entities) { 26842 this.ngZone.run(() => { 26843 if (this.nodeAssignmentBuilding) { 26844 const eligibleNodes = entities.filter((entity) => entity.type === "node" && this.nodeAssignmentEligibleNodeIds.has(entity.id)); 26845 if (eligibleNodes.length > 0) { 26846 this.selectionService.selectMany(eligibleNodes); 26847 } 26848 this.cdr.markForCheck(); 26849 return; 26850 } 26851 if (entities.length > 0) { 26852 this.selectionService.selectMany(entities); 26853 } 26854 this.cdr.markForCheck(); 26855 }); 26856 } 26857 /** 26858 * Check if an entity type is currently visible. 26859 */ 26860 isEntityTypeVisible(type) { 26861 if (this.nodeAssignmentBuilding && type === "node") { 26862 return true; 26863 } 26864 return this.viewState.isEntityTypeVisible(type); 26865 } 26866 /** 26867 * Update highlight visuals based on selection state. 26868 */ 26869 updateSelectionHighlights(entities) { 26870 const currentIds = new Set(entities.map((e) => e.id)); 26871 for (const id of this.previouslySelectedIds) { 26872 if (!currentIds.has(id)) { 26873 this.ngZone.runOutsideAngular(() => { 26874 this.levelRenderer.unhighlightEntity(id); 26875 this.groundPatchRenderer.unhighlightEntity(id); 26876 }); 26877 } 26878 } 26879 for (const entity of entities) { 26880 if (!this.previouslySelectedIds.has(entity.id)) { 26881 this.ngZone.runOutsideAngular(() => { 26882 if (entity.type === "groundPatch") { 26883 this.groundPatchRenderer.highlightEntity(entity.id); 26884 } else { 26885 this.levelRenderer.highlightEntity(entity.id, entity.type); 26886 } 26887 }); 26888 } 26889 } 26890 this.previouslySelectedIds = currentIds; 26891 this.applyNodeAssignmentHighlights(); 26892 this.syncSelectedBuildingResolvedDestinationHighlights(entities); 26893 this.ngZone.runOutsideAngular(() => { 26894 if (this.levelRenderer) { 26895 this.updateRotationHandle(entities); 26896 } 26897 }); 26898 } 26899 syncSelectedBuildingResolvedDestinationHighlights(entities) { 26900 const requestVersion = ++this.resolvedDestinationHighlightRequestVersion; 26901 const buildings = this.getBuildingsForResolvedDestinationHighlight(entities); 26902 if (buildings.length === 0) { 26903 this.applyResolvedDestinationHighlights(/* @__PURE__ */ new Set(), requestVersion); 26904 return; 26905 } 26906 void Promise.all(buildings.map((building) => { 26907 var _a3; 26908 return this.navigationDestinationResolver.resolveDestination({ 26909 type: "building", 26910 id: building.entity.id, 26911 name: (_a3 = building.entity.name) != null ? _a3 : void 0 26912 }); 26913 })).then((resolvedDestinations) => { 26914 if (requestVersion !== this.resolvedDestinationHighlightRequestVersion) { 26915 return; 26916 } 26917 const nodeIds = new Set(resolvedDestinations.map((destination) => destination.destinationNode.entity.id).filter((id) => !!id)); 26918 this.applyResolvedDestinationHighlights(nodeIds, requestVersion); 26919 }).catch((error) => { 26920 if (requestVersion !== this.resolvedDestinationHighlightRequestVersion) { 26921 return; 26922 } 26923 console.warn("[CampusMapV3] Failed to resolve selected building destination", error); 26924 this.applyResolvedDestinationHighlights(/* @__PURE__ */ new Set(), requestVersion); 26925 }); 26926 } 26927 applyResolvedDestinationHighlights(currentDestinationNodeIds, requestVersion) { 26928 if (requestVersion !== this.resolvedDestinationHighlightRequestVersion) { 26929 return; 26930 } 26931 this.ngZone.runOutsideAngular(() => { 26932 if (currentDestinationNodeIds.size > 0) { 26933 this.ensureResolvedDestinationNodesVisible(); 26934 } 26935 for (const nodeId of this.previouslyHighlightedResolvedDestinationNodeIds) { 26936 if (!currentDestinationNodeIds.has(nodeId)) { 26937 this.levelRenderer.unhighlightDestinationNode(nodeId); 26938 } 26939 } 26940 for (const nodeId of currentDestinationNodeIds) { 26941 this.levelRenderer.highlightDestinationNode(nodeId); 26942 } 26943 if (currentDestinationNodeIds.size === 0 && this.previouslyHighlightedResolvedDestinationNodeIds.size > 0 && !this.viewState.showNodes() && !this.defaultDestinationPickerBuilding && !this.nodeAssignmentBuilding) { 26944 this.levelRenderer.clearNodes(); 26945 this.levelRenderer.setNodesVisible(false); 26946 } 26947 }); 26948 this.previouslyHighlightedResolvedDestinationNodeIds = currentDestinationNodeIds; 26949 this.cdr.markForCheck(); 26950 } 26951 getBuildingsForResolvedDestinationHighlight(entities) { 26952 const buildingsById = /* @__PURE__ */ new Map(); 26953 for (const entity of entities) { 26954 const building = this.getBuildingForResolvedDestinationHighlight(entity); 26955 const buildingId = building == null ? void 0 : building.entity.id; 26956 if (building && buildingId) { 26957 buildingsById.set(buildingId, building); 26958 } 26959 } 26960 return [...buildingsById.values()]; 26961 } 26962 getBuildingForResolvedDestinationHighlight(entity) { 26963 var _a3, _b2; 26964 if (entity.type === "building") { 26965 const model = this.database.Models.get(entity.id); 26966 return (model == null ? void 0 : model.type) === EntityType.BUILDING ? model : null; 26967 } 26968 if (entity.type === "floor") { 26969 const model = this.database.Models.get(entity.id); 26970 if ((model == null ? void 0 : model.type) !== EntityType.FLOOR) { 26971 return null; 26972 } 26973 const floor = model; 26974 return (_a3 = floor.building) != null ? _a3 : null; 26975 } 26976 if (entity.type === "room") { 26977 const model = this.database.Models.get(entity.id); 26978 if ((model == null ? void 0 : model.type) !== EntityType.ROOM) { 26979 return null; 26980 } 26981 const room = model; 26982 return (_b2 = room.building) != null ? _b2 : null; 26983 } 26984 return null; 26985 } 26986 ensureResolvedDestinationNodesVisible() { 26987 if (!this.levelRenderer.nodesVisible) { 26988 this.renderNodesForLevel(this.viewState.selectedLevel()); 26989 this.levelRenderer.setNodesVisible(true); 26990 } 26991 } 26992 /** 26993 * Update the rotation handle position based on current selection. 26994 */ 26995 updateRotationHandle(entities) { 26996 if (entities.length === 0) { 26997 this.levelRenderer.hideRotationHandle(); 26998 return; 26999 } 27000 if (entities.length === 1 && entities[0].type === "node") { 27001 this.levelRenderer.hideRotationHandle(); 27002 return; 27003 } 27004 if (this.modeService.isEditingShape() || this.modeService.isAddNodeMode() || this.modeService.isConnectMode() || this.modeService.isDrawingRoom() || this.modeService.isDrawingFloorOutline() || this.modeService.isDrawingGroundPatch()) { 27005 this.levelRenderer.hideRotationHandle(); 27006 return; 27007 } 27008 const selectionData = this.calculateSelectionBounds(entities); 27009 if (!selectionData) { 27010 this.levelRenderer.hideRotationHandle(); 27011 return; 27012 } 27013 const { pivot, radius } = selectionData; 27014 this.levelRenderer.renderRotationHandle(pivot, radius); 27015 } 27016 /** 27017 * Calculate the bounding box and centroid of selected entities. 27018 */ 27019 calculateSelectionBounds(entities) { 27020 var _a3, _b2, _c, _d; 27021 const allPoints = []; 27022 for (const entity of entities) { 27023 const model = this.database.Models.get(entity.id); 27024 if (!model) 27025 continue; 27026 if (entity.type === "node") { 27027 const nodeEntity = model.entity; 27028 allPoints.push({ 27029 x: (_a3 = nodeEntity["locationX"]) != null ? _a3 : 0, 27030 y: (_b2 = nodeEntity["locationY"]) != null ? _b2 : 0 27031 }); 27032 } else if (entity.type === "room" || entity.type === "floor" || entity.type === "groundPatch") { 27033 const corners = model.corners; 27034 if (corners) { 27035 for (const corner of corners) { 27036 allPoints.push({ 27037 x: (_c = corner.entity["locationX"]) != null ? _c : 0, 27038 y: (_d = corner.entity["locationY"]) != null ? _d : 0 27039 }); 27040 } 27041 } 27042 } 27043 } 27044 if (allPoints.length === 0) 27045 return null; 27046 const bounds = PointUtils.polygonBounds(allPoints); 27047 if (!bounds) 27048 return null; 27049 const pivot = allPoints.length >= 3 ? PointUtils.polygonCentroid(allPoints) : { 27050 x: (bounds.minX + bounds.maxX) / 2, 27051 y: (bounds.minY + bounds.maxY) / 2 27052 }; 27053 let maxDist = 0; 27054 for (const p of allPoints) { 27055 const dist = PointUtils.distance(pivot, p); 27056 if (dist > maxDist) 27057 maxDist = dist; 27058 } 27059 const radius = maxDist + 50; 27060 return { pivot, radius }; 27061 } 27062 /** 27063 * Handle action from selection toolbar. 27064 */ 27065 onSelectionAction(action) { 27066 switch (action) { 27067 case "delete": 27068 this.deleteSelection(); 27069 break; 27070 case "edit": 27071 this.editSelectedEntity(); 27072 break; 27073 case "editShape": 27074 this.editSelectedEntityShape(); 27075 break; 27076 case "editBuilding": 27077 this.editSelectedFloorBuilding(); 27078 break; 27079 case "viewDetails": 27080 this.viewSelectedEntityDetails(); 27081 break; 27082 case "connect": 27083 this.startConnectMode(); 27084 break; 27085 case "assignBuilding": 27086 this.applyNodeBuildingAssignment(); 27087 break; 27088 case "toggleCampusEntrance": 27089 this.toggleSelectedCampusEntrances(); 27090 break; 27091 case "shareLink": 27092 void this.shareSelectedEntityLink(); 27093 break; 27094 case "deselect": 27095 if (this.modeService.isAnyModeActive()) { 27096 this.modeService.exitCurrentMode(); 27097 } 27098 this.selectionService.deselectAll(); 27099 this.cdr.markForCheck(); 27100 break; 27101 } 27102 } 27103 toggleSelectedCampusEntrances() {
27104 const outdoorNodes = this.getSelectedOutdoorNodesForBatch(); 27105 if (!outdoorNodes) 27106 return; 27107 const markAsEntrances = !outdoorNodes.every((node) => node.entity.isCampusEntrance === true); 27108 const command = new UpdateNodesCampusEntranceCommand({ 27109 nodeIds: outdoorNodes.map((node) => node.entity.id), 27110 isCampusEntrance: markAsEntrances, 27111 database: this.database, 27112 onVisualUpdate: () => this.refreshView() 27113 }); 27114 this.historyService.executeCommand(command); 27115 this.cdr.markForCheck(); 27116 } 27117 getSelectedOutdoorNodesForBatch() { 27118 const entities = this.selectionService.selectedEntities(); 27119 if (entities.length < 2 || entities.some((entity) => entity.type !== "node")) { 27120 return null; 27121 } 27122 const nodes = entities.map((entity) => this.database.Models.get(entity.id)); 27123 if (nodes.some((node) => (node == null ? void 0 : node.type) !== EntityType.NODE)) { 27124 return null; 27125 } 27126 const nodeModels = nodes; 27127 return nodeModels.every((node) => node.entity.type === NodeType.Outdoor) ? nodeModels : null; 27128 } 27129 shareSelectedEntityLink() { 27130 return __async(this, null, function* () { 27131 var _a3; 27132 const entity = this.selectionService.primaryEntity(); 27133 if (!entity || this.selectionService.selectedEntities().length !== 1) { 27134 return; 27135 } 27136 if (entity.type !== "room" && entity.type !== "floor" && entity.type !== "node") { 27137 return; 27138 } 27139 const model = this.database.Models.get(entity.id); 27140 const campusId = (_a3 = model == null ? void 0 : model.entity) == null ? void 0 : _a3.campusId; 27141 if (!model || !campusId) { 27142 const alert = yield this.alertController.create({ 27143 header: "Link unavailable", 27144 message: "This item is missing campus information and cannot be shared.", 27145 buttons: ["OK"] 27146 }); 27147 yield alert.present(); 27148 return; 27149 } 27150 const modal = yield this.modalController.create({ 27151 component: NavigationLinkShareModalComponent, 27152 componentProps: { 27153 kind: entity.type, 27154 targetId: entity.id, 27155 campusId, 27156 titleText: this.getShareLinkTitle(entity, model), 27157 locationText: this.getShareLinkLocation(model) 27158 }, 27159 cssClass: "navigation-link-share-modal", 27160 breakpoints: [0, 0.55, 0.85, 1], 27161 initialBreakpoint: 0.85 27162 }); 27163 yield modal.present(); 27164 }); 27165 } 27166 getShareLinkTitle(entity, model) { 27167 if (entity.name) { 27168 return entity.name; 27169 } 27170 if (model instanceof RoomModel) { 27171 return model.entity.name || model.entity.number || "Selected room"; 27172 } 27173 if (model instanceof FloorModel) { 27174 return model.entity.name || "Selected floor"; 27175 } 27176 if (model instanceof NodeModel) { 27177 return model.entity.tagName || `${model.entity.type || "Waypoint"} node`; 27178 } 27179 return "Selected destination"; 27180 } 27181 getShareLinkLocation(model) { 27182 var _a3, _b2, _c, _d, _e, _f; 27183 const parts = []; 27184 if (model instanceof RoomModel) { 27185 if ((_a3 = model.building) == null ? void 0 : _a3.entity.name) 27186 parts.push(model.building.entity.name); 27187 if ((_b2 = model.floor) == null ? void 0 : _b2.entity.name) 27188 parts.push(model.floor.entity.name); 27189 } else if (model instanceof FloorModel) { 27190 if ((_c = model.building) == null ? void 0 : _c.entity.name) 27191 parts.push(model.building.entity.name); 27192 } else if (model instanceof NodeModel) { 27193 if ((_e = (_d = model.floor) == null ? void 0 : _d.building) == null ? void 0 : _e.entity.name) 27194 parts.push(model.floor.building.entity.name); 27195 if ((_f = model.floor) == null ? void 0 : _f.entity.name) 27196 parts.push(model.floor.entity.name); 27197 } 27198 return parts.join(" \u2022 "); 27199 } 27200 /** 27201 * Toggle connect mode for the selected node. 27202 */ 27203 startConnectMode() { 27204 if (this.isConnectMode) { 27205 this.modeService.exitCurrentMode(); 27206 this.cdr.markForCheck(); 27207 return; 27208 } 27209 const entity = this.selectionService.primaryEntity(); 27210 if ((entity == null ? void 0 : entity.type) === "node" && this.selectionService.selectedEntities().length === 1) { 27211 this.modeService.toggleConnect(entity.id); 27212 this.cdr.markForCheck(); 27213 } 27214 } 27215 /** 27216 * Delete selected entities with confirmation dialog. 27217 */ 27218 deleteSelection() { 27219 return __async(this, null, function* () { 27220 const entities = this.selectionService.selectedEntities(); 27221 if (entities.length === 0) 27222 return; 27223 const { header, message } = this.buildDeleteConfirmationMessage(entities); 27224 const alert = yield this.alertController.create({ 27225 header, 27226 message, 27227 buttons: [ 27228 { text: "Cancel", role: "cancel" }, 27229 { text: "Delete", role: "destructive", cssClass: "danger" } 27230 ] 27231 }); 27232 yield alert.present(); 27233 const { role } = yield alert.onDidDismiss(); 27234 if (role !== "destructive") 27235 return; 27236 const command = new DeleteCommand({ 27237 targets: entities.map((e) => ({ 27238 id: e.id, 27239 type: e.type, 27240 name: e.name 27241 })), 27242 database: this.database, 27243 onVisualUpdate: () => this.refreshView() 27244 }); 27245 this.historyService.executeCommand(command); 27246 this.selectionService.deselectAll(); 27247 this.cdr.markForCheck(); 27248 }); 27249 } 27250 /**
27251 * Build confirmation dialog header and message for deletion. 27252 */ 27253 buildDeleteConfirmationMessage(entities) { 27254 const count = entities.length; 27255 if (count === 1) { 27256 const entity = entities[0]; 27257 const typeName = formatEntityType(entity.type); 27258 const displayName = entity.name ? `"${entity.name}"` : `this ${typeName.toLowerCase()}`; 27259 return { 27260 header: `Delete ${typeName}?`, 27261 message: `Are you sure you want to delete ${displayName}? This action can be undone.` 27262 }; 27263 } 27264 const groups = /* @__PURE__ */ new Map(); 27265 for (const e of entities) { 27266 groups.set(e.type, (groups.get(e.type) || 0) + 1); 27267 } 27268 const lines = []; 27269 for (const [type, typeCount] of groups) { 27270 const typeName = formatEntityType(type); 27271 lines.push(`\u2022 ${typeCount} ${typeName.toLowerCase()}${typeCount > 1 ? "s" : ""}`); 27272 } 27273 return { 27274 header: `Delete ${count} Items?`, 27275 message: `Are you sure you want to delete: 27276 27277${lines.join("\n")} 27278 27279This action can be undone.` 27280 }; 27281 } 27282 // ========================================================================= 27283 // Editor Actions (Undo/Redo/Save/Discard) 27284 // ========================================================================= 27285 /** 27286 * Handle action from editor toolbar. 27287 */ 27288 onEditorAction(action) { 27289 switch (action) { 27290 case "undo": 27291 this.historyService.undo(); 27292 this.refreshView(); 27293 this.cdr.markForCheck(); 27294 break; 27295 case "redo": 27296 this.historyService.redo(); 27297 this.refreshView(); 27298 this.cdr.markForCheck(); 27299 break; 27300 case "save": 27301 this.saveChanges(); 27302 break; 27303 case "discard": 27304 this.discardChanges(); 27305 break; 27306 } 27307 } 27308 /** 27309 * Save all changes to the database. 27310 */ 27311 saveChanges() { 27312 return __async(this, null, function* () { 27313 if (!this.historyService.hasUnsavedChanges()) 27314 return; 27315 yield this.historyService.saveChanges(); 27316 this.cdr.markForCheck(); 27317 }); 27318 } 27319 /** 27320 * Discard all unsaved changes and revert to original states. 27321 */ 27322 discardChanges() { 27323 return __async(this, null, function* () { 27324 if (!this.historyService.hasUnsavedChanges()) 27325 return; 27326 const alert = yield this.alertController.create({ 27327 header: "Discard Changes", 27328 message: "Are you sure you want to discard all unsaved changes?", 27329 buttons: [ 27330 { text: "Cancel", role: "cancel" }, 27331 { text: "Discard", role: "destructive", cssClass: "danger" } 27332 ] 27333 }); 27334 yield alert.present(); 27335 const { role } = yield alert.onDidDismiss(); 27336 if (role !== "destructive") 27337 return; 27338 yield this.historyService.discardChanges(); 27339 this.refreshView(); 27340 this.cdr.markForCheck(); 27341 }); 27342 } 27343 // ========================================================================= 27344 // Drag Move Handlers 27345 // ========================================================================= 27346 onWallLengthPreview(lengthCm) { 27347 var _a3; 27348 (_a3 = this.gestureHandlers) == null ? void 0 : _a3.roomShapeEdit.previewSelectedEdgeLength(lengthCm); 27349 } 27350 onWallLengthCommit() { 27351 var _a3; 27352 (_a3 = this.gestureHandlers) == null ? void 0 : _a3.roomShapeEdit.commitSelectedEdgeLength(); 27353 } 27354 onWallLengthCancel() { 27355 var _a3; 27356 (_a3 = this.gestureHandlers) == null ? void 0 : _a3.roomShapeEdit.cancelSelectedEdgeLengthPreview(); 27357 } 27358 /** 27359 * Disable camera controls when drag starts. 27360 */ 27361 handleDragStart() { 27362 this.cameraController.instance.enabled = false; 27363 } 27364 /** 27365 * Re-enable camera controls when drag ends. 27366 */ 27367 handleDragEnd() { 27368 this.cameraController.instance.enabled = true; 27369 } 27370 /** 27371 * Handle rotation angle update during rotation drag. 27372 */ 27373 handleRotationAngleUpdate(angle, screenX, screenY) { 27374 this.ngZone.run(() => { 27375 if (angle === 0 && screenX === 0 && screenY === 0) { 27376 this.rotationIndicatorVisible = false; 27377 } else { 27378 this.rotationIndicatorVisible = true; 27379 this.rotationIndicatorAngle = angle; 27380 this.rotationIndicatorX = screenX; 27381 this.rotationIndicatorY = screenY; 27382 } 27383 this.cdr.markForCheck(); 27384 }); 27385 } 27386 /** 27387 * Handle rotation handle hover state change. 27388 */ 27389 handleRotationHoverChange(hovered) { 27390 this.ngZone.runOutsideAngular(() => { 27391 this.levelRenderer.setRotationHandleHovered(hovered); 27392 }); 27393 } 27394 /** 27395 * Refresh the view after entity modifications. 27396 * Re-renders geometry without resetting camera zoom/pan. 27397 */ 27398 refreshView() { 27399 this.ngZone.runOutsideAngular(() => { 27400 this.renderLevel(this.viewState.selectedLevel()); 27401 if (this.campusData.getCampus()) { 27402 const patches = this.campusData.getGroundPatches(); 27403 this.groundPatchRenderer.render(patches); 27404 } 27405 this.refreshSelectionHighlights(); 27406 }); 27407 } 27408 /** 27409 * Recreate highlights for all currently selected entities. 27410 * Called after view refresh to sync highlight positions with
27410new mesh positions. 27411 */ 27412 refreshSelectionHighlights() { 27413 const entities = this.selectionService.selectedEntities(); 27414 if (entities.length === 0) 27415 return; 27416 this.levelRenderer.clearHighlights(); 27417 this.groundPatchRenderer.clearHighlights(); 27418 for (const entity of entities) { 27419 if (entity.type === "groundPatch") { 27420 this.groundPatchRenderer.highlightEntity(entity.id); 27421 } else { 27422 this.levelRenderer.highlightEntity(entity.id, entity.type); 27423 } 27424 } 27425 this.previouslySelectedIds = new Set(entities.map((e) => e.id)); 27426 this.levelRenderer.clearDestinationHighlights(); 27427 this.previouslyHighlightedResolvedDestinationNodeIds = /* @__PURE__ */ new Set(); 27428 this.syncSelectedBuildingResolvedDestinationHighlights(entities); 27429 this.updateRotationHandle(entities); 27430 } 27431 /** 27432 * Edit the selected entity (single selection only). 27433 */ 27434 editSelectedEntity() { 27435 const entity = this.selectionService.primaryEntity(); 27436 if (!entity) 27437 return; 27438 if (entity.type === "node") { 27439 this.openNodeEditPanel(entity.id, false); 27440 } else if (entity.type === "floor") { 27441 this.openFloorEditPanel(entity.id); 27442 } else if (entity.type === "room") { 27443 this.openRoomEditPanel(entity.id, false); 27444 } else if (entity.type === "building") { 27445 this.openBuildingEditPanel(entity.id); 27446 } else if (entity.type === "groundPatch") { 27447 this.openGroundPatchEditPanel(entity.id, false); 27448 } 27449 } 27450 /** 27451 * Toggle shape editing for the selected entity (room or floor polygon). 27452 */ 27453 editSelectedEntityShape() { 27454 const entity = this.selectionService.primaryEntity(); 27455 if (!entity) 27456 return; 27457 if (entity.type === "room" || entity.type === "floor" || entity.type === "groundPatch") { 27458 if (this.isEditingShape && this.modeService.editingEntityId() === entity.id) { 27459 this.modeService.exitCurrentMode(); 27460 } else { 27461 this.modeService.enterShapeEdit(entity.id, entity.type); 27462 } 27463 this.cdr.markForCheck(); 27464 } 27465 } 27466 /** 27467 * Open the building edit panel for the selected floor's building. 27468 */ 27469 editSelectedFloorBuilding() { 27470 var _a3, _b2; 27471 const entity = this.selectionService.primaryEntity(); 27472 if (!entity || entity.type !== "floor") 27473 return; 27474 const floor = this.database.Models.get(entity.id); 27475 if (!((_b2 = (_a3 = floor == null ? void 0 : floor.building) == null ? void 0 : _a3.entity) == null ? void 0 : _b2.id)) 27476 return; 27477 this.openBuildingEditPanel(floor.building.entity.id); 27478 } 27479 /** 27480 * View details of the selected entity (single selection only). 27481 * Currently only supported for node connections. 27482 */ 27483 viewSelectedEntityDetails() { 27484 const entity = this.selectionService.primaryEntity(); 27485 if (!entity) 27486 return; 27487 if (entity.type === "nodeConnection") { 27488 } 27489 } 27490 // ========================================================================= 27491 // Node CRUD Operations 27492 // ========================================================================= 27493 /** 27494 * Handle "Add Node" button click from view toolbar. 27495 */ 27496 onAddNodeClicked() { 27497 this.modeService.toggleAddNode(); 27498 this.cdr.markForCheck(); 27499 } 27500 /** 27501 * Handle "Draw Floor Outline" button click from view toolbar. 27502 */ 27503 onDrawFloorOutlineFromToolbar() { 27504 const entity = this.selectionService.primaryEntity(); 27505 if ((entity == null ? void 0 : entity.type) !== "floor") 27506 return; 27507 this.modeService.toggleDrawFloorOutline(entity.id); 27508 this.cdr.markForCheck(); 27509 } 27510 /** 27511 * Handle node placement from NodePlacementController. 27512 * Coordinates are in local (group) space. 27513 */ 27514 handleNodePlacement(localX, localY) { 27515 const campus = this.campusData.getCampus(); 27516 if (!campus) 27517 return; 27518 const floorId = this.levelRenderer.getFloorAtPosition(localX, localY); 27519 const node = Object.assign(new Node(), { 27520 id: crypto.randomUUID(), 27521 campusId: campus.entity.id, 27522 floorId: floorId != null ? floorId : void 0, 27523 locationX: localX, 27524 locationY: localY, 27525 type: floorId ? NodeType.Regular : NodeType.Outdoor, 27526 isDeleted: false, 27527 isChanged: true, 27528 version: 0 27529 }); 27530 const currentLevel = this.viewState.selectedLevel(); 27531 const floorsAtLevel = this.getFloorsAtLevel(currentLevel); 27532 const allFloors = this.getAllFloorsInCampus(); 27533 const outdoorFloors = allFloors.filter((f) => f.entity.level === void 0 || f.entity.level === null); 27534 const floorIdsAtLevel = new Set(floorsAtLevel.map((f) =>
27534 f.entity.id).filter(Boolean)); 27535 const outdoorFloorIds = new Set(outdoorFloors.map((f) => f.entity.id).filter(Boolean)); 27536 const isGroundLevel = currentLevel === 1; 27537 const showOutdoorAlways = this.viewState.showOutdoorNodesAlways(); 27538 const showOutdoor = isGroundLevel || showOutdoorAlways; 27539 const willHaveFloorId = !!node.floorId; 27540 const willBeInFloorIds = node.floorId ? floorIdsAtLevel.has(node.floorId) : false; 27541 const willBeOutdoorNoFloorId = !node.floorId && showOutdoor && node.type === "Outdoor"; 27542 const willBeOutdoorOnOutdoorFloor = showOutdoor && node.floorId && outdoorFloorIds.has(node.floorId) && node.type === "Outdoor"; 27543 const willBeRendered = willHaveFloorId ? willBeInFloorIds || willBeOutdoorOnOutdoorFloor : willBeOutdoorNoFloorId; 27544 const command = new CreateNodeCommand({ 27545 node, 27546 database: this.database, 27547 onVisualUpdate: () => this.refreshView(), 27548 onNodeCreated: (nodeId) => { 27549 this.ngZone.run(() => { 27550 this.modeService.exitCurrentMode(); 27551 this.selectionService.select({ 27552 id: nodeId, 27553 type: "node" 27554 }, "replace"); 27555 }); 27556 } 27557 }); 27558 this.historyService.executeCommand(command); 27559 this.cdr.markForCheck(); 27560 } 27561 /** 27562 * Open the node edit panel for a node. 27563 */ 27564 openNodeEditPanel(nodeId, isNew) { 27565 this.entityEdit.openNodePanel(nodeId, isNew); 27566 this.cdr.markForCheck(); 27567 } 27568 /** 27569 * Handle node property change from edit panel (live preview). 27570 */ 27571 onNodeEditPanelChange(changes) { 27572 var _a3; 27573 (_a3 = this.editPanelController) == null ? void 0 : _a3.handleNodeChange(changes); 27574 } 27575 /** 27576 * Handle apply button from node edit panel. 27577 */ 27578 onNodeEditPanelApply() { 27579 var _a3; 27580 (_a3 = this.editPanelController) == null ? void 0 : _a3.handleNodeApply(); 27581 } 27582 /** 27583 * Handle cancel button from node edit panel. 27584 */ 27585 onNodeEditPanelCancel() { 27586 var _a3; 27587 (_a3 = this.editPanelController) == null ? void 0 : _a3.handleNodeCancel(); 27588 } 27589 // ========================================================================= 27590 // Floor Edit Panel 27591 // ========================================================================= 27592 /** 27593 * Open the floor edit panel for a floor. 27594 */ 27595 openFloorEditPanel(floorId) { 27596 this.entityEdit.openFloorPanel(floorId); 27597 this.cdr.markForCheck(); 27598 } 27599 /** 27600 * Handle floor property change from edit panel. 27601 */ 27602 onFloorEditPanelChange(changes) { 27603 var _a3; 27604 (_a3 = this.editPanelController) == null ? void 0 : _a3.handleFloorChange(changes); 27605 } 27606 /** 27607 * Handle apply button from floor edit panel. 27608 */ 27609 onFloorEditPanelApply() { 27610 var _a3; 27611 (_a3 = this.editPanelController) == null ? void 0 : _a3.handleFloorApply(); 27612 } 27613 /** 27614 * Handle cancel button from floor edit panel. 27615 */ 27616 onFloorEditPanelCancel() { 27617 var _a3; 27618 (_a3 = this.editPanelController) == null ? void 0 : _a3.handleFloorCancel(); 27619 } 27620 /** 27621 * Handle draw outline request from floor edit panel. 27622 * Supports both new floors (create mode) and existing floors (edit mode). 27623 */ 27624 onFloorDrawOutline() { 27625 var _a3, _b2, _c, _d; 27626 const floor = this.entityEdit.editingFloor(); 27627 if (!floor) 27628 return; 27629 const isNew = this.entityEdit.isFloorNew(); 27630 const pendingChanges = this.entityEdit.getPendingChanges(); 27631 this.entityEdit.cancel(); 27632 if (isNew) { 27633 const floorData = { 27634 name: (_b2 = (_a3 = pendingChanges["name"]) != null ? _a3 : floor.entity.name) != null ? _b2 : "New Floor", 27635 buildingId: floor.entity.buildingId, 27636 campusId: floor.entity.campusId, 27637 level: (_d = (_c = pendingChanges["level"]) != null ? _c : floor.entity.level) != null ? _d : 1 27638 }; 27639 this.modeService.enterDrawFloorOutlineForNewFloor(floorData); 27640 } else { 27641 this.modeService.toggleDrawFloorOutline(floor.entity.id); 27642 } 27643 this.cdr.markForCheck(); 27644 } 27645 /** 27646 * Handle edit outline request from floor edit panel. 27647 */ 27648 onFloorEditOutline() { 27649 const floor = this.entityEdit.editingFloor(); 27650 if (!(floor == null ? void 0 : floor.entity.id)) 27651 return; 27652 this.entityEdit.cancel(); 27653 this.modeService.enterShapeEdit(floor.entity.id, "floor"); 27654 this.cdr.markForCheck(); 27655 } 27656 // ========================================================================= 27657 // Node Connection Creation 27658 // ========================================================================= 27659 /** 27660 * Handle connection creation from NodeConnectionController. 27661 */ 27662 handleConnectionCreate(nodeAId, nodeBId) { 27663 const command = new CreateNodeConnectionCommand({ 27664 nodeAId, 27665 nodeBId, 27666 database: this.database, 27667 onVisualUpdate: () => this.refreshView() 27668 }); 27669 this.historyService.executeCommand(command); 27670 this.modeService.exitCurrentMode(); 27671 this.cdr.markForCheck(); 27672 } 27673 // ========================================================================= 27674 // Room Polygon Drawing 27675 // ========================================================================= 27676 /** 27677 * Handle "Add Room" button click from view toolbar. 27678 */ 27679 onAddRoomClicked() { 27680 this.modeService.toggleDrawRoom(); 27681 this.cdr.markForCheck(); 27682 } 27683 /** 27684 * Handle "Create Ground Patch" button click from view toolbar. 27685 * Activates ground patch polygon drawing mode. 27686 */ 27687 onCreateGroundPatchClicked() { 27688 const campus = this.campusData.getCampus(); 27689 if (!(campus == null ? void 0 : campus.entity.id)) { 27690 return; 27691 } 27692 this.modeService.toggleDrawGroundPatch(campus.entity.id); 27693 this.cdr.markForCheck(); 27694 } 27695 /** 27696 * Handle "Create Floor" button click from view toolbar. 27697 * Opens floor edit panel in create mode for the appropriate building. 27698 */ 27699 onCreateFloorClicked() { 27700 return __async(this, null, function* () { 27701 const campus = this.campusData.getCampus(); 27702 if (!campus) { 27703 return; 27704 } 27705 const buildings = campus.buildings.filter((b) => !b.entity.isDeleted); 27706 if (buildings.length === 0) { 27707 const alert = yield this.alertController.create({ 27708 header: "No Building", 27709 message: "Please create a building first before adding floors.", 27710 buttons: ["OK"] 27711 }); 27712 yield alert.present(); 27713 return; 27714 } 27715 let targetBuilding; 27716 if (buildings.length === 1) { 27717 targetBuilding = buildings[0]; 27718 } else {
27719 const selectedBuilding = this.getSelectedBuilding(); 27720 if (selectedBuilding) { 27721 targetBuilding = selectedBuilding; 27722 } else { 27723 const selectedId = yield this.showBuildingSelectionModalForFloorCreation(); 27724 if (!selectedId) 27725 return; 27726 const found = buildings.find((b) => b.entity.id === selectedId); 27727 if (!found) 27728 return; 27729 targetBuilding = found; 27730 } 27731 } 27732 this.startFloorCreationForBuilding(targetBuilding); 27733 }); 27734 } 27735 /** 27736 * Start floor creation flow - sets pending data and enters drawing mode. 27737 */ 27738 startFloorCreationForBuilding(building) { 27739 var _a3, _b2, _c, _d, _e; 27740 const tempModel = (_a3 = this.treeActionController) == null ? void 0 : _a3.createFloorForBuilding(building.entity.id); 27741 if (!tempModel) 27742 return; 27743 this.modeService.setPendingFloorData({ 27744 name: (_b2 = tempModel.entity.name) != null ? _b2 : "New Floor", 27745 buildingId: building.entity.id, 27746 campusId: (_d = (_c = tempModel.campus) == null ? void 0 : _c.entity.id) != null ? _d : "", 27747 level: (_e = tempModel.entity.level) != null ? _e : 1 27748 }); 27749 this.modeService.startDrawingFloorOutline(); 27750 this.cdr.markForCheck(); 27751 } 27752 /** 27753 * Get the currently selected building, if any. 27754 */ 27755 getSelectedBuilding() { 27756 const primary = this.selectionService.primaryEntity(); 27757 if ((primary == null ? void 0 : primary.type) === "building") { 27758 const model = this.database.Models.get(primary.id); 27759 if ((model == null ? void 0 : model.type) === EntityType.BUILDING) { 27760 return model; 27761 } 27762 } 27763 return null; 27764 } 27765 /** 27766 * Show the building selection modal for floor creation. 27767 */ 27768 showBuildingSelectionModalForFloorCreation() { 27769 return new Promise((resolve) => { 27770 this.pendingBuildingSelectionResolve = resolve; 27771 this.showBuildingSelectionModal = true; 27772 this.cdr.markForCheck(); 27773 }); 27774 } 27775 /** 27776 * Handle building selection from modal. 27777 */ 27778 onBuildingSelected(buildingSelection) { 27779 this.showBuildingSelectionModal = false; 27780 if (this.pendingBuildingSelectionResolve) { 27781 this.pendingBuildingSelectionResolve(buildingSelection.id); 27782 this.pendingBuildingSelectionResolve = null; 27783 } 27784 this.cdr.markForCheck(); 27785 } 27786 /** 27787 * Handle modal cancel. 27788 */ 27789 onBuildingSelectionCancel() { 27790 this.showBuildingSelectionModal = false; 27791 if (this.pendingBuildingSelectionResolve) { 27792 this.pendingBuildingSelectionResolve(null); 27793 this.pendingBuildingSelectionResolve = null; 27794 } 27795 this.cdr.markForCheck(); 27796 } 27797 /** 27798 * Handle modal close. 27799 */ 27800 onBuildingSelectionClose() { 27801 this.showBuildingSelectionModal = false; 27802 if (this.pendingBuildingSelectionResolve) { 27803 this.pendingBuildingSelectionResolve(null); 27804 this.pendingBuildingSelectionResolve = null; 27805 } 27806 this.cdr.markForCheck(); 27807 } 27808 /** 27809 * Get building selection items for the modal. 27810 */ 27811 getBuildingSelectionItems() { 27812 const buildings = this.getAllBuildingsInCampus(); 27813 return buildings.map((b) => { 27814 var _a3, _b2, _c, _d, _e, _f, _g; 27815 return { 27816 id: b.entity.id, 27817 name: (_a3 = b.entity.name) != null ? _a3 : "Unnamed Building", 27818 floorCount: (_c = (_b2 = b.floors) == null ? void 0 : _b2.length) != null ? _c : 0, 27819 roomCount: (_e = (_d = b.rooms) == null ? void 0 : _d.length) != null ? _e : 0, 27820 nodeCount: (_g = (_f = b.floors) == null ? void 0 : _f.reduce((sum, f) => { 27821 var _a4, _b3; 27822 return sum + ((_b3 = (_a4 = f.nodes) == null ? void 0 : _a4.length) != null ? _b3 : 0); 27823 }, 0)) != null ? _g : 0 27824 }; 27825 }); 27826 } 27827 /** 27828 * Get all buildings in campus. 27829 */ 27830 getAllBuildingsInCampus() { 27831 const campus = this.campusData.getCampus(); 27832 if (!campus) 27833 return []; 27834 return Array.from(this.database.Models.values()).filter((m) => m.type === EntityType.BUILDING && !m.entity.isDeleted && m.entity.campusId === campus.entity.id); 27835 } 27836 /** 27837 * Handle completed polygon from RoomPolygonDrawController. 27838 */ 27839 handleRoomPolygonComplete(vertices, floorId) { 27840 const command = new CreateRoomPolygonCommand({ 27841 vertices, 27842 floorId, 27843 database: this.database, 27844 onVisualUpdate: () => this.refreshView(), 27845 onRoomCreated: (roomId) => { 27846 this.ngZone.run(() => { 27847 var _a3; 27848 this.modeService.exitCurrentMode(); 27849 const roomModel = this.database.Models.get(roomId); 27850 if (roomModel) { 27851 this.selectionService.select({ 27852 id: roomId, 27853 type: "room", 27854 name: (_a3 = roomModel.entity.name) != null ? _a3 : void 0 27855 }, "replace"); 27856 } 27857 }); 27858 } 27859 }); 27860 this.historyService.executeCommand(command); 27861 this.cdr.markForCheck(); 27862 } 27863 /** 27864 * Handle floor outline polygon completion. 27865 * Supports both creating a new floor with outline and adding outline to existing floor. 27866 */ 27867 handleFloorOutlineComplete(completionData) { 27868 if (completionData.mode === "create") { 27869 const command = new CreateFloorWithOutlineCommand({ 27870 floorData: completionData.data, 27871 vertices: completionData.vertices, 27872 database: this.database, 27873 onVisualUpdate: () => this.refreshView(), 27874 onFloorCreated: (createdFloorId) => { 27875 this.ngZone.run(() => { 27876 var _a3; 27877 this.modeService.exitCurrentMode(); 27878 const floorModel = this.database.Models.get(createdFloorId); 27879 if (floorModel) { 27880 if (floorModel.entity.level !== void 0 && floorModel.entity.level !== null) { 27881 this.onLevelSelected(floorModel.entity.level); 27882 } 27883 this.selectionService.select({ 27884 id: createdFloorId, 27885 type: "floor", 27886 name: (_a3 = floorModel.entity.name) != null ? _a3 : void 0 27887 }, "replace"); 27888 } 27889 this.cdr.markForCheck(); 27890 }); 27891 } 27892 }); 27893 this.historyService.executeCommand(command); 27894 } else { 27895 const command = new CreateFloorOutlineCommand({ 27896 vertices: completionData.vertices, 27897 floorId: completionData.floorId, 27898 database: this.database, 27899 onVisualUpdate: () => this.refreshView(), 27900 onOutlineCreated: (createdFloorId) => { 27901 this.ngZone.run(() => { 27902 var _a3; 27903 this.modeService.exitCurrentMode(); 27904 const floorModel = this.database.Models.get(createdFloorId); 27905 if (floorModel) { 27906 this.selectionService.select({ 27907 id: createdFloorId, 27908 type: "floor", 27909 name: (_a3 = floorModel.entity.name) != null ? _a3 : void 0 27910 }, "replace"); 27911 } 27912 this.cdr.markForCheck(); 27913 }); 27914 } 27915 }); 27916 this.historyService.executeCommand(command); 27917 } 27918 this.cdr.markForCheck(); 27919 } 27920 /** 27921 * Handle ground patch polygon completion. 27922 */ 27923 handleGroundPatchPolygonComplete(vertices, campusId) { 27924 const command = new CreateGroundPatchPolygonCommand({ 27925 vertices, 27926 campusId, 27927 database: this.database, 27928 onVisualUpdate: () => this.refreshView(), 27929 onGroundPatchCreated: (groundPatchId) => { 27930 this.ngZone.run(() => { 27931 var _a3; 27932 this.modeService.exitCurrentMode(); 27933 const groundPatchModel = this.database.Models.get(groundPatchId); 27934 if (groundPatchModel) { 27935 this.selectionService.select({ 27936 id: groundPatchId, 27937 type: "groundPatch", 27938 name: (_a3 = groundPatchModel.entity.type) != null ? _a3 : "Ground Patch" 27939 }, "replace"); 27940 } 27941 this.cdr.markForCheck(); 27942 }); 27943 } 27944 }); 27945 this.historyService.executeCommand(command); 27946 this.cdr.markForCheck(); 27947 } 27948 // ========================================================================= 27949 // Entity Sidebar 27950 // ========================================================================= 27951 /** 27952 * Handle sidebar collapse state change. 27953 */ 27954 onSidebarCollapsedChange(collapsed) { 27955 this.viewState.toggleSidebar(collapsed); 27956 setTimeout(() => { 27957 this.ngZone.runOutsideAngular(() => { 27958 this.handleResize(); 27959 }); 27960 }, 300); 27961 this.cdr.markForCheck(); 27962 } 27963 /** 27964 * Auto-collapse sidebar on mobile/small screens.
27965 * Auto-expand when returning to desktop if it was auto-collapsed. 27966 */ 27967 checkMobileAutoCollapse() { 27968 const wasCollapsed = this.viewState.sidebarCollapsed(); 27969 this.viewState.checkMobileAutoCollapse(); 27970 const nowCollapsed = this.viewState.sidebarCollapsed(); 27971 this.ngZone.run(() => this.cdr.markForCheck()); 27972 if (wasCollapsed !== nowCollapsed) { 27973 setTimeout(() => { 27974 this.ngZone.runOutsideAngular(() => { 27975 this.handleResize(); 27976 }); 27977 }, 300); 27978 } 27979 } 27980 /** 27981 * Handle center-on-selection toggle change. 27982 */ 27983 onCenterOnSelectionToggled(enabled) { 27984 this.viewState.toggleCenterOnSelection(enabled); 27985 this.cdr.markForCheck(); 27986 } 27987 /** 27988 * Get the bounding box of an entity for camera fitting. 27989 * Returns null if entity has no valid bounds. 27990 */ 27991 getEntityBounds(entityId) { 27992 return this.campusData.getEntityBounds(entityId); 27993 } 27994 /** 27995 * Fit camera to show entity with 10% padding if centerOnSelection is enabled. 27996 */ 27997 maybeCenterOnEntity(entityId) { 27998 if (!this.viewState.centerOnSelection() || !this.cameraController) 27999 return; 28000 const bounds = this.getEntityBounds(entityId); 28001 if (bounds) { 28002 this.ngZone.runOutsideAngular(() => { 28003 this.cameraController.fitBounds(bounds, 0.1); 28004 }); 28005 } 28006 } 28007 /** 28008 * Handle tree node click - select entity on map. 28009 */ 28010 onTreeNodeClicked(item) { 28011 var _a3; 28012 (_a3 = this.treeActionController) == null ? void 0 : _a3.handleNodeClick(item); 28013 this.cdr.markForCheck(); 28014 } 28015 /** 28016 * Handle tree node context menu - show action sheet. 28017 */ 28018 onTreeNodeContextMenu(event) { 28019 return __async(this, null, function* () { 28020 var _a3; 28021 yield (_a3 = this.treeActionController) == null ? void 0 : _a3.showContextMenu(event.item); 28022 }); 28023 } 28024 /** 28025 * Handle add building request from tree panel. 28026 * Shows the panel in create mode with empty building. 28027 */ 28028 onAddBuildingFromTree() { 28029 var _a3; 28030 const tempModel = (_a3 = this.treeActionController) == null ? void 0 : _a3.createBuildingModel(); 28031 if (tempModel) { 28032 this.entityEdit.openNewBuildingPanel(tempModel); 28033 this.cdr.markForCheck(); 28034 } 28035 } 28036 // ========================================================================= 28037 // Building Edit Panel 28038 // ========================================================================= 28039 /** 28040 * Open the building edit panel for an existing building. 28041 */ 28042 openBuildingEditPanel(buildingId) { 28043 this.entityEdit.openBuildingPanel(buildingId); 28044 this.cdr.markForCheck(); 28045 } 28046 /** 28047 * Handle building property change from edit panel. 28048 */ 28049 onBuildingEditPanelChange(changes) { 28050 var _a3; 28051 (_a3 = this.editPanelController) == null ? void 0 : _a3.handleBuildingChange(changes); 28052 } 28053 /** 28054 * Enter visual node-picking mode for the building default destination. 28055 */ 28056 onBuildingPickDefaultDestination() { 28057 const building = this.entityEdit.editingBuilding(); 28058 if (!(building == null ? void 0 : building.entity.id)) 28059 return; 28060 if (this.nodeAssignmentBuilding) { 28061 this.cancelNodeBuildingAssignment(); 28062 } 28063 const pickableNodeIds = this.getDefaultDestinationCandidateIds(building); 28064 if (pickableNodeIds.size === 0) { 28065 void this.toastService.showWarning("This building has no valid connected destination nodes to pick.", 4e3); 28066 return; 28067 } 28068 if (this.modeService.isAnyModeActive()) { 28069 this.modeService.exitCurrentMode(); 28070 } 28071 this.defaultDestinationPickerBuilding = building; 28072 this.defaultDestinationPickerNodeIds = pickableNodeIds; 28073 this.selectionService.deselectAll(); 28074 if (this.entityEdit.hasPendingChanges()) { 28075 this.entityEdit.apply(); 28076 } else { 28077 this.entityEdit.cancel(); 28078 } 28079 if (!this.viewState.showNodes()) { 28080 this.onNodesToggled(true); 28081 } else { 28082 this.refreshView(); 28083 } 28084 const targetLevel = this.getDefaultDestinationPickerLevel(building); 28085 if (targetLevel !== null && targetLevel !== this.viewState.selectedLevel()) { 28086 this.onLevelSelected(targetLevel); 28087 } else { 28088 this.applyDefaultDestinationPickerHighlights(); 28089 } 28090 void this.toastService.showInfo("Click a highlighted node to use as this building destination.", 4e3); 28091 this.cdr.markForCheck(); 28092 } 28093 /** 28094 * Clear the building default destination override. 28095 */ 28096 onBuildingClearDefaultDestination() { 28097 const building = this.entityEdit.editingBuilding(); 28098 if (!(building == null ? void 0 : building.entity.id) || !building.entity.defaultDestinationNodeId) 28099 return; 28100 const command = new UpdateBuildingCommand({ 28101 buildingId: building.entity.id, 28102 changes: { defaultDestinationNodeId: null }, 28103 database: this.database, 28104 onVisualUpdate: () => this.refreshView() 28105 }); 28106 this.historyService.executeCommand(command); 28107 this.cdr.markForCheck(); 28108 } 28109 onBuildingAssignNodes() { 28110 const building = this.entityEdit.editingBuilding(); 28111 if (!(building == null ? void 0 : building.entity.id)) 28112 return; 28113 if (this.defaultDestinationPickerBuilding) { 28114 this.cancelDefaultDestinationPicker(); 28115 } 28116 if (this.modeService.isAnyModeActive()) { 28117 this.modeService.exitCurrentMode(); 28118 } 28119 this.nodeAssignmentBuilding = building; 28120 this.nodeAssignmentEligibleNodeIds = this.getEligibleNodeIdsForBuilding(building); 28121 this.selectionService.deselectAll(); 28122 if (this.entityEdit.hasPendingChanges()) { 28123 this.entityEdit.apply(); 28124 } else { 28125 this.entityEdit.cancel(); 28126 } 28127 if (!this.viewState.showNodes()) { 28128 this.onNodesToggled(true); 28129 } else { 28130 this.refreshView(); 28131 } 28132 void this.toastService.showInfo("Click or Shift-drag eligible nodes, then assign them to this building.", 4e3); 28133 this.cdr.markForCheck(); 28134 } 28135 getAssignedNodeCount(building) { 28136 const buildingId = building == null ? void 0 : building.entity.id; 28137 if (!buildingId) 28138 return 0; 28139 return this.getNodesInCurrentCampus().filter((node) => node.entity.buildingId === buildingId).length; 28140 } 28141 getEligibleNodeCount(building) { 28142 if (!(building == null ? void 0 : building.entity.id)) 28143 return 0; 28144 const eligibleIds = this.getEligibleNodeIdsForBuilding(building); 28145 return this.getNodesInCurrentCampus().filter((node) => !!node.entity.id && eligibleIds.has(node.entity.id)).filter((node) => this.isNodeVisibleOnSelectedLevel(node)).length; 28146 } 28147 applyNodeBuildingAssignment() { 28148 const building = this.nodeAssignmentBuilding; 28149 if (!(building == null ? void 0 : building.entity.id)) 28150 return;
28151 const nodeIds = this.getSelectedEligibleAssignmentNodeIds().filter((nodeId) => { 28152 const model = this.database.Models.get(nodeId); 28153 return (model == null ? void 0 : model.entity.buildingId) !== building.entity.id; 28154 }); 28155 if (nodeIds.length === 0) { 28156 void this.toastService.showWarning("Select at least one unassigned eligible node.", 3e3); 28157 return; 28158 } 28159 const command = new UpdateNodesBuildingCommand({ 28160 nodeIds, 28161 buildingId: building.entity.id, 28162 database: this.database, 28163 onVisualUpdate: () => this.refreshView() 28164 }); 28165 this.historyService.executeCommand(command); 28166 this.cancelNodeBuildingAssignment(false); 28167 void this.toastService.showSuccess(`Assigned ${nodeIds.length} node${nodeIds.length === 1 ? "" : "s"} to building.`, 3e3); 28168 this.cdr.markForCheck(); 28169 } 28170 clearNodeAssignmentSelection() { 28171 this.selectionService.deselectAll(); 28172 this.cdr.markForCheck(); 28173 } 28174 cancelNodeBuildingAssignment(clearSelection = true) { 28175 this.clearNodeAssignmentHighlights(); 28176 this.nodeAssignmentBuilding = null; 28177 this.nodeAssignmentEligibleNodeIds.clear(); 28178 if (clearSelection) { 28179 this.selectionService.deselectAll(); 28180 } 28181 this.refreshSelectionHighlights(); 28182 this.cdr.markForCheck(); 28183 } 28184 /** 28185 * Handle apply button from building edit panel. 28186 */ 28187 onBuildingEditPanelApply() { 28188 var _a3; 28189 (_a3 = this.editPanelController) == null ? void 0 : _a3.handleBuildingApply(); 28190 } 28191 /** 28192 * Handle cancel button from building edit panel. 28193 */ 28194 onBuildingEditPanelCancel() { 28195 var _a3; 28196 (_a3 = this.editPanelController) == null ? void 0 : _a3.handleBuildingCancel(); 28197 } 28198 cancelDefaultDestinationPicker() { 28199 this.clearDefaultDestinationPickerHighlights(); 28200 this.defaultDestinationPickerBuilding = null; 28201 this.defaultDestinationPickerNodeIds.clear(); 28202 this.refreshSelectionHighlights(); 28203 this.cdr.markForCheck(); 28204 } 28205 selectDefaultDestinationNode(nodeId) { 28206 const building = this.defaultDestinationPickerBuilding; 28207 if (!(building == null ? void 0 : building.entity.id)) 28208 return; 28209 if (!this.defaultDestinationPickerNodeIds.has(nodeId)) { 28210 void this.toastService.showWarning("That node cannot be used as this building destination.", 3e3); 28211 return; 28212 } 28213 const command = new UpdateBuildingCommand({ 28214 buildingId: building.entity.id, 28215 changes: { defaultDestinationNodeId: nodeId }, 28216 database: this.database, 28217 onVisualUpdate: () => this.refreshView() 28218 }); 28219 this.historyService.executeCommand(command); 28220 this.cancelDefaultDestinationPicker(); 28221 this.selectionService.select({ id: nodeId, type: "node" }, "replace"); 28222 void this.toastService.showSuccess("Default destination node updated.", 3e3); 28223 this.cdr.markForCheck(); 28224 } 28225 handleNodeAssignmentSelection(entity) { 28226 if (!entity) 28227 return; 28228 if (entity.type !== "node") { 28229 void this.toastService.showWarning("Select nodes to assign to the building.", 3e3); 28230 return; 28231 } 28232 if (!this.nodeAssignmentEligibleNodeIds.has(entity.id)) { 28233 void this.toastService.showWarning("That node cannot be assigned to this building.", 3e3); 28234 return; 28235 } 28236 this.selectionService.select(entity, "toggle"); 28237 this.cdr.markForCheck(); 28238 } 28239 getSelectedEligibleAssignmentNodeIds() { 28240 return this.selectionService.selectedEntities().filter((entity) => entity.type === "node" && this.nodeAssignmentEligibleNodeIds.has(entity.id)).map((entity) => entity.id); 28241 } 28242 getEligibleNodeIdsForBuilding(building) { 28243 return new Set(this.getNodesInCurrentCampus().filter((node) => this.isNodeEligibleForBuildingAssignment(node, building)).map((node) => node.entity.id).filter((id) => !!id)); 28244 } 28245 getNodesInCurrentCampus() { 28246 var _a3; 28247 const campusId = (_a3 = this.campusData.getCampus()) == null ? void 0 : _a3.entity.id; 28248 if (!campusId) 28249 return []; 28250 return Array.from(this.database.Models.values()).filter((model) => model.type === EntityType.NODE).filter((node) => this.getNodeCampusId(node) === campusId); 28251 } 28252 isNodeEligibleForBuildingAssignment(node, building) { 28253 var _a3, _b2; 28254 if (!node.entity.id || node.entity.isDeleted) 28255 return false;
28256 if (node.entity.locationX === void 0 || node.entity.locationX === null) 28257 return false; 28258 if (node.entity.locationY === void 0 || node.entity.locationY === null) 28259 return false; 28260 if (!node.entity.floorId || node.entity.type === NodeType.Outdoor) { 28261 return true; 28262 } 28263 const floor = (_a3 = node.floor) != null ? _a3 : node.entity.floorId ? this.database.Models.get(node.entity.floorId) : void 0; 28264 return ((_b2 = floor == null ? void 0 : floor.building) == null ? void 0 : _b2.entity.id) === building.entity.id || (floor == null ? void 0 : floor.entity.buildingId) === building.entity.id; 28265 } 28266 isNodeVisibleOnSelectedLevel(node) { 28267 const level = this.viewState.selectedLevel(); 28268 const floorIds = new Set(this.getFloorsAtLevel(level).map((floor) => floor.entity.id).filter((id) => !!id)); 28269 const outdoorFloorIds = new Set(this.getAllFloorsInCampus().filter((floor) => floor.entity.level === void 0 || floor.entity.level === null).map((floor) => floor.entity.id).filter((id) => !!id)); 28270 const showOutdoor = level === 1 || this.viewState.showOutdoorNodesAlways(); 28271 if (!node.entity.floorId) { 28272 return showOutdoor; 28273 } 28274 return floorIds.has(node.entity.floorId) || showOutdoor && outdoorFloorIds.has(node.entity.floorId); 28275 } 28276 applyNodeAssignmentHighlights() { 28277 if (!this.nodeAssignmentBuilding) 28278 return; 28279 for (const nodeId of this.nodeAssignmentEligibleNodeIds) { 28280 this.levelRenderer.highlightEntity(nodeId, "node"); 28281 } 28282 } 28283 clearNodeAssignmentHighlights() { 28284 for (const nodeId of this.nodeAssignmentEligibleNodeIds) { 28285 this.levelRenderer.unhighlightEntity(nodeId); 28286 } 28287 } 28288 getDefaultDestinationCandidateIds(building) { 28289 return new Set(Array.from(this.database.Models.values()).filter((model) => model.type === EntityType.NODE).filter((node) => this.getNodeCampusId(node) === building.entity.campusId && this.isValidDefaultDestinationNode(node)).map((node) => node.entity.id).filter((id) => !!id)); 28290 } 28291 isValidDefaultDestinationNode(node) { 28292 return !!node.entity.id && !node.entity.isDeleted && node.entity.locationX !== void 0 && node.entity.locationX !== null && node.entity.locationY !== void 0 && node.entity.locationY !== null && !node.entity.tagName && !node.entity.tagDeviceId && (node.connections || []).some((connection) => { 28293 var _a3; 28294 const otherNode = ((_a3 = connection.nodeA) == null ? void 0 : _a3.entity.id) === node.entity.id ? connection.nodeB : connection.nodeA;
28295 return !!otherNode && !otherNode.entity.isDeleted && !!otherNode.entity.id; 28296 }); 28297 } 28298 getNodeCampusId(node) { 28299 var _a3, _b2, _c, _d, _e, _f, _g, _h; 28300 return (_h = (_g = (_d = (_b2 = node.entity.campusId) != null ? _b2 : (_a3 = node.campus) == null ? void 0 : _a3.entity.id) != null ? _d : (_c = node.floor) == null ? void 0 : _c.entity.campusId) != null ? _g : (_f = (_e = node.floor) == null ? void 0 : _e.building) == null ? void 0 : _f.entity.campusId) != null ? _h : null; 28301 } 28302 getDefaultDestinationPickerLevel(building) { 28303 var _a3, _b2; 28304 const defaultNodeId = building.entity.defaultDestinationNodeId; 28305 const defaultNode = defaultNodeId ? this.database.Models.get(defaultNodeId) : void 0; 28306 if (((_a3 = defaultNode == null ? void 0 : defaultNode.floor) == null ? void 0 : _a3.entity.level) !== void 0 && defaultNode.floor.entity.level !== null) { 28307 return defaultNode.floor.entity.level; 28308 } 28309 const firstCandidate = this.findFirstDefaultDestinationCandidate(building); 28310 if (((_b2 = firstCandidate == null ? void 0 : firstCandidate.floor) == null ? void 0 : _b2.entity.level) !== void 0 && firstCandidate.floor.entity.level !== null) { 28311 return firstCandidate.floor.entity.level; 28312 } 28313 return 1; 28314 } 28315 findFirstDefaultDestinationCandidate(building) { 28316 var _a3; 28317 return (_a3 = Array.from(this.database.Models.values()).filter((model) => model.type === EntityType.NODE).filter((node) => !!node.entity.id && this.defaultDestinationPickerNodeIds.has(node.entity.id) && this.getNodeCampusId(node) === building.entity.campusId).sort((a, b) => { 28318 var _a4, _b2, _c, _d; 28319 return ((_b2 = (_a4 = a.floor) == null ? void 0 : _a4.entity.level) != null ? _b2 : 9999) - ((_d = (_c = b.floor) == null ? void 0 : _c.entity.level) != null ? _d : 9999); 28320 })[0]) != null ? _a3 : null; 28321 } 28322 applyDefaultDestinationPickerHighlights() { 28323 if (!this.defaultDestinationPickerBuilding) 28324 return; 28325 this.clearDefaultDestinationPickerHighlights(); 28326 for (const nodeId of this.defaultDestinationPickerNodeIds) { 28327 this.levelRenderer.highlightEntity(nodeId, "node"); 28328 } 28329 } 28330 clearDefaultDestinationPickerHighlights() { 28331 for (const nodeId of this.defaultDestinationPickerNodeIds) { 28332 this.levelRenderer.unhighlightEntity(nodeId); 28333 } 28334 } 28335 // ========================================================================= 28336 // Room Edit Panel 28337 // ========================================================================= 28338 /** 28339 * Open the room edit panel for an existing room. 28340 */ 28341 openRoomEditPanel(roomId, isNew) { 28342 this.entityEdit.openRoomPanel(roomId, isNew); 28343 this.cdr.markForCheck(); 28344 } 28345 // ========================================================================= 28346 // Ground Patch Edit Panel 28347 // ========================================================================= 28348 /** 28349 * Open the ground patch edit panel. 28350 */ 28351 openGroundPatchEditPanel(groundPatchId, isNew) { 28352 this.entityEdit.openGroundPatchPanel(groundPatchId, isNew); 28353 this.cdr.markForCheck(); 28354 } 28355 /** 28356 * Handle add room request from tree panel. 28357 * Creates a new room in create mode. 28358 */ 28359 onAddRoomFromTree(floorId) { 28360 var _a3; 28361 (_a3 = this.treeActionController) == null ? void 0 : _a3.createRoomForFloor(floorId); 28362 this.cdr.markForCheck(); 28363 } 28364 /** 28365 * Handle room property change from edit panel. 28366 */ 28367 onRoomEditPanelChange(changes) { 28368 var _a3; 28369 (_a3 = this.editPanelController) == null ? void 0 : _a3.handleRoomChange(changes); 28370 } 28371 /** 28372 * Handle apply button from room edit panel. 28373 */ 28374 onRoomEditPanelApply() { 28375 var _a3; 28376 (_a3 = this.editPanelController) == null ? void 0 : _a3.handleRoomApply(); 28377 } 28378 /** 28379 * Handle cancel button from room edit panel. 28380 */ 28381 onRoomEditPanelCancel() { 28382 var _a3; 28383 (_a3 = this.editPanelController) == null ? void 0 : _a3.handleRoomCancel(); 28384 } 28385 /** 28386 * Handle assigning an existing person to the room. 28387 */ 28388 onPersonAssigned(event) { 28389 var _a3; 28390 (_a3 = this.editPanelController) == null ? void 0 : _a3.handlePersonAssigned(event.personId); 28391 } 28392 /** 28393 * Handle removing a person assignment from the room. 28394 */ 28395 onPersonRemoved(event) { 28396 var _a3; 28397 (_a3 = this.editPanelController) == null ? void 0 : _a3.handlePersonRemoved(event.personRoomId); 28398 } 28399 /** 28400 * Handle creating a new person and assigning to the room. 28401 */ 28402 onPersonCreated(event) { 28403 var _a3; 28404 (_a3 = this.editPanelController) == null ? void 0 : _a3.handlePersonCreated(event.
28404name); 28405 } 28406 // ========================================================================= 28407 // Ground Patch Edit Panel Events 28408 // ========================================================================= 28409 /** 28410 * Handle ground patch property change from edit panel. 28411 */ 28412 onGroundPatchEditPanelChange(changes) { 28413 var _a3; 28414 (_a3 = this.editPanelController) == null ? void 0 : _a3.handleGroundPatchChange(changes); 28415 } 28416 /** 28417 * Handle apply button from ground patch edit panel. 28418 */ 28419 onGroundPatchEditPanelApply() { 28420 var _a3; 28421 (_a3 = this.editPanelController) == null ? void 0 : _a3.handleGroundPatchApply(); 28422 } 28423 /** 28424 * Handle cancel button from ground patch edit panel. 28425 */ 28426 onGroundPatchEditPanelCancel() { 28427 var _a3; 28428 (_a3 = this.editPanelController) == null ? void 0 : _a3.handleGroundPatchCancel(); 28429 } 28430 /** 28431 * Handle edit outline button from ground patch edit panel. 28432 */ 28433 onGroundPatchEditOutline() { 28434 const groundPatch = this.entityEdit.editingGroundPatch(); 28435 if (!(groundPatch == null ? void 0 : groundPatch.entity.id)) 28436 return; 28437 this.entityEdit.closePanel(); 28438 this.modeService.enterShapeEdit(groundPatch.entity.id, "groundPatch"); 28439 this.cdr.markForCheck(); 28440 } 28441 // ========================================================================= 28442 // Navigation Guard 28443 // ========================================================================= 28444 /** 28445 * Called by CanDeactivate guard when navigating away. 28446 * Shows confirmation dialog if there are unsaved changes. 28447 */ 28448 canDeactivate() { 28449 return __async(this, null, function* () { 28450 if (!this.historyService.hasUnsavedChanges()) { 28451 return true; 28452 } 28453 const alert = yield this.alertController.create({ 28454 header: "Unsaved Changes", 28455 message: "You have unsaved changes. Discard them?", 28456 buttons: [ 28457 { text: "Stay", role: "cancel" }, 28458 { text: "Discard", role: "destructive", cssClass: "danger" } 28459 ] 28460 }); 28461 yield alert.present(); 28462 const { role } = yield alert.onDidDismiss(); 28463 if (role === "destructive") { 28464 yield this.historyService.discardChanges(); 28465 return true; 28466 } 28467 return false; 28468 }); 28469 } 28470 // ========================================================================= 28471 // Cleanup 28472 // ========================================================================= 28473 dispose() { 28474 var _a3, _b2, _c, _d, _e; 28475 (_a3 = this.cameraController) == null ? void 0 : _a3.dispose(); 28476 (_b2 = this.gestureHandlers) == null ? void 0 : _b2.disposeAll(); 28477 (_c = this.model3DController) == null ? void 0 : _c.dispose(); 28478 (_d = this.groundPatchRenderer) == null ? void 0 : _d.dispose(); 28479 (_e = this.levelRenderer) == null ? void 0 : _e.dispose(); 28480 this.mapScene.dispose(); 28481 } 28482}; 28483_CampusMapV3Page.\u0275fac = function CampusMapV3Page_Factory(__ngFactoryType__) { 28484 return new (__ngFactoryType__ || _CampusMapV3Page)(\u0275\u0275directiveInject(ActivatedRoute), \u0275\u0275directiveInject(DatabaseService), \u0275\u0275directiveInject(ModelCacheService), \u0275\u0275directiveInject(NgZone), \u0275\u0275directiveInject(ChangeDetectorRef)); 28485}; 28486_CampusMapV3Page.\u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _CampusMapV3Page, selectors: [["app-campus-map-v3"]], viewQuery: function CampusMapV3Page_Query(rf, ctx) { 28487 if (rf & 1) { 28488 \u0275\u0275viewQuery(_c07, 7); 28489 } 28490 if (rf & 2) { 28491 let _t; 28492 \u0275\u0275queryRefresh(_t = \u0275\u0275loadQuery()) && (ctx.canvasContainer = _t.first); 28493 } 28494}, hostBindings: function CampusMapV3Page_HostBindings(rf, ctx) { 28495 if (rf & 1) { 28496 \u0275\u0275listener("resize", function CampusMapV3Page_resize_HostBindingHandler() { 28497 return ctx.onResize(); 28498 }, false, \u0275\u0275resolveWindow)("beforeunload", function CampusMapV3Page_beforeunload_HostBindingHandler($event) { 28499 return ctx.onBeforeUnload($event); 28500 }, false, \u0275\u0275resolveWindow)("keydown", function CampusMapV3Page_keydown_HostBindingHandler($event) { 28501 return ctx.onKeyDown($event); 28502 }, false, \u0275\u0275resolveWindow); 28503 } 28504}, features: [\u0275\u0275ProvidersFeature([SelectionService, EditorHistoryService, TreeStateService, ViewStateService, CampusDataService, EntityEditService, Model3DService, ShapeEditStateService, EditorModeService, MapSceneService, GPSTransformManagerService])], decls: 25, vars: 5
285045, consts: [["canvasContainer", ""], [1, "ion-no-padding"], [3, "buildingsToggled", "roomsToggled", "nodesToggled", "groundReferenceToggled", "centerOnSelectionToggled", "outdoorNodesAlwaysToggled", "roomLabelsToggled", "outlinesOnlyToggled", "floorBackgroundsToggled", "collapsedChange", "nodeClicked", "nodeContextMenu", "addBuildingRequested", "showBuildings", "showRooms", "showNodes", "showGroundReference", "centerOnSelection", "showOutdoorNodesAlways", "showRoomLabels", "showOutlinesOnly", "showFloorBackgrounds", "collapsed", "isMobile"], [1, "canvas-container"], [1, "model-loading-overlay"], [3, "rect", "visible"], [1, "default-destination-picker-banner", 3, "left"], [1, "node-assignment-banner", 3, "left"], [3, "visible", "angle", "x", "y"], [3, "wall", "error"], ["position", "top-left"], [3, "model3DToggled", "model3DSettingsClicked", "rulerToggled", "fitToView", "addNodeClicked", "addRoomClicked", "drawFloorOutlineClicked", "menuClicked", "createGroundPatchClicked", "createFloorClicked", "gpsDebugClicked", "showNodes", "showRooms", "showFloorOutlineButton", "show3DModel", "has3DModel", "is3DModelLoading", "isRulerActive", "isAddNodeMode", "isDrawingRoom", "isDrawingFloorOutline", "isDrawingGroundPatch", "isDrawingFloor"], ["position", "top-right"], [3, "editorAction", "canUndo", "canRedo", "hasUnsavedChanges", "undoDescription", "redoDescription"], ["position", "right"], [3, "buildings"], [3, "levels", "currentLevel"], ["position", "bottom"], [3, "actionTriggered", "selectedEntities", "availableActions", "isEditingShape", "isConnectMode"], [1, "model-loading-card"], [1, "model-loading-title"], [1, "progress-bar"], [1, "progress-fill"], [1, "progress-text"], [1, "default-destination-picker-banner"], [1, "picker-copy"], [1, "picker-title"], [1, "picker-subtitle"], ["type", "button", 3, "click"], [1, "node-assignment-banner"], [1, "assignment-actions"], ["type", "button", 3, "click", "disabled"], [3, "lengthPreview", "commitRequested", "cancelRequested", "wall", "error"], [1, "model-3d-picker"], [3, "transformChanged", "transformCommit", "resetRequested", "closeRequested", "centerToViewportRequested", "scaleToViewportRequested", "centerToCampusRequested", "scaleToCampusRequested", "transform"], [1, "model-3d-picker-select", 3, "ionChange", "value", "interface"], ["slot", "start"], [3, "value"], [3, "backdropClick"], [3, "nodeChanged", "applyRequested", "cancelRequested", "closeRequested", "node", "floorName", "isNew", "buildings"], [3, "floorChanged", "applyRequested", "cancelRequested", "closeRequested", "drawOutlineRequested", "editOutlineRequested", "floor", "isNew"], [3, "buildingChanged", "pickDefaultDestinationRequested", "clearDefaultDestinationRequested", "assignNodesRequested", "applyRequested", "cancelRequested", "closeRequested", "building", "isNew", "assignedNodeCount", "eligibleNodeCount"], [3, "roomChanged", "applyRequested", "cancelRequested", "closeRequested", "personAssigned", "personRemoved", "personCreated", "room", "isNew", "allPersons"], [3, "groundPatchChanged", "applyRequested", "cancelRequested", "closeRequested", "editOutlineRequested", "groundPatch", "isNew"], [3, "buildingSelected", "cancelRequested", "closeRequested", "buildings"], [3, "closeRequested", "gpsNodeLocate"], [3, "levelSelected", "levels", "currentLevel"]], template: function CampusMapV3Page_Template(rf, ctx) { 28505 if (rf & 1) { 28506 const _r1 = \u0275\u0275getCurrentView(); 28507 \u0275\u0275elementStart(0, "ion-content", 1)(1, "app-entity-sidebar", 2); 28508 \u0275\u0275listener("buildingsToggled", function CampusMapV3Page_Template_app_entity_sidebar_buildingsToggled_1_listener($event) { 28509 \u0275\u0275restoreView(_r1); 28510 return \u0275\u0275resetView(ctx.onBuildingsToggled($event)); 28511 })("roomsToggled", function CampusMapV3Page_Template_app_entity_sidebar_roomsToggled_1_listener($event) { 28512 \u0275\u0275restoreView(_r1); 28513 return \u0275\u0275resetView(ctx.onRoomsToggled($event)); 28514 })("nodesToggled", function CampusMapV3Page_Template_app_entity_sidebar_nodesToggled_1_listener($event) { 28515 \u0275\u0275restoreView(_r1);
28516 return \u0275\u0275resetView(ctx.onNodesToggled($event)); 28517 })("groundReferenceToggled", function CampusMapV3Page_Template_app_entity_sidebar_groundReferenceToggled_1_listener($event) { 28518 \u0275\u0275restoreView(_r1); 28519 return \u0275\u0275resetView(ctx.onGroundReferenceToggled($event)); 28520 })("centerOnSelectionToggled", function CampusMapV3Page_Template_app_entity_sidebar_centerOnSelectionToggled_1_listener($event) { 28521 \u0275\u0275restoreView(_r1); 28522 return \u0275\u0275resetView(ctx.onCenterOnSelectionToggled($event)); 28523 })("outdoorNodesAlwaysToggled", function CampusMapV3Page_Template_app_entity_sidebar_outdoorNodesAlwaysToggled_1_listener($event) { 28524 \u0275\u0275restoreView(_r1); 28525 return \u0275\u0275resetView(ctx.onOutdoorNodesAlwaysToggled($event)); 28526 })("roomLabelsToggled", function CampusMapV3Page_Template_app_entity_sidebar_roomLabelsToggled_1_listener($event) { 28527 \u0275\u0275restoreView(_r1); 28528 return \u0275\u0275resetView(ctx.onRoomLabelsToggled($event)); 28529 })("outlinesOnlyToggled", function CampusMapV3Page_Template_app_entity_sidebar_outlinesOnlyToggled_1_listener($event) { 28530 \u0275\u0275restoreView(_r1); 28531 return \u0275\u0275resetView(ctx.onOutlinesOnlyToggled($event)); 28532 })("floorBackgroundsToggled", function CampusMapV3Page_Template_app_entity_sidebar_floorBackgroundsToggled_1_listener($event) { 28533 \u0275\u0275restoreView(_r1); 28534 return \u0275\u0275resetView(ctx.onFloorBackgroundsToggled($event)); 28535 })("collapsedChange", function CampusMapV3Page_Template_app_entity_sidebar_collapsedChange_1_listener($event) { 28536 \u0275\u0275restoreView(_r1); 28537 return \u0275\u0275resetView(ctx.onSidebarCollapsedChange($event)); 28538 })("nodeClicked", function CampusMapV3Page_Template_app_entity_sidebar_nodeClicked_1_listener($event) { 28539 \u0275\u0275restoreView(_r1); 28540 return \u0275\u0275resetView(ctx.onTreeNodeClicked($event)); 28541 })("nodeContextMenu", function CampusMapV3Page_Template_app_entity_sidebar_nodeContextMenu_1_listener($event) { 28542 \u0275\u0275restoreView(_r1); 28543 return \u0275\u0275resetView(ctx.onTreeNodeContextMenu($event)); 28544 })("addBuildingRequested", function CampusMapV3Page_Template_app_entity_sidebar_addBuildingRequested_1_listener() { 28545 \u0275\u0275restoreView(_r1); 28546 return \u0275\u0275resetView(ctx.onAddBuildingFromTree()); 28547 }); 28548 \u0275\u0275elementEnd(); 28549 \u0275\u0275element(2, "div", 3, 0); 28550 \u0275\u0275template(4, CampusMapV3Page_Conditional_4_Template, 8, 3, "div", 4); 28551 \u0275\u0275element(5, "app-marquee-box", 5); 28552 \u0275\u0275template(6, CampusMapV3Page_Conditional_6_Template, 8, 3, "div", 6)(7, CampusMapV3Page_Conditional_7_Template, 13, 6, "div", 7); 28553 \u0275\u0275element(8, "app-rotation-indicator", 8); 28554 \u0275\u0275template(9, CampusMapV3Page_Conditional_9_Template, 1, 2, "app-wall-length-inspector", 9); 28555 \u0275\u0275elementStart(10, "app-toolbar-container", 10)(11, "app-view-toolbar", 11); 28556 \u0275\u0275listener("model3DToggled", function CampusMapV3Page_Template_app_view_toolbar_model3DToggled_11_listener($event) { 28557 \u0275\u0275restoreView(_r1); 28558 return \u0275\u0275resetView(ctx.on3DModelToggled($event)); 28559 })("model3DSettingsClicked", function CampusMapV3Page_Template_app_view_toolbar_model3DSettingsClicked_11_listener() { 28560 \u0275\u0275restoreView(_r1); 28561 return \u0275\u0275resetView(ctx.onModel3DSettingsClicked()); 28562 })("rulerToggled", function CampusMapV3Page_Template_app_view_toolbar_rulerToggled_11_listener($event) { 28563 \u0275\u0275restoreView(_r1); 28564 return \u0275\u0275resetView(ctx.onRulerToggled($event)); 28565 })("fitToView", function CampusMapV3Page_Template_app_view_toolbar_fitToView_11_listener() { 28566 \u0275\u0275restoreView(_r1); 28567 return \u0275\u0275resetView(ctx.fitToView()); 28568 })("addNodeClicked", function CampusMapV3Page_Template_app_view_toolbar_addNodeClicked_11_listener() { 28569 \u0275\u0275restoreView(_r1); 28570 return \u0275\u0275resetView(ctx.onAddNodeClicked()); 28571 })("addRoomClicked", function CampusMapV3Page_Template_app_view_toolbar_addRoomClicked_11_listener() { 28572 \u0275\u0275restoreView(_r1);
28573 return \u0275\u0275resetView(ctx.onAddRoomClicked()); 28574 })("drawFloorOutlineClicked", function CampusMapV3Page_Template_app_view_toolbar_drawFloorOutlineClicked_11_listener() { 28575 \u0275\u0275restoreView(_r1); 28576 return \u0275\u0275resetView(ctx.onDrawFloorOutlineFromToolbar()); 28577 })("menuClicked", function CampusMapV3Page_Template_app_view_toolbar_menuClicked_11_listener() { 28578 \u0275\u0275restoreView(_r1); 28579 return \u0275\u0275resetView(ctx.onMenuClicked()); 28580 })("createGroundPatchClicked", function CampusMapV3Page_Template_app_view_toolbar_createGroundPatchClicked_11_listener() { 28581 \u0275\u0275restoreView(_r1); 28582 return \u0275\u0275resetView(ctx.onCreateGroundPatchClicked()); 28583 })("createFloorClicked", function CampusMapV3Page_Template_app_view_toolbar_createFloorClicked_11_listener() { 28584 \u0275\u0275restoreView(_r1); 28585 return \u0275\u0275resetView(ctx.onCreateFloorClicked()); 28586 })("gpsDebugClicked", function CampusMapV3Page_Template_app_view_toolbar_gpsDebugClicked_11_listener() { 28587 \u0275\u0275restoreView(_r1); 28588 return \u0275\u0275resetView(ctx.onGpsDebugClicked()); 28589 }); 28590 \u0275\u0275elementEnd()(); 28591 \u0275\u0275elementStart(12, "app-toolbar-container", 12)(13, "app-editor-toolbar", 13); 28592 \u0275\u0275listener("editorAction", function CampusMapV3Page_Template_app_editor_toolbar_editorAction_13_listener($event) { 28593 \u0275\u0275restoreView(_r1); 28594 return \u0275\u0275resetView(ctx.onEditorAction($event)); 28595 }); 28596 \u0275\u0275elementEnd()(); 28597 \u0275\u0275template(14, CampusMapV3Page_Conditional_14_Template, 3, 2, "app-toolbar-container", 14)(15, CampusMapV3Page_Conditional_15_Template, 2, 4, "app-modal-overlay")(16, CampusMapV3Page_Conditional_16_Template, 2, 2, "app-modal-overlay")(17, CampusMapV3Page_Conditional_17_Template, 2, 4, "app-modal-overlay")(18, CampusMapV3Page_Conditional_18_Template, 2, 3, "app-modal-overlay")(19, CampusMapV3Page_Conditional_19_Template, 2, 2, "app-modal-overlay")(20, CampusMapV3Page_Conditional_20_Template, 1, 1, "app-building-selection-modal", 15)(21, CampusMapV3Page_Conditional_21_Template, 2, 0, "app-modal-overlay")(22, CampusMapV3Page_Conditional_22_Template, 1, 2, "app-level-selector", 16); 28598 \u0275\u0275elementStart(23, "app-toolbar-container", 17)(24, "app-selection-toolbar", 18); 28599 \u0275\u0275listener("actionTriggered", function CampusMapV3Page_Template_app_selection_toolbar_actionTriggered_24_listener($event) { 28600 \u0275\u0275restoreView(_r1); 28601 return \u0275\u0275resetView(ctx.onSelectionAction($event)); 28602 }); 28603 \u0275\u0275elementEnd()()(); 28604 } 28605 if (rf & 2) { 28606 let tmp_22_0; 28607 \u0275\u0275advance(); 28608 \u0275\u0275property("showBuildings", ctx.viewState.showBuildings())("showRooms", ctx.viewState.showRooms())("showNodes", ctx.viewState.showNodes())("showGroundReference", ctx.viewState.showGroundReference())("centerOnSelection", ctx.viewState.centerOnSelection())("showOutdoorNodesAlways", ctx.viewState.showOutdoorNodesAlways())("showRoomLabels", ctx.viewState.showRoomLabels())("showOutlinesOnly", ctx.viewState.showOutlinesOnly())("showFloorBackgrounds", ctx.viewState.showFloorBackgrounds())("collapsed", ctx.viewState.sidebarCollapsed())("isMobile", ctx.viewState.isMobileView()); 28609 \u0275\u0275advance(); 28610 \u0275\u0275styleProp("margin-left", ctx.viewState.isMobileView() ? 0 : ctx.viewState.sidebarCollapsed() ? 0 : 280, "px"); 28611 \u0275\u0275advance(2); 28612 \u0275\u0275conditional(ctx.is3DModelLoading ? 4 : -1); 28613 \u0275\u0275advance(); 28614 \u0275\u0275property("rect", ctx.marqueeRect)("visible", ctx.showMarquee); 28615 \u0275\u0275advance(); 28616 \u0275\u0275conditional(ctx.defaultDestinationPickerBuilding ? 6 : -1); 28617 \u0275\u0275advance(); 28618 \u0275\u0275conditional(ctx.nodeAssignmentBuilding ? 7 : -1); 28619 \u0275\u0275advance(); 28620 \u0275\u0275property("visible", ctx.rotationIndicatorVisible)("angle", ctx.rotationIndicatorAngle)("x", ctx.rotationIndicatorX)("y", ctx.rotationIndicatorY); 28621 \u0275\u0275advance(); 28622 \u0275\u0275conditional((tmp_22_0 = ctx.shapeEditState.selectedRoomWall()) ? 9 : -1, tmp_22_0); 28623 \u0275\u0275advance(); 28624 \u0275\u0275styleProp("--toolbar-left", ctx.viewState.isMobileView() ? 15 : ctx.viewState.sidebarCollapsed() ? 15 : 295, "px"); 28625 \u0275\u0275advance(); 28626 \u0275\u0275property("showNodes", ctx.viewState.showNodes())("showRooms", ctx.viewState.showRooms())("showFloorOutlineButton", ctx.showFloorOutlineButton)("show3DModel", ctx.show3DModel)("has3DModel", ctx.has3DModel)("is3DModelLoading", ctx.is3DModelLoading)("isRulerActive", ctx.isRulerActive)("isAddNodeMode", ctx.isAddNodeMode)("isDrawingRoom", ctx.isDrawingRoom)("isDrawingFloorOutline", ctx.isDrawingFloorOutline)("isDrawingGroundPatch", ctx.modeService.isDrawingGroundPatch())("isDrawingFloor", false); 28627 \u0275\u0275advance(2); 28628 \u0275\u0275property("canUndo", ctx.historyService.canUndo())("canRedo", ctx.historyService.canRedo())("hasUnsavedChanges", ctx.historyService.hasUnsavedChanges())("undoDescription", ctx.historyService.undoDescription())("redo
28628Description", ctx.historyService.redoDescription()); 28629 \u0275\u0275advance(); 28630 \u0275\u0275conditional(ctx.show3DModel && ctx.showTransformPanel ? 14 : -1); 28631 \u0275\u0275advance(); 28632 \u0275\u0275conditional(ctx.entityEdit.showNodeEditPanel() && ctx.entityEdit.editingNode() ? 15 : -1); 28633 \u0275\u0275advance(); 28634 \u0275\u0275conditional(ctx.entityEdit.showFloorEditPanel() && ctx.entityEdit.editingFloor() ? 16 : -1); 28635 \u0275\u0275advance(); 28636 \u0275\u0275conditional(ctx.entityEdit.showBuildingEditPanel() && ctx.entityEdit.editingBuilding() ? 17 : -1); 28637 \u0275\u0275advance(); 28638 \u0275\u0275conditional(ctx.entityEdit.showRoomEditPanel() && ctx.entityEdit.editingRoom() ? 18 : -1); 28639 \u0275\u0275advance(); 28640 \u0275\u0275conditional(ctx.entityEdit.showGroundPatchEditPanel() && ctx.entityEdit.editingGroundPatch() ? 19 : -1); 28641 \u0275\u0275advance(); 28642 \u0275\u0275conditional(ctx.showBuildingSelectionModal ? 20 : -1); 28643 \u0275\u0275advance(); 28644 \u0275\u0275conditional(ctx.showGpsDebugModal ? 21 : -1); 28645 \u0275\u0275advance(); 28646 \u0275\u0275conditional(ctx.viewState.availableLevels().length > 0 ? 22 : -1); 28647 \u0275\u0275advance(2); 28648 \u0275\u0275property("selectedEntities", ctx.selectionService.selectedEntities())("availableActions", ctx.selectionToolbarActions)("isEditingShape", ctx.isEditingShape)("isConnectMode", ctx.isConnectMode); 28649 } 28650}, dependencies: [IonContent, IonSelect, IonSelectOption, IonLabel, TranslateModule, TranslatePipe, ToolbarContainerComponent, ModalOverlayComponent, ViewToolbarComponent, SelectionToolbarComponent, EditorToolbarComponent, LevelSelectorComponent, MarqueeBoxComponent, RotationIndicatorComponent, WallLengthInspectorComponent, ModelTransformPanelComponent, GpsDebugModalComponent, NodeEditPanelComponent, FloorEditPanelComponent, BuildingEditPanelComponent, RoomEditPanelComponent, GroundPatchEditPanelComponent, EntitySidebarComponent, BuildingSelectionModalComponent], styles: ["\n\n[_nghost-%COMP%] {\n display: block;\n width: 100%;\n height: 100%;\n}\n.canvas-container[_ngcontent-%COMP%] {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n overflow: hidden;\n touch-action: none;\n transition: margin-left 0.25s ease-out;\n}\n[_nghost-%COMP%] .room-wall-dimension-label {\n pointer-events: none;\n transform: translate(-50%, -30px);\n padding: 4px 7px;\n border: 1px solid rgba(249, 115, 22, 0.7);\n border-radius: 4px;\n background: rgba(15, 23, 42, 0.92);\n box-shadow: 0 4px 12px rgba(15, 23, 42, 0.25);\n color: #fff;\n font: 700 12px/1 system-ui, sans-serif;\n white-space: nowrap;\n}\n.default-destination-picker-banner[_ngcontent-%COMP%], \n.node-assignment-banner[_ngcontent-%COMP%] {\n position: absolute;\n top: 16px;\n right: 16px;\n z-index: 90;\n display: flex;\n align-items: center;\n gap: 14px;\n max-width: min(560px, 100% - 32px);\n padding: 10px 12px;\n background: rgba(255, 255, 255, 0.96);\n border: 1px solid rgba(16, 185, 129, 0.45);\n box-shadow: 0 10px 28px rgba(15, 23, 42, 0.18);\n color: #10201a;\n transition: left 0.25s ease-out;\n}\n.node-assignment-banner[_ngcontent-%COMP%] {\n border-color: rgba(59, 130, 246, 0.45);\n color: #0f2035;\n}\n.picker-copy[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n.picker-title[_ngcontent-%COMP%], \n.picker-subtitle[_ngcontent-%COMP%] {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.picker-title[_ngcontent-%COMP%] {\n font-size: 0.92rem;\n font-weight: 700;\n}\n.picker-subtitle[_ngcontent-%COMP%] {\n font-size: 0.8rem;\n color: #315348;\n}\n.default-destination-picker-banner[_ngcontent-%COMP%] button[_ngcontent-%COMP%], \n.node-assignment-banner[_ngcontent-%COMP%] button[_ngcontent-%COMP%] {\n border: 0;\n background: transparent;\n color: var(--ion-color-primary);\n font-weight: 600;\n cursor: pointer;\n padding: 6px 4px;\n}\n.node-assignment-banner[_ngcontent-%COMP%] button[_ngcontent-%COMP%]:disabled {\n opacity: 0.45;\n cursor: default;\n}\n.assignment-actions[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n}\n@media (max-width: 640px) {\n .default-destination-picker-banner[_ngcontent-%COMP%], \n .node-assignment-banner[_ngcontent-%COMP%] {\n right: 12px;\n left: 12px !important;\n max-width: none;\n flex-direction: column;
28650\n align-items: stretch;\n gap: 8px;\n }\n .assignment-actions[_ngcontent-%COMP%] {\n justify-content: space-between;\n gap: 6px;\n }\n}\nion-content[_ngcontent-%COMP%] {\n --padding-top: 0;\n --padding-bottom: 0;\n --padding-start: 0;\n --padding-end: 0;\n}\n .tool-palette-container.tool-palette-top-left {\n transition: left 0.25s ease-out;\n}\n.model-loading-overlay[_ngcontent-%COMP%] {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.3);\n z-index: 100;\n}\n.model-loading-card[_ngcontent-%COMP%] {\n background: var(--ion-background-color, white);\n padding: 24px 32px;\n border-radius: 12px;\n text-align: center;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);\n min-width: 240px;\n}\n.model-loading-title[_ngcontent-%COMP%] {\n display: block;\n font-size: 1rem;\n font-weight: 500;\n color: var(--ion-text-color);\n margin-bottom: 16px;\n}\n.progress-bar[_ngcontent-%COMP%] {\n width: 100%;\n height: 8px;\n background: var(--ion-color-light, #e0e0e0);\n border-radius: 4px;\n overflow: hidden;\n}\n.progress-fill[_ngcontent-%COMP%] {\n height: 100%;\n background: var(--ion-color-primary);\n border-radius: 4px;\n transition: width 0.2s ease;\n}\n.progress-text[_ngcontent-%COMP%] {\n display: block;\n font-size: 0.875rem;\n color: var(--ion-color-medium);\n margin-top: 8px;\n}\n.model-3d-picker[_ngcontent-%COMP%] {\n background: var(--tool-palette-bg, rgba(255, 255, 255, 0.95));\n border-radius: 12px;\n box-shadow: var(--tool-palette-shadow, 0 4px 12px rgba(0, 0, 0, 0.15));\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light, rgba(0, 0, 0, 0.08));\n min-width: 380px;\n max-width: 380px;\n overflow: hidden;\n padding: 4px 8px;\n}\n.model-3d-picker-select[_ngcontent-%COMP%] {\n --padding-start: 8px;\n width: 100%;\n font-size: 14px;\n color: var(--ion-text-color);\n}"], changeDetection: 0 }); 28651var CampusMapV3Page = _CampusMapV3Page; 28652(() => { 28653 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(CampusMapV3Page, [{ 28654 type: Component, 28655 args: [{ selector: "app-campus-map-v3", standalone: true, imports: [IonContent, IonSelect, IonSelectOption, IonSelectModal, IonLabel, TranslateModule, ToolbarContainerComponent, ModalOverlayComponent, ViewToolbarComponent, SelectionToolbarComponent, EditorToolbarComponent, LevelSelectorComponent, MarqueeBoxComponent, RotationIndicatorComponent, WallLengthInspectorComponent, ModelTransformPanelComponent, GpsDebugModalComponent, NodeEditPanelComponent, FloorEditPanelComponent, BuildingEditPanelComponent, RoomEditPanelComponent, GroundPatchEditPanelComponent, EntitySidebarComponent, BuildingSelectionModalComponent], providers: [SelectionService, EditorHistoryService, TreeStateService, ViewStateService, CampusDataService, EntityEditService, Model3DService, ShapeEditStateService, EditorModeService, MapSceneService, GPSTransformManagerService], changeDetection: ChangeDetectionStrategy.OnPush, template: `<ion-content class="ion-no-padding"> 28656 <!-- Entity Sidebar (full-height, always present) --> 28657 <app-entity-sidebar 28658 [showBuildings]="viewState.showBuildings()" 28659 [showRooms]="viewState.showRooms()" 28660 [showNodes]="viewState.showNodes()" 28661 [showGroundReference]="viewState.showGroundReference()" 28662 [centerOnSelection]="viewState.centerOnSelection()" 28663 [showOutdoorNodesAlways]="viewState.showOutdoorNodesAlways()" 28664 [showRoomLabels]="viewState.showRoomLabels()" 28665 [showOutlinesOnly]="viewState.showOutlinesOnly()" 28666 [showFloorBackgrounds]="viewState.showFloorBackgrounds()" 28667 [collapsed]="viewState.sidebarCollapsed()" 28668 [isMobile]="viewState.isMobileView()" 28669 (buildingsToggled)="onBuildingsToggled($event)" 28670 (roomsToggled)="onRoomsToggled($event)" 28671 (nodesToggled)="onNodesToggled($event)" 28672 (groundReferenceToggled)="onGroundReferenceToggled($event)" 28673 (centerOnSelectionToggled)="onCenterOnSelectionToggled($event)" 28674 (outdoorNodesAlwaysToggled)="onOutdoorNodesAlwaysToggled($event)" 28675 (roomLabelsToggled)="onRoomLabelsToggled($event)"
28676 (outlinesOnlyToggled)="onOutlinesOnlyToggled($event)" 28677 (floorBackgroundsToggled)="onFloorBackgroundsToggled($event)" 28678 (collapsedChange)="onSidebarCollapsedChange($event)" 28679 (nodeClicked)="onTreeNodeClicked($event)" 28680 (nodeContextMenu)="onTreeNodeContextMenu($event)" 28681 (addBuildingRequested)="onAddBuildingFromTree()" 28682 /> 28683 28684 <!-- Canvas container with dynamic margin for sidebar (desktop only) --> 28685 <div 28686 #canvasContainer 28687 class="canvas-container" 28688 [style.margin-left.px]="viewState.isMobileView() ? 0 : (viewState.sidebarCollapsed() ? 0 : 280)" 28689 ></div> 28690 28691 <!-- 3D Model loading overlay --> 28692 @if (is3DModelLoading) { 28693 <div class="model-loading-overlay"> 28694 <div class="model-loading-card"> 28695 <span class="model-loading-title">Loading 3D Model</span> 28696 <div class="progress-bar"> 28697 <div class="progress-fill" [style.width.%]="model3DLoadProgress"></div> 28698 </div> 28699 <span class="progress-text">{{ model3DLoadProgress }}%</span> 28700 </div> 28701 </div> 28702 } 28703 28704 <!-- Marquee selection box overlay --> 28705 <app-marquee-box [rect]="marqueeRect" [visible]="showMarquee" /> 28706 28707 @if (defaultDestinationPickerBuilding) { 28708 <div 28709 class="default-destination-picker-banner" 28710 [style.left.px]="viewState.isMobileView() ? 15 : (viewState.sidebarCollapsed() ? 15 : 295)"> 28711 <div class="picker-copy"> 28712 <span class="picker-title">Pick default destination node</span> 28713 <span class="picker-subtitle">Click a highlighted node for {{ defaultDestinationPickerBuilding.entity.name || 'this building' }}.</span> 28714 </div> 28715 <button type="button" (click)="cancelDefaultDestinationPicker()">Cancel</button> 28716 </div> 28717 } 28718 28719 @if (nodeAssignmentBuilding) { 28720 <div 28721 class="node-assignment-banner" 28722 [style.left.px]="viewState.isMobileView() ? 15 : (viewState.sidebarCollapsed() ? 15 : 295)"> 28723 <div class="picker-copy"> 28724 <span class="picker-title">Assign nodes to {{ nodeAssignmentBuilding.entity.name || 'this building' }}</span> 28725 <span class="picker-subtitle">{{ assignmentSelectedNodeCount }} selected</span> 28726 </div> 28727 <div class="assignment-actions"> 28728 <button type="button" [disabled]="assignmentSelectedNodeCount === 0" (click)="applyNodeBuildingAssignment()">Assign selected</button> 28729 <button type="button" [disabled]="assignmentSelectedNodeCount === 0" (click)="clearNodeAssignmentSelection()">Clear</button> 28730 <button type="button" (click)="cancelNodeBuildingAssignment()">Cancel</button> 28731 </div> 28732 </div> 28733 } 28734 28735 <!-- Rotation indicator overlay --> 28736 <app-rotation-indicator 28737 [visible]="rotationIndicatorVisible" 28738 [angle]="rotationIndicatorAngle" 28739 [x]="rotationIndicatorX" 28740 [y]="rotationIndicatorY" 28741 /> 28742 28743 @if (shapeEditState.selectedRoomWall(); as wall) { 28744 <app-wall-length-inspector 28745 [wall]="wall" 28746 [error]="shapeEditState.wallLengthError()" 28747 (lengthPreview)="onWallLengthPreview($event)" 28748 (commitRequested)="onWallLengthCommit()" 28749 (cancelRequested)="onWallLengthCancel()" 28750 /> 28751 } 28752 28753 <app-toolbar-container 28754 position="top-left" 28755 [style.--toolbar-left.px]="viewState.isMobileView() ? 15 : (viewState.sidebarCollapsed() ? 15 : 295)"> 28756 <app-view-toolbar 28757 [showNodes]="viewState.showNodes()" 28758 [showRooms]="viewState.showRooms()" 28759 [showFloorOutlineButton]="showFloorOutlineButton" 28760 [show3DModel]="show3DModel" 28761 [has3DModel]="has3DModel" 28762 [is3DModelLoading]="is3DModelLoading" 28763 [isRulerActive]="isRulerActive" 28764 [isAddNodeMode]="isAddNodeMode" 28765 [isDrawingRoom]="isDrawingRoom" 28766 [isDrawingFloorOutline]="isDrawingFloorOutline" 28767 [isDrawingGroundPatch]="modeService.isDrawingGroundPatch()" 28768 [isDrawingFloor]="false" 28769 (model3DToggled)="on3DModelToggled($event)" 28770 (model3DSettingsClicked)="onModel3DSettingsClicked()" 28771 (rulerToggled)="onRulerToggled($event)" 28772 (fitToView)="fitToView()" 28773 (addNodeClicked)="onAddNodeClicked()" 28774 (addRoomClicked)="onAddRoomClicked()" 28775 (drawFloorOutlineClicked)="onDrawFloorOutlineFromToolbar()" 28776 (menuClicked)="onMenuClicked()" 28777 (createGroundPatchClicked)="onCreateGroundPatchClicked()" 28778 (createFloorClicked)="onCreateFloorClicked()" 28779 (gpsDebugClicked)="onGpsDebugClicked()" 28780 /> 28781 </app-toolbar-container> 28782 28783 <app-toolbar-container position="top-right"> 28784 <app-editor-toolbar 28785 [canUndo]="historyService.canUndo()" 28786 [canRedo]="historyService.canRedo()" 28787 [hasUnsavedChanges]="historyService.hasUnsavedChanges()" 28788 [undoDescription]="historyService.undoDescription()" 28789 [redoDescription]="historyService.redoDescription()" 28790 (editorAction)="onEditorAction($event)" 28791 /> 28792 </app-toolbar-container> 28793 28794 <!-- 3D Model Transform Panel (shown when 3D model is visible and settings clicked) --> 28795 @if (show3DModel && showTransformPanel) { 28796 <app-toolbar-container position="right"> 28797 @if (model3DOptions.length > 1) { 28798 <div class="model-3d-picker"> 28799 <ion-select 28800 [value]="activeModel3DId" 28801 (ionChange)="onModel3DSelected($any($event).detail.value)" 28802 [interface]="'action-sheet'" 28803 [attr.aria-label]="'campusMapV3.modelPanel.selectModel' | translate" 28804 class="model-3d-picker-select"> 28805 <ion-label slot="start">{{ 'campusMapV3.modelPanel.selectModel' | translate }}</ion-label> 28806 @for (option of model3DOptions; track option.id) { 28807 <ion-select-option [value]="option.id">{{ option.name }}</ion-select-option> 28808 } 28809 </ion-select> 28810 </div> 28811 } 28812 <app-model-transform-panel 28813 [transform]="currentTransform" 28814 (transformChanged)="onTransformChanged($event)" 28815 (transformCommit)="onTransformCommit($event)" 28816 (resetRequested)="onResetTransform()" 28817 (closeRequested)="onCloseTransformPanel()" 28818 (centerToViewportRequested)="onCenterToViewport()" 28819 (scaleToViewportRequested)="onScaleToViewport()" 28820 (centerToCampusRequested)="onCenterToCampus()" 28821 (scaleToCampusRequested)="onScaleToCampus()" 28822 /> 28823 </app-toolbar-container> 28824 } 28825
28826 <!-- Node Edit Panel (shown when editing a node) --> 28827 @if (entityEdit.showNodeEditPanel() && entityEdit.editingNode()) { 28828 <app-modal-overlay (backdropClick)="onNodeEditPanelCancel()"> 28829 <app-node-edit-panel 28830 [node]="entityEdit.editingNode()!" 28831 [floorName]="entityEdit.editingNodeFloorName()" 28832 [isNew]="entityEdit.isNodeNew()" 28833 [buildings]="getAllBuildingsInCampus()" 28834 (nodeChanged)="onNodeEditPanelChange($event)" 28835 (applyRequested)="onNodeEditPanelApply()" 28836 (cancelRequested)="onNodeEditPanelCancel()" 28837 (closeRequested)="onNodeEditPanelCancel()" 28838 /> 28839 </app-modal-overlay> 28840 } 28841 28842 <!-- Floor Edit Panel (shown when editing a floor) --> 28843 @if (entityEdit.showFloorEditPanel() && entityEdit.editingFloor()) { 28844 <app-modal-overlay (backdropClick)="onFloorEditPanelCancel()"> 28845 <app-floor-edit-panel 28846 [floor]="entityEdit.editingFloor()!" 28847 [isNew]="entityEdit.isFloorNew()" 28848 (floorChanged)="onFloorEditPanelChange($event)" 28849 (applyRequested)="onFloorEditPanelApply()" 28850 (cancelRequested)="onFloorEditPanelCancel()" 28851 (closeRequested)="onFloorEditPanelCancel()" 28852 (drawOutlineRequested)="onFloorDrawOutline()" 28853 (editOutlineRequested)="onFloorEditOutline()" 28854 /> 28855 </app-modal-overlay> 28856 } 28857 28858 <!-- Building Edit Panel (shown when editing a building) --> 28859 @if (entityEdit.showBuildingEditPanel() && entityEdit.editingBuilding()) { 28860 <app-modal-overlay (backdropClick)="onBuildingEditPanelCancel()"> 28861 <app-building-edit-panel 28862 [building]="entityEdit.editingBuilding()!" 28863 [isNew]="entityEdit.isBuildingNew()" 28864 [assignedNodeCount]="getAssignedNodeCount(entityEdit.editingBuilding()!)" 28865 [eligibleNodeCount]="getEligibleNodeCount(entityEdit.editingBuilding()!)" 28866 (buildingChanged)="onBuildingEditPanelChange($event)" 28867 (pickDefaultDestinationRequested)="onBuildingPickDefaultDestination()" 28868 (clearDefaultDestinationRequested)="onBuildingClearDefaultDestination()" 28869 (assignNodesRequested)="onBuildingAssignNodes()" 28870 (applyRequested)="onBuildingEditPanelApply()" 28871 (cancelRequested)="onBuildingEditPanelCancel()" 28872 (closeRequested)="onBuildingEditPanelCancel()" 28873 /> 28874 </app-modal-overlay> 28875 } 28876 28877 <!-- Room Edit Panel (shown when editing a room) --> 28878 @if (entityEdit.showRoomEditPanel() && entityEdit.editingRoom()) { 28879 <app-modal-overlay (backdropClick)="onRoomEditPanelCancel()"> 28880 <app-room-edit-panel 28881 [room]="entityEdit.editingRoom()!" 28882 [isNew]="entityEdit.isRoomNew()" 28883 [allPersons]="campusPersons" 28884 (roomChanged)="onRoomEditPanelChange($event)" 28885 (applyRequested)="onRoomEditPanelApply()" 28886 (cancelRequested)="onRoomEditPanelCancel()" 28887 (closeRequested)="onRoomEditPanelCancel()" 28888 (personAssigned)="onPersonAssigned($event)" 28889 (personRemoved)="onPersonRemoved($event)" 28890 (personCreated)="onPersonCreated($event)" 28891 /> 28892 </app-modal-overlay> 28893 } 28894 28895 <!-- Ground Patch Edit Panel (shown when editing a ground patch) --> 28896 @if (entityEdit.showGroundPatchEditPanel() && entityEdit.editingGroundPatch()) { 28897 <app-modal-overlay (backdropClick)="onGroundPatchEditPanelCancel()"> 28898 <app-ground-patch-edit-panel 28899 [groundPatch]="entityEdit.editingGroundPatch()!" 28900 [isNew]="entityEdit.isGroundPatchNew()" 28901 (groundPatchChanged)="onGroundPatchEditPanelChange($event)" 28902 (applyRequested)="onGroundPatchEditPanelApply()" 28903 (cancelRequested)="onGroundPatchEditPanelCancel()" 28904 (closeRequested)="onGroundPatchEditPanelCancel()" 28905 (editOutlineRequested)="onGroundPatchEditOutline()" 28906 /> 28907 </app-modal-overlay> 28908 } 28909 28910 <!-- Building Selection Modal (shown when creating floor with multiple buildings) --> 28911 @if (showBuildingSelectionModal) { 28912 <app-building-selection-modal 28913 [buildings]="getBuildingSelectionItems()" 28914 (buildingSelected)="onBuildingSelected($event)" 28915 (cancelRequested)="onBuildingSelectionCancel()" 28916 (closeRequested)="onBuildingSelectionClose()" 28917 /> 28918 } 28919 28920 <!-- GPS Debug Modal --> 28921 @if (showGpsDebugModal) { 28922 <app-modal-overlay (backdropClick)="onGpsDebugClosed()"> 28923 <app-gps-debug-modal 28924 (closeRequested)="onGpsDebugClosed()" 28925 (gpsNodeLocate)="onGpsNodeLocate($event)" 28926 /> 28927 </app-modal-overlay> 28928 } 28929 28930 @if (viewState.availableLevels().length > 0) { 28931 <app-level-selector 28932 [levels]="viewState.availableLevels()" 28933 [currentLevel]="viewState.selectedLevel()" 28934 (levelSelected)="onLevelSelected($event)" 28935 /> 28936 } 28937 28938 <!-- Selection toolbar (shown when entities are selected) --> 28939 <app-toolbar-container position="bottom"> 28940 <app-selection-toolbar 28941 [selectedEntities]="selectionService.selectedEntities()" 28942 [availableActions]="selectionToolbarActions" 28943 [isEditingShape]="isEditingShape" 28944 [isConnectMode]="isConnectMode" 28945 (actionTriggered)="onSelectionAction($event)" 28946 /> 28947 </app-toolbar-container> 28948</ion-content> 28949`, styles: ["/* src/app/pages/campus-map-v3/campus-map-v3.page.scss */\n:host {\n display: block;\n width: 100%;\n height: 100%;\n}\n.canvas-container {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n overflow: hidden;\n touch-action: none;\n transition: margin-left 0.25s ease-out;\n}\n:host ::ng-deep .room-wall-dimension-label {\n pointer-events: none;\n transform: translate(-50%, -30px);\n padding: 4px 7px;\n border: 1px solid rgba(249, 115, 22, 0.7);\n border-radius: 4px;\n background: rgba(15, 23, 42, 0.92);\n box-shadow: 0 4px 12px rgba(15, 23, 42, 0.25);\n color: #fff;\n font: 700 12px/1 system-ui, sans-serif;\n white-space: nowrap;\n}\n.default-destination-picker-banner,\n.node-assignment-banner {\n position: absolute;\n top: 16px;\n right: 16px;\n z-index: 90;\n display: flex;\n align-items: center;\n gap: 14px;\n max-width: min(560px, 100% - 32px);\n padding: 10px 12px;\n background: rgba(255, 255, 255, 0.96);\n border: 1px solid rgba(16, 185, 129, 0.45);\n box-shadow: 0 10px 28px rgba(15, 23, 42, 0.18);\n color: #10201a;\n transition: left 0.25s ease-out;\n}\n.node-assignment-banner {\n border-color: rgba(59, 130, 246, 0.45);\n color: #0f2035;\n}\n.picker-copy {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n.picker-title,\n.picker-subtitle {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.picker-title {\n font-size: 0.92rem;\n font-weight: 700;\n}\n.picker-subtitle {\n font-size: 0.8rem;\n color: #315348;\n}\n.default-destination-picker-banner button,\n.node-assignment-banner button {\n border: 0;\n background: transparent;\n color: var(--ion-color-primary);\n font-weight: 600;\n cursor: pointer;\n padding: 6px 4px;\n}\n.node-assignment-banner button:disabled {\n opacity: 0.45;\n cursor: default;\n}\n.assignment-actions {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n}\n@media (max-width: 640px) {\n .default-destination-picker-banner,\n .no
28949de-assignment-banner {\n right: 12px;\n left: 12px !important;\n max-width: none;\n flex-direction: column;\n align-items: stretch;\n gap: 8px;\n }\n .assignment-actions {\n justify-content: space-between;\n gap: 6px;\n }\n}\nion-content {\n --padding-top: 0;\n --padding-bottom: 0;\n --padding-start: 0;\n --padding-end: 0;\n}\n::ng-deep .tool-palette-container.tool-palette-top-left {\n transition: left 0.25s ease-out;\n}\n.model-loading-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.3);\n z-index: 100;\n}\n.model-loading-card {\n background: var(--ion-background-color, white);\n padding: 24px 32px;\n border-radius: 12px;\n text-align: center;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);\n min-width: 240px;\n}\n.model-loading-title {\n display: block;\n font-size: 1rem;\n font-weight: 500;\n color: var(--ion-text-color);\n margin-bottom: 16px;\n}\n.progress-bar {\n width: 100%;\n height: 8px;\n background: var(--ion-color-light, #e0e0e0);\n border-radius: 4px;\n overflow: hidden;\n}\n.progress-fill {\n height: 100%;\n background: var(--ion-color-primary);\n border-radius: 4px;\n transition: width 0.2s ease;\n}\n.progress-text {\n display: block;\n font-size: 0.875rem;\n color: var(--ion-color-medium);\n margin-top: 8px;\n}\n.model-3d-picker {\n background: var(--tool-palette-bg, rgba(255, 255, 255, 0.95));\n border-radius: 12px;\n box-shadow: var(--tool-palette-shadow, 0 4px 12px rgba(0, 0, 0, 0.15));\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n border: 1px solid var(--tool-palette-border-light, rgba(0, 0, 0, 0.08));\n min-width: 380px;\n max-width: 380px;\n overflow: hidden;\n padding: 4px 8px;\n}\n.model-3d-picker-select {\n --padding-start: 8px;\n width: 100%;\n font-size: 14px;\n color: var(--ion-text-color);\n}\n"] }] 28950 }], () => [{ type: ActivatedRoute }, { type: DatabaseService }, { type: ModelCacheService }, { type: NgZone }, { type: ChangeDetectorRef }], { canvasContainer: [{ 28951 type: ViewChild, 28952 args: ["canvasContainer", { static: true }] 28953 }], onResize: [{ 28954 type: HostListener, 28955 args: ["window:resize"] 28956 }], onBeforeUnload: [{ 28957 type: HostListener, 28958 args: ["window:beforeunload", ["$event"]] 28959 }], onKeyDown: [{ 28960 type: HostListener, 28961 args: ["window:keydown", ["$event"]] 28962 }] }); 28963})(); 28964(() => { 28965 (typeof ngDevMode === "undefined" || ngDevMode) && \u0275setClassDebugInfo(CampusMapV3Page, { className: "CampusMapV3Page", filePath: "src/app/pages/campus-map-v3/campus-map-v3.page.ts", lineNumber: 119 }); 28966})(); 28967export { 28968 CampusMapV3Page 28969};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.