@fmsim/board 2.0.0-beta.6 → 2.0.0-beta.60

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 (60) hide show
  1. package/custom-elements.json +3868 -2431
  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/data-storage/board-model-cache.d.ts +13 -0
  9. package/dist/src/data-storage/board-model-cache.js +66 -0
  10. package/dist/src/data-storage/board-model-cache.js.map +1 -0
  11. package/dist/src/index.d.ts +3 -0
  12. package/dist/src/index.js +2 -0
  13. package/dist/src/index.js.map +1 -1
  14. package/dist/src/layers/movement-layer.js +2 -4
  15. package/dist/src/layers/movement-layer.js.map +1 -1
  16. package/dist/src/modeller/edit-toolbar-style.js +43 -3
  17. package/dist/src/modeller/edit-toolbar-style.js.map +1 -1
  18. package/dist/src/modeller/edit-toolbar.d.ts +8 -14
  19. package/dist/src/modeller/edit-toolbar.js +208 -211
  20. package/dist/src/modeller/edit-toolbar.js.map +1 -1
  21. package/dist/src/modeller/property-sidebar/shapes/shapes.js +31 -0
  22. package/dist/src/modeller/property-sidebar/shapes/shapes.js.map +1 -1
  23. package/dist/src/modeller/property-sidebar/specifics/specifics.d.ts +1 -0
  24. package/dist/src/modeller/property-sidebar/specifics/specifics.js +10 -0
  25. package/dist/src/modeller/property-sidebar/specifics/specifics.js.map +1 -1
  26. package/dist/src/modeller/scene-viewer/ox-scene-viewer.d.ts +2 -1
  27. package/dist/src/modeller/scene-viewer/ox-scene-viewer.js +5 -1
  28. package/dist/src/modeller/scene-viewer/ox-scene-viewer.js.map +1 -1
  29. package/dist/src/ox-board-component-info.d.ts +1 -0
  30. package/dist/src/ox-board-component-info.js +5 -1
  31. package/dist/src/ox-board-component-info.js.map +1 -1
  32. package/dist/src/ox-board-list.js +2 -2
  33. package/dist/src/ox-board-list.js.map +1 -1
  34. package/dist/src/ox-board-modeller.d.ts +5 -2
  35. package/dist/src/ox-board-modeller.js +14 -4
  36. package/dist/src/ox-board-modeller.js.map +1 -1
  37. package/dist/src/ox-board-viewer.d.ts +41 -13
  38. package/dist/src/ox-board-viewer.js +363 -85
  39. package/dist/src/ox-board-viewer.js.map +1 -1
  40. package/dist/src/ox-editor-board-selector.d.ts +3 -0
  41. package/dist/src/ox-editor-board-selector.js +38 -9
  42. package/dist/src/ox-editor-board-selector.js.map +1 -1
  43. package/dist/src/ox-playback-controls.d.ts +67 -0
  44. package/dist/src/ox-playback-controls.js +594 -0
  45. package/dist/src/ox-playback-controls.js.map +1 -0
  46. package/dist/src/playback/playback-engine.d.ts +95 -0
  47. package/dist/src/playback/playback-engine.js +368 -0
  48. package/dist/src/playback/playback-engine.js.map +1 -0
  49. package/dist/src/selector/board-creation-popup.js +2 -2
  50. package/dist/src/selector/board-creation-popup.js.map +1 -1
  51. package/dist/src/selector/board-thumbnail-card.js +6 -6
  52. package/dist/src/selector/board-thumbnail-card.js.map +1 -1
  53. package/dist/src/selector/ox-board-creation-card.js +2 -2
  54. package/dist/src/selector/ox-board-creation-card.js.map +1 -1
  55. package/dist/src/selector/ox-board-selector.js +21 -10
  56. package/dist/src/selector/ox-board-selector.js.map +1 -1
  57. package/dist/tsconfig.tsbuildinfo +1 -1
  58. package/icons/components/text.png +0 -0
  59. package/icons/icon-htoolbar.png +0 -0
  60. package/package.json +3 -3
@@ -4,9 +4,11 @@ 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 { PlaybackEngine } from './playback/playback-engine.js';
11
+ import { create, LoadTracker, LoadingOverlay, SCENE_MODE } from '@hatiolab/things-scene';
10
12
  import { isIOS, togglefullscreen } from '@operato/utils';
11
13
  import { ScrollbarStyles } from '@operato/styles';
12
14
  import { DataStorage } from './data-storage/data-storage.js';
@@ -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,54 @@ 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();
176
180
  this.resizeHandler = () => {
177
181
  this._scene && this._scene.fit();
178
182
  };
183
+ // ── Data Subscription ──
184
+ this._dataSubscriptionProvider = isRestfulAvailable() ? undefined : new DataSubscriptionProviderImpl();
185
+ }
186
+ get _is3dMode() {
187
+ var _a, _b, _c;
188
+ 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
189
  }
180
190
  render() {
181
- var fullscreen = !isIOS() && !this.hideFullscreen
182
- ? html `
183
- <md-fab
184
- id="fullscreen"
185
- @click=${(e) => this.onTapFullscreen()}
191
+ var _a, _b;
192
+ var fabGroup = this.hideFullscreen
193
+ ? html ``
194
+ : html `
195
+ <div
196
+ fab-group
197
+ ?expanded=${this._fabExpanded}
186
198
  @mouseover=${(e) => this.transientShowButtons(true)}
187
199
  @mouseout=${(e) => this.transientShowButtons()}
188
- title="fullscreen"
189
- ><md-icon slot="icon">${document.fullscreenElement ? 'fullscreen_exit' : 'fullscreen'}</md-icon></md-fab>
190
- `
191
- : html ``;
200
+ >
201
+ <md-fab
202
+ class="main-fab"
203
+ ?expanded=${this._fabExpanded}
204
+ @click=${() => { this._fabExpanded = !this._fabExpanded; }}
205
+ title="menu"
206
+ ><md-icon slot="icon">add</md-icon></md-fab>
207
+
208
+ ${isIOS() ? nothing : html `<md-fab
209
+ class="sub-fab"
210
+ @click=${() => { this._fabExpanded = false; this.onTapFullscreen(); }}
211
+ title="fullscreen"
212
+ ><md-icon slot="icon">${document.fullscreenElement ? 'fullscreen_exit' : 'fullscreen'}</md-icon></md-fab>`}
213
+
214
+ <md-fab
215
+ class="sub-fab"
216
+ @click=${() => { this._fabExpanded = false; this.onTapPlayback(); }}
217
+ title=${this._playbackActive ? 'stop playback' : 'start playback'}
218
+ ><md-icon slot="icon">${this._playbackActive ? 'stop' : 'replay'}</md-icon></md-fab>
219
+
220
+ <md-fab
221
+ class="sub-fab"
222
+ @click=${() => { this._fabExpanded = false; this.onTapToggle3D(); }}
223
+ title=${this._is3dMode ? '2D mode' : '3D mode'}
224
+ ><md-icon slot="icon">${this._is3dMode ? 'view_in_ar' : '3d_rotation'}</md-icon></md-fab>
225
+ </div>
226
+ `;
192
227
  var prev = !this.hideNavigation
193
228
  ? html `
194
229
  <md-icon
@@ -216,74 +251,88 @@ let BoardViewer = class BoardViewer extends LitElement {
216
251
  return html `
217
252
  ${prev}
218
253
 
219
- <div
220
- id="target"
221
- @touchstart=${(e) => this.transientShowButtons()}
222
- @mousemove=${(e) => this.transientShowButtons()}
223
- ></div>
254
+ <div id="target"></div>
224
255
 
225
256
  <slot></slot>
226
- ${next} ${fullscreen}
257
+ ${next} ${fabGroup}
258
+
259
+ ${this._playbackActive
260
+ ? html `<ox-playback-controls
261
+ .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'}
262
+ @playback-start=${this._onPlaybackStart}
263
+ @playback-pause=${this._onPlaybackPause}
264
+ @playback-resume=${this._onPlaybackResume}
265
+ @playback-stop=${this._onPlaybackStop}
266
+ @playback-seek=${this._onPlaybackSeek}
267
+ @playback-speed=${this._onPlaybackSpeed}
268
+ ></ox-playback-controls>`
269
+ : nothing}
227
270
  `;
228
271
  }
272
+ /** connect/disconnect 시 일괄 등록/해제할 window 이벤트 목록 (모니터링 연동) */
273
+ get _monitoringEvents() {
274
+ return [
275
+ ['fetchIFactsData', this.fetchIFactsDataHandler], // 장비 아이디 object를 캔버스 가운데로
276
+ ['fmbShowMovement', this.fmbShowMovementHandler], // Movement Data 표시
277
+ ['fmbMonitoringInit', this.fmbMonitoringInitHandler], // 보드 상태 정사이즈 복원
278
+ ['clickComponent', this.clickComponentHandler], // BoardComponentInfo popup
279
+ ['fmbStartAnimation', this.fmbStartAnimation], // AnimatedIcon 추가/제거
280
+ ['fmbStopAnimation', this.fmbStopAnimation],
281
+ ['fmbResetAnimation', this.fmbResetAnimation],
282
+ ['fmbShowLegend', this.fmbShowLegendHandler], // 범례 표시
283
+ ['fmbHideLegend', this.fmbHideLegendHandler],
284
+ ['fmbShowPopup', this.fmbShowPopupHandler], // 팝업 표시
285
+ ['fmbHidePopup', this.fmbHidePopupHandler],
286
+ ['fmbShowFloatingContainer', this.fmbShowFloatingContainerHandler], // FloatingContainer
287
+ ['fmbHideFloatingContainer', this.fmbHideFloatingContainerHandler]
288
+ ];
289
+ }
229
290
  connectedCallback() {
230
291
  super.connectedCallback();
231
292
  window.addEventListener('resize', this.resizeHandler);
293
+ // 3D 모드에서도 마우스 움직임 감지 (document 레벨, capture phase)
294
+ document.addEventListener('pointermove', this._pointerMoveHandler, true);
232
295
  this.renderRoot.addEventListener('close-scene', e => {
233
296
  e.preventDefault();
234
297
  this.onTapPrev();
235
298
  }, 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);
299
+ for (const [type, handler] of this._monitoringEvents) {
300
+ window.addEventListener(type, handler);
301
+ }
257
302
  }
258
303
  disconnectedCallback() {
304
+ var _a, _b, _c;
259
305
  super.disconnectedCallback();
260
306
  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);
307
+ document.removeEventListener('pointermove', this._pointerMoveHandler, true);
308
+ for (const [type, handler] of this._monitoringEvents) {
309
+ window.removeEventListener(type, handler);
310
+ }
282
311
  this.lastComponent = null;
312
+ // 플레이백이 활성 상태면 명시적 stop (provider resume 포함)
313
+ if (this._playbackActive) {
314
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.stop();
315
+ (_b = this._playbackEngine) === null || _b === void 0 ? void 0 : _b.detachScene();
316
+ (_c = this._dataSubscriptionProvider) === null || _c === void 0 ? void 0 : _c.resume();
317
+ this._playbackActive = false;
318
+ }
319
+ if (this._playbackEngine) {
320
+ this._playbackEngine.dispose();
321
+ this._playbackEngine = undefined;
322
+ }
283
323
  this.closeScene();
284
324
  }
285
325
  updated(changes) {
326
+ // loadTracker가 외부에서 전달되면 즉시 overlay 연결 (fetch 중에도 표시)
327
+ if (changes.has('loadTracker') && this.loadTracker) {
328
+ this._ensureOverlay(this.loadTracker);
329
+ }
286
330
  if (changes.has('board')) {
331
+ // tracker: 외부에서 안 왔으면 여기서 생성
332
+ if (!this.loadTracker) {
333
+ this.loadTracker = new LoadTracker();
334
+ this._ensureOverlay(this.loadTracker);
335
+ }
287
336
  this.hidePopup();
288
337
  this.closeScene();
289
338
  if (this.board && this.board.id) {
@@ -303,32 +352,42 @@ let BoardViewer = class BoardViewer extends LitElement {
303
352
  }
304
353
  }
305
354
  async initSceneAsync() {
306
- if (!this.board || !this.board.id)
355
+ if (!this.board || !this.board.id || !this.provider)
307
356
  return;
308
- this._scene = await this.provider.create(this.board.id);
309
- this.setupScene({ id: this.board.id, scene: this._scene });
357
+ try {
358
+ this._scene = await this.provider.create(this.board.id);
359
+ this.setupScene({ id: this.board.id, scene: this._scene });
360
+ }
361
+ catch (e) {
362
+ console.error('Failed to load scene:', this.board.id, e);
363
+ }
310
364
  }
311
365
  initScene() {
312
- if (!this.board || !this.board.id) {
366
+ if (!this.board || !this.board.id || !this.board.model) {
313
367
  return;
314
368
  }
315
369
  this._scene = create({
316
- model: {
317
- ...this.board.model
318
- },
370
+ model: JSON.parse(JSON.stringify(this.board.model)),
319
371
  style: this.board.style,
320
372
  mode: SCENE_MODE.VIEW,
321
373
  handlers: ['mcs-event-handler', 'zoom-handler', 'shift-handler', 'waypoint-handler'],
322
374
  layers: [{ type: 'scroll-layer' }, { type: 'movement-layer', id: 'movement-layer' }],
323
375
  refProvider: this.provider,
324
376
  dataStorage: this.board.id !== 'preview' ? new DataStorage(this.board.id) : undefined,
325
- dataSubscriptionProvider: isRestfulAvailable() ? undefined : new DataSubscriptionProviderImpl()
377
+ dataSubscriptionProvider: this._dataSubscriptionProvider,
378
+ loading: this.loadTracker ? { tracker: this.loadTracker } : undefined
326
379
  });
327
380
  if (this.baseUrl) {
328
381
  this._scene.baseUrl = this.baseUrl;
329
382
  }
330
383
  this.setupScene({ id: this.board.id, scene: this._scene });
331
384
  }
385
+ _ensureOverlay(tracker) {
386
+ if (!this._loadingOverlay) {
387
+ this._loadingOverlay = new LoadingOverlay();
388
+ }
389
+ this._loadingOverlay.attach(this.renderRoot, tracker);
390
+ }
332
391
  closeScene() {
333
392
  if (this._scene) {
334
393
  this.unbindSceneEvents(this._scene);
@@ -348,6 +407,10 @@ let BoardViewer = class BoardViewer extends LitElement {
348
407
  }
349
408
  setupScene({ id, scene }) {
350
409
  var _a;
410
+ // 플레이백 리셋
411
+ if (this._playbackActive) {
412
+ this._onPlaybackStop();
413
+ }
351
414
  this._scene = scene;
352
415
  const backgroundColor = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root.state.fillStyle;
353
416
  if (typeof backgroundColor === 'string') {
@@ -371,7 +434,7 @@ let BoardViewer = class BoardViewer extends LitElement {
371
434
  this.transientShowButtons();
372
435
  }
373
436
  async showScene(boardId, bindingData) {
374
- if (!boardId)
437
+ if (!boardId || !this.provider)
375
438
  return;
376
439
  try {
377
440
  var scene = await this.provider.get(boardId, true);
@@ -424,7 +487,7 @@ let BoardViewer = class BoardViewer extends LitElement {
424
487
  transientShowButtons(stop) {
425
488
  var buttons = [];
426
489
  !this.hideNavigation && buttons.push(this._next, this._prev);
427
- !this.hideFullscreen && buttons.push(this._fullscreen);
490
+ !this.hideFullscreen && buttons.push(this._fabGroup);
428
491
  if (buttons.length == 0) {
429
492
  return;
430
493
  }
@@ -447,7 +510,7 @@ let BoardViewer = class BoardViewer extends LitElement {
447
510
  }
448
511
  this._forward.length <= 0 ? this._next.setAttribute('hidden', '') : this._next.removeAttribute('hidden');
449
512
  this._backward.length <= 0 ? this._prev.setAttribute('hidden', '') : this._prev.removeAttribute('hidden');
450
- this._fullscreen && this._fullscreen.removeAttribute('hidden');
513
+ this._fabGroup && this._fabGroup.removeAttribute('hidden');
451
514
  this._fade_animations.forEach(animation => {
452
515
  animation.cancel();
453
516
  if (stop)
@@ -485,6 +548,182 @@ let BoardViewer = class BoardViewer extends LitElement {
485
548
  onTapFullscreen() {
486
549
  togglefullscreen(this, () => this.requestUpdate(), () => this.requestUpdate());
487
550
  }
551
+ onTapToggle3D() {
552
+ var _a, _b, _c;
553
+ const modelLayer = (_b = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root) === null || _b === void 0 ? void 0 : _b.rootModel;
554
+ if (!(modelLayer === null || modelLayer === void 0 ? void 0 : modelLayer.toggleThreeD))
555
+ return;
556
+ const entering3D = !modelLayer.is3dMode;
557
+ modelLayer.toggleThreeD();
558
+ this.requestUpdate();
559
+ // fmsim: 3D 진입 시 자동 회전
560
+ if (entering3D && ((_c = modelLayer._threeCapability) === null || _c === void 0 ? void 0 : _c.isInitialized)) {
561
+ modelLayer._threeCapability.controlsManager.reset();
562
+ modelLayer._threeCapability.controlsManager.setAutoRotate(true);
563
+ }
564
+ }
565
+ onTapPlayback() {
566
+ var _a;
567
+ if (this._playbackActive) {
568
+ this._onPlaybackStop();
569
+ }
570
+ else {
571
+ // 플레이백 시작 시 팝업 닫기
572
+ this.hidePopup();
573
+ // 1) 라이브 데이터 구독 실제 종료
574
+ (_a = this._dataSubscriptionProvider) === null || _a === void 0 ? void 0 : _a.pause();
575
+ this._unsubscribeSceneData();
576
+ // 2) 라이브 데이터 초기화 (깨끗한 상태에서 playback 시작)
577
+ this._resetSceneData();
578
+ this._playbackActive = true;
579
+ if (!this._playbackEngine) {
580
+ this._playbackEngine = new PlaybackEngine();
581
+ this._playbackEngine.on('frame', ({ time }) => {
582
+ const controls = this.renderRoot.querySelector('ox-playback-controls');
583
+ if (controls)
584
+ controls.currentTime = time;
585
+ });
586
+ this._playbackEngine.on('state-change', (state) => {
587
+ if (state === 'stopped') {
588
+ const controls = this.renderRoot.querySelector('ox-playback-controls');
589
+ if (controls)
590
+ controls.playbackState = 'stopped';
591
+ }
592
+ this.requestUpdate();
593
+ });
594
+ this._playbackEngine.on('buffer-update', ({ bufferedRanges }) => {
595
+ const controls = this.renderRoot.querySelector('ox-playback-controls');
596
+ if (controls)
597
+ controls.bufferedRanges = bufferedRanges;
598
+ });
599
+ if (this._fetchPlaybackData) {
600
+ this._playbackEngine.setFetchFunction(this._fetchPlaybackData);
601
+ }
602
+ }
603
+ if (this._scene) {
604
+ this._playbackEngine.attachScene(this._scene);
605
+ }
606
+ this.dispatchEvent(new CustomEvent('playback-toggle', {
607
+ bubbles: true,
608
+ composed: true,
609
+ detail: { active: true }
610
+ }));
611
+ }
612
+ }
613
+ async _onPlaybackStart(e) {
614
+ const { fromTime, speed } = e.detail;
615
+ if (!this._playbackEngine)
616
+ return;
617
+ const endTime = new Date(fromTime.getTime() + 3600000);
618
+ if (this._scene)
619
+ this._scene.playbackMode = true;
620
+ try {
621
+ await this._playbackEngine.startChunked(fromTime, endTime, speed);
622
+ }
623
+ catch (ex) {
624
+ console.error('[Playback] load failed:', ex);
625
+ }
626
+ }
627
+ _onPlaybackPause() {
628
+ var _a;
629
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.pause();
630
+ }
631
+ _onPlaybackResume() {
632
+ var _a;
633
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.resume();
634
+ }
635
+ _onPlaybackStop() {
636
+ var _a, _b, _c;
637
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.stop();
638
+ (_b = this._playbackEngine) === null || _b === void 0 ? void 0 : _b.detachScene();
639
+ this._playbackActive = false;
640
+ // playback 데이터 초기화 + playbackMode 해제 + 라이브 데이터 구독 재개
641
+ if (this._scene)
642
+ this._scene.playbackMode = false;
643
+ this._resetSceneData();
644
+ this._resubscribeSceneData();
645
+ (_c = this._dataSubscriptionProvider) === null || _c === void 0 ? void 0 : _c.resume();
646
+ this.dispatchEvent(new CustomEvent('playback-toggle', {
647
+ bubbles: true,
648
+ composed: true,
649
+ detail: { active: false }
650
+ }));
651
+ }
652
+ /**
653
+ * 플레이백 시작 시: RESTful data-subscription 컴포넌트의 WebSocket 을 실제 disconnect.
654
+ * GraphQL 경로는 _dataSubscriptionProvider.pause() 에서 일괄 처리.
655
+ */
656
+ _unsubscribeSceneData() {
657
+ var _a;
658
+ const root = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root;
659
+ if (!root || typeof root.traverse !== 'function')
660
+ return;
661
+ root.traverse((c) => {
662
+ if (!c || c === root)
663
+ return;
664
+ // RESTful data-subscription 컴포넌트 — 자체 WebSocket disconnect
665
+ if (c.subscription) {
666
+ try {
667
+ c.subscription.unsubscribe();
668
+ }
669
+ catch (_a) { }
670
+ c.subscription = null;
671
+ }
672
+ if (c.client) {
673
+ try {
674
+ c.client.unsubscribeAll();
675
+ c.client.close(true);
676
+ }
677
+ catch (_b) { }
678
+ c.client = null;
679
+ }
680
+ });
681
+ }
682
+ /**
683
+ * 플레이백 종료 시: RESTful data-subscription 컴포넌트의 WebSocket 을 재연결.
684
+ * GraphQL 경로는 _dataSubscriptionProvider.resume() 에서 일괄 처리.
685
+ */
686
+ _resubscribeSceneData() {
687
+ var _a;
688
+ const root = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root;
689
+ if (!root || typeof root.traverse !== 'function')
690
+ return;
691
+ root.traverse((c) => {
692
+ if (!c || c === root)
693
+ return;
694
+ // RESTful data-subscription 컴포넌트 — WebSocket 재연결
695
+ if (typeof c.requestData === 'function' && !c.client) {
696
+ c.requestData();
697
+ }
698
+ });
699
+ }
700
+ _resetSceneData() {
701
+ var _a, _b, _c;
702
+ const root = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root;
703
+ if (!root || typeof root.traverse !== 'function')
704
+ return;
705
+ root.traverse((c) => {
706
+ if (!c || c === root)
707
+ return;
708
+ try {
709
+ if ('data' in c)
710
+ c.data = null;
711
+ }
712
+ catch (_a) { }
713
+ });
714
+ if ((_b = root.carrierManager) === null || _b === void 0 ? void 0 : _b.dispose)
715
+ root.carrierManager.dispose();
716
+ if ((_c = root.transitionEffect) === null || _c === void 0 ? void 0 : _c.dispose)
717
+ root.transitionEffect.dispose();
718
+ }
719
+ _onPlaybackSeek(e) {
720
+ var _a;
721
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.seekTo(e.detail.toTime);
722
+ }
723
+ _onPlaybackSpeed(e) {
724
+ var _a;
725
+ (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.setSpeed(e.detail.speed);
726
+ }
488
727
  onRunBoard() {
489
728
  this.dispatchEvent(new CustomEvent('run-board', { bubbles: true, composed: true, detail: this.board.id }));
490
729
  }
@@ -578,6 +817,7 @@ let BoardViewer = class BoardViewer extends LitElement {
578
817
  }
579
818
  if (this.popup) {
580
819
  this.popup.component = component;
820
+ this.popup.playbackMode = this._playbackActive;
581
821
  }
582
822
  }
583
823
  hidePopup() {
@@ -643,10 +883,51 @@ BoardViewer.styles = [
643
883
  right: 5px;
644
884
  }
645
885
 
646
- #fullscreen {
886
+ [fab-group] {
647
887
  position: absolute;
648
888
  bottom: 15px;
649
889
  right: 16px;
890
+ display: flex;
891
+ flex-direction: column-reverse;
892
+ align-items: center;
893
+ gap: 12px;
894
+ z-index: 1000;
895
+ }
896
+
897
+ [fab-group] md-fab {
898
+ transition: transform 0.2s ease, opacity 0.2s ease;
899
+ }
900
+
901
+ [fab-group] md-fab.main-fab md-icon {
902
+ transition: transform 0.3s ease;
903
+ }
904
+
905
+ [fab-group] md-fab.main-fab[expanded] md-icon {
906
+ transform: rotate(45deg);
907
+ }
908
+
909
+ [fab-group] md-fab.sub-fab {
910
+ --md-fab-container-width: 40px;
911
+ --md-fab-container-height: 40px;
912
+ --md-icon-size: 20px;
913
+ transform: scale(0);
914
+ opacity: 0;
915
+ pointer-events: none;
916
+ }
917
+
918
+ [fab-group][expanded] md-fab.sub-fab {
919
+ transform: scale(1);
920
+ opacity: 1;
921
+ pointer-events: auto;
922
+ }
923
+
924
+ [fab-group] md-fab.sub-fab[disabled] {
925
+ opacity: 0.4;
926
+ pointer-events: none;
927
+ }
928
+
929
+ ox-playback-controls {
930
+ transition: opacity 0.3s ease;
650
931
  }
651
932
 
652
933
  [hidden] {
@@ -693,6 +974,9 @@ __decorate([
693
974
  __decorate([
694
975
  property({ type: Object })
695
976
  ], BoardViewer.prototype, "values", void 0);
977
+ __decorate([
978
+ property({ type: Object })
979
+ ], BoardViewer.prototype, "loadTracker", void 0);
696
980
  __decorate([
697
981
  property({ type: Boolean, reflect: true, attribute: 'hide-fullscreen' })
698
982
  ], BoardViewer.prototype, "hideFullscreen", void 0);
@@ -701,19 +985,10 @@ __decorate([
701
985
  ], BoardViewer.prototype, "hideNavigation", void 0);
702
986
  __decorate([
703
987
  state()
704
- ], BoardViewer.prototype, "_scene", void 0);
988
+ ], BoardViewer.prototype, "_fabExpanded", void 0);
705
989
  __decorate([
706
990
  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);
991
+ ], BoardViewer.prototype, "_playbackActive", void 0);
717
992
  __decorate([
718
993
  query('#target')
719
994
  ], BoardViewer.prototype, "_target", void 0);
@@ -724,8 +999,11 @@ __decorate([
724
999
  query('#next')
725
1000
  ], BoardViewer.prototype, "_next", void 0);
726
1001
  __decorate([
727
- query('#fullscreen')
728
- ], BoardViewer.prototype, "_fullscreen", void 0);
1002
+ query('[fab-group]')
1003
+ ], BoardViewer.prototype, "_fabGroup", void 0);
1004
+ __decorate([
1005
+ property({ attribute: false })
1006
+ ], BoardViewer.prototype, "_fetchPlaybackData", void 0);
729
1007
  BoardViewer = __decorate([
730
1008
  customElement('ox-board-viewer')
731
1009
  ], BoardViewer);