@fmsim/board 2.0.0-beta.61 → 2.0.0-beta.62

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.
@@ -10,7 +10,7 @@ export declare class RestfulInputAttachmentSelector extends LitElement {
10
10
  properties: any;
11
11
  private popup?;
12
12
  render(): import("lit-html").TemplateResult<1>;
13
- getIconByCategory(): "attachment" | "image" | "library_music" | "video_library";
13
+ getIconByCategory(): "image" | "library_music" | "video_library" | "attachment";
14
14
  _onInputChanged(e: KeyboardEvent): void;
15
15
  openSelector(): void;
16
16
  }
@@ -14,7 +14,7 @@ type PlaybackState = 'idle' | 'playing' | 'paused' | 'stopped';
14
14
  * @fires playback-stop - 중지 요청
15
15
  * @fires playback-seek - 드래그 릴리즈 시 시크 요청 { toTime }
16
16
  * @fires playback-seek-start - 드래그 첫 input 시 즉시 발행 (엔진 타이머·애니메이션 정지 신호, detail 없음)
17
- * @fires playback-seek-preview - 드래그 중 디바운스된 스크러빙 요청 { toTime }
17
+ * @fires playback-seek-preview - 드래그 중 throttle (leading+trailing, 200ms) 된 스크러빙 요청 { toTime }
18
18
  * @fires playback-speed - 배속 변경 요청 { speed }
19
19
  */
20
20
  export declare class PlaybackControls extends LitElement {
@@ -37,8 +37,12 @@ export declare class PlaybackControls extends LitElement {
37
37
  private _seeking;
38
38
  private _seekPreviewTime;
39
39
  private _speeds;
40
- /** 드래그 중 playback-seek-preview 이벤트 디바운스 타이머 */
41
- private _previewDebounce?;
40
+ /** 드래그 중 playback-seek-preview throttle 타이머 (leading+trailing) */
41
+ private _previewThrottle?;
42
+ /** 드래그 중 가장 최근 seekbar 위치 시각 */
43
+ private _latestPreviewTime;
44
+ /** 가장 최근에 실제 emit 한 preview 시각 (dedupe 용) */
45
+ private _lastEmittedPreviewTime;
42
46
  private get _endTime();
43
47
  private static _floorToHour;
44
48
  connectedCallback(): void;
@@ -64,10 +68,11 @@ export declare class PlaybackControls extends LitElement {
64
68
  private _onTogglePlayPause;
65
69
  private _onStop;
66
70
  private _onSeekInput;
71
+ /** latest 위치 시각으로 playback-seek-preview 를 발행 (dedupe). */
72
+ private _emitPreview;
67
73
  private _onSeekChange;
68
- private static readonly PREVIEW_DEBOUNCE_MS;
69
- private _schedulePreview;
70
- private _cancelPreviewDebounce;
74
+ private static readonly PREVIEW_THROTTLE_MS;
75
+ private _cancelPreviewThrottle;
71
76
  private _renderBufferBars;
72
77
  private _onSpeedChange;
73
78
  }
@@ -17,7 +17,7 @@ const ONE_HOUR = 3600000;
17
17
  * @fires playback-stop - 중지 요청
18
18
  * @fires playback-seek - 드래그 릴리즈 시 시크 요청 { toTime }
19
19
  * @fires playback-seek-start - 드래그 첫 input 시 즉시 발행 (엔진 타이머·애니메이션 정지 신호, detail 없음)
20
- * @fires playback-seek-preview - 드래그 중 디바운스된 스크러빙 요청 { toTime }
20
+ * @fires playback-seek-preview - 드래그 중 throttle (leading+trailing, 200ms) 된 스크러빙 요청 { toTime }
21
21
  * @fires playback-speed - 배속 변경 요청 { speed }
22
22
  */
23
23
  let PlaybackControls = PlaybackControls_1 = class PlaybackControls extends LitElement {
@@ -36,6 +36,10 @@ let PlaybackControls = PlaybackControls_1 = class PlaybackControls extends LitEl
36
36
  this._seeking = false;
37
37
  this._seekPreviewTime = null;
38
38
  this._speeds = [1, 2, 4];
39
+ /** 드래그 중 가장 최근 seekbar 위치 시각 */
40
+ this._latestPreviewTime = null;
41
+ /** 가장 최근에 실제 emit 한 preview 시각 (dedupe 용) */
42
+ this._lastEmittedPreviewTime = null;
39
43
  }
40
44
  get _endTime() {
41
45
  return new Date(this._startTime.getTime() + ONE_HOUR);
@@ -53,7 +57,7 @@ let PlaybackControls = PlaybackControls_1 = class PlaybackControls extends LitEl
53
57
  }
54
58
  disconnectedCallback() {
55
59
  super.disconnectedCallback();
56
- this._cancelPreviewDebounce();
60
+ this._cancelPreviewThrottle();
57
61
  }
58
62
  updated(changes) {
59
63
  if (changes.has('timeRange') && this.timeRange) {
@@ -242,36 +246,70 @@ let PlaybackControls = PlaybackControls_1 = class PlaybackControls extends LitEl
242
246
  }
243
247
  }
244
248
  _onStop() {
245
- this._cancelPreviewDebounce();
249
+ this._cancelPreviewThrottle();
246
250
  this._currentPlayTime = null;
247
251
  this._seekValue = 0;
248
252
  this.playbackState = 'idle';
249
253
  this.dispatchEvent(new CustomEvent('playback-stop', { bubbles: true, composed: true }));
250
254
  }
251
255
  _onSeekInput(e) {
252
- // 드래그 시작(첫 input) 탐지: 엔진 타이머 정지 신호 즉시 발행
256
+ // 드래그 시작(첫 input) 탐지
253
257
  const justStartedDrag = !this._seeking;
254
258
  this._seeking = true;
255
259
  this._seekValue = Number(e.target.value);
256
- // 드래그 중 preview 시간 표시 (실제 seek은 하지 않음)
260
+ // 드래그 중 preview 시간 계산 + UI 표시
257
261
  const ratio = this._seekValue / 1000;
258
262
  const from = this._startTime.getTime();
259
263
  const to = this._endTime.getTime();
260
264
  const previewTime = new Date(from + (to - from) * ratio);
261
265
  this._seekPreviewTime = previewTime;
266
+ this._latestPreviewTime = previewTime;
262
267
  if (justStartedDrag) {
263
- // 즉시 이벤트 — 재생 타이머 정지해서 scene 이 엔진 시간으로 갱신되는 것을 막음.
268
+ // 즉시 이벤트 — 재생 타이머 정지
264
269
  this.dispatchEvent(new CustomEvent('playback-seek-start', {
265
270
  bubbles: true,
266
271
  composed: true
267
272
  }));
268
273
  }
269
- // 디바운스된 playback-seek-preview 이벤트 — 드래그 중 snapshot 스크러빙
270
- this._schedulePreview(previewTime);
274
+ // throttle: leading-edge 에서 즉시 emit, 이후 200ms 내 input 은 latest 만 갱신.
275
+ // throttle 만료 시점에 latest 가 마지막 emit 과 다르면 trailing emit.
276
+ if (this._previewThrottle === undefined) {
277
+ // leading emit
278
+ this._emitPreview();
279
+ // trailing 예약
280
+ this._previewThrottle = window.setTimeout(() => {
281
+ var _a;
282
+ this._previewThrottle = undefined;
283
+ // 드래그가 끝났거나 (_seeking=false) 새 throttle 이 이미 있으면 스킵
284
+ if (!this._seeking)
285
+ return;
286
+ // latest 가 마지막 emit 과 같으면 중복 skip
287
+ const latest = this._latestPreviewTime;
288
+ if (latest && latest.getTime() !== ((_a = this._lastEmittedPreviewTime) === null || _a === void 0 ? void 0 : _a.getTime())) {
289
+ this._emitPreview();
290
+ }
291
+ }, PlaybackControls_1.PREVIEW_THROTTLE_MS);
292
+ }
293
+ // else: 이미 throttle 진행 중 → _latestPreviewTime 만 업데이트됨 (위에서 처리됨)
294
+ }
295
+ /** latest 위치 시각으로 playback-seek-preview 를 발행 (dedupe). */
296
+ _emitPreview() {
297
+ var _a;
298
+ const time = this._latestPreviewTime;
299
+ if (!time)
300
+ return;
301
+ if (((_a = this._lastEmittedPreviewTime) === null || _a === void 0 ? void 0 : _a.getTime()) === time.getTime())
302
+ return;
303
+ this._lastEmittedPreviewTime = time;
304
+ this.dispatchEvent(new CustomEvent('playback-seek-preview', {
305
+ detail: { toTime: time },
306
+ bubbles: true,
307
+ composed: true
308
+ }));
271
309
  }
272
310
  _onSeekChange(e) {
273
311
  this._seeking = false;
274
- this._cancelPreviewDebounce();
312
+ this._cancelPreviewThrottle();
275
313
  const ratio = Number(e.target.value) / 1000;
276
314
  const from = this._startTime.getTime();
277
315
  const to = this._endTime.getTime();
@@ -284,22 +322,13 @@ let PlaybackControls = PlaybackControls_1 = class PlaybackControls extends LitEl
284
322
  composed: true
285
323
  }));
286
324
  }
287
- _schedulePreview(previewTime) {
288
- this._cancelPreviewDebounce();
289
- this._previewDebounce = window.setTimeout(() => {
290
- this._previewDebounce = undefined;
291
- this.dispatchEvent(new CustomEvent('playback-seek-preview', {
292
- detail: { toTime: previewTime },
293
- bubbles: true,
294
- composed: true
295
- }));
296
- }, PlaybackControls_1.PREVIEW_DEBOUNCE_MS);
297
- }
298
- _cancelPreviewDebounce() {
299
- if (this._previewDebounce != null) {
300
- clearTimeout(this._previewDebounce);
301
- this._previewDebounce = undefined;
325
+ _cancelPreviewThrottle() {
326
+ if (this._previewThrottle != null) {
327
+ clearTimeout(this._previewThrottle);
328
+ this._previewThrottle = undefined;
302
329
  }
330
+ this._latestPreviewTime = null;
331
+ this._lastEmittedPreviewTime = null;
303
332
  }
304
333
  _renderBufferBars() {
305
334
  var _a;
@@ -549,7 +578,7 @@ PlaybackControls.styles = css `
549
578
  text-align: center;
550
579
  }
551
580
  `;
552
- PlaybackControls.PREVIEW_DEBOUNCE_MS = 200;
581
+ PlaybackControls.PREVIEW_THROTTLE_MS = 200;
553
582
  __decorate([
554
583
  property({ type: String })
555
584
  ], PlaybackControls.prototype, "playbackState", void 0);
@@ -1 +1 @@
1
- {"version":3,"file":"ox-playback-controls.js","sourceRoot":"","sources":["../../src/ox-playback-controls.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@material/web/iconbutton/icon-button.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\ntype PlaybackState = 'idle' | 'playing' | 'paused' | 'stopped'\n\nconst ONE_HOUR = 3600000\n\n/**\n * 플레이백 컨트롤 오버레이 바 (2행 구조)\n *\n * 1행: 시작 시점 설정 (datetime + ±1h 버튼) + 배속 선택\n * 2행: 재생 제어 (play/pause/stop) + 1시간 범위 시크바 + 닫기\n *\n * @fires playback-start - 플레이백 시작 요청 { fromTime, speed }\n * @fires playback-pause - 일시정지 요청\n * @fires playback-resume - 재개 요청\n * @fires playback-stop - 중지 요청\n * @fires playback-seek - 드래그 릴리즈 시 시크 요청 { toTime }\n * @fires playback-seek-start - 드래그 첫 input 시 즉시 발행 (엔진 타이머·애니메이션 정지 신호, detail 없음)\n * @fires playback-seek-preview - 드래그 중 디바운스된 스크러빙 요청 { toTime }\n * @fires playback-speed - 배속 변경 요청 { speed }\n */\n@customElement('ox-playback-controls')\nexport class PlaybackControls extends LitElement {\n static styles = css`\n :host {\n display: flex;\n justify-content: center;\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n z-index: 1001;\n pointer-events: none;\n }\n\n .control-bar {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 10px 16px;\n margin: 12px;\n max-width: 560px;\n width: 100%;\n background: rgba(0, 0, 0, 0.75);\n backdrop-filter: blur(8px);\n border-radius: 12px;\n color: #fff;\n pointer-events: auto;\n font-size: 13px;\n box-sizing: border-box;\n }\n\n /* 1행: 시점 설정 + 배속 */\n .row-time {\n display: flex;\n align-items: center;\n gap: 6px;\n }\n\n md-icon-button {\n --md-icon-button-icon-color: #fff;\n --md-icon-button-hover-icon-color: #fff;\n --md-icon-button-pressed-icon-color: #fff;\n --md-icon-button-focus-icon-color: #fff;\n --md-icon-button-state-layer-color: rgba(255, 255, 255, 0.2);\n }\n\n md-icon-button:hover {\n background: rgba(255, 255, 255, 0.15);\n border-radius: 50%;\n }\n\n .row-time md-icon-button {\n --md-icon-button-icon-size: 18px;\n --md-icon-button-container-width: 32px;\n --md-icon-button-container-height: 32px;\n }\n\n .date-input {\n background: rgba(255, 255, 255, 0.15);\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 6px;\n color: #fff;\n font-size: 13px;\n padding: 4px 8px;\n outline: none;\n color-scheme: dark;\n width: 130px;\n }\n\n .date-input:focus {\n border-color: rgba(255, 255, 255, 0.6);\n }\n\n .hour-select {\n background: rgba(255, 255, 255, 0.15);\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 6px;\n color: #fff;\n font-size: 13px;\n padding: 4px 6px;\n outline: none;\n color-scheme: dark;\n cursor: pointer;\n }\n\n .hour-select:focus {\n border-color: rgba(255, 255, 255, 0.6);\n }\n\n .time-label {\n font-size: 12px;\n opacity: 0.7;\n white-space: nowrap;\n }\n\n .spacer {\n flex: 1;\n }\n\n .speed-group {\n display: flex;\n gap: 2px;\n }\n\n .speed-btn {\n padding: 4px 8px;\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 4px;\n background: transparent;\n color: #fff;\n font-size: 12px;\n cursor: pointer;\n white-space: nowrap;\n }\n\n .speed-btn:hover {\n background: rgba(255, 255, 255, 0.15);\n }\n\n .speed-btn.active {\n background: rgba(255, 255, 255, 0.3);\n border-color: #fff;\n }\n\n /* 2행: 재생 제어 + 시크바 */\n .row-seek {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n\n .row-seek md-icon-button {\n --md-icon-button-icon-size: 20px;\n --md-icon-button-container-width: 36px;\n --md-icon-button-container-height: 36px;\n }\n\n .row-seek md-icon-button.primary {\n --md-icon-button-icon-size: 24px;\n --md-icon-button-container-width: 40px;\n --md-icon-button-container-height: 40px;\n }\n\n .row-seek md-icon {\n font-variation-settings: 'FILL' 1;\n }\n\n .timeline {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n }\n\n .seek-track {\n position: relative;\n width: 100%;\n height: 12px;\n }\n\n .seek-rail {\n position: absolute;\n top: 4px;\n left: 0;\n right: 0;\n height: 4px;\n background: rgba(255, 255, 255, 0.15);\n border-radius: 2px;\n }\n\n .buffer-bar {\n position: absolute;\n top: 4px;\n height: 4px;\n background: rgba(255, 255, 255, 0.4);\n border-radius: 2px;\n pointer-events: none;\n z-index: 1;\n }\n\n .seek-bar {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 12px;\n -webkit-appearance: none;\n appearance: none;\n background: transparent;\n outline: none;\n cursor: pointer;\n z-index: 2;\n margin: 0;\n }\n\n .seek-bar::-webkit-slider-runnable-track {\n height: 12px;\n background: transparent;\n }\n\n .seek-bar::-webkit-slider-thumb {\n -webkit-appearance: none;\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: #fff;\n cursor: pointer;\n }\n\n .seek-labels {\n display: flex;\n justify-content: space-between;\n font-size: 11px;\n opacity: 0.8;\n }\n\n .current-time {\n font-size: 12px;\n opacity: 0.9;\n white-space: nowrap;\n min-width: 60px;\n text-align: center;\n }\n `\n\n @property({ type: String }) playbackState: PlaybackState = 'idle'\n @property({ type: Number }) speed: number = 1\n @property({ type: String }) currentTime: string = ''\n @property({ type: Array }) bufferedRanges: { from: number; to: number }[] = []\n\n /** 외부에서 전달받은 초기 시간 범위 (from만 사용, to는 from+1h로 자동 계산) */\n @property({ type: Object }) timeRange: { from: Date; to: Date } = {\n from: new Date(Date.now() - ONE_HOUR),\n to: new Date()\n }\n\n @state() private _startTime: Date = PlaybackControls._floorToHour(new Date(Date.now() - ONE_HOUR))\n @state() declare private _seekValue: number\n @state() declare private _currentPlayTime: Date | null\n @state() private _seeking = false\n @state() private _seekPreviewTime: Date | null = null\n\n private _speeds = [1, 2, 4]\n /** 드래그 중 playback-seek-preview 이벤트 디바운스 타이머 */\n private _previewDebounce?: number\n\n private get _endTime(): Date {\n return new Date(this._startTime.getTime() + ONE_HOUR)\n }\n\n private static _floorToHour(date: Date): Date {\n const d = new Date(date)\n d.setMinutes(0, 0, 0)\n return d\n }\n\n connectedCallback() {\n super.connectedCallback()\n this._startTime = PlaybackControls._floorToHour(this.timeRange.from)\n this._seekValue = 0\n this._currentPlayTime = null\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this._cancelPreviewDebounce()\n }\n\n updated(changes: Map<string, any>) {\n if (changes.has('timeRange') && this.timeRange) {\n this._startTime = PlaybackControls._floorToHour(this.timeRange.from)\n }\n // 엔진 프레임(currentTime 프로프 변경)이 유일한 시간 소스.\n // drag 중에는 _seeking 가 true 이므로 엔진 업데이트 무시 (스크러빙 중 UI 튐 방지).\n if (changes.has('currentTime') && this.currentTime && !this._seeking) {\n this._syncFromEngine()\n }\n }\n\n render() {\n const isPlaying = this.playbackState === 'playing'\n const isPaused = this.playbackState === 'paused'\n const isActive = isPlaying || isPaused\n\n return html`\n <div class=\"control-bar\">\n <!-- 1행: 시점 설정 + 배속 -->\n <div class=\"row-time\">\n <md-icon-button @click=${this._shiftTime(-1)} title=\"-1h\">\n <md-icon>chevron_left</md-icon>\n </md-icon-button>\n\n <input\n type=\"date\"\n class=\"date-input\"\n .value=${this._toDateString(this._startTime)}\n @change=${this._onDateChange}\n />\n\n <select class=\"hour-select\" .value=${String(this._startTime.getHours())} @change=${this._onHourChange}>\n ${Array.from({ length: 24 }, (_, i) => html`\n <option value=${i} ?selected=${this._startTime.getHours() === i}>\n ${String(i).padStart(2, '0')}:00\n </option>\n `)}\n </select>\n\n <md-icon-button @click=${this._shiftTime(1)} title=\"+1h\">\n <md-icon>chevron_right</md-icon>\n </md-icon-button>\n\n <span class=\"time-label\">~ ${String(this._endTime.getHours()).padStart(2, '0')}:00</span>\n\n <span class=\"spacer\"></span>\n\n <div class=\"speed-group\">\n ${this._speeds.map(\n s => html`\n <button class=\"speed-btn ${this.speed === s ? 'active' : ''}\" @click=${() => this._onSpeedChange(s)}>\n ×${s}\n </button>\n `\n )}\n </div>\n </div>\n\n <!-- 2행: 재생 제어 + 시크바 + 닫기 -->\n <div class=\"row-seek\">\n <md-icon-button class=\"primary\" @click=${isActive ? this._onTogglePlayPause : this._onStart}>\n <md-icon>${isPlaying ? 'pause' : 'play_arrow'}</md-icon>\n </md-icon-button>\n\n <div class=\"timeline\">\n <div class=\"seek-track\">\n <div class=\"seek-rail\"></div>\n ${this._renderBufferBars()}\n <input\n type=\"range\"\n class=\"seek-bar\"\n min=\"0\"\n max=\"1000\"\n .value=${String(this._seekValue)}\n @input=${this._onSeekInput}\n @change=${this._onSeekChange}\n />\n </div>\n <div class=\"seek-labels\">\n <span>${this._formatTime(this._startTime)}</span>\n <span>${this._formatTime(this._endTime)}</span>\n </div>\n </div>\n\n <span class=\"current-time\">\n ${this._seeking && this._seekPreviewTime\n ? this._formatTime(this._seekPreviewTime)\n : this._currentPlayTime ? this._formatTime(this._currentPlayTime) : '--:--:--'}\n </span>\n\n <md-icon-button @click=${this._onStop}>\n <md-icon>close</md-icon>\n </md-icon-button>\n </div>\n </div>\n `\n }\n\n private _formatTime(date: Date): string {\n return date.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })\n }\n\n private _toDateString(date: Date): string {\n const pad = (n: number) => String(n).padStart(2, '0')\n return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`\n }\n\n /**\n * 엔진 프레임(currentTime 프로프) 을 유일한 시간 소스로 사용하여\n * _currentPlayTime 및 _seekValue 동기화.\n */\n private _syncFromEngine() {\n if (!this.currentTime || this._seeking) return\n const currentMs = new Date(this.currentTime).getTime()\n if (!Number.isFinite(currentMs)) return\n\n this._currentPlayTime = new Date(currentMs)\n\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n const range = to - from\n if (range <= 0) return\n this._seekValue = Math.max(0, Math.min(1000, Math.round(((currentMs - from) / range) * 1000)))\n }\n\n private _onDateChange(e: Event) {\n const value = (e.target as HTMLInputElement).value\n if (!value) return\n const [year, month, day] = value.split('-').map(Number)\n const newStart = new Date(this._startTime)\n newStart.setFullYear(year, month - 1, day)\n this._shiftStartTime(newStart)\n }\n\n private _onHourChange(e: Event) {\n const hour = Number((e.target as HTMLSelectElement).value)\n const newStart = new Date(this._startTime)\n newStart.setHours(hour, 0, 0, 0)\n this._shiftStartTime(newStart)\n }\n\n private _shiftTime(hours: number) {\n return () => {\n const newStart = new Date(this._startTime.getTime() + hours * ONE_HOUR)\n this._shiftStartTime(newStart)\n }\n }\n\n /**\n * 시작 시간 변경 시: 재생/일시정지 중이면 새 시간에서 재시작.\n * 정지 상태면 시간만 변경.\n */\n private _shiftStartTime(newStart: Date) {\n const wasPlaying = this.playbackState === 'playing'\n const wasPaused = this.playbackState === 'paused'\n\n this._startTime = newStart\n this._seekValue = 0\n this._currentPlayTime = new Date(newStart)\n\n if (wasPlaying) {\n // play 중: 새 시간에서 자동 재시작\n this._onStart()\n } else if (wasPaused) {\n // pause 중: 엔진에 새 시간 전달 + 데이터 fetch, 하지만 자동 재생 안 함\n this.dispatchEvent(\n new CustomEvent('playback-seek', {\n detail: { toTime: new Date(newStart) },\n bubbles: true,\n composed: true\n })\n )\n }\n }\n\n private _onStart() {\n // 현재 seekbar 위치에서 재생 시작 (_seekValue 가 유일한 슬라이더 위치 소스)\n const ratio = this._seekValue / 1000\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n const fromTime = new Date(from + (to - from) * ratio)\n\n this._currentPlayTime = fromTime\n this.playbackState = 'playing'\n\n this.dispatchEvent(\n new CustomEvent('playback-start', {\n detail: { fromTime, speed: this.speed },\n bubbles: true,\n composed: true\n })\n )\n }\n\n private _onTogglePlayPause() {\n if (this.playbackState === 'playing') {\n this.playbackState = 'paused'\n this.dispatchEvent(new CustomEvent('playback-pause', { bubbles: true, composed: true }))\n } else {\n this.playbackState = 'playing'\n this.dispatchEvent(new CustomEvent('playback-resume', { bubbles: true, composed: true }))\n }\n }\n\n private _onStop() {\n this._cancelPreviewDebounce()\n this._currentPlayTime = null\n this._seekValue = 0\n this.playbackState = 'idle'\n this.dispatchEvent(new CustomEvent('playback-stop', { bubbles: true, composed: true }))\n }\n\n private _onSeekInput(e: Event) {\n // 드래그 시작(첫 input) 탐지: 엔진 타이머 정지 신호 즉시 발행\n const justStartedDrag = !this._seeking\n this._seeking = true\n this._seekValue = Number((e.target as HTMLInputElement).value)\n\n // 드래그 중 preview 시간 표시 (실제 seek은 하지 않음)\n const ratio = this._seekValue / 1000\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n const previewTime = new Date(from + (to - from) * ratio)\n this._seekPreviewTime = previewTime\n\n if (justStartedDrag) {\n // 즉시 이벤트 — 재생 타이머 정지해서 scene 이 엔진 시간으로 갱신되는 것을 막음.\n this.dispatchEvent(\n new CustomEvent('playback-seek-start', {\n bubbles: true,\n composed: true\n })\n )\n }\n\n // 디바운스된 playback-seek-preview 이벤트 — 드래그 중 snapshot 스크러빙\n this._schedulePreview(previewTime)\n }\n\n private _onSeekChange(e: Event) {\n this._seeking = false\n this._cancelPreviewDebounce()\n\n const ratio = Number((e.target as HTMLInputElement).value) / 1000\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n const targetTime = new Date(from + (to - from) * ratio)\n\n this._seekPreviewTime = null\n this._currentPlayTime = targetTime\n\n this.dispatchEvent(\n new CustomEvent('playback-seek', {\n detail: { toTime: targetTime },\n bubbles: true,\n composed: true\n })\n )\n }\n\n private static readonly PREVIEW_DEBOUNCE_MS = 200\n\n private _schedulePreview(previewTime: Date) {\n this._cancelPreviewDebounce()\n this._previewDebounce = window.setTimeout(() => {\n this._previewDebounce = undefined\n this.dispatchEvent(\n new CustomEvent('playback-seek-preview', {\n detail: { toTime: previewTime },\n bubbles: true,\n composed: true\n })\n )\n }, PlaybackControls.PREVIEW_DEBOUNCE_MS)\n }\n\n private _cancelPreviewDebounce() {\n if (this._previewDebounce != null) {\n clearTimeout(this._previewDebounce)\n this._previewDebounce = undefined\n }\n }\n\n private _renderBufferBars() {\n if (!this.bufferedRanges?.length) return ''\n\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n const range = to - from\n if (range <= 0) return ''\n\n return this.bufferedRanges.map(r => {\n const left = Math.max(0, (r.from - from) / range) * 100\n const right = Math.min(1, (r.to - from) / range) * 100\n const width = right - left\n return html`<div class=\"buffer-bar\" style=\"left:${left}%;width:${width}%\"></div>`\n })\n }\n\n private _onSpeedChange(speed: number) {\n this.speed = speed\n\n this.dispatchEvent(\n new CustomEvent('playback-speed', {\n detail: { speed },\n bubbles: true,\n composed: true\n })\n )\n }\n}\n"]}
1
+ {"version":3,"file":"ox-playback-controls.js","sourceRoot":"","sources":["../../src/ox-playback-controls.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@material/web/iconbutton/icon-button.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\ntype PlaybackState = 'idle' | 'playing' | 'paused' | 'stopped'\n\nconst ONE_HOUR = 3600000\n\n/**\n * 플레이백 컨트롤 오버레이 바 (2행 구조)\n *\n * 1행: 시작 시점 설정 (datetime + ±1h 버튼) + 배속 선택\n * 2행: 재생 제어 (play/pause/stop) + 1시간 범위 시크바 + 닫기\n *\n * @fires playback-start - 플레이백 시작 요청 { fromTime, speed }\n * @fires playback-pause - 일시정지 요청\n * @fires playback-resume - 재개 요청\n * @fires playback-stop - 중지 요청\n * @fires playback-seek - 드래그 릴리즈 시 시크 요청 { toTime }\n * @fires playback-seek-start - 드래그 첫 input 시 즉시 발행 (엔진 타이머·애니메이션 정지 신호, detail 없음)\n * @fires playback-seek-preview - 드래그 중 throttle (leading+trailing, 200ms) 된 스크러빙 요청 { toTime }\n * @fires playback-speed - 배속 변경 요청 { speed }\n */\n@customElement('ox-playback-controls')\nexport class PlaybackControls extends LitElement {\n static styles = css`\n :host {\n display: flex;\n justify-content: center;\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n z-index: 1001;\n pointer-events: none;\n }\n\n .control-bar {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 10px 16px;\n margin: 12px;\n max-width: 560px;\n width: 100%;\n background: rgba(0, 0, 0, 0.75);\n backdrop-filter: blur(8px);\n border-radius: 12px;\n color: #fff;\n pointer-events: auto;\n font-size: 13px;\n box-sizing: border-box;\n }\n\n /* 1행: 시점 설정 + 배속 */\n .row-time {\n display: flex;\n align-items: center;\n gap: 6px;\n }\n\n md-icon-button {\n --md-icon-button-icon-color: #fff;\n --md-icon-button-hover-icon-color: #fff;\n --md-icon-button-pressed-icon-color: #fff;\n --md-icon-button-focus-icon-color: #fff;\n --md-icon-button-state-layer-color: rgba(255, 255, 255, 0.2);\n }\n\n md-icon-button:hover {\n background: rgba(255, 255, 255, 0.15);\n border-radius: 50%;\n }\n\n .row-time md-icon-button {\n --md-icon-button-icon-size: 18px;\n --md-icon-button-container-width: 32px;\n --md-icon-button-container-height: 32px;\n }\n\n .date-input {\n background: rgba(255, 255, 255, 0.15);\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 6px;\n color: #fff;\n font-size: 13px;\n padding: 4px 8px;\n outline: none;\n color-scheme: dark;\n width: 130px;\n }\n\n .date-input:focus {\n border-color: rgba(255, 255, 255, 0.6);\n }\n\n .hour-select {\n background: rgba(255, 255, 255, 0.15);\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 6px;\n color: #fff;\n font-size: 13px;\n padding: 4px 6px;\n outline: none;\n color-scheme: dark;\n cursor: pointer;\n }\n\n .hour-select:focus {\n border-color: rgba(255, 255, 255, 0.6);\n }\n\n .time-label {\n font-size: 12px;\n opacity: 0.7;\n white-space: nowrap;\n }\n\n .spacer {\n flex: 1;\n }\n\n .speed-group {\n display: flex;\n gap: 2px;\n }\n\n .speed-btn {\n padding: 4px 8px;\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 4px;\n background: transparent;\n color: #fff;\n font-size: 12px;\n cursor: pointer;\n white-space: nowrap;\n }\n\n .speed-btn:hover {\n background: rgba(255, 255, 255, 0.15);\n }\n\n .speed-btn.active {\n background: rgba(255, 255, 255, 0.3);\n border-color: #fff;\n }\n\n /* 2행: 재생 제어 + 시크바 */\n .row-seek {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n\n .row-seek md-icon-button {\n --md-icon-button-icon-size: 20px;\n --md-icon-button-container-width: 36px;\n --md-icon-button-container-height: 36px;\n }\n\n .row-seek md-icon-button.primary {\n --md-icon-button-icon-size: 24px;\n --md-icon-button-container-width: 40px;\n --md-icon-button-container-height: 40px;\n }\n\n .row-seek md-icon {\n font-variation-settings: 'FILL' 1;\n }\n\n .timeline {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n }\n\n .seek-track {\n position: relative;\n width: 100%;\n height: 12px;\n }\n\n .seek-rail {\n position: absolute;\n top: 4px;\n left: 0;\n right: 0;\n height: 4px;\n background: rgba(255, 255, 255, 0.15);\n border-radius: 2px;\n }\n\n .buffer-bar {\n position: absolute;\n top: 4px;\n height: 4px;\n background: rgba(255, 255, 255, 0.4);\n border-radius: 2px;\n pointer-events: none;\n z-index: 1;\n }\n\n .seek-bar {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 12px;\n -webkit-appearance: none;\n appearance: none;\n background: transparent;\n outline: none;\n cursor: pointer;\n z-index: 2;\n margin: 0;\n }\n\n .seek-bar::-webkit-slider-runnable-track {\n height: 12px;\n background: transparent;\n }\n\n .seek-bar::-webkit-slider-thumb {\n -webkit-appearance: none;\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: #fff;\n cursor: pointer;\n }\n\n .seek-labels {\n display: flex;\n justify-content: space-between;\n font-size: 11px;\n opacity: 0.8;\n }\n\n .current-time {\n font-size: 12px;\n opacity: 0.9;\n white-space: nowrap;\n min-width: 60px;\n text-align: center;\n }\n `\n\n @property({ type: String }) playbackState: PlaybackState = 'idle'\n @property({ type: Number }) speed: number = 1\n @property({ type: String }) currentTime: string = ''\n @property({ type: Array }) bufferedRanges: { from: number; to: number }[] = []\n\n /** 외부에서 전달받은 초기 시간 범위 (from만 사용, to는 from+1h로 자동 계산) */\n @property({ type: Object }) timeRange: { from: Date; to: Date } = {\n from: new Date(Date.now() - ONE_HOUR),\n to: new Date()\n }\n\n @state() private _startTime: Date = PlaybackControls._floorToHour(new Date(Date.now() - ONE_HOUR))\n @state() declare private _seekValue: number\n @state() declare private _currentPlayTime: Date | null\n @state() private _seeking = false\n @state() private _seekPreviewTime: Date | null = null\n\n private _speeds = [1, 2, 4]\n /** 드래그 중 playback-seek-preview throttle 타이머 (leading+trailing) */\n private _previewThrottle?: number\n /** 드래그 중 가장 최근 seekbar 위치 시각 */\n private _latestPreviewTime: Date | null = null\n /** 가장 최근에 실제 emit 한 preview 시각 (dedupe 용) */\n private _lastEmittedPreviewTime: Date | null = null\n\n private get _endTime(): Date {\n return new Date(this._startTime.getTime() + ONE_HOUR)\n }\n\n private static _floorToHour(date: Date): Date {\n const d = new Date(date)\n d.setMinutes(0, 0, 0)\n return d\n }\n\n connectedCallback() {\n super.connectedCallback()\n this._startTime = PlaybackControls._floorToHour(this.timeRange.from)\n this._seekValue = 0\n this._currentPlayTime = null\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this._cancelPreviewThrottle()\n }\n\n updated(changes: Map<string, any>) {\n if (changes.has('timeRange') && this.timeRange) {\n this._startTime = PlaybackControls._floorToHour(this.timeRange.from)\n }\n // 엔진 프레임(currentTime 프로프 변경)이 유일한 시간 소스.\n // drag 중에는 _seeking 가 true 이므로 엔진 업데이트 무시 (스크러빙 중 UI 튐 방지).\n if (changes.has('currentTime') && this.currentTime && !this._seeking) {\n this._syncFromEngine()\n }\n }\n\n render() {\n const isPlaying = this.playbackState === 'playing'\n const isPaused = this.playbackState === 'paused'\n const isActive = isPlaying || isPaused\n\n return html`\n <div class=\"control-bar\">\n <!-- 1행: 시점 설정 + 배속 -->\n <div class=\"row-time\">\n <md-icon-button @click=${this._shiftTime(-1)} title=\"-1h\">\n <md-icon>chevron_left</md-icon>\n </md-icon-button>\n\n <input\n type=\"date\"\n class=\"date-input\"\n .value=${this._toDateString(this._startTime)}\n @change=${this._onDateChange}\n />\n\n <select class=\"hour-select\" .value=${String(this._startTime.getHours())} @change=${this._onHourChange}>\n ${Array.from({ length: 24 }, (_, i) => html`\n <option value=${i} ?selected=${this._startTime.getHours() === i}>\n ${String(i).padStart(2, '0')}:00\n </option>\n `)}\n </select>\n\n <md-icon-button @click=${this._shiftTime(1)} title=\"+1h\">\n <md-icon>chevron_right</md-icon>\n </md-icon-button>\n\n <span class=\"time-label\">~ ${String(this._endTime.getHours()).padStart(2, '0')}:00</span>\n\n <span class=\"spacer\"></span>\n\n <div class=\"speed-group\">\n ${this._speeds.map(\n s => html`\n <button class=\"speed-btn ${this.speed === s ? 'active' : ''}\" @click=${() => this._onSpeedChange(s)}>\n ×${s}\n </button>\n `\n )}\n </div>\n </div>\n\n <!-- 2행: 재생 제어 + 시크바 + 닫기 -->\n <div class=\"row-seek\">\n <md-icon-button class=\"primary\" @click=${isActive ? this._onTogglePlayPause : this._onStart}>\n <md-icon>${isPlaying ? 'pause' : 'play_arrow'}</md-icon>\n </md-icon-button>\n\n <div class=\"timeline\">\n <div class=\"seek-track\">\n <div class=\"seek-rail\"></div>\n ${this._renderBufferBars()}\n <input\n type=\"range\"\n class=\"seek-bar\"\n min=\"0\"\n max=\"1000\"\n .value=${String(this._seekValue)}\n @input=${this._onSeekInput}\n @change=${this._onSeekChange}\n />\n </div>\n <div class=\"seek-labels\">\n <span>${this._formatTime(this._startTime)}</span>\n <span>${this._formatTime(this._endTime)}</span>\n </div>\n </div>\n\n <span class=\"current-time\">\n ${this._seeking && this._seekPreviewTime\n ? this._formatTime(this._seekPreviewTime)\n : this._currentPlayTime ? this._formatTime(this._currentPlayTime) : '--:--:--'}\n </span>\n\n <md-icon-button @click=${this._onStop}>\n <md-icon>close</md-icon>\n </md-icon-button>\n </div>\n </div>\n `\n }\n\n private _formatTime(date: Date): string {\n return date.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })\n }\n\n private _toDateString(date: Date): string {\n const pad = (n: number) => String(n).padStart(2, '0')\n return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`\n }\n\n /**\n * 엔진 프레임(currentTime 프로프) 을 유일한 시간 소스로 사용하여\n * _currentPlayTime 및 _seekValue 동기화.\n */\n private _syncFromEngine() {\n if (!this.currentTime || this._seeking) return\n const currentMs = new Date(this.currentTime).getTime()\n if (!Number.isFinite(currentMs)) return\n\n this._currentPlayTime = new Date(currentMs)\n\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n const range = to - from\n if (range <= 0) return\n this._seekValue = Math.max(0, Math.min(1000, Math.round(((currentMs - from) / range) * 1000)))\n }\n\n private _onDateChange(e: Event) {\n const value = (e.target as HTMLInputElement).value\n if (!value) return\n const [year, month, day] = value.split('-').map(Number)\n const newStart = new Date(this._startTime)\n newStart.setFullYear(year, month - 1, day)\n this._shiftStartTime(newStart)\n }\n\n private _onHourChange(e: Event) {\n const hour = Number((e.target as HTMLSelectElement).value)\n const newStart = new Date(this._startTime)\n newStart.setHours(hour, 0, 0, 0)\n this._shiftStartTime(newStart)\n }\n\n private _shiftTime(hours: number) {\n return () => {\n const newStart = new Date(this._startTime.getTime() + hours * ONE_HOUR)\n this._shiftStartTime(newStart)\n }\n }\n\n /**\n * 시작 시간 변경 시: 재생/일시정지 중이면 새 시간에서 재시작.\n * 정지 상태면 시간만 변경.\n */\n private _shiftStartTime(newStart: Date) {\n const wasPlaying = this.playbackState === 'playing'\n const wasPaused = this.playbackState === 'paused'\n\n this._startTime = newStart\n this._seekValue = 0\n this._currentPlayTime = new Date(newStart)\n\n if (wasPlaying) {\n // play 중: 새 시간에서 자동 재시작\n this._onStart()\n } else if (wasPaused) {\n // pause 중: 엔진에 새 시간 전달 + 데이터 fetch, 하지만 자동 재생 안 함\n this.dispatchEvent(\n new CustomEvent('playback-seek', {\n detail: { toTime: new Date(newStart) },\n bubbles: true,\n composed: true\n })\n )\n }\n }\n\n private _onStart() {\n // 현재 seekbar 위치에서 재생 시작 (_seekValue 가 유일한 슬라이더 위치 소스)\n const ratio = this._seekValue / 1000\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n const fromTime = new Date(from + (to - from) * ratio)\n\n this._currentPlayTime = fromTime\n this.playbackState = 'playing'\n\n this.dispatchEvent(\n new CustomEvent('playback-start', {\n detail: { fromTime, speed: this.speed },\n bubbles: true,\n composed: true\n })\n )\n }\n\n private _onTogglePlayPause() {\n if (this.playbackState === 'playing') {\n this.playbackState = 'paused'\n this.dispatchEvent(new CustomEvent('playback-pause', { bubbles: true, composed: true }))\n } else {\n this.playbackState = 'playing'\n this.dispatchEvent(new CustomEvent('playback-resume', { bubbles: true, composed: true }))\n }\n }\n\n private _onStop() {\n this._cancelPreviewThrottle()\n this._currentPlayTime = null\n this._seekValue = 0\n this.playbackState = 'idle'\n this.dispatchEvent(new CustomEvent('playback-stop', { bubbles: true, composed: true }))\n }\n\n private _onSeekInput(e: Event) {\n // 드래그 시작(첫 input) 탐지\n const justStartedDrag = !this._seeking\n this._seeking = true\n this._seekValue = Number((e.target as HTMLInputElement).value)\n\n // 드래그 중 preview 시간 계산 + UI 표시\n const ratio = this._seekValue / 1000\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n const previewTime = new Date(from + (to - from) * ratio)\n this._seekPreviewTime = previewTime\n this._latestPreviewTime = previewTime\n\n if (justStartedDrag) {\n // 즉시 이벤트 — 재생 타이머 정지\n this.dispatchEvent(\n new CustomEvent('playback-seek-start', {\n bubbles: true,\n composed: true\n })\n )\n }\n\n // throttle: leading-edge 에서 즉시 emit, 이후 200ms 내 input 은 latest 만 갱신.\n // throttle 만료 시점에 latest 가 마지막 emit 과 다르면 trailing emit.\n if (this._previewThrottle === undefined) {\n // leading emit\n this._emitPreview()\n // trailing 예약\n this._previewThrottle = window.setTimeout(() => {\n this._previewThrottle = undefined\n // 드래그가 끝났거나 (_seeking=false) 새 throttle 이 이미 있으면 스킵\n if (!this._seeking) return\n // latest 가 마지막 emit 과 같으면 중복 skip\n const latest = this._latestPreviewTime\n if (latest && latest.getTime() !== this._lastEmittedPreviewTime?.getTime()) {\n this._emitPreview()\n }\n }, PlaybackControls.PREVIEW_THROTTLE_MS)\n }\n // else: 이미 throttle 진행 중 → _latestPreviewTime 만 업데이트됨 (위에서 처리됨)\n }\n\n /** latest 위치 시각으로 playback-seek-preview 를 발행 (dedupe). */\n private _emitPreview() {\n const time = this._latestPreviewTime\n if (!time) return\n if (this._lastEmittedPreviewTime?.getTime() === time.getTime()) return\n this._lastEmittedPreviewTime = time\n this.dispatchEvent(\n new CustomEvent('playback-seek-preview', {\n detail: { toTime: time },\n bubbles: true,\n composed: true\n })\n )\n }\n\n private _onSeekChange(e: Event) {\n this._seeking = false\n this._cancelPreviewThrottle()\n\n const ratio = Number((e.target as HTMLInputElement).value) / 1000\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n const targetTime = new Date(from + (to - from) * ratio)\n\n this._seekPreviewTime = null\n this._currentPlayTime = targetTime\n\n this.dispatchEvent(\n new CustomEvent('playback-seek', {\n detail: { toTime: targetTime },\n bubbles: true,\n composed: true\n })\n )\n }\n\n private static readonly PREVIEW_THROTTLE_MS = 200\n\n private _cancelPreviewThrottle() {\n if (this._previewThrottle != null) {\n clearTimeout(this._previewThrottle)\n this._previewThrottle = undefined\n }\n this._latestPreviewTime = null\n this._lastEmittedPreviewTime = null\n }\n\n private _renderBufferBars() {\n if (!this.bufferedRanges?.length) return ''\n\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n const range = to - from\n if (range <= 0) return ''\n\n return this.bufferedRanges.map(r => {\n const left = Math.max(0, (r.from - from) / range) * 100\n const right = Math.min(1, (r.to - from) / range) * 100\n const width = right - left\n return html`<div class=\"buffer-bar\" style=\"left:${left}%;width:${width}%\"></div>`\n })\n }\n\n private _onSpeedChange(speed: number) {\n this.speed = speed\n\n this.dispatchEvent(\n new CustomEvent('playback-speed', {\n detail: { speed },\n bubbles: true,\n composed: true\n })\n )\n }\n}\n"]}