6adb93407b
Left-brain HUD look-and-feel pass (judged by eye in the sim):
- corner-bracket targeting reticles (capped arms) replace plain boxes
- translucent label chips, legible over any frame
- two sensor channels: cyan detections (with deterministic confidence
scores) vs amber measurements (center crosshair)
- bbox-sized "ANALYSIS . L{n} . {k} OBJ" status tag, escalating with Left
Affect channel (emotions in the HUD), per
docs/superpowers/specs/2026-06-22-affect-channel-hud-design.md:
- surfaces only when BOTH knobs up; strength = min(left, right)
- stable per-scene palette, more words appear with combined strength
- soft glowing violet words on their own opacity layer (the dream
leaking into the machine's read); math in player/alteration.py
- forest/cosmos/abyss palettes in the manifest; 5 new unit tests
Simulator no-cache middleware so by-eye iteration never serves a stale
app.js / api response (fixes the latent "plan has affect but clip.affect
empty" stale-/api/clips bug).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
177 lines
6.3 KiB
Python
177 lines
6.3 KiB
Python
"""The alteration engine: a knob vector -> a layered RenderPlan (design §4, §5).
|
|
|
|
Reconciled slice (2026-06-07): the Right axis is a DISCRETE selection of a
|
|
pre-baked, flow-stabilized restyle variant (not a continuous blend), the Left
|
|
axis carries its knob LEVEL so a runtime annotation track can pick which labels
|
|
show, and a frozen `Calibration` parameterizes the knob->strength curves so they
|
|
can be tuned by eye in the simulator and baked into DEFAULT_CALIBRATION.
|
|
|
|
Layers compose per §4.2:
|
|
- Substrate: ColorGrade (Dark/Light mood, center = identity §5) + a pre-baked
|
|
Right restyle variant.
|
|
- Overlay: AnalyticalOverlay (Left), composited on top at runtime.
|
|
Left and Right stack (different layers); Dark/Light are the two poles of one
|
|
mood grade. See docs/superpowers/specs/2026-06-07-reconciled-simulator-
|
|
alteration-slice-design.md.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from hef.selection import Coordinate
|
|
|
|
KNOB_MAX = 4 # knob full-scale (0..4)
|
|
|
|
|
|
def _clamp(x: float, lo: float, hi: float) -> float:
|
|
return max(lo, min(hi, x))
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Calibration:
|
|
"""Tunable knob->strength curves (settled by eye in the sim, then baked).
|
|
|
|
- mood_gain: scales the signed Dark/Light tone (result clamped to [-1, 1]).
|
|
- overlay_gain: scales the Left overlay intensity (clamped to [0, 1]).
|
|
- right_variant_map: knob value (0..4) -> pre-baked Right variant index.
|
|
"""
|
|
|
|
mood_gain: float = 1.0
|
|
overlay_gain: float = 1.0
|
|
right_variant_map: tuple = (0, 1, 2, 3, 4)
|
|
|
|
|
|
# The LOCKED calibration (session 0010, 2026-06-07) — settled by eye in the
|
|
# simulator, closing the open session-0006 knob->strength decision. Convention:
|
|
# every experience knob runs 0 = off .. 4 = max, and equal Dark/Light = identity
|
|
# (raw footage); there is no "centered at 2 = no push" coordinate.
|
|
# - mood_gain = 1.0: full Dark/Light knob reaches the full mood grade. By-eye
|
|
# evidence (POC renders + sim, after the dark-grade fix this session) shows
|
|
# full tilt is peaceful on every axis, so no softening is warranted.
|
|
# - overlay_gain = 1.0: full Left = opacity 1.0; the HUD is legible, not
|
|
# overwhelming, sitting above the grade.
|
|
# - right_variant_map linear: the 5 knob notches map 1:1 onto the 5 discrete
|
|
# pre-baked Right strengths (0 = raw base).
|
|
# These are unity/linear by deliberate choice, not as placeholders. The curve
|
|
# stays parameterized so a future re-bake or a different feel is one edit away;
|
|
# test_default_calibration_is_locked guards the values from drifting silently.
|
|
DEFAULT_CALIBRATION = Calibration(
|
|
mood_gain=1.0,
|
|
overlay_gain=1.0,
|
|
right_variant_map=(0, 1, 2, 3, 4),
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ColorGrade:
|
|
"""Mood-axis grade (§5). `tone` is signed: >0 warm yellow->white (light),
|
|
<0 cool blue->black (dark), 0 = identity (raw, ungraded)."""
|
|
|
|
tone: float
|
|
|
|
@property
|
|
def is_identity(self) -> bool:
|
|
return self.tone == 0.0
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AnalyticalOverlay:
|
|
"""Left axis (§4.1): analytical HUD/annotation, composited on top at runtime.
|
|
`level` is the Left knob (0..4); a runtime annotation track uses it to pick
|
|
which labels appear. `intensity` 0..1 is the overlay opacity/strength."""
|
|
|
|
level: int
|
|
intensity: float
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AffectOverlay:
|
|
"""Affect channel (Left x Right): emotion-words surfaced only when BOTH the
|
|
analytical (Left) and dreamlike (Right) knobs are up. `strength` is
|
|
`min(left, right)` (0..4) — a runtime affect track picks which feeling words
|
|
appear by it; `intensity` 0..1 is their opacity. The dream leaking into the
|
|
machine's reading (affect-channel design, session 0013)."""
|
|
|
|
strength: int
|
|
intensity: float
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Restyle:
|
|
"""Right axis (§4.1): selects a pre-baked, flow-stabilized restyle variant.
|
|
`variant` is a discrete index (0 = raw base, no restyle)."""
|
|
|
|
variant: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RenderPlan:
|
|
"""The full layered alteration for one knob vector (§4.2)."""
|
|
|
|
grade: ColorGrade
|
|
overlay: AnalyticalOverlay
|
|
affect: AffectOverlay
|
|
restyle: Restyle
|
|
|
|
@property
|
|
def is_identity(self) -> bool:
|
|
"""True when the plan leaves the neutral base un-altered. Affect needs no
|
|
clause: `min(left,right) > 0` implies `left > 0`, so `overlay.level > 0`
|
|
already makes a plan with active affect non-identity."""
|
|
return (
|
|
self.grade.is_identity
|
|
and self.overlay.level == 0
|
|
and self.restyle.variant == 0
|
|
)
|
|
|
|
|
|
def _overlay_intensity(left: int, cal: Calibration) -> float:
|
|
return _clamp(cal.overlay_gain * left / KNOB_MAX, 0.0, 1.0)
|
|
|
|
|
|
def _affect_strength(left: int, right: int) -> int:
|
|
"""Affect surfaces only when BOTH knobs are up — gated by the smaller one."""
|
|
return min(left, right)
|
|
|
|
|
|
def _affect_intensity(left: int, right: int, cal: Calibration) -> float:
|
|
return _clamp(cal.overlay_gain * _affect_strength(left, right) / KNOB_MAX, 0.0, 1.0)
|
|
|
|
|
|
def _right_variant(right: int, cal: Calibration) -> int:
|
|
return cal.right_variant_map[right]
|
|
|
|
|
|
def _mood_tone(dark: int, light: int, cal: Calibration) -> float:
|
|
return _clamp(cal.mood_gain * (light - dark) / KNOB_MAX, -1.0, 1.0)
|
|
|
|
|
|
def plan_alteration(
|
|
coord: Coordinate, calibration: Calibration = DEFAULT_CALIBRATION
|
|
) -> RenderPlan:
|
|
"""Map a knob vector to its layered RenderPlan (design §4)."""
|
|
return RenderPlan(
|
|
grade=ColorGrade(tone=_mood_tone(coord.dark, coord.light, calibration)),
|
|
overlay=AnalyticalOverlay(
|
|
level=coord.left,
|
|
intensity=_overlay_intensity(coord.left, calibration),
|
|
),
|
|
affect=AffectOverlay(
|
|
strength=_affect_strength(coord.left, coord.right),
|
|
intensity=_affect_intensity(coord.left, coord.right, calibration),
|
|
),
|
|
restyle=Restyle(variant=_right_variant(coord.right, calibration)),
|
|
)
|
|
|
|
|
|
def render_plan_to_dict(plan: RenderPlan) -> dict:
|
|
"""JSON-serializable form for the simulator API."""
|
|
return {
|
|
"grade": {"tone": plan.grade.tone},
|
|
"overlay": {"level": plan.overlay.level, "intensity": plan.overlay.intensity},
|
|
"affect": {"strength": plan.affect.strength, "intensity": plan.affect.intensity},
|
|
"restyle": {"variant": plan.restyle.variant},
|
|
"is_identity": plan.is_identity,
|
|
}
|