src/solar-system/blocks/scale-block.js
// src/solar-system/blocks/scale-block.js
//
// SCALE block — four sliders: body size, distance, exaggeration, lunarOrbit.
//
// mountScaleBlock(target, ctx) → teardown
// - target: an HTMLElement that will receive a `.section-block` child
// - ctx: panelCtx (must expose `onScaleChange` and either
// `scaleSetters.setScale` or be permissive — falls back to the
// module-local setter)
// - returns a teardown function that removes subscribers and clears the target
//
// The block is responsible for its own DOM, listeners, and teardown. Each
// slider is a 0-100 range mapped to the underlying scaleState domain
// ([0, 1] for size/distance, [0, 2] for exaggeration); the block reads
// scaleState directly on mount and re-syncs on every onScaleChange so a
// voyage that drives setScale(...) keeps the sliders in lock-step.
import { t, onLangChange } from '../../i18n.js';
import { scaleState, setScale, onScaleChange } from '../state/scale.js';
const SLIDERS = [
{ axis: 'size', i18n: 'solarSystem.blocks.scale.size', range: 100 },
{ axis: 'distance', i18n: 'solarSystem.blocks.scale.distance', range: 100 },
// Exaggeration domain is [0, 2]; the slider stays 0-200 so each tick
// is one centi-unit of exaggeration (matches the legacy slider mapping).
{ axis: 'exaggeration', i18n: 'solarSystem.blocks.scale.exaggeration', range: 200 },
// Lunar-orbit domain is [0, 2] (slider 0-200), decoupled from `distance`:
// 0 = compressed moon orbits (default), 1 = true proportional, 2 = beyond.
{ axis: 'lunarOrbit', i18n: 'solarSystem.blocks.scale.lunarOrbit', range: 200 },
];
export function mountScaleBlock(target, ctx = {}) {
const setScaleFn =
ctx?.scaleSetters?.setScale ??
((delta, opts) => setScale(delta, { duration: 0, source: 'panel', ...opts }));
const subscribe = ctx?.onScaleChange ?? onScaleChange;
const root = document.createElement('div');
root.classList.add('section-block', 'scale-block');
const header = document.createElement('h3');
header.classList.add('section-block__header');
header.setAttribute('data-i18n', 'solarSystem.blocks.scale.title');
header.textContent = t('solarSystem.blocks.scale.title') || 'ÉCHELLE';
const body = document.createElement('div');
body.classList.add('section-block__body', 'scale-block__body');
const sliderWraps = []; // { axis, label, input, range }
for (const s of SLIDERS) {
const wrap = document.createElement('div');
wrap.classList.add('scale-block__slider');
wrap.dataset.axis = s.axis;
const label = document.createElement('label');
label.classList.add('scale-block__label');
label.setAttribute('data-i18n', s.i18n);
label.textContent = t(s.i18n);
const input = document.createElement('input');
input.type = 'range';
input.min = '0';
input.max = String(s.range);
input.step = '1';
input.dataset.axis = s.axis;
input.value = String(Math.round((scaleState.global[s.axis] ?? 0) * 100));
input.addEventListener('input', () => {
const value = Number(input.value) / 100;
setScaleFn({ [s.axis]: value }, { duration: 0, source: 'panel' });
});
wrap.appendChild(label);
wrap.appendChild(input);
body.appendChild(wrap);
sliderWraps.push({ axis: s.axis, label, input, i18n: s.i18n });
}
root.append(header, body);
target.appendChild(root);
function refresh() {
for (const sw of sliderWraps) {
const next = String(Math.round((scaleState.global[sw.axis] ?? 0) * 100));
// Don't override the slider while the user is dragging it (mirrors
// dev-panel's _scaleCard pattern).
if (document.activeElement !== sw.input && sw.input.value !== next) {
sw.input.value = next;
}
}
}
const offScale = subscribe(refresh);
const offLang = onLangChange(() => {
header.textContent = t('solarSystem.blocks.scale.title') || 'ÉCHELLE';
for (const sw of sliderWraps) sw.label.textContent = t(sw.i18n);
});
return () => {
try {
offScale?.();
} catch {
// best-effort
}
try {
offLang?.();
} catch {
// best-effort
}
target.replaceChildren();
};
}
← Astrarium