@volter/editor-threejs 0.5.66 → 0.5.68
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/contributions/animation-timeline.utility.tsx +44 -0
- package/contributions/three-integration.service.ts +13 -0
- package/package.json +96 -6
- package/src/adapter/renderer-config.ts +3 -4
- package/src/adapter/three-contract.ts +72 -0
- package/src/ecs/object-marks.ts +1 -1
- package/src/ecs/user-data.ts +0 -9
- package/src/host-hierarchy-objects.ts +31 -0
- package/src/kit/animation/three-clips-subject.ts +190 -0
- package/src/kit/asset-compare.ts +294 -0
- package/src/kit/asset-preview-command.ts +265 -0
- package/src/kit/asset-preview-framing.ts +357 -0
- package/src/kit/asset-preview.ts +2802 -0
- package/src/kit/asset-workflow/model-inspection.ts +830 -0
- package/src/kit/authoring/component-instance-root.ts +171 -0
- package/src/kit/authoring/design-time-settle.ts +343 -0
- package/src/kit/authoring/live-object-transform.ts +62 -0
- package/src/kit/authoring/object3d-document-session-registry.ts +154 -0
- package/src/kit/authoring/object3d-document-session.ts +1965 -0
- package/src/kit/authoring/object3d-gesture-controller.ts +113 -0
- package/src/kit/authoring/quarks-particle-systems.ts +19 -0
- package/src/kit/authoring/shell-viewport-policy.ts +48 -0
- package/src/kit/authoring/source-object3d-authoring-adapter.ts +526 -0
- package/src/kit/authoring/three-projection-core.ts +226 -0
- package/src/kit/authoring/viewport-pick-context.ts +39 -0
- package/src/kit/authoring/viewport-raycast.ts +240 -0
- package/src/kit/authoring/world-hidden-viewport.ts +95 -0
- package/src/kit/camera-authoring.ts +175 -0
- package/src/kit/components/CameraInfo.tsx +56 -0
- package/src/kit/components/InspectorObjectPreview.tsx +57 -0
- package/src/kit/components/Object3DDocumentToolbar.tsx +549 -0
- package/src/kit/components/Object3DDocumentViewport.tsx +58 -0
- package/src/kit/components/StageHost.tsx +2547 -0
- package/src/kit/components/StageOverlays.tsx +21 -0
- package/src/kit/components/StatsOverlay.tsx +78 -0
- package/src/kit/components/ToolObject3DPreview.tsx +39 -0
- package/src/kit/components/ViewportFurniture.tsx +655 -0
- package/src/kit/components/ViewportOverlay.tsx +215 -0
- package/src/kit/components/ViewportShadingMenu.tsx +340 -0
- package/src/kit/components/ViewportViewMenu.tsx +155 -0
- package/src/kit/components/asset-viewers/EntityModelDocument.tsx +121 -0
- package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
- package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +395 -0
- package/src/kit/components/asset-viewers/LutAssetDocument.tsx +444 -0
- package/src/kit/components/asset-viewers/ModelAssetDocument.tsx +105 -0
- package/src/kit/components/asset-viewers/Object3DPreview.tsx +356 -0
- package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +527 -0
- package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
- package/src/kit/components/asset-viewers/three-asset-viewers.tsx +132 -0
- package/src/kit/components/object3d-contribution-surfaces.tsx +33 -0
- package/src/kit/components/stage-keyboard.tsx +40 -0
- package/src/kit/components/stage-overlay-set.tsx +105 -0
- package/src/kit/components/stage-presence-markers.ts +482 -0
- package/src/kit/components/stage-transform-chrome.ts +30 -0
- package/src/kit/components/stage-transform-tools.tsx +73 -0
- package/src/kit/components/stage-view-name.ts +30 -0
- package/src/kit/components/standard-viewport-dressing.ts +1042 -0
- package/src/kit/components/world-root-binding.ts +64 -0
- package/src/kit/constraint-helper.ts +338 -0
- package/src/kit/editor-shell-store.ts +814 -0
- package/src/kit/editor-viewport.ts +6621 -0
- package/src/kit/entity-lod.ts +31 -0
- package/src/kit/entity-object.ts +92 -0
- package/src/kit/hierarchy-mark-reader.ts +74 -0
- package/src/kit/instanced-presentation.ts +164 -0
- package/src/kit/live-module-source.ts +230 -0
- package/src/kit/model-thumbnail.ts +539 -0
- package/src/kit/play-camera-flight.ts +300 -0
- package/src/kit/projection/three.ts +898 -0
- package/src/kit/reflection-probe-helper.ts +142 -0
- package/src/kit/scene-document-viewport.ts +51 -0
- package/src/kit/scene-framing.ts +315 -0
- package/src/kit/scene-view-fog.ts +89 -0
- package/src/kit/spatial-handle-visuals.ts +332 -0
- package/src/kit/stories/three-story-model.ts +66 -0
- package/src/kit/three-canvas-render.ts +44 -0
- package/src/kit/three-hierarchy-row-media.ts +26 -0
- package/src/kit/three-inspection-media.ts +73 -0
- package/src/kit/three-integration.ts +86 -0
- package/src/kit/three-state.ts +33 -0
- package/src/kit/three-viewport/bone-selection-highlight.ts +119 -0
- package/src/kit/three-viewport/camera-fit.ts +41 -0
- package/src/kit/three-viewport/interactive-renderer.ts +132 -0
- package/src/kit/three-viewport/selection-brackets.ts +355 -0
- package/src/kit/three-viewport/selection-outline.ts +333 -0
- package/src/kit/three-viewport/skeleton-helper.ts +61 -0
- package/src/kit/three-viewport/source-color.ts +197 -0
- package/src/kit/three-viewport/studio-environment.ts +96 -0
- package/src/kit/trigger-volume-helper.ts +116 -0
- package/src/kit/viewport-actions.ts +128 -0
- package/src/kit/viewport-authoring-policy.ts +154 -0
- package/src/kit/viewport-commands.ts +318 -0
- package/src/kit/viewport-hotkeys.ts +119 -0
- package/src/kit/viewport-shading-boundary.ts +12 -0
- package/src/kit/viewport-status-facet.ts +53 -0
- package/src/object3d-contributions.ts +494 -0
- package/src/render/viewport-shading.ts +6 -2
- package/src/viewport-api.ts +92 -0
- package/src/viewport-door.ts +237 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { faCloudSun, faSliders } from '@fortawesome/free-solid-svg-icons';
|
|
2
|
+
import { NumberInput, themeVars } from '@volter/editor-sdk/widgets';
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import * as THREE from 'three';
|
|
5
|
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
6
|
+
import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
|
|
7
|
+
import { HDRLoader } from 'three/addons/loaders/HDRLoader.js';
|
|
8
|
+
import type { InspectionSection } from '@volter/editor-sdk/kit/inspection-model';
|
|
9
|
+
import { CONTRIBUTED_SECTION_ORDER, PROPERTIES_SECTION_ORDER } from '@volter/editor-sdk/kit/inspection-model';
|
|
10
|
+
import { AssetEditorShell } from '@volter/editor-sdk/kit/components/AssetEditorShell';
|
|
11
|
+
import { subscribeProjectAsset } from '@volter/editor-sdk/kit/components/asset-editor-persistence';
|
|
12
|
+
|
|
13
|
+
interface EnvironmentImage {
|
|
14
|
+
readonly texture: THREE.DataTexture;
|
|
15
|
+
readonly width: number;
|
|
16
|
+
readonly height: number;
|
|
17
|
+
readonly format: 'HDR' | 'EXR';
|
|
18
|
+
readonly panorama: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface PreviewSettings {
|
|
22
|
+
readonly exposure: number;
|
|
23
|
+
readonly intensity: number;
|
|
24
|
+
readonly rotation: number;
|
|
25
|
+
readonly blur: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function basename(path: string): string {
|
|
29
|
+
return path.split('/').pop() ?? path;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function clamp(value: number, min: number, max: number): number {
|
|
33
|
+
return Math.min(max, Math.max(min, value));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isExr(path: string): boolean {
|
|
37
|
+
return path.split(/[?#]/, 1)[0]!.toLowerCase().endsWith('.exr');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function loadEnvironmentImage(path: string): Promise<EnvironmentImage> {
|
|
41
|
+
const format = isExr(path) ? 'EXR' : 'HDR';
|
|
42
|
+
const loader = format === 'EXR' ? new EXRLoader() : new HDRLoader();
|
|
43
|
+
const texture = await loader.loadAsync(path);
|
|
44
|
+
const image = texture.image as { width?: number; height?: number };
|
|
45
|
+
const width = image.width ?? 0;
|
|
46
|
+
const height = image.height ?? 0;
|
|
47
|
+
const panorama = width > 0 && height > 0 && Math.abs(width / height - 2) <= 0.04;
|
|
48
|
+
texture.mapping = THREE.EquirectangularReflectionMapping;
|
|
49
|
+
texture.needsUpdate = true;
|
|
50
|
+
return { texture, width, height, format, panorama };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createPreviewScene(image: EnvironmentImage): {
|
|
54
|
+
readonly scene: THREE.Scene;
|
|
55
|
+
readonly camera: THREE.PerspectiveCamera;
|
|
56
|
+
readonly geometries: readonly THREE.BufferGeometry[];
|
|
57
|
+
readonly materials: readonly THREE.Material[];
|
|
58
|
+
} {
|
|
59
|
+
const scene = new THREE.Scene();
|
|
60
|
+
const camera = new THREE.PerspectiveCamera(38, 1, 0.05, 100);
|
|
61
|
+
camera.position.set(6.2, 3.4, 7.8);
|
|
62
|
+
camera.lookAt(0, 0.7, 0);
|
|
63
|
+
const geometries: THREE.BufferGeometry[] = [];
|
|
64
|
+
const materials: THREE.Material[] = [];
|
|
65
|
+
|
|
66
|
+
if (!image.panorama) {
|
|
67
|
+
scene.background = new THREE.Color('#11141b');
|
|
68
|
+
const aspect = image.width > 0 && image.height > 0 ? image.width / image.height : 1;
|
|
69
|
+
const geometry = new THREE.PlaneGeometry(
|
|
70
|
+
aspect >= 1 ? 4.8 : 4.8 * aspect,
|
|
71
|
+
aspect >= 1 ? 4.8 / aspect : 4.8,
|
|
72
|
+
);
|
|
73
|
+
const material = new THREE.MeshBasicMaterial({ map: image.texture, toneMapped: true });
|
|
74
|
+
geometries.push(geometry);
|
|
75
|
+
materials.push(material);
|
|
76
|
+
scene.add(new THREE.Mesh(geometry, material));
|
|
77
|
+
camera.position.set(0, 0, 6.5);
|
|
78
|
+
camera.lookAt(0, 0, 0);
|
|
79
|
+
return { scene, camera, geometries, materials };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
scene.background = image.texture;
|
|
83
|
+
scene.environment = image.texture;
|
|
84
|
+
|
|
85
|
+
const addMesh = (
|
|
86
|
+
geometry: THREE.BufferGeometry,
|
|
87
|
+
material: THREE.Material,
|
|
88
|
+
position: readonly [number, number, number],
|
|
89
|
+
) => {
|
|
90
|
+
geometries.push(geometry);
|
|
91
|
+
materials.push(material);
|
|
92
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
93
|
+
mesh.position.set(...position);
|
|
94
|
+
scene.add(mesh);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
addMesh(
|
|
98
|
+
new THREE.SphereGeometry(1, 64, 40),
|
|
99
|
+
new THREE.MeshStandardMaterial({ color: '#d8dde5', roughness: 0.08, metalness: 1 }),
|
|
100
|
+
[-2.3, 1.15, 0],
|
|
101
|
+
);
|
|
102
|
+
addMesh(
|
|
103
|
+
new THREE.SphereGeometry(1, 64, 40),
|
|
104
|
+
new THREE.MeshStandardMaterial({ color: '#c97341', roughness: 0.32, metalness: 0.25 }),
|
|
105
|
+
[0, 1.15, 0],
|
|
106
|
+
);
|
|
107
|
+
addMesh(
|
|
108
|
+
new THREE.SphereGeometry(1, 64, 40),
|
|
109
|
+
new THREE.MeshStandardMaterial({ color: '#d7d5cf', roughness: 0.82, metalness: 0 }),
|
|
110
|
+
[2.3, 1.15, 0],
|
|
111
|
+
);
|
|
112
|
+
const floorGeometry = new THREE.CircleGeometry(5.7, 96);
|
|
113
|
+
const floorMaterial = new THREE.MeshStandardMaterial({
|
|
114
|
+
color: '#737881',
|
|
115
|
+
roughness: 0.58,
|
|
116
|
+
metalness: 0,
|
|
117
|
+
});
|
|
118
|
+
geometries.push(floorGeometry);
|
|
119
|
+
materials.push(floorMaterial);
|
|
120
|
+
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
|
|
121
|
+
floor.rotation.x = -Math.PI / 2;
|
|
122
|
+
floor.position.y = 0.08;
|
|
123
|
+
scene.add(floor);
|
|
124
|
+
|
|
125
|
+
return { scene, camera, geometries, materials };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function EnvironmentPreview({
|
|
129
|
+
image,
|
|
130
|
+
settingsRef,
|
|
131
|
+
}: {
|
|
132
|
+
image: EnvironmentImage;
|
|
133
|
+
settingsRef: React.RefObject<PreviewSettings>;
|
|
134
|
+
}) {
|
|
135
|
+
const hostRef = useRef<HTMLDivElement>(null);
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
const host = hostRef.current;
|
|
139
|
+
if (!host) return;
|
|
140
|
+
let disposed = false;
|
|
141
|
+
let frame = 0;
|
|
142
|
+
const canvas = document.createElement('canvas');
|
|
143
|
+
canvas.setAttribute('aria-label', `${image.format} environment preview`);
|
|
144
|
+
canvas.style.width = '100%';
|
|
145
|
+
canvas.style.height = '100%';
|
|
146
|
+
canvas.style.display = 'block';
|
|
147
|
+
host.replaceChildren(canvas);
|
|
148
|
+
|
|
149
|
+
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
|
150
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
151
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
152
|
+
const preview = createPreviewScene(image);
|
|
153
|
+
const controls = new OrbitControls(preview.camera, canvas);
|
|
154
|
+
controls.target.set(0, image.panorama ? 0.8 : 0, 0);
|
|
155
|
+
controls.enableDamping = true;
|
|
156
|
+
controls.enablePan = false;
|
|
157
|
+
controls.minDistance = image.panorama ? 3.8 : 4;
|
|
158
|
+
controls.maxDistance = image.panorama ? 14 : 10;
|
|
159
|
+
controls.update();
|
|
160
|
+
|
|
161
|
+
const size = { width: 1, height: 1 };
|
|
162
|
+
const draw = () => {
|
|
163
|
+
const settings = settingsRef.current;
|
|
164
|
+
renderer.toneMappingExposure = clamp(settings.exposure, 0.05, 8);
|
|
165
|
+
if (image.panorama) {
|
|
166
|
+
const rotation = THREE.MathUtils.degToRad(settings.rotation);
|
|
167
|
+
preview.scene.backgroundRotation.y = rotation;
|
|
168
|
+
preview.scene.environmentRotation.y = rotation;
|
|
169
|
+
preview.scene.backgroundBlurriness = clamp(settings.blur, 0, 1);
|
|
170
|
+
preview.scene.environmentIntensity = clamp(settings.intensity, 0, 8);
|
|
171
|
+
}
|
|
172
|
+
controls.update();
|
|
173
|
+
renderer.render(preview.scene, preview.camera);
|
|
174
|
+
};
|
|
175
|
+
const resize = () => {
|
|
176
|
+
const bounds = host.getBoundingClientRect();
|
|
177
|
+
size.width = Math.max(1, Math.floor(bounds.width));
|
|
178
|
+
size.height = Math.max(1, Math.floor(bounds.height));
|
|
179
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
180
|
+
renderer.setSize(size.width, size.height, false);
|
|
181
|
+
preview.camera.aspect = size.width / size.height;
|
|
182
|
+
preview.camera.updateProjectionMatrix();
|
|
183
|
+
draw();
|
|
184
|
+
};
|
|
185
|
+
resize();
|
|
186
|
+
const observer = new ResizeObserver(resize);
|
|
187
|
+
observer.observe(host);
|
|
188
|
+
|
|
189
|
+
const render = () => {
|
|
190
|
+
if (disposed) return;
|
|
191
|
+
draw();
|
|
192
|
+
frame = requestAnimationFrame(render);
|
|
193
|
+
};
|
|
194
|
+
frame = requestAnimationFrame(render);
|
|
195
|
+
|
|
196
|
+
return () => {
|
|
197
|
+
disposed = true;
|
|
198
|
+
cancelAnimationFrame(frame);
|
|
199
|
+
observer.disconnect();
|
|
200
|
+
controls.dispose();
|
|
201
|
+
for (const geometry of preview.geometries) geometry.dispose();
|
|
202
|
+
for (const material of preview.materials) material.dispose();
|
|
203
|
+
renderer.dispose();
|
|
204
|
+
canvas.remove();
|
|
205
|
+
};
|
|
206
|
+
}, [image, settingsRef]);
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div
|
|
210
|
+
ref={hostRef}
|
|
211
|
+
data-testid="environment-preview"
|
|
212
|
+
style={{ width: '100%', height: '100%', minHeight: 260, background: '#11141b' }}
|
|
213
|
+
/>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function PreviewControls({
|
|
218
|
+
panorama,
|
|
219
|
+
settings,
|
|
220
|
+
onChange,
|
|
221
|
+
}: {
|
|
222
|
+
panorama: boolean;
|
|
223
|
+
settings: PreviewSettings;
|
|
224
|
+
onChange: (next: PreviewSettings) => void;
|
|
225
|
+
}) {
|
|
226
|
+
return (
|
|
227
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, padding: 4 }}>
|
|
228
|
+
<label style={{ display: 'flex', flexDirection: 'column', gap: 5, fontSize: 11 }}>
|
|
229
|
+
Exposure
|
|
230
|
+
<NumberInput
|
|
231
|
+
value={settings.exposure}
|
|
232
|
+
onChange={(exposure) => onChange({ ...settings, exposure: clamp(exposure, 0.05, 8) })}
|
|
233
|
+
step={0.05}
|
|
234
|
+
precision={2}
|
|
235
|
+
/>
|
|
236
|
+
</label>
|
|
237
|
+
{panorama && (
|
|
238
|
+
<>
|
|
239
|
+
<label style={{ display: 'flex', flexDirection: 'column', gap: 5, fontSize: 11 }}>
|
|
240
|
+
Lighting intensity
|
|
241
|
+
<NumberInput
|
|
242
|
+
value={settings.intensity}
|
|
243
|
+
onChange={(intensity) => onChange({ ...settings, intensity: clamp(intensity, 0, 8) })}
|
|
244
|
+
step={0.05}
|
|
245
|
+
precision={2}
|
|
246
|
+
/>
|
|
247
|
+
</label>
|
|
248
|
+
<label style={{ display: 'flex', flexDirection: 'column', gap: 5, fontSize: 11 }}>
|
|
249
|
+
Rotation
|
|
250
|
+
<NumberInput
|
|
251
|
+
value={settings.rotation}
|
|
252
|
+
onChange={(rotation) => onChange({ ...settings, rotation: rotation % 360 })}
|
|
253
|
+
step={1}
|
|
254
|
+
precision={0}
|
|
255
|
+
/>
|
|
256
|
+
</label>
|
|
257
|
+
<label style={{ display: 'flex', flexDirection: 'column', gap: 5, fontSize: 11 }}>
|
|
258
|
+
Background blur
|
|
259
|
+
<NumberInput
|
|
260
|
+
value={settings.blur}
|
|
261
|
+
onChange={(blur) => onChange({ ...settings, blur: clamp(blur, 0, 1) })}
|
|
262
|
+
step={0.05}
|
|
263
|
+
precision={2}
|
|
264
|
+
/>
|
|
265
|
+
</label>
|
|
266
|
+
</>
|
|
267
|
+
)}
|
|
268
|
+
<div style={{ color: themeVars.content.muted, fontSize: 10, lineHeight: 1.45 }}>
|
|
269
|
+
Preview controls are session-only. Bind the image, orientation, and accepted intensity in
|
|
270
|
+
the owning scene source.
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function EnvironmentAssetDocument({
|
|
277
|
+
documentId,
|
|
278
|
+
assetPath,
|
|
279
|
+
active = true,
|
|
280
|
+
}: {
|
|
281
|
+
documentId: string;
|
|
282
|
+
assetPath: string;
|
|
283
|
+
active?: boolean;
|
|
284
|
+
}) {
|
|
285
|
+
const [revision, setRevision] = useState(0);
|
|
286
|
+
const [image, setImage] = useState<EnvironmentImage | null>(null);
|
|
287
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
288
|
+
const [settings, setSettings] = useState<PreviewSettings>({
|
|
289
|
+
exposure: 1,
|
|
290
|
+
intensity: 1,
|
|
291
|
+
rotation: 0,
|
|
292
|
+
blur: 0,
|
|
293
|
+
});
|
|
294
|
+
const settingsRef = useRef(settings);
|
|
295
|
+
settingsRef.current = settings;
|
|
296
|
+
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
if (!active) return;
|
|
299
|
+
return subscribeProjectAsset(assetPath, () => setRevision((value) => value + 1));
|
|
300
|
+
}, [active, assetPath]);
|
|
301
|
+
|
|
302
|
+
useEffect(() => {
|
|
303
|
+
if (!active) return;
|
|
304
|
+
let cancelled = false;
|
|
305
|
+
let owned: EnvironmentImage | null = null;
|
|
306
|
+
setImage(null);
|
|
307
|
+
setLoadError(null);
|
|
308
|
+
void loadEnvironmentImage(assetPath).then(
|
|
309
|
+
(next) => {
|
|
310
|
+
owned = next;
|
|
311
|
+
if (cancelled) {
|
|
312
|
+
next.texture.dispose();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
setImage(next);
|
|
316
|
+
},
|
|
317
|
+
(cause) => {
|
|
318
|
+
if (!cancelled) setLoadError(cause instanceof Error ? cause.message : String(cause));
|
|
319
|
+
},
|
|
320
|
+
);
|
|
321
|
+
return () => {
|
|
322
|
+
cancelled = true;
|
|
323
|
+
owned?.texture.dispose();
|
|
324
|
+
};
|
|
325
|
+
}, [active, assetPath, revision]);
|
|
326
|
+
|
|
327
|
+
const sections = useMemo<InspectionSection[]>(
|
|
328
|
+
() => [
|
|
329
|
+
{
|
|
330
|
+
id: 'environment-image',
|
|
331
|
+
title: 'Environment Image',
|
|
332
|
+
icon: faCloudSun,
|
|
333
|
+
order: PROPERTIES_SECTION_ORDER,
|
|
334
|
+
body: {
|
|
335
|
+
kind: 'custom',
|
|
336
|
+
...(image
|
|
337
|
+
? {
|
|
338
|
+
data: {
|
|
339
|
+
format: image.format,
|
|
340
|
+
width: image.width,
|
|
341
|
+
height: image.height,
|
|
342
|
+
panorama: image.panorama,
|
|
343
|
+
},
|
|
344
|
+
}
|
|
345
|
+
: {}),
|
|
346
|
+
render: () => (
|
|
347
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '6px 10px' }}>
|
|
348
|
+
<span style={{ color: themeVars.content.muted }}>Format</span>
|
|
349
|
+
<span>{image?.format ?? '—'}</span>
|
|
350
|
+
<span style={{ color: themeVars.content.muted }}>Resolution</span>
|
|
351
|
+
<span>{image ? `${image.width} × ${image.height}` : '—'}</span>
|
|
352
|
+
<span style={{ color: themeVars.content.muted }}>Projection</span>
|
|
353
|
+
<span>{image?.panorama ? '2:1 equirectangular' : 'Flat image'}</span>
|
|
354
|
+
</div>
|
|
355
|
+
),
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
id: 'environment-preview-settings',
|
|
360
|
+
title: 'Preview',
|
|
361
|
+
icon: faSliders,
|
|
362
|
+
order: CONTRIBUTED_SECTION_ORDER,
|
|
363
|
+
body: {
|
|
364
|
+
kind: 'custom',
|
|
365
|
+
data: { ...settings },
|
|
366
|
+
render: () => (
|
|
367
|
+
<PreviewControls
|
|
368
|
+
panorama={image?.panorama ?? false}
|
|
369
|
+
settings={settings}
|
|
370
|
+
onChange={setSettings}
|
|
371
|
+
/>
|
|
372
|
+
),
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
[image, settings],
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
const title = basename(assetPath);
|
|
380
|
+
if (loadError) {
|
|
381
|
+
return (
|
|
382
|
+
<AssetEditorShell
|
|
383
|
+
documentId={documentId}
|
|
384
|
+
active={active}
|
|
385
|
+
type="environment image"
|
|
386
|
+
title={title}
|
|
387
|
+
status="HDR image unavailable"
|
|
388
|
+
>
|
|
389
|
+
<div role="alert" style={{ padding: 20, color: themeVars.semantic.danger }}>
|
|
390
|
+
{loadError}
|
|
391
|
+
</div>
|
|
392
|
+
</AssetEditorShell>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!image) {
|
|
397
|
+
return (
|
|
398
|
+
<AssetEditorShell
|
|
399
|
+
documentId={documentId}
|
|
400
|
+
active={active}
|
|
401
|
+
type="environment image"
|
|
402
|
+
title={title}
|
|
403
|
+
status="Loading HDR image"
|
|
404
|
+
>
|
|
405
|
+
<div style={{ padding: 20, color: themeVars.content.muted }}>Loading environment…</div>
|
|
406
|
+
</AssetEditorShell>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return (
|
|
411
|
+
<AssetEditorShell
|
|
412
|
+
documentId={documentId}
|
|
413
|
+
active={active}
|
|
414
|
+
type="environment image"
|
|
415
|
+
title={title}
|
|
416
|
+
sections={sections}
|
|
417
|
+
status={`${image.format} · ${image.width} × ${image.height}`}
|
|
418
|
+
fill
|
|
419
|
+
>
|
|
420
|
+
<div style={{ position: 'relative', width: '100%', height: '100%', minHeight: 0 }}>
|
|
421
|
+
<EnvironmentPreview image={image} settingsRef={settingsRef} />
|
|
422
|
+
<div
|
|
423
|
+
style={{
|
|
424
|
+
position: 'absolute',
|
|
425
|
+
left: 12,
|
|
426
|
+
bottom: 12,
|
|
427
|
+
padding: '4px 7px',
|
|
428
|
+
borderRadius: 4,
|
|
429
|
+
background: 'rgba(12,14,20,0.72)',
|
|
430
|
+
color: '#f2f4f8',
|
|
431
|
+
fontSize: 10,
|
|
432
|
+
pointerEvents: 'none',
|
|
433
|
+
}}
|
|
434
|
+
>
|
|
435
|
+
{image.panorama ? 'Drag to orbit · wheel to frame' : 'Non-2:1 image · flat preview'}
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
</AssetEditorShell>
|
|
439
|
+
);
|
|
440
|
+
}
|