feat: dirty-Markierung + Standard-Hinweis + Bloom 0.01-Schritte im Panel
- Werte abweichend vom Standard werden amber markiert - klickbarer Hinweis '↺ <Standard>' zeigt Originalwert (Bugfix: _defaultEl wurde nie zugewiesen) - Bloom-Schwelle jetzt in 0.01-Schritten einstellbar Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
6e8e207afa
commit
951cb26f3f
2 files changed
+98
-19
No files matched your search
+84
-18
@@ -14,7 +14,7 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import * as CFG from './config.js';
|
import * as CFG from './config.js';
|
||||||
import { loadSettings, saveSettings, clearSettings } from './config.js';
|
import { loadSettings, saveSettings, clearSettings, DEFAULTS } from './config.js';
|
||||||
|
|
||||||
// ---------- Apply-Registry (von main.js gefüllt) ----------
|
// ---------- Apply-Registry (von main.js gefüllt) ----------
|
||||||
const applyFns = Object.create(null);
|
const applyFns = Object.create(null);
|
||||||
@@ -141,7 +141,7 @@ const SCHEMA = [
|
|||||||
{ path: 'BLOOM_ENABLED', label: 'Bloom an', type: 'checkbox', live: true },
|
{ path: 'BLOOM_ENABLED', label: 'Bloom an', type: 'checkbox', live: true },
|
||||||
{ path: 'BLOOM.strength', label: 'Bloom-Stärke', type: 'range', min: 0, max: 2, step: 0.05, live: true },
|
{ path: 'BLOOM.strength', label: 'Bloom-Stärke', type: 'range', min: 0, max: 2, step: 0.05, live: true },
|
||||||
{ path: 'BLOOM.radius', label: 'Bloom-Radius', type: 'range', min: 0, max: 2, step: 0.05, live: true },
|
{ path: 'BLOOM.radius', label: 'Bloom-Radius', type: 'range', min: 0, max: 2, step: 0.05, live: true },
|
||||||
{ path: 'BLOOM.threshold', label: 'Bloom-Schwelle', type: 'range', min: 0, max: 2, step: 0.05, live: true },
|
{ path: 'BLOOM.threshold', label: 'Bloom-Schwelle', type: 'range', min: 0, max: 2, step: 0.01, live: true },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -202,6 +202,7 @@ function onControlChange(item, value, skipDisplay) {
|
|||||||
markReloadNeeded();
|
markReloadNeeded();
|
||||||
}
|
}
|
||||||
if (!skipDisplay && item._valueEl) item._valueEl.textContent = fmt(item, value);
|
if (!skipDisplay && item._valueEl) item._valueEl.textContent = fmt(item, value);
|
||||||
|
updateDirty(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
function markReloadNeeded() {
|
function markReloadNeeded() {
|
||||||
@@ -225,9 +226,57 @@ function fmt(item, value) {
|
|||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Wert eines Controls aus config lesen ----------
|
// ---------- Effektiver Wert (Override aus settings, sonst config = Default) ----------
|
||||||
function currentValue(item) {
|
// Für Reload-Werte, die erst beim Neustart greifen, steht die letzte
|
||||||
return getPath(CFG, item.path);
|
// Änderung nur in `settings` (localStorage), nicht im config-Export.
|
||||||
|
// Daher für Anzeige + Dirty-Check immer `settings` zuerst fragen.
|
||||||
|
function effectiveValue(item) {
|
||||||
|
const v = getPath(settings, item.path);
|
||||||
|
return v !== undefined ? v : getPath(CFG, item.path);
|
||||||
|
}
|
||||||
|
// Code-Default (unabhängig von Overrides), aus dem DEFAULTS-Snapshot.
|
||||||
|
function defaultFor(item) {
|
||||||
|
return getPath(DEFAULTS, item.path);
|
||||||
|
}
|
||||||
|
// Gleichheit mit numerischer Toleranz (Slider-Rundungsfehler abfangen,
|
||||||
|
// sonst bliebe ein per Slider auf Default zurückbewegter Wert "dirty").
|
||||||
|
function valueEquals(a, b) {
|
||||||
|
if (typeof a === 'number' && typeof b === 'number') {
|
||||||
|
if (a === b) return true;
|
||||||
|
return Math.abs(a - b) < 1e-6 * Math.max(1, Math.abs(a), Math.abs(b));
|
||||||
|
}
|
||||||
|
return a === b;
|
||||||
|
}
|
||||||
|
// ---------- Dirty-Markierung pro Control ----------
|
||||||
|
// Zeigt an, dass ein Wert vom Standard abweicht: Label + Wert werden
|
||||||
|
// amber, und der klickbare "↺ Standard: X"-Hinweis erscheint.
|
||||||
|
function updateDirty(item) {
|
||||||
|
if (!item._row) return;
|
||||||
|
const dirty = !valueEquals(effectiveValue(item), defaultFor(item));
|
||||||
|
item._row.classList.toggle('dirty', dirty);
|
||||||
|
if (item._defaultEl) {
|
||||||
|
item._defaultEl.hidden = !dirty;
|
||||||
|
item._defaultEl.textContent = '↺ ' + fmt(item, defaultFor(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Auf Default zurücksetzen (Klick auf den Standard-Hinweis).
|
||||||
|
function resetItem(item) {
|
||||||
|
const d = defaultFor(item);
|
||||||
|
const inp = item._input;
|
||||||
|
if (item.type === 'checkbox') {
|
||||||
|
if (inp) inp.checked = !!d;
|
||||||
|
onControlChange(item, !!d);
|
||||||
|
} else if (item.type === 'select') {
|
||||||
|
if (inp) inp.value = String(d);
|
||||||
|
onControlChange(item, Number(d));
|
||||||
|
} else if (item.type === 'color') {
|
||||||
|
if (inp) inp.value = toHex(d);
|
||||||
|
onControlChange(item, d);
|
||||||
|
} else {
|
||||||
|
const v = Number(d);
|
||||||
|
if (inp) inp.value = v;
|
||||||
|
onControlChange(item, v);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- DOM-Aufbau ----------
|
// ---------- DOM-Aufbau ----------
|
||||||
@@ -240,28 +289,41 @@ function el(tag, cls, html) {
|
|||||||
|
|
||||||
function buildControl(item) {
|
function buildControl(item) {
|
||||||
const row = el('div', 'ctrl-row');
|
const row = el('div', 'ctrl-row');
|
||||||
|
item._row = row;
|
||||||
|
|
||||||
const head = el('div', 'ctrl-head');
|
const head = el('div', 'ctrl-head');
|
||||||
const label = el('span', 'ctrl-label', item.label);
|
const label = el('span', 'ctrl-label', item.label);
|
||||||
const badge = el('span', 'badge ' + (item.live === false || item.groupReload ? 'reload' : 'live'),
|
// Klickbarer Standard-Hinweis (nur sichtbar, wenn der Wert abweicht).
|
||||||
item.live === false || item.groupReload ? '⟳' : '●');
|
const defSpan = el('span', 'ctrl-default');
|
||||||
badge.title = item.live === false || item.groupReload ? 'Braucht Neustart' : 'Greift sofort';
|
defSpan.title = 'Auf Standard zurücksetzen';
|
||||||
head.append(label, badge);
|
defSpan.addEventListener('click', (e) => { e.stopPropagation(); resetItem(item); });
|
||||||
|
item._defaultEl = defSpan;
|
||||||
|
const isReload = item.live === false || item.groupReload;
|
||||||
|
const badge = el('span', 'badge ' + (isReload ? 'reload' : 'live'), isReload ? '⟳' : '●');
|
||||||
|
badge.title = isReload ? 'Braucht Neustart' : 'Greift sofort';
|
||||||
|
const right = el('span', 'ctrl-head-right');
|
||||||
|
right.append(defSpan, badge);
|
||||||
|
head.append(label, right);
|
||||||
row.append(head);
|
row.append(head);
|
||||||
|
|
||||||
const val = el('span', 'ctrl-value');
|
const val = el('span', 'ctrl-value');
|
||||||
item._valueEl = val;
|
const numSpan = el('span', 'cv-num');
|
||||||
|
val.append(numSpan);
|
||||||
|
item._valueEl = numSpan;
|
||||||
|
|
||||||
let input;
|
let input;
|
||||||
if (item.type === 'checkbox') {
|
if (item.type === 'checkbox') {
|
||||||
input = el('label', 'switch');
|
input = el('label', 'switch');
|
||||||
const cb = document.createElement('input');
|
const cb = document.createElement('input');
|
||||||
cb.type = 'checkbox';
|
cb.type = 'checkbox';
|
||||||
cb.checked = !!currentValue(item);
|
cb.checked = !!effectiveValue(item);
|
||||||
cb.addEventListener('change', () => onControlChange(item, cb.checked));
|
cb.addEventListener('change', () => onControlChange(item, cb.checked));
|
||||||
const slider = el('span', 'switch-track');
|
const slider = el('span', 'switch-track');
|
||||||
input.append(cb, slider);
|
input.append(cb, slider);
|
||||||
val.textContent = cb.checked ? 'an' : 'aus';
|
item._input = cb;
|
||||||
|
numSpan.textContent = cb.checked ? 'an' : 'aus';
|
||||||
row.append(input, val);
|
row.append(input, val);
|
||||||
|
updateDirty(item);
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,34 +335,38 @@ function buildControl(item) {
|
|||||||
opt.textContent = o.label;
|
opt.textContent = o.label;
|
||||||
input.append(opt);
|
input.append(opt);
|
||||||
});
|
});
|
||||||
input.value = String(currentValue(item));
|
input.value = String(effectiveValue(item));
|
||||||
input.addEventListener('change', () => onControlChange(item, Number(input.value)));
|
input.addEventListener('change', () => onControlChange(item, Number(input.value)));
|
||||||
val.textContent = fmt(item, Number(input.value));
|
item._input = input;
|
||||||
|
numSpan.textContent = fmt(item, Number(input.value));
|
||||||
row.append(input, val);
|
row.append(input, val);
|
||||||
|
updateDirty(item);
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
input = document.createElement('input');
|
input = document.createElement('input');
|
||||||
input.type = item.type;
|
input.type = item.type;
|
||||||
const cv = currentValue(item);
|
const cv = effectiveValue(item);
|
||||||
if (item.type === 'color') {
|
if (item.type === 'color') {
|
||||||
input.value = toHex(cv);
|
input.value = toHex(cv);
|
||||||
input.addEventListener('input', () => onControlChange(item, fromHex(input.value)));
|
input.addEventListener('input', () => onControlChange(item, fromHex(input.value)));
|
||||||
val.textContent = toHex(cv);
|
numSpan.textContent = toHex(cv);
|
||||||
} else if (item.type === 'number') {
|
} else if (item.type === 'number') {
|
||||||
if (item.min != null) input.min = item.min;
|
if (item.min != null) input.min = item.min;
|
||||||
if (item.max != null) input.max = item.max;
|
if (item.max != null) input.max = item.max;
|
||||||
if (item.step != null) input.step = item.step;
|
if (item.step != null) input.step = item.step;
|
||||||
input.value = cv;
|
input.value = cv;
|
||||||
input.addEventListener('change', () => onControlChange(item, Number(input.value)));
|
input.addEventListener('change', () => onControlChange(item, Number(input.value)));
|
||||||
val.textContent = fmt(item, Number(cv));
|
numSpan.textContent = fmt(item, Number(cv));
|
||||||
} else { // range
|
} else { // range
|
||||||
input.min = item.min; input.max = item.max; input.step = item.step;
|
input.min = item.min; input.max = item.max; input.step = item.step;
|
||||||
input.value = cv;
|
input.value = cv;
|
||||||
input.addEventListener('input', () => onControlChange(item, Number(input.value)));
|
input.addEventListener('input', () => onControlChange(item, Number(input.value)));
|
||||||
val.textContent = fmt(item, Number(cv));
|
numSpan.textContent = fmt(item, Number(cv));
|
||||||
}
|
}
|
||||||
|
item._input = input;
|
||||||
row.append(input, val);
|
row.append(input, val);
|
||||||
|
updateDirty(item);
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ canvas { display: block; }
|
|||||||
#overlay {
|
#overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: radial-gradient(circle at 50% 40%, rgba(40,70,110,0.85), rgba(10,20,35,0.92));
|
background: radial-gradient(circle at 50% 50%, #3339 0%, #111c 120%);
|
||||||
|
backdrop-filter: blur(1em);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -202,6 +203,18 @@ body.panel-open #crosshair { opacity: 0; pointer-events: none; transition: opaci
|
|||||||
.badge { font-size: 10px; padding: 1px 5px; border-radius: 8px; margin-left: 6px; vertical-align: middle; }
|
.badge { font-size: 10px; padding: 1px 5px; border-radius: 8px; margin-left: 6px; vertical-align: middle; }
|
||||||
.badge.live { background: rgba(80,200,120,0.18); color: #7ee2a0; }
|
.badge.live { background: rgba(80,200,120,0.18); color: #7ee2a0; }
|
||||||
.badge.reload { background: rgba(255,180,60,0.18); color: #ffc870; }
|
.badge.reload { background: rgba(255,180,60,0.18); color: #ffc870; }
|
||||||
|
.ctrl-head-right { display: inline-flex; align-items: center; gap: 7px; }
|
||||||
|
/* Standard-Wert-Hinweis (nur sichtbar wenn der Wert abweicht = dirty) */
|
||||||
|
.ctrl-default {
|
||||||
|
font-size: 11px; color: #8a92a0; cursor: pointer; white-space: nowrap;
|
||||||
|
padding: 1px 6px; border-radius: 8px; border: 1px solid transparent;
|
||||||
|
transition: color 0.15s, background 0.15s, border-color 0.15s; user-select: none;
|
||||||
|
}
|
||||||
|
.ctrl-default[hidden] { display: none; }
|
||||||
|
.ctrl-default:hover { color: #ffc870; background: rgba(255,180,60,0.12); border-color: rgba(255,180,60,0.35); }
|
||||||
|
/* Dirty-Markierung: Wert weicht vom Standard ab → Label + Wert in amber */
|
||||||
|
.ctrl-row.dirty .ctrl-label { color: #ffc870; }
|
||||||
|
.ctrl-row.dirty .ctrl-value { color: #ffc870; }
|
||||||
|
|
||||||
/* Range-Slider */
|
/* Range-Slider */
|
||||||
.ctrl-row input[type=range] {
|
.ctrl-row input[type=range] {
|
||||||
|
|||||||
Reference in new issue
Block a user