@operato/board 10.0.0-beta.1 → 10.0.0-beta.10

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 CHANGED
@@ -3,6 +3,104 @@
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.10](https://github.com/hatiolab/operato/compare/v10.0.0-beta.9...v10.0.0-beta.10) (2026-03-14)
7
+
8
+ **Note:** Version bump only for package @operato/board
9
+
10
+
11
+
12
+
13
+
14
+ ## [10.0.0-beta.9](https://github.com/hatiolab/operato/compare/v10.0.0-beta.8...v10.0.0-beta.9) (2026-03-13)
15
+
16
+
17
+ ### :rocket: New Features
18
+
19
+ * **board:** 2D/3D 모드별 edit-toolbar 분기 ([26ca616](https://github.com/hatiolab/operato/commit/26ca616b85294af77a8eed5f24c937c612db57f8))
20
+ * **board:** 3D 모드 기즈모 버튼, 툴바 UX 개선 ([45b46ff](https://github.com/hatiolab/operato/commit/45b46ff47ac2258bac66db51c0a6b46b16f26fbe))
21
+
22
+
23
+
24
+ ## [10.0.0-beta.8](https://github.com/hatiolab/operato/compare/v10.0.0-beta.7...v10.0.0-beta.8) (2026-03-13)
25
+
26
+
27
+ ### :rocket: New Features
28
+
29
+ * **board:** add playback feature for historical data replay ([e8a3a73](https://github.com/hatiolab/operato/commit/e8a3a730648c2b5ec17244b157e1ce59b728b524))
30
+
31
+
32
+
33
+ ## [10.0.0-beta.7](https://github.com/hatiolab/operato/compare/v10.0.0-beta.6...v10.0.0-beta.7) (2026-03-09)
34
+
35
+
36
+ ### :bug: Bug Fix
37
+
38
+ * **board:** correct invalid CSS overflow and remove debug console.log ([7292222](https://github.com/hatiolab/operato/commit/7292222646bde1140018465b4fe57f529103c48c))
39
+
40
+
41
+
42
+ ## [10.0.0-beta.6](https://github.com/hatiolab/operato/compare/v10.0.0-beta.5...v10.0.0-beta.6) (2026-03-09)
43
+
44
+
45
+ ### :bug: Bug Fix
46
+
47
+ * **board:** remove unnecessary reactive properties to prevent Lit update warnings ([2e6ad75](https://github.com/hatiolab/operato/commit/2e6ad75b853fc27a56410e4fff7de24325d24d24))
48
+
49
+
50
+
51
+ ## [10.0.0-beta.5](https://github.com/hatiolab/operato/compare/v10.0.0-beta.4...v10.0.0-beta.5) (2026-03-08)
52
+
53
+ **Note:** Version bump only for package @operato/board
54
+
55
+
56
+
57
+
58
+
59
+ ## [10.0.0-beta.4](https://github.com/hatiolab/operato/compare/v10.0.0-beta.3...v10.0.0-beta.4) (2026-03-06)
60
+
61
+ **Note:** Version bump only for package @operato/board
62
+
63
+
64
+
65
+
66
+
67
+ ## [10.0.0-beta.3](https://github.com/hatiolab/operato/compare/v10.0.0-beta.2...v10.0.0-beta.3) (2026-03-06)
68
+
69
+ **Note:** Version bump only for package @operato/board
70
+
71
+
72
+
73
+
74
+
75
+ ## [10.0.0-beta.2](https://github.com/hatiolab/operato/compare/v10.0.0-beta.1...v10.0.0-beta.2) (2026-03-05)
76
+
77
+
78
+ ### :house: Code Refactoring
79
+
80
+ * ox-board-component-info 팝업 제거, clickComponent 이벤트로 대체 ([be3a8b8](https://github.com/hatiolab/operato/commit/be3a8b8c292bb38f55bccbea3c6cfba39c329393))
81
+ * restore Model type from things-scene instead of any ([80c6268](https://github.com/hatiolab/operato/commit/80c6268e7c1a6d473f34820e0dac67ea898b2103))
82
+
83
+
84
+ ### :bug: Bug Fix
85
+
86
+ * **board:** 3D 모드에서 호스트 배경색 설정 건너뜀 ([eae693a](https://github.com/hatiolab/operato/commit/eae693a92bb2889d33c9725fe660e5102c433e21))
87
+ * property-sidebar undefined 값 바인딩 경고 수정 ([4e35dba](https://github.com/hatiolab/operato/commit/4e35dba73bfabb19e89e5f599d6a9737e9ab8103))
88
+ * things-scene v10 타입 강화에 따른 as any 제거 및 타입 구체화 ([cb50140](https://github.com/hatiolab/operato/commit/cb5014034e5e808aa63ea2ca411fe61fb0a79c8e))
89
+ * things-scene v10 타입 노출에 따른 board 타입 에러 수정 ([ae720db](https://github.com/hatiolab/operato/commit/ae720db1dc4d4cd440df11483e1531e3cd0509ce))
90
+
91
+
92
+ ### :rocket: New Features
93
+
94
+ * **property-panel:** 3D 편집 UX — 별도 3D 탭 + Material3D 에디터 + scene-level 설정 ([c64475f](https://github.com/hatiolab/operato/commit/c64475f560be02a83412ea1959cfb9328ee84fb9))
95
+
96
+
97
+ ### :mega: Other
98
+
99
+ * modernize publish config for npm release ([7fe28ab](https://github.com/hatiolab/operato/commit/7fe28ab8818f8dc4a281f9f82db5d13b49f2cf9d))
100
+ * use local file resolution for things-scene and add dev/release toggle ([2e3c54d](https://github.com/hatiolab/operato/commit/2e3c54dd38ad6bfe95d5125c9cfb36736bba675f))
101
+
102
+
103
+
6
104
  ### [9.2.1](https://github.com/hatiolab/operato/compare/v9.2.0...v9.2.1) (2025-11-09)
7
105
 
8
106
  **Note:** Version bump only for package @operato/board
@@ -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"]}
@@ -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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"edit-toolbar-style.js","sourceRoot":"","sources":["../../../src/modeller/edit-toolbar-style.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,MAAM,CAAC,MAAM,KAAK,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiOvB,CAAA","sourcesContent":["/**\n * @license Copyright © HatioLab Inc. All rights reserved.\n */\n\nimport { css } from 'lit'\n\nexport const style = css`\n :host {\n background-color: var(--edit-toolbar-background-color, var(--md-sys-color-secondary, #394e64));\n\n overflow-x: hidden;\n }\n\n [tools] {\n display: flex;\n align-items: center;\n overflow: none;\n padding: 0px 10px;\n }\n\n [tools] > * {\n padding: 0px;\n }\n\n [tools] > span[button] {\n min-width: 30px;\n }\n\n [tools] > span[padding] {\n flex: 1;\n }\n\n [tools] > .vline {\n display: block;\n flex: none;\n border-left: 1px solid rgba(255, 255, 255, 0.2);\n border-right: 1px solid rgba(0, 0, 0, 0.15);\n width: 0px;\n height: 18px;\n margin: 0 3px;\n }\n\n span[button] {\n min-height: 35px;\n\n background: var(--url-icon-htoolbar) no-repeat;\n background-position-x: 50%;\n opacity: 0.8;\n }\n span[button]:hover {\n opacity: 1;\n background-color: rgba(0, 0, 0, 0.1);\n cursor: pointer;\n }\n\n #fullscreen,\n #toggle-property {\n flex: none;\n }\n\n #align-left {\n background-position-y: 8px;\n }\n\n #align-center {\n background-position-y: -42px;\n }\n\n #align-right {\n background-position-y: -92px;\n }\n\n #align-top {\n background-position-y: -142px;\n }\n\n #align-middle {\n background-position-y: -192px;\n }\n\n #align-bottom {\n background-position-y: -242px;\n }\n\n #undo {\n background-position-y: -592px;\n }\n\n #redo {\n background-position-y: -642px;\n }\n\n #front {\n background-position-y: -292px;\n }\n\n #back {\n background-position-y: -342px;\n }\n\n #forward {\n background-position-y: -392px;\n }\n\n #backward {\n background-position-y: -442px;\n }\n\n #symmetry-x {\n background-position-y: -492px;\n }\n\n #symmetry-y {\n background-position-y: -542px;\n }\n\n #group {\n background-position-y: -492px;\n }\n\n #ungroup {\n background-position-y: -542px;\n }\n\n #fullscreen {\n background-position-y: -692px;\n }\n\n #toggle-property {\n background-position-y: -692px;\n float: right;\n }\n\n #zoomin {\n background-position-y: -742px;\n }\n\n #zoomout {\n background-position-y: -792px;\n }\n\n #fit-scene {\n background-position-y: -1492px;\n }\n\n #cut {\n background-position-y: -842px;\n }\n\n #copy {\n background-position-y: -892px;\n }\n\n #paste {\n background-position-y: -942px;\n }\n\n #delete {\n background-position-y: -992px;\n }\n\n #font-increase {\n background-position-y: -1042px;\n }\n\n #font-decrease {\n background-position-y: -1092px;\n }\n\n #style-copy {\n background-position-y: -1142px;\n }\n\n #databind-copy {\n background-position-y: -1692px;\n }\n\n #context-menu {\n background-position-y: -692px;\n }\n\n #symmetry-x {\n background-position-y: -1192px;\n }\n\n #symmetry-y {\n background-position-y: -1242px;\n }\n\n #rotate-cw {\n background-position-y: -1292px;\n }\n\n #rotate-ccw {\n background-position-y: -1342px;\n }\n\n #distribute-horizontal {\n background-position-y: -1542px;\n }\n\n #distribute-vertical {\n background-position-y: -1593px;\n }\n\n #toggle-property {\n background-position-y: -1392px;\n }\n\n #preview {\n background-position-y: -1640px;\n }\n\n /* bigger buttons */\n #fullscreen {\n background: var(--url-icon-fullscreen) 50% 10px no-repeat;\n width: var(--edit-toolbar-bigger-icon-size);\n height: var(--edit-toolbar-bigger-icon-size);\n border-left: var(--edit-toolbar-bigger-icon-line);\n }\n\n #toggle-property {\n background: var(--url-icon-collapse) 80% 10px no-repeat;\n width: var(--edit-toolbar-bigger-icon-size);\n height: var(--edit-toolbar-bigger-icon-size);\n border-left: var(--edit-toolbar-bigger-icon-line);\n }\n\n #toggle-property[active] {\n background: var(--url-icon-collapse-active) 80% 10px no-repeat;\n }\n`\n"]}
1
+ {"version":3,"file":"edit-toolbar-style.js","sourceRoot":"","sources":["../../../src/modeller/edit-toolbar-style.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,MAAM,CAAC,MAAM,KAAK,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsQvB,CAAA","sourcesContent":["/**\n * @license Copyright © HatioLab Inc. All rights reserved.\n */\n\nimport { css } from 'lit'\n\nexport const style = css`\n :host {\n background-color: var(--edit-toolbar-background-color, var(--md-sys-color-secondary, #394e64));\n\n overflow-x: hidden;\n }\n\n [tools] {\n display: flex;\n align-items: center;\n overflow: hidden;\n padding: 0px 10px;\n }\n\n [tools] > * {\n padding: 0px;\n }\n\n [tools] > span[button] {\n min-width: 30px;\n }\n\n [tools] > span[padding] {\n flex: 1;\n }\n\n [tools] > .vline {\n display: block;\n flex: none;\n border-left: 1px solid rgba(255, 255, 255, 0.2);\n border-right: 1px solid rgba(0, 0, 0, 0.15);\n width: 0px;\n height: 18px;\n margin: 0 3px;\n }\n\n span[button] {\n min-height: 35px;\n\n background: var(--url-icon-htoolbar) no-repeat;\n background-position-x: 50%;\n opacity: 0.8;\n }\n span[button]:hover {\n opacity: 1;\n background-color: rgba(0, 0, 0, 0.1);\n cursor: pointer;\n }\n\n #fullscreen,\n #toggle-property {\n flex: none;\n }\n\n #align-left {\n background-position-y: 8px;\n }\n\n #align-center {\n background-position-y: -42px;\n }\n\n #align-right {\n background-position-y: -92px;\n }\n\n #align-top {\n background-position-y: -142px;\n }\n\n #align-middle {\n background-position-y: -192px;\n }\n\n #align-bottom {\n background-position-y: -242px;\n }\n\n #undo {\n background-position-y: -592px;\n }\n\n #redo {\n background-position-y: -642px;\n }\n\n #front {\n background-position-y: -292px;\n }\n\n #back {\n background-position-y: -342px;\n }\n\n #forward {\n background-position-y: -392px;\n }\n\n #backward {\n background-position-y: -442px;\n }\n\n #symmetry-x {\n background-position-y: -492px;\n }\n\n #symmetry-y {\n background-position-y: -542px;\n }\n\n #group {\n background-position-y: -492px;\n }\n\n #ungroup {\n background-position-y: -542px;\n }\n\n #fullscreen {\n background-position-y: -692px;\n }\n\n #toggle-property {\n background-position-y: -692px;\n float: right;\n }\n\n #zoomin {\n background-position-y: -742px;\n }\n\n #zoomout {\n background-position-y: -792px;\n }\n\n #fit-scene {\n background-position-y: -1492px;\n }\n\n #cut {\n background-position-y: -842px;\n }\n\n #copy {\n background-position-y: -892px;\n }\n\n #paste {\n background-position-y: -942px;\n }\n\n #delete {\n background-position-y: -992px;\n }\n\n #font-increase {\n background-position-y: -1042px;\n }\n\n #font-decrease {\n background-position-y: -1092px;\n }\n\n #style-copy {\n background-position-y: -1142px;\n }\n\n #databind-copy {\n background-position-y: -1692px;\n }\n\n #context-menu {\n background-position-y: -692px;\n }\n\n #symmetry-x {\n background-position-y: -1192px;\n }\n\n #symmetry-y {\n background-position-y: -1242px;\n }\n\n #rotate-cw {\n background-position-y: -1292px;\n }\n\n #rotate-ccw {\n background-position-y: -1342px;\n }\n\n #distribute-horizontal {\n background-position-y: -1542px;\n }\n\n #distribute-vertical {\n background-position-y: -1593px;\n }\n\n #align-z-front {\n background-position-y: -142px;\n filter: hue-rotate(180deg);\n }\n\n #align-z-middle {\n background-position-y: -192px;\n filter: hue-rotate(180deg);\n }\n\n #align-z-back {\n background-position-y: -242px;\n filter: hue-rotate(180deg);\n }\n\n #distribute-z {\n background-position-y: -1593px;\n filter: hue-rotate(180deg);\n }\n\n .gizmo-btn {\n background: none !important;\n color: rgba(255, 255, 255, 0.5);\n min-width: 24px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n border-radius: 3px;\n padding: 2px;\n }\n\n .gizmo-btn[disabled] {\n opacity: 0.3;\n pointer-events: none;\n }\n\n #toggle-property {\n background-position-y: -1392px;\n }\n\n #preview {\n background-position-y: -1640px;\n }\n\n /* bigger buttons */\n #fullscreen {\n background: var(--url-icon-fullscreen) 50% 10px no-repeat;\n width: var(--edit-toolbar-bigger-icon-size);\n height: var(--edit-toolbar-bigger-icon-size);\n border-left: var(--edit-toolbar-bigger-icon-line);\n }\n\n #toggle-property {\n background: var(--url-icon-collapse) 80% 10px no-repeat;\n width: var(--edit-toolbar-bigger-icon-size);\n height: var(--edit-toolbar-bigger-icon-size);\n border-left: var(--edit-toolbar-bigger-icon-line);\n }\n\n #toggle-property[active] {\n background: var(--url-icon-collapse-active) 80% 10px no-repeat;\n }\n`\n"]}
@@ -8,6 +8,8 @@ export declare class EditToolbar extends LitElement {
8
8
  scene?: Scene;
9
9
  selected: any[];
10
10
  hideProperty: boolean;
11
+ private _dimension;
12
+ private _gizmoAttached;
11
13
  private cliped?;
12
14
  private redo;
13
15
  private undo;
@@ -36,6 +38,9 @@ export declare class EditToolbar extends LitElement {
36
38
  onUndo(undoable: boolean, redoable: boolean): void;
37
39
  onRedo(undoable: boolean, redoable: boolean): void;
38
40
  onSceneChanged(after?: Scene, before?: Scene): void;
41
+ private _onDimensionChanged;
42
+ private _onGizmoAttachChanged;
43
+ private _setGizmoMode;
39
44
  onSelectedChanged(after: Component[], before: Component[]): void;
40
45
  onTapUndo(): void;
41
46
  onTapRedo(): void;