@operato/scene-visualizer 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.
@@ -0,0 +1,388 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * GLTF 애니메이션 타겟 선택 에디터.
5
+ * All / None / Custom 모드로 started에 바인딩할 애니메이션 선택.
6
+ * 각 애니메이션을 개별 미리보기(play/stop) 가능.
7
+ */
8
+ import { __decorate } from "tslib";
9
+ import '@material/web/icon/icon.js';
10
+ import '@operato/i18n/ox-i18n.js';
11
+ import { css, html, nothing } from 'lit';
12
+ import { customElement, property, state } from 'lit/decorators.js';
13
+ import { OxPropertyEditor } from '@operato/property-editor';
14
+ import { ScrollbarStyles } from '@operato/styles/scrollbar-styles.js';
15
+ function getMode(value) {
16
+ if (value === '*')
17
+ return 'all';
18
+ if (value === 'none')
19
+ return 'none';
20
+ if (Array.isArray(value) && value.length > 0)
21
+ return 'custom';
22
+ if (Array.isArray(value))
23
+ return 'custom'; // empty array = custom with nothing
24
+ if (!value)
25
+ return 'all'; // undefined = default = all
26
+ return 'all';
27
+ }
28
+ let GLTFPlayTargetsEditor = class GLTFPlayTargetsEditor extends OxPropertyEditor {
29
+ static styles = [
30
+ ...OxPropertyEditor.styles,
31
+ ScrollbarStyles,
32
+ css `
33
+ .mode-selector {
34
+ display: flex;
35
+ gap: 0;
36
+ margin-bottom: 6px;
37
+ border: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.15));
38
+ border-radius: 6px;
39
+ overflow: hidden;
40
+ }
41
+
42
+ .mode-btn {
43
+ flex: 1;
44
+ padding: 5px 0;
45
+ font-size: 11px;
46
+ font-weight: 500;
47
+ text-align: center;
48
+ cursor: pointer;
49
+ background: var(--md-sys-color-surface-container, #f3edf7);
50
+ color: var(--md-sys-color-on-surface, #1c1b1f);
51
+ border: none;
52
+ border-right: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.1));
53
+ transition: all 0.15s;
54
+ }
55
+
56
+ .mode-btn:last-child {
57
+ border-right: none;
58
+ }
59
+
60
+ .mode-btn:hover {
61
+ background: var(--md-sys-color-surface-container-highest, #e6e0e9);
62
+ }
63
+
64
+ .mode-btn[active] {
65
+ background: var(--md-sys-color-primary, #6750a4);
66
+ color: var(--md-sys-color-on-primary, #fff);
67
+ }
68
+
69
+ .anim-list {
70
+ display: flex;
71
+ flex-direction: column;
72
+ gap: 1px;
73
+ max-height: 200px;
74
+ overflow-y: auto;
75
+ font-size: 12px;
76
+ }
77
+
78
+ .anim-item {
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 6px;
82
+ padding: 4px 8px;
83
+ border-radius: 4px;
84
+ transition: background 0.1s;
85
+ }
86
+
87
+ .anim-item:hover {
88
+ background: var(--md-sys-color-primary-container, rgba(103, 80, 164, 0.12));
89
+ }
90
+
91
+ .anim-item input[type='checkbox'] {
92
+ margin: 0;
93
+ accent-color: var(--md-sys-color-primary, #6750a4);
94
+ }
95
+
96
+ .anim-item span {
97
+ flex: 1;
98
+ font-size: 12px;
99
+ color: var(--md-sys-color-on-surface, #1c1b1f);
100
+ }
101
+
102
+ .anim-item md-icon {
103
+ --md-icon-size: 18px;
104
+ cursor: pointer;
105
+ color: var(--md-sys-color-on-surface-variant, #666);
106
+ transition: color 0.15s;
107
+ }
108
+
109
+ .anim-item md-icon:hover {
110
+ color: var(--md-sys-color-primary, #6750a4);
111
+ }
112
+
113
+ .anim-item md-icon[playing] {
114
+ color: var(--md-sys-color-primary, #6750a4);
115
+ }
116
+
117
+ .mode-desc {
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 6px;
121
+ font-size: 11px;
122
+ color: var(--md-sys-color-on-surface-variant, #666);
123
+ padding: 4px 0;
124
+ }
125
+
126
+ .mini-btn {
127
+ font-size: 10px;
128
+ padding: 2px 8px;
129
+ border: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.15));
130
+ border-radius: 4px;
131
+ background: var(--md-sys-color-surface-container, #f3edf7);
132
+ color: var(--md-sys-color-on-surface, #1c1b1f);
133
+ cursor: pointer;
134
+ }
135
+
136
+ .mini-btn:hover {
137
+ background: var(--md-sys-color-surface-container-highest, #e6e0e9);
138
+ }
139
+
140
+ .play-toggle {
141
+ display: flex;
142
+ align-items: center;
143
+ gap: 6px;
144
+ padding: 4px 0;
145
+ margin-bottom: 4px;
146
+ cursor: pointer;
147
+ font-size: 12px;
148
+ font-weight: 500;
149
+ color: var(--md-sys-color-on-surface, #1c1b1f);
150
+ }
151
+
152
+ .play-toggle input[type='checkbox'] {
153
+ accent-color: var(--md-sys-color-primary, #6750a4);
154
+ }
155
+
156
+ .empty {
157
+ color: var(--md-sys-color-on-surface-variant, #999);
158
+ font-size: 11px;
159
+ padding: 8px;
160
+ text-align: center;
161
+ }
162
+ `
163
+ ];
164
+ _component = null;
165
+ _lastExternalValue = undefined;
166
+ updated(changes) {
167
+ if (changes.has('src')) {
168
+ this.dispatchEvent(new CustomEvent('i-need-selected', {
169
+ bubbles: true,
170
+ composed: true,
171
+ detail: {
172
+ callback: (selected) => {
173
+ this._component = selected?.[0];
174
+ this._refreshAnimNames();
175
+ }
176
+ }
177
+ }));
178
+ }
179
+ }
180
+ _refreshAnimNames() {
181
+ if (!this._component) {
182
+ this._animNames = [];
183
+ return;
184
+ }
185
+ this._animNames = this._component.animationNames ?? [];
186
+ }
187
+ _pollTimer;
188
+ connectedCallback() {
189
+ super.connectedCallback();
190
+ // GLTF 비동기 로드 후 애니메이션 목록 갱신을 위한 폴링 (최대 20회 = 20초)
191
+ let retries = 0;
192
+ this._pollTimer = window.setInterval(() => {
193
+ if (!this._animNames?.length) {
194
+ this._refreshAnimNames();
195
+ }
196
+ if (this._animNames?.length || ++retries >= 20) {
197
+ clearInterval(this._pollTimer);
198
+ this._pollTimer = undefined;
199
+ }
200
+ }, 1000);
201
+ }
202
+ disconnectedCallback() {
203
+ super.disconnectedCallback();
204
+ if (this._pollTimer) {
205
+ clearInterval(this._pollTimer);
206
+ }
207
+ }
208
+ editorTemplate(value, _spec) {
209
+ if (!this._animNames?.length) {
210
+ this._refreshAnimNames();
211
+ }
212
+ if (value !== this._lastExternalValue) {
213
+ this._lastExternalValue = value;
214
+ this._mode = getMode(value);
215
+ }
216
+ const mode = this._mode;
217
+ const animCount = this._animNames?.length ?? 0;
218
+ const targets = Array.isArray(this.value) ? this.value : (Array.isArray(value) ? value : []);
219
+ const isPlaying = this._getPlayState();
220
+ return html `
221
+ <fieldset fullwidth>
222
+ ${animCount > 0
223
+ ? html `
224
+ <label class="play-toggle">
225
+ <input
226
+ type="checkbox"
227
+ .checked=${isPlaying}
228
+ @change=${(e) => {
229
+ e.stopPropagation();
230
+ this._setPlay(e.target.checked);
231
+ }}
232
+ />
233
+ <span>Play</span>
234
+ </label>
235
+
236
+ <div class="mode-selector">
237
+ <div class="mode-btn" ?active=${mode === 'all'} @click=${() => this._setMode('all')}>All</div>
238
+ <div class="mode-btn" ?active=${mode === 'none'} @click=${() => this._setMode('none')}>None</div>
239
+ <div class="mode-btn" ?active=${mode === 'custom'} @click=${() => this._setMode('custom')}>Custom</div>
240
+ </div>
241
+
242
+ ${mode === 'all'
243
+ ? html `<div class="mode-desc">${animCount} animations — play all</div>`
244
+ : nothing}
245
+ ${mode === 'none'
246
+ ? html `<div class="mode-desc">No animations auto-play</div>`
247
+ : nothing}
248
+ ${mode === 'custom'
249
+ ? html `
250
+ <div class="mode-desc">
251
+ <span style="flex:1">${targets.length} / ${animCount} selected</span>
252
+ <button class="mini-btn" @click=${() => this._applyValue([...(this._animNames ?? [])])}>All</button>
253
+ <button class="mini-btn" @click=${() => this._applyValue([])}>Clear</button>
254
+ </div>
255
+ `
256
+ : nothing}
257
+
258
+ <div class="anim-list">
259
+ ${(this._animNames ?? []).map(name => html `
260
+ <div class="anim-item">
261
+ ${mode === 'custom'
262
+ ? html `
263
+ <input
264
+ type="checkbox"
265
+ .checked=${targets.includes(name)}
266
+ @change=${(e) => {
267
+ e.stopPropagation();
268
+ this._onToggle(name, e.target.checked, targets);
269
+ }}
270
+ />
271
+ `
272
+ : nothing}
273
+ <span>${name}</span>
274
+ <md-icon
275
+ ?playing=${this._playingAnim === name}
276
+ @click=${() => this._togglePreview(name)}
277
+ title="preview"
278
+ >${this._playingAnim === name ? 'stop' : 'play_arrow'}</md-icon>
279
+ </div>
280
+ `)}
281
+ </div>
282
+ `
283
+ : html `<div class="empty">No animations available</div>`}
284
+ </fieldset>
285
+ `;
286
+ }
287
+ _setMode(mode) {
288
+ const prevMode = this._mode;
289
+ this._mode = mode;
290
+ let newValue;
291
+ switch (mode) {
292
+ case 'all':
293
+ newValue = '*';
294
+ break;
295
+ case 'none':
296
+ newValue = 'none';
297
+ break;
298
+ case 'custom':
299
+ if (prevMode === 'all') {
300
+ newValue = [...(this._animNames ?? [])];
301
+ }
302
+ else if (prevMode === 'none') {
303
+ newValue = [];
304
+ }
305
+ else {
306
+ newValue = Array.isArray(this.value) ? this.value : [];
307
+ }
308
+ break;
309
+ }
310
+ this._applyValue(newValue);
311
+ }
312
+ _onToggle(name, checked, current) {
313
+ const list = [...current];
314
+ if (checked) {
315
+ if (!list.includes(name))
316
+ list.push(name);
317
+ }
318
+ else {
319
+ const idx = list.indexOf(name);
320
+ if (idx >= 0)
321
+ list.splice(idx, 1);
322
+ }
323
+ this._applyValue(list);
324
+ }
325
+ _applyValue(newValue) {
326
+ this.value = newValue;
327
+ this.requestUpdate();
328
+ this.dispatchEvent(new CustomEvent('i-need-selected', {
329
+ bubbles: true,
330
+ composed: true,
331
+ detail: {
332
+ callback: (selected) => {
333
+ selected[0].set('playTargets', newValue);
334
+ }
335
+ }
336
+ }));
337
+ }
338
+ _getPlayState() {
339
+ return this._component?.state?.play !== false;
340
+ }
341
+ _setPlay(value) {
342
+ if (!this._component)
343
+ return;
344
+ this._component.set('play', value);
345
+ this.requestUpdate();
346
+ }
347
+ _togglePreview(name) {
348
+ if (!this._component)
349
+ return;
350
+ const ro = this._component.realObject;
351
+ if (!ro?._animationActions)
352
+ return;
353
+ const action = ro._animationActions.get(name);
354
+ if (!action)
355
+ return;
356
+ if (this._playingAnim === name) {
357
+ action.stop();
358
+ this._playingAnim = null;
359
+ }
360
+ else {
361
+ // 이전 프리뷰 중지
362
+ if (this._playingAnim) {
363
+ ro._animationActions.get(this._playingAnim)?.stop();
364
+ }
365
+ action.play();
366
+ this._playingAnim = name;
367
+ }
368
+ this._component.invalidate();
369
+ this.requestUpdate();
370
+ }
371
+ };
372
+ __decorate([
373
+ property({ type: String })
374
+ ], GLTFPlayTargetsEditor.prototype, "src", void 0);
375
+ __decorate([
376
+ state()
377
+ ], GLTFPlayTargetsEditor.prototype, "_animNames", void 0);
378
+ __decorate([
379
+ state()
380
+ ], GLTFPlayTargetsEditor.prototype, "_mode", void 0);
381
+ __decorate([
382
+ state()
383
+ ], GLTFPlayTargetsEditor.prototype, "_playingAnim", void 0);
384
+ GLTFPlayTargetsEditor = __decorate([
385
+ customElement('property-editor-gltf-play-targets')
386
+ ], GLTFPlayTargetsEditor);
387
+ export default GLTFPlayTargetsEditor;
388
+ //# sourceMappingURL=property-editor-gltf-play-targets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"property-editor-gltf-play-targets.js","sourceRoot":"","sources":["../../src/editors/property-editor-gltf-play-targets.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * GLTF 애니메이션 타겟 선택 에디터.\n * All / None / Custom 모드로 started에 바인딩할 애니메이션 선택.\n * 각 애니메이션을 개별 미리보기(play/stop) 가능.\n */\n\nimport '@material/web/icon/icon.js'\nimport '@operato/i18n/ox-i18n.js'\n\nimport { css, html, PropertyValues, TemplateResult, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { OxPropertyEditor, PropertySpec } from '@operato/property-editor'\nimport { ScrollbarStyles } from '@operato/styles/scrollbar-styles.js'\n\ntype AnimMode = 'all' | 'none' | 'custom'\n\nfunction getMode(value: any): AnimMode {\n if (value === '*') return 'all'\n if (value === 'none') return 'none'\n if (Array.isArray(value) && value.length > 0) return 'custom'\n if (Array.isArray(value)) return 'custom' // empty array = custom with nothing\n if (!value) return 'all' // undefined = default = all\n return 'all'\n}\n\n@customElement('property-editor-gltf-play-targets')\nexport default class GLTFPlayTargetsEditor extends OxPropertyEditor {\n static styles = [\n ...OxPropertyEditor.styles,\n ScrollbarStyles,\n css`\n .mode-selector {\n display: flex;\n gap: 0;\n margin-bottom: 6px;\n border: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.15));\n border-radius: 6px;\n overflow: hidden;\n }\n\n .mode-btn {\n flex: 1;\n padding: 5px 0;\n font-size: 11px;\n font-weight: 500;\n text-align: center;\n cursor: pointer;\n background: var(--md-sys-color-surface-container, #f3edf7);\n color: var(--md-sys-color-on-surface, #1c1b1f);\n border: none;\n border-right: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.1));\n transition: all 0.15s;\n }\n\n .mode-btn:last-child {\n border-right: none;\n }\n\n .mode-btn:hover {\n background: var(--md-sys-color-surface-container-highest, #e6e0e9);\n }\n\n .mode-btn[active] {\n background: var(--md-sys-color-primary, #6750a4);\n color: var(--md-sys-color-on-primary, #fff);\n }\n\n .anim-list {\n display: flex;\n flex-direction: column;\n gap: 1px;\n max-height: 200px;\n overflow-y: auto;\n font-size: 12px;\n }\n\n .anim-item {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 8px;\n border-radius: 4px;\n transition: background 0.1s;\n }\n\n .anim-item:hover {\n background: var(--md-sys-color-primary-container, rgba(103, 80, 164, 0.12));\n }\n\n .anim-item input[type='checkbox'] {\n margin: 0;\n accent-color: var(--md-sys-color-primary, #6750a4);\n }\n\n .anim-item span {\n flex: 1;\n font-size: 12px;\n color: var(--md-sys-color-on-surface, #1c1b1f);\n }\n\n .anim-item md-icon {\n --md-icon-size: 18px;\n cursor: pointer;\n color: var(--md-sys-color-on-surface-variant, #666);\n transition: color 0.15s;\n }\n\n .anim-item md-icon:hover {\n color: var(--md-sys-color-primary, #6750a4);\n }\n\n .anim-item md-icon[playing] {\n color: var(--md-sys-color-primary, #6750a4);\n }\n\n .mode-desc {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n color: var(--md-sys-color-on-surface-variant, #666);\n padding: 4px 0;\n }\n\n .mini-btn {\n font-size: 10px;\n padding: 2px 8px;\n border: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.15));\n border-radius: 4px;\n background: var(--md-sys-color-surface-container, #f3edf7);\n color: var(--md-sys-color-on-surface, #1c1b1f);\n cursor: pointer;\n }\n\n .mini-btn:hover {\n background: var(--md-sys-color-surface-container-highest, #e6e0e9);\n }\n\n .play-toggle {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 0;\n margin-bottom: 4px;\n cursor: pointer;\n font-size: 12px;\n font-weight: 500;\n color: var(--md-sys-color-on-surface, #1c1b1f);\n }\n\n .play-toggle input[type='checkbox'] {\n accent-color: var(--md-sys-color-primary, #6750a4);\n }\n\n .empty {\n color: var(--md-sys-color-on-surface-variant, #999);\n font-size: 11px;\n padding: 8px;\n text-align: center;\n }\n `\n ]\n\n @property({ type: String }) declare src: string | undefined\n\n @state() declare private _animNames: string[]\n @state() declare private _mode: AnimMode\n @state() declare private _playingAnim: string | null\n\n private _component: any = null\n private _lastExternalValue: any = undefined\n\n updated(changes: PropertyValues<this>) {\n if (changes.has('src')) {\n this.dispatchEvent(\n new CustomEvent('i-need-selected', {\n bubbles: true,\n composed: true,\n detail: {\n callback: (selected: any[]) => {\n this._component = selected?.[0]\n this._refreshAnimNames()\n }\n }\n })\n )\n }\n }\n\n private _refreshAnimNames() {\n if (!this._component) {\n this._animNames = []\n return\n }\n\n this._animNames = (this._component.animationNames as string[]) ?? []\n }\n\n private _pollTimer?: number\n\n connectedCallback() {\n super.connectedCallback()\n // GLTF 비동기 로드 후 애니메이션 목록 갱신을 위한 폴링 (최대 20회 = 20초)\n let retries = 0\n this._pollTimer = window.setInterval(() => {\n if (!this._animNames?.length) {\n this._refreshAnimNames()\n }\n if (this._animNames?.length || ++retries >= 20) {\n clearInterval(this._pollTimer)\n this._pollTimer = undefined\n }\n }, 1000)\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n if (this._pollTimer) {\n clearInterval(this._pollTimer)\n }\n }\n\n editorTemplate(value: any, _spec: PropertySpec): TemplateResult {\n if (!this._animNames?.length) {\n this._refreshAnimNames()\n }\n\n if (value !== this._lastExternalValue) {\n this._lastExternalValue = value\n this._mode = getMode(value)\n }\n\n const mode = this._mode\n const animCount = this._animNames?.length ?? 0\n const targets: string[] = Array.isArray(this.value) ? this.value : (Array.isArray(value) ? value : [])\n\n const isPlaying = this._getPlayState()\n\n return html`\n <fieldset fullwidth>\n ${animCount > 0\n ? html`\n <label class=\"play-toggle\">\n <input\n type=\"checkbox\"\n .checked=${isPlaying}\n @change=${(e: Event) => {\n e.stopPropagation()\n this._setPlay((e.target as HTMLInputElement).checked)\n }}\n />\n <span>Play</span>\n </label>\n\n <div class=\"mode-selector\">\n <div class=\"mode-btn\" ?active=${mode === 'all'} @click=${() => this._setMode('all')}>All</div>\n <div class=\"mode-btn\" ?active=${mode === 'none'} @click=${() => this._setMode('none')}>None</div>\n <div class=\"mode-btn\" ?active=${mode === 'custom'} @click=${() => this._setMode('custom')}>Custom</div>\n </div>\n\n ${mode === 'all'\n ? html`<div class=\"mode-desc\">${animCount} animations — play all</div>`\n : nothing}\n ${mode === 'none'\n ? html`<div class=\"mode-desc\">No animations auto-play</div>`\n : nothing}\n ${mode === 'custom'\n ? html`\n <div class=\"mode-desc\">\n <span style=\"flex:1\">${targets.length} / ${animCount} selected</span>\n <button class=\"mini-btn\" @click=${() => this._applyValue([...(this._animNames ?? [])])}>All</button>\n <button class=\"mini-btn\" @click=${() => this._applyValue([])}>Clear</button>\n </div>\n `\n : nothing}\n\n <div class=\"anim-list\">\n ${(this._animNames ?? []).map(\n name => html`\n <div class=\"anim-item\">\n ${mode === 'custom'\n ? html`\n <input\n type=\"checkbox\"\n .checked=${targets.includes(name)}\n @change=${(e: Event) => {\n e.stopPropagation()\n this._onToggle(name, (e.target as HTMLInputElement).checked, targets)\n }}\n />\n `\n : nothing}\n <span>${name}</span>\n <md-icon\n ?playing=${this._playingAnim === name}\n @click=${() => this._togglePreview(name)}\n title=\"preview\"\n >${this._playingAnim === name ? 'stop' : 'play_arrow'}</md-icon>\n </div>\n `\n )}\n </div>\n `\n : html`<div class=\"empty\">No animations available</div>`}\n </fieldset>\n `\n }\n\n private _setMode(mode: AnimMode) {\n const prevMode = this._mode\n this._mode = mode\n\n let newValue: any\n switch (mode) {\n case 'all':\n newValue = '*'\n break\n case 'none':\n newValue = 'none'\n break\n case 'custom':\n if (prevMode === 'all') {\n newValue = [...(this._animNames ?? [])]\n } else if (prevMode === 'none') {\n newValue = []\n } else {\n newValue = Array.isArray(this.value) ? this.value : []\n }\n break\n }\n\n this._applyValue(newValue)\n }\n\n private _onToggle(name: string, checked: boolean, current: string[]) {\n const list = [...current]\n if (checked) {\n if (!list.includes(name)) list.push(name)\n } else {\n const idx = list.indexOf(name)\n if (idx >= 0) list.splice(idx, 1)\n }\n this._applyValue(list)\n }\n\n private _applyValue(newValue: any) {\n this.value = newValue\n this.requestUpdate()\n\n this.dispatchEvent(\n new CustomEvent('i-need-selected', {\n bubbles: true,\n composed: true,\n detail: {\n callback: (selected: any[]) => {\n selected[0].set('playTargets', newValue)\n }\n }\n })\n )\n }\n\n private _getPlayState(): boolean {\n return this._component?.state?.play !== false\n }\n\n private _setPlay(value: boolean) {\n if (!this._component) return\n this._component.set('play', value)\n this.requestUpdate()\n }\n\n private _togglePreview(name: string) {\n if (!this._component) return\n\n const ro = this._component.realObject as any\n if (!ro?._animationActions) return\n\n const action = ro._animationActions.get(name)\n if (!action) return\n\n if (this._playingAnim === name) {\n action.stop()\n this._playingAnim = null\n } else {\n // 이전 프리뷰 중지\n if (this._playingAnim) {\n ro._animationActions.get(this._playingAnim)?.stop()\n }\n action.play()\n this._playingAnim = name\n }\n\n this._component.invalidate()\n this.requestUpdate()\n }\n}\n"]}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@operato/scene-visualizer",
3
3
  "description": "visualizer component for operato-scene",
4
4
  "author": "heartyoh",
5
- "version": "10.0.0-beta.7",
5
+ "version": "10.0.0-beta.8",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "module": "dist/index.js",
@@ -63,5 +63,5 @@
63
63
  "prettier --write"
64
64
  ]
65
65
  },
66
- "gitHead": "703631c6240e8d44f00cf3c6c51981ba9da9adbc"
66
+ "gitHead": "ef571c6300992a46028e8cb015ab42bc44a120a3"
67
67
  }
@@ -16,7 +16,13 @@
16
16
  "label.camera-x": "camera X",
17
17
  "label.camera-y": "camera Y",
18
18
  "label.camera-z": "camera Z",
19
+ "label.gltf-info": "GLTF Info",
19
20
  "label.gltf-file-info": "file info.",
21
+ "label.fill-targets": "Fill Targets",
22
+ "label.no-gltf-meshes": "No meshes available",
23
+ "label.original-ratio": "Original Ratio",
24
+ "label.fit-ratio": "Fit Ratio",
25
+ "label.play-targets": "Play Targets",
20
26
  "label.gamma-factor": "gamma factor",
21
27
  "label.section-digits": "section digits",
22
28
  "label.shelf-locations": "shelf locations",
@@ -16,7 +16,12 @@
16
16
  "label.camera-x": "カメラX",
17
17
  "label.camera-y": "カメラY",
18
18
  "label.camera-z": "カメラZ",
19
+ "label.gltf-info": "GLTF情報",
19
20
  "label.gltf-file-info": "ファイル情報",
21
+ "label.fill-targets": "カラー適用対象",
22
+ "label.no-gltf-meshes": "メッシュがありません",
23
+ "label.original-ratio": "元の比率",
24
+ "label.fit-ratio": "比率調整",
20
25
  "label.gamma-factor": "ガンマ係数",
21
26
  "label.section-digits": "セクションの桁数",
22
27
  "label.shelf-locations": "棚の位置",
@@ -16,7 +16,12 @@
16
16
  "label.camera-x": "카메라 X",
17
17
  "label.camera-y": "카메라 Y",
18
18
  "label.camera-z": "카메라 Z",
19
- "label.gltf-info": "파일정보",
19
+ "label.gltf-info": "GLTF 정보",
20
+ "label.fill-targets": "색상 적용 대상",
21
+ "label.no-gltf-meshes": "메시가 없습니다",
22
+ "label.original-ratio": "원본 비율",
23
+ "label.fit-ratio": "비율 맞춤",
24
+ "label.play-targets": "재생 대상",
20
25
  "label.gamma-factor": "감마 인자",
21
26
  "label.section-digits": "섹션 넘버링",
22
27
  "label.shelf-locations": "선반 로케이션",
@@ -16,7 +16,12 @@
16
16
  "label.camera-x": "camera X",
17
17
  "label.camera-y": "camera Y",
18
18
  "label.camera-z": "camera Z",
19
+ "label.gltf-info": "GLTF Info",
19
20
  "label.gltf-file-info": "file info.",
21
+ "label.fill-targets": "Fill Targets",
22
+ "label.no-gltf-meshes": "No meshes available",
23
+ "label.original-ratio": "Original Ratio",
24
+ "label.fit-ratio": "Fit Ratio",
20
25
  "label.gamma-factor": "gamma factor",
21
26
  "label.section-digits": "section digits",
22
27
  "label.shelf-locations": "shelf locations",
@@ -16,7 +16,12 @@
16
16
  "label.camera-x": "摄像机X",
17
17
  "label.camera-y": "摄像机Y",
18
18
  "label.camera-z": "摄像机Z",
19
+ "label.gltf-info": "GLTF信息",
19
20
  "label.gltf-file-info": "文件信息",
21
+ "label.fill-targets": "颜色目标",
22
+ "label.no-gltf-meshes": "没有可用的网格",
23
+ "label.original-ratio": "原始比例",
24
+ "label.fit-ratio": "比例适配",
20
25
  "label.gamma-factor": "伽马因子",
21
26
  "label.section-digits": "部门数字",
22
27
  "label.shelf-locations": "货架位置",