Files
tinyusdz/web/js/usdlux.html
Syoyo Fujita 3b4bf5a687 update html file.
revert to use fancy-teapot for materialx.js demo.
2026-01-06 08:08:30 +09:00

2062 lines
79 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>TinyUSDZ UsdLux Light Demo</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
overflow: hidden;
font-family: 'Segoe UI', Arial, sans-serif;
background: #1a1a2e;
}
canvas {
display: block;
}
#info {
position: absolute;
top: 10px;
left: 10px;
color: white;
background: rgba(0, 0, 0, 0.85);
padding: 15px;
border-radius: 8px;
font-size: 13px;
max-width: 380px;
max-height: calc(100vh - 40px);
overflow-y: auto;
border: 1px solid rgba(255, 255, 255, 0.1);
}
#info h3 {
margin-top: 0;
margin-bottom: 12px;
color: #ffd700;
font-size: 16px;
}
#info p {
margin: 5px 0;
font-size: 12px;
color: #ccc;
}
#file-controls {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
#fileInput {
display: none;
}
.button {
display: inline-block;
padding: 8px 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
margin-right: 6px;
margin-bottom: 6px;
transition: all 0.2s;
}
.button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.button.secondary {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.button.danger {
background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
}
#light-info {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
#light-info h4 {
margin: 0 0 10px 0;
color: #6bb6ff;
font-size: 14px;
}
.light-list {
max-height: 300px;
overflow-y: auto;
}
.light-item {
padding: 10px;
margin: 6px 0;
background: rgba(255, 255, 255, 0.08);
border-radius: 6px;
border-left: 3px solid #ffd700;
cursor: pointer;
transition: all 0.2s;
}
.light-item:hover {
background: rgba(255, 255, 255, 0.12);
}
.light-item.selected {
background: rgba(102, 126, 234, 0.3);
border-left-color: #667eea;
}
.light-item .light-name {
font-weight: bold;
color: #fff;
margin-bottom: 4px;
}
.light-item .light-type {
display: inline-block;
padding: 2px 8px;
background: rgba(255, 215, 0, 0.2);
color: #ffd700;
border-radius: 10px;
font-size: 10px;
text-transform: uppercase;
margin-left: 8px;
}
.light-item .light-details {
font-size: 11px;
color: #aaa;
margin-top: 4px;
}
.light-item.disabled {
opacity: 0.5;
border-left-color: #666;
}
.light-item.disabled .light-name {
color: #888;
}
.light-toggle {
float: right;
width: 36px;
height: 20px;
background: #444;
border: none;
border-radius: 10px;
cursor: pointer;
position: relative;
transition: background 0.2s;
}
.light-toggle::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background: #888;
border-radius: 50%;
transition: all 0.2s;
}
.light-toggle.on {
background: #4CAF50;
}
.light-toggle.on::after {
left: 18px;
background: #fff;
}
.light-color-swatch {
display: inline-block;
width: 14px;
height: 14px;
border-radius: 3px;
vertical-align: middle;
margin-right: 6px;
border: 1px solid rgba(255, 255, 255, 0.3);
}
/* Envmap preview styles */
#envmap-section {
margin-top: 15px;
padding-top: 10px;
border-top: 1px solid #444;
}
#envmap-section h4 {
margin-bottom: 8px;
color: #9c7;
}
.envmap-color-swatch {
display: inline-block;
width: 20px;
height: 20px;
border-radius: 3px;
vertical-align: middle;
margin-right: 8px;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.envmap-preview-container {
margin-top: 10px;
background: #1a1a2e;
border-radius: 4px;
padding: 8px;
}
#envmap-preview-canvas {
width: 100%;
height: auto;
border-radius: 3px;
image-rendering: auto;
cursor: crosshair;
}
.envmap-preview-info {
margin-top: 5px;
font-size: 10px;
color: #888;
display: flex;
justify-content: space-between;
}
#loadingIndicator {
display: none;
color: #ffd700;
font-weight: bold;
margin-top: 8px;
}
#loadingIndicator.active {
display: block;
}
#drop-zone {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(102, 126, 234, 0.9);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
}
#drop-zone.active {
display: flex;
}
#drop-zone-content {
text-align: center;
color: white;
}
#drop-zone-content h2 {
font-size: 32px;
margin-bottom: 10px;
}
#drop-zone-content p {
font-size: 16px;
opacity: 0.8;
}
#scene-stats {
position: absolute;
bottom: 10px;
left: 10px;
color: white;
background: rgba(0, 0, 0, 0.7);
padding: 10px 15px;
border-radius: 6px;
font-size: 11px;
}
#scene-stats span {
margin-right: 15px;
}
.stat-label {
color: #888;
}
.stat-value {
color: #6bb6ff;
font-weight: bold;
}
#controls-info {
position: absolute;
bottom: 10px;
right: 10px;
color: white;
background: rgba(0, 0, 0, 0.7);
padding: 10px 15px;
border-radius: 6px;
font-size: 11px;
text-align: right;
}
#embedded-select, #tonemap-select {
width: 100%;
padding: 6px;
margin-bottom: 8px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.1);
color: white;
font-size: 12px;
}
#embedded-select option, #tonemap-select option {
background: #1a1a2e;
color: white;
}
.settings-section {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
.settings-section h4 {
margin: 0 0 10px 0;
color: #95e06c;
font-size: 13px;
}
.setting-row {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.setting-row label {
flex: 0 0 80px;
font-size: 11px;
color: #888;
}
.setting-row select, .setting-row input[type="range"] {
flex: 1;
}
.setting-value {
width: 45px;
text-align: right;
font-size: 11px;
color: #6bb6ff;
margin-left: 8px;
}
input[type="range"] {
-webkit-appearance: none;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
height: 6px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
background: #667eea;
border-radius: 50%;
cursor: pointer;
}
.tonemap-info {
font-size: 10px;
color: #666;
margin-top: 4px;
font-style: italic;
}
#spectral-section {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
#spectral-section h4 {
margin: 0 0 10px 0;
color: #e066ff;
font-size: 13px;
}
#spectral-canvas {
width: 100%;
height: 120px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
margin-bottom: 8px;
}
#spectral-info {
font-size: 10px;
color: #aaa;
margin-top: 6px;
padding: 6px;
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
}
#spectral-info div {
margin-bottom: 2px;
}
#spectral-info strong {
color: #e066ff;
}
.wavelength-preview {
display: inline-block;
width: 20px;
height: 14px;
vertical-align: middle;
border-radius: 3px;
border: 1px solid rgba(255, 255, 255, 0.3);
margin-left: 8px;
}
#monochrome-controls {
display: none;
}
#monochrome-controls.visible {
display: block;
}
/* HDRI Projection Styles */
#hdri-section {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
#hdri-section h4 {
margin: 0 0 10px 0;
color: #ffb366;
font-size: 13px;
}
#hdri-status {
font-size: 10px;
color: #aaa;
margin-top: 6px;
padding: 6px;
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
}
#hdri-status.has-hdri {
background: rgba(255, 179, 102, 0.15);
border: 1px solid rgba(255, 179, 102, 0.3);
}
#hdri-status strong {
color: #ffb366;
}
.hdri-button-row {
display: flex;
gap: 4px;
flex-wrap: wrap;
margin-bottom: 6px;
}
.hdri-button-row .button {
flex: 1;
min-width: 70px;
font-size: 10px;
padding: 6px 8px;
}
/* HDRI Preview Panel */
#hdri-preview-panel {
position: fixed;
bottom: 50px;
right: 10px;
width: 420px;
background: rgba(0, 0, 0, 0.9);
border: 1px solid rgba(255, 179, 102, 0.3);
border-radius: 8px;
padding: 12px;
display: none;
z-index: 100;
}
#hdri-preview-panel.visible {
display: block;
}
#hdri-preview-panel h4 {
margin: 0 0 8px 0;
color: #ffb366;
font-size: 13px;
display: flex;
justify-content: space-between;
align-items: center;
}
#hdri-preview-panel .close-btn {
background: none;
border: none;
color: #888;
font-size: 18px;
cursor: pointer;
padding: 0;
line-height: 1;
}
#hdri-preview-panel .close-btn:hover {
color: #fff;
}
#hdri-preview-canvas {
width: 100%;
max-height: 200px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
background: #111;
image-rendering: pixelated;
}
#hdri-preview-info {
font-size: 10px;
color: #888;
margin-top: 8px;
display: flex;
justify-content: space-between;
}
#hdri-preview-options {
display: flex;
align-items: center;
gap: 12px;
margin-top: 6px;
font-size: 10px;
}
#hdri-preview-options label {
display: flex;
align-items: center;
gap: 4px;
color: #aaa;
cursor: pointer;
}
#hdri-preview-options input[type="checkbox"] {
width: 14px;
height: 14px;
cursor: pointer;
}
#hdri-pixel-info {
font-size: 10px;
color: #0ff;
font-family: monospace;
background: rgba(0, 0, 0, 0.5);
padding: 4px 8px;
border-radius: 3px;
margin-top: 6px;
min-height: 18px;
}
#hdri-pixel-info .coord {
color: #888;
}
#hdri-pixel-info .value {
color: #fff;
}
.button.hdri {
background: linear-gradient(135deg, #ff9a56 0%, #ff6b6b 100%);
}
.button.hdri:hover {
box-shadow: 0 4px 12px rgba(255, 154, 86, 0.4);
}
.button.hdri-apply {
background: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
}
.button.hdri-apply.active {
background: linear-gradient(135deg, #2f56ab 0%, #63a8e0 100%);
}
/* HDRI Position Controls */
.position-row {
display: flex;
align-items: center;
gap: 4px;
margin: 6px 0;
}
.position-row label {
font-size: 10px;
color: #aaa;
min-width: 45px;
}
.position-input-group {
display: flex;
gap: 4px;
flex: 1;
}
.position-input-wrapper {
display: flex;
align-items: center;
gap: 2px;
}
.position-input-wrapper span {
font-size: 10px;
color: #888;
min-width: 10px;
}
.position-input {
width: 50px;
padding: 3px 4px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 3px;
color: #fff;
font-size: 10px;
text-align: center;
}
.position-input:focus {
outline: none;
border-color: rgba(0, 255, 255, 0.5);
background: rgba(0, 255, 255, 0.1);
}
.locator-toggle {
display: flex;
align-items: center;
gap: 6px;
margin: 6px 0;
}
.locator-toggle label {
font-size: 10px;
color: #aaa;
}
.locator-btn {
padding: 4px 10px;
font-size: 10px;
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 0.4);
border-radius: 4px;
color: #0ff;
cursor: pointer;
transition: all 0.2s;
}
.locator-btn:hover {
background: rgba(0, 255, 255, 0.3);
}
.locator-btn.active {
background: rgba(0, 255, 255, 0.5);
color: #000;
}
/* Object visibility toggles */
.object-toggles {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.object-toggle-item {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.08);
border-radius: 4px;
cursor: pointer;
font-size: 11px;
color: #aaa;
transition: all 0.2s;
}
.object-toggle-item:hover {
background: rgba(255, 255, 255, 0.12);
}
.object-toggle-item input[type="checkbox"] {
width: 14px;
height: 14px;
cursor: pointer;
accent-color: #667eea;
}
.object-toggle-item input[type="checkbox"]:checked + span {
color: #fff;
}
.object-toggle-item.toggle-all {
background: rgba(102, 126, 234, 0.2);
border: 1px solid rgba(102, 126, 234, 0.4);
width: 100%;
justify-content: center;
margin-bottom: 4px;
}
.object-toggle-item.toggle-all:hover {
background: rgba(102, 126, 234, 0.3);
}
.object-toggle-item.toggle-all span {
font-weight: bold;
}
/* Light Properties Editor */
#light-properties {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
display: none;
}
#light-properties.visible {
display: block;
}
#light-properties h4 {
margin: 0 0 10px 0;
color: #ff9966;
font-size: 13px;
}
#light-properties .prop-name {
font-size: 12px;
color: #fff;
margin-bottom: 8px;
padding: 4px 8px;
background: rgba(255, 153, 102, 0.2);
border-radius: 4px;
}
.color-picker-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.color-picker-row label {
flex: 0 0 70px;
font-size: 11px;
color: #888;
}
.color-picker-row input[type="color"] {
width: 40px;
height: 28px;
padding: 0;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
cursor: pointer;
background: transparent;
}
.color-picker-row input[type="color"]::-webkit-color-swatch-wrapper {
padding: 2px;
}
.color-picker-row input[type="color"]::-webkit-color-swatch {
border-radius: 2px;
border: none;
}
.color-picker-row .color-value {
font-size: 10px;
color: #aaa;
font-family: monospace;
}
.intensity-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.intensity-row label {
flex: 0 0 70px;
font-size: 11px;
color: #888;
}
.intensity-row input[type="range"] {
flex: 1;
}
.intensity-row input[type="number"] {
width: 60px;
padding: 4px 6px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
color: #fff;
font-size: 11px;
text-align: center;
}
.intensity-row input[type="number"]:focus {
outline: none;
border-color: rgba(255, 153, 102, 0.5);
background: rgba(255, 153, 102, 0.1);
}
/* Position/Rotation row with 3 inputs */
.vector3-row {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 8px;
}
.vector3-row label {
flex: 0 0 70px;
font-size: 11px;
color: #888;
}
.vector3-row .vector-inputs {
display: flex;
gap: 4px;
flex: 1;
}
.vector3-row .vector-input-group {
display: flex;
align-items: center;
gap: 2px;
}
.vector3-row .vector-input-group span {
font-size: 10px;
color: #666;
width: 12px;
}
.vector3-row .vector-input-group span.x-label { color: #ff6666; }
.vector3-row .vector-input-group span.y-label { color: #66ff66; }
.vector3-row .vector-input-group span.z-label { color: #6666ff; }
.vector3-row input[type="number"] {
width: 50px;
padding: 3px 4px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 3px;
color: #fff;
font-size: 10px;
text-align: center;
}
.vector3-row input[type="number"]:focus {
outline: none;
border-color: rgba(255, 153, 102, 0.5);
background: rgba(255, 153, 102, 0.1);
}
/* Cone/Shaping section */
.shaping-section {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
display: none;
}
.shaping-section.visible {
display: block;
}
.shaping-section .section-title {
font-size: 10px;
color: #ff9966;
margin-bottom: 6px;
text-transform: uppercase;
}
/* Selection Mode Toggle */
#selection-mode {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
#selection-mode h4 {
margin: 0 0 8px 0;
color: #6bb6ff;
font-size: 14px;
}
.mode-toggle-group {
display: flex;
gap: 4px;
}
.mode-toggle-btn {
flex: 1;
padding: 6px 8px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
color: #888;
font-size: 11px;
cursor: pointer;
transition: all 0.2s;
}
.mode-toggle-btn:hover {
background: rgba(255, 255, 255, 0.15);
color: #ccc;
}
.mode-toggle-btn.active {
background: rgba(102, 126, 234, 0.3);
border-color: #667eea;
color: #fff;
}
.mode-toggle-btn .shortcut {
display: inline-block;
margin-left: 4px;
padding: 1px 4px;
background: rgba(0, 0, 0, 0.3);
border-radius: 2px;
font-size: 9px;
color: #666;
}
.mode-toggle-btn.active .shortcut {
background: rgba(0, 0, 0, 0.4);
color: #aaa;
}
/* Lighting Mode Toggle */
#lighting-mode {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
#lighting-mode h4 {
margin: 0 0 8px 0;
color: #6bb6ff;
font-size: 14px;
}
.lighting-mode-toggle {
display: flex;
gap: 4px;
}
.lighting-mode-btn {
flex: 1;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
color: #888;
font-size: 11px;
cursor: pointer;
transition: all 0.2s;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.lighting-mode-btn:hover {
background: rgba(255, 255, 255, 0.15);
color: #ccc;
}
.lighting-mode-btn.active {
background: rgba(102, 126, 234, 0.3);
border-color: #667eea;
color: #fff;
}
.lighting-mode-btn.active.envmap {
background: rgba(17, 153, 142, 0.3);
border-color: #11998e;
}
.lighting-mode-btn .mode-icon {
font-size: 16px;
}
.lighting-mode-btn .mode-label {
font-size: 10px;
}
.lighting-mode-info {
margin-top: 6px;
font-size: 10px;
color: #666;
font-style: italic;
}
</style>
</head>
<body>
<div id="drop-zone">
<div id="drop-zone-content">
<h2>Drop USD File Here</h2>
<p>Supports .usd, .usda, .usdc, .usdz</p>
</div>
</div>
<div id="info">
<h3>UsdLux Light Demo</h3>
<p>
Visualize USD lights in Three.js.<br>
Drag & drop or use the button to load USD files.
</p>
<div id="file-controls">
<label for="embedded-select" style="font-size: 11px; color: #888;">Embedded Scenes:</label>
<select id="embedded-select">
<option value="basic">Basic Lights</option>
<option value="spotlight">Spotlight with IES</option>
<option value="area">Area Lights</option>
<option value="dome">Dome Light (IBL)</option>
<option value="complete" selected>Complete Scene</option>
</select>
<input type="file" id="fileInput" accept=".usd,.usda,.usdc,.usdz">
<button class="button" onclick="document.getElementById('fileInput').click()">Load USD File</button>
<button class="button secondary" onclick="window.loadEmbeddedScene()">Load Embedded</button>
<button class="button danger" onclick="window.clearLights()">Clear Lights</button>
</div>
<p>
<strong>Current:</strong> <span id="currentFile">Embedded Scene</span>
<span id="loadingIndicator">Loading...</span>
</p>
<div id="light-info">
<h4>Lights (<span id="lightCount">0</span>)</h4>
<div class="light-list" id="lightList">
<p style="color: #666; font-style: italic;">No lights loaded</p>
</div>
</div>
<div id="selection-mode">
<h4>Selection Mode</h4>
<div class="mode-toggle-group">
<button class="mode-toggle-btn active" id="mode-all" onclick="setSelectionModeUI('all')">
All
</button>
<button class="mode-toggle-btn" id="mode-meshes" onclick="setSelectionModeUI('meshes')">
Meshes<span class="shortcut">M</span>
</button>
<button class="mode-toggle-btn" id="mode-lights" onclick="setSelectionModeUI('lights')">
Lights<span class="shortcut">L</span>
</button>
</div>
</div>
<div id="light-properties">
<h4>Light Properties</h4>
<div class="prop-name" id="selected-light-name">No light selected</div>
<!-- Transform Mode Buttons -->
<div class="transform-mode-row" style="margin-bottom: 10px;">
<div class="mode-toggle-group">
<button class="mode-toggle-btn active" id="light-mode-pos" onclick="setLightTransformModeUI('translate')">
Pos<span class="shortcut">W</span>
</button>
<button class="mode-toggle-btn" id="light-mode-rot" onclick="setLightTransformModeUI('rotate')">
Rot<span class="shortcut">E</span>
</button>
<button class="mode-toggle-btn" id="light-mode-scale" onclick="setLightTransformModeUI('scale')">
Scale<span class="shortcut">R</span>
</button>
<button class="mode-toggle-btn" id="light-mode-target" style="display: none;" onclick="setLightTransformModeUI('target')">
Target<span class="shortcut">T</span>
</button>
</div>
</div>
<div class="color-picker-row">
<label>Color:</label>
<input type="color" id="light-color-picker" value="#ffffff">
<span class="color-value" id="light-color-value">#ffffff</span>
</div>
<div class="intensity-row">
<label>Intensity:</label>
<input type="range" id="light-intensity-slider" min="0" max="100" step="0.1" value="1">
<input type="number" id="light-intensity-input" min="0" max="10000" step="0.1" value="1">
</div>
<div class="intensity-row">
<label>Exposure:</label>
<input type="range" id="light-exposure-slider" min="-10" max="10" step="0.1" value="0">
<input type="number" id="light-exposure-input" min="-10" max="10" step="0.1" value="0">
</div>
<!-- Position controls (hidden for infinite lights) -->
<div class="vector3-row" id="light-position-row">
<label>Position:</label>
<div class="vector-inputs">
<div class="vector-input-group">
<span class="x-label">X</span>
<input type="number" id="light-pos-x" step="0.1" value="0">
</div>
<div class="vector-input-group">
<span class="y-label">Y</span>
<input type="number" id="light-pos-y" step="0.1" value="0">
</div>
<div class="vector-input-group">
<span class="z-label">Z</span>
<input type="number" id="light-pos-z" step="0.1" value="0">
</div>
</div>
</div>
<!-- Rotation controls -->
<div class="vector3-row" id="light-rotation-row">
<label>Rotation:</label>
<div class="vector-inputs">
<div class="vector-input-group">
<span class="x-label">X</span>
<input type="number" id="light-rot-x" step="1" value="0">
</div>
<div class="vector-input-group">
<span class="y-label">Y</span>
<input type="number" id="light-rot-y" step="1" value="0">
</div>
<div class="vector-input-group">
<span class="z-label">Z</span>
<input type="number" id="light-rot-z" step="1" value="0">
</div>
</div>
</div>
<!-- Shaping/Cone controls (only for SpotLight) -->
<div class="shaping-section" id="light-shaping-section">
<div class="section-title">Cone Shaping</div>
<div class="intensity-row">
<label>Cone Angle:</label>
<input type="range" id="light-cone-angle-slider" min="1" max="180" step="1" value="90">
<input type="number" id="light-cone-angle-input" min="1" max="180" step="1" value="90">
</div>
<div class="intensity-row">
<label>Softness:</label>
<input type="range" id="light-cone-softness-slider" min="0" max="1" step="0.01" value="0">
<input type="number" id="light-cone-softness-input" min="0" max="1" step="0.01" value="0">
</div>
</div>
</div>
<div id="envmap-section" style="display: none;">
<h4>Environment Map (DomeLight)</h4>
<div class="envmap-info">
<div class="setting-row">
<label>Color:</label>
<span class="envmap-color-swatch" id="envmap-color-swatch"></span>
<span id="envmap-color-value" style="font-size: 11px; color: #aaa;"></span>
</div>
<div class="setting-row">
<label>Texture:</label>
<span id="envmap-texture-info" style="font-size: 11px; color: #aaa;">None</span>
</div>
</div>
<div class="envmap-preview-container" id="envmap-preview-container" style="display: none;">
<canvas id="envmap-preview-canvas" width="256" height="128"></canvas>
<div class="envmap-preview-info">
<span id="envmap-dimensions"></span>
<span id="envmap-pixel-value" style="margin-left: 10px;"></span>
</div>
</div>
</div>
<div class="settings-section">
<h4>Scene Objects</h4>
<div class="object-toggles">
<label class="object-toggle-item toggle-all">
<input type="checkbox" id="toggle-all-meshes" checked>
<span>All Meshes</span>
</label>
<label class="object-toggle-item">
<input type="checkbox" id="toggle-ground" checked>
<span>Ground</span>
</label>
<label class="object-toggle-item">
<input type="checkbox" id="toggle-grid" checked>
<span>Grid</span>
</label>
<label class="object-toggle-item">
<input type="checkbox" id="toggle-sphere" checked>
<span>Sphere</span>
</label>
<label class="object-toggle-item">
<input type="checkbox" id="toggle-torus" checked>
<span>Torus</span>
</label>
<label class="object-toggle-item">
<input type="checkbox" id="toggle-box" checked>
<span>Box</span>
</label>
<label class="object-toggle-item">
<input type="checkbox" id="toggle-helpers" checked>
<span>Light Helpers</span>
</label>
</div>
</div>
<div class="settings-section">
<h4>Display Settings</h4>
<div class="setting-row">
<label>Tone Map:</label>
<select id="tonemap-select">
<option value="raw">Raw (Linear)</option>
<option value="reinhard">Reinhard</option>
<option value="aces1" selected>ACES 1.3</option>
<option value="aces2">ACES 2.0</option>
<option value="agx">AgX</option>
<option value="neutral">Neutral</option>
</select>
</div>
<div class="tonemap-info" id="tonemap-info">Film-like response with highlight roll-off</div>
<div class="setting-row">
<label>Exposure:</label>
<input type="range" id="exposure-slider" min="-3" max="3" step="0.1" value="0">
<span class="setting-value" id="exposure-value">0.0 EV</span>
</div>
<div class="setting-row">
<label>Gamma:</label>
<input type="range" id="gamma-slider" min="1.0" max="3.0" step="0.1" value="2.2">
<span class="setting-value" id="gamma-value">2.2</span>
</div>
</div>
<div id="lighting-mode">
<h4>Lighting Mode</h4>
<div class="lighting-mode-toggle">
<button class="lighting-mode-btn active" id="lighting-mode-lights" onclick="setLightingModeUI('lights')">
<span class="mode-icon">💡</span>
<span class="mode-label">Use Lights</span>
</button>
<button class="lighting-mode-btn envmap" id="lighting-mode-envmap" onclick="setLightingModeUI('envmap')">
<span class="mode-icon">🌐</span>
<span class="mode-label">Use Envmap</span>
</button>
</div>
<div class="lighting-mode-info" id="lighting-mode-info">
Direct lighting from scene lights
</div>
<div style="margin-top: 8px; display: flex; align-items: center; gap: 6px;">
<input type="checkbox" id="envmap-background-checkbox" onchange="window.setShowEnvmapBackground(this.checked)">
<label for="envmap-background-checkbox" style="font-size: 11px; color: #aaa; cursor: pointer;">Show as Background</label>
</div>
</div>
<div id="hdri-section">
<h4>HDRI Projection</h4>
<p style="font-size: 10px; color: #888; margin: 0 0 8px 0;">
Project scene lights to an environment map (no TinyUSDZ required)
</p>
<div class="setting-row">
<label>Resolution:</label>
<select id="hdri-resolution-select">
<option value="512">512 x 256</option>
<option value="1024" selected>1024 x 512</option>
<option value="2048">2048 x 1024</option>
<option value="4096">4096 x 2048</option>
</select>
</div>
<div class="setting-row">
<label>Max Dist:</label>
<input type="range" id="hdri-maxdist-slider" min="10" max="500" step="10" value="100">
<span class="setting-value" id="hdri-maxdist-value">100</span>
</div>
<div class="position-row">
<label>Position:</label>
<div class="position-input-group">
<div class="position-input-wrapper">
<span>X</span>
<input type="number" class="position-input" id="hdri-pos-x" value="0" step="0.1">
</div>
<div class="position-input-wrapper">
<span>Y</span>
<input type="number" class="position-input" id="hdri-pos-y" value="0" step="0.1">
</div>
<div class="position-input-wrapper">
<span>Z</span>
<input type="number" class="position-input" id="hdri-pos-z" value="0" step="0.1">
</div>
</div>
</div>
<div class="locator-toggle">
<label>3D Locator:</label>
<button class="locator-btn" id="hdri-locator-btn" onclick="toggleLocatorBtn()">Show Gizmo</button>
</div>
<div class="hdri-button-row">
<button class="button hdri" onclick="window.projectLightsToHDRI()">Project</button>
<button class="button secondary" onclick="window.toggleHDRIPreview()">Show HDRI</button>
</div>
<div class="hdri-button-row">
<button class="button hdri-apply" id="hdri-apply-btn" onclick="toggleHDRIApply()">Apply to Scene</button>
<button class="button" style="background: #555;" onclick="window.exportHDRI('exr')">Export EXR</button>
<button class="button" style="background: #555;" onclick="window.exportHDRI('hdr')">Export HDR</button>
</div>
<div id="hdri-status">
<span style="color: #666;">No HDRI generated yet</span>
</div>
</div>
<div id="spectral-section">
<h4>Spectral Settings</h4>
<canvas id="spectral-canvas" width="340" height="120"></canvas>
<div class="setting-row">
<label>Color Mode:</label>
<select id="spectral-mode-select">
<option value="rgb" selected>RGB (USD color)</option>
<option value="spectral">Spectral (SPD to RGB)</option>
<option value="monochrome">Monochrome</option>
</select>
</div>
<div id="monochrome-controls">
<div class="setting-row">
<label>Wavelength:</label>
<input type="range" id="wavelength-slider" min="380" max="780" step="1" value="550">
<span class="setting-value" id="wavelength-value">550nm</span>
<span class="wavelength-preview" id="wavelength-preview"></span>
</div>
</div>
<div style="margin-top: 8px;">
<button class="button" style="font-size: 10px; padding: 5px 10px;" onclick="window.applyDemoSpectralData()">Apply Demo SPD</button>
</div>
<div class="setting-row" style="margin-top: 8px;">
<label>Blackbody:</label>
<input type="range" id="blackbody-slider" min="1000" max="10000" step="100" value="5500">
<span class="setting-value" id="blackbody-value">5500K</span>
</div>
<div id="spectral-info">
<span style="color: #666;">Select a light to view spectral data</span>
</div>
</div>
</div>
<div id="scene-stats">
<span><span class="stat-label">Lights:</span> <span class="stat-value" id="statLights">0</span></span>
<span><span class="stat-label">Meshes:</span> <span class="stat-value" id="statMeshes">1</span></span>
<span><span class="stat-label">FPS:</span> <span class="stat-value" id="statFPS">60</span></span>
</div>
<div id="controls-info">
<strong>Controls:</strong><br>
Left-click + drag: Rotate<br>
Right-click + drag: Pan<br>
Scroll: Zoom<br>
<strong>Transform:</strong> W/E/R/T
</div>
<!-- HDRI Preview Panel -->
<div id="hdri-preview-panel">
<h4>
<span>HDRI Preview</span>
<button class="close-btn" onclick="window.toggleHDRIPreview()">&times;</button>
</h4>
<canvas id="hdri-preview-canvas"></canvas>
<div id="hdri-preview-info">
<span id="hdri-preview-size">-</span>
<span id="hdri-preview-range">-</span>
</div>
<div id="hdri-preview-options">
<label>
<input type="checkbox" id="hdri-normalize-checkbox" onchange="window.updateHDRIPreviewCanvas()">
Normalize
</label>
</div>
<div id="hdri-pixel-info">
<span class="coord">Move mouse over image</span>
</div>
<div class="hdri-button-row" style="margin-top: 8px;">
<button class="button" style="font-size: 10px; background: #555;" onclick="window.refreshHDRIProjection()">Refresh</button>
<button class="button hdri-apply" id="hdri-preview-apply-btn" onclick="toggleHDRIApply()">Apply to Scene</button>
</div>
<div style="margin-top: 6px; display: flex; align-items: center; gap: 6px;">
<input type="checkbox" id="hdri-live-update" onchange="window.setHDRILiveUpdate(this.checked)">
<label for="hdri-live-update" style="font-size: 10px; color: #aaa;">Live Update</label>
</div>
</div>
<script type="module">
// Handle file upload
document.getElementById('fileInput').addEventListener('change', async (event) => {
const file = event.target.files[0];
if (!file) return;
document.getElementById('loadingIndicator').classList.add('active');
document.getElementById('currentFile').textContent = file.name;
const customEvent = new CustomEvent('loadUSDFile', { detail: { file } });
window.dispatchEvent(customEvent);
event.target.value = '';
});
// Drag and drop handling
const dropZone = document.getElementById('drop-zone');
// Only handle drag events that contain files (external drag)
function hasFiles(e) {
return e.dataTransfer && e.dataTransfer.types && e.dataTransfer.types.includes('Files');
}
document.body.addEventListener('dragenter', (e) => {
if (hasFiles(e)) {
e.preventDefault();
e.stopPropagation();
dropZone.classList.add('active');
}
});
document.body.addEventListener('dragover', (e) => {
if (hasFiles(e)) {
e.preventDefault();
e.stopPropagation();
}
});
document.body.addEventListener('dragleave', (e) => {
// Only handle if it's a file drag leaving the window
if (hasFiles(e) && e.relatedTarget === null) {
dropZone.classList.remove('active');
}
});
dropZone.addEventListener('dragleave', (e) => {
if (e.target === dropZone) {
dropZone.classList.remove('active');
}
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
dropZone.classList.remove('active');
const files = e.dataTransfer.files;
if (files.length > 0) {
const file = files[0];
const ext = file.name.toLowerCase().split('.').pop();
if (['usd', 'usda', 'usdc', 'usdz'].includes(ext)) {
document.getElementById('loadingIndicator').classList.add('active');
document.getElementById('currentFile').textContent = file.name;
const customEvent = new CustomEvent('loadUSDFile', { detail: { file } });
window.dispatchEvent(customEvent);
} else {
alert('Please drop a USD file (.usd, .usda, .usdc, .usdz)');
}
}
});
// Global functions
window.hideLoadingIndicator = function() {
document.getElementById('loadingIndicator').classList.remove('active');
};
// Selection mode UI functions
window.setSelectionModeUI = function(mode) {
// Update buttons
document.getElementById('mode-all').classList.toggle('active', mode === 'all');
document.getElementById('mode-meshes').classList.toggle('active', mode === 'meshes');
document.getElementById('mode-lights').classList.toggle('active', mode === 'lights');
// Update the actual selection mode in the 3D scene
if (window.setSelectionMode) {
window.setSelectionMode(mode);
}
};
window.updateSelectionModeUI = function(mode) {
// Just update buttons (called from keyboard shortcuts)
document.getElementById('mode-all').classList.toggle('active', mode === 'all');
document.getElementById('mode-meshes').classList.toggle('active', mode === 'meshes');
document.getElementById('mode-lights').classList.toggle('active', mode === 'lights');
};
// Lighting mode UI functions
window.setLightingModeUI = function(mode) {
// Update buttons
const lightsBtn = document.getElementById('lighting-mode-lights');
const envmapBtn = document.getElementById('lighting-mode-envmap');
const infoEl = document.getElementById('lighting-mode-info');
lightsBtn.classList.toggle('active', mode === 'lights');
envmapBtn.classList.toggle('active', mode === 'envmap');
// Update info text
if (mode === 'lights') {
infoEl.textContent = 'Direct lighting from scene lights';
} else {
infoEl.textContent = 'Using projected HDRI environment';
}
// Call the actual mode setter
if (window.setLightingMode) {
window.setLightingMode(mode);
}
};
window.updateLightingModeUI = function(mode) {
// Just update UI (called from JS when mode changes)
const lightsBtn = document.getElementById('lighting-mode-lights');
const envmapBtn = document.getElementById('lighting-mode-envmap');
const infoEl = document.getElementById('lighting-mode-info');
if (lightsBtn) lightsBtn.classList.toggle('active', mode === 'lights');
if (envmapBtn) envmapBtn.classList.toggle('active', mode === 'envmap');
if (infoEl) {
if (mode === 'lights') {
infoEl.textContent = 'Direct lighting from scene lights';
} else {
infoEl.textContent = 'Using projected HDRI environment';
}
}
};
window.updateLightList = function(lights) {
const lightList = document.getElementById('lightList');
const lightCount = document.getElementById('lightCount');
const statLights = document.getElementById('statLights');
lightCount.textContent = lights.length;
statLights.textContent = lights.length;
if (lights.length === 0) {
lightList.innerHTML = '<p style="color: #666; font-style: italic;">No lights loaded</p>';
return;
}
lightList.innerHTML = '';
lights.forEach((light, index) => {
const item = document.createElement('div');
item.className = 'light-item';
item.dataset.index = index;
const colorHex = light.color ?
`#${Math.round(light.color[0] * 255).toString(16).padStart(2, '0')}${Math.round(light.color[1] * 255).toString(16).padStart(2, '0')}${Math.round(light.color[2] * 255).toString(16).padStart(2, '0')}`
: '#ffffff';
let details = `Intensity: ${(light.intensity || 1).toFixed(2)}`;
if (light.exposure && light.exposure !== 0) {
details += ` | Exp: ${light.exposure.toFixed(1)} EV`;
}
if (light.radius && light.type !== 'distant' && light.type !== 'dome') {
details += ` | R: ${light.radius.toFixed(2)}`;
}
if (light.type === 'rect' && light.width && light.height) {
details += ` | ${light.width.toFixed(1)}x${light.height.toFixed(1)}`;
}
if (light.shapingConeAngle && light.shapingConeAngle < 90) {
details += ` | Cone: ${light.shapingConeAngle.toFixed(0)}`;
}
// Spectral indicator
const hasSpectral = light.spectralEmission &&
((light.spectralEmission.samples && light.spectralEmission.samples.length > 0) ||
(light.spectralEmission.preset && light.spectralEmission.preset !== 'none'));
const spectralBadge = hasSpectral ?
'<span style="background: linear-gradient(90deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #8b00ff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-size: 9px; margin-left: 5px;">SPD</span>' : '';
// Check if light is enabled (default to true)
const isEnabled = light.enabled !== false;
item.innerHTML = `
<button class="light-toggle ${isEnabled ? 'on' : ''}" data-index="${index}" title="Toggle light on/off"></button>
<div class="light-name">
<span class="light-color-swatch" style="background: ${colorHex}"></span>
${light.name || 'Light ' + index}
<span class="light-type">${light.type || 'unknown'}</span>
${spectralBadge}
</div>
<div class="light-details">${details}</div>
`;
if (!isEnabled) {
item.classList.add('disabled');
}
// Toggle button click handler
const toggleBtn = item.querySelector('.light-toggle');
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent item selection
const newState = window.toggleLight(index);
if (newState) {
toggleBtn.classList.add('on');
item.classList.remove('disabled');
} else {
toggleBtn.classList.remove('on');
item.classList.add('disabled');
}
});
// Item click handler (for selection)
item.addEventListener('click', (e) => {
// Ignore if clicking the toggle button
if (e.target.classList.contains('light-toggle')) return;
// Remove selected from all items
lightList.querySelectorAll('.light-item').forEach(i => i.classList.remove('selected'));
item.classList.add('selected');
// Focus camera on this light
window.focusOnLight(index);
// Select light in 3D scene with transform controls
if (window.selectLight3D) {
window.selectLight3D(index);
}
// Select light for spectral display
if (window.selectLightForSpectral) {
window.selectLightForSpectral(index);
}
});
lightList.appendChild(item);
});
// Function to update toggle states (called when setAllLightsEnabled is used)
window.updateLightListStates = function() {
lightList.querySelectorAll('.light-item').forEach((item, index) => {
const toggleBtn = item.querySelector('.light-toggle');
const isEnabled = window.isLightEnabled ? window.isLightEnabled(index) : true;
if (isEnabled) {
toggleBtn.classList.add('on');
item.classList.remove('disabled');
} else {
toggleBtn.classList.remove('on');
item.classList.add('disabled');
}
});
};
};
// Highlight a light in the list (called from 3D scene selection)
window.highlightLightInList = function(index) {
const lightList = document.getElementById('lightList');
if (!lightList) return;
// Remove selected from all items
lightList.querySelectorAll('.light-item').forEach(item => {
item.classList.remove('selected');
});
// Add selected to the target item
const items = lightList.querySelectorAll('.light-item');
if (index >= 0 && index < items.length) {
items[index].classList.add('selected');
// Scroll the item into view
items[index].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
};
// Embedded scene selector
document.getElementById('embedded-select').addEventListener('change', (e) => {
window.loadEmbeddedScene(e.target.value);
});
// Object visibility toggles
const meshCheckboxes = ['toggle-ground', 'toggle-sphere', 'toggle-torus', 'toggle-box'];
// All Meshes toggle
document.getElementById('toggle-all-meshes').addEventListener('change', (e) => {
const visible = e.target.checked;
meshCheckboxes.forEach(id => {
const checkbox = document.getElementById(id);
checkbox.checked = visible;
});
if (window.setAllMeshesVisible) {
window.setAllMeshesVisible(visible);
}
});
// Update "All Meshes" checkbox when individual mesh toggles change
function updateAllMeshesCheckbox() {
const allChecked = meshCheckboxes.every(id => document.getElementById(id).checked);
const someChecked = meshCheckboxes.some(id => document.getElementById(id).checked);
const allMeshesCheckbox = document.getElementById('toggle-all-meshes');
allMeshesCheckbox.checked = allChecked;
allMeshesCheckbox.indeterminate = someChecked && !allChecked;
}
document.getElementById('toggle-ground').addEventListener('change', (e) => {
if (window.setObjectVisible) window.setObjectVisible('ground', e.target.checked);
updateAllMeshesCheckbox();
});
document.getElementById('toggle-grid').addEventListener('change', (e) => {
if (window.setObjectVisible) window.setObjectVisible('grid', e.target.checked);
});
document.getElementById('toggle-sphere').addEventListener('change', (e) => {
if (window.setObjectVisible) window.setObjectVisible('sphere', e.target.checked);
updateAllMeshesCheckbox();
});
document.getElementById('toggle-torus').addEventListener('change', (e) => {
if (window.setObjectVisible) window.setObjectVisible('torus', e.target.checked);
updateAllMeshesCheckbox();
});
document.getElementById('toggle-box').addEventListener('change', (e) => {
if (window.setObjectVisible) window.setObjectVisible('box', e.target.checked);
updateAllMeshesCheckbox();
});
document.getElementById('toggle-helpers').addEventListener('change', (e) => {
if (window.setObjectVisible) window.setObjectVisible('helpers', e.target.checked);
});
// ============================================
// Light Properties Editor
// ============================================
// Color picker
document.getElementById('light-color-picker').addEventListener('input', (e) => {
const hex = e.target.value;
document.getElementById('light-color-value').textContent = hex;
if (window.setSelectedLightColor) {
// Convert hex to RGB [0-1]
const r = parseInt(hex.substr(1, 2), 16) / 255;
const g = parseInt(hex.substr(3, 2), 16) / 255;
const b = parseInt(hex.substr(5, 2), 16) / 255;
window.setSelectedLightColor(r, g, b);
}
});
// Intensity slider
document.getElementById('light-intensity-slider').addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
document.getElementById('light-intensity-input').value = value;
if (window.setSelectedLightIntensity) {
window.setSelectedLightIntensity(value);
}
});
// Intensity input
document.getElementById('light-intensity-input').addEventListener('input', (e) => {
const value = parseFloat(e.target.value) || 0;
// Clamp slider value to slider max
const sliderMax = parseFloat(document.getElementById('light-intensity-slider').max);
document.getElementById('light-intensity-slider').value = Math.min(value, sliderMax);
if (window.setSelectedLightIntensity) {
window.setSelectedLightIntensity(value);
}
});
// Exposure slider
document.getElementById('light-exposure-slider').addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
document.getElementById('light-exposure-input').value = value;
if (window.setSelectedLightExposure) {
window.setSelectedLightExposure(value);
}
});
// Exposure input
document.getElementById('light-exposure-input').addEventListener('input', (e) => {
const value = parseFloat(e.target.value) || 0;
document.getElementById('light-exposure-slider').value = Math.max(-10, Math.min(10, value));
if (window.setSelectedLightExposure) {
window.setSelectedLightExposure(value);
}
});
// Position inputs
['x', 'y', 'z'].forEach(axis => {
document.getElementById(`light-pos-${axis}`).addEventListener('input', (e) => {
const value = parseFloat(e.target.value) || 0;
if (window.setSelectedLightPosition) {
const x = parseFloat(document.getElementById('light-pos-x').value) || 0;
const y = parseFloat(document.getElementById('light-pos-y').value) || 0;
const z = parseFloat(document.getElementById('light-pos-z').value) || 0;
window.setSelectedLightPosition(x, y, z);
}
});
});
// Rotation inputs (in degrees)
['x', 'y', 'z'].forEach(axis => {
document.getElementById(`light-rot-${axis}`).addEventListener('input', (e) => {
const value = parseFloat(e.target.value) || 0;
if (window.setSelectedLightRotation) {
const x = parseFloat(document.getElementById('light-rot-x').value) || 0;
const y = parseFloat(document.getElementById('light-rot-y').value) || 0;
const z = parseFloat(document.getElementById('light-rot-z').value) || 0;
window.setSelectedLightRotation(x, y, z);
}
});
});
// Cone angle slider
document.getElementById('light-cone-angle-slider').addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
document.getElementById('light-cone-angle-input').value = value;
if (window.setSelectedLightConeAngle) {
window.setSelectedLightConeAngle(value);
}
});
// Cone angle input
document.getElementById('light-cone-angle-input').addEventListener('input', (e) => {
const value = parseFloat(e.target.value) || 90;
document.getElementById('light-cone-angle-slider').value = Math.max(1, Math.min(180, value));
if (window.setSelectedLightConeAngle) {
window.setSelectedLightConeAngle(value);
}
});
// Cone softness slider
document.getElementById('light-cone-softness-slider').addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
document.getElementById('light-cone-softness-input').value = value;
if (window.setSelectedLightConeSoftness) {
window.setSelectedLightConeSoftness(value);
}
});
// Cone softness input
document.getElementById('light-cone-softness-input').addEventListener('input', (e) => {
const value = parseFloat(e.target.value) || 0;
document.getElementById('light-cone-softness-slider').value = Math.max(0, Math.min(1, value));
if (window.setSelectedLightConeSoftness) {
window.setSelectedLightConeSoftness(value);
}
});
// Transform mode UI handler
window.setLightTransformModeUI = function(mode) {
// Update button states
document.getElementById('light-mode-pos').classList.toggle('active', mode === 'translate');
document.getElementById('light-mode-rot').classList.toggle('active', mode === 'rotate');
document.getElementById('light-mode-scale').classList.toggle('active', mode === 'scale');
document.getElementById('light-mode-target').classList.toggle('active', mode === 'target');
// Call the actual mode setter
if (window.setLightTransformMode) {
window.setLightTransformMode(mode);
}
};
// Update transform mode UI from keyboard shortcuts
window.updateLightTransformModeUI = function(mode) {
document.getElementById('light-mode-pos').classList.toggle('active', mode === 'translate');
document.getElementById('light-mode-rot').classList.toggle('active', mode === 'rotate');
document.getElementById('light-mode-scale').classList.toggle('active', mode === 'scale');
document.getElementById('light-mode-target').classList.toggle('active', mode === 'target');
};
// Callback to show/hide light properties panel and update values
window.showLightProperties = function(lightData) {
const panel = document.getElementById('light-properties');
if (!lightData) {
panel.classList.remove('visible');
return;
}
panel.classList.add('visible');
// Update light name
const nameEl = document.getElementById('selected-light-name');
nameEl.textContent = `${lightData.name || 'Unnamed'} (${lightData.type || 'unknown'})`;
// Update color picker
const color = lightData.color || [1, 1, 1];
const hex = '#' +
Math.round(color[0] * 255).toString(16).padStart(2, '0') +
Math.round(color[1] * 255).toString(16).padStart(2, '0') +
Math.round(color[2] * 255).toString(16).padStart(2, '0');
document.getElementById('light-color-picker').value = hex;
document.getElementById('light-color-value').textContent = hex;
// Update intensity
const intensity = lightData.intensity || 1;
document.getElementById('light-intensity-slider').value = Math.min(intensity, 100);
document.getElementById('light-intensity-input').value = intensity;
// Update exposure
const exposure = lightData.exposure || 0;
document.getElementById('light-exposure-slider').value = Math.max(-10, Math.min(10, exposure));
document.getElementById('light-exposure-input').value = exposure;
// Determine light type for showing/hiding controls
const lightType = lightData.type || 'point';
const isInfiniteLight = (lightType === 'distant' || lightType === 'dome');
// Check both coneAngle and shapingConeAngle (USD uses shapingConeAngle as half-angle)
const coneAngle = lightData.coneAngle !== undefined ? lightData.coneAngle :
(lightData.shapingConeAngle !== undefined ? lightData.shapingConeAngle * 2 : undefined);
const hasShaping = coneAngle !== undefined && coneAngle < 180;
// Show/hide position controls (hidden for infinite lights)
const posRow = document.getElementById('light-position-row');
posRow.style.display = isInfiniteLight ? 'none' : 'flex';
// Update position values
const pos = lightData.position || [0, 0, 0];
document.getElementById('light-pos-x').value = pos[0]?.toFixed(2) || 0;
document.getElementById('light-pos-y').value = pos[1]?.toFixed(2) || 0;
document.getElementById('light-pos-z').value = pos[2]?.toFixed(2) || 0;
// Update rotation values (convert from radians to degrees if needed)
const rot = lightData.rotation || [0, 0, 0];
// Assume rotation is stored in degrees
document.getElementById('light-rot-x').value = Math.round(rot[0] || 0);
document.getElementById('light-rot-y').value = Math.round(rot[1] || 0);
document.getElementById('light-rot-z').value = Math.round(rot[2] || 0);
// Show/hide shaping controls (only for SpotLight with cone shaping)
const shapingSection = document.getElementById('light-shaping-section');
if (hasShaping) {
shapingSection.classList.add('visible');
// Update cone angle (use coneAngle computed above, or fall back to 90)
const displayConeAngle = coneAngle || 90;
document.getElementById('light-cone-angle-slider').value = Math.max(1, Math.min(180, displayConeAngle));
document.getElementById('light-cone-angle-input').value = displayConeAngle.toFixed(0);
// Update cone softness (check both property names)
const coneSoftness = lightData.coneSoftness !== undefined ? lightData.coneSoftness :
(lightData.shapingConeSoftness || 0);
document.getElementById('light-cone-softness-slider').value = Math.max(0, Math.min(1, coneSoftness));
document.getElementById('light-cone-softness-input').value = coneSoftness.toFixed(2);
} else {
shapingSection.classList.remove('visible');
}
// Show/hide Target button (only for SpotLights and DirectionalLights)
const targetBtn = document.getElementById('light-mode-target');
const hasTarget = hasShaping || lightType === 'distant';
targetBtn.style.display = hasTarget ? 'inline-block' : 'none';
// Reset transform mode UI to translate (default)
document.getElementById('light-mode-pos').classList.add('active');
document.getElementById('light-mode-rot').classList.remove('active');
document.getElementById('light-mode-scale').classList.remove('active');
document.getElementById('light-mode-target').classList.remove('active');
};
// Hide light properties when no light selected
window.hideLightProperties = function() {
document.getElementById('light-properties').classList.remove('visible');
};
// Tone mapping descriptions
const tonemapDescriptions = {
'raw': 'No tone mapping - linear HDR values clamped',
'reinhard': 'Simple curve, preserves detail in highlights',
'aces1': 'ACES 1.3 - Film-like response with highlight roll-off',
'aces2': 'ACES 2.0 - Improved gamut mapping and desaturation',
'agx': 'AgX - Modern filmic look, similar to Blender',
'neutral': 'Neutral - Balanced tone curve for general use'
};
// Tone mapping selector
document.getElementById('tonemap-select').addEventListener('change', (e) => {
const value = e.target.value;
window.setToneMapping(value);
document.getElementById('tonemap-info').textContent = tonemapDescriptions[value] || '';
});
// Exposure slider
document.getElementById('exposure-slider').addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
document.getElementById('exposure-value').textContent = value.toFixed(1) + ' EV';
window.setExposure(value);
});
// Gamma slider
document.getElementById('gamma-slider').addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
document.getElementById('gamma-value').textContent = value.toFixed(1);
window.setGamma(value);
});
// Spectral mode selector
document.getElementById('spectral-mode-select').addEventListener('change', (e) => {
const mode = e.target.value;
window.setSpectralMode(mode);
// Show/hide monochrome controls
const monoControls = document.getElementById('monochrome-controls');
if (mode === 'monochrome') {
monoControls.classList.add('visible');
updateWavelengthPreview(parseInt(document.getElementById('wavelength-slider').value));
} else {
monoControls.classList.remove('visible');
}
});
// Wavelength slider for monochrome mode
document.getElementById('wavelength-slider').addEventListener('input', (e) => {
const wavelength = parseInt(e.target.value);
document.getElementById('wavelength-value').textContent = wavelength + 'nm';
updateWavelengthPreview(wavelength);
window.setMonochromeWavelength(wavelength);
});
// Update wavelength preview color
function updateWavelengthPreview(wavelength) {
const rgb = window.wavelengthToRGB ? window.wavelengthToRGB(wavelength) : { r: 0.5, g: 0.5, b: 0.5 };
const preview = document.getElementById('wavelength-preview');
if (preview) {
preview.style.backgroundColor = `rgb(${Math.round(rgb.r * 255)}, ${Math.round(rgb.g * 255)}, ${Math.round(rgb.b * 255)})`;
}
}
// Initialize wavelength preview
setTimeout(() => {
updateWavelengthPreview(550);
}, 500);
// Blackbody temperature slider
document.getElementById('blackbody-slider').addEventListener('input', (e) => {
const temp = parseInt(e.target.value);
document.getElementById('blackbody-value').textContent = temp + 'K';
});
// Apply blackbody on slider change end
document.getElementById('blackbody-slider').addEventListener('change', (e) => {
const temp = parseInt(e.target.value);
if (window.applyBlackbodyToSelected) {
window.applyBlackbodyToSelected(temp);
}
});
// ============================================
// HDRI Projection UI Handlers
// ============================================
// Track HDRI state
let hdriApplied = false;
// HDRI Resolution selector
document.getElementById('hdri-resolution-select').addEventListener('change', (e) => {
const width = parseInt(e.target.value);
if (window.setHDRIResolution) {
window.setHDRIResolution(width);
}
});
// HDRI Max Distance slider
document.getElementById('hdri-maxdist-slider').addEventListener('input', (e) => {
const value = parseInt(e.target.value);
document.getElementById('hdri-maxdist-value').textContent = value;
});
document.getElementById('hdri-maxdist-slider').addEventListener('change', (e) => {
const value = parseInt(e.target.value);
if (window.setHDRIMaxDistance) {
window.setHDRIMaxDistance(value);
}
// Update range indicator size
if (window.updateHDRIRangeIndicator) {
window.updateHDRIRangeIndicator(value);
}
});
// HDRI Position inputs
const posXInput = document.getElementById('hdri-pos-x');
const posYInput = document.getElementById('hdri-pos-y');
const posZInput = document.getElementById('hdri-pos-z');
function updatePositionFromInputs() {
const x = parseFloat(posXInput.value) || 0;
const y = parseFloat(posYInput.value) || 0;
const z = parseFloat(posZInput.value) || 0;
if (window.setHDRILocatorPosition) {
window.setHDRILocatorPosition(x, y, z);
}
}
posXInput.addEventListener('change', updatePositionFromInputs);
posYInput.addEventListener('change', updatePositionFromInputs);
posZInput.addEventListener('change', updatePositionFromInputs);
// Callback to update position UI from locator gizmo changes
window.updateHDRIPositionUI = function(pos) {
posXInput.value = pos.x.toFixed(2);
posYInput.value = pos.y.toFixed(2);
posZInput.value = pos.z.toFixed(2);
};
// HDRI Locator toggle
let locatorVisible = false;
window.toggleLocatorBtn = function() {
const btn = document.getElementById('hdri-locator-btn');
if (window.toggleHDRILocator) {
locatorVisible = window.toggleHDRILocator();
if (locatorVisible) {
btn.classList.add('active');
btn.textContent = 'Hide Gizmo';
} else {
btn.classList.remove('active');
btn.textContent = 'Show Gizmo';
}
}
};
// Toggle HDRI apply to scene
window.toggleHDRIApply = function() {
if (hdriApplied) {
if (window.removeHDRIFromScene) {
window.removeHDRIFromScene();
}
hdriApplied = false;
} else {
if (window.applyHDRIToScene) {
window.applyHDRIToScene();
}
hdriApplied = true;
}
updateHDRIApplyButtons();
};
function updateHDRIApplyButtons() {
const btns = document.querySelectorAll('.hdri-apply');
btns.forEach(btn => {
if (hdriApplied) {
btn.classList.add('active');
btn.textContent = 'Remove from Scene';
} else {
btn.classList.remove('active');
btn.textContent = 'Apply to Scene';
}
});
}
// HDRI Status update callback
window.updateHDRIStatus = function(status) {
const statusEl = document.getElementById('hdri-status');
const previewSize = document.getElementById('hdri-preview-size');
const previewRange = document.getElementById('hdri-preview-range');
if (status.generated) {
statusEl.classList.add('has-hdri');
statusEl.innerHTML = `
<div><strong>Generated:</strong> ${status.width} x ${status.height}</div>
<div><strong>Lights:</strong> ${status.lights}</div>
<div><strong>Max value:</strong> ${status.maxValue.toFixed(2)}</div>
`;
previewSize.textContent = `${status.width} x ${status.height}`;
previewRange.textContent = `Max: ${status.maxValue.toFixed(2)}`;
}
if (status.applied !== undefined) {
hdriApplied = status.applied;
updateHDRIApplyButtons();
}
};
// HDRI Preview visibility callback
window.setHDRIPreviewVisible = function(visible) {
const panel = document.getElementById('hdri-preview-panel');
if (visible) {
panel.classList.add('visible');
} else {
panel.classList.remove('visible');
}
};
// Store current exposure for HDRI preview
window.currentExposure = 0;
const originalSetExposure = window.setExposure;
if (typeof originalSetExposure === 'function') {
window.setExposure = function(value) {
window.currentExposure = value;
originalSetExposure(value);
// Update HDRI preview if visible
if (document.getElementById('hdri-preview-panel').classList.contains('visible')) {
if (window.updateHDRIPreviewCanvas) {
window.updateHDRIPreviewCanvas();
}
}
};
}
</script>
<script type="module" src="./usdlux.js"></script>
</body>
</html>