PageSourceSearch

https://stridenavigation.com/chunk-DVJAMVHI.js

js stridenavigation.com collected 2026-09-24 14:53:39 UTC 1,319,144 bytes, 28,969 lines download raw bytes

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.