@volter/editor-threejs 0.5.65 → 0.5.67
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/NOTICE +2 -0
- package/contributions/animation-mixers.service.ts +20 -0
- package/contributions/animation-timeline.utility.tsx +44 -0
- package/contributions/three-integration.service.ts +13 -0
- package/dist-node/serving.mjs +405 -0
- package/package.json +113 -5
- package/serving/animation-live-module.ts +70 -0
- package/serving/animation-stamp.ts +88 -0
- package/serving/index.ts +14 -0
- package/serving/model-import-conversion.ts +344 -0
- package/src/adapter/ingest/scene-capture.ts +1 -23
- package/src/adapter/renderer-config.ts +3 -4
- package/src/adapter/three-contract.ts +72 -0
- package/src/animation/live-mixers.ts +55 -0
- package/src/ecs/object-marks.ts +1 -1
- package/src/ecs/user-data.ts +0 -16
- 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/content-bounds.ts +38 -4
- package/src/viewport/environment.ts +16 -0
- package/src/viewport-api.ts +92 -0
- package/src/viewport-door.ts +237 -0
- package/src/animation/animation-clock.ts +0 -479
- package/src/animation/runtime-inspection.ts +0 -45
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { faCircleHalfStroke, faSliders } from '@fortawesome/free-solid-svg-icons';
|
|
2
|
+
import { Checkbox, NumberInput, themeVars } from '@volter/editor-sdk/widgets';
|
|
3
|
+
import { EffectComposer, EffectPass, LUT3DEffect, RenderPass } from 'postprocessing';
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import * as THREE from 'three';
|
|
6
|
+
import { LUTCubeLoader } from 'three/addons/loaders/LUTCubeLoader.js';
|
|
7
|
+
import { readProjectTextFile } from '@volter/editor-sdk/kit/editor-api';
|
|
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 CubeLut {
|
|
14
|
+
readonly title: string | null;
|
|
15
|
+
readonly size: number;
|
|
16
|
+
readonly domainMin: THREE.Vector3;
|
|
17
|
+
readonly domainMax: THREE.Vector3;
|
|
18
|
+
readonly texture3D: THREE.Data3DTexture;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface PreviewSettings {
|
|
22
|
+
readonly intensity: number;
|
|
23
|
+
readonly split: number;
|
|
24
|
+
readonly tetrahedral: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function basename(path: string): string {
|
|
28
|
+
return path.split('/').pop() ?? path;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function clamp(value: number, min: number, max: number): number {
|
|
32
|
+
return Math.min(max, Math.max(min, value));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function vectorLabel(value: THREE.Vector3): string {
|
|
36
|
+
return [value.x, value.y, value.z].map((component) => component.toFixed(2)).join(', ');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildComparisonScene(): {
|
|
40
|
+
scene: THREE.Scene;
|
|
41
|
+
camera: THREE.PerspectiveCamera;
|
|
42
|
+
dispose: () => void;
|
|
43
|
+
} {
|
|
44
|
+
const scene = new THREE.Scene();
|
|
45
|
+
scene.background = new THREE.Color('#171a22');
|
|
46
|
+
const camera = new THREE.PerspectiveCamera(34, 1, 0.1, 100);
|
|
47
|
+
camera.position.set(6.6, 4.6, 8.8);
|
|
48
|
+
camera.lookAt(0, 0.7, 0);
|
|
49
|
+
|
|
50
|
+
scene.add(new THREE.HemisphereLight('#dce8ff', '#322a27', 2.1));
|
|
51
|
+
const key = new THREE.DirectionalLight('#fff3db', 3.8);
|
|
52
|
+
key.position.set(4, 7, 5);
|
|
53
|
+
scene.add(key);
|
|
54
|
+
|
|
55
|
+
const geometries: THREE.BufferGeometry[] = [];
|
|
56
|
+
const materials: THREE.Material[] = [];
|
|
57
|
+
const addMesh = (
|
|
58
|
+
geometry: THREE.BufferGeometry,
|
|
59
|
+
material: THREE.Material,
|
|
60
|
+
position: readonly [number, number, number],
|
|
61
|
+
): THREE.Mesh => {
|
|
62
|
+
geometries.push(geometry);
|
|
63
|
+
materials.push(material);
|
|
64
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
65
|
+
mesh.position.set(...position);
|
|
66
|
+
mesh.castShadow = true;
|
|
67
|
+
mesh.receiveShadow = true;
|
|
68
|
+
scene.add(mesh);
|
|
69
|
+
return mesh;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const swatches = [
|
|
73
|
+
['#e64d45', -2.4],
|
|
74
|
+
['#e5ad42', -1.2],
|
|
75
|
+
['#55b86a', 0],
|
|
76
|
+
['#47a9d8', 1.2],
|
|
77
|
+
['#8a68d7', 2.4],
|
|
78
|
+
] as const;
|
|
79
|
+
for (const [color, x] of swatches) {
|
|
80
|
+
addMesh(
|
|
81
|
+
new THREE.SphereGeometry(0.48, 48, 32),
|
|
82
|
+
new THREE.MeshStandardMaterial({ color, roughness: 0.3, metalness: 0.08 }),
|
|
83
|
+
[x, 0.65, 0],
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const neutralValues = [0.06, 0.18, 0.5, 0.82, 1];
|
|
88
|
+
neutralValues.forEach((value, index) => {
|
|
89
|
+
const channel = Math.round(value * 255);
|
|
90
|
+
addMesh(
|
|
91
|
+
new THREE.BoxGeometry(0.78, 0.78, 0.78),
|
|
92
|
+
new THREE.MeshStandardMaterial({
|
|
93
|
+
color: new THREE.Color(`rgb(${channel}, ${channel}, ${channel})`),
|
|
94
|
+
roughness: 0.72,
|
|
95
|
+
}),
|
|
96
|
+
[-2.4 + index * 1.2, 1.85, -0.1],
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const floor = addMesh(
|
|
101
|
+
new THREE.PlaneGeometry(9, 6),
|
|
102
|
+
new THREE.MeshStandardMaterial({ color: '#777b86', roughness: 0.92 }),
|
|
103
|
+
[0, 0.08, -1.8],
|
|
104
|
+
);
|
|
105
|
+
floor.rotation.x = -Math.PI / 2;
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
scene,
|
|
109
|
+
camera,
|
|
110
|
+
dispose: () => {
|
|
111
|
+
for (const geometry of geometries) geometry.dispose();
|
|
112
|
+
for (const material of materials) material.dispose();
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function LutPreview({
|
|
118
|
+
lut,
|
|
119
|
+
settingsRef,
|
|
120
|
+
}: {
|
|
121
|
+
lut: CubeLut;
|
|
122
|
+
settingsRef: React.RefObject<PreviewSettings>;
|
|
123
|
+
}) {
|
|
124
|
+
const hostRef = useRef<HTMLDivElement>(null);
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
const host = hostRef.current;
|
|
128
|
+
if (!host) return;
|
|
129
|
+
let disposed = false;
|
|
130
|
+
let frame = 0;
|
|
131
|
+
const canvas = document.createElement('canvas');
|
|
132
|
+
canvas.setAttribute('aria-label', 'LUT before and after preview');
|
|
133
|
+
canvas.style.width = '100%';
|
|
134
|
+
canvas.style.height = '100%';
|
|
135
|
+
canvas.style.display = 'block';
|
|
136
|
+
host.replaceChildren(canvas);
|
|
137
|
+
|
|
138
|
+
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
|
139
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
140
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
141
|
+
renderer.toneMappingExposure = 1;
|
|
142
|
+
renderer.shadowMap.enabled = true;
|
|
143
|
+
|
|
144
|
+
const comparison = buildComparisonScene();
|
|
145
|
+
const composer = new EffectComposer(renderer);
|
|
146
|
+
const lutEffect = new LUT3DEffect(lut.texture3D, { tetrahedralInterpolation: true });
|
|
147
|
+
composer.addPass(new RenderPass(comparison.scene, comparison.camera));
|
|
148
|
+
composer.addPass(new EffectPass(comparison.camera, lutEffect));
|
|
149
|
+
|
|
150
|
+
const size = { width: 1, height: 1 };
|
|
151
|
+
const draw = () => {
|
|
152
|
+
const settings = settingsRef.current;
|
|
153
|
+
const divider = Math.round(size.width * clamp(settings.split, 0.05, 0.95));
|
|
154
|
+
lutEffect.blendMode.opacity.value = clamp(settings.intensity, 0, 1);
|
|
155
|
+
lutEffect.tetrahedralInterpolation = settings.tetrahedral;
|
|
156
|
+
|
|
157
|
+
renderer.setScissorTest(false);
|
|
158
|
+
renderer.setRenderTarget(null);
|
|
159
|
+
renderer.clear();
|
|
160
|
+
renderer.setScissorTest(true);
|
|
161
|
+
renderer.setScissor(0, 0, divider, size.height);
|
|
162
|
+
renderer.render(comparison.scene, comparison.camera);
|
|
163
|
+
renderer.setScissor(divider, 0, size.width - divider, size.height);
|
|
164
|
+
composer.render();
|
|
165
|
+
renderer.setScissorTest(false);
|
|
166
|
+
};
|
|
167
|
+
const resize = () => {
|
|
168
|
+
const bounds = host.getBoundingClientRect();
|
|
169
|
+
size.width = Math.max(1, Math.floor(bounds.width));
|
|
170
|
+
size.height = Math.max(1, Math.floor(bounds.height));
|
|
171
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
172
|
+
renderer.setSize(size.width, size.height, false);
|
|
173
|
+
composer.setSize(size.width, size.height);
|
|
174
|
+
comparison.camera.aspect = size.width / size.height;
|
|
175
|
+
comparison.camera.updateProjectionMatrix();
|
|
176
|
+
};
|
|
177
|
+
resize();
|
|
178
|
+
draw();
|
|
179
|
+
const observer = new ResizeObserver(() => {
|
|
180
|
+
resize();
|
|
181
|
+
// ResizeObserver still fires for a hidden tab; repaint the still frame
|
|
182
|
+
// because requestAnimationFrame may remain suspended there.
|
|
183
|
+
draw();
|
|
184
|
+
});
|
|
185
|
+
observer.observe(host);
|
|
186
|
+
|
|
187
|
+
const render = () => {
|
|
188
|
+
if (disposed) return;
|
|
189
|
+
draw();
|
|
190
|
+
frame = requestAnimationFrame(render);
|
|
191
|
+
};
|
|
192
|
+
frame = requestAnimationFrame(render);
|
|
193
|
+
|
|
194
|
+
return () => {
|
|
195
|
+
disposed = true;
|
|
196
|
+
cancelAnimationFrame(frame);
|
|
197
|
+
observer.disconnect();
|
|
198
|
+
composer.dispose();
|
|
199
|
+
lutEffect.dispose();
|
|
200
|
+
comparison.dispose();
|
|
201
|
+
renderer.dispose();
|
|
202
|
+
canvas.remove();
|
|
203
|
+
};
|
|
204
|
+
}, [lut, settingsRef]);
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div
|
|
208
|
+
ref={hostRef}
|
|
209
|
+
data-testid="lut-preview"
|
|
210
|
+
style={{ width: '100%', height: '100%', minHeight: 260, background: '#171a22' }}
|
|
211
|
+
/>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function PreviewControls({
|
|
216
|
+
settings,
|
|
217
|
+
onChange,
|
|
218
|
+
}: {
|
|
219
|
+
settings: PreviewSettings;
|
|
220
|
+
onChange: (next: PreviewSettings) => void;
|
|
221
|
+
}) {
|
|
222
|
+
return (
|
|
223
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, padding: 4 }}>
|
|
224
|
+
<label style={{ display: 'flex', flexDirection: 'column', gap: 5, fontSize: 11 }}>
|
|
225
|
+
Intensity
|
|
226
|
+
<NumberInput
|
|
227
|
+
value={settings.intensity}
|
|
228
|
+
onChange={(intensity) => onChange({ ...settings, intensity: clamp(intensity, 0, 1) })}
|
|
229
|
+
step={0.01}
|
|
230
|
+
precision={2}
|
|
231
|
+
/>
|
|
232
|
+
</label>
|
|
233
|
+
<label style={{ display: 'flex', flexDirection: 'column', gap: 5, fontSize: 11 }}>
|
|
234
|
+
Comparison split
|
|
235
|
+
<NumberInput
|
|
236
|
+
value={settings.split}
|
|
237
|
+
onChange={(split) => onChange({ ...settings, split: clamp(split, 0.05, 0.95) })}
|
|
238
|
+
step={0.01}
|
|
239
|
+
precision={2}
|
|
240
|
+
/>
|
|
241
|
+
</label>
|
|
242
|
+
<label style={{ display: 'flex', alignItems: 'center', gap: 7, fontSize: 11 }}>
|
|
243
|
+
<Checkbox
|
|
244
|
+
checked={settings.tetrahedral}
|
|
245
|
+
onChange={(event) => onChange({ ...settings, tetrahedral: event.target.checked })}
|
|
246
|
+
/>
|
|
247
|
+
Tetrahedral interpolation
|
|
248
|
+
</label>
|
|
249
|
+
<div style={{ color: themeVars.content.muted, fontSize: 10, lineHeight: 1.45 }}>
|
|
250
|
+
Preview controls are session-only. Bind the LUT and accepted intensity in the owning TSX
|
|
251
|
+
post-processing composition.
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function loadCubeLut(assetPath: string): Promise<CubeLut> {
|
|
258
|
+
const source = await readProjectTextFile(assetPath);
|
|
259
|
+
if (source === null) throw new Error(`CUBE LUT not found: ${assetPath}`);
|
|
260
|
+
return new LUTCubeLoader().parse(source) as CubeLut;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function LutAssetDocument({
|
|
264
|
+
documentId,
|
|
265
|
+
assetPath,
|
|
266
|
+
active = true,
|
|
267
|
+
}: {
|
|
268
|
+
documentId: string;
|
|
269
|
+
assetPath: string;
|
|
270
|
+
active?: boolean;
|
|
271
|
+
}) {
|
|
272
|
+
const [revision, setRevision] = useState(0);
|
|
273
|
+
const [lut, setLut] = useState<CubeLut | null>(null);
|
|
274
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
275
|
+
const [settings, setSettings] = useState<PreviewSettings>({
|
|
276
|
+
intensity: 1,
|
|
277
|
+
split: 0.5,
|
|
278
|
+
tetrahedral: true,
|
|
279
|
+
});
|
|
280
|
+
const settingsRef = useRef(settings);
|
|
281
|
+
settingsRef.current = settings;
|
|
282
|
+
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
if (!active) return;
|
|
285
|
+
return subscribeProjectAsset(assetPath, () => setRevision((value) => value + 1));
|
|
286
|
+
}, [active, assetPath]);
|
|
287
|
+
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
if (!active) return;
|
|
290
|
+
let cancelled = false;
|
|
291
|
+
let owned: CubeLut | null = null;
|
|
292
|
+
setLut(null);
|
|
293
|
+
setLoadError(null);
|
|
294
|
+
void loadCubeLut(assetPath).then(
|
|
295
|
+
(next) => {
|
|
296
|
+
owned = next;
|
|
297
|
+
if (cancelled) {
|
|
298
|
+
next.texture3D.dispose();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
setLut(next);
|
|
302
|
+
},
|
|
303
|
+
(cause) => {
|
|
304
|
+
if (!cancelled) setLoadError(cause instanceof Error ? cause.message : String(cause));
|
|
305
|
+
},
|
|
306
|
+
);
|
|
307
|
+
return () => {
|
|
308
|
+
cancelled = true;
|
|
309
|
+
owned?.texture3D.dispose();
|
|
310
|
+
};
|
|
311
|
+
}, [active, assetPath, revision]);
|
|
312
|
+
|
|
313
|
+
const sections = useMemo<InspectionSection[]>(
|
|
314
|
+
() => [
|
|
315
|
+
{
|
|
316
|
+
id: 'lut-profile',
|
|
317
|
+
title: 'Color Grade',
|
|
318
|
+
icon: faCircleHalfStroke,
|
|
319
|
+
order: PROPERTIES_SECTION_ORDER,
|
|
320
|
+
body: {
|
|
321
|
+
kind: 'custom',
|
|
322
|
+
...(lut
|
|
323
|
+
? {
|
|
324
|
+
data: {
|
|
325
|
+
title: lut.title,
|
|
326
|
+
size: lut.size,
|
|
327
|
+
domainMin: lut.domainMin.toArray(),
|
|
328
|
+
domainMax: lut.domainMax.toArray(),
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
: {}),
|
|
332
|
+
render: () => (
|
|
333
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '6px 10px' }}>
|
|
334
|
+
<span style={{ color: themeVars.content.muted }}>Title</span>
|
|
335
|
+
<span>{lut?.title || basename(assetPath)}</span>
|
|
336
|
+
<span style={{ color: themeVars.content.muted }}>Cube</span>
|
|
337
|
+
<span>{lut ? `${lut.size}³` : '—'}</span>
|
|
338
|
+
<span style={{ color: themeVars.content.muted }}>Domain</span>
|
|
339
|
+
<span>
|
|
340
|
+
{lut ? `${vectorLabel(lut.domainMin)} → ${vectorLabel(lut.domainMax)}` : '—'}
|
|
341
|
+
</span>
|
|
342
|
+
</div>
|
|
343
|
+
),
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: 'lut-preview-settings',
|
|
348
|
+
title: 'Preview',
|
|
349
|
+
icon: faSliders,
|
|
350
|
+
order: CONTRIBUTED_SECTION_ORDER,
|
|
351
|
+
body: {
|
|
352
|
+
kind: 'custom',
|
|
353
|
+
data: { ...settings },
|
|
354
|
+
render: () => <PreviewControls settings={settings} onChange={setSettings} />,
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
[assetPath, lut, settings],
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
if (loadError) {
|
|
362
|
+
return (
|
|
363
|
+
<AssetEditorShell
|
|
364
|
+
documentId={documentId}
|
|
365
|
+
active={active}
|
|
366
|
+
type="color grade"
|
|
367
|
+
title={basename(assetPath)}
|
|
368
|
+
status="CUBE LUT unavailable"
|
|
369
|
+
>
|
|
370
|
+
<div role="alert" style={{ padding: 20, color: themeVars.semantic.danger }}>
|
|
371
|
+
{loadError}
|
|
372
|
+
</div>
|
|
373
|
+
</AssetEditorShell>
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!lut) {
|
|
378
|
+
return (
|
|
379
|
+
<AssetEditorShell
|
|
380
|
+
documentId={documentId}
|
|
381
|
+
active={active}
|
|
382
|
+
type="color grade"
|
|
383
|
+
title={basename(assetPath)}
|
|
384
|
+
status="Loading CUBE LUT"
|
|
385
|
+
>
|
|
386
|
+
<div style={{ padding: 20, color: themeVars.content.muted }}>Loading color grade…</div>
|
|
387
|
+
</AssetEditorShell>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<AssetEditorShell
|
|
393
|
+
documentId={documentId}
|
|
394
|
+
active={active}
|
|
395
|
+
type="color grade"
|
|
396
|
+
title={basename(assetPath)}
|
|
397
|
+
sections={sections}
|
|
398
|
+
status={`CUBE LUT · ${lut.size}³`}
|
|
399
|
+
fill
|
|
400
|
+
>
|
|
401
|
+
<div
|
|
402
|
+
style={{
|
|
403
|
+
position: 'relative',
|
|
404
|
+
width: '100%',
|
|
405
|
+
height: '100%',
|
|
406
|
+
minHeight: 0,
|
|
407
|
+
background: '#171a22',
|
|
408
|
+
}}
|
|
409
|
+
>
|
|
410
|
+
<LutPreview lut={lut} settingsRef={settingsRef} />
|
|
411
|
+
<div
|
|
412
|
+
style={{
|
|
413
|
+
position: 'absolute',
|
|
414
|
+
left: 12,
|
|
415
|
+
top: 12,
|
|
416
|
+
padding: '4px 7px',
|
|
417
|
+
borderRadius: 4,
|
|
418
|
+
background: 'rgba(12,14,20,0.72)',
|
|
419
|
+
color: '#f2f4f8',
|
|
420
|
+
fontSize: 10,
|
|
421
|
+
pointerEvents: 'none',
|
|
422
|
+
}}
|
|
423
|
+
>
|
|
424
|
+
Original
|
|
425
|
+
</div>
|
|
426
|
+
<div
|
|
427
|
+
style={{
|
|
428
|
+
position: 'absolute',
|
|
429
|
+
right: 12,
|
|
430
|
+
top: 12,
|
|
431
|
+
padding: '4px 7px',
|
|
432
|
+
borderRadius: 4,
|
|
433
|
+
background: 'rgba(12,14,20,0.72)',
|
|
434
|
+
color: '#f2f4f8',
|
|
435
|
+
fontSize: 10,
|
|
436
|
+
pointerEvents: 'none',
|
|
437
|
+
}}
|
|
438
|
+
>
|
|
439
|
+
{lut.title || 'Graded'}
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
</AssetEditorShell>
|
|
443
|
+
);
|
|
444
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { themeVars } from '@volter/editor-sdk/widgets';
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import type * as THREE from 'three';
|
|
4
|
+
import {
|
|
5
|
+
disposeProjectAssetModel,
|
|
6
|
+
loadProjectAssetModelSource,
|
|
7
|
+
type ProjectAssetModelSource,
|
|
8
|
+
} from '../../asset-preview';
|
|
9
|
+
import {
|
|
10
|
+
createModelPreviewSource,
|
|
11
|
+
loadModelThumbnailObject,
|
|
12
|
+
modelThumbnailFormat,
|
|
13
|
+
} from '../../model-thumbnail';
|
|
14
|
+
import { Object3DDocumentViewport } from '../StageHost';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* File-backed models use the same native Object3D document host as source
|
|
18
|
+
* factories. Asset-specific diagnostics and provenance live in Inspector
|
|
19
|
+
* contributions; this center document owns only loading and native lifetime.
|
|
20
|
+
*
|
|
21
|
+
* A GLB that carries clips also gets a WRITER: the surgical clip persistence
|
|
22
|
+
* appends accessors to the file's own BIN chunk, so editing a key in the
|
|
23
|
+
* animation workspace saves back into the asset instead of being read-only.
|
|
24
|
+
* When that binding refuses, its reason — not a generic one — is what the
|
|
25
|
+
* workspace shows.
|
|
26
|
+
*/
|
|
27
|
+
export function ModelAssetDocument({
|
|
28
|
+
documentId,
|
|
29
|
+
assetPath,
|
|
30
|
+
displayName,
|
|
31
|
+
active,
|
|
32
|
+
}: {
|
|
33
|
+
readonly documentId: string;
|
|
34
|
+
readonly assetPath: string;
|
|
35
|
+
readonly displayName: string;
|
|
36
|
+
readonly active: boolean;
|
|
37
|
+
}) {
|
|
38
|
+
const [source, setSource] = useState<ProjectAssetModelSource | null>(null);
|
|
39
|
+
const [error, setError] = useState<string | null>(null);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!active) {
|
|
43
|
+
setSource(null);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
let owned: THREE.Object3D | null = null;
|
|
48
|
+
setSource(null);
|
|
49
|
+
setError(null);
|
|
50
|
+
const format = modelThumbnailFormat(assetPath);
|
|
51
|
+
const loading: Promise<ProjectAssetModelSource> =
|
|
52
|
+
format === 'glb' || format === 'gltf' || format === 'spz'
|
|
53
|
+
? loadProjectAssetModelSource(assetPath, controller.signal)
|
|
54
|
+
: format
|
|
55
|
+
? loadModelThumbnailObject({ url: assetPath, format })
|
|
56
|
+
.then(createModelPreviewSource)
|
|
57
|
+
.then((root) => ({ root, sourceBytes: null }))
|
|
58
|
+
: Promise.reject(new Error(`No native Three.js model loader supports ${assetPath}.`));
|
|
59
|
+
void loading.then(
|
|
60
|
+
(loaded) => {
|
|
61
|
+
if (controller.signal.aborted) {
|
|
62
|
+
disposeProjectAssetModel(loaded.root);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
owned = loaded.root;
|
|
66
|
+
setSource(loaded);
|
|
67
|
+
},
|
|
68
|
+
(cause) => {
|
|
69
|
+
if (!controller.signal.aborted)
|
|
70
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
return () => {
|
|
74
|
+
controller.abort();
|
|
75
|
+
if (owned) disposeProjectAssetModel(owned);
|
|
76
|
+
};
|
|
77
|
+
}, [active, assetPath]);
|
|
78
|
+
|
|
79
|
+
const model = source?.root ?? null;
|
|
80
|
+
const build = useCallback(() => {
|
|
81
|
+
if (!model) throw new Error(`Model asset ${assetPath} is not loaded.`);
|
|
82
|
+
return { root: model, animations: model.animations, dispose() {} };
|
|
83
|
+
}, [assetPath, model]);
|
|
84
|
+
if (error)
|
|
85
|
+
return (
|
|
86
|
+
<div role="alert" style={{ padding: 20, color: themeVars.semantic.danger }}>
|
|
87
|
+
{error}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
if (!model)
|
|
91
|
+
return (
|
|
92
|
+
<div style={{ padding: 20, color: themeVars.content.muted }}>Loading {displayName}…</div>
|
|
93
|
+
);
|
|
94
|
+
return (
|
|
95
|
+
<Object3DDocumentViewport
|
|
96
|
+
documentId={documentId}
|
|
97
|
+
sourcePath={assetPath}
|
|
98
|
+
displayName={displayName}
|
|
99
|
+
build={build}
|
|
100
|
+
active={active}
|
|
101
|
+
assetType="model"
|
|
102
|
+
modelSource={{ kind: 'project-file', path: assetPath }}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
}
|