@operato/board 10.0.0-beta.2 → 10.0.0-beta.20

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 (31) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/dist/src/data-storage/board-model-cache.d.ts +30 -0
  3. package/dist/src/data-storage/board-model-cache.js +93 -0
  4. package/dist/src/data-storage/board-model-cache.js.map +1 -0
  5. package/dist/src/graphql/playback-subscription.d.ts +74 -0
  6. package/dist/src/graphql/playback-subscription.js +191 -0
  7. package/dist/src/graphql/playback-subscription.js.map +1 -0
  8. package/dist/src/index.d.ts +2 -0
  9. package/dist/src/index.js +1 -0
  10. package/dist/src/index.js.map +1 -1
  11. package/dist/src/modeller/edit-toolbar-style.js +38 -1
  12. package/dist/src/modeller/edit-toolbar-style.js.map +1 -1
  13. package/dist/src/modeller/edit-toolbar.d.ts +7 -16
  14. package/dist/src/modeller/edit-toolbar.js +197 -199
  15. package/dist/src/modeller/edit-toolbar.js.map +1 -1
  16. package/dist/src/modeller/scene-viewer/ox-scene-viewer.d.ts +2 -1
  17. package/dist/src/modeller/scene-viewer/ox-scene-viewer.js +7 -11
  18. package/dist/src/modeller/scene-viewer/ox-scene-viewer.js.map +1 -1
  19. package/dist/src/ox-board-modeller.d.ts +5 -1
  20. package/dist/src/ox-board-modeller.js +82 -5
  21. package/dist/src/ox-board-modeller.js.map +1 -1
  22. package/dist/src/ox-board-viewer.d.ts +50 -1
  23. package/dist/src/ox-board-viewer.js +269 -25
  24. package/dist/src/ox-board-viewer.js.map +1 -1
  25. package/dist/src/ox-playback-controls.d.ts +48 -0
  26. package/dist/src/ox-playback-controls.js +419 -0
  27. package/dist/src/ox-playback-controls.js.map +1 -0
  28. package/dist/src/selector/ox-board-selector.js +11 -1
  29. package/dist/src/selector/ox-board-selector.js.map +1 -1
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +12 -12
package/CHANGELOG.md CHANGED
@@ -3,6 +3,173 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [10.0.0-beta.20](https://github.com/hatiolab/operato/compare/v10.0.0-beta.19...v10.0.0-beta.20) (2026-03-29)
7
+
8
+
9
+ ### :house: Code Refactoring
10
+
11
+ * **board:** fab를 expand/collapse 패턴으로 통일 ([ef369ef](https://github.com/hatiolab/operato/commit/ef369ef959780f52499a3ce08fae3f10a7d2a910))
12
+
13
+
14
+
15
+ ## [10.0.0-beta.19](https://github.com/hatiolab/operato/compare/v10.0.0-beta.18...v10.0.0-beta.19) (2026-03-29)
16
+
17
+
18
+ ### :rocket: New Features
19
+
20
+ * **board:** viewer 3D 토글 fab 추가, 보드 선택 카드 강조 + 자동 스크롤 ([9c5c14c](https://github.com/hatiolab/operato/commit/9c5c14c9dfcb69d3830f8e50c1e5a6d82de5f419))
21
+
22
+
23
+
24
+ ## [10.0.0-beta.18](https://github.com/hatiolab/operato/compare/v10.0.0-beta.17...v10.0.0-beta.18) (2026-03-25)
25
+
26
+ **Note:** Version bump only for package @operato/board
27
+
28
+
29
+
30
+
31
+
32
+ ## [10.0.0-beta.17](https://github.com/hatiolab/operato/compare/v10.0.0-beta.16...v10.0.0-beta.17) (2026-03-23)
33
+
34
+
35
+ ### :rocket: New Features
36
+
37
+ * **board:** add board model cache (IndexedDB) ([122036d](https://github.com/hatiolab/operato/commit/122036d19063a4f9e47d5f4d325615f6e895a328))
38
+
39
+
40
+
41
+ ## [10.0.0-beta.16](https://github.com/hatiolab/operato/compare/v10.0.0-beta.15...v10.0.0-beta.16) (2026-03-23)
42
+
43
+
44
+ ### :rocket: New Features
45
+
46
+ * **board:** scene monitor 연동 — load tracker/overlay 통합 ([265a716](https://github.com/hatiolab/operato/commit/265a7167b21337b3842d2bae5ef95ba3d071053c))
47
+
48
+
49
+
50
+ ## [10.0.0-beta.15](https://github.com/hatiolab/operato/compare/v10.0.0-beta.14...v10.0.0-beta.15) (2026-03-22)
51
+
52
+
53
+ ### :rocket: New Features
54
+
55
+ * **board:** 데이터바인딩 팝업 에디터 + property 콤보박스 + D키 핫키 ([d467180](https://github.com/hatiolab/operato/commit/d467180e106b1d268099bf9895082e2c6cf8d07b))
56
+
57
+
58
+ ### :bug: Bug Fix
59
+
60
+ * **board:** 데이터바인딩 팝업 모델 반영 + GLTF animations 트리 + 사이드바 저장 조건 완화 ([46fe68e](https://github.com/hatiolab/operato/commit/46fe68ea8c3af43c5c4275a42cd7bb5e761ec118))
61
+
62
+
63
+
64
+ ## [10.0.0-beta.14](https://github.com/hatiolab/operato/compare/v10.0.0-beta.13...v10.0.0-beta.14) (2026-03-20)
65
+
66
+
67
+ ### :rocket: New Features
68
+
69
+ * **board:** 모델러에 snap-guide-layer 추가 ([85e091b](https://github.com/hatiolab/operato/commit/85e091b4f3486845ef9804610d9237f5d8902de3))
70
+
71
+
72
+ ### :house: Code Refactoring
73
+
74
+ * **board:** 모델러에서 guide-layer 제거 ([dbfd8fe](https://github.com/hatiolab/operato/commit/dbfd8fe19a0739671e6e27849dd6f3b1d3b53c97))
75
+
76
+
77
+
78
+ ## [10.0.0-beta.13](https://github.com/hatiolab/operato/compare/v10.0.0-beta.12...v10.0.0-beta.13) (2026-03-19)
79
+
80
+
81
+ ### :bug: Bug Fix
82
+
83
+ * **board:** 편집 툴바 조건부 버튼 이벤트 바인딩 + dimension 초기화 ([e642ffa](https://github.com/hatiolab/operato/commit/e642ffa8be05cf4305f857fd4bef633611401c42))
84
+
85
+
86
+
87
+ ## [10.0.0-beta.12](https://github.com/hatiolab/operato/compare/v10.0.0-beta.11...v10.0.0-beta.12) (2026-03-17)
88
+
89
+ **Note:** Version bump only for package @operato/board
90
+
91
+
92
+
93
+
94
+
95
+ ## [10.0.0-beta.11](https://github.com/hatiolab/operato/compare/v10.0.0-beta.10...v10.0.0-beta.11) (2026-03-15)
96
+
97
+
98
+ ### :bug: Bug Fix
99
+
100
+ * **board:** 프리뷰 팝업에 preventCloseOnBlur 옵션 추가 ([c73cd97](https://github.com/hatiolab/operato/commit/c73cd97210bb280b932b10fca125ef2022c6766e))
101
+
102
+
103
+
104
+ ## [10.0.0-beta.10](https://github.com/hatiolab/operato/compare/v10.0.0-beta.9...v10.0.0-beta.10) (2026-03-14)
105
+
106
+ **Note:** Version bump only for package @operato/board
107
+
108
+
109
+
110
+
111
+
112
+ ## [10.0.0-beta.9](https://github.com/hatiolab/operato/compare/v10.0.0-beta.8...v10.0.0-beta.9) (2026-03-13)
113
+
114
+
115
+ ### :rocket: New Features
116
+
117
+ * **board:** 2D/3D 모드별 edit-toolbar 분기 ([26ca616](https://github.com/hatiolab/operato/commit/26ca616b85294af77a8eed5f24c937c612db57f8))
118
+ * **board:** 3D 모드 기즈모 버튼, 툴바 UX 개선 ([45b46ff](https://github.com/hatiolab/operato/commit/45b46ff47ac2258bac66db51c0a6b46b16f26fbe))
119
+
120
+
121
+
122
+ ## [10.0.0-beta.8](https://github.com/hatiolab/operato/compare/v10.0.0-beta.7...v10.0.0-beta.8) (2026-03-13)
123
+
124
+
125
+ ### :rocket: New Features
126
+
127
+ * **board:** add playback feature for historical data replay ([e8a3a73](https://github.com/hatiolab/operato/commit/e8a3a730648c2b5ec17244b157e1ce59b728b524))
128
+
129
+
130
+
131
+ ## [10.0.0-beta.7](https://github.com/hatiolab/operato/compare/v10.0.0-beta.6...v10.0.0-beta.7) (2026-03-09)
132
+
133
+
134
+ ### :bug: Bug Fix
135
+
136
+ * **board:** correct invalid CSS overflow and remove debug console.log ([7292222](https://github.com/hatiolab/operato/commit/7292222646bde1140018465b4fe57f529103c48c))
137
+
138
+
139
+
140
+ ## [10.0.0-beta.6](https://github.com/hatiolab/operato/compare/v10.0.0-beta.5...v10.0.0-beta.6) (2026-03-09)
141
+
142
+
143
+ ### :bug: Bug Fix
144
+
145
+ * **board:** remove unnecessary reactive properties to prevent Lit update warnings ([2e6ad75](https://github.com/hatiolab/operato/commit/2e6ad75b853fc27a56410e4fff7de24325d24d24))
146
+
147
+
148
+
149
+ ## [10.0.0-beta.5](https://github.com/hatiolab/operato/compare/v10.0.0-beta.4...v10.0.0-beta.5) (2026-03-08)
150
+
151
+ **Note:** Version bump only for package @operato/board
152
+
153
+
154
+
155
+
156
+
157
+ ## [10.0.0-beta.4](https://github.com/hatiolab/operato/compare/v10.0.0-beta.3...v10.0.0-beta.4) (2026-03-06)
158
+
159
+ **Note:** Version bump only for package @operato/board
160
+
161
+
162
+
163
+
164
+
165
+ ## [10.0.0-beta.3](https://github.com/hatiolab/operato/compare/v10.0.0-beta.2...v10.0.0-beta.3) (2026-03-06)
166
+
167
+ **Note:** Version bump only for package @operato/board
168
+
169
+
170
+
171
+
172
+
6
173
  ## [10.0.0-beta.2](https://github.com/hatiolab/operato/compare/v10.0.0-beta.1...v10.0.0-beta.2) (2026-03-05)
7
174
 
8
175
 
@@ -0,0 +1,30 @@
1
+ export interface CachedBoard {
2
+ id: string;
3
+ name: string;
4
+ model: any;
5
+ updatedAt: string;
6
+ }
7
+ export declare class BoardModelCache {
8
+ /**
9
+ * 캐시에서 보드 모델 조회
10
+ * @returns 캐시된 보드 또는 null
11
+ */
12
+ static get(boardId: string): Promise<CachedBoard | null>;
13
+ /**
14
+ * 보드 모델을 캐시에 저장
15
+ */
16
+ static put(boardId: string, name: string, model: string, updatedAt: string): Promise<void>;
17
+ /**
18
+ * 캐시가 최신인지 확인
19
+ * @returns true면 캐시 유효, false면 갱신 필요
20
+ */
21
+ static isValid(boardId: string, serverUpdatedAt: string): Promise<boolean>;
22
+ /**
23
+ * 특정 보드 캐시 삭제
24
+ */
25
+ static remove(boardId: string): Promise<void>;
26
+ /**
27
+ * 전체 캐시 클리어
28
+ */
29
+ static clear(): Promise<void>;
30
+ }
@@ -0,0 +1,93 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * BoardModelCache — 보드 모델 JSON을 IndexedDB에 캐싱.
5
+ *
6
+ * 캐시 히트 시 네트워크 대기 없이 즉시 scene 생성 가능.
7
+ * updatedAt 비교로 캐시 무효화.
8
+ */
9
+ import Dexie from 'dexie';
10
+ class BoardModelDatabase extends Dexie {
11
+ constructor() {
12
+ super('board-model-cache');
13
+ this.version(1).stores({
14
+ board_models: 'id'
15
+ });
16
+ }
17
+ }
18
+ const db = new BoardModelDatabase();
19
+ export class BoardModelCache {
20
+ /**
21
+ * 캐시에서 보드 모델 조회
22
+ * @returns 캐시된 보드 또는 null
23
+ */
24
+ static async get(boardId) {
25
+ try {
26
+ const record = await db.board_models.get(boardId);
27
+ if (!record)
28
+ return null;
29
+ return {
30
+ id: record.id,
31
+ name: record.name,
32
+ model: JSON.parse(record.model),
33
+ updatedAt: record.updatedAt
34
+ };
35
+ }
36
+ catch (_a) {
37
+ return null;
38
+ }
39
+ }
40
+ /**
41
+ * 보드 모델을 캐시에 저장
42
+ */
43
+ static async put(boardId, name, model, updatedAt) {
44
+ try {
45
+ await db.board_models.put({
46
+ id: boardId,
47
+ name,
48
+ model,
49
+ updatedAt,
50
+ cachedAt: Date.now()
51
+ });
52
+ }
53
+ catch (_a) {
54
+ // 캐시 저장 실패는 무시 — 기능에 영향 없음
55
+ }
56
+ }
57
+ /**
58
+ * 캐시가 최신인지 확인
59
+ * @returns true면 캐시 유효, false면 갱신 필요
60
+ */
61
+ static async isValid(boardId, serverUpdatedAt) {
62
+ try {
63
+ const record = await db.board_models.get(boardId);
64
+ return (record === null || record === void 0 ? void 0 : record.updatedAt) === serverUpdatedAt;
65
+ }
66
+ catch (_a) {
67
+ return false;
68
+ }
69
+ }
70
+ /**
71
+ * 특정 보드 캐시 삭제
72
+ */
73
+ static async remove(boardId) {
74
+ try {
75
+ await db.board_models.delete(boardId);
76
+ }
77
+ catch (_a) {
78
+ // ignore
79
+ }
80
+ }
81
+ /**
82
+ * 전체 캐시 클리어
83
+ */
84
+ static async clear() {
85
+ try {
86
+ await db.board_models.clear();
87
+ }
88
+ catch (_a) {
89
+ // ignore
90
+ }
91
+ }
92
+ }
93
+ //# sourceMappingURL=board-model-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"board-model-cache.js","sourceRoot":"","sources":["../../../src/data-storage/board-model-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAUzB,MAAM,kBAAmB,SAAQ,KAAK;IAGpC;QACE,KAAK,CAAC,mBAAmB,CAAC,CAAA;QAC1B,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;YACrB,YAAY,EAAE,IAAI;SACnB,CAAC,CAAA;IACJ,CAAC;CACF;AAED,MAAM,EAAE,GAAG,IAAI,kBAAkB,EAAE,CAAA;AASnC,MAAM,OAAO,eAAe;IAC1B;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,OAAe;QAC9B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YACjD,IAAI,CAAC,MAAM;gBAAE,OAAO,IAAI,CAAA;YAExB,OAAO;gBACL,EAAE,EAAE,MAAM,CAAC,EAAE;gBACb,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC;gBAC/B,SAAS,EAAE,MAAM,CAAC,SAAS;aAC5B,CAAA;QACH,CAAC;QAAC,WAAM,CAAC;YACP,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,OAAe,EAAE,IAAY,EAAE,KAAa,EAAE,SAAiB;QAC9E,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC;gBACxB,EAAE,EAAE,OAAO;gBACX,IAAI;gBACJ,KAAK;gBACL,SAAS;gBACT,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE;aACrB,CAAC,CAAA;QACJ,CAAC;QAAC,WAAM,CAAC;YACP,2BAA2B;QAC7B,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,OAAe,EAAE,eAAuB;QAC3D,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YACjD,OAAO,CAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,SAAS,MAAK,eAAe,CAAA;QAC9C,CAAC;QAAC,WAAM,CAAC;YACP,OAAO,KAAK,CAAA;QACd,CAAC;IACH,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAe;QACjC,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QACvC,CAAC;QAAC,WAAM,CAAC;YACP,SAAS;QACX,CAAC;IACH,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,KAAK,CAAC,KAAK;QAChB,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,CAAA;QAC/B,CAAC;QAAC,WAAM,CAAC;YACP,SAAS;QACX,CAAC;IACH,CAAC;CACF","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * BoardModelCache — 보드 모델 JSON을 IndexedDB에 캐싱.\n *\n * 캐시 히트 시 네트워크 대기 없이 즉시 scene 생성 가능.\n * updatedAt 비교로 캐시 무효화.\n */\n\nimport Dexie from 'dexie'\n\ninterface IBoardModelRecord {\n id: string // board ID (primary key)\n name: string\n model: string // JSON string (파싱 전)\n updatedAt: string // 서버의 updatedAt\n cachedAt: number // 로컬 캐시 시점\n}\n\nclass BoardModelDatabase extends Dexie {\n board_models!: Dexie.Table<IBoardModelRecord, string>\n\n constructor() {\n super('board-model-cache')\n this.version(1).stores({\n board_models: 'id'\n })\n }\n}\n\nconst db = new BoardModelDatabase()\n\nexport interface CachedBoard {\n id: string\n name: string\n model: any // 파싱된 모델 객체\n updatedAt: string\n}\n\nexport class BoardModelCache {\n /**\n * 캐시에서 보드 모델 조회\n * @returns 캐시된 보드 또는 null\n */\n static async get(boardId: string): Promise<CachedBoard | null> {\n try {\n const record = await db.board_models.get(boardId)\n if (!record) return null\n\n return {\n id: record.id,\n name: record.name,\n model: JSON.parse(record.model),\n updatedAt: record.updatedAt\n }\n } catch {\n return null\n }\n }\n\n /**\n * 보드 모델을 캐시에 저장\n */\n static async put(boardId: string, name: string, model: string, updatedAt: string): Promise<void> {\n try {\n await db.board_models.put({\n id: boardId,\n name,\n model,\n updatedAt,\n cachedAt: Date.now()\n })\n } catch {\n // 캐시 저장 실패는 무시 — 기능에 영향 없음\n }\n }\n\n /**\n * 캐시가 최신인지 확인\n * @returns true면 캐시 유효, false면 갱신 필요\n */\n static async isValid(boardId: string, serverUpdatedAt: string): Promise<boolean> {\n try {\n const record = await db.board_models.get(boardId)\n return record?.updatedAt === serverUpdatedAt\n } catch {\n return false\n }\n }\n\n /**\n * 특정 보드 캐시 삭제\n */\n static async remove(boardId: string): Promise<void> {\n try {\n await db.board_models.delete(boardId)\n } catch {\n // ignore\n }\n }\n\n /**\n * 전체 캐시 클리어\n */\n static async clear(): Promise<void> {\n try {\n await db.board_models.clear()\n } catch {\n // ignore\n }\n }\n}\n"]}
@@ -0,0 +1,74 @@
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
+ }
8
+ export interface PlaybackConfig {
9
+ /** 플레이백 가능한 시간 범위 */
10
+ timeRange?: {
11
+ from: Date;
12
+ to: Date;
13
+ };
14
+ }
15
+ /**
16
+ * PlaybackProvider
17
+ *
18
+ * 백엔드 플레이백 서비스와 point-to-point 구독으로 통신하는 DataSubscriptionProvider.
19
+ * 백엔드가 요청된 시점/배속으로 스냅샷을 실시간처럼 푸시하면,
20
+ * 이를 받아서 등록된 컴포넌트에 전달한다.
21
+ *
22
+ * 실시간 DataSubscriptionProviderImpl과 동일한 인터페이스를 구현하므로
23
+ * Scene 입장에서는 실시간/플레이백을 구분하지 않는다.
24
+ */
25
+ export declare class PlaybackProvider implements DataSubscriptionProvider {
26
+ private _components;
27
+ private _subscription;
28
+ private _state;
29
+ private _speed;
30
+ private _currentTime;
31
+ private _onStatusChange?;
32
+ constructor(onStatusChange?: (status: PlaybackStatus) => void);
33
+ get state(): PlaybackState;
34
+ get speed(): number;
35
+ get currentTime(): string;
36
+ /**
37
+ * DataSubscriptionProvider.subscribe 구현
38
+ * 컴포넌트를 tag별로 등록한다. 실제 데이터는 백엔드 구독을 통해 수신된다.
39
+ */
40
+ subscribe(tag: string, component: Component): Promise<{
41
+ unsubscribe: () => void;
42
+ }>;
43
+ /**
44
+ * 플레이백 시작 — 백엔드에 point-to-point 구독을 개시한다.
45
+ */
46
+ start(fromTime: Date, speed?: number): Promise<void>;
47
+ /**
48
+ * 일시정지
49
+ */
50
+ pause(): Promise<void>;
51
+ /**
52
+ * 재개
53
+ */
54
+ resume(): Promise<void>;
55
+ /**
56
+ * 특정 시점으로 이동
57
+ */
58
+ seek(toTime: Date): Promise<void>;
59
+ /**
60
+ * 배속 변경
61
+ */
62
+ setSpeed(speed: number): Promise<void>;
63
+ /**
64
+ * 플레이백 중지 및 구독 해제
65
+ */
66
+ stop(): void;
67
+ /**
68
+ * DataSubscriptionProvider.dispose 구현
69
+ */
70
+ dispose(): void;
71
+ private _distributeSnapshot;
72
+ private _sendCommand;
73
+ private _notifyStatus;
74
+ }
@@ -0,0 +1,191 @@
1
+ import gql from 'graphql-tag';
2
+ import { subscribe } from '@operato/graphql';
3
+ /**
4
+ * PlaybackProvider
5
+ *
6
+ * 백엔드 플레이백 서비스와 point-to-point 구독으로 통신하는 DataSubscriptionProvider.
7
+ * 백엔드가 요청된 시점/배속으로 스냅샷을 실시간처럼 푸시하면,
8
+ * 이를 받아서 등록된 컴포넌트에 전달한다.
9
+ *
10
+ * 실시간 DataSubscriptionProviderImpl과 동일한 인터페이스를 구현하므로
11
+ * Scene 입장에서는 실시간/플레이백을 구분하지 않는다.
12
+ */
13
+ export class PlaybackProvider {
14
+ constructor(onStatusChange) {
15
+ this._components = new Map();
16
+ this._subscription = null;
17
+ this._state = 'idle';
18
+ this._speed = 1;
19
+ this._currentTime = '';
20
+ this._onStatusChange = onStatusChange;
21
+ }
22
+ get state() {
23
+ return this._state;
24
+ }
25
+ get speed() {
26
+ return this._speed;
27
+ }
28
+ get currentTime() {
29
+ return this._currentTime;
30
+ }
31
+ /**
32
+ * DataSubscriptionProvider.subscribe 구현
33
+ * 컴포넌트를 tag별로 등록한다. 실제 데이터는 백엔드 구독을 통해 수신된다.
34
+ */
35
+ async subscribe(tag, component) {
36
+ if (!this._components.has(tag)) {
37
+ this._components.set(tag, new Set());
38
+ }
39
+ this._components.get(tag).add(component);
40
+ return {
41
+ unsubscribe: () => {
42
+ const components = this._components.get(tag);
43
+ if (components) {
44
+ components.delete(component);
45
+ if (components.size === 0) {
46
+ this._components.delete(tag);
47
+ }
48
+ }
49
+ }
50
+ };
51
+ }
52
+ /**
53
+ * 플레이백 시작 — 백엔드에 point-to-point 구독을 개시한다.
54
+ */
55
+ async start(fromTime, speed = 1) {
56
+ var _a;
57
+ // 기존 구독이 있으면 정리
58
+ (_a = this._subscription) === null || _a === void 0 ? void 0 : _a.unsubscribe();
59
+ this._speed = speed;
60
+ this._state = 'playing';
61
+ this._subscription = await subscribe({
62
+ query: gql `
63
+ subscription Playback($fromTime: String!, $speed: Float!) {
64
+ playback(fromTime: $fromTime, speed: $speed) {
65
+ timestamp
66
+ data
67
+ }
68
+ }
69
+ `,
70
+ variables: {
71
+ fromTime: fromTime.toISOString(),
72
+ speed
73
+ }
74
+ }, {
75
+ next: ({ data }) => {
76
+ if (!(data === null || data === void 0 ? void 0 : data.playback))
77
+ return;
78
+ const { timestamp, data: snapshotData } = data.playback;
79
+ this._currentTime = timestamp;
80
+ // 스냅샷 데이터를 tag별로 분배
81
+ this._distributeSnapshot(snapshotData);
82
+ this._notifyStatus();
83
+ },
84
+ error: (err) => {
85
+ console.error('[PlaybackProvider] subscription error:', err);
86
+ this._state = 'stopped';
87
+ this._notifyStatus();
88
+ },
89
+ complete: () => {
90
+ this._state = 'stopped';
91
+ this._notifyStatus();
92
+ }
93
+ });
94
+ this._notifyStatus();
95
+ }
96
+ /**
97
+ * 일시정지
98
+ */
99
+ async pause() {
100
+ if (this._state !== 'playing')
101
+ return;
102
+ this._state = 'paused';
103
+ // 백엔드에 pause 명령 전송 (mutation)
104
+ await this._sendCommand('pause');
105
+ this._notifyStatus();
106
+ }
107
+ /**
108
+ * 재개
109
+ */
110
+ async resume() {
111
+ if (this._state !== 'paused')
112
+ return;
113
+ this._state = 'playing';
114
+ await this._sendCommand('resume');
115
+ this._notifyStatus();
116
+ }
117
+ /**
118
+ * 특정 시점으로 이동
119
+ */
120
+ async seek(toTime) {
121
+ await this._sendCommand('seek', { toTime: toTime.toISOString() });
122
+ this._currentTime = toTime.toISOString();
123
+ this._notifyStatus();
124
+ }
125
+ /**
126
+ * 배속 변경
127
+ */
128
+ async setSpeed(speed) {
129
+ this._speed = speed;
130
+ await this._sendCommand('speed', { speed });
131
+ this._notifyStatus();
132
+ }
133
+ /**
134
+ * 플레이백 중지 및 구독 해제
135
+ */
136
+ stop() {
137
+ var _a;
138
+ (_a = this._subscription) === null || _a === void 0 ? void 0 : _a.unsubscribe();
139
+ this._subscription = null;
140
+ this._state = 'stopped';
141
+ this._notifyStatus();
142
+ }
143
+ /**
144
+ * DataSubscriptionProvider.dispose 구현
145
+ */
146
+ dispose() {
147
+ this.stop();
148
+ this._components.clear();
149
+ }
150
+ _distributeSnapshot(snapshotData) {
151
+ if (!snapshotData || typeof snapshotData !== 'object')
152
+ return;
153
+ // snapshotData: { tag1: value1, tag2: value2, ... }
154
+ for (const [tag, value] of Object.entries(snapshotData)) {
155
+ const components = this._components.get(tag);
156
+ if (components) {
157
+ for (const component of components) {
158
+ component.data = value;
159
+ }
160
+ }
161
+ }
162
+ }
163
+ async _sendCommand(command, params = {}) {
164
+ try {
165
+ const { client } = await import('@operato/graphql');
166
+ await client.mutate({
167
+ mutation: gql `
168
+ mutation PlaybackControl($command: String!, $params: String) {
169
+ playbackControl(command: $command, params: $params)
170
+ }
171
+ `,
172
+ variables: {
173
+ command,
174
+ params: JSON.stringify(params)
175
+ }
176
+ });
177
+ }
178
+ catch (e) {
179
+ console.error('[PlaybackProvider] command failed:', command, e);
180
+ }
181
+ }
182
+ _notifyStatus() {
183
+ var _a;
184
+ (_a = this._onStatusChange) === null || _a === void 0 ? void 0 : _a.call(this, {
185
+ state: this._state,
186
+ currentTime: this._currentTime,
187
+ speed: this._speed
188
+ });
189
+ }
190
+ }
191
+ //# sourceMappingURL=playback-subscription.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playback-subscription.js","sourceRoot":"","sources":["../../../src/graphql/playback-subscription.ts"],"names":[],"mappings":"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","sourcesContent":["import { Component, DataSubscriptionProvider } from '@hatiolab/things-scene'\n\nimport gql from 'graphql-tag'\nimport { subscribe } from '@operato/graphql'\n\nexport type PlaybackState = 'idle' | 'playing' | 'paused' | 'stopped'\n\nexport interface PlaybackStatus {\n state: PlaybackState\n currentTime: string\n speed: number\n}\n\nexport interface PlaybackConfig {\n /** 플레이백 가능한 시간 범위 */\n timeRange?: { from: Date; to: Date }\n}\n\n/**\n * PlaybackProvider\n *\n * 백엔드 플레이백 서비스와 point-to-point 구독으로 통신하는 DataSubscriptionProvider.\n * 백엔드가 요청된 시점/배속으로 스냅샷을 실시간처럼 푸시하면,\n * 이를 받아서 등록된 컴포넌트에 전달한다.\n *\n * 실시간 DataSubscriptionProviderImpl과 동일한 인터페이스를 구현하므로\n * Scene 입장에서는 실시간/플레이백을 구분하지 않는다.\n */\nexport class PlaybackProvider implements DataSubscriptionProvider {\n private _components: Map<string, Set<Component>> = new Map()\n private _subscription: { unsubscribe: () => void } | null = null\n private _state: PlaybackState = 'idle'\n private _speed: number = 1\n private _currentTime: string = ''\n private _onStatusChange?: (status: PlaybackStatus) => void\n\n constructor(onStatusChange?: (status: PlaybackStatus) => void) {\n this._onStatusChange = onStatusChange\n }\n\n get state() {\n return this._state\n }\n\n get speed() {\n return this._speed\n }\n\n get currentTime() {\n return this._currentTime\n }\n\n /**\n * DataSubscriptionProvider.subscribe 구현\n * 컴포넌트를 tag별로 등록한다. 실제 데이터는 백엔드 구독을 통해 수신된다.\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 * 플레이백 시작 — 백엔드에 point-to-point 구독을 개시한다.\n */\n async start(fromTime: Date, speed: number = 1) {\n // 기존 구독이 있으면 정리\n this._subscription?.unsubscribe()\n\n this._speed = speed\n this._state = 'playing'\n\n this._subscription = await subscribe(\n {\n query: gql`\n subscription Playback($fromTime: String!, $speed: Float!) {\n playback(fromTime: $fromTime, speed: $speed) {\n timestamp\n data\n }\n }\n `,\n variables: {\n fromTime: fromTime.toISOString(),\n speed\n }\n },\n {\n next: ({ data }: { data: any }) => {\n if (!data?.playback) return\n\n const { timestamp, data: snapshotData } = data.playback\n this._currentTime = timestamp\n\n // 스냅샷 데이터를 tag별로 분배\n this._distributeSnapshot(snapshotData)\n\n this._notifyStatus()\n },\n error: (err: any) => {\n console.error('[PlaybackProvider] subscription error:', err)\n this._state = 'stopped'\n this._notifyStatus()\n },\n complete: () => {\n this._state = 'stopped'\n this._notifyStatus()\n }\n }\n )\n\n this._notifyStatus()\n }\n\n /**\n * 일시정지\n */\n async pause() {\n if (this._state !== 'playing') return\n\n this._state = 'paused'\n\n // 백엔드에 pause 명령 전송 (mutation)\n await this._sendCommand('pause')\n this._notifyStatus()\n }\n\n /**\n * 재개\n */\n async resume() {\n if (this._state !== 'paused') return\n\n this._state = 'playing'\n\n await this._sendCommand('resume')\n this._notifyStatus()\n }\n\n /**\n * 특정 시점으로 이동\n */\n async seek(toTime: Date) {\n await this._sendCommand('seek', { toTime: toTime.toISOString() })\n this._currentTime = toTime.toISOString()\n this._notifyStatus()\n }\n\n /**\n * 배속 변경\n */\n async setSpeed(speed: number) {\n this._speed = speed\n await this._sendCommand('speed', { speed })\n this._notifyStatus()\n }\n\n /**\n * 플레이백 중지 및 구독 해제\n */\n stop() {\n this._subscription?.unsubscribe()\n this._subscription = null\n this._state = 'stopped'\n this._notifyStatus()\n }\n\n /**\n * DataSubscriptionProvider.dispose 구현\n */\n dispose() {\n this.stop()\n this._components.clear()\n }\n\n private _distributeSnapshot(snapshotData: any) {\n if (!snapshotData || typeof snapshotData !== 'object') return\n\n // snapshotData: { tag1: value1, tag2: value2, ... }\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 private async _sendCommand(command: string, params: Record<string, any> = {}) {\n try {\n const { client } = await import('@operato/graphql')\n await client.mutate({\n mutation: gql`\n mutation PlaybackControl($command: String!, $params: String) {\n playbackControl(command: $command, params: $params)\n }\n `,\n variables: {\n command,\n params: JSON.stringify(params)\n }\n })\n } catch (e) {\n console.error('[PlaybackProvider] command failed:', command, e)\n }\n }\n\n private _notifyStatus() {\n this._onStatusChange?.({\n state: this._state,\n currentTime: this._currentTime,\n speed: this._speed\n })\n }\n}\n"]}
@@ -1,6 +1,8 @@
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';
5
7
  export * from './ox-board-player.js';
6
8
  export * from './ox-board-modeller.js';
package/dist/src/index.js CHANGED
@@ -1,6 +1,7 @@
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';
5
6
  export * from './ox-board-player.js';
6
7
  export * from './ox-board-modeller.js';
@@ -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;AAElF,cAAc,sBAAsB,CAAA;AACpC,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'\n\nexport * from './ox-board-viewer.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"]}
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,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-player.js'\nexport * from './ox-board-modeller.js'\nexport * from './ox-board-list.js'\nexport * from './ox-board-template-viewer.js'\n"]}
@@ -12,7 +12,7 @@ export const style = css `
12
12
  [tools] {
13
13
  display: flex;
14
14
  align-items: center;
15
- overflow: none;
15
+ overflow: hidden;
16
16
  padding: 0px 10px;
17
17
  }
18
18
 
@@ -201,6 +201,43 @@ export const style = css `
201
201
  background-position-y: -1593px;
202
202
  }
203
203
 
204
+ #align-z-front {
205
+ background-position-y: -142px;
206
+ filter: hue-rotate(180deg);
207
+ }
208
+
209
+ #align-z-middle {
210
+ background-position-y: -192px;
211
+ filter: hue-rotate(180deg);
212
+ }
213
+
214
+ #align-z-back {
215
+ background-position-y: -242px;
216
+ filter: hue-rotate(180deg);
217
+ }
218
+
219
+ #distribute-z {
220
+ background-position-y: -1593px;
221
+ filter: hue-rotate(180deg);
222
+ }
223
+
224
+ .gizmo-btn {
225
+ background: none !important;
226
+ color: rgba(255, 255, 255, 0.5);
227
+ min-width: 24px;
228
+ display: inline-flex;
229
+ align-items: center;
230
+ justify-content: center;
231
+ cursor: pointer;
232
+ border-radius: 3px;
233
+ padding: 2px;
234
+ }
235
+
236
+ .gizmo-btn[disabled] {
237
+ opacity: 0.3;
238
+ pointer-events: none;
239
+ }
240
+
204
241
  #toggle-property {
205
242
  background-position-y: -1392px;
206
243
  }