@fmsim/board 2.0.0-beta.11 → 2.0.0-beta.110

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/custom-elements.json +6296 -2102
  2. package/demo/index-modeller.html +1 -1
  3. package/demo/index-player.html +2 -2
  4. package/demo/index-viewer.html +2 -2
  5. package/demo/index.html +2 -2
  6. package/dist/src/board-i18n.d.ts +37 -0
  7. package/dist/src/board-i18n.js +121 -0
  8. package/dist/src/board-i18n.js.map +1 -0
  9. package/dist/src/component/etc.js +36 -0
  10. package/dist/src/component/etc.js.map +1 -1
  11. package/dist/src/component/index.d.ts +1 -0
  12. package/dist/src/component/index.js +1 -0
  13. package/dist/src/component/index.js.map +1 -1
  14. package/dist/src/component/restful-property-editor-backdrop-faces.d.ts +50 -0
  15. package/dist/src/component/restful-property-editor-backdrop-faces.js +524 -0
  16. package/dist/src/component/restful-property-editor-backdrop-faces.js.map +1 -0
  17. package/dist/src/config/flow-replay.d.ts +14 -0
  18. package/dist/src/config/flow-replay.js +20 -0
  19. package/dist/src/config/flow-replay.js.map +1 -0
  20. package/dist/src/data-storage/board-model-cache.d.ts +13 -0
  21. package/dist/src/data-storage/board-model-cache.js +66 -0
  22. package/dist/src/data-storage/board-model-cache.js.map +1 -0
  23. package/dist/src/index.d.ts +5 -0
  24. package/dist/src/index.js +4 -0
  25. package/dist/src/index.js.map +1 -1
  26. package/dist/src/layers/mcs-mouse-event-handler.js +28 -3
  27. package/dist/src/layers/mcs-mouse-event-handler.js.map +1 -1
  28. package/dist/src/layers/movement-layer.d.ts +9 -6
  29. package/dist/src/layers/movement-layer.js +35 -10
  30. package/dist/src/layers/movement-layer.js.map +1 -1
  31. package/dist/src/modeller/bulk-create-dialog.d.ts +62 -0
  32. package/dist/src/modeller/bulk-create-dialog.js +595 -0
  33. package/dist/src/modeller/bulk-create-dialog.js.map +1 -0
  34. package/dist/src/modeller/bulk-create.d.ts +107 -0
  35. package/dist/src/modeller/bulk-create.js +201 -0
  36. package/dist/src/modeller/bulk-create.js.map +1 -0
  37. package/dist/src/modeller/component-toolbar/component-menu.d.ts +1 -1
  38. package/dist/src/modeller/component-toolbar/component-toolbar.d.ts +7 -1
  39. package/dist/src/modeller/component-toolbar/component-toolbar.js +18 -2
  40. package/dist/src/modeller/component-toolbar/component-toolbar.js.map +1 -1
  41. package/dist/src/modeller/edit-toolbar.d.ts +13 -0
  42. package/dist/src/modeller/edit-toolbar.js +177 -7
  43. package/dist/src/modeller/edit-toolbar.js.map +1 -1
  44. package/dist/src/modeller/property-sidebar/abstract-property.js +10 -2
  45. package/dist/src/modeller/property-sidebar/abstract-property.js.map +1 -1
  46. package/dist/src/modeller/property-sidebar/data-binding/data-binding.d.ts +1 -1
  47. package/dist/src/modeller/property-sidebar/effects/effects.d.ts +10 -1
  48. package/dist/src/modeller/property-sidebar/effects/effects.js +67 -1
  49. package/dist/src/modeller/property-sidebar/effects/effects.js.map +1 -1
  50. package/dist/src/modeller/property-sidebar/effects/event-handler-adapter.d.ts +44 -0
  51. package/dist/src/modeller/property-sidebar/effects/event-handler-adapter.js +70 -0
  52. package/dist/src/modeller/property-sidebar/effects/event-handler-adapter.js.map +1 -0
  53. package/dist/src/modeller/property-sidebar/effects/property-event.d.ts +13 -3
  54. package/dist/src/modeller/property-sidebar/effects/property-event.js +20 -11
  55. package/dist/src/modeller/property-sidebar/effects/property-event.js.map +1 -1
  56. package/dist/src/modeller/property-sidebar/inspector/inspector-controller.d.ts +66 -0
  57. package/dist/src/modeller/property-sidebar/inspector/inspector-controller.js +288 -0
  58. package/dist/src/modeller/property-sidebar/inspector/inspector-controller.js.map +1 -0
  59. package/dist/src/modeller/property-sidebar/inspector/inspector-group-view.d.ts +15 -0
  60. package/dist/src/modeller/property-sidebar/inspector/inspector-group-view.js +61 -0
  61. package/dist/src/modeller/property-sidebar/inspector/inspector-group-view.js.map +1 -0
  62. package/dist/src/modeller/property-sidebar/inspector/inspector-highlight.d.ts +7 -0
  63. package/dist/src/modeller/property-sidebar/inspector/inspector-highlight.js +25 -0
  64. package/dist/src/modeller/property-sidebar/inspector/inspector-highlight.js.map +1 -0
  65. package/dist/src/modeller/property-sidebar/inspector/inspector-model.d.ts +44 -0
  66. package/dist/src/modeller/property-sidebar/inspector/inspector-model.js +72 -0
  67. package/dist/src/modeller/property-sidebar/inspector/inspector-model.js.map +1 -0
  68. package/dist/src/modeller/property-sidebar/inspector/inspector-tree-view.d.ts +18 -0
  69. package/dist/src/modeller/property-sidebar/inspector/inspector-tree-view.js +61 -0
  70. package/dist/src/modeller/property-sidebar/inspector/inspector-tree-view.js.map +1 -0
  71. package/dist/src/modeller/property-sidebar/inspector/inspector.d.ts +17 -12
  72. package/dist/src/modeller/property-sidebar/inspector/inspector.js +286 -151
  73. package/dist/src/modeller/property-sidebar/inspector/inspector.js.map +1 -1
  74. package/dist/src/modeller/property-sidebar/property-sidebar.d.ts +1 -1
  75. package/dist/src/modeller/property-sidebar/shapes/ox-input-entity-id.d.ts +75 -0
  76. package/dist/src/modeller/property-sidebar/shapes/ox-input-entity-id.js +355 -0
  77. package/dist/src/modeller/property-sidebar/shapes/ox-input-entity-id.js.map +1 -0
  78. package/dist/src/modeller/property-sidebar/shapes/shapes.d.ts +22 -1
  79. package/dist/src/modeller/property-sidebar/shapes/shapes.js +110 -2
  80. package/dist/src/modeller/property-sidebar/shapes/shapes.js.map +1 -1
  81. package/dist/src/modeller/property-sidebar/specifics/parent-object.d.ts +29 -0
  82. package/dist/src/modeller/property-sidebar/specifics/parent-object.js +74 -0
  83. package/dist/src/modeller/property-sidebar/specifics/parent-object.js.map +1 -0
  84. package/dist/src/modeller/property-sidebar/specifics/specifics.d.ts +27 -1
  85. package/dist/src/modeller/property-sidebar/specifics/specifics.js +95 -3
  86. package/dist/src/modeller/property-sidebar/specifics/specifics.js.map +1 -1
  87. package/dist/src/modeller/property-sidebar/styles/styles.d.ts +1 -1
  88. package/dist/src/modeller/scene-viewer/ox-scene-layer.d.ts +4 -3
  89. package/dist/src/modeller/scene-viewer/ox-scene-layer.js +2 -1
  90. package/dist/src/modeller/scene-viewer/ox-scene-layer.js.map +1 -1
  91. package/dist/src/modeller/scene-viewer/ox-scene-viewer.d.ts +2 -1
  92. package/dist/src/modeller/scene-viewer/ox-scene-viewer.js +5 -1
  93. package/dist/src/modeller/scene-viewer/ox-scene-viewer.js.map +1 -1
  94. package/dist/src/ox-board-component-info.d.ts +1 -0
  95. package/dist/src/ox-board-component-info.js +42 -2
  96. package/dist/src/ox-board-component-info.js.map +1 -1
  97. package/dist/src/ox-board-modeller.d.ts +6 -3
  98. package/dist/src/ox-board-modeller.js +21 -1
  99. package/dist/src/ox-board-modeller.js.map +1 -1
  100. package/dist/src/ox-board-viewer.d.ts +117 -13
  101. package/dist/src/ox-board-viewer.js +688 -102
  102. package/dist/src/ox-board-viewer.js.map +1 -1
  103. package/dist/src/ox-editor-board-selector.d.ts +3 -0
  104. package/dist/src/ox-editor-board-selector.js +40 -9
  105. package/dist/src/ox-editor-board-selector.js.map +1 -1
  106. package/dist/src/ox-playback-controls.d.ts +115 -18
  107. package/dist/src/ox-playback-controls.js +744 -167
  108. package/dist/src/ox-playback-controls.js.map +1 -1
  109. package/dist/src/playback/carrier-tracking-store.d.ts +51 -0
  110. package/dist/src/playback/carrier-tracking-store.js +115 -0
  111. package/dist/src/playback/carrier-tracking-store.js.map +1 -0
  112. package/dist/src/playback/playback-engine.d.ts +106 -0
  113. package/dist/src/playback/playback-engine.js +395 -0
  114. package/dist/src/playback/playback-engine.js.map +1 -0
  115. package/dist/src/scene-monitor-renderer.d.ts +10 -0
  116. package/dist/src/scene-monitor-renderer.js +226 -0
  117. package/dist/src/scene-monitor-renderer.js.map +1 -0
  118. package/dist/src/selector/ox-board-selector.js +14 -2
  119. package/dist/src/selector/ox-board-selector.js.map +1 -1
  120. package/dist/tsconfig.tsbuildinfo +1 -1
  121. package/icons/components/text.png +0 -0
  122. package/icons/fab/mode-2d.svg +25 -0
  123. package/icons/fab/mode-3d.svg +25 -0
  124. package/icons/fab/playback-play.svg +49 -0
  125. package/icons/fab/playback-stop.svg +50 -0
  126. package/package.json +8 -4
@@ -1,3 +1,4 @@
1
+ var BoardViewer_1;
1
2
  import { __decorate } from "tslib";
2
3
  import '@material/web/fab/fab.js';
3
4
  import '@material/web/icon/icon.js';
@@ -7,12 +8,22 @@ import './layers/mcs-mouse-event-handler.js';
7
8
  import './ox-playback-controls.js';
8
9
  import { css, html, LitElement, nothing } from 'lit';
9
10
  import { customElement, property, query, state } from 'lit/decorators.js';
10
- import { create, SCENE_MODE } from '@hatiolab/things-scene';
11
- import { isIOS, togglefullscreen } from '@operato/utils';
11
+ import { PlaybackEngine } from './playback/playback-engine.js';
12
+ import { isFlowReplayEnabled } from './config/flow-replay.js';
13
+ import { CarrierTrackingStore } from './playback/carrier-tracking-store.js';
14
+ import { create, LoadTracker, LoadingOverlay, SCENE_MODE, dataLagTracker, isThreedEnabled } from '@hatiolab/things-scene';
15
+ import { isIOS, fullscreen, exitfullscreen, togglefullscreen } from '@operato/utils';
12
16
  import { ScrollbarStyles } from '@operato/styles';
13
17
  import { DataStorage } from './data-storage/data-storage.js';
14
18
  import { runScenario, startScenario, DataSubscriptionProviderImpl, isRestfulAvailable } from '@fmsim/api';
15
- let BoardViewer = class BoardViewer extends LitElement {
19
+ import { SceneMonitorRenderer } from './scene-monitor-renderer.js';
20
+ // FAB 아이콘 — packages/board/icons/fab/ 의 외부 SVG asset 로 분리.
21
+ // 기존엔 inline SVG 였으나 (a) 가독성 향상, (b) 디자이너 단독 수정, (c) 캐시 활용 목적.
22
+ const ICON_FAB_PLAYBACK_PLAY = new URL('../../icons/fab/playback-play.svg', import.meta.url).href;
23
+ const ICON_FAB_PLAYBACK_STOP = new URL('../../icons/fab/playback-stop.svg', import.meta.url).href;
24
+ const ICON_FAB_MODE_2D = new URL('../../icons/fab/mode-2d.svg', import.meta.url).href;
25
+ const ICON_FAB_MODE_3D = new URL('../../icons/fab/mode-3d.svg', import.meta.url).href;
26
+ let BoardViewer = BoardViewer_1 = class BoardViewer extends LitElement {
16
27
  constructor() {
17
28
  var _a;
18
29
  super(...arguments);
@@ -43,9 +54,16 @@ let BoardViewer = class BoardViewer extends LitElement {
43
54
  };
44
55
  // 모니터링에서 Movement Data를 보여준다.
45
56
  this.fmbShowMovementHandler = (e) => {
46
- var _a;
57
+ var _a, _b, _c;
58
+ // 플레이백 중에는 외부 모니터링이 보내는 실시간 반송 정보를 그리지 않는다 —
59
+ // 사용자가 플레이백 시점의 *과거 상태* 를 보고 있는데 실시간 화살표가 겹치면
60
+ // 혼란을 줌. (백엔드 상세 정보 요청은 ox-board-component-info.ts:225 에서
61
+ // 같은 playbackMode 가드로 이미 차단됨.)
62
+ if ((_b = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.rootContainer) === null || _b === void 0 ? void 0 : _b.playbackMode) {
63
+ return;
64
+ }
47
65
  const movement = e.detail.value;
48
- const movementLayer = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.findById(`movement-layer`);
66
+ const movementLayer = (_c = this._scene) === null || _c === void 0 ? void 0 : _c.findById(`movement-layer`);
49
67
  if (!movementLayer) {
50
68
  return;
51
69
  }
@@ -109,26 +127,25 @@ let BoardViewer = class BoardViewer extends LitElement {
109
127
  floatingContainer.hidden = true;
110
128
  }
111
129
  };
112
- // BoardComponentInfo popup을 보여준다.
130
+ // BoardComponentInfo popup 을 보여주고 deco-border 로 selected highlight.
131
+ // 영구 추적과는 독립 — query-submit 핸들러 (onQuerySubmit) 가 별도로 담당.
113
132
  this.clickComponentHandler = (e) => {
114
- var _a, _b;
115
133
  const component = e.detail;
116
134
  const { type } = component.state;
117
135
  if (type == 'model-layer') {
118
- (_a = this.lastComponent) === null || _a === void 0 ? void 0 : _a.trigger('deco-border-off');
119
- // this.lastComponent?.trigger('deco-arrow-reset')
136
+ this._clearClickHighlight();
120
137
  this.hidePopup();
121
138
  return;
122
139
  }
123
140
  if ('isLVComponent' in component) {
124
- (_b = this.lastComponent) === null || _b === void 0 ? void 0 : _b.trigger('deco-border-off');
125
- // this.lastComponent?.trigger('deco-arrow-reset')
141
+ this._clearClickHighlight();
126
142
  this.lastComponent = component;
127
143
  component.trigger('deco-border', { distance: 8, lineWidth: 4, color: '#f007' });
128
- // component.trigger('deco-arrow', { color: 'red', animation: true, size: 20 })
129
144
  this.togglePopup(component);
130
145
  }
131
146
  };
147
+ // 영구 추적 라이프사이클은 things-scene 의 Scene.tracker 가 일괄 관리.
148
+ // (polling, deco-border 부착/해제, 3D outline 색 동기화, centerTo)
132
149
  this.fmbStartAnimation = (e) => {
133
150
  var _a;
134
151
  const { target, animation, decorator, border, arrow } = e.detail || {};
@@ -175,18 +192,66 @@ let BoardViewer = class BoardViewer extends LitElement {
175
192
  var _a;
176
193
  (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root.trigger('deco-reset');
177
194
  };
195
+ /**
196
+ * 스토커 게이지 표시 모드 전환 (런타임 전용, 비영속).
197
+ * 외부: window.dispatchEvent(new CustomEvent('fmbSetStockerGaugeMode', { detail: { mode } }))
198
+ * mode: 'capacity'(기본) | 'carrier' | 'abnormal'. RootContainer.stockerGaugeMode setter 가
199
+ * 2D 리드로우 + 3D 게이지 재빌드를 트리거한다(fmsim/machine root-container-override).
200
+ * 미지값은 무시 — state 오염 방지(render 는 미지값을 capacity 로 폴백하지만 명시 게이트).
201
+ *
202
+ * 반드시 rootContainer(=RootContainer, isRoot()===true)에 설정해야 한다. scene.root 는
203
+ * model_layer(Layer)라 override 가 없고, 컴포넌트의 this.root 는 RootContainer 로 해석되므로
204
+ * scene.root 에 쓰면 죽은 속성만 박히고 게이지에 반영되지 않는다.
205
+ */
206
+ this.fmbSetStockerGaugeModeHandler = (e) => {
207
+ var _a, _b;
208
+ const mode = (_a = e.detail) === null || _a === void 0 ? void 0 : _a.mode;
209
+ if (mode !== 'capacity' && mode !== 'carrier' && mode !== 'abnormal') {
210
+ return;
211
+ }
212
+ const rootContainer = (_b = this._scene) === null || _b === void 0 ? void 0 : _b.rootContainer;
213
+ if (rootContainer) {
214
+ rootContainer.stockerGaugeMode = mode;
215
+ }
216
+ };
178
217
  this._pointerMoveHandler = () => this.transientShowButtons();
218
+ /**
219
+ * playback 모드는 전체화면 모드와 한 몸 — 어떤 경로로든 전체화면이 해제되면
220
+ * playback 도 함께 종료. (단독 전체화면 토글은 기존처럼 동작하도록 `_playbackActive` 가드.)
221
+ */
222
+ this._fullscreenChangeHandler = () => {
223
+ if (!document.fullscreenElement && this._playbackActive) {
224
+ this._onPlaybackStop();
225
+ }
226
+ };
179
227
  this.resizeHandler = () => {
180
228
  this._scene && this._scene.fit();
181
229
  };
230
+ // ── Data Subscription ──
231
+ this._dataSubscriptionProvider = isRestfulAvailable() ? undefined : new DataSubscriptionProviderImpl();
232
+ /**
233
+ * 영구추적 fallback 데이터 캐시 — 플레이백 윈도우 × carrierName 단위로
234
+ * carrier 위치 이력을 보관. carrier 가 scene 에 직접 안 보일 때
235
+ * Tracker fallback 콜백이 lookupAt 으로 머신/유닛을 찾아 반환.
236
+ */
237
+ this._carrierTrackingStore = new CarrierTrackingStore();
238
+ /**
239
+ * fallback 결과(target 컴포넌트) dedupe — 직전과 동일한 target 일 땐 로그 skip
240
+ * + deco-arrow 라이프사이클 (target 변경 시 이전 끄고 새로 부착).
241
+ */
242
+ this._lastFallbackTarget = null;
243
+ /** _resolveCarrierFallback 마지막 호출 timestamp — carrier 발견 감지용. */
244
+ this._lastFallbackResolvedAt = 0;
182
245
  }
183
246
  get _is3dMode() {
184
247
  var _a, _b, _c;
185
- return !!((_c = (_b = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root) === null || _b === void 0 ? void 0 : _b.state) === null || _c === void 0 ? void 0 : _c.threed);
248
+ return !!((_c = (_b = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root) === null || _b === void 0 ? void 0 : _b.rootModel) === null || _c === void 0 ? void 0 : _c.is3dMode);
186
249
  }
187
250
  render() {
188
- var fabGroup = !isIOS() && !this.hideFullscreen
189
- ? html `
251
+ var _a, _b;
252
+ var fabGroup = this.hideFullscreen
253
+ ? html ``
254
+ : html `
190
255
  <div
191
256
  fab-group
192
257
  ?expanded=${this._fabExpanded}
@@ -200,26 +265,33 @@ let BoardViewer = class BoardViewer extends LitElement {
200
265
  title="menu"
201
266
  ><md-icon slot="icon">add</md-icon></md-fab>
202
267
 
203
- <md-fab
268
+ ${isIOS() ? nothing : html `<md-fab
204
269
  class="sub-fab"
205
270
  @click=${() => { this._fabExpanded = false; this.onTapFullscreen(); }}
206
271
  title="fullscreen"
207
- ><md-icon slot="icon">${document.fullscreenElement ? 'fullscreen_exit' : 'fullscreen'}</md-icon></md-fab>
272
+ ><md-icon slot="icon">${document.fullscreenElement ? 'fullscreen_exit' : 'fullscreen'}</md-icon></md-fab>`}
208
273
 
209
- <md-fab
274
+ ${isFlowReplayEnabled() ? html `<md-fab
210
275
  class="sub-fab"
211
276
  @click=${() => { this._fabExpanded = false; this.onTapPlayback(); }}
212
277
  title=${this._playbackActive ? 'stop playback' : 'start playback'}
213
- ><md-icon slot="icon">${this._playbackActive ? 'stop' : 'replay'}</md-icon></md-fab>
278
+ ><span slot="icon" class="fab-icon-wrap"><img
279
+ class="fab-icon"
280
+ src=${this._playbackActive ? ICON_FAB_PLAYBACK_STOP : ICON_FAB_PLAYBACK_PLAY}
281
+ alt=${this._playbackActive ? 'stop playback' : 'start playback'}
282
+ /></span></md-fab>` : nothing}
214
283
 
215
- <md-fab
284
+ ${isThreedEnabled() ? html `<md-fab
216
285
  class="sub-fab"
217
286
  @click=${() => { this._fabExpanded = false; this.onTapToggle3D(); }}
218
287
  title=${this._is3dMode ? '2D mode' : '3D mode'}
219
- ><md-icon slot="icon">${this._is3dMode ? 'view_in_ar' : '3d_rotation'}</md-icon></md-fab>
288
+ ><span slot="icon" class="fab-icon-wrap"><img
289
+ class="fab-icon"
290
+ src=${this._is3dMode ? ICON_FAB_MODE_2D : ICON_FAB_MODE_3D}
291
+ alt=${this._is3dMode ? '2D mode' : '3D mode'}
292
+ /></span></md-fab>` : nothing}
220
293
  </div>
221
- `
222
- : html ``;
294
+ `;
223
295
  var prev = !this.hideNavigation
224
296
  ? html `
225
297
  <md-icon
@@ -254,72 +326,100 @@ let BoardViewer = class BoardViewer extends LitElement {
254
326
 
255
327
  ${this._playbackActive
256
328
  ? html `<ox-playback-controls
257
- @playback-stop=${() => this._stopPlayback()}
329
+ .playbackState=${((_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.state) === 'playing' ? 'playing' : ((_b = this._playbackEngine) === null || _b === void 0 ? void 0 : _b.state) === 'paused' ? 'paused' : 'idle'}
330
+ @playback-start=${this._onPlaybackStart}
331
+ @playback-pause=${this._onPlaybackPause}
332
+ @playback-resume=${this._onPlaybackResume}
333
+ @playback-stop=${this._onPlaybackStop}
334
+ @playback-seek=${this._onPlaybackSeek}
335
+ @playback-seek-start=${this._onPlaybackSeekStart}
336
+ @playback-seek-preview=${this._onPlaybackSeekPreview}
337
+ @playback-speed=${this._onPlaybackSpeed}
258
338
  ></ox-playback-controls>`
259
339
  : nothing}
260
340
  `;
261
341
  }
342
+ /** 마우스 클릭/popup 의 selected highlight 해제. */
343
+ _clearClickHighlight() {
344
+ if (this.lastComponent) {
345
+ this.lastComponent.trigger('deco-border-off');
346
+ this.lastComponent = null;
347
+ }
348
+ }
349
+ /** connect/disconnect 시 일괄 등록/해제할 window 이벤트 목록 (모니터링 연동) */
350
+ get _monitoringEvents() {
351
+ return [
352
+ ['fetchIFactsData', this.fetchIFactsDataHandler], // 장비 아이디 object를 캔버스 가운데로
353
+ ['fmbShowMovement', this.fmbShowMovementHandler], // Movement Data 표시
354
+ ['fmbMonitoringInit', this.fmbMonitoringInitHandler], // 보드 상태 정사이즈 복원
355
+ ['clickComponent', this.clickComponentHandler], // BoardComponentInfo popup
356
+ ['fmbStartAnimation', this.fmbStartAnimation], // AnimatedIcon 추가/제거
357
+ ['fmbStopAnimation', this.fmbStopAnimation],
358
+ ['fmbResetAnimation', this.fmbResetAnimation],
359
+ ['fmbShowLegend', this.fmbShowLegendHandler], // 범례 표시
360
+ ['fmbHideLegend', this.fmbHideLegendHandler],
361
+ ['fmbShowPopup', this.fmbShowPopupHandler], // 팝업 표시
362
+ ['fmbHidePopup', this.fmbHidePopupHandler],
363
+ ['fmbShowFloatingContainer', this.fmbShowFloatingContainerHandler], // FloatingContainer
364
+ ['fmbHideFloatingContainer', this.fmbHideFloatingContainerHandler],
365
+ ['fmbSetStockerGaugeMode', this.fmbSetStockerGaugeModeHandler] // 스토커 게이지 모드 전환
366
+ ];
367
+ }
262
368
  connectedCallback() {
263
369
  super.connectedCallback();
264
370
  window.addEventListener('resize', this.resizeHandler);
265
371
  // 3D 모드에서도 마우스 움직임 감지 (document 레벨, capture phase)
266
372
  document.addEventListener('pointermove', this._pointerMoveHandler, true);
373
+ document.addEventListener('fullscreenchange', this._fullscreenChangeHandler);
374
+ document.addEventListener('webkitfullscreenchange', this._fullscreenChangeHandler);
267
375
  this.renderRoot.addEventListener('close-scene', e => {
268
376
  e.preventDefault();
269
377
  this.onTapPrev();
270
378
  }, false);
271
- // 모니터링에서 장비 아이디에 해당하는 object를 찾아 캔버스 가운데 위치하도록 조정한다.
272
- window.addEventListener('fetchIFactsData', this.fetchIFactsDataHandler);
273
- // 모니터링에서 Movement Data를 보여준다.
274
- window.addEventListener('fmbShowMovement', this.fmbShowMovementHandler);
275
- // 모니터링에서 보드 상태를 원래상태 (정사이즈)로 돌려놓는다.
276
- window.addEventListener('fmbMonitoringInit', this.fmbMonitoringInitHandler);
277
- // BoardComponentInfo popup을 보여준다.
278
- window.addEventListener('clickComponent', this.clickComponentHandler);
279
- // AnimatedIcon 을 추가하거나 제거한다.
280
- window.addEventListener('fmbStartAnimation', this.fmbStartAnimation);
281
- window.addEventListener('fmbStopAnimation', this.fmbStopAnimation);
282
- window.addEventListener('fmbResetAnimation', this.fmbResetAnimation);
283
- // 모니터링에서 범례를 보여준다.
284
- window.addEventListener('fmbShowLegend', this.fmbShowLegendHandler);
285
- window.addEventListener('fmbHideLegend', this.fmbHideLegendHandler);
286
- // 모니터링에서 범례를 보여준다.
287
- window.addEventListener('fmbShowPopup', this.fmbShowPopupHandler);
288
- window.addEventListener('fmbHidePopup', this.fmbHidePopupHandler);
289
- // 모니터링에서 FloatingContainer를 보여준다.
290
- window.addEventListener('fmbShowFloatingContainer', this.fmbShowFloatingContainerHandler);
291
- window.addEventListener('fmbHideFloatingContainer', this.fmbHideFloatingContainerHandler);
379
+ for (const [type, handler] of this._monitoringEvents) {
380
+ window.addEventListener(type, handler);
381
+ }
292
382
  }
293
383
  disconnectedCallback() {
384
+ var _a, _b, _c, _d, _e;
294
385
  super.disconnectedCallback();
295
386
  window.removeEventListener('resize', this.resizeHandler);
296
387
  document.removeEventListener('pointermove', this._pointerMoveHandler, true);
297
- // 모니터링에서 장비 아이디에 해당하는 object를 찾아 캔버스 가운데 위치하도록 조정한다.
298
- window.removeEventListener('fetchIFactsData', this.fetchIFactsDataHandler);
299
- // 모니터링에서 Movement Data를 보여준다.
300
- window.removeEventListener('fmbShowMovement', this.fmbShowMovementHandler);
301
- // 모니터링에서 보드 상태를 원래상태 (정사이즈)로 돌려놓는다.
302
- window.removeEventListener('fmbMonitoringInit', this.fmbMonitoringInitHandler);
303
- // BoardComponentInfo popup을 보여준다.
304
- window.removeEventListener('clickComponent', this.clickComponentHandler);
305
- // AnimatedIcon을 추가하거나 제거한다.
306
- window.removeEventListener('fmbStartAnimation', this.fmbStartAnimation);
307
- window.removeEventListener('fmbStopAnimation', this.fmbStopAnimation);
308
- window.removeEventListener('fmbResetAnimation', this.fmbResetAnimation);
309
- // 모니터링에서 범례를 보여준다.
310
- window.removeEventListener('fmbShowLegend', this.fmbShowLegendHandler);
311
- window.removeEventListener('fmbHideLegend', this.fmbHideLegendHandler);
312
- // 모니터링에서 범례를 보여준다.
313
- window.removeEventListener('fmbShowPopup', this.fmbShowPopupHandler);
314
- window.removeEventListener('fmbHidePopup', this.fmbHidePopupHandler);
315
- // 모니터링에서 FloatingContainer를 보여준다.
316
- window.removeEventListener('fmbShowFloatingContainer', this.fmbShowFloatingContainerHandler);
317
- window.removeEventListener('fmbHideFloatingContainer', this.fmbHideFloatingContainerHandler);
318
- this.lastComponent = null;
388
+ document.removeEventListener('fullscreenchange', this._fullscreenChangeHandler);
389
+ document.removeEventListener('webkitfullscreenchange', this._fullscreenChangeHandler);
390
+ for (const [type, handler] of this._monitoringEvents) {
391
+ window.removeEventListener(type, handler);
392
+ }
393
+ this._clearClickHighlight();
394
+ (_b = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.tracker) === null || _b === void 0 ? void 0 : _b.stop();
395
+ // 플레이백이 활성 상태면 명시적 stop (provider resume 포함)
396
+ if (this._playbackActive) {
397
+ (_c = this._playbackEngine) === null || _c === void 0 ? void 0 : _c.stop();
398
+ (_d = this._playbackEngine) === null || _d === void 0 ? void 0 : _d.detachScene();
399
+ (_e = this._dataSubscriptionProvider) === null || _e === void 0 ? void 0 : _e.resume();
400
+ this._playbackActive = false;
401
+ }
402
+ this._playbackWindow = undefined;
403
+ this._carrierTrackingStore.clear();
404
+ this._clearFallbackArrow();
405
+ this._stopFallbackArrowMonitor();
406
+ if (this._playbackEngine) {
407
+ this._playbackEngine.dispose();
408
+ this._playbackEngine = undefined;
409
+ }
319
410
  this.closeScene();
320
411
  }
321
412
  updated(changes) {
413
+ // loadTracker가 외부에서 전달되면 즉시 overlay 연결 (fetch 중에도 표시)
414
+ if (changes.has('loadTracker') && this.loadTracker) {
415
+ this._ensureOverlay(this.loadTracker);
416
+ }
322
417
  if (changes.has('board')) {
418
+ // tracker: 외부에서 안 왔으면 여기서 생성
419
+ if (!this.loadTracker) {
420
+ this.loadTracker = new LoadTracker();
421
+ this._ensureOverlay(this.loadTracker);
422
+ }
323
423
  this.hidePopup();
324
424
  this.closeScene();
325
425
  if (this.board && this.board.id) {
@@ -339,33 +439,48 @@ let BoardViewer = class BoardViewer extends LitElement {
339
439
  }
340
440
  }
341
441
  async initSceneAsync() {
342
- if (!this.board || !this.board.id)
442
+ if (!this.board || !this.board.id || !this.provider)
343
443
  return;
344
- this._scene = await this.provider.create(this.board.id);
345
- this.setupScene({ id: this.board.id, scene: this._scene });
444
+ try {
445
+ this._scene = await this.provider.create(this.board.id);
446
+ this.setupScene({ id: this.board.id, scene: this._scene });
447
+ }
448
+ catch (e) {
449
+ console.error('Failed to load scene:', this.board.id, e);
450
+ }
346
451
  }
347
452
  initScene() {
348
- if (!this.board || !this.board.id) {
453
+ if (!this.board || !this.board.id || !this.board.model) {
349
454
  return;
350
455
  }
351
456
  this._scene = create({
352
- model: {
353
- ...this.board.model
354
- },
457
+ model: JSON.parse(JSON.stringify(this.board.model)),
355
458
  style: this.board.style,
356
459
  mode: SCENE_MODE.VIEW,
357
460
  handlers: ['mcs-event-handler', 'zoom-handler', 'shift-handler', 'waypoint-handler'],
358
461
  layers: [{ type: 'scroll-layer' }, { type: 'movement-layer', id: 'movement-layer' }],
359
462
  refProvider: this.provider,
360
463
  dataStorage: this.board.id !== 'preview' ? new DataStorage(this.board.id) : undefined,
361
- dataSubscriptionProvider: isRestfulAvailable() ? undefined : new DataSubscriptionProviderImpl()
464
+ dataSubscriptionProvider: this._dataSubscriptionProvider,
465
+ loading: this.loadTracker ? { tracker: this.loadTracker } : undefined
362
466
  });
363
467
  if (this.baseUrl) {
364
468
  this._scene.baseUrl = this.baseUrl;
365
469
  }
366
470
  this.setupScene({ id: this.board.id, scene: this._scene });
367
471
  }
472
+ _ensureOverlay(tracker) {
473
+ if (!this._loadingOverlay) {
474
+ this._loadingOverlay = new LoadingOverlay();
475
+ this._loadingOverlay.renderer = new SceneMonitorRenderer();
476
+ }
477
+ this._loadingOverlay.attach(this.renderRoot, tracker, dataLagTracker);
478
+ }
368
479
  closeScene() {
480
+ var _a, _b;
481
+ // scene 교체/종료 시 highlight 상태도 함께 정리. tracking watcher 누수 방지.
482
+ this._clearClickHighlight();
483
+ (_b = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.tracker) === null || _b === void 0 ? void 0 : _b.stop();
369
484
  if (this._scene) {
370
485
  this.unbindSceneEvents(this._scene);
371
486
  this._scene.target = null;
@@ -384,7 +499,15 @@ let BoardViewer = class BoardViewer extends LitElement {
384
499
  }
385
500
  setupScene({ id, scene }) {
386
501
  var _a;
502
+ // 플레이백 리셋
503
+ if (this._playbackActive) {
504
+ this._onPlaybackStop();
505
+ }
387
506
  this._scene = scene;
507
+ // M키 로딩 오버레이(scene 내장)에 fmsim Scene Monitor 렌더러 주입 — 카테고리별 표현.
508
+ if (scene === null || scene === void 0 ? void 0 : scene.loadingOverlay) {
509
+ scene.loadingOverlay.renderer = new SceneMonitorRenderer();
510
+ }
388
511
  const backgroundColor = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root.state.fillStyle;
389
512
  if (typeof backgroundColor === 'string') {
390
513
  this.style.backgroundColor = backgroundColor;
@@ -407,7 +530,7 @@ let BoardViewer = class BoardViewer extends LitElement {
407
530
  this.transientShowButtons();
408
531
  }
409
532
  async showScene(boardId, bindingData) {
410
- if (!boardId)
533
+ if (!boardId || !this.provider)
411
534
  return;
412
535
  try {
413
536
  var scene = await this.provider.get(boardId, true);
@@ -445,6 +568,206 @@ let BoardViewer = class BoardViewer extends LitElement {
445
568
  this._scene.on('run-scenario', this.onRunScenario, this);
446
569
  this._scene.on('legend-toggle', this.onLegendToggle, this);
447
570
  this._scene.on('open-popup', this.onOpenPopup, this);
571
+ // VIEW 모드 SearchOverlay 디폴트 = 영구 추적 (things-scene 자동 처리).
572
+ // application 은 trackerOptions (color/fallback 등) 또는 onSearchSubmit/onSearchClose
573
+ // hook (디폴트 override) 만 제공.
574
+ //
575
+ // fallback: 원본 carrier 컴포넌트가 scene 에 없을 때 호출 (Tracker 1s polling).
576
+ // 플레이백 중이면 carrier-location history 에서 현재 시각의 머신/유닛을 lookup 해
577
+ // 해당 컴포넌트를 highlight 대상으로 반환. 플레이백 비활성 / 시간 정보 없음 /
578
+ // NA / 보드에 머신 없음 → null 반환해서 highlight 없이 추적만 유지.
579
+ this._scene.trackerOptions = {
580
+ color: BoardViewer_1.TRACKING_COLOR,
581
+ fallback: (_id, scene) => this._resolveCarrierFallback(scene)
582
+ };
583
+ // SearchOverlay submit/close hook — carrier-location store 의 충전/정리 트리거.
584
+ // 디폴트 동작 (tracker.start/stop) 은 그대로 위임 (return undefined).
585
+ this._scene.onSearchSubmit = (detail) => {
586
+ this._onCarrierTrackingSubmit(detail === null || detail === void 0 ? void 0 : detail.query);
587
+ };
588
+ this._scene.onSearchClose = () => {
589
+ this._carrierTrackingStore.clear();
590
+ };
591
+ }
592
+ /**
593
+ * Tracker fallback resolver — 플레이백 시각의 carrier 위치를 컴포넌트로 변환.
594
+ * NA / 정보 없음 / 머신 미존재 → null (Tracker 가 highlight 없이 추적만 유지).
595
+ *
596
+ * 매 1초 polling 마다 호출되며, 결과 target 이 직전과 동일하면 로그 skip.
597
+ */
598
+ _resolveCarrierFallback(scene) {
599
+ var _a, _b, _c, _d;
600
+ if (!this._playbackActive)
601
+ return null;
602
+ const currentTime = (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.currentTime;
603
+ if (!currentTime)
604
+ return null;
605
+ const timeMs = Date.parse(currentTime.replace(' ', 'T'));
606
+ if (!Number.isFinite(timeMs))
607
+ return null;
608
+ const carrier = (_d = (_c = (_b = this._scene) === null || _b === void 0 ? void 0 : _b.tracker) === null || _c === void 0 ? void 0 : _c.trackingId) !== null && _d !== void 0 ? _d : '?';
609
+ const loc = this._carrierTrackingStore.lookupAt(timeMs);
610
+ this._lastFallbackResolvedAt = Date.now();
611
+ const logIfChanged = (target, msg) => {
612
+ if (target === this._lastFallbackTarget)
613
+ return;
614
+ // 이전 target 의 bouncing arrow 해제 + 새 target 에 부착 (가시성 강화).
615
+ // deco-border 는 things-scene Tracker 가 처리하므로 우리는 deco-arrow 만 관리.
616
+ this._setFallbackArrow(this._lastFallbackTarget, null);
617
+ this._setFallbackArrow(null, target);
618
+ this._lastFallbackTarget = target;
619
+ console.log(msg);
620
+ };
621
+ if (!loc) {
622
+ logIfChanged(null, `[CarrierTracking] fallback: history miss (carrier=${carrier}, t=${currentTime})`);
623
+ return null;
624
+ }
625
+ if (loc.machineName === 'NA') {
626
+ logIfChanged(null, `[CarrierTracking] fallback: NA (carrier=${carrier}, t=${currentTime})`);
627
+ return null;
628
+ }
629
+ // unitName 우선 (더 정밀), 없으면 machineName 으로 폴백.
630
+ // 같은 id 컴포넌트가 여러 개일 수 있어 (예: 머신 본체 + 부착된 게이지) findAllById
631
+ // 로 후보 모두 가져온 뒤, mcs-mouse-event-handler 의 클릭 dispatch 와 동일한
632
+ // 패턴으로 머신 본체로 수렴:
633
+ // - 'isLVComponent' 후보 선택
634
+ // - parentObject 가 있으면 그쪽으로 escalate (게이지·바 → 머신 본체)
635
+ // - 없으면 자기 자신 사용
636
+ const resolveLVById = (id) => {
637
+ var _a, _b;
638
+ const candidates = (((_a = scene === null || scene === void 0 ? void 0 : scene.findAllById) === null || _a === void 0 ? void 0 : _a.call(scene, id)) || []);
639
+ const c = candidates.find(c => 'isLVComponent' in c);
640
+ if (!c)
641
+ return null;
642
+ return (_b = c.parentObject) !== null && _b !== void 0 ? _b : c;
643
+ };
644
+ let target = null;
645
+ let hitVia = null;
646
+ if (loc.unitName) {
647
+ const t = resolveLVById(loc.unitName);
648
+ if (t) {
649
+ target = t;
650
+ hitVia = 'unit';
651
+ }
652
+ }
653
+ if (!target && loc.machineName) {
654
+ const t = resolveLVById(loc.machineName);
655
+ if (t) {
656
+ target = t;
657
+ hitVia = 'machine';
658
+ }
659
+ }
660
+ if (hitVia === 'unit') {
661
+ logIfChanged(target, `[CarrierTracking] fallback applied via unit=${loc.unitName} ` +
662
+ `(machine=${loc.machineName} not tried) (carrier=${carrier}, t=${currentTime})`);
663
+ }
664
+ else if (hitVia === 'machine') {
665
+ logIfChanged(target, `[CarrierTracking] fallback applied via machine=${loc.machineName} ` +
666
+ `(unit=${loc.unitName} miss) (carrier=${carrier}, t=${currentTime})`);
667
+ }
668
+ else {
669
+ logIfChanged(target, `[CarrierTracking] fallback: scene 미존재 — unit=${loc.unitName} (miss), ` +
670
+ `machine=${loc.machineName} (miss) (carrier=${carrier}, t=${currentTime})`);
671
+ }
672
+ return target;
673
+ }
674
+ /**
675
+ * deco-arrow 라이프사이클 — 이전 target 끄고 새 target 에 bouncing arrow 부착.
676
+ * 둘 다 null 가능 (해제만, 부착만).
677
+ */
678
+ _setFallbackArrow(off, on) {
679
+ var _a, _b, _c, _d;
680
+ if (off) {
681
+ try {
682
+ (_b = (_a = off).trigger) === null || _b === void 0 ? void 0 : _b.call(_a, 'deco-arrow-off');
683
+ }
684
+ catch (_e) { }
685
+ }
686
+ if (on) {
687
+ try {
688
+ ;
689
+ (_d = (_c = on).trigger) === null || _d === void 0 ? void 0 : _d.call(_c, 'deco-arrow', {
690
+ animation: true,
691
+ repeat: true,
692
+ size: 0.75,
693
+ // TRACKING_COLOR(#00aaff7f) 의 색조는 유지하되 알파(7f) 만 제거해 불투명.
694
+ color: BoardViewer_1.TRACKING_COLOR.slice(0, 7),
695
+ duration: 5000
696
+ });
697
+ }
698
+ catch (_f) { }
699
+ }
700
+ }
701
+ /**
702
+ * 1초 polling — 화살표 부착 대상을 결정.
703
+ * 1) 플레이백 종료 / 추적 ID 없음 → 화살표 해제
704
+ * 2) carrier 가 scene 에 직접 존재 → **carrier 자체에 화살표 부착**
705
+ * (things-scene Tracker 가 carrier 발견 시 fallback 콜백 호출을 멈춰
706
+ * resolver 경로로는 우리가 모름. monitor 에서 직접 책임)
707
+ * 3) carrier 없음 + resolver 가 3초 이상 호출 안 됨 → 화살표 해제
708
+ * (그 외엔 resolver 의 logIfChanged 가 fallback target 으로 부착 중)
709
+ */
710
+ _startFallbackArrowMonitor() {
711
+ if (this._fallbackArrowMonitor != null)
712
+ return;
713
+ this._fallbackArrowMonitor = window.setInterval(() => {
714
+ var _a, _b, _c, _d, _e;
715
+ // 1) 플레이백 종료 / 추적 ID 해제
716
+ const trackingId = (_b = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.tracker) === null || _b === void 0 ? void 0 : _b.trackingId;
717
+ if (!this._playbackActive || !trackingId) {
718
+ this._clearFallbackArrow();
719
+ return;
720
+ }
721
+ // 2) carrier 가 scene 에 직접 존재 — carrier(또는 parentObject)에 화살표 부착
722
+ const carrierComp = (((_d = (_c = this._scene) === null || _c === void 0 ? void 0 : _c.findAllById) === null || _d === void 0 ? void 0 : _d.call(_c, trackingId)) || [])
723
+ .find(c => 'isLVComponent' in c);
724
+ if (carrierComp) {
725
+ const arrowTarget = (_e = carrierComp.parentObject) !== null && _e !== void 0 ? _e : carrierComp;
726
+ if (arrowTarget !== this._lastFallbackTarget) {
727
+ this._setFallbackArrow(this._lastFallbackTarget, null);
728
+ this._setFallbackArrow(null, arrowTarget);
729
+ this._lastFallbackTarget = arrowTarget;
730
+ }
731
+ return;
732
+ }
733
+ // 3) resolver 가 일정 시간 호출되지 않음 — Tracker 가 추적 중단했을 가능성
734
+ if (this._lastFallbackTarget && Date.now() - this._lastFallbackResolvedAt > 3000) {
735
+ this._clearFallbackArrow();
736
+ }
737
+ }, 1000);
738
+ }
739
+ _stopFallbackArrowMonitor() {
740
+ if (this._fallbackArrowMonitor != null) {
741
+ clearInterval(this._fallbackArrowMonitor);
742
+ this._fallbackArrowMonitor = undefined;
743
+ }
744
+ }
745
+ /** 부착된 deco-arrow 강제 해제 + dedupe 상태 초기화. */
746
+ _clearFallbackArrow() {
747
+ if (this._lastFallbackTarget) {
748
+ this._setFallbackArrow(this._lastFallbackTarget, null);
749
+ this._lastFallbackTarget = null;
750
+ }
751
+ }
752
+ /**
753
+ * onSearchSubmit hook — 영구추적 carrierName 이 설정되는 순간.
754
+ * 플레이백 윈도우가 잡혀 있으면 즉시 store 충전. 윈도우가 없으면
755
+ * 플레이백 시작 시 (_onPlaybackStart) 에 충전.
756
+ */
757
+ _onCarrierTrackingSubmit(carrierName) {
758
+ this._clearFallbackArrow(); // 새 carrier — 기존 화살표/dedupe 정리
759
+ if (!carrierName) {
760
+ this._carrierTrackingStore.clear();
761
+ return;
762
+ }
763
+ const win = this._playbackWindow;
764
+ if (this._playbackActive && win) {
765
+ this._carrierTrackingStore.load(carrierName, win.from, win.to);
766
+ }
767
+ else {
768
+ // 플레이백 비활성 — 윈도우 정해질 때까지 대기. 캐시는 비워둠.
769
+ this._carrierTrackingStore.clear();
770
+ }
448
771
  }
449
772
  unbindSceneEvents(scene) {
450
773
  scene.off('run', this.onRunBoard, this);
@@ -522,34 +845,265 @@ let BoardViewer = class BoardViewer extends LitElement {
522
845
  togglefullscreen(this, () => this.requestUpdate(), () => this.requestUpdate());
523
846
  }
524
847
  onTapToggle3D() {
525
- if (!this._scene)
526
- return;
527
- const root = this._scene.root;
528
- if (!root)
848
+ var _a, _b, _c;
849
+ const modelLayer = (_b = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root) === null || _b === void 0 ? void 0 : _b.rootModel;
850
+ if (!(modelLayer === null || modelLayer === void 0 ? void 0 : modelLayer.toggleThreeD))
529
851
  return;
530
- root.set('threed', !this._is3dMode);
852
+ const entering3D = !modelLayer.is3dMode;
853
+ modelLayer.toggleThreeD();
531
854
  this.requestUpdate();
855
+ // fmsim: 3D 진입 시 자동 회전
856
+ if (entering3D && ((_c = modelLayer._threeCapability) === null || _c === void 0 ? void 0 : _c.isInitialized)) {
857
+ modelLayer._threeCapability.controlsManager.reset();
858
+ modelLayer._threeCapability.controlsManager.setAutoRotate(true);
859
+ }
532
860
  }
533
861
  onTapPlayback() {
862
+ var _a, _b;
863
+ // Flow-Replay 비활성 시 시작 경로 차단 (FAB 은 이미 숨겨지지만 방어적 가드).
864
+ if (!isFlowReplayEnabled() && !this._playbackActive)
865
+ return;
534
866
  if (this._playbackActive) {
535
- this._stopPlayback();
867
+ this._onPlaybackStop();
536
868
  }
537
869
  else {
870
+ // 플레이백 시작 시 팝업 닫기
871
+ this.hidePopup();
872
+ // 1) 라이브 데이터 구독 실제 종료
873
+ (_a = this._dataSubscriptionProvider) === null || _a === void 0 ? void 0 : _a.pause();
874
+ this._unsubscribeSceneData();
875
+ // 2) 라이브 데이터 초기화 (깨끗한 상태에서 playback 시작)
876
+ this._resetSceneData();
538
877
  this._playbackActive = true;
878
+ // playbackMode 를 "라이브 아님" 의미로 통일 — 준비 단계부터 set 해서
879
+ // FAB 클릭 직후 ~ Play 누르기 전 사이의 갭에서도 가드 (mouse handler,
880
+ // movement layer, restful subscription) 가 작동하도록.
881
+ if ((_b = this._scene) === null || _b === void 0 ? void 0 : _b.rootContainer)
882
+ this._scene.rootContainer.playbackMode = true;
883
+ if (!this._playbackEngine) {
884
+ this._playbackEngine = new PlaybackEngine();
885
+ this._playbackEngine.on('frame', ({ time }) => {
886
+ const controls = this.renderRoot.querySelector('ox-playback-controls');
887
+ if (controls)
888
+ controls.currentTime = time;
889
+ });
890
+ this._playbackEngine.on('state-change', (state) => {
891
+ if (state === 'stopped') {
892
+ const controls = this.renderRoot.querySelector('ox-playback-controls');
893
+ if (controls)
894
+ controls.playbackState = 'stopped';
895
+ }
896
+ this.requestUpdate();
897
+ });
898
+ this._playbackEngine.on('buffer-update', ({ bufferedRanges }) => {
899
+ const controls = this.renderRoot.querySelector('ox-playback-controls');
900
+ if (controls)
901
+ controls.bufferedRanges = bufferedRanges;
902
+ });
903
+ if (this._fetchPlaybackData) {
904
+ this._playbackEngine.setFetchFunction(this._fetchPlaybackData);
905
+ }
906
+ }
907
+ if (this._scene) {
908
+ this._playbackEngine.attachScene(this._scene);
909
+ }
539
910
  this.dispatchEvent(new CustomEvent('playback-toggle', {
540
911
  bubbles: true,
541
912
  composed: true,
542
913
  detail: { active: true }
543
914
  }));
915
+ // playback 은 전체화면에서만 동작 — 이미 전체화면이면 그대로 유지
916
+ if (!document.fullscreenElement && !isIOS()) {
917
+ fullscreen(this, () => this.requestUpdate(), () => this.requestUpdate());
918
+ }
919
+ }
920
+ }
921
+ async _onPlaybackStart(e) {
922
+ var _a, _b, _c;
923
+ const { fromTime, toTime, speed } = e.detail;
924
+ if (!this._playbackEngine)
925
+ return;
926
+ // toTime is the user-selected window END (= controls' _appliedTo).
927
+ // The previous hardcoded `fromTime + 1h` ignored the controls'
928
+ // range selection, so 10m / 30m / Range modes silently played past
929
+ // the requested window. Falling back to `+1h` only if toTime was
930
+ // omitted (legacy callers).
931
+ const endTime = toTime instanceof Date ? toTime : new Date(fromTime.getTime() + 3600000);
932
+ // playbackMode 는 onTapPlayback (준비 단계) 에서 이미 set — idempotent
933
+ // 재설정으로 안전망 유지.
934
+ if ((_a = this._scene) === null || _a === void 0 ? void 0 : _a.rootContainer)
935
+ this._scene.rootContainer.playbackMode = true;
936
+ // 플레이백 윈도우 기록 + 영구추적 활성 시 carrier history 충전.
937
+ // 윈도우 변경 (재시작) 시에도 같은 경로 — store.load 가 key 비교로
938
+ // 동일 (carrierName, from, to) 이면 no-op.
939
+ this._playbackWindow = { from: fromTime, to: endTime };
940
+ const carrierName = (_c = (_b = this._scene) === null || _b === void 0 ? void 0 : _b.tracker) === null || _c === void 0 ? void 0 : _c.trackingId;
941
+ if (carrierName) {
942
+ this._carrierTrackingStore.load(carrierName, fromTime, endTime);
943
+ }
944
+ this._startFallbackArrowMonitor();
945
+ try {
946
+ await this._playbackEngine.startChunked(fromTime, endTime, speed);
947
+ }
948
+ catch (ex) {
949
+ console.error('[Playback] load failed:', ex);
544
950
  }
545
951
  }
546
- _stopPlayback() {
952
+ _onPlaybackPause() {
953
+ var _a;
954
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.pause();
955
+ }
956
+ _onPlaybackResume() {
957
+ var _a;
958
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.resume();
959
+ }
960
+ async _onPlaybackStop() {
961
+ var _a, _b, _c;
962
+ const scene = this._scene; // 비동기 구간 scene 교체 감지용 캡처
963
+ // 1) 즉시: 엔진 정지 + 애니메이션 억제 ON (in-flight 애니메이션 즉시 종료)
964
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.detachScene();
965
+ (_b = this._playbackEngine) === null || _b === void 0 ? void 0 : _b.stop();
547
966
  this._playbackActive = false;
967
+ this._playbackWindow = undefined;
968
+ this._carrierTrackingStore.clear();
969
+ this._clearFallbackArrow();
970
+ this._stopFallbackArrowMonitor();
971
+ if (scene) {
972
+ scene.suppressAnimations = true;
973
+ if (scene.rootContainer)
974
+ scene.rootContainer.playbackMode = false;
975
+ }
976
+ // UI 신호 + fullscreen 해제
548
977
  this.dispatchEvent(new CustomEvent('playback-toggle', {
549
978
  bubbles: true,
550
979
  composed: true,
551
980
  detail: { active: false }
552
981
  }));
982
+ if (document.fullscreenElement) {
983
+ exitfullscreen();
984
+ }
985
+ // 2) 1초 후: 보드 리셋 + live 구독 재개
986
+ await new Promise(resolve => setTimeout(resolve, 1000));
987
+ if (this._playbackActive || this._scene !== scene)
988
+ return;
989
+ if (!scene)
990
+ return;
991
+ this._resetSceneData();
992
+ this._resubscribeSceneData();
993
+ (_c = this._dataSubscriptionProvider) === null || _c === void 0 ? void 0 : _c.resume();
994
+ // 3) 2초 후 (추가 1초): 억제 해제 — 첫 live batch 가 instant 배치된 뒤 정상 애니메이션 복원
995
+ await new Promise(resolve => setTimeout(resolve, 1000));
996
+ if (this._scene === scene) {
997
+ scene.suppressAnimations = false;
998
+ }
999
+ }
1000
+ /**
1001
+ * 플레이백 시작 시: RESTful data-subscription 컴포넌트의 WebSocket 을 실제 disconnect.
1002
+ * GraphQL 경로는 _dataSubscriptionProvider.pause() 에서 일괄 처리.
1003
+ */
1004
+ _unsubscribeSceneData() {
1005
+ var _a;
1006
+ const root = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root;
1007
+ if (!root || typeof root.traverse !== 'function')
1008
+ return;
1009
+ root.traverse((c) => {
1010
+ if (!c || c === root)
1011
+ return;
1012
+ // RESTful data-subscription 컴포넌트 — 자체 WebSocket disconnect
1013
+ if (c.subscription) {
1014
+ try {
1015
+ c.subscription.unsubscribe();
1016
+ }
1017
+ catch (_a) { }
1018
+ c.subscription = null;
1019
+ }
1020
+ if (c.client) {
1021
+ try {
1022
+ c.client.unsubscribeAll();
1023
+ c.client.close(true);
1024
+ }
1025
+ catch (_b) { }
1026
+ c.client = null;
1027
+ }
1028
+ });
1029
+ }
1030
+ /**
1031
+ * 플레이백 종료 시: RESTful data-subscription 컴포넌트의 WebSocket 을 재연결.
1032
+ * GraphQL 경로는 _dataSubscriptionProvider.resume() 에서 일괄 처리.
1033
+ */
1034
+ _resubscribeSceneData() {
1035
+ var _a;
1036
+ const root = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root;
1037
+ if (!root || typeof root.traverse !== 'function')
1038
+ return;
1039
+ root.traverse((c) => {
1040
+ if (!c || c === root)
1041
+ return;
1042
+ // RESTful data-subscription 컴포넌트 — WebSocket 재연결
1043
+ if (typeof c.requestData === 'function' && !c.client) {
1044
+ c.requestData();
1045
+ }
1046
+ });
1047
+ }
1048
+ _resetSceneData() {
1049
+ var _a, _b, _c, _d;
1050
+ const root = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root;
1051
+ if (!root || typeof root.traverse !== 'function')
1052
+ return;
1053
+ root.traverse((c) => {
1054
+ if (!c || c === root)
1055
+ return;
1056
+ // null 대신 {} 로 리셋 — typeof null === 'object' 함정 회피 + destructuring 안전 +
1057
+ // statusColor 등 data?.X 형태 getter 가 자연스럽게 default 로 폴백
1058
+ try {
1059
+ if ('data' in c)
1060
+ c.data = {};
1061
+ }
1062
+ catch (_a) { }
1063
+ // 차량 등의 위치 캐시 초기화 — 없으면 이전 waypoint 경로로 스냅백되는 잔상 발생
1064
+ if ('lastWaypoint' in c)
1065
+ c.lastWaypoint = undefined;
1066
+ });
1067
+ // vehicle 의 waypoint 이동 애니메이션 전체 중단
1068
+ // (Animation.stepAnimation 의 suppress 체크만으로는 WaypointHandler 의 animatorMap 이
1069
+ // 완전히 비워지지 않을 수 있어 명시적 reset)
1070
+ if ((_b = root.waypointHandler) === null || _b === void 0 ? void 0 : _b.reset)
1071
+ root.waypointHandler.reset();
1072
+ // 모든 decorator 제거 — bouncing-arrow/border-highlighting 같은 SVG 애니메이션 decorator 는
1073
+ // decorator-registry 가 붙잡고 있어 data=null 만으로는 제거되지 않음. 명시적 reset 필요.
1074
+ try {
1075
+ root.trigger('deco-reset');
1076
+ }
1077
+ catch (_e) { }
1078
+ if ((_c = root.carrierManager) === null || _c === void 0 ? void 0 : _c.dispose)
1079
+ root.carrierManager.dispose();
1080
+ if ((_d = root.transitionEffect) === null || _d === void 0 ? void 0 : _d.dispose)
1081
+ root.transitionEffect.dispose();
1082
+ }
1083
+ _onPlaybackSeek(e) {
1084
+ var _a;
1085
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.seekTo(e.detail.toTime);
1086
+ }
1087
+ /**
1088
+ * 드래그 시작 신호 — 엔진의 재생 타이머 정지해서 스크러빙 동안
1089
+ * scene 이 엔진 시간으로 독립 갱신되지 않도록 함. _state 는 유지되므로
1090
+ * 드래그 종료 시 seekTo 가 원래 재생/일시정지 상태로 복원.
1091
+ */
1092
+ _onPlaybackSeekStart() {
1093
+ var _a;
1094
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.pauseForSeek();
1095
+ }
1096
+ /**
1097
+ * 드래그 중 디바운스된 seek preview. 해당 시간의 snapshot 만 scene 에 적용.
1098
+ * 보드 리셋 없음, 애니메이션 억제.
1099
+ */
1100
+ _onPlaybackSeekPreview(e) {
1101
+ var _a;
1102
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.seekPreview(e.detail.toTime);
1103
+ }
1104
+ _onPlaybackSpeed(e) {
1105
+ var _a;
1106
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.setSpeed(e.detail.speed);
553
1107
  }
554
1108
  onRunBoard() {
555
1109
  this.dispatchEvent(new CustomEvent('run-board', { bubbles: true, composed: true, detail: this.board.id }));
@@ -643,6 +1197,10 @@ let BoardViewer = class BoardViewer extends LitElement {
643
1197
  this.popup = popup;
644
1198
  }
645
1199
  if (this.popup) {
1200
+ // playbackMode 를 component 보다 먼저 set — BoardComponentInfo 의 updated()
1201
+ // 가 component 변경을 보는 시점에 playbackMode 가 반드시 최신값이어야
1202
+ // detailInfo() 백엔드 호출 가드가 일관 동작.
1203
+ this.popup.playbackMode = this._playbackActive;
646
1204
  this.popup.component = component;
647
1205
  }
648
1206
  }
@@ -733,14 +1291,48 @@ BoardViewer.styles = [
733
1291
  }
734
1292
 
735
1293
  [fab-group] md-fab.sub-fab {
736
- --md-fab-container-width: 40px;
737
- --md-fab-container-height: 40px;
738
- --md-icon-size: 20px;
1294
+ --md-fab-container-width: 48px;
1295
+ --md-fab-container-height: 48px;
1296
+ /* fab 표면 색 = image 의 자체 원형 배경 (#F9EBEA) 과 일치.
1297
+ * 이렇게 하면 md-fab 의 내부 padding 영역도 같은 색이라
1298
+ * 시각적으로 사라짐 — 세 버튼 모두 visible 48px 컨테이너 통일. */
1299
+ --md-fab-container-color: #F9EBEA;
1300
+ --md-fab-icon-color: #F87171;
1301
+ --md-icon-size: 36px;
739
1302
  transform: scale(0);
740
1303
  opacity: 0;
741
1304
  pointer-events: none;
742
1305
  }
743
1306
 
1307
+ /* visible 영역은 컨테이너(48px) 그대로 — fullscreen 버튼과 동일.
1308
+ * img 자체는 48px 박스이고 transform scale 로 내부 그래픽만 확대.
1309
+ * wrap 의 overflow:hidden + border-radius:50% 가 fab 원형 밖으로
1310
+ * 넘치는 영역을 잘라내어 외형 크기는 변하지 않음. */
1311
+ [fab-group] md-fab.sub-fab .fab-icon-wrap {
1312
+ width: 48px;
1313
+ height: 48px;
1314
+ display: block;
1315
+ overflow: hidden;
1316
+ border-radius: 50%;
1317
+ }
1318
+
1319
+ [fab-group] md-fab.sub-fab .fab-icon-wrap img.fab-icon {
1320
+ width: 48px;
1321
+ height: 48px;
1322
+ display: block;
1323
+ transform: scale(1.15);
1324
+ transform-origin: center;
1325
+ }
1326
+
1327
+ /* legacy inline svg 경로 — 동일하게 box 48 유지 + scale 로 그래픽만 확대. */
1328
+ [fab-group] md-fab.sub-fab svg[slot="icon"] {
1329
+ width: 48px;
1330
+ height: 48px;
1331
+ display: block;
1332
+ transform: scale(1.15);
1333
+ transform-origin: center;
1334
+ }
1335
+
744
1336
  [fab-group][expanded] md-fab.sub-fab {
745
1337
  transform: scale(1);
746
1338
  opacity: 1;
@@ -785,6 +1377,9 @@ BoardViewer.styles = [
785
1377
  }
786
1378
  `
787
1379
  ];
1380
+ /** SearchOverlay 의 query-submit 시 사용할 추적 색깔. things-scene 의 Scene.tracker
1381
+ * 가 폴링/데코/3D outline 색 동기화 일괄 관리. */
1382
+ BoardViewer.TRACKING_COLOR = '#00aaff7f';
788
1383
  __decorate([
789
1384
  property({ type: String })
790
1385
  ], BoardViewer.prototype, "baseUrl", void 0);
@@ -800,27 +1395,15 @@ __decorate([
800
1395
  __decorate([
801
1396
  property({ type: Object })
802
1397
  ], BoardViewer.prototype, "values", void 0);
1398
+ __decorate([
1399
+ property({ type: Object })
1400
+ ], BoardViewer.prototype, "loadTracker", void 0);
803
1401
  __decorate([
804
1402
  property({ type: Boolean, reflect: true, attribute: 'hide-fullscreen' })
805
1403
  ], BoardViewer.prototype, "hideFullscreen", void 0);
806
1404
  __decorate([
807
1405
  property({ type: Boolean, reflect: true, attribute: 'hide-navigation' })
808
1406
  ], BoardViewer.prototype, "hideNavigation", void 0);
809
- __decorate([
810
- state()
811
- ], BoardViewer.prototype, "_scene", void 0);
812
- __decorate([
813
- state()
814
- ], BoardViewer.prototype, "_forward", void 0);
815
- __decorate([
816
- state()
817
- ], BoardViewer.prototype, "_backward", void 0);
818
- __decorate([
819
- state()
820
- ], BoardViewer.prototype, "_oldtarget", void 0);
821
- __decorate([
822
- state()
823
- ], BoardViewer.prototype, "_fade_animations", void 0);
824
1407
  __decorate([
825
1408
  state()
826
1409
  ], BoardViewer.prototype, "_fabExpanded", void 0);
@@ -839,7 +1422,10 @@ __decorate([
839
1422
  __decorate([
840
1423
  query('[fab-group]')
841
1424
  ], BoardViewer.prototype, "_fabGroup", void 0);
842
- BoardViewer = __decorate([
1425
+ __decorate([
1426
+ property({ attribute: false })
1427
+ ], BoardViewer.prototype, "_fetchPlaybackData", void 0);
1428
+ BoardViewer = BoardViewer_1 = __decorate([
843
1429
  customElement('ox-board-viewer')
844
1430
  ], BoardViewer);
845
1431
  export { BoardViewer };