tango-app-ui-store-builder 1.2.22 → 1.2.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import { CommonModule } from '@angular/common';
5
5
  import * as FileSaver from 'file-saver';
6
6
  import { firstValueFrom } from 'rxjs';
7
7
  import * as i1 from '@ng-bootstrap/ng-bootstrap';
8
- import { S as StoreBuilderService } from './tango-app-ui-store-builder-tango-app-ui-store-builder-D1UC-7Qi.mjs';
8
+ import { S as StoreBuilderService } from './tango-app-ui-store-builder-tango-app-ui-store-builder-CXw6OsFW.mjs';
9
9
  import * as i4 from 'tango-app-ui-shared';
10
10
  import '@angular/router';
11
11
  import 'tango-app-ui-global';
@@ -233,4 +233,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
233
233
  }] } });
234
234
 
235
235
  export { DxfSourceViewerComponent };
236
- //# sourceMappingURL=tango-app-ui-store-builder-dxf-source-viewer.component-KfK9D7DB.mjs.map
236
+ //# sourceMappingURL=tango-app-ui-store-builder-dxf-source-viewer.component-BplzhK87.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tango-app-ui-store-builder-dxf-source-viewer.component-KfK9D7DB.mjs","sources":["../../../projects/tango-store-builder/src/lib/components/dxf-source-viewer/dxf-source-viewer.component.ts","../../../projects/tango-store-builder/src/lib/components/dxf-source-viewer/dxf-source-viewer.component.html"],"sourcesContent":["import {\n AfterViewInit,\n Component,\n ElementRef,\n Input,\n OnDestroy,\n OnInit,\n ViewChild,\n} from \"@angular/core\";\nimport { CommonModule } from \"@angular/common\";\nimport { NgbActiveModal } from \"@ng-bootstrap/ng-bootstrap\";\nimport * as FileSaver from \"file-saver\";\nimport { firstValueFrom } from \"rxjs\";\nimport { ToastService } from \"tango-app-ui-shared\";\nimport { StoreBuilderService } from \"../../services/store-builder.service\";\n\n/**\n * Read-only viewer for a floor's SOURCE CAD/DXF files.\n *\n * Renders the floor's `cadFiles` (stored on the storelayout doc) as a timeline —\n * oldest first, latest last/highlighted — and shows the selected drawing in a 2D\n * `dxf-viewer` (WebGL, built-in pan/zoom + layer toggle). Each entry also has a\n * download button.\n *\n * `dxf-viewer` (and its heavy `three` peer) are dynamically imported here, and this\n * component itself is opened via a dynamic import (see CadSourceService), so the\n * whole thing lands in a lazy chunk and stays out of the main bundle.\n *\n * NOTE ON TEXT: dxf-viewer needs a font file to render TEXT/MTEXT entities. Drop a\n * .ttf into assets and list it in FONT_URLS to enable labels; with no font, geometry\n * renders but text is blank (no crash).\n */\n\n// Add a hosted TTF here (e.g. \"assets/fonts/Roboto-Regular.ttf\") to render DXF text.\nconst FONT_URLS: string[] = [];\n\ninterface CadFileEntry {\n key: string;\n fileName?: string;\n uploadedAt?: string | Date;\n inferred?: boolean;\n // resolved at runtime from listCadFiles:\n url?: string;\n size?: number;\n available?: boolean;\n}\n\n@Component({\n selector: \"lib-dxf-source-viewer\",\n standalone: true,\n imports: [CommonModule],\n templateUrl: \"./dxf-source-viewer.component.html\",\n styleUrl: \"./dxf-source-viewer.component.scss\",\n})\nexport class DxfSourceViewerComponent\n implements OnInit, AfterViewInit, OnDestroy\n{\n @Input() storeName = \"\";\n /** The floor's stored cadFiles array (oldest -> newest). */\n @Input() cadFiles: CadFileEntry[] = [];\n\n @ViewChild(\"viewerHost\") viewerHost!: ElementRef<HTMLDivElement>;\n\n entries: CadFileEntry[] = [];\n selected: CadFileEntry | null = null;\n\n isResolving = false;\n isRendering = false;\n loadError = \"\";\n\n layers: { name: string; displayName: string; visible: boolean }[] = [];\n\n private viewer: any;\n private viewReady = false;\n\n constructor(\n public activeModal: NgbActiveModal,\n private storeBuilderService: StoreBuilderService,\n private toastService: ToastService,\n ) {}\n\n async ngOnInit(): Promise<void> {\n await this.resolveUrls();\n }\n\n ngAfterViewInit(): void {\n this.viewReady = true;\n // If URLs resolved before the view was ready, render the default selection now.\n if (this.selected?.available) this.render(this.selected);\n }\n\n ngOnDestroy(): void {\n this.destroyViewer();\n }\n\n /** Match each stored cadFiles entry to a presigned URL from listCadFiles(storeName). */\n private async resolveUrls(): Promise<void> {\n this.isResolving = true;\n this.loadError = \"\";\n try {\n const res: any = await firstValueFrom(\n this.storeBuilderService.getCadFiles(this.storeName),\n );\n const s3Files: any[] = res?.data?.files ?? [];\n const byKey = new Map<string, any>(s3Files.map((f) => [f.key, f]));\n\n // Preserve stored order (oldest -> newest); render newest at top of timeline.\n this.entries = (this.cadFiles ?? []).map((c) => {\n const match = byKey.get(c.key);\n return {\n ...c,\n fileName: c.fileName ?? c.key?.split(\"/\").pop(),\n url: match?.url,\n size: match?.size,\n available: !!match?.url,\n };\n });\n\n // Default selection: latest available entry (last in stored order).\n const latestAvailable = [...this.entries]\n .reverse()\n .find((e) => e.available);\n if (latestAvailable) {\n this.select(latestAvailable);\n } else if (this.entries.length) {\n this.loadError =\n \"The source file(s) could not be found in storage. They may have been removed.\";\n }\n } catch (e: any) {\n this.loadError =\n e?.error?.error?.errMsg ??\n e?.error?.message ??\n \"Failed to load source CAD files.\";\n } finally {\n this.isResolving = false;\n }\n }\n\n /** Timeline shows newest first for scannability. */\n get timeline(): CadFileEntry[] {\n return [...this.entries].reverse();\n }\n\n isLatest(entry: CadFileEntry): boolean {\n return this.entries.length > 0 && entry === this.entries[this.entries.length - 1];\n }\n\n select(entry: CadFileEntry): void {\n if (!entry?.available || this.selected === entry) return;\n this.selected = entry;\n if (this.viewReady) this.render(entry);\n }\n\n private async render(entry: CadFileEntry): Promise<void> {\n if (!entry?.url) return;\n this.isRendering = true;\n this.loadError = \"\";\n this.layers = [];\n try {\n const { DxfViewer } = await import(\"dxf-viewer\");\n // three ships no type declarations; it's a runtime dep of dxf-viewer.\n // We only need Color here, so import it untyped rather than pulling @types/three.\n // @ts-ignore\n const THREE = await import(\"three\");\n\n if (!this.viewer) {\n this.viewer = new DxfViewer(this.viewerHost.nativeElement, {\n autoResize: true,\n // Correct entity colors for contrast against the (white) background so\n // light-coloured layers/text stay visible.\n colorCorrection: true,\n clearColor: new THREE.Color(0xffffff),\n clearAlpha: 1,\n });\n } else {\n this.viewer.Clear();\n }\n\n await this.viewer.Load({ url: entry.url, fonts: FONT_URLS });\n\n // Build the layer-toggle list.\n const layers = this.viewer.GetLayers?.() ?? [];\n this.layers = [...layers].map((l: any) => ({\n name: l.name,\n displayName: l.displayName ?? l.name,\n visible: true,\n }));\n } catch (e) {\n console.error(\"@@ ~ dxf render [ERR]:\", e);\n this.loadError = \"Failed to render this DXF drawing.\";\n } finally {\n this.isRendering = false;\n }\n }\n\n toggleLayer(layer: { name: string; visible: boolean }): void {\n layer.visible = !layer.visible;\n this.viewer?.ShowLayer?.(layer.name, layer.visible);\n }\n\n get allLayersVisible(): boolean {\n return this.layers.length > 0 && this.layers.every((l) => l.visible);\n }\n\n toggleAllLayers(event: Event): void {\n const checked = (event.target as HTMLInputElement).checked;\n for (const layer of this.layers) {\n layer.visible = checked;\n this.viewer?.ShowLayer?.(layer.name, checked);\n }\n }\n\n async download(entry: CadFileEntry, event: MouseEvent): Promise<void> {\n event.stopPropagation();\n if (!entry?.url) {\n this.toastService.getErrorToast(\"Download URL is not available.\");\n return;\n }\n try {\n const response = await fetch(entry.url);\n if (!response.ok) throw new Error(`HTTP ${response.status}`);\n const blob = await response.blob();\n FileSaver.saveAs(blob, entry.fileName ?? \"drawing.dxf\");\n } catch {\n this.toastService.getErrorToast(\n `Failed to download ${entry.fileName ?? \"file\"}.`,\n );\n }\n }\n\n private destroyViewer(): void {\n try {\n this.viewer?.Destroy?.();\n } catch {}\n this.viewer = null;\n }\n\n close(): void {\n this.activeModal.dismiss();\n }\n}\n","<div class=\"dxf-modal\">\n <div class=\"dxf-modal__header\">\n <div>\n <h3 class=\"dxf-modal__title\">Source CAD drawing</h3>\n <p class=\"dxf-modal__subtitle\">{{ storeName }}</p>\n </div>\n <button type=\"button\" class=\"dxf-icon-btn\" aria-label=\"Close\" (click)=\"close()\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M12 4L4 12M4 4l8 8\" stroke=\"#475467\" stroke-width=\"1.6\" stroke-linecap=\"round\" />\n </svg>\n </button>\n </div>\n\n <div class=\"dxf-modal__body\">\n <!-- Timeline of source files -->\n <aside class=\"dxf-timeline\">\n <div class=\"dxf-timeline__head\">File history</div>\n\n <div *ngIf=\"isResolving\" class=\"dxf-muted\">Loading files…</div>\n\n <div *ngIf=\"!isResolving && !entries.length\" class=\"dxf-muted\">\n No source CAD files for this floor.\n </div>\n\n <ul class=\"dxf-timeline__list\" *ngIf=\"!isResolving && entries.length\">\n <li\n *ngFor=\"let entry of timeline\"\n class=\"dxf-timeline__item\"\n [class.is-selected]=\"entry === selected\"\n [class.is-disabled]=\"!entry.available\"\n (click)=\"select(entry)\"\n >\n <div class=\"dxf-timeline__dot\"></div>\n <div class=\"dxf-timeline__content\">\n <div class=\"dxf-timeline__name\" [title]=\"entry.fileName\">\n {{ entry.fileName }}\n <span *ngIf=\"isLatest(entry)\" class=\"dxf-badge dxf-badge--latest\">Latest</span>\n <span *ngIf=\"entry.inferred\" class=\"dxf-badge dxf-badge--inferred\" title=\"Matched from storage, not recorded at generation time\">Inferred</span>\n </div>\n <div class=\"dxf-timeline__meta\">\n <span *ngIf=\"entry.uploadedAt\">{{ entry.uploadedAt | date: \"medium\" }}</span>\n <span *ngIf=\"!entry.available\" class=\"dxf-unavailable\">Unavailable</span>\n </div>\n </div>\n <button\n type=\"button\"\n class=\"dxf-icon-btn dxf-icon-btn--sm\"\n [disabled]=\"!entry.available\"\n aria-label=\"Download\"\n (click)=\"download(entry, $event)\"\n >\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M8 2v8m0 0L5 7m3 3l3-3M3 13h10\" stroke=\"#475467\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n </button>\n </li>\n </ul>\n </aside>\n\n <!-- Viewer -->\n <section class=\"dxf-viewer\">\n <div class=\"dxf-viewer__toolbar\" *ngIf=\"layers.length\">\n <div class=\"dxf-layers\">\n <span class=\"dxf-layers__label\">Layers</span>\n <label class=\"dxf-layers__item dxf-layers__item--all\">\n <input type=\"checkbox\" [checked]=\"allLayersVisible\" (change)=\"toggleAllLayers($event)\" />\n All\n </label>\n <label class=\"dxf-layers__item\" *ngFor=\"let layer of layers\">\n <input type=\"checkbox\" [checked]=\"layer.visible\" (change)=\"toggleLayer(layer)\" />\n {{ layer.displayName }}\n </label>\n </div>\n </div>\n\n <div class=\"dxf-viewer__stage\">\n <div #viewerHost class=\"dxf-viewer__host\"></div>\n\n <div *ngIf=\"isRendering\" class=\"dxf-overlay\">\n <svg class=\"dxf-spinner\" viewBox=\"0 0 50 50\">\n <circle class=\"path\" cx=\"25\" cy=\"25\" r=\"20\" fill=\"none\" stroke-width=\"4\"></circle>\n </svg>\n <span>Rendering drawing…</span>\n </div>\n\n <div *ngIf=\"loadError && !isRendering\" class=\"dxf-overlay dxf-overlay--error\">\n {{ loadError }}\n </div>\n\n <div *ngIf=\"!selected && !isResolving && !loadError\" class=\"dxf-overlay dxf-muted\">\n Select a file to view.\n </div>\n </div>\n\n <p class=\"dxf-viewer__hint\">Scroll to zoom · drag to pan</p>\n </section>\n </div>\n</div>\n"],"names":["i2.StoreBuilderService","i3","i4"],"mappings":"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"}
1
+ {"version":3,"file":"tango-app-ui-store-builder-dxf-source-viewer.component-BplzhK87.mjs","sources":["../../../projects/tango-store-builder/src/lib/components/dxf-source-viewer/dxf-source-viewer.component.ts","../../../projects/tango-store-builder/src/lib/components/dxf-source-viewer/dxf-source-viewer.component.html"],"sourcesContent":["import {\n AfterViewInit,\n Component,\n ElementRef,\n Input,\n OnDestroy,\n OnInit,\n ViewChild,\n} from \"@angular/core\";\nimport { CommonModule } from \"@angular/common\";\nimport { NgbActiveModal } from \"@ng-bootstrap/ng-bootstrap\";\nimport * as FileSaver from \"file-saver\";\nimport { firstValueFrom } from \"rxjs\";\nimport { ToastService } from \"tango-app-ui-shared\";\nimport { StoreBuilderService } from \"../../services/store-builder.service\";\n\n/**\n * Read-only viewer for a floor's SOURCE CAD/DXF files.\n *\n * Renders the floor's `cadFiles` (stored on the storelayout doc) as a timeline —\n * oldest first, latest last/highlighted — and shows the selected drawing in a 2D\n * `dxf-viewer` (WebGL, built-in pan/zoom + layer toggle). Each entry also has a\n * download button.\n *\n * `dxf-viewer` (and its heavy `three` peer) are dynamically imported here, and this\n * component itself is opened via a dynamic import (see CadSourceService), so the\n * whole thing lands in a lazy chunk and stays out of the main bundle.\n *\n * NOTE ON TEXT: dxf-viewer needs a font file to render TEXT/MTEXT entities. Drop a\n * .ttf into assets and list it in FONT_URLS to enable labels; with no font, geometry\n * renders but text is blank (no crash).\n */\n\n// Add a hosted TTF here (e.g. \"assets/fonts/Roboto-Regular.ttf\") to render DXF text.\nconst FONT_URLS: string[] = [];\n\ninterface CadFileEntry {\n key: string;\n fileName?: string;\n uploadedAt?: string | Date;\n inferred?: boolean;\n // resolved at runtime from listCadFiles:\n url?: string;\n size?: number;\n available?: boolean;\n}\n\n@Component({\n selector: \"lib-dxf-source-viewer\",\n standalone: true,\n imports: [CommonModule],\n templateUrl: \"./dxf-source-viewer.component.html\",\n styleUrl: \"./dxf-source-viewer.component.scss\",\n})\nexport class DxfSourceViewerComponent\n implements OnInit, AfterViewInit, OnDestroy\n{\n @Input() storeName = \"\";\n /** The floor's stored cadFiles array (oldest -> newest). */\n @Input() cadFiles: CadFileEntry[] = [];\n\n @ViewChild(\"viewerHost\") viewerHost!: ElementRef<HTMLDivElement>;\n\n entries: CadFileEntry[] = [];\n selected: CadFileEntry | null = null;\n\n isResolving = false;\n isRendering = false;\n loadError = \"\";\n\n layers: { name: string; displayName: string; visible: boolean }[] = [];\n\n private viewer: any;\n private viewReady = false;\n\n constructor(\n public activeModal: NgbActiveModal,\n private storeBuilderService: StoreBuilderService,\n private toastService: ToastService,\n ) {}\n\n async ngOnInit(): Promise<void> {\n await this.resolveUrls();\n }\n\n ngAfterViewInit(): void {\n this.viewReady = true;\n // If URLs resolved before the view was ready, render the default selection now.\n if (this.selected?.available) this.render(this.selected);\n }\n\n ngOnDestroy(): void {\n this.destroyViewer();\n }\n\n /** Match each stored cadFiles entry to a presigned URL from listCadFiles(storeName). */\n private async resolveUrls(): Promise<void> {\n this.isResolving = true;\n this.loadError = \"\";\n try {\n const res: any = await firstValueFrom(\n this.storeBuilderService.getCadFiles(this.storeName),\n );\n const s3Files: any[] = res?.data?.files ?? [];\n const byKey = new Map<string, any>(s3Files.map((f) => [f.key, f]));\n\n // Preserve stored order (oldest -> newest); render newest at top of timeline.\n this.entries = (this.cadFiles ?? []).map((c) => {\n const match = byKey.get(c.key);\n return {\n ...c,\n fileName: c.fileName ?? c.key?.split(\"/\").pop(),\n url: match?.url,\n size: match?.size,\n available: !!match?.url,\n };\n });\n\n // Default selection: latest available entry (last in stored order).\n const latestAvailable = [...this.entries]\n .reverse()\n .find((e) => e.available);\n if (latestAvailable) {\n this.select(latestAvailable);\n } else if (this.entries.length) {\n this.loadError =\n \"The source file(s) could not be found in storage. They may have been removed.\";\n }\n } catch (e: any) {\n this.loadError =\n e?.error?.error?.errMsg ??\n e?.error?.message ??\n \"Failed to load source CAD files.\";\n } finally {\n this.isResolving = false;\n }\n }\n\n /** Timeline shows newest first for scannability. */\n get timeline(): CadFileEntry[] {\n return [...this.entries].reverse();\n }\n\n isLatest(entry: CadFileEntry): boolean {\n return this.entries.length > 0 && entry === this.entries[this.entries.length - 1];\n }\n\n select(entry: CadFileEntry): void {\n if (!entry?.available || this.selected === entry) return;\n this.selected = entry;\n if (this.viewReady) this.render(entry);\n }\n\n private async render(entry: CadFileEntry): Promise<void> {\n if (!entry?.url) return;\n this.isRendering = true;\n this.loadError = \"\";\n this.layers = [];\n try {\n const { DxfViewer } = await import(\"dxf-viewer\");\n // three ships no type declarations; it's a runtime dep of dxf-viewer.\n // We only need Color here, so import it untyped rather than pulling @types/three.\n // @ts-ignore\n const THREE = await import(\"three\");\n\n if (!this.viewer) {\n this.viewer = new DxfViewer(this.viewerHost.nativeElement, {\n autoResize: true,\n // Correct entity colors for contrast against the (white) background so\n // light-coloured layers/text stay visible.\n colorCorrection: true,\n clearColor: new THREE.Color(0xffffff),\n clearAlpha: 1,\n });\n } else {\n this.viewer.Clear();\n }\n\n await this.viewer.Load({ url: entry.url, fonts: FONT_URLS });\n\n // Build the layer-toggle list.\n const layers = this.viewer.GetLayers?.() ?? [];\n this.layers = [...layers].map((l: any) => ({\n name: l.name,\n displayName: l.displayName ?? l.name,\n visible: true,\n }));\n } catch (e) {\n console.error(\"@@ ~ dxf render [ERR]:\", e);\n this.loadError = \"Failed to render this DXF drawing.\";\n } finally {\n this.isRendering = false;\n }\n }\n\n toggleLayer(layer: { name: string; visible: boolean }): void {\n layer.visible = !layer.visible;\n this.viewer?.ShowLayer?.(layer.name, layer.visible);\n }\n\n get allLayersVisible(): boolean {\n return this.layers.length > 0 && this.layers.every((l) => l.visible);\n }\n\n toggleAllLayers(event: Event): void {\n const checked = (event.target as HTMLInputElement).checked;\n for (const layer of this.layers) {\n layer.visible = checked;\n this.viewer?.ShowLayer?.(layer.name, checked);\n }\n }\n\n async download(entry: CadFileEntry, event: MouseEvent): Promise<void> {\n event.stopPropagation();\n if (!entry?.url) {\n this.toastService.getErrorToast(\"Download URL is not available.\");\n return;\n }\n try {\n const response = await fetch(entry.url);\n if (!response.ok) throw new Error(`HTTP ${response.status}`);\n const blob = await response.blob();\n FileSaver.saveAs(blob, entry.fileName ?? \"drawing.dxf\");\n } catch {\n this.toastService.getErrorToast(\n `Failed to download ${entry.fileName ?? \"file\"}.`,\n );\n }\n }\n\n private destroyViewer(): void {\n try {\n this.viewer?.Destroy?.();\n } catch {}\n this.viewer = null;\n }\n\n close(): void {\n this.activeModal.dismiss();\n }\n}\n","<div class=\"dxf-modal\">\n <div class=\"dxf-modal__header\">\n <div>\n <h3 class=\"dxf-modal__title\">Source CAD drawing</h3>\n <p class=\"dxf-modal__subtitle\">{{ storeName }}</p>\n </div>\n <button type=\"button\" class=\"dxf-icon-btn\" aria-label=\"Close\" (click)=\"close()\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M12 4L4 12M4 4l8 8\" stroke=\"#475467\" stroke-width=\"1.6\" stroke-linecap=\"round\" />\n </svg>\n </button>\n </div>\n\n <div class=\"dxf-modal__body\">\n <!-- Timeline of source files -->\n <aside class=\"dxf-timeline\">\n <div class=\"dxf-timeline__head\">File history</div>\n\n <div *ngIf=\"isResolving\" class=\"dxf-muted\">Loading files…</div>\n\n <div *ngIf=\"!isResolving && !entries.length\" class=\"dxf-muted\">\n No source CAD files for this floor.\n </div>\n\n <ul class=\"dxf-timeline__list\" *ngIf=\"!isResolving && entries.length\">\n <li\n *ngFor=\"let entry of timeline\"\n class=\"dxf-timeline__item\"\n [class.is-selected]=\"entry === selected\"\n [class.is-disabled]=\"!entry.available\"\n (click)=\"select(entry)\"\n >\n <div class=\"dxf-timeline__dot\"></div>\n <div class=\"dxf-timeline__content\">\n <div class=\"dxf-timeline__name\" [title]=\"entry.fileName\">\n {{ entry.fileName }}\n <span *ngIf=\"isLatest(entry)\" class=\"dxf-badge dxf-badge--latest\">Latest</span>\n <span *ngIf=\"entry.inferred\" class=\"dxf-badge dxf-badge--inferred\" title=\"Matched from storage, not recorded at generation time\">Inferred</span>\n </div>\n <div class=\"dxf-timeline__meta\">\n <span *ngIf=\"entry.uploadedAt\">{{ entry.uploadedAt | date: \"medium\" }}</span>\n <span *ngIf=\"!entry.available\" class=\"dxf-unavailable\">Unavailable</span>\n </div>\n </div>\n <button\n type=\"button\"\n class=\"dxf-icon-btn dxf-icon-btn--sm\"\n [disabled]=\"!entry.available\"\n aria-label=\"Download\"\n (click)=\"download(entry, $event)\"\n >\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M8 2v8m0 0L5 7m3 3l3-3M3 13h10\" stroke=\"#475467\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n </button>\n </li>\n </ul>\n </aside>\n\n <!-- Viewer -->\n <section class=\"dxf-viewer\">\n <div class=\"dxf-viewer__toolbar\" *ngIf=\"layers.length\">\n <div class=\"dxf-layers\">\n <span class=\"dxf-layers__label\">Layers</span>\n <label class=\"dxf-layers__item dxf-layers__item--all\">\n <input type=\"checkbox\" [checked]=\"allLayersVisible\" (change)=\"toggleAllLayers($event)\" />\n All\n </label>\n <label class=\"dxf-layers__item\" *ngFor=\"let layer of layers\">\n <input type=\"checkbox\" [checked]=\"layer.visible\" (change)=\"toggleLayer(layer)\" />\n {{ layer.displayName }}\n </label>\n </div>\n </div>\n\n <div class=\"dxf-viewer__stage\">\n <div #viewerHost class=\"dxf-viewer__host\"></div>\n\n <div *ngIf=\"isRendering\" class=\"dxf-overlay\">\n <svg class=\"dxf-spinner\" viewBox=\"0 0 50 50\">\n <circle class=\"path\" cx=\"25\" cy=\"25\" r=\"20\" fill=\"none\" stroke-width=\"4\"></circle>\n </svg>\n <span>Rendering drawing…</span>\n </div>\n\n <div *ngIf=\"loadError && !isRendering\" class=\"dxf-overlay dxf-overlay--error\">\n {{ loadError }}\n </div>\n\n <div *ngIf=\"!selected && !isResolving && !loadError\" class=\"dxf-overlay dxf-muted\">\n Select a file to view.\n </div>\n </div>\n\n <p class=\"dxf-viewer__hint\">Scroll to zoom · drag to pan</p>\n </section>\n </div>\n</div>\n"],"names":["i2.StoreBuilderService","i3","i4"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgBA;;;;;;;;;;;;;;;AAeG;AAEH;AACA,MAAM,SAAS,GAAa,EAAE;MAoBjB,wBAAwB,CAAA;AAsB1B,IAAA,WAAA;AACC,IAAA,mBAAA;AACA,IAAA,YAAA;IArBD,SAAS,GAAG,EAAE;;IAEd,QAAQ,GAAmB,EAAE;AAEb,IAAA,UAAU;IAEnC,OAAO,GAAmB,EAAE;IAC5B,QAAQ,GAAwB,IAAI;IAEpC,WAAW,GAAG,KAAK;IACnB,WAAW,GAAG,KAAK;IACnB,SAAS,GAAG,EAAE;IAEd,MAAM,GAA8D,EAAE;AAE9D,IAAA,MAAM;IACN,SAAS,GAAG,KAAK;AAEzB,IAAA,WAAA,CACS,WAA2B,EAC1B,mBAAwC,EACxC,YAA0B,EAAA;QAF3B,IAAA,CAAA,WAAW,GAAX,WAAW;QACV,IAAA,CAAA,mBAAmB,GAAnB,mBAAmB;QACnB,IAAA,CAAA,YAAY,GAAZ,YAAY;IACnB;AAEH,IAAA,MAAM,QAAQ,GAAA;AACZ,QAAA,MAAM,IAAI,CAAC,WAAW,EAAE;IAC1B;IAEA,eAAe,GAAA;AACb,QAAA,IAAI,CAAC,SAAS,GAAG,IAAI;;AAErB,QAAA,IAAI,IAAI,CAAC,QAAQ,EAAE,SAAS;AAAE,YAAA,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;IAC1D;IAEA,WAAW,GAAA;QACT,IAAI,CAAC,aAAa,EAAE;IACtB;;AAGQ,IAAA,MAAM,WAAW,GAAA;AACvB,QAAA,IAAI,CAAC,WAAW,GAAG,IAAI;AACvB,QAAA,IAAI,CAAC,SAAS,GAAG,EAAE;QACnB,IAAI;AACF,YAAA,MAAM,GAAG,GAAQ,MAAM,cAAc,CACnC,IAAI,CAAC,mBAAmB,CAAC,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,CACrD;YACD,MAAM,OAAO,GAAU,GAAG,EAAE,IAAI,EAAE,KAAK,IAAI,EAAE;YAC7C,MAAM,KAAK,GAAG,IAAI,GAAG,CAAc,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;;AAGlE,YAAA,IAAI,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,KAAI;gBAC7C,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;gBAC9B,OAAO;AACL,oBAAA,GAAG,CAAC;AACJ,oBAAA,QAAQ,EAAE,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE;oBAC/C,GAAG,EAAE,KAAK,EAAE,GAAG;oBACf,IAAI,EAAE,KAAK,EAAE,IAAI;AACjB,oBAAA,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE,GAAG;iBACxB;AACH,YAAA,CAAC,CAAC;;AAGF,YAAA,MAAM,eAAe,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO;AACrC,iBAAA,OAAO;iBACP,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC;AAC3B,YAAA,IAAI,eAAe,EAAE;AACnB,gBAAA,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC;AAC7B,YAAA;AAAM,iBAAA,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE;AAC9B,gBAAA,IAAI,CAAC,SAAS;AACZ,oBAAA,+EAA+E;AAClF,YAAA;AACF,QAAA;AAAC,QAAA,OAAO,CAAM,EAAE;AACf,YAAA,IAAI,CAAC,SAAS;AACZ,gBAAA,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM;oBACvB,CAAC,EAAE,KAAK,EAAE,OAAO;AACjB,oBAAA,kCAAkC;AACrC,QAAA;AAAS,gBAAA;AACR,YAAA,IAAI,CAAC,WAAW,GAAG,KAAK;AACzB,QAAA;IACH;;AAGA,IAAA,IAAI,QAAQ,GAAA;QACV,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE;IACpC;AAEA,IAAA,QAAQ,CAAC,KAAmB,EAAA;QAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;IACnF;AAEA,IAAA,MAAM,CAAC,KAAmB,EAAA;QACxB,IAAI,CAAC,KAAK,EAAE,SAAS,IAAI,IAAI,CAAC,QAAQ,KAAK,KAAK;YAAE;AAClD,QAAA,IAAI,CAAC,QAAQ,GAAG,KAAK;QACrB,IAAI,IAAI,CAAC,SAAS;AAAE,YAAA,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;IACxC;IAEQ,MAAM,MAAM,CAAC,KAAmB,EAAA;QACtC,IAAI,CAAC,KAAK,EAAE,GAAG;YAAE;AACjB,QAAA,IAAI,CAAC,WAAW,GAAG,IAAI;AACvB,QAAA,IAAI,CAAC,SAAS,GAAG,EAAE;AACnB,QAAA,IAAI,CAAC,MAAM,GAAG,EAAE;QAChB,IAAI;YACF,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,OAAO,YAAY,CAAC;;;;AAIhD,YAAA,MAAM,KAAK,GAAG,MAAM,OAAO,OAAO,CAAC;AAEnC,YAAA,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;gBAChB,IAAI,CAAC,MAAM,GAAG,IAAI,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,aAAa,EAAE;AACzD,oBAAA,UAAU,EAAE,IAAI;;;AAGhB,oBAAA,eAAe,EAAE,IAAI;AACrB,oBAAA,UAAU,EAAE,IAAI,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC;AACrC,oBAAA,UAAU,EAAE,CAAC;AACd,iBAAA,CAAC;AACH,YAAA;AAAM,iBAAA;AACL,gBAAA,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE;AACpB,YAAA;AAED,YAAA,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;;YAG5D,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,IAAI,EAAE;AAC9C,YAAA,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAM,MAAM;gBACzC,IAAI,EAAE,CAAC,CAAC,IAAI;AACZ,gBAAA,WAAW,EAAE,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,IAAI;AACpC,gBAAA,OAAO,EAAE,IAAI;AACd,aAAA,CAAC,CAAC;AACJ,QAAA;AAAC,QAAA,OAAO,CAAC,EAAE;AACV,YAAA,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,CAAC,CAAC;AAC1C,YAAA,IAAI,CAAC,SAAS,GAAG,oCAAoC;AACtD,QAAA;AAAS,gBAAA;AACR,YAAA,IAAI,CAAC,WAAW,GAAG,KAAK;AACzB,QAAA;IACH;AAEA,IAAA,WAAW,CAAC,KAAyC,EAAA;AACnD,QAAA,KAAK,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,OAAO;AAC9B,QAAA,IAAI,CAAC,MAAM,EAAE,SAAS,GAAG,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC;IACrD;AAEA,IAAA,IAAI,gBAAgB,GAAA;QAClB,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC;IACtE;AAEA,IAAA,eAAe,CAAC,KAAY,EAAA;AAC1B,QAAA,MAAM,OAAO,GAAI,KAAK,CAAC,MAA2B,CAAC,OAAO;AAC1D,QAAA,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE;AAC/B,YAAA,KAAK,CAAC,OAAO,GAAG,OAAO;AACvB,YAAA,IAAI,CAAC,MAAM,EAAE,SAAS,GAAG,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC;AAC9C,QAAA;IACH;AAEA,IAAA,MAAM,QAAQ,CAAC,KAAmB,EAAE,KAAiB,EAAA;QACnD,KAAK,CAAC,eAAe,EAAE;AACvB,QAAA,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE;AACf,YAAA,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC,gCAAgC,CAAC;YACjE;AACD,QAAA;QACD,IAAI;YACF,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC;YACvC,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,CAAA,KAAA,EAAQ,QAAQ,CAAC,MAAM,CAAA,CAAE,CAAC;AAC5D,YAAA,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE;YAClC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,IAAI,aAAa,CAAC;AACxD,QAAA;QAAC,MAAM;AACN,YAAA,IAAI,CAAC,YAAY,CAAC,aAAa,CAC7B,CAAA,mBAAA,EAAsB,KAAK,CAAC,QAAQ,IAAI,MAAM,CAAA,CAAA,CAAG,CAClD;AACF,QAAA;IACH;IAEQ,aAAa,GAAA;QACnB,IAAI;AACF,YAAA,IAAI,CAAC,MAAM,EAAE,OAAO,IAAI;AACzB,QAAA;AAAC,QAAA,MAAM,EAAA;AACR,QAAA,IAAI,CAAC,MAAM,GAAG,IAAI;IACpB;IAEA,KAAK,GAAA;AACH,QAAA,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE;IAC5B;wGAzLW,wBAAwB,EAAA,IAAA,EAAA,CAAA,EAAA,KAAA,EAAA,EAAA,CAAA,cAAA,EAAA,EAAA,EAAA,KAAA,EAAAA,mBAAA,EAAA,EAAA,EAAA,KAAA,EAAAC,EAAA,CAAA,YAAA,EAAA,CAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;4FAAxB,wBAAwB,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,uBAAA,EAAA,MAAA,EAAA,EAAA,SAAA,EAAA,WAAA,EAAA,QAAA,EAAA,UAAA,EAAA,EAAA,WAAA,EAAA,CAAA,EAAA,YAAA,EAAA,YAAA,EAAA,KAAA,EAAA,IAAA,EAAA,SAAA,EAAA,CAAA,YAAA,CAAA,EAAA,WAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,ECtDrC,2lIAkGA,EAAA,MAAA,EAAA,CAAA,m5GAAA,CAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,EDhDY,YAAY,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAAC,EAAA,CAAA,OAAA,EAAA,QAAA,EAAA,kBAAA,EAAA,MAAA,EAAA,CAAA,SAAA,EAAA,cAAA,EAAA,eAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAAA,EAAA,CAAA,IAAA,EAAA,QAAA,EAAA,QAAA,EAAA,MAAA,EAAA,CAAA,MAAA,EAAA,UAAA,EAAA,UAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,MAAA,EAAA,IAAA,EAAAA,EAAA,CAAA,QAAA,EAAA,IAAA,EAAA,MAAA,EAAA,CAAA,EAAA,CAAA;;4FAIX,wBAAwB,EAAA,UAAA,EAAA,CAAA;kBAPpC,SAAS;AACE,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,uBAAuB,EAAA,UAAA,EACrB,IAAI,EAAA,OAAA,EACP,CAAC,YAAY,CAAC,EAAA,QAAA,EAAA,2lIAAA,EAAA,MAAA,EAAA,CAAA,m5GAAA,CAAA,EAAA;6IAOd,SAAS,EAAA,CAAA;sBAAjB;gBAEQ,QAAQ,EAAA,CAAA;sBAAhB;gBAEwB,UAAU,EAAA,CAAA;sBAAlC,SAAS;uBAAC,YAAY;;;;;"}
@@ -7624,7 +7624,7 @@ class CadSourceService {
7624
7624
  async open(storeName, cadFiles) {
7625
7625
  if (!this.hasSource(cadFiles))
7626
7626
  return;
7627
- const { DxfSourceViewerComponent } = await import('./tango-app-ui-store-builder-dxf-source-viewer.component-KfK9D7DB.mjs');
7627
+ const { DxfSourceViewerComponent } = await import('./tango-app-ui-store-builder-dxf-source-viewer.component-BplzhK87.mjs');
7628
7628
  const ref = this.ngbModal.open(DxfSourceViewerComponent, {
7629
7629
  size: "xl",
7630
7630
  centered: true,
@@ -62369,31 +62369,88 @@ class OnboardStorePlanoComponent {
62369
62369
  height: maxY - minY,
62370
62370
  };
62371
62371
  }
62372
- // Bounding box of just the wall + entrance objects — the store perimeter.
62373
- // Used as the fit target so stray fixtures placed outside the layout (common
62374
- // on CAD imports, where a fixture's relativePosition can be unset/garbage)
62375
- // can't inflate the frame. Returns zero-size when there is no wall/entrance
62376
- // on the canvas so callers can fall back to full bounds.
62377
- getWallPerimeterBounds() {
62378
- const objects = this.canvas
62372
+ // Shared median helper for the robust (outlier-tolerant) bounds below.
62373
+ median(values) {
62374
+ if (!values.length)
62375
+ return 0;
62376
+ const sorted = [...values].sort((a, b) => a - b);
62377
+ const mid = Math.floor(sorted.length / 2);
62378
+ return sorted.length % 2
62379
+ ? sorted[mid]
62380
+ : (sorted[mid - 1] + sorted[mid]) / 2;
62381
+ }
62382
+ // Union bounds of the canvas objects with gross positional outliers removed.
62383
+ // A single element carrying a bad far coordinate — a CAD orphan wall stranded
62384
+ // at the origin, an unplaced fixture — would otherwise inflate the frame and
62385
+ // collapse the fit zoom to an invisible speck. We drop objects whose center
62386
+ // lies far outside the cluster (median center + a generous multiple of the
62387
+ // spread) and fit to what remains, so no single stray of ANY type (wall,
62388
+ // entrance, fixture, other) can hide the whole layout. Falls back to the full
62389
+ // union when there are too few objects to reason about statistically.
62390
+ getRobustLayoutBounds() {
62391
+ const entries = this.canvas
62379
62392
  .getObjects()
62380
- .filter((o) => o?.objType === "wall" || o?.objType === "entrance");
62381
- if (!objects.length)
62393
+ .map((o) => ({ o, r: o.getBoundingRect() }))
62394
+ .filter(({ r }) => Number.isFinite(r.left) &&
62395
+ Number.isFinite(r.top) &&
62396
+ Number.isFinite(r.width) &&
62397
+ Number.isFinite(r.height));
62398
+ if (!entries.length)
62382
62399
  return { left: 0, top: 0, width: 0, height: 0 };
62383
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
62384
- for (const obj of objects) {
62385
- const rect = obj.getBoundingRect();
62386
- minX = Math.min(minX, rect.left);
62387
- minY = Math.min(minY, rect.top);
62388
- maxX = Math.max(maxX, rect.left + rect.width);
62389
- maxY = Math.max(maxY, rect.top + rect.height);
62390
- }
62391
- return {
62392
- left: minX,
62393
- top: minY,
62394
- width: maxX - minX,
62395
- height: maxY - minY,
62400
+ const union = (list) => {
62401
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
62402
+ for (const { r } of list) {
62403
+ minX = Math.min(minX, r.left);
62404
+ minY = Math.min(minY, r.top);
62405
+ maxX = Math.max(maxX, r.left + r.width);
62406
+ maxY = Math.max(maxY, r.top + r.height);
62407
+ }
62408
+ return { left: minX, top: minY, width: maxX - minX, height: maxY - minY };
62396
62409
  };
62410
+ if (entries.length < 4)
62411
+ return union(entries);
62412
+ const cxs = entries.map(({ r }) => r.left + r.width / 2);
62413
+ const cys = entries.map(({ r }) => r.top + r.height / 2);
62414
+ const medX = this.median(cxs);
62415
+ const medY = this.median(cys);
62416
+ const dists = entries.map((_, i) => Math.hypot(cxs[i] - medX, cys[i] - medY));
62417
+ const medDist = this.median(dists);
62418
+ const mad = this.median(dists.map((d) => Math.abs(d - medDist)));
62419
+ const spread = mad > 1 ? mad : medDist > 1 ? medDist : 1;
62420
+ const threshold = medDist + 6 * spread;
62421
+ const kept = entries.filter((_, i) => dists[i] <= threshold);
62422
+ const dropped = entries.filter((_, i) => dists[i] > threshold);
62423
+ if (dropped.length) {
62424
+ console.warn(`[fit] ignoring ${dropped.length} out-of-cluster object(s) while fitting`, dropped.map(({ o, r }) => ({
62425
+ type: o?.objType,
62426
+ id: o?.fixtureId ?? o?.data?._id ?? o?.data?.elementNumber,
62427
+ bounds: r,
62428
+ })));
62429
+ }
62430
+ return kept.length ? union(kept) : union(entries);
62431
+ }
62432
+ // Wall start-points (px) with gross outliers removed. A CAD orphan wall far
62433
+ // from the store must not define the perimeter that reclaim places elements
62434
+ // into, so strip it the same way the fit does before building the polygon.
62435
+ getRobustWallPolygon() {
62436
+ const pts = (this.floorData?.layoutPolygon ?? [])
62437
+ .filter((w) => w?.elementType === "wall")
62438
+ .map((w) => ({
62439
+ x: this.toPixels(w.relativePosition?.x ?? 0, w.relativePosition?.unit || "ft"),
62440
+ y: this.toPixels(w.relativePosition?.y ?? 0, w.relativePosition?.unit || "ft"),
62441
+ }))
62442
+ .filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y));
62443
+ if (pts.length < 4)
62444
+ return pts;
62445
+ const medX = this.median(pts.map((p) => p.x));
62446
+ const medY = this.median(pts.map((p) => p.y));
62447
+ const dists = pts.map((p) => Math.hypot(p.x - medX, p.y - medY));
62448
+ const medDist = this.median(dists);
62449
+ const mad = this.median(dists.map((d) => Math.abs(d - medDist)));
62450
+ const spread = mad > 1 ? mad : medDist > 1 ? medDist : 1;
62451
+ const threshold = medDist + 6 * spread;
62452
+ const kept = pts.filter((_, i) => dists[i] <= threshold);
62453
+ return kept.length >= 3 ? kept : pts;
62397
62454
  }
62398
62455
  // Reclaims elements whose stored position renders them outside the wall
62399
62456
  // perimeter — the bad-coordinate artifacts a CAD import can leave behind.
@@ -62406,7 +62463,10 @@ class OnboardStorePlanoComponent {
62406
62463
  const floor = this.floorData;
62407
62464
  if (!floor?.layoutPolygon?.length)
62408
62465
  return 0;
62409
- const wallPolygon = this.buildLayoutPolygonFromWalls();
62466
+ // Robust wall polygon so an orphan wall (bad coordinate) can't define the
62467
+ // perimeter — otherwise fixtures/other elements would all look "inside" the
62468
+ // inflated frame and never get reclaimed.
62469
+ const wallPolygon = this.getRobustWallPolygon();
62410
62470
  if (wallPolygon.length < 3)
62411
62471
  return 0;
62412
62472
  const bounds = this.polygonBounds(wallPolygon);
@@ -62627,15 +62687,12 @@ class OnboardStorePlanoComponent {
62627
62687
  return;
62628
62688
  if (this.layoutForm.enabled)
62629
62689
  return;
62630
- // Fit to the wall/entrance perimeter (the true store extent) rather than the
62631
- // union of every object. On CAD-imported stores a few fixtures can land far
62632
- // outside the layout, and using the full bounds would blow up the frame and
62633
- // collapse the zoom to near-zero, leaving the layout invisibly small. Fall
62634
- // back to full bounds only when there is no wall/entrance to anchor on.
62635
- const perimeter = this.getWallPerimeterBounds();
62636
- const bounds = perimeter.width && perimeter.height
62637
- ? perimeter
62638
- : this.getFullLayoutBounds();
62690
+ // Fit to the cluster of objects with gross outliers removed, rather than the
62691
+ // union of everything. A single element with a bad far coordinate e.g. a
62692
+ // CAD orphan wall stranded at the origin while the real store sits thousands
62693
+ // of feet away would otherwise blow up the frame and collapse the zoom to
62694
+ // an invisible speck. getRobustLayoutBounds() drops such strays of any type.
62695
+ const bounds = this.getRobustLayoutBounds();
62639
62696
  if (!bounds.width || !bounds.height)
62640
62697
  return;
62641
62698
  const canvasW = this.canvas.getWidth();
@@ -80273,4 +80330,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
80273
80330
  */
80274
80331
 
80275
80332
  export { StoreBuilderService as S, TangoStoreBuilderModule as T, TangoStorePlanoModule as a };
80276
- //# sourceMappingURL=tango-app-ui-store-builder-tango-app-ui-store-builder-D1UC-7Qi.mjs.map
80333
+ //# sourceMappingURL=tango-app-ui-store-builder-tango-app-ui-store-builder-CXw6OsFW.mjs.map