@fmsim/board 2.0.0-beta.7 → 2.0.0-beta.70

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 (68) hide show
  1. package/custom-elements.json +3381 -1419
  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/component/etc.js +35 -0
  7. package/dist/src/component/etc.js.map +1 -1
  8. package/dist/src/component/index.d.ts +1 -0
  9. package/dist/src/component/index.js +1 -0
  10. package/dist/src/component/index.js.map +1 -1
  11. package/dist/src/component/restful-property-editor-backdrop-faces.d.ts +50 -0
  12. package/dist/src/component/restful-property-editor-backdrop-faces.js +524 -0
  13. package/dist/src/component/restful-property-editor-backdrop-faces.js.map +1 -0
  14. package/dist/src/data-storage/board-model-cache.d.ts +13 -0
  15. package/dist/src/data-storage/board-model-cache.js +66 -0
  16. package/dist/src/data-storage/board-model-cache.js.map +1 -0
  17. package/dist/src/index.d.ts +3 -0
  18. package/dist/src/index.js +2 -0
  19. package/dist/src/index.js.map +1 -1
  20. package/dist/src/layers/movement-layer.js +2 -4
  21. package/dist/src/layers/movement-layer.js.map +1 -1
  22. package/dist/src/modeller/edit-toolbar-style.js +43 -3
  23. package/dist/src/modeller/edit-toolbar-style.js.map +1 -1
  24. package/dist/src/modeller/edit-toolbar.d.ts +8 -14
  25. package/dist/src/modeller/edit-toolbar.js +208 -211
  26. package/dist/src/modeller/edit-toolbar.js.map +1 -1
  27. package/dist/src/modeller/property-sidebar/abstract-property.js +10 -2
  28. package/dist/src/modeller/property-sidebar/abstract-property.js.map +1 -1
  29. package/dist/src/modeller/property-sidebar/shapes/shapes.js +31 -0
  30. package/dist/src/modeller/property-sidebar/shapes/shapes.js.map +1 -1
  31. package/dist/src/modeller/property-sidebar/specifics/specifics.d.ts +1 -0
  32. package/dist/src/modeller/property-sidebar/specifics/specifics.js +10 -0
  33. package/dist/src/modeller/property-sidebar/specifics/specifics.js.map +1 -1
  34. package/dist/src/modeller/scene-viewer/ox-scene-viewer.d.ts +2 -1
  35. package/dist/src/modeller/scene-viewer/ox-scene-viewer.js +5 -1
  36. package/dist/src/modeller/scene-viewer/ox-scene-viewer.js.map +1 -1
  37. package/dist/src/ox-board-component-info.d.ts +1 -0
  38. package/dist/src/ox-board-component-info.js +5 -1
  39. package/dist/src/ox-board-component-info.js.map +1 -1
  40. package/dist/src/ox-board-list.js +2 -2
  41. package/dist/src/ox-board-list.js.map +1 -1
  42. package/dist/src/ox-board-modeller.d.ts +5 -2
  43. package/dist/src/ox-board-modeller.js +14 -4
  44. package/dist/src/ox-board-modeller.js.map +1 -1
  45. package/dist/src/ox-board-viewer.d.ts +60 -13
  46. package/dist/src/ox-board-viewer.js +439 -86
  47. package/dist/src/ox-board-viewer.js.map +1 -1
  48. package/dist/src/ox-editor-board-selector.d.ts +3 -0
  49. package/dist/src/ox-editor-board-selector.js +38 -9
  50. package/dist/src/ox-editor-board-selector.js.map +1 -1
  51. package/dist/src/ox-playback-controls.d.ts +154 -0
  52. package/dist/src/ox-playback-controls.js +947 -0
  53. package/dist/src/ox-playback-controls.js.map +1 -0
  54. package/dist/src/playback/playback-engine.d.ts +106 -0
  55. package/dist/src/playback/playback-engine.js +395 -0
  56. package/dist/src/playback/playback-engine.js.map +1 -0
  57. package/dist/src/selector/board-creation-popup.js +2 -2
  58. package/dist/src/selector/board-creation-popup.js.map +1 -1
  59. package/dist/src/selector/board-thumbnail-card.js +6 -6
  60. package/dist/src/selector/board-thumbnail-card.js.map +1 -1
  61. package/dist/src/selector/ox-board-creation-card.js +2 -2
  62. package/dist/src/selector/ox-board-creation-card.js.map +1 -1
  63. package/dist/src/selector/ox-board-selector.js +21 -10
  64. package/dist/src/selector/ox-board-selector.js.map +1 -1
  65. package/dist/tsconfig.tsbuildinfo +1 -1
  66. package/icons/components/text.png +0 -0
  67. package/icons/icon-htoolbar.png +0 -0
  68. package/package.json +8 -4
@@ -4,10 +4,12 @@ import '@material/web/icon/icon.js';
4
4
  import './layers/movement-layer.js';
5
5
  import './ox-board-component-info.js';
6
6
  import './layers/mcs-mouse-event-handler.js';
7
- import { css, html, LitElement } from 'lit';
7
+ import './ox-playback-controls.js';
8
+ import { css, html, LitElement, nothing } from 'lit';
8
9
  import { customElement, property, query, state } from 'lit/decorators.js';
9
- import { create, SCENE_MODE } from '@hatiolab/things-scene';
10
- import { isIOS, togglefullscreen } from '@operato/utils';
10
+ import { PlaybackEngine } from './playback/playback-engine.js';
11
+ import { create, LoadTracker, LoadingOverlay, SCENE_MODE } from '@hatiolab/things-scene';
12
+ import { isIOS, fullscreen, exitfullscreen, togglefullscreen } from '@operato/utils';
11
13
  import { ScrollbarStyles } from '@operato/styles';
12
14
  import { DataStorage } from './data-storage/data-storage.js';
13
15
  import { runScenario, startScenario, DataSubscriptionProviderImpl, isRestfulAvailable } from '@fmsim/api';
@@ -24,6 +26,8 @@ let BoardViewer = class BoardViewer extends LitElement {
24
26
  this._forward = [];
25
27
  this._backward = [];
26
28
  this._oldtarget = null;
29
+ this._fabExpanded = false;
30
+ this._playbackActive = false;
27
31
  this.currentBoardId = (_a = this.board) === null || _a === void 0 ? void 0 : _a.id;
28
32
  this.popup = null;
29
33
  this.lastComponent = null;
@@ -33,7 +37,6 @@ let BoardViewer = class BoardViewer extends LitElement {
33
37
  const id = e.detail.value;
34
38
  const components = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.findAll(`#${id}`);
35
39
  if (!components || components.length == 0) {
36
- console.log('No equipment was found.');
37
40
  return;
38
41
  }
39
42
  window.dispatchEvent(new CustomEvent('clickComponent', { detail: components[0] }));
@@ -173,22 +176,63 @@ let BoardViewer = class BoardViewer extends LitElement {
173
176
  var _a;
174
177
  (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root.trigger('deco-reset');
175
178
  };
179
+ this._pointerMoveHandler = () => this.transientShowButtons();
180
+ /**
181
+ * playback 모드는 전체화면 모드와 한 몸 — 어떤 경로로든 전체화면이 해제되면
182
+ * playback 도 함께 종료. (단독 전체화면 토글은 기존처럼 동작하도록 `_playbackActive` 가드.)
183
+ */
184
+ this._fullscreenChangeHandler = () => {
185
+ if (!document.fullscreenElement && this._playbackActive) {
186
+ this._onPlaybackStop();
187
+ }
188
+ };
176
189
  this.resizeHandler = () => {
177
190
  this._scene && this._scene.fit();
178
191
  };
192
+ // ── Data Subscription ──
193
+ this._dataSubscriptionProvider = isRestfulAvailable() ? undefined : new DataSubscriptionProviderImpl();
194
+ }
195
+ get _is3dMode() {
196
+ var _a, _b, _c;
197
+ 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);
179
198
  }
180
199
  render() {
181
- var fullscreen = !isIOS() && !this.hideFullscreen
182
- ? html `
183
- <md-fab
184
- id="fullscreen"
185
- @click=${(e) => this.onTapFullscreen()}
200
+ var _a, _b;
201
+ var fabGroup = this.hideFullscreen
202
+ ? html ``
203
+ : html `
204
+ <div
205
+ fab-group
206
+ ?expanded=${this._fabExpanded}
186
207
  @mouseover=${(e) => this.transientShowButtons(true)}
187
208
  @mouseout=${(e) => this.transientShowButtons()}
188
- title="fullscreen"
189
- ><md-icon slot="icon">${document.fullscreenElement ? 'fullscreen_exit' : 'fullscreen'}</md-icon></md-fab>
190
- `
191
- : html ``;
209
+ >
210
+ <md-fab
211
+ class="main-fab"
212
+ ?expanded=${this._fabExpanded}
213
+ @click=${() => { this._fabExpanded = !this._fabExpanded; }}
214
+ title="menu"
215
+ ><md-icon slot="icon">add</md-icon></md-fab>
216
+
217
+ ${isIOS() ? nothing : html `<md-fab
218
+ class="sub-fab"
219
+ @click=${() => { this._fabExpanded = false; this.onTapFullscreen(); }}
220
+ title="fullscreen"
221
+ ><md-icon slot="icon">${document.fullscreenElement ? 'fullscreen_exit' : 'fullscreen'}</md-icon></md-fab>`}
222
+
223
+ <md-fab
224
+ class="sub-fab"
225
+ @click=${() => { this._fabExpanded = false; this.onTapPlayback(); }}
226
+ title=${this._playbackActive ? 'stop playback' : 'start playback'}
227
+ ><md-icon slot="icon">${this._playbackActive ? 'motion_photos_paused' : 'motion_play'}</md-icon></md-fab>
228
+
229
+ <md-fab
230
+ class="sub-fab"
231
+ @click=${() => { this._fabExpanded = false; this.onTapToggle3D(); }}
232
+ title=${this._is3dMode ? '2D mode' : '3D mode'}
233
+ ><md-icon slot="icon">${this._is3dMode ? 'view_in_ar' : '3d_rotation'}</md-icon></md-fab>
234
+ </div>
235
+ `;
192
236
  var prev = !this.hideNavigation
193
237
  ? html `
194
238
  <md-icon
@@ -216,74 +260,94 @@ let BoardViewer = class BoardViewer extends LitElement {
216
260
  return html `
217
261
  ${prev}
218
262
 
219
- <div
220
- id="target"
221
- @touchstart=${(e) => this.transientShowButtons()}
222
- @mousemove=${(e) => this.transientShowButtons()}
223
- ></div>
263
+ <div id="target"></div>
224
264
 
225
265
  <slot></slot>
226
- ${next} ${fullscreen}
266
+ ${next} ${fabGroup}
267
+
268
+ ${this._playbackActive
269
+ ? html `<ox-playback-controls
270
+ .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'}
271
+ @playback-start=${this._onPlaybackStart}
272
+ @playback-pause=${this._onPlaybackPause}
273
+ @playback-resume=${this._onPlaybackResume}
274
+ @playback-stop=${this._onPlaybackStop}
275
+ @playback-seek=${this._onPlaybackSeek}
276
+ @playback-seek-start=${this._onPlaybackSeekStart}
277
+ @playback-seek-preview=${this._onPlaybackSeekPreview}
278
+ @playback-speed=${this._onPlaybackSpeed}
279
+ ></ox-playback-controls>`
280
+ : nothing}
227
281
  `;
228
282
  }
283
+ /** connect/disconnect 시 일괄 등록/해제할 window 이벤트 목록 (모니터링 연동) */
284
+ get _monitoringEvents() {
285
+ return [
286
+ ['fetchIFactsData', this.fetchIFactsDataHandler], // 장비 아이디 object를 캔버스 가운데로
287
+ ['fmbShowMovement', this.fmbShowMovementHandler], // Movement Data 표시
288
+ ['fmbMonitoringInit', this.fmbMonitoringInitHandler], // 보드 상태 정사이즈 복원
289
+ ['clickComponent', this.clickComponentHandler], // BoardComponentInfo popup
290
+ ['fmbStartAnimation', this.fmbStartAnimation], // AnimatedIcon 추가/제거
291
+ ['fmbStopAnimation', this.fmbStopAnimation],
292
+ ['fmbResetAnimation', this.fmbResetAnimation],
293
+ ['fmbShowLegend', this.fmbShowLegendHandler], // 범례 표시
294
+ ['fmbHideLegend', this.fmbHideLegendHandler],
295
+ ['fmbShowPopup', this.fmbShowPopupHandler], // 팝업 표시
296
+ ['fmbHidePopup', this.fmbHidePopupHandler],
297
+ ['fmbShowFloatingContainer', this.fmbShowFloatingContainerHandler], // FloatingContainer
298
+ ['fmbHideFloatingContainer', this.fmbHideFloatingContainerHandler]
299
+ ];
300
+ }
229
301
  connectedCallback() {
230
302
  super.connectedCallback();
231
303
  window.addEventListener('resize', this.resizeHandler);
304
+ // 3D 모드에서도 마우스 움직임 감지 (document 레벨, capture phase)
305
+ document.addEventListener('pointermove', this._pointerMoveHandler, true);
306
+ document.addEventListener('fullscreenchange', this._fullscreenChangeHandler);
307
+ document.addEventListener('webkitfullscreenchange', this._fullscreenChangeHandler);
232
308
  this.renderRoot.addEventListener('close-scene', e => {
233
309
  e.preventDefault();
234
310
  this.onTapPrev();
235
311
  }, false);
236
- // 모니터링에서 장비 아이디에 해당하는 object를 찾아 캔버스 가운데 위치하도록 조정한다.
237
- window.addEventListener('fetchIFactsData', this.fetchIFactsDataHandler);
238
- // 모니터링에서 Movement Data를 보여준다.
239
- window.addEventListener('fmbShowMovement', this.fmbShowMovementHandler);
240
- // 모니터링에서 보드 상태를 원래상태 (정사이즈)로 돌려놓는다.
241
- window.addEventListener('fmbMonitoringInit', this.fmbMonitoringInitHandler);
242
- // BoardComponentInfo popup을 보여준다.
243
- window.addEventListener('clickComponent', this.clickComponentHandler);
244
- // AnimatedIcon 을 추가하거나 제거한다.
245
- window.addEventListener('fmbStartAnimation', this.fmbStartAnimation);
246
- window.addEventListener('fmbStopAnimation', this.fmbStopAnimation);
247
- window.addEventListener('fmbResetAnimation', this.fmbResetAnimation);
248
- // 모니터링에서 범례를 보여준다.
249
- window.addEventListener('fmbShowLegend', this.fmbShowLegendHandler);
250
- window.addEventListener('fmbHideLegend', this.fmbHideLegendHandler);
251
- // 모니터링에서 범례를 보여준다.
252
- window.addEventListener('fmbShowPopup', this.fmbShowPopupHandler);
253
- window.addEventListener('fmbHidePopup', this.fmbHidePopupHandler);
254
- // 모니터링에서 FloatingContainer를 보여준다.
255
- window.addEventListener('fmbShowFloatingContainer', this.fmbShowFloatingContainerHandler);
256
- window.addEventListener('fmbHideFloatingContainer', this.fmbHideFloatingContainerHandler);
312
+ for (const [type, handler] of this._monitoringEvents) {
313
+ window.addEventListener(type, handler);
314
+ }
257
315
  }
258
316
  disconnectedCallback() {
317
+ var _a, _b, _c;
259
318
  super.disconnectedCallback();
260
319
  window.removeEventListener('resize', this.resizeHandler);
261
- // 모니터링에서 장비 아이디에 해당하는 object를 찾아 캔버스 가운데 위치하도록 조정한다.
262
- window.removeEventListener('fetchIFactsData', this.fetchIFactsDataHandler);
263
- // 모니터링에서 Movement Data를 보여준다.
264
- window.removeEventListener('fmbShowMovement', this.fmbShowMovementHandler);
265
- // 모니터링에서 보드 상태를 원래상태 (정사이즈)로 돌려놓는다.
266
- window.removeEventListener('fmbMonitoringInit', this.fmbMonitoringInitHandler);
267
- // BoardComponentInfo popup을 보여준다.
268
- window.removeEventListener('clickComponent', this.clickComponentHandler);
269
- // AnimatedIcon을 추가하거나 제거한다.
270
- window.removeEventListener('fmbStartAnimation', this.fmbStartAnimation);
271
- window.removeEventListener('fmbStopAnimation', this.fmbStopAnimation);
272
- window.removeEventListener('fmbResetAnimation', this.fmbResetAnimation);
273
- // 모니터링에서 범례를 보여준다.
274
- window.removeEventListener('fmbShowLegend', this.fmbShowLegendHandler);
275
- window.removeEventListener('fmbHideLegend', this.fmbHideLegendHandler);
276
- // 모니터링에서 범례를 보여준다.
277
- window.removeEventListener('fmbShowPopup', this.fmbShowPopupHandler);
278
- window.removeEventListener('fmbHidePopup', this.fmbHidePopupHandler);
279
- // 모니터링에서 FloatingContainer를 보여준다.
280
- window.removeEventListener('fmbShowFloatingContainer', this.fmbShowFloatingContainerHandler);
281
- window.removeEventListener('fmbHideFloatingContainer', this.fmbHideFloatingContainerHandler);
320
+ document.removeEventListener('pointermove', this._pointerMoveHandler, true);
321
+ document.removeEventListener('fullscreenchange', this._fullscreenChangeHandler);
322
+ document.removeEventListener('webkitfullscreenchange', this._fullscreenChangeHandler);
323
+ for (const [type, handler] of this._monitoringEvents) {
324
+ window.removeEventListener(type, handler);
325
+ }
282
326
  this.lastComponent = null;
327
+ // 플레이백이 활성 상태면 명시적 stop (provider resume 포함)
328
+ if (this._playbackActive) {
329
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.stop();
330
+ (_b = this._playbackEngine) === null || _b === void 0 ? void 0 : _b.detachScene();
331
+ (_c = this._dataSubscriptionProvider) === null || _c === void 0 ? void 0 : _c.resume();
332
+ this._playbackActive = false;
333
+ }
334
+ if (this._playbackEngine) {
335
+ this._playbackEngine.dispose();
336
+ this._playbackEngine = undefined;
337
+ }
283
338
  this.closeScene();
284
339
  }
285
340
  updated(changes) {
341
+ // loadTracker가 외부에서 전달되면 즉시 overlay 연결 (fetch 중에도 표시)
342
+ if (changes.has('loadTracker') && this.loadTracker) {
343
+ this._ensureOverlay(this.loadTracker);
344
+ }
286
345
  if (changes.has('board')) {
346
+ // tracker: 외부에서 안 왔으면 여기서 생성
347
+ if (!this.loadTracker) {
348
+ this.loadTracker = new LoadTracker();
349
+ this._ensureOverlay(this.loadTracker);
350
+ }
287
351
  this.hidePopup();
288
352
  this.closeScene();
289
353
  if (this.board && this.board.id) {
@@ -303,32 +367,42 @@ let BoardViewer = class BoardViewer extends LitElement {
303
367
  }
304
368
  }
305
369
  async initSceneAsync() {
306
- if (!this.board || !this.board.id)
370
+ if (!this.board || !this.board.id || !this.provider)
307
371
  return;
308
- this._scene = await this.provider.create(this.board.id);
309
- this.setupScene({ id: this.board.id, scene: this._scene });
372
+ try {
373
+ this._scene = await this.provider.create(this.board.id);
374
+ this.setupScene({ id: this.board.id, scene: this._scene });
375
+ }
376
+ catch (e) {
377
+ console.error('Failed to load scene:', this.board.id, e);
378
+ }
310
379
  }
311
380
  initScene() {
312
- if (!this.board || !this.board.id) {
381
+ if (!this.board || !this.board.id || !this.board.model) {
313
382
  return;
314
383
  }
315
384
  this._scene = create({
316
- model: {
317
- ...this.board.model
318
- },
385
+ model: JSON.parse(JSON.stringify(this.board.model)),
319
386
  style: this.board.style,
320
387
  mode: SCENE_MODE.VIEW,
321
388
  handlers: ['mcs-event-handler', 'zoom-handler', 'shift-handler', 'waypoint-handler'],
322
389
  layers: [{ type: 'scroll-layer' }, { type: 'movement-layer', id: 'movement-layer' }],
323
390
  refProvider: this.provider,
324
391
  dataStorage: this.board.id !== 'preview' ? new DataStorage(this.board.id) : undefined,
325
- dataSubscriptionProvider: isRestfulAvailable() ? undefined : new DataSubscriptionProviderImpl()
392
+ dataSubscriptionProvider: this._dataSubscriptionProvider,
393
+ loading: this.loadTracker ? { tracker: this.loadTracker } : undefined
326
394
  });
327
395
  if (this.baseUrl) {
328
396
  this._scene.baseUrl = this.baseUrl;
329
397
  }
330
398
  this.setupScene({ id: this.board.id, scene: this._scene });
331
399
  }
400
+ _ensureOverlay(tracker) {
401
+ if (!this._loadingOverlay) {
402
+ this._loadingOverlay = new LoadingOverlay();
403
+ }
404
+ this._loadingOverlay.attach(this.renderRoot, tracker);
405
+ }
332
406
  closeScene() {
333
407
  if (this._scene) {
334
408
  this.unbindSceneEvents(this._scene);
@@ -348,6 +422,10 @@ let BoardViewer = class BoardViewer extends LitElement {
348
422
  }
349
423
  setupScene({ id, scene }) {
350
424
  var _a;
425
+ // 플레이백 리셋
426
+ if (this._playbackActive) {
427
+ this._onPlaybackStop();
428
+ }
351
429
  this._scene = scene;
352
430
  const backgroundColor = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root.state.fillStyle;
353
431
  if (typeof backgroundColor === 'string') {
@@ -371,7 +449,7 @@ let BoardViewer = class BoardViewer extends LitElement {
371
449
  this.transientShowButtons();
372
450
  }
373
451
  async showScene(boardId, bindingData) {
374
- if (!boardId)
452
+ if (!boardId || !this.provider)
375
453
  return;
376
454
  try {
377
455
  var scene = await this.provider.get(boardId, true);
@@ -424,7 +502,7 @@ let BoardViewer = class BoardViewer extends LitElement {
424
502
  transientShowButtons(stop) {
425
503
  var buttons = [];
426
504
  !this.hideNavigation && buttons.push(this._next, this._prev);
427
- !this.hideFullscreen && buttons.push(this._fullscreen);
505
+ !this.hideFullscreen && buttons.push(this._fabGroup);
428
506
  if (buttons.length == 0) {
429
507
  return;
430
508
  }
@@ -447,7 +525,7 @@ let BoardViewer = class BoardViewer extends LitElement {
447
525
  }
448
526
  this._forward.length <= 0 ? this._next.setAttribute('hidden', '') : this._next.removeAttribute('hidden');
449
527
  this._backward.length <= 0 ? this._prev.setAttribute('hidden', '') : this._prev.removeAttribute('hidden');
450
- this._fullscreen && this._fullscreen.removeAttribute('hidden');
528
+ this._fabGroup && this._fabGroup.removeAttribute('hidden');
451
529
  this._fade_animations.forEach(animation => {
452
530
  animation.cancel();
453
531
  if (stop)
@@ -485,6 +563,242 @@ let BoardViewer = class BoardViewer extends LitElement {
485
563
  onTapFullscreen() {
486
564
  togglefullscreen(this, () => this.requestUpdate(), () => this.requestUpdate());
487
565
  }
566
+ onTapToggle3D() {
567
+ var _a, _b, _c;
568
+ const modelLayer = (_b = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root) === null || _b === void 0 ? void 0 : _b.rootModel;
569
+ if (!(modelLayer === null || modelLayer === void 0 ? void 0 : modelLayer.toggleThreeD))
570
+ return;
571
+ const entering3D = !modelLayer.is3dMode;
572
+ modelLayer.toggleThreeD();
573
+ this.requestUpdate();
574
+ // fmsim: 3D 진입 시 자동 회전
575
+ if (entering3D && ((_c = modelLayer._threeCapability) === null || _c === void 0 ? void 0 : _c.isInitialized)) {
576
+ modelLayer._threeCapability.controlsManager.reset();
577
+ modelLayer._threeCapability.controlsManager.setAutoRotate(true);
578
+ }
579
+ }
580
+ onTapPlayback() {
581
+ var _a;
582
+ if (this._playbackActive) {
583
+ this._onPlaybackStop();
584
+ }
585
+ else {
586
+ // 플레이백 시작 시 팝업 닫기
587
+ this.hidePopup();
588
+ // 1) 라이브 데이터 구독 실제 종료
589
+ (_a = this._dataSubscriptionProvider) === null || _a === void 0 ? void 0 : _a.pause();
590
+ this._unsubscribeSceneData();
591
+ // 2) 라이브 데이터 초기화 (깨끗한 상태에서 playback 시작)
592
+ this._resetSceneData();
593
+ this._playbackActive = true;
594
+ if (!this._playbackEngine) {
595
+ this._playbackEngine = new PlaybackEngine();
596
+ this._playbackEngine.on('frame', ({ time }) => {
597
+ const controls = this.renderRoot.querySelector('ox-playback-controls');
598
+ if (controls)
599
+ controls.currentTime = time;
600
+ });
601
+ this._playbackEngine.on('state-change', (state) => {
602
+ if (state === 'stopped') {
603
+ const controls = this.renderRoot.querySelector('ox-playback-controls');
604
+ if (controls)
605
+ controls.playbackState = 'stopped';
606
+ }
607
+ this.requestUpdate();
608
+ });
609
+ this._playbackEngine.on('buffer-update', ({ bufferedRanges }) => {
610
+ const controls = this.renderRoot.querySelector('ox-playback-controls');
611
+ if (controls)
612
+ controls.bufferedRanges = bufferedRanges;
613
+ });
614
+ if (this._fetchPlaybackData) {
615
+ this._playbackEngine.setFetchFunction(this._fetchPlaybackData);
616
+ }
617
+ }
618
+ if (this._scene) {
619
+ this._playbackEngine.attachScene(this._scene);
620
+ }
621
+ this.dispatchEvent(new CustomEvent('playback-toggle', {
622
+ bubbles: true,
623
+ composed: true,
624
+ detail: { active: true }
625
+ }));
626
+ // playback 은 전체화면에서만 동작 — 이미 전체화면이면 그대로 유지
627
+ if (!document.fullscreenElement && !isIOS()) {
628
+ fullscreen(this, () => this.requestUpdate(), () => this.requestUpdate());
629
+ }
630
+ }
631
+ }
632
+ async _onPlaybackStart(e) {
633
+ const { fromTime, toTime, speed } = e.detail;
634
+ if (!this._playbackEngine)
635
+ return;
636
+ // toTime is the user-selected window END (= controls' _appliedTo).
637
+ // The previous hardcoded `fromTime + 1h` ignored the controls'
638
+ // range selection, so 10m / 30m / Range modes silently played past
639
+ // the requested window. Falling back to `+1h` only if toTime was
640
+ // omitted (legacy callers).
641
+ const endTime = toTime instanceof Date ? toTime : new Date(fromTime.getTime() + 3600000);
642
+ if (this._scene)
643
+ this._scene.playbackMode = true;
644
+ try {
645
+ await this._playbackEngine.startChunked(fromTime, endTime, speed);
646
+ }
647
+ catch (ex) {
648
+ console.error('[Playback] load failed:', ex);
649
+ }
650
+ }
651
+ _onPlaybackPause() {
652
+ var _a;
653
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.pause();
654
+ }
655
+ _onPlaybackResume() {
656
+ var _a;
657
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.resume();
658
+ }
659
+ async _onPlaybackStop() {
660
+ var _a, _b, _c;
661
+ const scene = this._scene; // 비동기 구간 scene 교체 감지용 캡처
662
+ // 1) 즉시: 엔진 정지 + 애니메이션 억제 ON (in-flight 애니메이션 즉시 종료)
663
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.detachScene();
664
+ (_b = this._playbackEngine) === null || _b === void 0 ? void 0 : _b.stop();
665
+ this._playbackActive = false;
666
+ if (scene) {
667
+ scene.suppressAnimations = true;
668
+ scene.playbackMode = false;
669
+ }
670
+ // UI 신호 + fullscreen 해제
671
+ this.dispatchEvent(new CustomEvent('playback-toggle', {
672
+ bubbles: true,
673
+ composed: true,
674
+ detail: { active: false }
675
+ }));
676
+ if (document.fullscreenElement) {
677
+ exitfullscreen();
678
+ }
679
+ // 2) 1초 후: 보드 리셋 + live 구독 재개
680
+ await new Promise(resolve => setTimeout(resolve, 1000));
681
+ if (this._playbackActive || this._scene !== scene)
682
+ return;
683
+ if (!scene)
684
+ return;
685
+ this._resetSceneData();
686
+ this._resubscribeSceneData();
687
+ (_c = this._dataSubscriptionProvider) === null || _c === void 0 ? void 0 : _c.resume();
688
+ // 3) 2초 후 (추가 1초): 억제 해제 — 첫 live batch 가 instant 배치된 뒤 정상 애니메이션 복원
689
+ await new Promise(resolve => setTimeout(resolve, 1000));
690
+ if (this._scene === scene) {
691
+ scene.suppressAnimations = false;
692
+ }
693
+ }
694
+ /**
695
+ * 플레이백 시작 시: RESTful data-subscription 컴포넌트의 WebSocket 을 실제 disconnect.
696
+ * GraphQL 경로는 _dataSubscriptionProvider.pause() 에서 일괄 처리.
697
+ */
698
+ _unsubscribeSceneData() {
699
+ var _a;
700
+ const root = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root;
701
+ if (!root || typeof root.traverse !== 'function')
702
+ return;
703
+ root.traverse((c) => {
704
+ if (!c || c === root)
705
+ return;
706
+ // RESTful data-subscription 컴포넌트 — 자체 WebSocket disconnect
707
+ if (c.subscription) {
708
+ try {
709
+ c.subscription.unsubscribe();
710
+ }
711
+ catch (_a) { }
712
+ c.subscription = null;
713
+ }
714
+ if (c.client) {
715
+ try {
716
+ c.client.unsubscribeAll();
717
+ c.client.close(true);
718
+ }
719
+ catch (_b) { }
720
+ c.client = null;
721
+ }
722
+ });
723
+ }
724
+ /**
725
+ * 플레이백 종료 시: RESTful data-subscription 컴포넌트의 WebSocket 을 재연결.
726
+ * GraphQL 경로는 _dataSubscriptionProvider.resume() 에서 일괄 처리.
727
+ */
728
+ _resubscribeSceneData() {
729
+ var _a;
730
+ const root = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root;
731
+ if (!root || typeof root.traverse !== 'function')
732
+ return;
733
+ root.traverse((c) => {
734
+ if (!c || c === root)
735
+ return;
736
+ // RESTful data-subscription 컴포넌트 — WebSocket 재연결
737
+ if (typeof c.requestData === 'function' && !c.client) {
738
+ c.requestData();
739
+ }
740
+ });
741
+ }
742
+ _resetSceneData() {
743
+ var _a, _b, _c, _d;
744
+ const root = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root;
745
+ if (!root || typeof root.traverse !== 'function')
746
+ return;
747
+ root.traverse((c) => {
748
+ if (!c || c === root)
749
+ return;
750
+ // null 대신 {} 로 리셋 — typeof null === 'object' 함정 회피 + destructuring 안전 +
751
+ // statusColor 등 data?.X 형태 getter 가 자연스럽게 default 로 폴백
752
+ try {
753
+ if ('data' in c)
754
+ c.data = {};
755
+ }
756
+ catch (_a) { }
757
+ // 차량 등의 위치 캐시 초기화 — 없으면 이전 waypoint 경로로 스냅백되는 잔상 발생
758
+ if ('lastWaypoint' in c)
759
+ c.lastWaypoint = undefined;
760
+ });
761
+ // vehicle 의 waypoint 이동 애니메이션 전체 중단
762
+ // (Animation.stepAnimation 의 suppress 체크만으로는 WaypointHandler 의 animatorMap 이
763
+ // 완전히 비워지지 않을 수 있어 명시적 reset)
764
+ if ((_b = root.waypointHandler) === null || _b === void 0 ? void 0 : _b.reset)
765
+ root.waypointHandler.reset();
766
+ // 모든 decorator 제거 — bouncing-arrow/border-highlighting 같은 SVG 애니메이션 decorator 는
767
+ // decorator-registry 가 붙잡고 있어 data=null 만으로는 제거되지 않음. 명시적 reset 필요.
768
+ try {
769
+ root.trigger('deco-reset');
770
+ }
771
+ catch (_e) { }
772
+ if ((_c = root.carrierManager) === null || _c === void 0 ? void 0 : _c.dispose)
773
+ root.carrierManager.dispose();
774
+ if ((_d = root.transitionEffect) === null || _d === void 0 ? void 0 : _d.dispose)
775
+ root.transitionEffect.dispose();
776
+ }
777
+ _onPlaybackSeek(e) {
778
+ var _a;
779
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.seekTo(e.detail.toTime);
780
+ }
781
+ /**
782
+ * 드래그 시작 신호 — 엔진의 재생 타이머 정지해서 스크러빙 동안
783
+ * scene 이 엔진 시간으로 독립 갱신되지 않도록 함. _state 는 유지되므로
784
+ * 드래그 종료 시 seekTo 가 원래 재생/일시정지 상태로 복원.
785
+ */
786
+ _onPlaybackSeekStart() {
787
+ var _a;
788
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.pauseForSeek();
789
+ }
790
+ /**
791
+ * 드래그 중 디바운스된 seek preview. 해당 시간의 snapshot 만 scene 에 적용.
792
+ * 보드 리셋 없음, 애니메이션 억제.
793
+ */
794
+ _onPlaybackSeekPreview(e) {
795
+ var _a;
796
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.seekPreview(e.detail.toTime);
797
+ }
798
+ _onPlaybackSpeed(e) {
799
+ var _a;
800
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.setSpeed(e.detail.speed);
801
+ }
488
802
  onRunBoard() {
489
803
  this.dispatchEvent(new CustomEvent('run-board', { bubbles: true, composed: true, detail: this.board.id }));
490
804
  }
@@ -578,6 +892,7 @@ let BoardViewer = class BoardViewer extends LitElement {
578
892
  }
579
893
  if (this.popup) {
580
894
  this.popup.component = component;
895
+ this.popup.playbackMode = this._playbackActive;
581
896
  }
582
897
  }
583
898
  hidePopup() {
@@ -643,10 +958,51 @@ BoardViewer.styles = [
643
958
  right: 5px;
644
959
  }
645
960
 
646
- #fullscreen {
961
+ [fab-group] {
647
962
  position: absolute;
648
963
  bottom: 15px;
649
964
  right: 16px;
965
+ display: flex;
966
+ flex-direction: column-reverse;
967
+ align-items: center;
968
+ gap: 12px;
969
+ z-index: 1000;
970
+ }
971
+
972
+ [fab-group] md-fab {
973
+ transition: transform 0.2s ease, opacity 0.2s ease;
974
+ }
975
+
976
+ [fab-group] md-fab.main-fab md-icon {
977
+ transition: transform 0.3s ease;
978
+ }
979
+
980
+ [fab-group] md-fab.main-fab[expanded] md-icon {
981
+ transform: rotate(45deg);
982
+ }
983
+
984
+ [fab-group] md-fab.sub-fab {
985
+ --md-fab-container-width: 40px;
986
+ --md-fab-container-height: 40px;
987
+ --md-icon-size: 20px;
988
+ transform: scale(0);
989
+ opacity: 0;
990
+ pointer-events: none;
991
+ }
992
+
993
+ [fab-group][expanded] md-fab.sub-fab {
994
+ transform: scale(1);
995
+ opacity: 1;
996
+ pointer-events: auto;
997
+ }
998
+
999
+ [fab-group] md-fab.sub-fab[disabled] {
1000
+ opacity: 0.4;
1001
+ pointer-events: none;
1002
+ }
1003
+
1004
+ ox-playback-controls {
1005
+ transition: opacity 0.3s ease;
650
1006
  }
651
1007
 
652
1008
  [hidden] {
@@ -693,6 +1049,9 @@ __decorate([
693
1049
  __decorate([
694
1050
  property({ type: Object })
695
1051
  ], BoardViewer.prototype, "values", void 0);
1052
+ __decorate([
1053
+ property({ type: Object })
1054
+ ], BoardViewer.prototype, "loadTracker", void 0);
696
1055
  __decorate([
697
1056
  property({ type: Boolean, reflect: true, attribute: 'hide-fullscreen' })
698
1057
  ], BoardViewer.prototype, "hideFullscreen", void 0);
@@ -701,19 +1060,10 @@ __decorate([
701
1060
  ], BoardViewer.prototype, "hideNavigation", void 0);
702
1061
  __decorate([
703
1062
  state()
704
- ], BoardViewer.prototype, "_scene", void 0);
1063
+ ], BoardViewer.prototype, "_fabExpanded", void 0);
705
1064
  __decorate([
706
1065
  state()
707
- ], BoardViewer.prototype, "_forward", void 0);
708
- __decorate([
709
- state()
710
- ], BoardViewer.prototype, "_backward", void 0);
711
- __decorate([
712
- state()
713
- ], BoardViewer.prototype, "_oldtarget", void 0);
714
- __decorate([
715
- state()
716
- ], BoardViewer.prototype, "_fade_animations", void 0);
1066
+ ], BoardViewer.prototype, "_playbackActive", void 0);
717
1067
  __decorate([
718
1068
  query('#target')
719
1069
  ], BoardViewer.prototype, "_target", void 0);
@@ -724,8 +1074,11 @@ __decorate([
724
1074
  query('#next')
725
1075
  ], BoardViewer.prototype, "_next", void 0);
726
1076
  __decorate([
727
- query('#fullscreen')
728
- ], BoardViewer.prototype, "_fullscreen", void 0);
1077
+ query('[fab-group]')
1078
+ ], BoardViewer.prototype, "_fabGroup", void 0);
1079
+ __decorate([
1080
+ property({ attribute: false })
1081
+ ], BoardViewer.prototype, "_fetchPlaybackData", void 0);
729
1082
  BoardViewer = __decorate([
730
1083
  customElement('ox-board-viewer')
731
1084
  ], BoardViewer);