Deduplicate the Gram-Schmidt basis construction that was copy-pasted
in decode_lcs_to_hsl, encode_hsl_to_lcs, and calibration.py. Extract
anchor hue values into a module-level constant to prevent silent
divergence. Also remove unnecessary tensor clone in _angle_to_hue and
use torch.empty instead of torch.zeros where all elements are overwritten.
Replace _hue_to_polygon_point with _hue_to_chroma_vector to fix
Type II and interpolated color intervention modes. The old function
interpolated raw 3D anchor positions (mixing lightness into chroma
directions) and ignored calibrated anchor angles. The new function
projects anchors onto the plane perpendicular to the achromatic axis
to get pure chroma radii, then interpolates radius and angle in the
same segment structure as _angle_to_hue for round-trip consistency.
Replace two calibration nodes with a single smart LCSLoadData that
fingerprints VAE weights (sha256 of first decoder weight) and
auto-manages cached calibration files as data/lcs_{fingerprint}.safetensors.
Frontend JS syncs preset values into sliders on user interaction,
but headless runners (e.g. Lumi-Batcher) skip the frontend entirely.
When a preset is selected but all sliders are still at defaults,
execute() now applies the preset values server-side as a fallback.
The warm_dir was a unit vector (norm=1) but LCS chroma vectors have
norms ~40-46, so color_temperature=0.5 only shifted chroma by 1.2%.
Scale warm_dir by mean anchor chroma magnitude so the shift is
proportional. Slider range narrowed from ±2.0 to ±1.0 and preset
values adjusted accordingly (Warm=0.15, Cool=-0.15, Cinematic=0.05).
Selecting a preset now updates slider values in real-time via a
frontend JS extension, and users can tweak values after selecting.
Sliders are always visible (removed optional=True). Preset values
live only in JS (single source of truth); Python keeps just the
name list for the combo widget. Sync only fires on explicit user
dropdown interaction, preserving saved slider tweaks on workflow load.
Float equality (!=) can fail after JS→JSON→Python round-trips on
slider values. Switch to epsilon-based _is_default_tone() helper.
Also precompute the warm/cool direction vector once at closure-build
time instead of recomputing it on every denoising step.
The node now controls contrast, brightness, saturation, and color
temperature — "Contrast Adjust" was misleading. Adds 9 one-click
tonal presets (Cinematic, HDR, Vivid, Dramatic, Low Key, High Key,
Warm, Cool, Desaturated) plus Custom mode for manual slider control.
Shifts chroma along the warm (Red+Yellow) ↔ cool (Blue+Cyan) axis
via additive offset before saturation scaling, so saturation=0 still
yields grayscale. Warm direction is derived from chromatic projections
of the 6 hue anchors (stripping their lightness component).
l_scalar (lightness projection onto achromatic axis) can naturally
extend beyond [0,1] during denoising. Clamping destroys highlight/shadow
detail — directly contradicting the purpose of contrast adjustment.
Type I color intervention already operates without clamping LCS coords.
The no-op skip in execute() ensures default params have zero effect.
Operates directly in 3D LCS space by decomposing patches into lightness
(projection onto black→white achromatic axis) and chroma (perpendicular
residual). No HSL round-trip needed, making it faster than the color
intervention path.