@operato/property-panel 10.0.0-beta.5 → 10.0.0-beta.50
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 +318 -0
- package/dist/src/ox-property-panel.d.ts +7 -0
- package/dist/src/ox-property-panel.js +62 -34
- package/dist/src/ox-property-panel.js.map +1 -1
- package/dist/src/property-panel/abstract-property.js +11 -2
- package/dist/src/property-panel/abstract-property.js.map +1 -1
- package/dist/src/property-panel/data-binding/data-binding-mapper.js +11 -3
- package/dist/src/property-panel/data-binding/data-binding-mapper.js.map +1 -1
- package/dist/src/property-panel/data-binding/data-binding-popup.d.ts +63 -0
- package/dist/src/property-panel/data-binding/data-binding-popup.js +1414 -0
- package/dist/src/property-panel/data-binding/data-binding-popup.js.map +1 -0
- package/dist/src/property-panel/data-binding/data-binding.d.ts +1 -0
- package/dist/src/property-panel/data-binding/data-binding.js +33 -2
- package/dist/src/property-panel/data-binding/data-binding.js.map +1 -1
- package/dist/src/property-panel/effects/property-event-tap.js +21 -1
- package/dist/src/property-panel/effects/property-event-tap.js.map +1 -1
- package/dist/src/property-panel/effects/property-shadow.js +14 -5
- package/dist/src/property-panel/effects/property-shadow.js.map +1 -1
- package/dist/src/property-panel/shapes/ox-placeholder-convert-editor.d.ts +20 -0
- package/dist/src/property-panel/shapes/ox-placeholder-convert-editor.js +125 -0
- package/dist/src/property-panel/shapes/ox-placeholder-convert-editor.js.map +1 -0
- package/dist/src/property-panel/shapes/placeholder-convert.d.ts +34 -0
- package/dist/src/property-panel/shapes/placeholder-convert.js +117 -0
- package/dist/src/property-panel/shapes/placeholder-convert.js.map +1 -0
- package/dist/src/property-panel/shapes/shapes.js +28 -10
- package/dist/src/property-panel/shapes/shapes.js.map +1 -1
- package/dist/src/property-panel/specifics/specifics.d.ts +1 -0
- package/dist/src/property-panel/specifics/specifics.js +2 -1
- package/dist/src/property-panel/specifics/specifics.js.map +1 -1
- package/dist/src/property-panel/styles/styles.js +2 -0
- package/dist/src/property-panel/styles/styles.js.map +1 -1
- package/dist/src/property-panel/threed/property-material3d.js +8 -1
- package/dist/src/property-panel/threed/property-material3d.js.map +1 -1
- package/dist/src/property-panel/threed/property-scene3d.d.ts +34 -7
- package/dist/src/property-panel/threed/property-scene3d.js +329 -246
- package/dist/src/property-panel/threed/property-scene3d.js.map +1 -1
- package/dist/src/property-panel/threed/threed.js +8 -0
- package/dist/src/property-panel/threed/threed.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -5
- package/translations/en.json +27 -1
- package/translations/ja.json +27 -1
- package/translations/ko.json +27 -1
- package/translations/ms.json +27 -1
- package/translations/zh.json +27 -1
|
@@ -3,49 +3,74 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { __decorate } from "tslib";
|
|
5
5
|
import '@operato/input/ox-input-color.js';
|
|
6
|
+
import '@operato/i18n/ox-i18n.js';
|
|
6
7
|
import './property-material3d.js';
|
|
7
8
|
import { css, html } from 'lit';
|
|
8
9
|
import { property } from 'lit/decorators.js';
|
|
9
10
|
import { PropertyGridStyles } from '@operato/styles/property-grid-styles.js';
|
|
10
11
|
import { AbstractProperty } from '../abstract-property.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Environment별 기본 조명 프로파일.
|
|
14
|
+
* Environment 드롭다운에서 선택 시 sky 값과 함께 이 조명 필드들이 자동 적용된다.
|
|
15
|
+
* Light Mood(아래)는 이 위에 얹는 오버라이드 역할.
|
|
16
|
+
*/
|
|
17
|
+
const ENVIRONMENT_DEFAULTS = {
|
|
18
|
+
// 실내
|
|
19
|
+
studio: {
|
|
20
|
+
hemiSkyColor: '#ffffff', hemiGroundColor: '#888888', hemiIntensity: 3.0,
|
|
21
|
+
dirLightEnabled: true, dirLightColor: '#ffffff', dirLightIntensity: 2.0,
|
|
22
|
+
dirLightFollowCamera: true
|
|
23
|
+
},
|
|
24
|
+
warehouse: {
|
|
25
|
+
hemiSkyColor: '#e0e4ea', hemiGroundColor: '#707880', hemiIntensity: 2.5,
|
|
26
|
+
dirLightEnabled: true, dirLightColor: '#f8fafc', dirLightIntensity: 1.8,
|
|
27
|
+
dirLightFollowCamera: true
|
|
28
|
+
},
|
|
29
|
+
factory: {
|
|
30
|
+
hemiSkyColor: '#d8dce2', hemiGroundColor: '#606870', hemiIntensity: 2.0,
|
|
31
|
+
dirLightEnabled: true, dirLightColor: '#e8eef2', dirLightIntensity: 2.0,
|
|
32
|
+
dirLightFollowCamera: true
|
|
17
33
|
},
|
|
18
|
-
|
|
19
|
-
hemiIntensity: 0
|
|
20
|
-
dirLightEnabled: true,
|
|
21
|
-
|
|
22
|
-
dirLightIntensity: 0.6
|
|
34
|
+
office: {
|
|
35
|
+
hemiSkyColor: '#ffffff', hemiGroundColor: '#aaaaaa', hemiIntensity: 3.0,
|
|
36
|
+
dirLightEnabled: true, dirLightColor: '#ffffff', dirLightIntensity: 1.5,
|
|
37
|
+
dirLightFollowCamera: true
|
|
23
38
|
},
|
|
24
|
-
|
|
25
|
-
hemiIntensity: 0
|
|
26
|
-
dirLightEnabled: true,
|
|
27
|
-
|
|
28
|
-
dirLightIntensity: 0.7
|
|
39
|
+
home: {
|
|
40
|
+
hemiSkyColor: '#ffe8c8', hemiGroundColor: '#7a5a40', hemiIntensity: 2.0,
|
|
41
|
+
dirLightEnabled: true, dirLightColor: '#ffd8a0', dirLightIntensity: 1.5,
|
|
42
|
+
dirLightFollowCamera: true
|
|
29
43
|
},
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
dirLightColor: '#
|
|
34
|
-
|
|
44
|
+
// 실외 (날씨)
|
|
45
|
+
sunny: {
|
|
46
|
+
hemiSkyColor: '#87ceeb', hemiGroundColor: '#8b7355', hemiIntensity: 2.0,
|
|
47
|
+
dirLightEnabled: true, dirLightColor: '#fff4cf', dirLightIntensity: 3.0,
|
|
48
|
+
dirLightFollowCamera: false, dirLightAzimuth: 135, dirLightElevation: 60
|
|
35
49
|
},
|
|
36
|
-
|
|
37
|
-
hemiIntensity: 0
|
|
38
|
-
dirLightEnabled: true,
|
|
39
|
-
|
|
40
|
-
dirLightIntensity: 0.4
|
|
50
|
+
cloudy: {
|
|
51
|
+
hemiSkyColor: '#c8cbd0', hemiGroundColor: '#9ba1a8', hemiIntensity: 3.0,
|
|
52
|
+
dirLightEnabled: true, dirLightColor: '#e8e8e8', dirLightIntensity: 0.6,
|
|
53
|
+
dirLightFollowCamera: false, dirLightAzimuth: 120, dirLightElevation: 50
|
|
41
54
|
},
|
|
42
|
-
|
|
43
|
-
hemiIntensity:
|
|
44
|
-
dirLightEnabled:
|
|
45
|
-
|
|
46
|
-
dirLightIntensity: 0
|
|
55
|
+
rainy: {
|
|
56
|
+
hemiSkyColor: '#8a909a', hemiGroundColor: '#4a4f55', hemiIntensity: 1.5,
|
|
57
|
+
dirLightEnabled: true, dirLightColor: '#b8c0c8', dirLightIntensity: 0.3,
|
|
58
|
+
dirLightFollowCamera: false, dirLightAzimuth: 110, dirLightElevation: 40
|
|
47
59
|
}
|
|
48
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* Light Mood 프리셋 — Environment 기본값 위에 얹는 분위기 오버라이드.
|
|
63
|
+
* Environment(지금 씬이 어디에 있는가)와 독립적으로 작동하며 sky는 건드리지 않는다.
|
|
64
|
+
* 원자 조명 필드(hemi/dir)만 세팅.
|
|
65
|
+
*/
|
|
66
|
+
const LIGHTING_PRESETS = {
|
|
67
|
+
Bright: { hemiIntensity: 3.0, dirLightEnabled: true, dirLightColor: '#ffffff', dirLightIntensity: 2.5 },
|
|
68
|
+
Dim: { hemiIntensity: 0.5, dirLightEnabled: true, dirLightColor: '#fff5e0', dirLightIntensity: 0.5 },
|
|
69
|
+
Warm: { hemiIntensity: 1.5, dirLightEnabled: true, dirLightColor: '#ffaa55', dirLightIntensity: 2.0 },
|
|
70
|
+
Cool: { hemiIntensity: 1.5, dirLightEnabled: true, dirLightColor: '#88bbff', dirLightIntensity: 2.0 },
|
|
71
|
+
Dramatic: { hemiIntensity: 0.3, dirLightEnabled: true, dirLightColor: '#ffffff', dirLightIntensity: 3.0 },
|
|
72
|
+
Flat: { hemiIntensity: 3.0, dirLightEnabled: false, dirLightColor: '#ffffff', dirLightIntensity: 0 }
|
|
73
|
+
};
|
|
49
74
|
/**
|
|
50
75
|
* Scene-level 3D settings for model-layer.
|
|
51
76
|
* Includes 3D mode, camera, renderer, lighting, presets, and floor configuration.
|
|
@@ -58,19 +83,112 @@ export class PropertyScene3D extends AbstractProperty {
|
|
|
58
83
|
render() {
|
|
59
84
|
const value = this.value || {};
|
|
60
85
|
return html `
|
|
61
|
-
${this._renderMode(value)} ${this.
|
|
62
|
-
${this.
|
|
86
|
+
${this._renderMode(value)} ${this._renderPlacement(value)}
|
|
87
|
+
${this._renderCamera(value)}
|
|
88
|
+
${this._renderRenderer(value)} ${this._renderSky(value)}
|
|
63
89
|
${this._renderHemisphereLight(value)} ${this._renderKeyLight(value)} ${this._renderLightingPresets()}
|
|
64
90
|
${this._renderFloor(value)}
|
|
65
91
|
`;
|
|
66
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Camera 초기 설정 — Initial View(뷰 프리셋), FOV/Near/Far, 현재 카메라 저장 버튼.
|
|
95
|
+
* 사용자가 씬을 3D로 돌려본 후 "이 각도가 마음에 든다" 싶으면
|
|
96
|
+
* Save Current View로 현재 viewport의 position을 모델에 기록 → 다음 로드 시 같은 각도로 시작.
|
|
97
|
+
*/
|
|
98
|
+
_renderCamera(value) {
|
|
99
|
+
var _a, _b, _c;
|
|
100
|
+
const views = [
|
|
101
|
+
{ value: 'perspective', label: 'Perspective' },
|
|
102
|
+
{ value: 'top', label: 'Top' },
|
|
103
|
+
{ value: 'front', label: 'Front' },
|
|
104
|
+
{ value: 'back', label: 'Back' },
|
|
105
|
+
{ value: 'right', label: 'Right' },
|
|
106
|
+
{ value: 'left', label: 'Left' }
|
|
107
|
+
];
|
|
108
|
+
const currentView = value.initialCameraView || 'perspective';
|
|
109
|
+
const fov = Number((_a = value.fov) !== null && _a !== void 0 ? _a : 45);
|
|
110
|
+
const near = Number((_b = value.near) !== null && _b !== void 0 ? _b : 0.1);
|
|
111
|
+
const far = Number((_c = value.far) !== null && _c !== void 0 ? _c : 20000);
|
|
112
|
+
return html `
|
|
113
|
+
<fieldset>
|
|
114
|
+
<legend>Camera</legend>
|
|
115
|
+
<div class="property-grid">
|
|
116
|
+
<label>Initial View</label>
|
|
117
|
+
<select value-key="initialCameraView">
|
|
118
|
+
${views.map(v => html `<option value=${v.value} ?selected=${currentView === v.value}>${v.label}</option>`)}
|
|
119
|
+
</select>
|
|
120
|
+
|
|
121
|
+
<label>FOV</label>
|
|
122
|
+
<input type="number" value-key="fov" step="1" min="1" max="170" .value=${String(fov)} />
|
|
123
|
+
|
|
124
|
+
<label>Near</label>
|
|
125
|
+
<input type="number" value-key="near" step="0.1" min="0.01" .value=${String(near)} />
|
|
126
|
+
|
|
127
|
+
<label>Far</label>
|
|
128
|
+
<input type="number" value-key="far" step="100" min="1" .value=${String(far)} />
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div class="camera-actions">
|
|
132
|
+
<button type="button" @click=${this._onSaveCameraView}>Save Current View</button>
|
|
133
|
+
</div>
|
|
134
|
+
</fieldset>
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* 현재 3D viewport의 카메라 상태를 모델에 저장한다.
|
|
139
|
+
* scene의 getCameraState를 호출하여 view/cameraX/cameraY/cameraZ를 얻어 property-change로 전파.
|
|
140
|
+
*/
|
|
141
|
+
_onSaveCameraView() {
|
|
142
|
+
var _a, _b, _c;
|
|
143
|
+
const cap = (_c = (_b = (_a = this.scene) === null || _a === void 0 ? void 0 : _a.rootContainer) === null || _b === void 0 ? void 0 : _b.model_layer) === null || _c === void 0 ? void 0 : _c._threeCapability;
|
|
144
|
+
if (!(cap === null || cap === void 0 ? void 0 : cap.getCameraState))
|
|
145
|
+
return;
|
|
146
|
+
const state = cap.getCameraState();
|
|
147
|
+
if (!state)
|
|
148
|
+
return;
|
|
149
|
+
this.dispatchEvent(new CustomEvent('property-change', {
|
|
150
|
+
bubbles: true,
|
|
151
|
+
composed: true,
|
|
152
|
+
detail: {
|
|
153
|
+
initialCameraView: state.view,
|
|
154
|
+
cameraX: state.cameraX,
|
|
155
|
+
cameraY: state.cameraY,
|
|
156
|
+
cameraZ: state.cameraZ
|
|
157
|
+
}
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Scene placement 모드 선택 — zPos 해석과 볼륨 origin 방향을 씬 전체에 일관 적용.
|
|
162
|
+
* floor : zPos=바닥 높이, 볼륨이 위로 쌓임 (공장/창고/건축)
|
|
163
|
+
* space : zPos=볼륨 중심 Y, 바닥 개념 없음 (우주/추상/전시)
|
|
164
|
+
* inverted: zPos=천장, 볼륨이 아래로 매달림 (천장 설비, 베타)
|
|
165
|
+
*/
|
|
166
|
+
_renderPlacement(value) {
|
|
167
|
+
const options = [
|
|
168
|
+
{ value: 'floor', label: 'Floor (default)' },
|
|
169
|
+
{ value: 'space', label: 'Space' },
|
|
170
|
+
{ value: 'inverted', label: 'Inverted (beta)' }
|
|
171
|
+
];
|
|
172
|
+
const current = value.placement || 'floor';
|
|
173
|
+
return html `
|
|
174
|
+
<fieldset>
|
|
175
|
+
<legend>Placement</legend>
|
|
176
|
+
<div class="property-grid">
|
|
177
|
+
<label>Mode</label>
|
|
178
|
+
<select value-key="placement">
|
|
179
|
+
${options.map(o => html `<option value=${o.value} ?selected=${current === o.value}>${o.label}</option>`)}
|
|
180
|
+
</select>
|
|
181
|
+
</div>
|
|
182
|
+
</fieldset>
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
67
185
|
_renderMode(value) {
|
|
68
186
|
return html `
|
|
69
187
|
<fieldset>
|
|
70
188
|
<legend>3D Mode</legend>
|
|
71
189
|
<div class="property-grid">
|
|
72
190
|
<input id="cb-threed" type="checkbox" value-key="threed" .checked=${!!value.threed} />
|
|
73
|
-
<label for="cb-threed">
|
|
191
|
+
<label for="cb-threed"><ox-i18n msgid="label.start-in-3d">Start in 3D</ox-i18n></label>
|
|
74
192
|
|
|
75
193
|
<label>Viewer Auto</label>
|
|
76
194
|
<select value-key="cameraAutoPlay">
|
|
@@ -82,135 +200,21 @@ export class PropertyScene3D extends AbstractProperty {
|
|
|
82
200
|
${o.label}
|
|
83
201
|
</option>`)}
|
|
84
202
|
</select>
|
|
85
|
-
</div>
|
|
86
|
-
</fieldset>
|
|
87
|
-
`;
|
|
88
|
-
}
|
|
89
|
-
_renderCamera(value) {
|
|
90
|
-
var _a, _b, _c, _d;
|
|
91
|
-
return html `
|
|
92
|
-
<fieldset>
|
|
93
|
-
<legend>Camera</legend>
|
|
94
|
-
<div class="property-grid">
|
|
95
|
-
<label>View</label>
|
|
96
|
-
<select value-key="initialCameraView">
|
|
97
|
-
${['perspective', 'top', 'front', 'back', 'right', 'left'].map(v => html `<option value=${v} ?selected=${(value.initialCameraView || 'perspective') === v}>
|
|
98
|
-
${v.charAt(0).toUpperCase() + v.slice(1)}
|
|
99
|
-
</option>`)}
|
|
100
|
-
</select>
|
|
101
203
|
|
|
102
|
-
<label class="half-label">FOV</label>
|
|
103
|
-
<input
|
|
104
|
-
class="half-editor"
|
|
105
|
-
type="number"
|
|
106
|
-
value-key="fov"
|
|
107
|
-
min="10"
|
|
108
|
-
max="120"
|
|
109
|
-
.value=${String((_a = value.fov) !== null && _a !== void 0 ? _a : 45)}
|
|
110
|
-
/>
|
|
111
|
-
<label class="half-label">Zoom</label>
|
|
112
204
|
<input
|
|
113
|
-
|
|
114
|
-
type="
|
|
115
|
-
value-key="
|
|
116
|
-
|
|
117
|
-
.value=${String((_b = value.zoom) !== null && _b !== void 0 ? _b : 100)}
|
|
118
|
-
/>
|
|
119
|
-
|
|
120
|
-
<label class="half-label">Near</label>
|
|
121
|
-
<input
|
|
122
|
-
class="half-editor"
|
|
123
|
-
type="number"
|
|
124
|
-
value-key="near"
|
|
125
|
-
min="0.01"
|
|
126
|
-
step="0.01"
|
|
127
|
-
.value=${String((_c = value.near) !== null && _c !== void 0 ? _c : 0.1)}
|
|
205
|
+
id="cb-floor-constraint"
|
|
206
|
+
type="checkbox"
|
|
207
|
+
value-key="cameraFloorConstraint"
|
|
208
|
+
.checked=${!!value.cameraFloorConstraint}
|
|
128
209
|
/>
|
|
129
|
-
<label
|
|
130
|
-
<input class="half-editor" type="number" value-key="far" .value=${String((_d = value.far) !== null && _d !== void 0 ? _d : 20000)} />
|
|
131
|
-
</div>
|
|
132
|
-
</fieldset>
|
|
133
|
-
|
|
134
|
-
${this._renderBookmarks(value)}
|
|
135
|
-
`;
|
|
136
|
-
}
|
|
137
|
-
_renderBookmarks(value) {
|
|
138
|
-
const bookmarks = value.cameraBookmarks || [];
|
|
139
|
-
return html `
|
|
140
|
-
<fieldset>
|
|
141
|
-
<legend>Bookmarks</legend>
|
|
142
|
-
<div class="property-grid">
|
|
143
|
-
<div class="bookmark-slots">
|
|
144
|
-
${[1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => {
|
|
145
|
-
const filled = !!bookmarks[i];
|
|
146
|
-
return html `
|
|
147
|
-
<div
|
|
148
|
-
class="bookmark-slot ${filled ? 'filled' : ''}"
|
|
149
|
-
title=${filled ? 'Click: Go / Long-click: Overwrite / Right-click: Delete' : 'Long-click: Save current camera'}
|
|
150
|
-
@click=${(e) => { if (filled) {
|
|
151
|
-
this._bookmarkAction('navigate', i);
|
|
152
|
-
const t = e.currentTarget;
|
|
153
|
-
t.classList.add('saved');
|
|
154
|
-
setTimeout(() => t.classList.remove('saved'), 300);
|
|
155
|
-
} }}
|
|
156
|
-
@contextmenu=${(e) => { e.preventDefault(); if (filled)
|
|
157
|
-
this._bookmarkAction('clear', i); }}
|
|
158
|
-
@mousedown=${(e) => this._onBookmarkMouseDown(e, i)}
|
|
159
|
-
@mouseup=${() => this._onBookmarkMouseUp()}
|
|
160
|
-
@mouseleave=${() => this._onBookmarkMouseUp()}
|
|
161
|
-
>${i}</div>
|
|
162
|
-
`;
|
|
163
|
-
})}
|
|
164
|
-
</div>
|
|
210
|
+
<label for="cb-floor-constraint"><ox-i18n msgid="label.keep-above-ground">Keep Above Ground</ox-i18n></label>
|
|
165
211
|
</div>
|
|
166
212
|
</fieldset>
|
|
167
213
|
`;
|
|
168
214
|
}
|
|
169
|
-
_onBookmarkMouseDown(e, index) {
|
|
170
|
-
if (e.button !== 0)
|
|
171
|
-
return;
|
|
172
|
-
const target = e.currentTarget;
|
|
173
|
-
this._pressTarget = target;
|
|
174
|
-
// 롱클릭 시작: filling 애니메이션
|
|
175
|
-
target.classList.add('saving');
|
|
176
|
-
this._longPressTimer = setTimeout(() => {
|
|
177
|
-
this._longPressTimer = undefined;
|
|
178
|
-
target.classList.remove('saving');
|
|
179
|
-
this._bookmarkAction('save', index);
|
|
180
|
-
// 저장 완료 피드백
|
|
181
|
-
target.classList.add('saved');
|
|
182
|
-
setTimeout(() => target.classList.remove('saved'), 300);
|
|
183
|
-
}, 600);
|
|
184
|
-
}
|
|
185
|
-
_onBookmarkMouseUp() {
|
|
186
|
-
if (this._longPressTimer) {
|
|
187
|
-
clearTimeout(this._longPressTimer);
|
|
188
|
-
this._longPressTimer = undefined;
|
|
189
|
-
}
|
|
190
|
-
if (this._pressTarget) {
|
|
191
|
-
this._pressTarget.classList.remove('saving');
|
|
192
|
-
this._pressTarget = undefined;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
_bookmarkAction(action, index) {
|
|
196
|
-
var _a;
|
|
197
|
-
const root = (_a = this.scene) === null || _a === void 0 ? void 0 : _a.root;
|
|
198
|
-
const cap = root === null || root === void 0 ? void 0 : root._threeCapability;
|
|
199
|
-
if (!cap)
|
|
200
|
-
return;
|
|
201
|
-
if (action === 'save') {
|
|
202
|
-
cap.saveCameraToSlot(index);
|
|
203
|
-
}
|
|
204
|
-
else if (action === 'navigate') {
|
|
205
|
-
cap.animateToSlot(index);
|
|
206
|
-
}
|
|
207
|
-
else if (action === 'clear') {
|
|
208
|
-
cap.bookmarkManager.clearSlot(index);
|
|
209
|
-
root.set('cameraBookmarks', cap.bookmarkManager.exportSlots());
|
|
210
|
-
}
|
|
211
|
-
this.requestUpdate();
|
|
212
|
-
}
|
|
213
215
|
_renderRenderer(value) {
|
|
216
|
+
var _a;
|
|
217
|
+
const exposure = Number((_a = value.exposure) !== null && _a !== void 0 ? _a : 1.2);
|
|
214
218
|
return html `
|
|
215
219
|
<fieldset>
|
|
216
220
|
<legend>Renderer</legend>
|
|
@@ -228,27 +232,62 @@ export class PropertyScene3D extends AbstractProperty {
|
|
|
228
232
|
${o.label}
|
|
229
233
|
</option>`)}
|
|
230
234
|
</select>
|
|
235
|
+
|
|
236
|
+
<label>Exposure</label>
|
|
237
|
+
<div class="range-with-value">
|
|
238
|
+
<input
|
|
239
|
+
type="range"
|
|
240
|
+
min="0.3"
|
|
241
|
+
max="2.5"
|
|
242
|
+
step="0.05"
|
|
243
|
+
value-key="exposure"
|
|
244
|
+
.value=${String(exposure)}
|
|
245
|
+
/>
|
|
246
|
+
<span>${exposure.toFixed(2)}</span>
|
|
247
|
+
</div>
|
|
231
248
|
</div>
|
|
232
249
|
</fieldset>
|
|
233
250
|
`;
|
|
234
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Environment 드롭다운 — 씬의 공간적 맥락을 정의.
|
|
254
|
+
* 선택 시 sky 값과 함께 ENVIRONMENT_DEFAULTS의 기본 조명까지 한 번에 적용한다.
|
|
255
|
+
* "원클릭 전체 look" — 사용자가 원하면 Light Mood/원자 컨트롤로 위에 얹어 조정.
|
|
256
|
+
*/
|
|
235
257
|
_renderSky(value) {
|
|
236
|
-
const
|
|
237
|
-
{ value: '', label: 'None' },
|
|
238
|
-
{ value: 'color', label: 'Color' },
|
|
239
|
-
{ value: 'studio', label: 'Studio' },
|
|
240
|
-
{ value: 'warehouse', label: 'Warehouse' },
|
|
241
|
-
{ value: 'factory', label: 'Factory' },
|
|
242
|
-
{ value: 'office', label: 'Office' },
|
|
243
|
-
{ value: 'home', label: 'Home' },
|
|
258
|
+
const envOptions = [
|
|
259
|
+
{ value: '', label: 'None', group: 'basic' },
|
|
260
|
+
{ value: 'color', label: 'Color', group: 'basic' },
|
|
261
|
+
{ value: 'studio', label: 'Studio', group: 'indoor' },
|
|
262
|
+
{ value: 'warehouse', label: 'Warehouse', group: 'indoor' },
|
|
263
|
+
{ value: 'factory', label: 'Factory', group: 'indoor' },
|
|
264
|
+
{ value: 'office', label: 'Office', group: 'indoor' },
|
|
265
|
+
{ value: 'home', label: 'Home', group: 'indoor' },
|
|
266
|
+
{ value: 'sunny', label: 'Sunny', group: 'outdoor' },
|
|
267
|
+
{ value: 'cloudy', label: 'Cloudy', group: 'outdoor' },
|
|
268
|
+
{ value: 'rainy', label: 'Rainy', group: 'outdoor' }
|
|
244
269
|
];
|
|
245
270
|
return html `
|
|
246
271
|
<fieldset>
|
|
247
|
-
<legend>
|
|
272
|
+
<legend>Environment</legend>
|
|
248
273
|
<div class="property-grid">
|
|
249
274
|
<label>Preset</label>
|
|
250
|
-
<select
|
|
251
|
-
|
|
275
|
+
<select @change=${this._onEnvironmentChange}>
|
|
276
|
+
<optgroup label="Basic">
|
|
277
|
+
${envOptions
|
|
278
|
+
.filter(o => o.group === 'basic')
|
|
279
|
+
.map(o => html `<option value=${o.value} ?selected=${(value.sky || '') === o.value}>${o.label}</option>`)}
|
|
280
|
+
</optgroup>
|
|
281
|
+
<optgroup label="Indoor">
|
|
282
|
+
${envOptions
|
|
283
|
+
.filter(o => o.group === 'indoor')
|
|
284
|
+
.map(o => html `<option value=${o.value} ?selected=${(value.sky || '') === o.value}>${o.label}</option>`)}
|
|
285
|
+
</optgroup>
|
|
286
|
+
<optgroup label="Outdoor">
|
|
287
|
+
${envOptions
|
|
288
|
+
.filter(o => o.group === 'outdoor')
|
|
289
|
+
.map(o => html `<option value=${o.value} ?selected=${(value.sky || '') === o.value}>${o.label}</option>`)}
|
|
290
|
+
</optgroup>
|
|
252
291
|
</select>
|
|
253
292
|
|
|
254
293
|
${value.sky === 'color'
|
|
@@ -261,12 +300,36 @@ export class PropertyScene3D extends AbstractProperty {
|
|
|
261
300
|
</fieldset>
|
|
262
301
|
`;
|
|
263
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* Environment 선택 핸들러 — sky 필드만 바꾸는 게 아니라 ENVIRONMENT_DEFAULTS에
|
|
305
|
+
* 정의된 hemi/dir 기본 조명까지 한 번의 property-change로 함께 적용한다.
|
|
306
|
+
*/
|
|
307
|
+
_onEnvironmentChange(e) {
|
|
308
|
+
const select = e.target;
|
|
309
|
+
const sky = select.value;
|
|
310
|
+
const detail = { sky };
|
|
311
|
+
const defaults = ENVIRONMENT_DEFAULTS[sky];
|
|
312
|
+
if (defaults) {
|
|
313
|
+
Object.assign(detail, defaults);
|
|
314
|
+
}
|
|
315
|
+
this.dispatchEvent(new CustomEvent('property-change', {
|
|
316
|
+
bubbles: true,
|
|
317
|
+
composed: true,
|
|
318
|
+
detail
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
264
321
|
_renderHemisphereLight(value) {
|
|
265
322
|
var _a, _b;
|
|
266
323
|
return html `
|
|
267
324
|
<fieldset>
|
|
268
325
|
<legend>Hemisphere Light</legend>
|
|
269
326
|
<div class="property-grid">
|
|
327
|
+
<label>Sky</label>
|
|
328
|
+
<ox-input-color value-key="hemiSkyColor" .value=${value.hemiSkyColor || '#ffffff'}></ox-input-color>
|
|
329
|
+
|
|
330
|
+
<label>Ground</label>
|
|
331
|
+
<ox-input-color value-key="hemiGroundColor" .value=${value.hemiGroundColor || '#444444'}></ox-input-color>
|
|
332
|
+
|
|
270
333
|
<label>Intensity</label>
|
|
271
334
|
<div class="range-with-value">
|
|
272
335
|
<input
|
|
@@ -284,7 +347,9 @@ export class PropertyScene3D extends AbstractProperty {
|
|
|
284
347
|
`;
|
|
285
348
|
}
|
|
286
349
|
_renderKeyLight(value) {
|
|
287
|
-
var _a, _b;
|
|
350
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
351
|
+
const followCamera = value.dirLightFollowCamera !== false;
|
|
352
|
+
const shadowEnabled = value.dirShadowEnabled !== false;
|
|
288
353
|
return html `
|
|
289
354
|
<fieldset>
|
|
290
355
|
<legend>Key Light</legend>
|
|
@@ -312,22 +377,102 @@ export class PropertyScene3D extends AbstractProperty {
|
|
|
312
377
|
/>
|
|
313
378
|
<span>${((_b = value.dirLightIntensity) !== null && _b !== void 0 ? _b : 0.5).toFixed(1)}</span>
|
|
314
379
|
</div>
|
|
380
|
+
|
|
381
|
+
<input
|
|
382
|
+
id="cb-dirlight-follow"
|
|
383
|
+
type="checkbox"
|
|
384
|
+
value-key="dirLightFollowCamera"
|
|
385
|
+
.checked=${followCamera}
|
|
386
|
+
/>
|
|
387
|
+
<label for="cb-dirlight-follow">Follow Camera</label>
|
|
388
|
+
|
|
389
|
+
${!followCamera
|
|
390
|
+
? html `
|
|
391
|
+
<label>Azimuth</label>
|
|
392
|
+
<div class="range-with-value">
|
|
393
|
+
<input
|
|
394
|
+
type="range"
|
|
395
|
+
min="0"
|
|
396
|
+
max="360"
|
|
397
|
+
step="5"
|
|
398
|
+
value-key="dirLightAzimuth"
|
|
399
|
+
.value=${String((_c = value.dirLightAzimuth) !== null && _c !== void 0 ? _c : 30)}
|
|
400
|
+
/>
|
|
401
|
+
<span>${Math.round(((_d = value.dirLightAzimuth) !== null && _d !== void 0 ? _d : 30))}°</span>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
<label>Elevation</label>
|
|
405
|
+
<div class="range-with-value">
|
|
406
|
+
<input
|
|
407
|
+
type="range"
|
|
408
|
+
min="0"
|
|
409
|
+
max="90"
|
|
410
|
+
step="5"
|
|
411
|
+
value-key="dirLightElevation"
|
|
412
|
+
.value=${String((_e = value.dirLightElevation) !== null && _e !== void 0 ? _e : 45)}
|
|
413
|
+
/>
|
|
414
|
+
<span>${Math.round(((_f = value.dirLightElevation) !== null && _f !== void 0 ? _f : 45))}°</span>
|
|
415
|
+
</div>
|
|
416
|
+
`
|
|
417
|
+
: ''}
|
|
418
|
+
|
|
419
|
+
<input
|
|
420
|
+
id="cb-dirlight-shadow"
|
|
421
|
+
type="checkbox"
|
|
422
|
+
value-key="dirShadowEnabled"
|
|
423
|
+
.checked=${shadowEnabled}
|
|
424
|
+
/>
|
|
425
|
+
<label for="cb-dirlight-shadow">Cast Shadow</label>
|
|
426
|
+
|
|
427
|
+
${shadowEnabled
|
|
428
|
+
? html `
|
|
429
|
+
<label>Shadow Bias</label>
|
|
430
|
+
<div class="range-with-value">
|
|
431
|
+
<input
|
|
432
|
+
type="range"
|
|
433
|
+
min="-0.01"
|
|
434
|
+
max="0.01"
|
|
435
|
+
step="0.0001"
|
|
436
|
+
value-key="dirShadowBias"
|
|
437
|
+
.value=${String((_g = value.dirShadowBias) !== null && _g !== void 0 ? _g : -0.0005)}
|
|
438
|
+
/>
|
|
439
|
+
<span>${((_h = value.dirShadowBias) !== null && _h !== void 0 ? _h : -0.0005).toFixed(4)}</span>
|
|
440
|
+
</div>
|
|
441
|
+
`
|
|
442
|
+
: ''}
|
|
315
443
|
</div>
|
|
316
444
|
</fieldset>
|
|
317
445
|
`;
|
|
318
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* Light Mood — Environment가 설정한 기본 조명 위에 얹는 분위기 오버라이드.
|
|
449
|
+
* sky는 건드리지 않고 hemi/dir 값만 적용. Environment와 독립적으로 조합 가능.
|
|
450
|
+
*/
|
|
319
451
|
_renderLightingPresets() {
|
|
320
452
|
return html `
|
|
321
453
|
<fieldset>
|
|
322
|
-
<legend>
|
|
454
|
+
<legend>Light Mood</legend>
|
|
323
455
|
<div class="property-grid">
|
|
324
|
-
<
|
|
325
|
-
|
|
326
|
-
|
|
456
|
+
<label>Apply</label>
|
|
457
|
+
<select @change=${this._onPresetChange}>
|
|
458
|
+
<option value="" selected>Select...</option>
|
|
459
|
+
${Object.keys(LIGHTING_PRESETS).map(name => html `<option value=${name}>${name}</option>`)}
|
|
460
|
+
</select>
|
|
327
461
|
</div>
|
|
328
462
|
</fieldset>
|
|
329
463
|
`;
|
|
330
464
|
}
|
|
465
|
+
_onPresetChange(e) {
|
|
466
|
+
const select = e.target;
|
|
467
|
+
const name = select.value;
|
|
468
|
+
if (!name)
|
|
469
|
+
return;
|
|
470
|
+
const preset = LIGHTING_PRESETS[name];
|
|
471
|
+
if (preset)
|
|
472
|
+
this._applyLightingPreset(preset);
|
|
473
|
+
// 동일 preset을 다시 선택할 수 있도록 초기값으로 리셋
|
|
474
|
+
select.value = '';
|
|
475
|
+
}
|
|
331
476
|
_renderFloor(value) {
|
|
332
477
|
return html `
|
|
333
478
|
<property-material3d
|
|
@@ -357,30 +502,6 @@ export class PropertyScene3D extends AbstractProperty {
|
|
|
357
502
|
PropertyScene3D.styles = [
|
|
358
503
|
PropertyGridStyles,
|
|
359
504
|
css `
|
|
360
|
-
.preset-buttons {
|
|
361
|
-
grid-column: 1 / -1;
|
|
362
|
-
display: flex;
|
|
363
|
-
flex-wrap: wrap;
|
|
364
|
-
gap: 4px;
|
|
365
|
-
padding: 2px 0;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
.preset-buttons button {
|
|
369
|
-
flex: 1;
|
|
370
|
-
min-width: 60px;
|
|
371
|
-
padding: 4px 6px;
|
|
372
|
-
border: 1px solid var(--md-sys-color-outline, #ccc);
|
|
373
|
-
border-radius: 4px;
|
|
374
|
-
background: var(--md-sys-color-surface, #fff);
|
|
375
|
-
color: var(--md-sys-color-on-surface, #333);
|
|
376
|
-
font-size: 11px;
|
|
377
|
-
cursor: pointer;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
.preset-buttons button:hover {
|
|
381
|
-
background: var(--md-sys-color-primary-container, #e0e0e0);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
505
|
.range-with-value {
|
|
385
506
|
grid-column: 9 / -1;
|
|
386
507
|
display: flex;
|
|
@@ -402,62 +523,24 @@ PropertyScene3D.styles = [
|
|
|
402
523
|
color: var(--md-sys-color-on-secondary-container);
|
|
403
524
|
}
|
|
404
525
|
|
|
405
|
-
.
|
|
406
|
-
grid-column: 1 / -1;
|
|
407
|
-
display: grid;
|
|
408
|
-
grid-template-columns: repeat(9, 1fr);
|
|
409
|
-
gap: 2px;
|
|
410
|
-
padding: 4px 0 0;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
.bookmark-slot {
|
|
526
|
+
.camera-actions {
|
|
414
527
|
display: flex;
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
height: 24px;
|
|
418
|
-
border-radius: 3px;
|
|
419
|
-
font-size: 11px;
|
|
420
|
-
font-weight: 600;
|
|
421
|
-
cursor: pointer;
|
|
422
|
-
color: var(--md-sys-color-outline, #999);
|
|
423
|
-
background: var(--md-sys-color-surface-variant, #f0f0f0);
|
|
424
|
-
border: 1px solid transparent;
|
|
425
|
-
user-select: none;
|
|
426
|
-
transition: all 0.12s ease;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
.bookmark-slot:hover {
|
|
430
|
-
background: var(--md-sys-color-secondary-container, #e0e0e0);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
.bookmark-slot.filled {
|
|
434
|
-
color: var(--md-sys-color-primary, #6750A4);
|
|
435
|
-
background: var(--md-sys-color-primary-container, #EADDFF);
|
|
436
|
-
border-color: var(--md-sys-color-primary, #6750A4);
|
|
437
|
-
font-weight: 700;
|
|
528
|
+
justify-content: flex-end;
|
|
529
|
+
margin-top: 6px;
|
|
438
530
|
}
|
|
439
531
|
|
|
440
|
-
.
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
@keyframes bookmark-fill {
|
|
449
|
-
from { box-shadow: inset 24px 0 0 0 transparent; }
|
|
450
|
-
to { box-shadow: inset 24px 0 0 0 var(--md-sys-color-primary-container, #EADDFF); }
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
.bookmark-slot.saved {
|
|
454
|
-
animation: bookmark-saved 0.3s ease;
|
|
532
|
+
.camera-actions button {
|
|
533
|
+
font-size: 12px;
|
|
534
|
+
padding: 4px 10px;
|
|
535
|
+
border: 1px solid var(--md-sys-color-outline, #c4c7c5);
|
|
536
|
+
border-radius: 4px;
|
|
537
|
+
background: var(--md-sys-color-surface, #fff);
|
|
538
|
+
color: var(--md-sys-color-on-surface, #1c1b1f);
|
|
539
|
+
cursor: pointer;
|
|
455
540
|
}
|
|
456
541
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
50% { transform: scale(1.2); }
|
|
460
|
-
100% { transform: scale(1); }
|
|
542
|
+
.camera-actions button:hover {
|
|
543
|
+
background: var(--md-sys-color-surface-variant, #e7e0ec);
|
|
461
544
|
}
|
|
462
545
|
`
|
|
463
546
|
];
|