@operato/property-panel 10.0.0-beta.38 → 10.0.0-beta.39

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,21 @@
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.39](https://github.com/hatiolab/operato/compare/v10.0.0-beta.38...v10.0.0-beta.39) (2026-04-18)
7
+
8
+
9
+ ### :rocket: New Features
10
+
11
+ * **property-panel:** hemisphere 색상 편집 + key light 방향/그림자 컨트롤 ([d0cc346](https://github.com/hatiolab/operato/commit/d0cc34651460e0b0d8aa290f0897c75aa7fb3fc7))
12
+ * **property-panel:** lighting preset UI 개선 — 값 범위 확장 + 버튼→select 전환 ([b866db9](https://github.com/hatiolab/operato/commit/b866db9a2871573095b415d0ead63401ae336c89)), closes [#ffaa55](https://github.com/hatiolab/operato/issues/ffaa55) [#88](https://github.com/hatiolab/operato/issues/88)
13
+
14
+
15
+ ### :house: Code Refactoring
16
+
17
+ * **property-panel:** scene3d 탭에서 bookmarks 섹션 제거 ([50d12de](https://github.com/hatiolab/operato/commit/50d12de7bc20a192503907db05a8506951931206))
18
+
19
+
20
+
6
21
  ## [10.0.0-beta.38](https://github.com/hatiolab/operato/compare/v10.0.0-beta.37...v10.0.0-beta.38) (2026-04-17)
7
22
 
8
23
 
@@ -23,17 +23,12 @@ export declare class PropertyScene3D extends AbstractProperty {
23
23
  */
24
24
  private _renderPlacement;
25
25
  private _renderMode;
26
- private _renderBookmarks;
27
- private _longPressTimer?;
28
- private _pressTarget?;
29
- private _onBookmarkMouseDown;
30
- private _onBookmarkMouseUp;
31
- private _bookmarkAction;
32
26
  private _renderRenderer;
33
27
  private _renderSky;
34
28
  private _renderHemisphereLight;
35
29
  private _renderKeyLight;
36
30
  private _renderLightingPresets;
31
+ private _onPresetChange;
37
32
  private _renderFloor;
38
33
  private _applyLightingPreset;
39
34
  private _onFloorMaterialChange;
@@ -10,42 +10,14 @@ import { property } from 'lit/decorators.js';
10
10
  import { PropertyGridStyles } from '@operato/styles/property-grid-styles.js';
11
11
  import { AbstractProperty } from '../abstract-property.js';
12
12
  const LIGHTING_PRESETS = {
13
- Neutral: {
14
- hemiIntensity: 0.6,
15
- dirLightEnabled: true,
16
- dirLightColor: '#ffffff',
17
- dirLightIntensity: 0.5
18
- },
19
- Studio: {
20
- hemiIntensity: 0.5,
21
- dirLightEnabled: true,
22
- dirLightColor: '#ffffff',
23
- dirLightIntensity: 0.6
24
- },
25
- Bright: {
26
- hemiIntensity: 0.8,
27
- dirLightEnabled: true,
28
- dirLightColor: '#ffffff',
29
- dirLightIntensity: 0.7
30
- },
31
- Warm: {
32
- hemiIntensity: 0.5,
33
- dirLightEnabled: true,
34
- dirLightColor: '#ffcc88',
35
- dirLightIntensity: 0.4
36
- },
37
- Cool: {
38
- hemiIntensity: 0.5,
39
- dirLightEnabled: true,
40
- dirLightColor: '#cce0ff',
41
- dirLightIntensity: 0.4
42
- },
43
- Flat: {
44
- hemiIntensity: 0.8,
45
- dirLightEnabled: false,
46
- dirLightColor: '#ffffff',
47
- dirLightIntensity: 0
48
- }
13
+ Neutral: { hemiIntensity: 1.0, dirLightEnabled: true, dirLightColor: '#ffffff', dirLightIntensity: 1.0 },
14
+ Studio: { hemiIntensity: 0.6, dirLightEnabled: true, dirLightColor: '#ffffff', dirLightIntensity: 1.8 },
15
+ Bright: { hemiIntensity: 2.0, dirLightEnabled: true, dirLightColor: '#ffffff', dirLightIntensity: 2.0 },
16
+ Dim: { hemiIntensity: 0.2, dirLightEnabled: true, dirLightColor: '#fff5e0', dirLightIntensity: 0.5 },
17
+ Warm: { hemiIntensity: 0.8, dirLightEnabled: true, dirLightColor: '#ffaa55', dirLightIntensity: 1.5 },
18
+ Cool: { hemiIntensity: 0.8, dirLightEnabled: true, dirLightColor: '#88bbff', dirLightIntensity: 1.5 },
19
+ Dramatic: { hemiIntensity: 0.15, dirLightEnabled: true, dirLightColor: '#ffffff', dirLightIntensity: 2.5 },
20
+ Flat: { hemiIntensity: 2.0, dirLightEnabled: false, dirLightColor: '#ffffff', dirLightIntensity: 0 }
49
21
  };
50
22
  /**
51
23
  * Scene-level 3D settings for model-layer.
@@ -59,7 +31,7 @@ export class PropertyScene3D extends AbstractProperty {
59
31
  render() {
60
32
  const value = this.value || {};
61
33
  return html `
62
- ${this._renderMode(value)} ${this._renderPlacement(value)} ${this._renderBookmarks(value)}
34
+ ${this._renderMode(value)} ${this._renderPlacement(value)}
63
35
  ${this._renderRenderer(value)} ${this._renderSky(value)}
64
36
  ${this._renderHemisphereLight(value)} ${this._renderKeyLight(value)} ${this._renderLightingPresets()}
65
37
  ${this._renderFloor(value)}
@@ -120,82 +92,6 @@ export class PropertyScene3D extends AbstractProperty {
120
92
  </fieldset>
121
93
  `;
122
94
  }
123
- _renderBookmarks(value) {
124
- const bookmarks = value.cameraBookmarks || [];
125
- return html `
126
- <fieldset>
127
- <legend>Bookmarks</legend>
128
- <div class="property-grid">
129
- <div class="bookmark-slots">
130
- ${[1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => {
131
- const filled = !!bookmarks[i];
132
- return html `
133
- <div
134
- class="bookmark-slot ${filled ? 'filled' : ''}"
135
- title=${filled ? 'Click: Go / Long-click: Overwrite / Right-click: Delete' : 'Long-click: Save current camera'}
136
- @click=${(e) => { if (filled) {
137
- this._bookmarkAction('navigate', i);
138
- const t = e.currentTarget;
139
- t.classList.add('saved');
140
- setTimeout(() => t.classList.remove('saved'), 300);
141
- } }}
142
- @contextmenu=${(e) => { e.preventDefault(); if (filled)
143
- this._bookmarkAction('clear', i); }}
144
- @mousedown=${(e) => this._onBookmarkMouseDown(e, i)}
145
- @mouseup=${() => this._onBookmarkMouseUp()}
146
- @mouseleave=${() => this._onBookmarkMouseUp()}
147
- >${i}</div>
148
- `;
149
- })}
150
- </div>
151
- </div>
152
- </fieldset>
153
- `;
154
- }
155
- _onBookmarkMouseDown(e, index) {
156
- if (e.button !== 0)
157
- return;
158
- const target = e.currentTarget;
159
- this._pressTarget = target;
160
- // 롱클릭 시작: filling 애니메이션
161
- target.classList.add('saving');
162
- this._longPressTimer = setTimeout(() => {
163
- this._longPressTimer = undefined;
164
- target.classList.remove('saving');
165
- this._bookmarkAction('save', index);
166
- // 저장 완료 피드백
167
- target.classList.add('saved');
168
- setTimeout(() => target.classList.remove('saved'), 300);
169
- }, 600);
170
- }
171
- _onBookmarkMouseUp() {
172
- if (this._longPressTimer) {
173
- clearTimeout(this._longPressTimer);
174
- this._longPressTimer = undefined;
175
- }
176
- if (this._pressTarget) {
177
- this._pressTarget.classList.remove('saving');
178
- this._pressTarget = undefined;
179
- }
180
- }
181
- _bookmarkAction(action, index) {
182
- var _a;
183
- const root = (_a = this.scene) === null || _a === void 0 ? void 0 : _a.root;
184
- const cap = root === null || root === void 0 ? void 0 : root._threeCapability;
185
- if (!cap)
186
- return;
187
- if (action === 'save') {
188
- cap.saveCameraToSlot(index);
189
- }
190
- else if (action === 'navigate') {
191
- cap.animateToSlot(index);
192
- }
193
- else if (action === 'clear') {
194
- cap.bookmarkManager.clearSlot(index);
195
- root.set('cameraBookmarks', cap.bookmarkManager.exportSlots());
196
- }
197
- this.requestUpdate();
198
- }
199
95
  _renderRenderer(value) {
200
96
  return html `
201
97
  <fieldset>
@@ -253,6 +149,12 @@ export class PropertyScene3D extends AbstractProperty {
253
149
  <fieldset>
254
150
  <legend>Hemisphere Light</legend>
255
151
  <div class="property-grid">
152
+ <label>Sky</label>
153
+ <ox-input-color value-key="hemiSkyColor" .value=${value.hemiSkyColor || '#ffffff'}></ox-input-color>
154
+
155
+ <label>Ground</label>
156
+ <ox-input-color value-key="hemiGroundColor" .value=${value.hemiGroundColor || '#444444'}></ox-input-color>
157
+
256
158
  <label>Intensity</label>
257
159
  <div class="range-with-value">
258
160
  <input
@@ -270,7 +172,9 @@ export class PropertyScene3D extends AbstractProperty {
270
172
  `;
271
173
  }
272
174
  _renderKeyLight(value) {
273
- var _a, _b;
175
+ var _a, _b, _c, _d, _e, _f, _g, _h;
176
+ const followCamera = value.dirLightFollowCamera !== false;
177
+ const shadowEnabled = value.dirShadowEnabled !== false;
274
178
  return html `
275
179
  <fieldset>
276
180
  <legend>Key Light</legend>
@@ -298,6 +202,69 @@ export class PropertyScene3D extends AbstractProperty {
298
202
  />
299
203
  <span>${((_b = value.dirLightIntensity) !== null && _b !== void 0 ? _b : 0.5).toFixed(1)}</span>
300
204
  </div>
205
+
206
+ <input
207
+ id="cb-dirlight-follow"
208
+ type="checkbox"
209
+ value-key="dirLightFollowCamera"
210
+ .checked=${followCamera}
211
+ />
212
+ <label for="cb-dirlight-follow">Follow Camera</label>
213
+
214
+ ${!followCamera
215
+ ? html `
216
+ <label>Azimuth</label>
217
+ <div class="range-with-value">
218
+ <input
219
+ type="range"
220
+ min="0"
221
+ max="360"
222
+ step="5"
223
+ value-key="dirLightAzimuth"
224
+ .value=${String((_c = value.dirLightAzimuth) !== null && _c !== void 0 ? _c : 30)}
225
+ />
226
+ <span>${Math.round(((_d = value.dirLightAzimuth) !== null && _d !== void 0 ? _d : 30))}°</span>
227
+ </div>
228
+
229
+ <label>Elevation</label>
230
+ <div class="range-with-value">
231
+ <input
232
+ type="range"
233
+ min="0"
234
+ max="90"
235
+ step="5"
236
+ value-key="dirLightElevation"
237
+ .value=${String((_e = value.dirLightElevation) !== null && _e !== void 0 ? _e : 45)}
238
+ />
239
+ <span>${Math.round(((_f = value.dirLightElevation) !== null && _f !== void 0 ? _f : 45))}°</span>
240
+ </div>
241
+ `
242
+ : ''}
243
+
244
+ <input
245
+ id="cb-dirlight-shadow"
246
+ type="checkbox"
247
+ value-key="dirShadowEnabled"
248
+ .checked=${shadowEnabled}
249
+ />
250
+ <label for="cb-dirlight-shadow">Cast Shadow</label>
251
+
252
+ ${shadowEnabled
253
+ ? html `
254
+ <label>Shadow Bias</label>
255
+ <div class="range-with-value">
256
+ <input
257
+ type="range"
258
+ min="-0.01"
259
+ max="0.01"
260
+ step="0.0001"
261
+ value-key="dirShadowBias"
262
+ .value=${String((_g = value.dirShadowBias) !== null && _g !== void 0 ? _g : -0.0005)}
263
+ />
264
+ <span>${((_h = value.dirShadowBias) !== null && _h !== void 0 ? _h : -0.0005).toFixed(4)}</span>
265
+ </div>
266
+ `
267
+ : ''}
301
268
  </div>
302
269
  </fieldset>
303
270
  `;
@@ -307,13 +274,26 @@ export class PropertyScene3D extends AbstractProperty {
307
274
  <fieldset>
308
275
  <legend>Lighting Presets</legend>
309
276
  <div class="property-grid">
310
- <div class="preset-buttons">
311
- ${Object.entries(LIGHTING_PRESETS).map(([name, values]) => html `<button @click=${() => this._applyLightingPreset(values)}>${name}</button>`)}
312
- </div>
277
+ <label>Preset</label>
278
+ <select @change=${this._onPresetChange}>
279
+ <option value="" selected>Select...</option>
280
+ ${Object.keys(LIGHTING_PRESETS).map(name => html `<option value=${name}>${name}</option>`)}
281
+ </select>
313
282
  </div>
314
283
  </fieldset>
315
284
  `;
316
285
  }
286
+ _onPresetChange(e) {
287
+ const select = e.target;
288
+ const name = select.value;
289
+ if (!name)
290
+ return;
291
+ const preset = LIGHTING_PRESETS[name];
292
+ if (preset)
293
+ this._applyLightingPreset(preset);
294
+ // 동일 preset을 다시 선택할 수 있도록 초기값으로 리셋
295
+ select.value = '';
296
+ }
317
297
  _renderFloor(value) {
318
298
  return html `
319
299
  <property-material3d
@@ -343,30 +323,6 @@ export class PropertyScene3D extends AbstractProperty {
343
323
  PropertyScene3D.styles = [
344
324
  PropertyGridStyles,
345
325
  css `
346
- .preset-buttons {
347
- grid-column: 1 / -1;
348
- display: flex;
349
- flex-wrap: wrap;
350
- gap: 4px;
351
- padding: 2px 0;
352
- }
353
-
354
- .preset-buttons button {
355
- flex: 1;
356
- min-width: 60px;
357
- padding: 4px 6px;
358
- border: 1px solid var(--md-sys-color-outline, #ccc);
359
- border-radius: 4px;
360
- background: var(--md-sys-color-surface, #fff);
361
- color: var(--md-sys-color-on-surface, #333);
362
- font-size: 11px;
363
- cursor: pointer;
364
- }
365
-
366
- .preset-buttons button:hover {
367
- background: var(--md-sys-color-primary-container, #e0e0e0);
368
- }
369
-
370
326
  .range-with-value {
371
327
  grid-column: 9 / -1;
372
328
  display: flex;
@@ -388,63 +344,6 @@ PropertyScene3D.styles = [
388
344
  color: var(--md-sys-color-on-secondary-container);
389
345
  }
390
346
 
391
- .bookmark-slots {
392
- grid-column: 1 / -1;
393
- display: grid;
394
- grid-template-columns: repeat(9, 1fr);
395
- gap: 2px;
396
- padding: 4px 0 0;
397
- }
398
-
399
- .bookmark-slot {
400
- display: flex;
401
- align-items: center;
402
- justify-content: center;
403
- height: 24px;
404
- border-radius: 3px;
405
- font-size: 11px;
406
- font-weight: 600;
407
- cursor: pointer;
408
- color: var(--md-sys-color-outline, #999);
409
- background: var(--md-sys-color-surface-variant, #f0f0f0);
410
- border: 1px solid transparent;
411
- user-select: none;
412
- transition: all 0.12s ease;
413
- }
414
-
415
- .bookmark-slot:hover {
416
- background: var(--md-sys-color-secondary-container, #e0e0e0);
417
- }
418
-
419
- .bookmark-slot.filled {
420
- color: var(--md-sys-color-primary, #6750A4);
421
- background: var(--md-sys-color-primary-container, #EADDFF);
422
- border-color: var(--md-sys-color-primary, #6750A4);
423
- font-weight: 700;
424
- }
425
-
426
- .bookmark-slot:active {
427
- transform: scale(0.9);
428
- }
429
-
430
- .bookmark-slot.saving {
431
- animation: bookmark-fill 0.6s linear forwards;
432
- }
433
-
434
- @keyframes bookmark-fill {
435
- from { box-shadow: inset 24px 0 0 0 transparent; }
436
- to { box-shadow: inset 24px 0 0 0 var(--md-sys-color-primary-container, #EADDFF); }
437
- }
438
-
439
- .bookmark-slot.saved {
440
- animation: bookmark-saved 0.3s ease;
441
- }
442
-
443
- @keyframes bookmark-saved {
444
- 0% { transform: scale(1); }
445
- 50% { transform: scale(1.2); }
446
- 100% { transform: scale(1); }
447
- }
448
347
  `
449
348
  ];
450
349
  __decorate([
@@ -1 +1 @@
1
- {"version":3,"file":"property-scene3d.js","sourceRoot":"","sources":["../../../../src/property-panel/threed/property-scene3d.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @license Copyright © HatioLab Inc. All rights reserved.\n */\n\nimport '@operato/input/ox-input-color.js'\nimport '@operato/i18n/ox-i18n.js'\n\nimport './property-material3d.js'\n\nimport { css, html } from 'lit'\nimport { property } from 'lit/decorators.js'\n\nimport { Properties, Scene } from '@hatiolab/things-scene'\nimport { PropertyGridStyles } from '@operato/styles/property-grid-styles.js'\n\nimport { AbstractProperty } from '../abstract-property.js'\n\nconst LIGHTING_PRESETS: Record<string, Record<string, unknown>> = {\n Neutral: {\n hemiIntensity: 0.6,\n dirLightEnabled: true,\n dirLightColor: '#ffffff',\n dirLightIntensity: 0.5\n },\n Studio: {\n hemiIntensity: 0.5,\n dirLightEnabled: true,\n dirLightColor: '#ffffff',\n dirLightIntensity: 0.6\n },\n Bright: {\n hemiIntensity: 0.8,\n dirLightEnabled: true,\n dirLightColor: '#ffffff',\n dirLightIntensity: 0.7\n },\n Warm: {\n hemiIntensity: 0.5,\n dirLightEnabled: true,\n dirLightColor: '#ffcc88',\n dirLightIntensity: 0.4\n },\n Cool: {\n hemiIntensity: 0.5,\n dirLightEnabled: true,\n dirLightColor: '#cce0ff',\n dirLightIntensity: 0.4\n },\n Flat: {\n hemiIntensity: 0.8,\n dirLightEnabled: false,\n dirLightColor: '#ffffff',\n dirLightIntensity: 0\n }\n}\n\n/**\n * Scene-level 3D settings for model-layer.\n * Includes 3D mode, camera, renderer, lighting, presets, and floor configuration.\n */\nexport class PropertyScene3D extends AbstractProperty {\n static styles = [\n PropertyGridStyles,\n css`\n .preset-buttons {\n grid-column: 1 / -1;\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n padding: 2px 0;\n }\n\n .preset-buttons button {\n flex: 1;\n min-width: 60px;\n padding: 4px 6px;\n border: 1px solid var(--md-sys-color-outline, #ccc);\n border-radius: 4px;\n background: var(--md-sys-color-surface, #fff);\n color: var(--md-sys-color-on-surface, #333);\n font-size: 11px;\n cursor: pointer;\n }\n\n .preset-buttons button:hover {\n background: var(--md-sys-color-primary-container, #e0e0e0);\n }\n\n .range-with-value {\n grid-column: 9 / -1;\n display: flex;\n align-items: center;\n gap: 4px;\n }\n\n .range-with-value input[type='range'] {\n flex: 1;\n border: none;\n background: transparent;\n padding: 0;\n }\n\n .range-with-value span {\n min-width: 2.5em;\n text-align: right;\n font-size: 11px;\n color: var(--md-sys-color-on-secondary-container);\n }\n\n .bookmark-slots {\n grid-column: 1 / -1;\n display: grid;\n grid-template-columns: repeat(9, 1fr);\n gap: 2px;\n padding: 4px 0 0;\n }\n\n .bookmark-slot {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 24px;\n border-radius: 3px;\n font-size: 11px;\n font-weight: 600;\n cursor: pointer;\n color: var(--md-sys-color-outline, #999);\n background: var(--md-sys-color-surface-variant, #f0f0f0);\n border: 1px solid transparent;\n user-select: none;\n transition: all 0.12s ease;\n }\n\n .bookmark-slot:hover {\n background: var(--md-sys-color-secondary-container, #e0e0e0);\n }\n\n .bookmark-slot.filled {\n color: var(--md-sys-color-primary, #6750A4);\n background: var(--md-sys-color-primary-container, #EADDFF);\n border-color: var(--md-sys-color-primary, #6750A4);\n font-weight: 700;\n }\n\n .bookmark-slot:active {\n transform: scale(0.9);\n }\n\n .bookmark-slot.saving {\n animation: bookmark-fill 0.6s linear forwards;\n }\n\n @keyframes bookmark-fill {\n from { box-shadow: inset 24px 0 0 0 transparent; }\n to { box-shadow: inset 24px 0 0 0 var(--md-sys-color-primary-container, #EADDFF); }\n }\n\n .bookmark-slot.saved {\n animation: bookmark-saved 0.3s ease;\n }\n\n @keyframes bookmark-saved {\n 0% { transform: scale(1); }\n 50% { transform: scale(1.2); }\n 100% { transform: scale(1); }\n }\n `\n ]\n\n @property({ type: Object }) value?: Properties\n @property({ type: Object }) scene: Scene | null = null\n\n render() {\n const value = this.value || {}\n\n return html`\n ${this._renderMode(value)} ${this._renderPlacement(value)} ${this._renderBookmarks(value)}\n ${this._renderRenderer(value)} ${this._renderSky(value)}\n ${this._renderHemisphereLight(value)} ${this._renderKeyLight(value)} ${this._renderLightingPresets()}\n ${this._renderFloor(value)}\n `\n }\n\n /**\n * Scene placement 모드 선택 — zPos 해석과 볼륨 origin 방향을 씬 전체에 일관 적용.\n * floor : zPos=바닥 높이, 볼륨이 위로 쌓임 (공장/창고/건축)\n * space : zPos=볼륨 중심 Y, 바닥 개념 없음 (우주/추상/전시)\n * inverted: zPos=천장, 볼륨이 아래로 매달림 (천장 설비, 베타)\n */\n private _renderPlacement(value: Properties) {\n const options = [\n { value: 'floor', label: 'Floor (default)' },\n { value: 'space', label: 'Space' },\n { value: 'inverted', label: 'Inverted (beta)' }\n ]\n const current = (value.placement as string) || 'floor'\n\n return html`\n <fieldset>\n <legend>Placement</legend>\n <div class=\"property-grid\">\n <label>Mode</label>\n <select value-key=\"placement\">\n ${options.map(\n o => html`<option value=${o.value} ?selected=${current === o.value}>${o.label}</option>`\n )}\n </select>\n </div>\n </fieldset>\n `\n }\n\n private _renderMode(value: Properties) {\n return html`\n <fieldset>\n <legend>3D Mode</legend>\n <div class=\"property-grid\">\n <input id=\"cb-threed\" type=\"checkbox\" value-key=\"threed\" .checked=${!!value.threed} />\n <label for=\"cb-threed\"><ox-i18n msgid=\"label.start-in-3d\">Start in 3D</ox-i18n></label>\n\n <label>Viewer Auto</label>\n <select value-key=\"cameraAutoPlay\">\n ${[\n { value: '', label: 'None' },\n { value: 'orbit', label: 'Orbit' },\n { value: 'play', label: 'Play Bookmarks' }\n ].map(\n o =>\n html`<option value=${o.value} ?selected=${(value.cameraAutoPlay || '') === o.value}>\n ${o.label}\n </option>`\n )}\n </select>\n\n <input\n id=\"cb-floor-constraint\"\n type=\"checkbox\"\n value-key=\"cameraFloorConstraint\"\n .checked=${!!value.cameraFloorConstraint}\n />\n <label for=\"cb-floor-constraint\"><ox-i18n msgid=\"label.keep-above-ground\">Keep Above Ground</ox-i18n></label>\n </div>\n </fieldset>\n `\n }\n\n private _renderBookmarks(value: Properties) {\n const bookmarks: (any | null)[] = value.cameraBookmarks || []\n\n return html`\n <fieldset>\n <legend>Bookmarks</legend>\n <div class=\"property-grid\">\n <div class=\"bookmark-slots\">\n ${[1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => {\n const filled = !!bookmarks[i]\n return html`\n <div\n class=\"bookmark-slot ${filled ? 'filled' : ''}\"\n title=${filled ? 'Click: Go / Long-click: Overwrite / Right-click: Delete' : 'Long-click: Save current camera'}\n @click=${(e: MouseEvent) => { if (filled) { this._bookmarkAction('navigate', i); const t = e.currentTarget as HTMLElement; t.classList.add('saved'); setTimeout(() => t.classList.remove('saved'), 300) } }}\n @contextmenu=${(e: Event) => { e.preventDefault(); if (filled) this._bookmarkAction('clear', i) }}\n @mousedown=${(e: MouseEvent) => this._onBookmarkMouseDown(e, i)}\n @mouseup=${() => this._onBookmarkMouseUp()}\n @mouseleave=${() => this._onBookmarkMouseUp()}\n >${i}</div>\n `\n })}\n </div>\n </div>\n </fieldset>\n `\n }\n\n private _longPressTimer?: ReturnType<typeof setTimeout>\n private _pressTarget?: HTMLElement\n\n private _onBookmarkMouseDown(e: MouseEvent, index: number) {\n if (e.button !== 0) return\n const target = e.currentTarget as HTMLElement\n this._pressTarget = target\n\n // 롱클릭 시작: filling 애니메이션\n target.classList.add('saving')\n\n this._longPressTimer = setTimeout(() => {\n this._longPressTimer = undefined\n target.classList.remove('saving')\n this._bookmarkAction('save', index)\n\n // 저장 완료 피드백\n target.classList.add('saved')\n setTimeout(() => target.classList.remove('saved'), 300)\n }, 600)\n }\n\n private _onBookmarkMouseUp() {\n if (this._longPressTimer) {\n clearTimeout(this._longPressTimer)\n this._longPressTimer = undefined\n }\n if (this._pressTarget) {\n this._pressTarget.classList.remove('saving')\n this._pressTarget = undefined\n }\n }\n\n private _bookmarkAction(action: 'save' | 'navigate' | 'clear', index: number) {\n const root = this.scene?.root as any\n const cap = root?._threeCapability\n if (!cap) return\n\n if (action === 'save') {\n cap.saveCameraToSlot(index)\n } else if (action === 'navigate') {\n cap.animateToSlot(index)\n } else if (action === 'clear') {\n cap.bookmarkManager.clearSlot(index)\n root.set('cameraBookmarks', cap.bookmarkManager.exportSlots())\n }\n\n this.requestUpdate()\n }\n\n private _renderRenderer(value: Properties) {\n return html`\n <fieldset>\n <legend>Renderer</legend>\n <div class=\"property-grid\">\n <input id=\"cb-antialias\" type=\"checkbox\" value-key=\"antialias\" .checked=${value.antialias !== false} />\n <label for=\"cb-antialias\">Anti-alias</label>\n\n <label>Precision</label>\n <select value-key=\"precision\">\n ${[\n { value: 'highp', label: 'High' },\n { value: 'mediump', label: 'Medium' },\n { value: 'lowp', label: 'Low' }\n ].map(\n o =>\n html`<option value=${o.value} ?selected=${(value.precision || 'highp') === o.value}>\n ${o.label}\n </option>`\n )}\n </select>\n </div>\n </fieldset>\n `\n }\n\n private _renderSky(value: Properties) {\n const skyOptions = [\n { value: '', label: 'None' },\n { value: 'color', label: 'Color' },\n { value: 'studio', label: 'Studio' },\n { value: 'warehouse', label: 'Warehouse' },\n { value: 'factory', label: 'Factory' },\n { value: 'office', label: 'Office' },\n { value: 'home', label: 'Home' },\n ]\n\n return html`\n <fieldset>\n <legend>Sky</legend>\n <div class=\"property-grid\">\n <label>Preset</label>\n <select value-key=\"sky\">\n ${skyOptions.map(\n o => html`<option value=${o.value} ?selected=${(value.sky || '') === o.value}>${o.label}</option>`\n )}\n </select>\n\n ${value.sky === 'color'\n ? html`\n <label>Color</label>\n <ox-input-color value-key=\"skyColor\" .value=${value.skyColor || '#87ceeb'}> </ox-input-color>\n `\n : ''}\n </div>\n </fieldset>\n `\n }\n\n private _renderHemisphereLight(value: Properties) {\n return html`\n <fieldset>\n <legend>Hemisphere Light</legend>\n <div class=\"property-grid\">\n <label>Intensity</label>\n <div class=\"range-with-value\">\n <input\n type=\"range\"\n min=\"0\"\n max=\"3\"\n step=\"0.1\"\n value-key=\"hemiIntensity\"\n .value=${String(value.hemiIntensity ?? 0.6)}\n />\n <span>${(value.hemiIntensity ?? 0.6).toFixed(1)}</span>\n </div>\n </div>\n </fieldset>\n `\n }\n\n private _renderKeyLight(value: Properties) {\n return html`\n <fieldset>\n <legend>Key Light</legend>\n <div class=\"property-grid\">\n <input\n id=\"cb-dirlight\"\n type=\"checkbox\"\n value-key=\"dirLightEnabled\"\n .checked=${value.dirLightEnabled !== false}\n />\n <label for=\"cb-dirlight\">Enabled</label>\n\n <label>Color</label>\n <ox-input-color value-key=\"dirLightColor\" .value=${value.dirLightColor || '#ffffff'}> </ox-input-color>\n\n <label>Intensity</label>\n <div class=\"range-with-value\">\n <input\n type=\"range\"\n min=\"0\"\n max=\"3\"\n step=\"0.1\"\n value-key=\"dirLightIntensity\"\n .value=${String(value.dirLightIntensity ?? 0.5)}\n />\n <span>${(value.dirLightIntensity ?? 0.5).toFixed(1)}</span>\n </div>\n </div>\n </fieldset>\n `\n }\n\n private _renderLightingPresets() {\n return html`\n <fieldset>\n <legend>Lighting Presets</legend>\n <div class=\"property-grid\">\n <div class=\"preset-buttons\">\n ${Object.entries(LIGHTING_PRESETS).map(\n ([name, values]) =>\n html`<button @click=${() => this._applyLightingPreset(values)}>${name}</button>`\n )}\n </div>\n </div>\n </fieldset>\n `\n }\n\n private _renderFloor(value: Properties) {\n return html`\n <property-material3d\n legend=\"Floor Material\"\n .value=${value.floorMaterial3d || { receiveShadow: true }}\n @property-change=${this._onFloorMaterialChange}\n >\n </property-material3d>\n `\n }\n\n private _applyLightingPreset(values: Record<string, unknown>) {\n this.dispatchEvent(\n new CustomEvent('property-change', {\n bubbles: true,\n composed: true,\n detail: values\n })\n )\n }\n\n private _onFloorMaterialChange(e: CustomEvent) {\n e.stopPropagation()\n this.dispatchEvent(\n new CustomEvent('property-change', {\n bubbles: true,\n composed: true,\n detail: { floorMaterial3d: e.detail.material3d }\n })\n )\n }\n\n}\n"]}
1
+ {"version":3,"file":"property-scene3d.js","sourceRoot":"","sources":["../../../../src/property-panel/threed/property-scene3d.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @license Copyright © HatioLab Inc. All rights reserved.\n */\n\nimport '@operato/input/ox-input-color.js'\nimport '@operato/i18n/ox-i18n.js'\n\nimport './property-material3d.js'\n\nimport { css, html } from 'lit'\nimport { property } from 'lit/decorators.js'\n\nimport { Properties, Scene } from '@hatiolab/things-scene'\nimport { PropertyGridStyles } from '@operato/styles/property-grid-styles.js'\n\nimport { AbstractProperty } from '../abstract-property.js'\n\nconst LIGHTING_PRESETS: Record<string, Record<string, unknown>> = {\n Neutral: { hemiIntensity: 1.0, dirLightEnabled: true, dirLightColor: '#ffffff', dirLightIntensity: 1.0 },\n Studio: { hemiIntensity: 0.6, dirLightEnabled: true, dirLightColor: '#ffffff', dirLightIntensity: 1.8 },\n Bright: { hemiIntensity: 2.0, dirLightEnabled: true, dirLightColor: '#ffffff', dirLightIntensity: 2.0 },\n Dim: { hemiIntensity: 0.2, dirLightEnabled: true, dirLightColor: '#fff5e0', dirLightIntensity: 0.5 },\n Warm: { hemiIntensity: 0.8, dirLightEnabled: true, dirLightColor: '#ffaa55', dirLightIntensity: 1.5 },\n Cool: { hemiIntensity: 0.8, dirLightEnabled: true, dirLightColor: '#88bbff', dirLightIntensity: 1.5 },\n Dramatic: { hemiIntensity: 0.15, dirLightEnabled: true, dirLightColor: '#ffffff', dirLightIntensity: 2.5 },\n Flat: { hemiIntensity: 2.0, dirLightEnabled: false, dirLightColor: '#ffffff', dirLightIntensity: 0 }\n}\n\n/**\n * Scene-level 3D settings for model-layer.\n * Includes 3D mode, camera, renderer, lighting, presets, and floor configuration.\n */\nexport class PropertyScene3D extends AbstractProperty {\n static styles = [\n PropertyGridStyles,\n css`\n .range-with-value {\n grid-column: 9 / -1;\n display: flex;\n align-items: center;\n gap: 4px;\n }\n\n .range-with-value input[type='range'] {\n flex: 1;\n border: none;\n background: transparent;\n padding: 0;\n }\n\n .range-with-value span {\n min-width: 2.5em;\n text-align: right;\n font-size: 11px;\n color: var(--md-sys-color-on-secondary-container);\n }\n\n `\n ]\n\n @property({ type: Object }) value?: Properties\n @property({ type: Object }) scene: Scene | null = null\n\n render() {\n const value = this.value || {}\n\n return html`\n ${this._renderMode(value)} ${this._renderPlacement(value)}\n ${this._renderRenderer(value)} ${this._renderSky(value)}\n ${this._renderHemisphereLight(value)} ${this._renderKeyLight(value)} ${this._renderLightingPresets()}\n ${this._renderFloor(value)}\n `\n }\n\n /**\n * Scene placement 모드 선택 — zPos 해석과 볼륨 origin 방향을 씬 전체에 일관 적용.\n * floor : zPos=바닥 높이, 볼륨이 위로 쌓임 (공장/창고/건축)\n * space : zPos=볼륨 중심 Y, 바닥 개념 없음 (우주/추상/전시)\n * inverted: zPos=천장, 볼륨이 아래로 매달림 (천장 설비, 베타)\n */\n private _renderPlacement(value: Properties) {\n const options = [\n { value: 'floor', label: 'Floor (default)' },\n { value: 'space', label: 'Space' },\n { value: 'inverted', label: 'Inverted (beta)' }\n ]\n const current = (value.placement as string) || 'floor'\n\n return html`\n <fieldset>\n <legend>Placement</legend>\n <div class=\"property-grid\">\n <label>Mode</label>\n <select value-key=\"placement\">\n ${options.map(\n o => html`<option value=${o.value} ?selected=${current === o.value}>${o.label}</option>`\n )}\n </select>\n </div>\n </fieldset>\n `\n }\n\n private _renderMode(value: Properties) {\n return html`\n <fieldset>\n <legend>3D Mode</legend>\n <div class=\"property-grid\">\n <input id=\"cb-threed\" type=\"checkbox\" value-key=\"threed\" .checked=${!!value.threed} />\n <label for=\"cb-threed\"><ox-i18n msgid=\"label.start-in-3d\">Start in 3D</ox-i18n></label>\n\n <label>Viewer Auto</label>\n <select value-key=\"cameraAutoPlay\">\n ${[\n { value: '', label: 'None' },\n { value: 'orbit', label: 'Orbit' },\n { value: 'play', label: 'Play Bookmarks' }\n ].map(\n o =>\n html`<option value=${o.value} ?selected=${(value.cameraAutoPlay || '') === o.value}>\n ${o.label}\n </option>`\n )}\n </select>\n\n <input\n id=\"cb-floor-constraint\"\n type=\"checkbox\"\n value-key=\"cameraFloorConstraint\"\n .checked=${!!value.cameraFloorConstraint}\n />\n <label for=\"cb-floor-constraint\"><ox-i18n msgid=\"label.keep-above-ground\">Keep Above Ground</ox-i18n></label>\n </div>\n </fieldset>\n `\n }\n\n private _renderRenderer(value: Properties) {\n return html`\n <fieldset>\n <legend>Renderer</legend>\n <div class=\"property-grid\">\n <input id=\"cb-antialias\" type=\"checkbox\" value-key=\"antialias\" .checked=${value.antialias !== false} />\n <label for=\"cb-antialias\">Anti-alias</label>\n\n <label>Precision</label>\n <select value-key=\"precision\">\n ${[\n { value: 'highp', label: 'High' },\n { value: 'mediump', label: 'Medium' },\n { value: 'lowp', label: 'Low' }\n ].map(\n o =>\n html`<option value=${o.value} ?selected=${(value.precision || 'highp') === o.value}>\n ${o.label}\n </option>`\n )}\n </select>\n </div>\n </fieldset>\n `\n }\n\n private _renderSky(value: Properties) {\n const skyOptions = [\n { value: '', label: 'None' },\n { value: 'color', label: 'Color' },\n { value: 'studio', label: 'Studio' },\n { value: 'warehouse', label: 'Warehouse' },\n { value: 'factory', label: 'Factory' },\n { value: 'office', label: 'Office' },\n { value: 'home', label: 'Home' },\n ]\n\n return html`\n <fieldset>\n <legend>Sky</legend>\n <div class=\"property-grid\">\n <label>Preset</label>\n <select value-key=\"sky\">\n ${skyOptions.map(\n o => html`<option value=${o.value} ?selected=${(value.sky || '') === o.value}>${o.label}</option>`\n )}\n </select>\n\n ${value.sky === 'color'\n ? html`\n <label>Color</label>\n <ox-input-color value-key=\"skyColor\" .value=${value.skyColor || '#87ceeb'}> </ox-input-color>\n `\n : ''}\n </div>\n </fieldset>\n `\n }\n\n private _renderHemisphereLight(value: Properties) {\n return html`\n <fieldset>\n <legend>Hemisphere Light</legend>\n <div class=\"property-grid\">\n <label>Sky</label>\n <ox-input-color value-key=\"hemiSkyColor\" .value=${value.hemiSkyColor || '#ffffff'}></ox-input-color>\n\n <label>Ground</label>\n <ox-input-color value-key=\"hemiGroundColor\" .value=${value.hemiGroundColor || '#444444'}></ox-input-color>\n\n <label>Intensity</label>\n <div class=\"range-with-value\">\n <input\n type=\"range\"\n min=\"0\"\n max=\"3\"\n step=\"0.1\"\n value-key=\"hemiIntensity\"\n .value=${String(value.hemiIntensity ?? 0.6)}\n />\n <span>${(value.hemiIntensity ?? 0.6).toFixed(1)}</span>\n </div>\n </div>\n </fieldset>\n `\n }\n\n private _renderKeyLight(value: Properties) {\n const followCamera = value.dirLightFollowCamera !== false\n const shadowEnabled = value.dirShadowEnabled !== false\n return html`\n <fieldset>\n <legend>Key Light</legend>\n <div class=\"property-grid\">\n <input\n id=\"cb-dirlight\"\n type=\"checkbox\"\n value-key=\"dirLightEnabled\"\n .checked=${value.dirLightEnabled !== false}\n />\n <label for=\"cb-dirlight\">Enabled</label>\n\n <label>Color</label>\n <ox-input-color value-key=\"dirLightColor\" .value=${value.dirLightColor || '#ffffff'}> </ox-input-color>\n\n <label>Intensity</label>\n <div class=\"range-with-value\">\n <input\n type=\"range\"\n min=\"0\"\n max=\"3\"\n step=\"0.1\"\n value-key=\"dirLightIntensity\"\n .value=${String(value.dirLightIntensity ?? 0.5)}\n />\n <span>${(value.dirLightIntensity ?? 0.5).toFixed(1)}</span>\n </div>\n\n <input\n id=\"cb-dirlight-follow\"\n type=\"checkbox\"\n value-key=\"dirLightFollowCamera\"\n .checked=${followCamera}\n />\n <label for=\"cb-dirlight-follow\">Follow Camera</label>\n\n ${!followCamera\n ? html`\n <label>Azimuth</label>\n <div class=\"range-with-value\">\n <input\n type=\"range\"\n min=\"0\"\n max=\"360\"\n step=\"5\"\n value-key=\"dirLightAzimuth\"\n .value=${String(value.dirLightAzimuth ?? 30)}\n />\n <span>${Math.round((value.dirLightAzimuth ?? 30) as number)}°</span>\n </div>\n\n <label>Elevation</label>\n <div class=\"range-with-value\">\n <input\n type=\"range\"\n min=\"0\"\n max=\"90\"\n step=\"5\"\n value-key=\"dirLightElevation\"\n .value=${String(value.dirLightElevation ?? 45)}\n />\n <span>${Math.round((value.dirLightElevation ?? 45) as number)}°</span>\n </div>\n `\n : ''}\n\n <input\n id=\"cb-dirlight-shadow\"\n type=\"checkbox\"\n value-key=\"dirShadowEnabled\"\n .checked=${shadowEnabled}\n />\n <label for=\"cb-dirlight-shadow\">Cast Shadow</label>\n\n ${shadowEnabled\n ? html`\n <label>Shadow Bias</label>\n <div class=\"range-with-value\">\n <input\n type=\"range\"\n min=\"-0.01\"\n max=\"0.01\"\n step=\"0.0001\"\n value-key=\"dirShadowBias\"\n .value=${String(value.dirShadowBias ?? -0.0005)}\n />\n <span>${(value.dirShadowBias ?? -0.0005).toFixed(4)}</span>\n </div>\n `\n : ''}\n </div>\n </fieldset>\n `\n }\n\n private _renderLightingPresets() {\n return html`\n <fieldset>\n <legend>Lighting Presets</legend>\n <div class=\"property-grid\">\n <label>Preset</label>\n <select @change=${this._onPresetChange}>\n <option value=\"\" selected>Select...</option>\n ${Object.keys(LIGHTING_PRESETS).map(\n name => html`<option value=${name}>${name}</option>`\n )}\n </select>\n </div>\n </fieldset>\n `\n }\n\n private _onPresetChange(e: Event) {\n const select = e.target as HTMLSelectElement\n const name = select.value\n if (!name) return\n const preset = LIGHTING_PRESETS[name]\n if (preset) this._applyLightingPreset(preset)\n // 동일 preset을 다시 선택할 수 있도록 초기값으로 리셋\n select.value = ''\n }\n\n private _renderFloor(value: Properties) {\n return html`\n <property-material3d\n legend=\"Floor Material\"\n .value=${value.floorMaterial3d || { receiveShadow: true }}\n @property-change=${this._onFloorMaterialChange}\n >\n </property-material3d>\n `\n }\n\n private _applyLightingPreset(values: Record<string, unknown>) {\n this.dispatchEvent(\n new CustomEvent('property-change', {\n bubbles: true,\n composed: true,\n detail: values\n })\n )\n }\n\n private _onFloorMaterialChange(e: CustomEvent) {\n e.stopPropagation()\n this.dispatchEvent(\n new CustomEvent('property-change', {\n bubbles: true,\n composed: true,\n detail: { floorMaterial3d: e.detail.material3d }\n })\n )\n }\n\n}\n"]}