@operato/board 10.0.0-beta.7 → 10.0.0-beta.8
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 +9 -0
- package/dist/src/graphql/playback-subscription.d.ts +74 -0
- package/dist/src/graphql/playback-subscription.js +191 -0
- package/dist/src/graphql/playback-subscription.js.map +1 -0
- package/dist/src/ox-board-viewer.d.ts +42 -0
- package/dist/src/ox-board-viewer.js +209 -18
- package/dist/src/ox-board-viewer.js.map +1 -1
- package/dist/src/ox-playback-controls.d.ts +48 -0
- package/dist/src/ox-playback-controls.js +419 -0
- package/dist/src/ox-playback-controls.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
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.8](https://github.com/hatiolab/operato/compare/v10.0.0-beta.7...v10.0.0-beta.8) (2026-03-13)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### :rocket: New Features
|
|
10
|
+
|
|
11
|
+
* **board:** add playback feature for historical data replay ([e8a3a73](https://github.com/hatiolab/operato/commit/e8a3a730648c2b5ec17244b157e1ce59b728b524))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
6
15
|
## [10.0.0-beta.7](https://github.com/hatiolab/operato/compare/v10.0.0-beta.6...v10.0.0-beta.7) (2026-03-09)
|
|
7
16
|
|
|
8
17
|
|
|
@@ -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"]}
|
|
@@ -2,6 +2,7 @@ import '@material/web/icon/icon.js';
|
|
|
2
2
|
import '@material/web/fab/fab.js';
|
|
3
3
|
import { LitElement, PropertyValues } from 'lit';
|
|
4
4
|
import { Component, ReferenceProvider } from '@hatiolab/things-scene';
|
|
5
|
+
import './ox-playback-controls.js';
|
|
5
6
|
export declare class BoardViewer extends LitElement {
|
|
6
7
|
static styles: import("lit").CSSResult[];
|
|
7
8
|
baseUrl: string;
|
|
@@ -12,6 +13,17 @@ export declare class BoardViewer extends LitElement {
|
|
|
12
13
|
history: boolean;
|
|
13
14
|
hideFullscreen: boolean;
|
|
14
15
|
hideNavigation: boolean;
|
|
16
|
+
playbackEnabled: boolean;
|
|
17
|
+
playbackTimeRange?: {
|
|
18
|
+
from: Date;
|
|
19
|
+
to: Date;
|
|
20
|
+
};
|
|
21
|
+
private _playbackState;
|
|
22
|
+
private _playbackActive;
|
|
23
|
+
private _playbackSpeed;
|
|
24
|
+
private _playbackCurrentTime;
|
|
25
|
+
private _playbackProvider;
|
|
26
|
+
private _savedRealProvider;
|
|
15
27
|
_scene: any;
|
|
16
28
|
_forward: {
|
|
17
29
|
id: string;
|
|
@@ -29,6 +41,7 @@ export declare class BoardViewer extends LitElement {
|
|
|
29
41
|
_prev: HTMLElement;
|
|
30
42
|
_next: HTMLElement;
|
|
31
43
|
_fullscreen: HTMLElement;
|
|
44
|
+
_fabGroup: HTMLElement;
|
|
32
45
|
render(): import("lit-html").TemplateResult<1>;
|
|
33
46
|
private resizeHandler;
|
|
34
47
|
connectedCallback(): void;
|
|
@@ -61,6 +74,35 @@ export declare class BoardViewer extends LitElement {
|
|
|
61
74
|
onExportData(filename: string, value: string | number | object, component: Component): Promise<void>;
|
|
62
75
|
onImportData(_: string, value: string | number | object, component: Component): Promise<void>;
|
|
63
76
|
onClickEvent(e: MouseEvent, hint: any): void;
|
|
77
|
+
/**
|
|
78
|
+
* 외부에서 플레이백을 활성화한다.
|
|
79
|
+
* playback-enabled 속성을 설정하면 컨트롤바가 나타나고,
|
|
80
|
+
* 이 메서드로 시간 범위 등 상세 설정을 할 수 있다.
|
|
81
|
+
*/
|
|
82
|
+
enablePlayback(config?: {
|
|
83
|
+
timeRange?: {
|
|
84
|
+
from: Date;
|
|
85
|
+
to: Date;
|
|
86
|
+
};
|
|
87
|
+
}): void;
|
|
88
|
+
disablePlayback(): void;
|
|
89
|
+
private _onTogglePlaybackPanel;
|
|
90
|
+
private _onPlaybackStart;
|
|
91
|
+
private _onPlaybackPause;
|
|
92
|
+
private _onPlaybackResume;
|
|
93
|
+
private _onPlaybackStop;
|
|
94
|
+
private _onPlaybackSeek;
|
|
95
|
+
private _onPlaybackSpeed;
|
|
96
|
+
private _startPlayback;
|
|
97
|
+
private _stopPlayback;
|
|
98
|
+
/**
|
|
99
|
+
* 모든 tag 컴포넌트의 기존 구독을 해제한다.
|
|
100
|
+
*/
|
|
101
|
+
private _unsubscribeAll;
|
|
102
|
+
/**
|
|
103
|
+
* 모든 tag 컴포넌트를 현재 provider로 재구독한다.
|
|
104
|
+
*/
|
|
105
|
+
private _resubscribeAll;
|
|
64
106
|
hidePopup(): void;
|
|
65
107
|
getSceneData(): any;
|
|
66
108
|
getSceneValues(): any;
|
|
@@ -2,13 +2,15 @@ import { __decorate } from "tslib";
|
|
|
2
2
|
import '@material/web/icon/icon.js';
|
|
3
3
|
import '@material/web/fab/fab.js';
|
|
4
4
|
import { css, html, LitElement } from 'lit';
|
|
5
|
-
import { customElement, property, query } from 'lit/decorators.js';
|
|
5
|
+
import { customElement, property, query, state } from 'lit/decorators.js';
|
|
6
6
|
import * as XLSX from 'xlsx';
|
|
7
7
|
import { create, SCENE_MODE } from '@hatiolab/things-scene';
|
|
8
8
|
import { isIOS, togglefullscreen } from '@operato/utils';
|
|
9
9
|
import { ScrollbarStyles } from '@operato/styles';
|
|
10
10
|
import { BoardDataStorage } from './data-storage/data-storage.js';
|
|
11
11
|
import { DataSubscriptionProviderImpl } from './graphql/data-subscription.js';
|
|
12
|
+
import { PlaybackProvider } from './graphql/playback-subscription.js';
|
|
13
|
+
import './ox-playback-controls.js';
|
|
12
14
|
import { runScenario, startScenario } from './graphql/scenario.js';
|
|
13
15
|
import { fetchPlayGroupByName } from './graphql/play-group.js';
|
|
14
16
|
function objectToQueryString(obj) {
|
|
@@ -36,6 +38,13 @@ let BoardViewer = class BoardViewer extends LitElement {
|
|
|
36
38
|
this.history = false;
|
|
37
39
|
this.hideFullscreen = false;
|
|
38
40
|
this.hideNavigation = false;
|
|
41
|
+
this.playbackEnabled = false;
|
|
42
|
+
this._playbackState = 'idle';
|
|
43
|
+
this._playbackActive = false;
|
|
44
|
+
this._playbackSpeed = 1;
|
|
45
|
+
this._playbackCurrentTime = '';
|
|
46
|
+
this._playbackProvider = null;
|
|
47
|
+
this._savedRealProvider = null;
|
|
39
48
|
this._scene = null;
|
|
40
49
|
this._forward = [];
|
|
41
50
|
this._backward = [];
|
|
@@ -45,19 +54,6 @@ let BoardViewer = class BoardViewer extends LitElement {
|
|
|
45
54
|
};
|
|
46
55
|
}
|
|
47
56
|
render() {
|
|
48
|
-
var fullscreen = !isIOS() && !this.hideFullscreen
|
|
49
|
-
? html `
|
|
50
|
-
<md-fab
|
|
51
|
-
id="fullscreen"
|
|
52
|
-
@click=${(e) => this.onTapFullscreen()}
|
|
53
|
-
@mouseover=${(e) => this.transientShowButtons(true)}
|
|
54
|
-
@mouseout=${(e) => this.transientShowButtons()}
|
|
55
|
-
title="fullscreen"
|
|
56
|
-
>
|
|
57
|
-
<md-icon slot="icon">${document.fullscreenElement ? 'fullscreen_exit' : 'fullscreen'}</md-icon>
|
|
58
|
-
</md-fab>
|
|
59
|
-
`
|
|
60
|
-
: html ``;
|
|
61
57
|
var prev = !this.hideNavigation
|
|
62
58
|
? html `
|
|
63
59
|
<md-icon
|
|
@@ -92,7 +88,49 @@ let BoardViewer = class BoardViewer extends LitElement {
|
|
|
92
88
|
></div>
|
|
93
89
|
|
|
94
90
|
<slot></slot>
|
|
95
|
-
${next}
|
|
91
|
+
${next}
|
|
92
|
+
|
|
93
|
+
<div
|
|
94
|
+
class="fab-group"
|
|
95
|
+
@mouseover=${(e) => this.transientShowButtons(true)}
|
|
96
|
+
@mouseout=${(e) => this.transientShowButtons()}
|
|
97
|
+
>
|
|
98
|
+
${!isIOS() && !this.hideFullscreen
|
|
99
|
+
? html `
|
|
100
|
+
<md-fab id="fullscreen" @click=${() => this.onTapFullscreen()} title="fullscreen">
|
|
101
|
+
<md-icon slot="icon">${document.fullscreenElement ? 'fullscreen_exit' : 'fullscreen'}</md-icon>
|
|
102
|
+
</md-fab>
|
|
103
|
+
`
|
|
104
|
+
: html ``}
|
|
105
|
+
${this.playbackEnabled
|
|
106
|
+
? html `
|
|
107
|
+
<md-fab
|
|
108
|
+
id="playback"
|
|
109
|
+
@click=${() => this._onTogglePlaybackPanel()}
|
|
110
|
+
title="playback"
|
|
111
|
+
>
|
|
112
|
+
<md-icon slot="icon">${this._playbackActive ? 'stop' : 'history'}</md-icon>
|
|
113
|
+
</md-fab>
|
|
114
|
+
`
|
|
115
|
+
: html ``}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
${this._playbackActive
|
|
119
|
+
? html `
|
|
120
|
+
<ox-playback-controls
|
|
121
|
+
.playbackState=${this._playbackState}
|
|
122
|
+
.speed=${this._playbackSpeed}
|
|
123
|
+
.currentTime=${this._playbackCurrentTime}
|
|
124
|
+
.timeRange=${this.playbackTimeRange || { from: new Date(Date.now() - 3600000), to: new Date() }}
|
|
125
|
+
@playback-start=${this._onPlaybackStart}
|
|
126
|
+
@playback-pause=${this._onPlaybackPause}
|
|
127
|
+
@playback-resume=${this._onPlaybackResume}
|
|
128
|
+
@playback-stop=${this._onPlaybackStop}
|
|
129
|
+
@playback-seek=${this._onPlaybackSeek}
|
|
130
|
+
@playback-speed=${this._onPlaybackSpeed}
|
|
131
|
+
></ox-playback-controls>
|
|
132
|
+
`
|
|
133
|
+
: html ``}
|
|
96
134
|
`;
|
|
97
135
|
}
|
|
98
136
|
connectedCallback() {
|
|
@@ -156,6 +194,13 @@ let BoardViewer = class BoardViewer extends LitElement {
|
|
|
156
194
|
this.setupScene({ id: this.board.id, scene: this._scene });
|
|
157
195
|
}
|
|
158
196
|
closeScene() {
|
|
197
|
+
// 플레이백 중이면 정리
|
|
198
|
+
if (this._playbackProvider) {
|
|
199
|
+
this._playbackProvider.dispose();
|
|
200
|
+
this._playbackProvider = null;
|
|
201
|
+
this._savedRealProvider = null;
|
|
202
|
+
this._playbackState = 'idle';
|
|
203
|
+
}
|
|
159
204
|
if (this._scene) {
|
|
160
205
|
this.unbindSceneEvents(this._scene);
|
|
161
206
|
this._scene.target = null;
|
|
@@ -285,7 +330,8 @@ let BoardViewer = class BoardViewer extends LitElement {
|
|
|
285
330
|
transientShowButtons(stop) {
|
|
286
331
|
var buttons = [];
|
|
287
332
|
!this.hideNavigation && buttons.push(this._next, this._prev);
|
|
288
|
-
|
|
333
|
+
if (this._fabGroup)
|
|
334
|
+
buttons.push(this._fabGroup);
|
|
289
335
|
if (buttons.length == 0) {
|
|
290
336
|
return;
|
|
291
337
|
}
|
|
@@ -308,7 +354,7 @@ let BoardViewer = class BoardViewer extends LitElement {
|
|
|
308
354
|
}
|
|
309
355
|
this._forward.length <= 0 ? this._next.setAttribute('hidden', '') : this._next.removeAttribute('hidden');
|
|
310
356
|
this._backward.length <= 0 ? this._prev.setAttribute('hidden', '') : this._prev.removeAttribute('hidden');
|
|
311
|
-
this.
|
|
357
|
+
this._fabGroup && this._fabGroup.removeAttribute('hidden');
|
|
312
358
|
this._fade_animations.forEach(animation => {
|
|
313
359
|
animation.cancel();
|
|
314
360
|
if (stop)
|
|
@@ -452,6 +498,122 @@ let BoardViewer = class BoardViewer extends LitElement {
|
|
|
452
498
|
// clickComponent 이벤트만 발생시킨다.
|
|
453
499
|
window.dispatchEvent(new CustomEvent('clickComponent', { detail: component }));
|
|
454
500
|
}
|
|
501
|
+
/* playback */
|
|
502
|
+
/**
|
|
503
|
+
* 외부에서 플레이백을 활성화한다.
|
|
504
|
+
* playback-enabled 속성을 설정하면 컨트롤바가 나타나고,
|
|
505
|
+
* 이 메서드로 시간 범위 등 상세 설정을 할 수 있다.
|
|
506
|
+
*/
|
|
507
|
+
enablePlayback(config) {
|
|
508
|
+
this.playbackEnabled = true;
|
|
509
|
+
if (config === null || config === void 0 ? void 0 : config.timeRange) {
|
|
510
|
+
this.playbackTimeRange = config.timeRange;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
disablePlayback() {
|
|
514
|
+
this._stopPlayback();
|
|
515
|
+
this._playbackActive = false;
|
|
516
|
+
this.playbackEnabled = false;
|
|
517
|
+
}
|
|
518
|
+
_onTogglePlaybackPanel() {
|
|
519
|
+
if (this._playbackActive) {
|
|
520
|
+
// 패널 닫기 — 재생 중이면 중지
|
|
521
|
+
this._stopPlayback();
|
|
522
|
+
this._playbackActive = false;
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
this._playbackActive = true;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async _onPlaybackStart(e) {
|
|
529
|
+
const { fromTime, speed } = e.detail;
|
|
530
|
+
await this._startPlayback(fromTime, speed);
|
|
531
|
+
}
|
|
532
|
+
async _onPlaybackPause() {
|
|
533
|
+
var _a;
|
|
534
|
+
await ((_a = this._playbackProvider) === null || _a === void 0 ? void 0 : _a.pause());
|
|
535
|
+
}
|
|
536
|
+
async _onPlaybackResume() {
|
|
537
|
+
var _a;
|
|
538
|
+
await ((_a = this._playbackProvider) === null || _a === void 0 ? void 0 : _a.resume());
|
|
539
|
+
}
|
|
540
|
+
_onPlaybackStop() {
|
|
541
|
+
this._stopPlayback();
|
|
542
|
+
this._playbackActive = false;
|
|
543
|
+
}
|
|
544
|
+
async _onPlaybackSeek(e) {
|
|
545
|
+
var _a;
|
|
546
|
+
await ((_a = this._playbackProvider) === null || _a === void 0 ? void 0 : _a.seek(e.detail.toTime));
|
|
547
|
+
}
|
|
548
|
+
async _onPlaybackSpeed(e) {
|
|
549
|
+
var _a;
|
|
550
|
+
await ((_a = this._playbackProvider) === null || _a === void 0 ? void 0 : _a.setSpeed(e.detail.speed));
|
|
551
|
+
}
|
|
552
|
+
async _startPlayback(fromTime, speed) {
|
|
553
|
+
if (!this._scene)
|
|
554
|
+
return;
|
|
555
|
+
const rootContainer = this._scene.rootContainer;
|
|
556
|
+
// 실시간 provider 보관 및 구독 해제
|
|
557
|
+
if (!this._savedRealProvider) {
|
|
558
|
+
this._savedRealProvider = rootContainer.app.dataSubscriptionProvider;
|
|
559
|
+
await this._unsubscribeAll(rootContainer);
|
|
560
|
+
}
|
|
561
|
+
// PlaybackProvider 생성
|
|
562
|
+
this._playbackProvider = new PlaybackProvider((status) => {
|
|
563
|
+
this._playbackState = status.state;
|
|
564
|
+
this._playbackSpeed = status.speed;
|
|
565
|
+
this._playbackCurrentTime = status.currentTime;
|
|
566
|
+
});
|
|
567
|
+
// provider 교체 및 재구독
|
|
568
|
+
rootContainer.app.dataSubscriptionProvider = this._playbackProvider;
|
|
569
|
+
await this._resubscribeAll(rootContainer);
|
|
570
|
+
// 플레이백 시작
|
|
571
|
+
await this._playbackProvider.start(fromTime, speed);
|
|
572
|
+
}
|
|
573
|
+
async _stopPlayback() {
|
|
574
|
+
if (!this._scene || !this._playbackProvider)
|
|
575
|
+
return;
|
|
576
|
+
const rootContainer = this._scene.rootContainer;
|
|
577
|
+
// 플레이백 구독 해제
|
|
578
|
+
this._playbackProvider.dispose();
|
|
579
|
+
await this._unsubscribeAll(rootContainer);
|
|
580
|
+
// 실시간 provider 복귀
|
|
581
|
+
if (this._savedRealProvider) {
|
|
582
|
+
rootContainer.app.dataSubscriptionProvider = this._savedRealProvider;
|
|
583
|
+
await this._resubscribeAll(rootContainer);
|
|
584
|
+
this._savedRealProvider = null;
|
|
585
|
+
}
|
|
586
|
+
this._playbackProvider = null;
|
|
587
|
+
this._playbackState = 'idle';
|
|
588
|
+
this._playbackCurrentTime = '';
|
|
589
|
+
this._playbackSpeed = 1;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* 모든 tag 컴포넌트의 기존 구독을 해제한다.
|
|
593
|
+
*/
|
|
594
|
+
async _unsubscribeAll(rootContainer) {
|
|
595
|
+
const promises = [];
|
|
596
|
+
rootContainer.model_layer.traverse((component) => {
|
|
597
|
+
var _a;
|
|
598
|
+
if ((_a = component.model) === null || _a === void 0 ? void 0 : _a.tag) {
|
|
599
|
+
promises.push(rootContainer.unsubscribe(component.model.tag, component));
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
await Promise.all(promises);
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* 모든 tag 컴포넌트를 현재 provider로 재구독한다.
|
|
606
|
+
*/
|
|
607
|
+
async _resubscribeAll(rootContainer) {
|
|
608
|
+
const promises = [];
|
|
609
|
+
rootContainer.model_layer.traverse((component) => {
|
|
610
|
+
var _a;
|
|
611
|
+
if ((_a = component.model) === null || _a === void 0 ? void 0 : _a.tag) {
|
|
612
|
+
promises.push(rootContainer.subscribe(component.model.tag, component));
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
await Promise.all(promises);
|
|
616
|
+
}
|
|
455
617
|
hidePopup() {
|
|
456
618
|
if (this.popup) {
|
|
457
619
|
this.removeChild(this.popup);
|
|
@@ -584,11 +746,19 @@ BoardViewer.styles = [
|
|
|
584
746
|
z-index: 1000;
|
|
585
747
|
}
|
|
586
748
|
|
|
587
|
-
|
|
749
|
+
.fab-group {
|
|
588
750
|
position: absolute;
|
|
589
751
|
bottom: 15px;
|
|
590
752
|
right: 16px;
|
|
591
753
|
z-index: 1000;
|
|
754
|
+
display: flex;
|
|
755
|
+
flex-direction: column-reverse;
|
|
756
|
+
gap: 8px;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.fab-group md-fab {
|
|
760
|
+
--md-fab-container-width: 48px;
|
|
761
|
+
--md-fab-container-height: 48px;
|
|
592
762
|
}
|
|
593
763
|
|
|
594
764
|
[hidden] {
|
|
@@ -627,6 +797,24 @@ __decorate([
|
|
|
627
797
|
__decorate([
|
|
628
798
|
property({ type: Boolean, reflect: true, attribute: 'hide-navigation' })
|
|
629
799
|
], BoardViewer.prototype, "hideNavigation", void 0);
|
|
800
|
+
__decorate([
|
|
801
|
+
property({ type: Boolean, reflect: true, attribute: 'playback-enabled' })
|
|
802
|
+
], BoardViewer.prototype, "playbackEnabled", void 0);
|
|
803
|
+
__decorate([
|
|
804
|
+
property({ type: Object, attribute: 'playback-time-range' })
|
|
805
|
+
], BoardViewer.prototype, "playbackTimeRange", void 0);
|
|
806
|
+
__decorate([
|
|
807
|
+
state()
|
|
808
|
+
], BoardViewer.prototype, "_playbackState", void 0);
|
|
809
|
+
__decorate([
|
|
810
|
+
state()
|
|
811
|
+
], BoardViewer.prototype, "_playbackActive", void 0);
|
|
812
|
+
__decorate([
|
|
813
|
+
state()
|
|
814
|
+
], BoardViewer.prototype, "_playbackSpeed", void 0);
|
|
815
|
+
__decorate([
|
|
816
|
+
state()
|
|
817
|
+
], BoardViewer.prototype, "_playbackCurrentTime", void 0);
|
|
630
818
|
__decorate([
|
|
631
819
|
query('#target')
|
|
632
820
|
], BoardViewer.prototype, "_target", void 0);
|
|
@@ -639,6 +827,9 @@ __decorate([
|
|
|
639
827
|
__decorate([
|
|
640
828
|
query('#fullscreen')
|
|
641
829
|
], BoardViewer.prototype, "_fullscreen", void 0);
|
|
830
|
+
__decorate([
|
|
831
|
+
query('.fab-group')
|
|
832
|
+
], BoardViewer.prototype, "_fabGroup", void 0);
|
|
642
833
|
BoardViewer = __decorate([
|
|
643
834
|
customElement('ox-board-viewer')
|
|
644
835
|
], BoardViewer);
|