@operato/board 10.0.0-beta.5 → 10.0.0-beta.50
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.
- package/CHANGELOG.md +408 -0
- package/dist/src/component/container.js +18 -3
- package/dist/src/component/container.js.map +1 -1
- package/dist/src/component/conveyance.d.ts +2 -0
- package/dist/src/component/conveyance.js +38 -0
- package/dist/src/component/conveyance.js.map +1 -0
- package/dist/src/component/etc.js +2 -10
- package/dist/src/component/etc.js.map +1 -1
- package/dist/src/component/facility.d.ts +2 -0
- package/dist/src/component/facility.js +35 -0
- package/dist/src/component/facility.js.map +1 -0
- package/dist/src/component/index.d.ts +5 -0
- package/dist/src/component/index.js +5 -0
- package/dist/src/component/index.js.map +1 -1
- package/dist/src/component/line.js +4 -28
- package/dist/src/component/line.js.map +1 -1
- package/dist/src/component/manufacturing.d.ts +2 -0
- package/dist/src/component/manufacturing.js +41 -0
- package/dist/src/component/manufacturing.js.map +1 -0
- package/dist/src/component/register-default-groups.js +19 -2
- package/dist/src/component/register-default-groups.js.map +1 -1
- package/dist/src/component/shape.js +5 -29
- package/dist/src/component/shape.js.map +1 -1
- package/dist/src/component/storage.d.ts +2 -0
- package/dist/src/component/storage.js +27 -0
- package/dist/src/component/storage.js.map +1 -0
- package/dist/src/component/text-and-media.js +2 -25
- package/dist/src/component/text-and-media.js.map +1 -1
- package/dist/src/component/transport.d.ts +2 -0
- package/dist/src/component/transport.js +36 -0
- package/dist/src/component/transport.js.map +1 -0
- package/dist/src/component/warehouse.d.ts +1 -0
- package/dist/src/component/warehouse.js +8 -1
- package/dist/src/component/warehouse.js.map +1 -1
- package/dist/src/data-storage/board-model-cache.d.ts +30 -0
- package/dist/src/data-storage/board-model-cache.js +93 -0
- package/dist/src/data-storage/board-model-cache.js.map +1 -0
- package/dist/src/graphql/playback-buffer.d.ts +79 -0
- package/dist/src/graphql/playback-buffer.js +139 -0
- package/dist/src/graphql/playback-buffer.js.map +1 -0
- package/dist/src/graphql/playback-buffer.test.d.ts +1 -0
- package/dist/src/graphql/playback-buffer.test.js +261 -0
- package/dist/src/graphql/playback-buffer.test.js.map +1 -0
- package/dist/src/graphql/playback-subscription.d.ts +89 -0
- package/dist/src/graphql/playback-subscription.js +258 -0
- package/dist/src/graphql/playback-subscription.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/modeller/bulk-create-dialog.d.ts +51 -0
- package/dist/src/modeller/bulk-create-dialog.js +531 -0
- package/dist/src/modeller/bulk-create-dialog.js.map +1 -0
- package/dist/src/modeller/component-toolbar/component-menu.js +8 -1
- package/dist/src/modeller/component-toolbar/component-menu.js.map +1 -1
- package/dist/src/modeller/edit-toolbar-style.js +38 -1
- package/dist/src/modeller/edit-toolbar-style.js.map +1 -1
- package/dist/src/modeller/edit-toolbar.d.ts +21 -16
- package/dist/src/modeller/edit-toolbar.js +305 -201
- package/dist/src/modeller/edit-toolbar.js.map +1 -1
- package/dist/src/modeller/scene-viewer/ox-scene-viewer.d.ts +2 -1
- package/dist/src/modeller/scene-viewer/ox-scene-viewer.js +7 -11
- package/dist/src/modeller/scene-viewer/ox-scene-viewer.js.map +1 -1
- package/dist/src/ox-board-modeller.d.ts +8 -1
- package/dist/src/ox-board-modeller.js +125 -6
- package/dist/src/ox-board-modeller.js.map +1 -1
- package/dist/src/ox-board-preview.d.ts +36 -0
- package/dist/src/ox-board-preview.js +114 -0
- package/dist/src/ox-board-preview.js.map +1 -0
- package/dist/src/ox-board-template-list.d.ts +1 -0
- package/dist/src/ox-board-template-list.js +19 -1
- package/dist/src/ox-board-template-list.js.map +1 -1
- package/dist/src/ox-board-viewer.d.ts +50 -1
- package/dist/src/ox-board-viewer.js +271 -28
- package/dist/src/ox-board-viewer.js.map +1 -1
- package/dist/src/ox-playback-controls.d.ts +56 -0
- package/dist/src/ox-playback-controls.js +515 -0
- package/dist/src/ox-playback-controls.js.map +1 -0
- package/dist/src/selector/ox-board-selector.js +11 -1
- package/dist/src/selector/ox-board-selector.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -12
- package/translations/en.json +19 -1
- package/translations/ja.json +19 -1
- package/translations/ko.json +19 -1
- package/translations/ms.json +19 -1
- package/translations/zh.json +19 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Component, DataSubscriptionProvider } from '@hatiolab/things-scene';
|
|
2
|
+
export type PlaybackState = 'idle' | 'playing' | 'paused' | 'stopped';
|
|
3
|
+
export interface PlaybackStatus {
|
|
4
|
+
state: PlaybackState;
|
|
5
|
+
currentTime: string;
|
|
6
|
+
speed: number;
|
|
7
|
+
bufferedRanges?: {
|
|
8
|
+
from: number;
|
|
9
|
+
to: number;
|
|
10
|
+
}[];
|
|
11
|
+
}
|
|
12
|
+
export interface PlaybackConfig {
|
|
13
|
+
/** 플레이백 가능한 시간 범위 */
|
|
14
|
+
timeRange?: {
|
|
15
|
+
from: Date;
|
|
16
|
+
to: Date;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* PlaybackProvider — YouTube 스트리밍 방식의 청크 기반 플레이백
|
|
21
|
+
*
|
|
22
|
+
* - 10분 단위로 데이터를 fetch하여 버퍼에 적재
|
|
23
|
+
* - 남은 데이터 1분 이하 시 다음 10분 prefetch
|
|
24
|
+
* - seek 시 기존 버퍼 전체 폐기 후 fresh fetch
|
|
25
|
+
* - requestAnimationFrame 기반 재생 루프
|
|
26
|
+
*/
|
|
27
|
+
export declare class PlaybackProvider implements DataSubscriptionProvider {
|
|
28
|
+
private _components;
|
|
29
|
+
private _buffer;
|
|
30
|
+
private _state;
|
|
31
|
+
private _speed;
|
|
32
|
+
private _playHead;
|
|
33
|
+
private _lastFrameTime;
|
|
34
|
+
private _rafId;
|
|
35
|
+
private _lastDistributedSnapshot;
|
|
36
|
+
private _onStatusChange?;
|
|
37
|
+
private _totalRange;
|
|
38
|
+
constructor(onStatusChange?: (status: PlaybackStatus) => void);
|
|
39
|
+
get state(): PlaybackState;
|
|
40
|
+
get speed(): number;
|
|
41
|
+
get currentTime(): string;
|
|
42
|
+
/**
|
|
43
|
+
* DataSubscriptionProvider.subscribe 구현
|
|
44
|
+
*/
|
|
45
|
+
subscribe(tag: string, component: Component): Promise<{
|
|
46
|
+
unsubscribe: () => void;
|
|
47
|
+
}>;
|
|
48
|
+
/**
|
|
49
|
+
* 플레이백 시작
|
|
50
|
+
*/
|
|
51
|
+
start(fromTime: Date, speed?: number, totalRange?: {
|
|
52
|
+
from: Date;
|
|
53
|
+
to: Date;
|
|
54
|
+
}): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* 일시정지
|
|
57
|
+
*/
|
|
58
|
+
pause(): void;
|
|
59
|
+
/**
|
|
60
|
+
* 재개
|
|
61
|
+
*/
|
|
62
|
+
resume(): void;
|
|
63
|
+
/**
|
|
64
|
+
* seek — 기존 버퍼 폐기 + 새 위치에서 fresh fetch
|
|
65
|
+
*/
|
|
66
|
+
seek(toTime: Date): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* 배속 변경
|
|
69
|
+
*/
|
|
70
|
+
setSpeed(speed: number): void;
|
|
71
|
+
/**
|
|
72
|
+
* 중지
|
|
73
|
+
*/
|
|
74
|
+
stop(): void;
|
|
75
|
+
/**
|
|
76
|
+
* DataSubscriptionProvider.dispose 구현
|
|
77
|
+
*/
|
|
78
|
+
dispose(): void;
|
|
79
|
+
private _startPlayLoop;
|
|
80
|
+
private _stopPlayLoop;
|
|
81
|
+
private _playLoop;
|
|
82
|
+
private _distributeSnapshot;
|
|
83
|
+
private _createFetcher;
|
|
84
|
+
/**
|
|
85
|
+
* 백엔드 응답을 PlaybackSnapshot[] 형식으로 변환
|
|
86
|
+
*/
|
|
87
|
+
private _parsePlaybackResponse;
|
|
88
|
+
private _notifyStatus;
|
|
89
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import gql from 'graphql-tag';
|
|
2
|
+
import { PlaybackBuffer } from './playback-buffer.js';
|
|
3
|
+
/**
|
|
4
|
+
* PlaybackProvider — YouTube 스트리밍 방식의 청크 기반 플레이백
|
|
5
|
+
*
|
|
6
|
+
* - 10분 단위로 데이터를 fetch하여 버퍼에 적재
|
|
7
|
+
* - 남은 데이터 1분 이하 시 다음 10분 prefetch
|
|
8
|
+
* - seek 시 기존 버퍼 전체 폐기 후 fresh fetch
|
|
9
|
+
* - requestAnimationFrame 기반 재생 루프
|
|
10
|
+
*/
|
|
11
|
+
export class PlaybackProvider {
|
|
12
|
+
constructor(onStatusChange) {
|
|
13
|
+
this._components = new Map();
|
|
14
|
+
this._buffer = null;
|
|
15
|
+
this._state = 'idle';
|
|
16
|
+
this._speed = 1;
|
|
17
|
+
this._playHead = 0;
|
|
18
|
+
this._lastFrameTime = 0;
|
|
19
|
+
this._rafId = 0;
|
|
20
|
+
this._lastDistributedSnapshot = null;
|
|
21
|
+
this._totalRange = null;
|
|
22
|
+
this._playLoop = (now) => {
|
|
23
|
+
if (this._state !== 'playing' || !this._buffer)
|
|
24
|
+
return;
|
|
25
|
+
const delta = (now - this._lastFrameTime) * this._speed;
|
|
26
|
+
this._lastFrameTime = now;
|
|
27
|
+
this._playHead += delta;
|
|
28
|
+
// 전체 범위 끝 도달 체크
|
|
29
|
+
if (this._totalRange && this._playHead >= this._totalRange.to.getTime()) {
|
|
30
|
+
this._playHead = this._totalRange.to.getTime();
|
|
31
|
+
this._state = 'stopped';
|
|
32
|
+
this._notifyStatus();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// 현재 스냅샷 배포
|
|
36
|
+
const snap = this._buffer.getSnapshotAt(this._playHead);
|
|
37
|
+
if (snap && snap !== this._lastDistributedSnapshot) {
|
|
38
|
+
this._distributeSnapshot(snap.data);
|
|
39
|
+
this._lastDistributedSnapshot = snap;
|
|
40
|
+
}
|
|
41
|
+
// prefetch 체크 (비동기, 루프 블로킹 안 함)
|
|
42
|
+
this._buffer.checkPrefetch(this._playHead);
|
|
43
|
+
this._notifyStatus();
|
|
44
|
+
// 다음 프레임
|
|
45
|
+
this._rafId = requestAnimationFrame(this._playLoop);
|
|
46
|
+
};
|
|
47
|
+
this._onStatusChange = onStatusChange;
|
|
48
|
+
}
|
|
49
|
+
get state() { return this._state; }
|
|
50
|
+
get speed() { return this._speed; }
|
|
51
|
+
get currentTime() { return this._playHead ? new Date(this._playHead).toISOString() : ''; }
|
|
52
|
+
/**
|
|
53
|
+
* DataSubscriptionProvider.subscribe 구현
|
|
54
|
+
*/
|
|
55
|
+
async subscribe(tag, component) {
|
|
56
|
+
if (!this._components.has(tag)) {
|
|
57
|
+
this._components.set(tag, new Set());
|
|
58
|
+
}
|
|
59
|
+
this._components.get(tag).add(component);
|
|
60
|
+
return {
|
|
61
|
+
unsubscribe: () => {
|
|
62
|
+
const components = this._components.get(tag);
|
|
63
|
+
if (components) {
|
|
64
|
+
components.delete(component);
|
|
65
|
+
if (components.size === 0) {
|
|
66
|
+
this._components.delete(tag);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 플레이백 시작
|
|
74
|
+
*/
|
|
75
|
+
async start(fromTime, speed = 1, totalRange) {
|
|
76
|
+
this.stop();
|
|
77
|
+
this._speed = speed;
|
|
78
|
+
this._totalRange = totalRange || { from: fromTime, to: new Date(fromTime.getTime() + 3600000) };
|
|
79
|
+
// 버퍼 생성 + 초기 로드
|
|
80
|
+
this._buffer = new PlaybackBuffer(this._createFetcher(), this._totalRange);
|
|
81
|
+
await this._buffer.loadInitial(fromTime.getTime());
|
|
82
|
+
this._playHead = fromTime.getTime();
|
|
83
|
+
this._state = 'playing';
|
|
84
|
+
this._lastFrameTime = performance.now();
|
|
85
|
+
this._startPlayLoop();
|
|
86
|
+
this._notifyStatus();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 일시정지
|
|
90
|
+
*/
|
|
91
|
+
pause() {
|
|
92
|
+
if (this._state !== 'playing')
|
|
93
|
+
return;
|
|
94
|
+
this._state = 'paused';
|
|
95
|
+
this._stopPlayLoop();
|
|
96
|
+
this._notifyStatus();
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 재개
|
|
100
|
+
*/
|
|
101
|
+
resume() {
|
|
102
|
+
if (this._state !== 'paused')
|
|
103
|
+
return;
|
|
104
|
+
this._state = 'playing';
|
|
105
|
+
this._lastFrameTime = performance.now();
|
|
106
|
+
this._startPlayLoop();
|
|
107
|
+
this._notifyStatus();
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* seek — 기존 버퍼 폐기 + 새 위치에서 fresh fetch
|
|
111
|
+
*/
|
|
112
|
+
async seek(toTime) {
|
|
113
|
+
if (!this._buffer)
|
|
114
|
+
return;
|
|
115
|
+
const wasPlaying = this._state === 'playing';
|
|
116
|
+
this._stopPlayLoop();
|
|
117
|
+
// 버퍼 전체 폐기 + 새 위치 로드
|
|
118
|
+
await this._buffer.seek(toTime.getTime());
|
|
119
|
+
this._playHead = toTime.getTime();
|
|
120
|
+
this._lastDistributedSnapshot = null;
|
|
121
|
+
// 새 위치의 첫 스냅샷 즉시 배포
|
|
122
|
+
const snap = this._buffer.getSnapshotAt(this._playHead);
|
|
123
|
+
if (snap) {
|
|
124
|
+
this._distributeSnapshot(snap.data);
|
|
125
|
+
this._lastDistributedSnapshot = snap;
|
|
126
|
+
}
|
|
127
|
+
if (wasPlaying) {
|
|
128
|
+
this._state = 'playing';
|
|
129
|
+
this._lastFrameTime = performance.now();
|
|
130
|
+
this._startPlayLoop();
|
|
131
|
+
}
|
|
132
|
+
this._notifyStatus();
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 배속 변경
|
|
136
|
+
*/
|
|
137
|
+
setSpeed(speed) {
|
|
138
|
+
this._speed = speed;
|
|
139
|
+
this._notifyStatus();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 중지
|
|
143
|
+
*/
|
|
144
|
+
stop() {
|
|
145
|
+
var _a;
|
|
146
|
+
this._stopPlayLoop();
|
|
147
|
+
(_a = this._buffer) === null || _a === void 0 ? void 0 : _a.clear();
|
|
148
|
+
this._buffer = null;
|
|
149
|
+
this._state = 'stopped';
|
|
150
|
+
this._playHead = 0;
|
|
151
|
+
this._lastDistributedSnapshot = null;
|
|
152
|
+
this._notifyStatus();
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* DataSubscriptionProvider.dispose 구현
|
|
156
|
+
*/
|
|
157
|
+
dispose() {
|
|
158
|
+
this.stop();
|
|
159
|
+
this._state = 'idle';
|
|
160
|
+
this._components.clear();
|
|
161
|
+
}
|
|
162
|
+
// --- 재생 루프 ---
|
|
163
|
+
_startPlayLoop() {
|
|
164
|
+
this._rafId = requestAnimationFrame(this._playLoop);
|
|
165
|
+
}
|
|
166
|
+
_stopPlayLoop() {
|
|
167
|
+
if (this._rafId) {
|
|
168
|
+
cancelAnimationFrame(this._rafId);
|
|
169
|
+
this._rafId = 0;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// --- 데이터 배포 ---
|
|
173
|
+
_distributeSnapshot(snapshotData) {
|
|
174
|
+
if (!snapshotData || typeof snapshotData !== 'object')
|
|
175
|
+
return;
|
|
176
|
+
for (const [tag, value] of Object.entries(snapshotData)) {
|
|
177
|
+
const components = this._components.get(tag);
|
|
178
|
+
if (components) {
|
|
179
|
+
for (const component of components) {
|
|
180
|
+
component.data = value;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// --- 데이터 fetcher ---
|
|
186
|
+
_createFetcher() {
|
|
187
|
+
return async (fromTime, toTime) => {
|
|
188
|
+
var _a;
|
|
189
|
+
try {
|
|
190
|
+
const { client } = await import('@operato/graphql');
|
|
191
|
+
const response = await client.query({
|
|
192
|
+
query: gql `
|
|
193
|
+
query PlaybackChunk($startTime: String!, $endTime: String!) {
|
|
194
|
+
playback(startTime: $startTime, endTime: $endTime) {
|
|
195
|
+
status
|
|
196
|
+
playback {
|
|
197
|
+
scenarios {
|
|
198
|
+
scenarioName
|
|
199
|
+
snapshots {
|
|
200
|
+
type
|
|
201
|
+
time
|
|
202
|
+
data
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
`,
|
|
209
|
+
variables: {
|
|
210
|
+
startTime: fromTime.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ''),
|
|
211
|
+
endTime: toTime.toISOString().replace('T', ' ').replace(/\.\d+Z$/, '')
|
|
212
|
+
},
|
|
213
|
+
fetchPolicy: 'no-cache'
|
|
214
|
+
});
|
|
215
|
+
return this._parsePlaybackResponse((_a = response.data) === null || _a === void 0 ? void 0 : _a.playback);
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
console.error('[PlaybackProvider] chunk fetch error:', e);
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* 백엔드 응답을 PlaybackSnapshot[] 형식으로 변환
|
|
225
|
+
*/
|
|
226
|
+
_parsePlaybackResponse(response) {
|
|
227
|
+
var _a;
|
|
228
|
+
if (!(response === null || response === void 0 ? void 0 : response.status) || !((_a = response === null || response === void 0 ? void 0 : response.playback) === null || _a === void 0 ? void 0 : _a.scenarios))
|
|
229
|
+
return [];
|
|
230
|
+
const snapshotMap = new Map();
|
|
231
|
+
for (const scenario of response.playback.scenarios) {
|
|
232
|
+
for (const record of scenario.snapshots) {
|
|
233
|
+
const time = new Date(record.time.replace(' ', 'T')).getTime();
|
|
234
|
+
const data = typeof record.data === 'string' ? JSON.parse(record.data) : record.data;
|
|
235
|
+
if (!snapshotMap.has(time)) {
|
|
236
|
+
snapshotMap.set(time, {});
|
|
237
|
+
}
|
|
238
|
+
const merged = snapshotMap.get(time);
|
|
239
|
+
// scenarioName을 tag로 사용
|
|
240
|
+
merged[scenario.scenarioName] = data;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return Array.from(snapshotMap.entries())
|
|
244
|
+
.map(([timestamp, data]) => ({ timestamp, data }))
|
|
245
|
+
.sort((a, b) => a.timestamp - b.timestamp);
|
|
246
|
+
}
|
|
247
|
+
// --- 상태 통보 ---
|
|
248
|
+
_notifyStatus() {
|
|
249
|
+
var _a, _b;
|
|
250
|
+
(_a = this._onStatusChange) === null || _a === void 0 ? void 0 : _a.call(this, {
|
|
251
|
+
state: this._state,
|
|
252
|
+
currentTime: this._playHead ? new Date(this._playHead).toISOString() : '',
|
|
253
|
+
speed: this._speed,
|
|
254
|
+
bufferedRanges: (_b = this._buffer) === null || _b === void 0 ? void 0 : _b.getBufferedRanges()
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
//# sourceMappingURL=playback-subscription.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playback-subscription.js","sourceRoot":"","sources":["../../../src/graphql/playback-subscription.ts"],"names":[],"mappings":"AACA,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,cAAc,EAA4C,MAAM,sBAAsB,CAAA;AAgB/F;;;;;;;GAOG;AACH,MAAM,OAAO,gBAAgB;IAY3B,YAAY,cAAiD;QAXrD,gBAAW,GAAgC,IAAI,GAAG,EAAE,CAAA;QACpD,YAAO,GAA0B,IAAI,CAAA;QACrC,WAAM,GAAkB,MAAM,CAAA;QAC9B,WAAM,GAAW,CAAC,CAAA;QAClB,cAAS,GAAW,CAAC,CAAA;QACrB,mBAAc,GAAW,CAAC,CAAA;QAC1B,WAAM,GAAW,CAAC,CAAA;QAClB,6BAAwB,GAA4B,IAAI,CAAA;QAExD,gBAAW,GAAoC,IAAI,CAAA;QAmJnD,cAAS,GAAG,CAAC,GAAW,EAAE,EAAE;YAClC,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,OAAO;gBAAE,OAAM;YAEtD,MAAM,KAAK,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,IAAI,CAAC,MAAM,CAAA;YACvD,IAAI,CAAC,cAAc,GAAG,GAAG,CAAA;YACzB,IAAI,CAAC,SAAS,IAAI,KAAK,CAAA;YAEvB,gBAAgB;YAChB,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC;gBACxE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,CAAA;gBAC9C,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;gBACvB,IAAI,CAAC,aAAa,EAAE,CAAA;gBACpB,OAAM;YACR,CAAC;YAED,YAAY;YACZ,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;YACvD,IAAI,IAAI,IAAI,IAAI,KAAK,IAAI,CAAC,wBAAwB,EAAE,CAAC;gBACnD,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBACnC,IAAI,CAAC,wBAAwB,GAAG,IAAI,CAAA;YACtC,CAAC;YAED,gCAAgC;YAChC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;YAE1C,IAAI,CAAC,aAAa,EAAE,CAAA;YAEpB,SAAS;YACT,IAAI,CAAC,MAAM,GAAG,qBAAqB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACrD,CAAC,CAAA;QA7KC,IAAI,CAAC,eAAe,GAAG,cAAc,CAAA;IACvC,CAAC;IAED,IAAI,KAAK,KAAK,OAAO,IAAI,CAAC,MAAM,CAAA,CAAC,CAAC;IAClC,IAAI,KAAK,KAAK,OAAO,IAAI,CAAC,MAAM,CAAA,CAAC,CAAC;IAClC,IAAI,WAAW,KAAK,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA,CAAC,CAAC;IAEzF;;OAEG;IACH,KAAK,CAAC,SAAS,CAAC,GAAW,EAAE,SAAoB;QAC/C,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/B,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,GAAG,EAAE,CAAC,CAAA;QACtC,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QAEzC,OAAO;YACL,WAAW,EAAE,GAAG,EAAE;gBAChB,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;gBAC5C,IAAI,UAAU,EAAE,CAAC;oBACf,UAAU,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;oBAC5B,IAAI,UAAU,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;wBAC1B,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;oBAC9B,CAAC;gBACH,CAAC;YACH,CAAC;SACF,CAAA;IACH,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,KAAK,CAAC,QAAc,EAAE,QAAgB,CAAC,EAAE,UAAqC;QAClF,IAAI,CAAC,IAAI,EAAE,CAAA;QAEX,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;QACnB,IAAI,CAAC,WAAW,GAAG,UAAU,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,OAAO,CAAC,EAAE,CAAA;QAE/F,gBAAgB;QAChB,IAAI,CAAC,OAAO,GAAG,IAAI,cAAc,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,IAAI,CAAC,WAAW,CAAC,CAAA;QAC1E,MAAM,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAA;QAElD,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAA;QACnC,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;QACvB,IAAI,CAAC,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE,CAAA;QAEvC,IAAI,CAAC,cAAc,EAAE,CAAA;QACrB,IAAI,CAAC,aAAa,EAAE,CAAA;IACtB,CAAC;IAED;;OAEG;IACH,KAAK;QACH,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS;YAAE,OAAM;QACrC,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAA;QACtB,IAAI,CAAC,aAAa,EAAE,CAAA;QACpB,IAAI,CAAC,aAAa,EAAE,CAAA;IACtB,CAAC;IAED;;OAEG;IACH,MAAM;QACJ,IAAI,IAAI,CAAC,MAAM,KAAK,QAAQ;YAAE,OAAM;QACpC,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;QACvB,IAAI,CAAC,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE,CAAA;QACvC,IAAI,CAAC,cAAc,EAAE,CAAA;QACrB,IAAI,CAAC,aAAa,EAAE,CAAA;IACtB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,IAAI,CAAC,MAAY;QACrB,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAM;QAEzB,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,KAAK,SAAS,CAAA;QAC5C,IAAI,CAAC,aAAa,EAAE,CAAA;QAEpB,qBAAqB;QACrB,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAA;QACzC,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,OAAO,EAAE,CAAA;QACjC,IAAI,CAAC,wBAAwB,GAAG,IAAI,CAAA;QAEpC,oBAAoB;QACpB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACvD,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACnC,IAAI,CAAC,wBAAwB,GAAG,IAAI,CAAA;QACtC,CAAC;QAED,IAAI,UAAU,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;YACvB,IAAI,CAAC,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE,CAAA;YACvC,IAAI,CAAC,cAAc,EAAE,CAAA;QACvB,CAAC;QAED,IAAI,CAAC,aAAa,EAAE,CAAA;IACtB,CAAC;IAED;;OAEG;IACH,QAAQ,CAAC,KAAa;QACpB,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;QACnB,IAAI,CAAC,aAAa,EAAE,CAAA;IACtB,CAAC;IAED;;OAEG;IACH,IAAI;;QACF,IAAI,CAAC,aAAa,EAAE,CAAA;QACpB,MAAA,IAAI,CAAC,OAAO,0CAAE,KAAK,EAAE,CAAA;QACrB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAA;QACnB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;QACvB,IAAI,CAAC,SAAS,GAAG,CAAC,CAAA;QAClB,IAAI,CAAC,wBAAwB,GAAG,IAAI,CAAA;QACpC,IAAI,CAAC,aAAa,EAAE,CAAA;IACtB,CAAC;IAED;;OAEG;IACH,OAAO;QACL,IAAI,CAAC,IAAI,EAAE,CAAA;QACX,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAA;IAC1B,CAAC;IAED,gBAAgB;IAER,cAAc;QACpB,IAAI,CAAC,MAAM,GAAG,qBAAqB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IACrD,CAAC;IAEO,aAAa;QACnB,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,oBAAoB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YACjC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAA;QACjB,CAAC;IACH,CAAC;IAiCD,iBAAiB;IAET,mBAAmB,CAAC,YAAiB;QAC3C,IAAI,CAAC,YAAY,IAAI,OAAO,YAAY,KAAK,QAAQ;YAAE,OAAM;QAE7D,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;YACxD,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YAC5C,IAAI,UAAU,EAAE,CAAC;gBACf,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;oBACnC,SAAS,CAAC,IAAI,GAAG,KAAK,CAAA;gBACxB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,sBAAsB;IAEd,cAAc;QACpB,OAAO,KAAK,EAAE,QAAc,EAAE,MAAY,EAA+B,EAAE;;YACzE,IAAI,CAAC;gBACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAA;gBACnD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;oBAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;;;;WAgBT;oBACD,SAAS,EAAE;wBACT,SAAS,EAAE,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC;wBAC1E,OAAO,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC;qBACvE;oBACD,WAAW,EAAE,UAAU;iBACxB,CAAC,CAAA;gBAEF,OAAO,IAAI,CAAC,sBAAsB,CAAC,MAAA,QAAQ,CAAC,IAAI,0CAAE,QAAQ,CAAC,CAAA;YAC7D,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,OAAO,CAAC,KAAK,CAAC,uCAAuC,EAAE,CAAC,CAAC,CAAA;gBACzD,OAAO,EAAE,CAAA;YACX,CAAC;QACH,CAAC,CAAA;IACH,CAAC;IAED;;OAEG;IACK,sBAAsB,CAAC,QAAa;;QAC1C,IAAI,CAAC,CAAA,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAE,MAAM,CAAA,IAAI,CAAC,CAAA,MAAA,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAE,QAAQ,0CAAE,SAAS,CAAA;YAAE,OAAO,EAAE,CAAA;QAElE,MAAM,WAAW,GAAG,IAAI,GAAG,EAA+B,CAAA;QAE1D,KAAK,MAAM,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC;YACnD,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,SAAS,EAAE,CAAC;gBACxC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,CAAA;gBAC9D,MAAM,IAAI,GAAG,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAA;gBAEpF,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC3B,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;gBAC3B,CAAC;gBACD,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAE,CAAA;gBACrC,wBAAwB;gBACxB,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,GAAG,IAAI,CAAA;YACtC,CAAC;QACH,CAAC;QAED,OAAO,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC;aACrC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;aACjD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC,CAAA;IAC9C,CAAC;IAED,gBAAgB;IAER,aAAa;;QACnB,MAAA,IAAI,CAAC,eAAe,qDAAG;YACrB,KAAK,EAAE,IAAI,CAAC,MAAM;YAClB,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE;YACzE,KAAK,EAAE,IAAI,CAAC,MAAM;YAClB,cAAc,EAAE,MAAA,IAAI,CAAC,OAAO,0CAAE,iBAAiB,EAAE;SAClD,CAAC,CAAA;IACJ,CAAC;CACF","sourcesContent":["import { Component, DataSubscriptionProvider } from '@hatiolab/things-scene'\nimport gql from 'graphql-tag'\nimport { PlaybackBuffer, type PlaybackSnapshot, type ChunkFetcher } from './playback-buffer.js'\n\nexport type PlaybackState = 'idle' | 'playing' | 'paused' | 'stopped'\n\nexport interface PlaybackStatus {\n state: PlaybackState\n currentTime: string\n speed: number\n bufferedRanges?: { from: number; to: number }[]\n}\n\nexport interface PlaybackConfig {\n /** 플레이백 가능한 시간 범위 */\n timeRange?: { from: Date; to: Date }\n}\n\n/**\n * PlaybackProvider — YouTube 스트리밍 방식의 청크 기반 플레이백\n *\n * - 10분 단위로 데이터를 fetch하여 버퍼에 적재\n * - 남은 데이터 1분 이하 시 다음 10분 prefetch\n * - seek 시 기존 버퍼 전체 폐기 후 fresh fetch\n * - requestAnimationFrame 기반 재생 루프\n */\nexport class PlaybackProvider implements DataSubscriptionProvider {\n private _components: Map<string, Set<Component>> = new Map()\n private _buffer: PlaybackBuffer | null = null\n private _state: PlaybackState = 'idle'\n private _speed: number = 1\n private _playHead: number = 0\n private _lastFrameTime: number = 0\n private _rafId: number = 0\n private _lastDistributedSnapshot: PlaybackSnapshot | null = null\n private _onStatusChange?: (status: PlaybackStatus) => void\n private _totalRange: { from: Date; to: Date } | null = null\n\n constructor(onStatusChange?: (status: PlaybackStatus) => void) {\n this._onStatusChange = onStatusChange\n }\n\n get state() { return this._state }\n get speed() { return this._speed }\n get currentTime() { return this._playHead ? new Date(this._playHead).toISOString() : '' }\n\n /**\n * DataSubscriptionProvider.subscribe 구현\n */\n async subscribe(tag: string, component: Component) {\n if (!this._components.has(tag)) {\n this._components.set(tag, new Set())\n }\n this._components.get(tag)!.add(component)\n\n return {\n unsubscribe: () => {\n const components = this._components.get(tag)\n if (components) {\n components.delete(component)\n if (components.size === 0) {\n this._components.delete(tag)\n }\n }\n }\n }\n }\n\n /**\n * 플레이백 시작\n */\n async start(fromTime: Date, speed: number = 1, totalRange?: { from: Date; to: Date }) {\n this.stop()\n\n this._speed = speed\n this._totalRange = totalRange || { from: fromTime, to: new Date(fromTime.getTime() + 3600000) }\n\n // 버퍼 생성 + 초기 로드\n this._buffer = new PlaybackBuffer(this._createFetcher(), this._totalRange)\n await this._buffer.loadInitial(fromTime.getTime())\n\n this._playHead = fromTime.getTime()\n this._state = 'playing'\n this._lastFrameTime = performance.now()\n\n this._startPlayLoop()\n this._notifyStatus()\n }\n\n /**\n * 일시정지\n */\n pause() {\n if (this._state !== 'playing') return\n this._state = 'paused'\n this._stopPlayLoop()\n this._notifyStatus()\n }\n\n /**\n * 재개\n */\n resume() {\n if (this._state !== 'paused') return\n this._state = 'playing'\n this._lastFrameTime = performance.now()\n this._startPlayLoop()\n this._notifyStatus()\n }\n\n /**\n * seek — 기존 버퍼 폐기 + 새 위치에서 fresh fetch\n */\n async seek(toTime: Date) {\n if (!this._buffer) return\n\n const wasPlaying = this._state === 'playing'\n this._stopPlayLoop()\n\n // 버퍼 전체 폐기 + 새 위치 로드\n await this._buffer.seek(toTime.getTime())\n this._playHead = toTime.getTime()\n this._lastDistributedSnapshot = null\n\n // 새 위치의 첫 스냅샷 즉시 배포\n const snap = this._buffer.getSnapshotAt(this._playHead)\n if (snap) {\n this._distributeSnapshot(snap.data)\n this._lastDistributedSnapshot = snap\n }\n\n if (wasPlaying) {\n this._state = 'playing'\n this._lastFrameTime = performance.now()\n this._startPlayLoop()\n }\n\n this._notifyStatus()\n }\n\n /**\n * 배속 변경\n */\n setSpeed(speed: number) {\n this._speed = speed\n this._notifyStatus()\n }\n\n /**\n * 중지\n */\n stop() {\n this._stopPlayLoop()\n this._buffer?.clear()\n this._buffer = null\n this._state = 'stopped'\n this._playHead = 0\n this._lastDistributedSnapshot = null\n this._notifyStatus()\n }\n\n /**\n * DataSubscriptionProvider.dispose 구현\n */\n dispose() {\n this.stop()\n this._state = 'idle'\n this._components.clear()\n }\n\n // --- 재생 루프 ---\n\n private _startPlayLoop() {\n this._rafId = requestAnimationFrame(this._playLoop)\n }\n\n private _stopPlayLoop() {\n if (this._rafId) {\n cancelAnimationFrame(this._rafId)\n this._rafId = 0\n }\n }\n\n private _playLoop = (now: number) => {\n if (this._state !== 'playing' || !this._buffer) return\n\n const delta = (now - this._lastFrameTime) * this._speed\n this._lastFrameTime = now\n this._playHead += delta\n\n // 전체 범위 끝 도달 체크\n if (this._totalRange && this._playHead >= this._totalRange.to.getTime()) {\n this._playHead = this._totalRange.to.getTime()\n this._state = 'stopped'\n this._notifyStatus()\n return\n }\n\n // 현재 스냅샷 배포\n const snap = this._buffer.getSnapshotAt(this._playHead)\n if (snap && snap !== this._lastDistributedSnapshot) {\n this._distributeSnapshot(snap.data)\n this._lastDistributedSnapshot = snap\n }\n\n // prefetch 체크 (비동기, 루프 블로킹 안 함)\n this._buffer.checkPrefetch(this._playHead)\n\n this._notifyStatus()\n\n // 다음 프레임\n this._rafId = requestAnimationFrame(this._playLoop)\n }\n\n // --- 데이터 배포 ---\n\n private _distributeSnapshot(snapshotData: any) {\n if (!snapshotData || typeof snapshotData !== 'object') return\n\n for (const [tag, value] of Object.entries(snapshotData)) {\n const components = this._components.get(tag)\n if (components) {\n for (const component of components) {\n component.data = value\n }\n }\n }\n }\n\n // --- 데이터 fetcher ---\n\n private _createFetcher(): ChunkFetcher {\n return async (fromTime: Date, toTime: Date): Promise<PlaybackSnapshot[]> => {\n try {\n const { client } = await import('@operato/graphql')\n const response = await client.query({\n query: gql`\n query PlaybackChunk($startTime: String!, $endTime: String!) {\n playback(startTime: $startTime, endTime: $endTime) {\n status\n playback {\n scenarios {\n scenarioName\n snapshots {\n type\n time\n data\n }\n }\n }\n }\n }\n `,\n variables: {\n startTime: fromTime.toISOString().replace('T', ' ').replace(/\\.\\d+Z$/, ''),\n endTime: toTime.toISOString().replace('T', ' ').replace(/\\.\\d+Z$/, '')\n },\n fetchPolicy: 'no-cache'\n })\n\n return this._parsePlaybackResponse(response.data?.playback)\n } catch (e) {\n console.error('[PlaybackProvider] chunk fetch error:', e)\n return []\n }\n }\n }\n\n /**\n * 백엔드 응답을 PlaybackSnapshot[] 형식으로 변환\n */\n private _parsePlaybackResponse(response: any): PlaybackSnapshot[] {\n if (!response?.status || !response?.playback?.scenarios) return []\n\n const snapshotMap = new Map<number, Record<string, any>>()\n\n for (const scenario of response.playback.scenarios) {\n for (const record of scenario.snapshots) {\n const time = new Date(record.time.replace(' ', 'T')).getTime()\n const data = typeof record.data === 'string' ? JSON.parse(record.data) : record.data\n\n if (!snapshotMap.has(time)) {\n snapshotMap.set(time, {})\n }\n const merged = snapshotMap.get(time)!\n // scenarioName을 tag로 사용\n merged[scenario.scenarioName] = data\n }\n }\n\n return Array.from(snapshotMap.entries())\n .map(([timestamp, data]) => ({ timestamp, data }))\n .sort((a, b) => a.timestamp - b.timestamp)\n }\n\n // --- 상태 통보 ---\n\n private _notifyStatus() {\n this._onStatusChange?.({\n state: this._state,\n currentTime: this._playHead ? new Date(this._playHead).toISOString() : '',\n speed: this._speed,\n bufferedRanges: this._buffer?.getBufferedRanges()\n })\n }\n}\n"]}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export * from './types.js';
|
|
2
2
|
export { registerDefaultGroups } from './component/register-default-groups.js';
|
|
3
3
|
export { BoardDataStorage, PlaylistStorage } from './data-storage/data-storage.js';
|
|
4
|
+
export { BoardModelCache } from './data-storage/board-model-cache.js';
|
|
5
|
+
export type { CachedBoard } from './data-storage/board-model-cache.js';
|
|
4
6
|
export * from './ox-board-viewer.js';
|
|
7
|
+
export * from './ox-board-preview.js';
|
|
5
8
|
export * from './ox-board-player.js';
|
|
6
9
|
export * from './ox-board-modeller.js';
|
|
7
10
|
export * from './ox-board-list.js';
|
package/dist/src/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export * from './types.js';
|
|
2
2
|
export { registerDefaultGroups } from './component/register-default-groups.js';
|
|
3
3
|
export { BoardDataStorage, PlaylistStorage } from './data-storage/data-storage.js';
|
|
4
|
+
export { BoardModelCache } from './data-storage/board-model-cache.js';
|
|
4
5
|
export * from './ox-board-viewer.js';
|
|
6
|
+
export * from './ox-board-preview.js';
|
|
5
7
|
export * from './ox-board-player.js';
|
|
6
8
|
export * from './ox-board-modeller.js';
|
|
7
9
|
export * from './ox-board-list.js';
|
package/dist/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAE1B,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAA;AAC9E,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAE1B,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAA;AAC9E,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAA;AAClF,OAAO,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAA;AAGrE,cAAc,sBAAsB,CAAA;AACpC,cAAc,uBAAuB,CAAA;AACrC,cAAc,sBAAsB,CAAA;AACpC,cAAc,wBAAwB,CAAA;AACtC,cAAc,oBAAoB,CAAA;AAClC,cAAc,+BAA+B,CAAA","sourcesContent":["export * from './types.js'\n\nexport { registerDefaultGroups } from './component/register-default-groups.js'\nexport { BoardDataStorage, PlaylistStorage } from './data-storage/data-storage.js'\nexport { BoardModelCache } from './data-storage/board-model-cache.js'\nexport type { CachedBoard } from './data-storage/board-model-cache.js'\n\nexport * from './ox-board-viewer.js'\nexport * from './ox-board-preview.js'\nexport * from './ox-board-player.js'\nexport * from './ox-board-modeller.js'\nexport * from './ox-board-list.js'\nexport * from './ox-board-template-viewer.js'\n"]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import { generateBulkComponents, type BulkCreateArea as Area } from '@hatiolab/things-scene';
|
|
3
|
+
export type ConflictPolicy = 'skip' | 'duplicate';
|
|
4
|
+
export interface BulkCreateDialogProps {
|
|
5
|
+
/** 템플릿 컴포넌트 state (read-only 표시용). */
|
|
6
|
+
template: Record<string, any>;
|
|
7
|
+
/** 초기 영역 (보드 좌표). 사용자가 dialog 내에서 조정 가능. */
|
|
8
|
+
initialArea: Area;
|
|
9
|
+
/** 기존 scene 의 모든 ID — 충돌 검사. */
|
|
10
|
+
existingIds?: Set<string>;
|
|
11
|
+
/** OK 클릭 시 콜백 — 생성할 model 배열 전달. */
|
|
12
|
+
onConfirm: (models: ReturnType<typeof generateBulkComponents>) => void;
|
|
13
|
+
/** 취소 / 닫기 콜백. */
|
|
14
|
+
onCancel?: () => void;
|
|
15
|
+
}
|
|
16
|
+
export declare class BulkCreateDialog extends LitElement {
|
|
17
|
+
static styles: import("lit").CSSResult;
|
|
18
|
+
template: Record<string, any>;
|
|
19
|
+
initialArea: Area;
|
|
20
|
+
existingIds: Set<string>;
|
|
21
|
+
onConfirm: BulkCreateDialogProps['onConfirm'];
|
|
22
|
+
onCancel?: BulkCreateDialogProps['onCancel'];
|
|
23
|
+
private pattern;
|
|
24
|
+
private count;
|
|
25
|
+
private rows;
|
|
26
|
+
private cols;
|
|
27
|
+
private idPattern;
|
|
28
|
+
private idStart;
|
|
29
|
+
private idPadding;
|
|
30
|
+
private area;
|
|
31
|
+
private conflictPolicy;
|
|
32
|
+
countInput: HTMLInputElement;
|
|
33
|
+
connectedCallback(): void;
|
|
34
|
+
/** rows/cols 가 미입력 (undefined) 이면 영역 종횡비 + count 로 자동 계산해 표시. */
|
|
35
|
+
private get displayRows();
|
|
36
|
+
private get displayCols();
|
|
37
|
+
firstUpdated(): void;
|
|
38
|
+
private updateArea;
|
|
39
|
+
private get previewModels();
|
|
40
|
+
private get conflicts();
|
|
41
|
+
/** 충돌 정책 적용 후 실제 생성될 model 배열. */
|
|
42
|
+
private get effectiveModels();
|
|
43
|
+
private onOk;
|
|
44
|
+
private onCancelClick;
|
|
45
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
46
|
+
}
|
|
47
|
+
declare global {
|
|
48
|
+
interface HTMLElementTagNameMap {
|
|
49
|
+
'bulk-create-dialog': BulkCreateDialog;
|
|
50
|
+
}
|
|
51
|
+
}
|