From 951cb26f3f52fa2420b32eadc2d5c99322a5896a Mon Sep 17 00:00:00 2001 From: minecraft-clone Date: Sun, 21 Jun 2026 18:24:35 +0200 Subject: [PATCH] feat: dirty-Markierung + Standard-Hinweis + Bloom 0.01-Schritte im Panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Werte abweichend vom Standard werden amber markiert - klickbarer Hinweis '↺ ' zeigt Originalwert (Bugfix: _defaultEl wurde nie zugewiesen) - Bloom-Schwelle jetzt in 0.01-Schritten einstellbar Co-Authored-By: Claude --- js/panel.js | 102 ++++++++++++++++++++++++++++++++++++++++++---------- style.css | 15 +++++++- 2 files changed, 98 insertions(+), 19 deletions(-) diff --git a/js/panel.js b/js/panel.js index 8bee175..071ec6c 100644 --- a/js/panel.js +++ b/js/panel.js @@ -14,7 +14,7 @@ // ============================================================ import * as THREE from 'three'; 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) ---------- const applyFns = Object.create(null); @@ -141,7 +141,7 @@ const SCHEMA = [ { 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.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(); } if (!skipDisplay && item._valueEl) item._valueEl.textContent = fmt(item, value); + updateDirty(item); } function markReloadNeeded() { @@ -225,9 +226,57 @@ function fmt(item, value) { return String(value); } -// ---------- Wert eines Controls aus config lesen ---------- -function currentValue(item) { - return getPath(CFG, item.path); +// ---------- Effektiver Wert (Override aus settings, sonst config = Default) ---------- +// Für Reload-Werte, die erst beim Neustart greifen, steht die letzte +// Ä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 ---------- @@ -240,28 +289,41 @@ function el(tag, cls, html) { function buildControl(item) { const row = el('div', 'ctrl-row'); + item._row = row; + const head = el('div', 'ctrl-head'); const label = el('span', 'ctrl-label', item.label); - const badge = el('span', 'badge ' + (item.live === false || item.groupReload ? 'reload' : 'live'), - item.live === false || item.groupReload ? '⟳' : '●'); - badge.title = item.live === false || item.groupReload ? 'Braucht Neustart' : 'Greift sofort'; - head.append(label, badge); + // Klickbarer Standard-Hinweis (nur sichtbar, wenn der Wert abweicht). + const defSpan = el('span', 'ctrl-default'); + defSpan.title = 'Auf Standard zurücksetzen'; + 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); const val = el('span', 'ctrl-value'); - item._valueEl = val; + const numSpan = el('span', 'cv-num'); + val.append(numSpan); + item._valueEl = numSpan; let input; if (item.type === 'checkbox') { input = el('label', 'switch'); const cb = document.createElement('input'); cb.type = 'checkbox'; - cb.checked = !!currentValue(item); + cb.checked = !!effectiveValue(item); cb.addEventListener('change', () => onControlChange(item, cb.checked)); const slider = el('span', 'switch-track'); input.append(cb, slider); - val.textContent = cb.checked ? 'an' : 'aus'; + item._input = cb; + numSpan.textContent = cb.checked ? 'an' : 'aus'; row.append(input, val); + updateDirty(item); return row; } @@ -273,34 +335,38 @@ function buildControl(item) { opt.textContent = o.label; input.append(opt); }); - input.value = String(currentValue(item)); + input.value = String(effectiveValue(item)); 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); + updateDirty(item); return row; } input = document.createElement('input'); input.type = item.type; - const cv = currentValue(item); + const cv = effectiveValue(item); if (item.type === 'color') { input.value = toHex(cv); input.addEventListener('input', () => onControlChange(item, fromHex(input.value))); - val.textContent = toHex(cv); + numSpan.textContent = toHex(cv); } else if (item.type === 'number') { if (item.min != null) input.min = item.min; if (item.max != null) input.max = item.max; if (item.step != null) input.step = item.step; input.value = cv; input.addEventListener('change', () => onControlChange(item, Number(input.value))); - val.textContent = fmt(item, Number(cv)); + numSpan.textContent = fmt(item, Number(cv)); } else { // range input.min = item.min; input.max = item.max; input.step = item.step; input.value = cv; 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); + updateDirty(item); return row; } diff --git a/style.css b/style.css index 81089a1..8899e60 100644 --- a/style.css +++ b/style.css @@ -84,7 +84,8 @@ canvas { display: block; } #overlay { position: fixed; 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; align-items: 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.live { background: rgba(80,200,120,0.18); color: #7ee2a0; } .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 */ .ctrl-row input[type=range] {