refactor(manifold): delete the dead console UI stratum
Phase 1 group 5 (S15, S16, S18, S19, L22, L23, L20, L21, L1 delete-half). - S15: the four-way focus/altitude system. setFocus was never called anywhere, so only the 'composite' branch was reachable. Deleted SplitStage, ReadoutStrip, InputMini, AltitudeNav, CompactAxis, the UI Focus type/prop, the focus branches, stripPinned and the vacuous keyboard gates. MiniMeters kept; engine.feedback.setFocus (a different, live thing) untouched. - S16 + L1: the decorative stratum that rendered real-looking controls driving nothing — A/B machinery, the fake seed, seededGradient + weightsRevision, snapshots, master volume, bpm, and the learningRate/decay/tame/spreadLevel sliders with their Drawers rows. Each was confirmed self-referential first. NOTE a real behaviour change falls out of dropping `snapshots`: Undo outside an active explore-and-place session used to pop a UI-only snapshot that restored noiseCap/seed. It is now simply inactive unless a genuine core-backed scratchpad undo exists. Geometric-dislike mode never had a real undo primitive in the core, so only the fake path is gone. - S18: BackendAdvanced.tsx and its Drawers block. It self-described as a duplicate of the inline OutputsBackendConfig editor and BOTH rendered in the same expanded drawer. OutputsBackendConfig already covers every backend. - S19: pruned ConsoleCtx to the fields Dock/Drawers/OutputsBackendConfig actually read; deleted the Axes type + axes/setAxis and the preset/setPreset/offsetActive chain (permanently 'Sculpt'/false). KEPT ctx.modes and ctx.setModeId despite having no reader today — the Phase 5 instrument picker (§7.6, adopted) is built on exactly that plumbing. - L22: the 5 dead primitives (Panel, StatusLine, ControlAxis, CurvePlot, Sparkline) and their barrel exports, plus the now-dead .mf-axis-input CSS. - L23: OutputControl/toOutputControl, ModeIconComponent and the BACKENDS catalogue; Drawers now reads modeDesc.label/description from OUTPUT_MODES, the surviving single catalogue. - L20: the solo-mode selector's two unimplemented options no longer pretend to be selectable. - L21: FeedbackController vestiges — seed/undoDepth options, maxUndo, and six ControllerEngine members nothing called (the finding named three; the other three are used on the real EngineApi by debug/probe.ts, a different interface, so removing them from ControllerEngine is safe). Gates: run-all-tests.sh ALL GREEN (typecheck, 33 Playwright specs).
This commit is contained in:
parent
c98d25c255
commit
9b686eb312
23 changed files with 95 additions and 1902 deletions
|
|
@ -1,8 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* Manifold — app root. The convertible Console (ConsoleApp) wired to the real
|
* Manifold — app root. The convertible Console (ConsoleApp) wired to the real
|
||||||
* engine, mounted under EngineProvider. Defaults to the hero `focus="composite"`
|
* engine, mounted under EngineProvider. ConsoleApp renders its single
|
||||||
* (the convertible centerpiece). The `?debug=1` probe is installed once the
|
* "composite" stage unconditionally (the dead focus/altitude system was
|
||||||
* engine is live.
|
* deleted 2026-07 — simplification audit S15). The `?debug=1` probe is
|
||||||
|
* installed once the engine is live.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
@ -75,7 +76,7 @@ export function App() {
|
||||||
return (
|
return (
|
||||||
<EngineProvider options={engineOptions()} fallback={<Loading />}>
|
<EngineProvider options={engineOptions()} fallback={<Loading />}>
|
||||||
<ProbeInstaller />
|
<ProbeInstaller />
|
||||||
<ConsoleApp focus="composite" />
|
<ConsoleApp />
|
||||||
</EngineProvider>
|
</EngineProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,46 +14,46 @@
|
||||||
* FeedbackMode::ExploreAndPlace (set on mount; the controller forwards the
|
* FeedbackMode::ExploreAndPlace (set on mount; the controller forwards the
|
||||||
* Idle→Exploring→Placing lifecycle to engine.feedback.* — nisps/ml/feedback.hpp,
|
* Idle→Exploring→Placing lifecycle to engine.feedback.* — nisps/ml/feedback.hpp,
|
||||||
* per docs/adr/rl-feedback-design.md).
|
* per docs/adr/rl-feedback-design.md).
|
||||||
* - AltitudeNav switches `focus` via React state (in|split|out|composite), not
|
|
||||||
* by navigating to separate HTML files.
|
|
||||||
* - `c15` is labelled "Powerful Synth Engine" (in model.ts) — "C15" never shows.
|
* - `c15` is labelled "Powerful Synth Engine" (in model.ts) — "C15" never shows.
|
||||||
*
|
*
|
||||||
* UI-only state (params status/min/max/curve, snapshots, A/B seed, axes,
|
* The dead focus/altitude system (AltitudeNav; SplitStage/ReadoutStrip/InputMini
|
||||||
* noiseCap, health/rev visuals) is preserved as faithful local React state.
|
* stages; the `in`/`split`/`out` branches) was deleted 2026-07 (simplification
|
||||||
|
* audit S15) — `setFocus` was never called anywhere, so `composite` was the
|
||||||
|
* only reachable stage. The decorative A/B toggle, fake seed, snapshot stack,
|
||||||
|
* fabricated health/gradient visuals, and the never-touched master
|
||||||
|
* volume/bpm/learning-rate/decay/spread-level sliders were deleted alongside it
|
||||||
|
* (S16/L1) — each was write-only, read by nothing but its own control.
|
||||||
|
*
|
||||||
|
* UI-only state (params status/min/max/curve, noiseCap) is preserved as
|
||||||
|
* faithful local React state.
|
||||||
*/
|
*/
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { useEngine, useEngineVersion, ExplorationController } from '../engine';
|
import { useEngine, useEngineVersion, ExplorationController } from '../engine';
|
||||||
import { MF_MODES, modeEngineId, seededGradient, shapeValues } from './model';
|
import { MF_MODES, modeEngineId, shapeValues } from './model';
|
||||||
import type { MFParam } from './model';
|
import type { MFParam } from './model';
|
||||||
import { CompositeStage } from './CompositeStage';
|
import { CompositeStage } from './CompositeStage';
|
||||||
import { ParticleStage } from './ParticleStage';
|
import { ParticleStage } from './ParticleStage';
|
||||||
import { SandwichStage } from './SandwichStage';
|
import { SandwichStage } from './SandwichStage';
|
||||||
import { SplitStage } from './SplitStage';
|
|
||||||
import { OutputStage } from './OutputStage';
|
import { OutputStage } from './OutputStage';
|
||||||
import { InputMini } from './InputMini';
|
|
||||||
import { Manifold } from './Manifold';
|
import { Manifold } from './Manifold';
|
||||||
import { ReadoutStrip } from './ReadoutStrip';
|
|
||||||
import { VerdictCluster } from './VerdictCluster';
|
import { VerdictCluster } from './VerdictCluster';
|
||||||
import { Dock } from './Dock';
|
import { Dock } from './Dock';
|
||||||
import { ReshapeModal } from './ReshapeModal';
|
import { ReshapeModal } from './ReshapeModal';
|
||||||
import type {
|
import type {
|
||||||
Axes,
|
|
||||||
ConsoleCtx,
|
ConsoleCtx,
|
||||||
DrawerDepth,
|
DrawerDepth,
|
||||||
DrawerKey,
|
DrawerKey,
|
||||||
FeedbackMarker,
|
FeedbackMarker,
|
||||||
FeedbackModeUI,
|
FeedbackModeUI,
|
||||||
Focus,
|
|
||||||
OutputMode,
|
OutputMode,
|
||||||
Pin,
|
Pin,
|
||||||
Snapshot,
|
|
||||||
SoloMode,
|
SoloMode,
|
||||||
} from './types';
|
} from './types';
|
||||||
import type { BackendId } from '../dock/output-state';
|
import type { BackendId } from '../dock/output-state';
|
||||||
import { buildArmMask } from '../dock/output-state';
|
import { buildArmMask } from '../dock/output-state';
|
||||||
import { FeedbackController, type ProtoFeedbackMode } from '../feedback';
|
import { FeedbackController, type ProtoFeedbackMode } from '../feedback';
|
||||||
import { DEFAULT_OUTPUT_MODE, OUTPUT_MODES, outputModeDescriptor } from './output-mode';
|
import { DEFAULT_OUTPUT_MODE, outputModeDescriptor } from './output-mode';
|
||||||
import { useSettings, resolveInputMap } from '../settings/settings-store';
|
import { useSettings, resolveInputMap } from '../settings/settings-store';
|
||||||
import { useBackendManager } from '../backends';
|
import { useBackendManager } from '../backends';
|
||||||
import { useInputLayer } from '../inputs';
|
import { useInputLayer } from '../inputs';
|
||||||
|
|
@ -77,8 +77,6 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let SNAP_ID = 0;
|
|
||||||
|
|
||||||
/** Small pill-button style for the exploring-scratchpad banner controls. */
|
/** Small pill-button style for the exploring-scratchpad banner controls. */
|
||||||
function pillBtn(color: string): CSSProperties {
|
function pillBtn(color: string): CSSProperties {
|
||||||
return {
|
return {
|
||||||
|
|
@ -93,39 +91,22 @@ function pillBtn(color: string): CSSProperties {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConsoleAppProps {
|
export function ConsoleApp() {
|
||||||
focus?: Focus;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProps) {
|
|
||||||
const engine = useEngine();
|
const engine = useEngine();
|
||||||
const version = useEngineVersion(engine);
|
const version = useEngineVersion(engine);
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
|
|
||||||
const [focus, setFocus] = useState<Focus>(initialFocus);
|
|
||||||
const [modeId, setModeId] = useState('paf_synth');
|
const [modeId, setModeId] = useState('paf_synth');
|
||||||
const mode = MF_MODES.find((m) => m.id === modeId) ?? MF_MODES[0];
|
const mode = MF_MODES.find((m) => m.id === modeId) ?? MF_MODES[0];
|
||||||
const [params, setParams] = useState<MFParam[]>(() => mode.params.map((p) => ({ ...p })));
|
const [params, setParams] = useState<MFParam[]>(() => mode.params.map((p) => ({ ...p })));
|
||||||
const [pos, setPos] = useState<[number, number]>([0.5, 0.5]);
|
const [pos, setPos] = useState<[number, number]>([0.5, 0.5]);
|
||||||
|
|
||||||
// A/B seed-snapshot model (kept as visual parity; A holds a remembered weight
|
|
||||||
// snapshot conceptually — here we mirror the JSX's seed-based preview marker).
|
|
||||||
const [seed, setSeed] = useState(0.4);
|
|
||||||
const [axes, setAxes] = useState<Axes>({ boldness: 0.55, memory: 0.4, precision: 0.5 });
|
|
||||||
const [preset, setPreset] = useState('Sculpt');
|
|
||||||
const [noiseCap, setNoiseCap] = useState(0.12);
|
const [noiseCap, setNoiseCap] = useState(0.12);
|
||||||
const [examples, setExamples] = useState(0);
|
const [examples, setExamples] = useState(0);
|
||||||
const [addingExample, setAddingExample] = useState(false);
|
const [addingExample, setAddingExample] = useState(false);
|
||||||
const [loss, setLoss] = useState<number[]>([]);
|
const [loss, setLoss] = useState<number[]>([]);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [snapshots, setSnapshots] = useState<Snapshot[]>([]);
|
|
||||||
const [ab, setAB] = useState<'A' | 'B'>('B');
|
|
||||||
const [, setHoldingA] = useState(false);
|
|
||||||
const aRef = useRef<{ seed: number } | null>(null);
|
|
||||||
const [spread, setSpread] = useState(false);
|
const [spread, setSpread] = useState(false);
|
||||||
const [tame, setTame] = useState(0.85);
|
|
||||||
const [health, setHealth] = useState(0.8);
|
|
||||||
const [rev, setRev] = useState(1);
|
|
||||||
const [active, setActive] = useState<DrawerKey | null>(null);
|
const [active, setActive] = useState<DrawerKey | null>(null);
|
||||||
const [depth, setDepth] = useState<DrawerDepth>('condensed');
|
const [depth, setDepth] = useState<DrawerDepth>('condensed');
|
||||||
// Sandwich (parameter-landscape) centre-stage toggle — dock-bottom layers icon.
|
// Sandwich (parameter-landscape) centre-stage toggle — dock-bottom layers icon.
|
||||||
|
|
@ -146,9 +127,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
const [picking, setPicking] = useState(false);
|
const [picking, setPicking] = useState(false);
|
||||||
const [anchorCount, setAnchorCount] = useState(0);
|
const [anchorCount, setAnchorCount] = useState(0);
|
||||||
const [undoDepth, setUndoDepth] = useState(0);
|
const [undoDepth, setUndoDepth] = useState(0);
|
||||||
const [learningRate, setLearningRate] = useState(0.00001);
|
|
||||||
const [decay, setDecay] = useState(0.97);
|
|
||||||
const [spreadLevel, setSpreadLevel] = useState(0.6);
|
|
||||||
// Exploration gestures (Jolt held weight-morph + OU explore-intensity). The
|
// Exploration gestures (Jolt held weight-morph + OU explore-intensity). The
|
||||||
// maths lives in the ExplorationController (engine/exploration.ts); these are
|
// maths lives in the ExplorationController (engine/exploration.ts); these are
|
||||||
// the React-visible reflections the Learning drawer renders.
|
// the React-visible reflections the Learning drawer renders.
|
||||||
|
|
@ -170,8 +148,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
const [vcvSendRaw, setVcvSendRaw] = useState(false);
|
const [vcvSendRaw, setVcvSendRaw] = useState(false);
|
||||||
// Feedback markers plotted on the 2D map (both polarities; session-scoped).
|
// Feedback markers plotted on the 2D map (both polarities; session-scoped).
|
||||||
const [markers, setMarkers] = useState<FeedbackMarker[]>([]);
|
const [markers, setMarkers] = useState<FeedbackMarker[]>([]);
|
||||||
const [volume, setVolume] = useState(0.8);
|
|
||||||
const [bpm, setBpm] = useState(120);
|
|
||||||
const [audioStarted, setAudioStarted] = useState(false);
|
const [audioStarted, setAudioStarted] = useState(false);
|
||||||
const [follow, setFollow] = useState(false);
|
const [follow, setFollow] = useState(false);
|
||||||
const [split, setSplit] = useState(() => {
|
const [split, setSplit] = useState(() => {
|
||||||
|
|
@ -181,7 +157,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('mf-composite-split', String(split));
|
localStorage.setItem('mf-composite-split', String(split));
|
||||||
}, [split]);
|
}, [split]);
|
||||||
const [stripPinned, setStripPinned] = useState(true);
|
|
||||||
const [firstSession, setFirstSession] = useState(true);
|
const [firstSession, setFirstSession] = useState(true);
|
||||||
const [pins, setPins] = useState<Pin[]>([]);
|
const [pins, setPins] = useState<Pin[]>([]);
|
||||||
|
|
||||||
|
|
@ -192,7 +167,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
const controllerRef = useRef<FeedbackController | null>(null);
|
const controllerRef = useRef<FeedbackController | null>(null);
|
||||||
if (engine && !controllerRef.current) {
|
if (engine && !controllerRef.current) {
|
||||||
controllerRef.current = new FeedbackController(engine, {
|
controllerRef.current = new FeedbackController(engine, {
|
||||||
seed: 0xfeedbacc,
|
|
||||||
spread: 0.6,
|
spread: 0.6,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -289,8 +263,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
setPos([0.5, 0.5]);
|
setPos([0.5, 0.5]);
|
||||||
setExamples(0);
|
setExamples(0);
|
||||||
setLoss([]);
|
setLoss([]);
|
||||||
setSnapshots([]);
|
|
||||||
setSeed(0.4);
|
|
||||||
setFollow(false);
|
setFollow(false);
|
||||||
setPins([]);
|
setPins([]);
|
||||||
setMarkers([]);
|
setMarkers([]);
|
||||||
|
|
@ -392,10 +364,8 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
if (!c || !c.isPicking()) return;
|
if (!c || !c.isPicking()) return;
|
||||||
c.placeCommit(x, y);
|
c.placeCommit(x, y);
|
||||||
setPos([x, y]);
|
setPos([x, y]);
|
||||||
pushSnap('anchor');
|
|
||||||
pushMarker([x, y], 'positive');
|
pushMarker([x, y], 'positive');
|
||||||
syncController();
|
syncController();
|
||||||
setRev((r) => r + 1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Output backend transport (backends-spec). The manager consumes the engine
|
// Output backend transport (backends-spec). The manager consumes the engine
|
||||||
|
|
@ -441,11 +411,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
[params, version, engine],
|
[params, version, engine],
|
||||||
);
|
);
|
||||||
|
|
||||||
const gradient = useMemo(() => seededGradient(rev), [rev]);
|
|
||||||
|
|
||||||
const pushSnap = (tag: string) =>
|
|
||||||
setSnapshots((s) => [...s, { id: ++SNAP_ID, tag, noise: noiseCap, seed }].slice(-50));
|
|
||||||
|
|
||||||
/** Plot a feedback marker at the input location it was given (session-scoped). */
|
/** Plot a feedback marker at the input location it was given (session-scoped). */
|
||||||
const pushMarker = (at: [number, number], polarity: 'positive' | 'negative') =>
|
const pushMarker = (at: [number, number], polarity: 'positive' | 'negative') =>
|
||||||
setMarkers((m) => [...m, { x: at[0], y: at[1], polarity }].slice(-200));
|
setMarkers((m) => [...m, { x: at[0], y: at[1], polarity }].slice(-200));
|
||||||
|
|
@ -473,17 +438,14 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
if (feedbackMode === 'explore-and-place') {
|
if (feedbackMode === 'explore-and-place') {
|
||||||
if (c?.getState().exploring) {
|
if (c?.getState().exploring) {
|
||||||
// Place the current candidate → next manifold tap chooses the location.
|
// Place the current candidate → next manifold tap chooses the location.
|
||||||
pushSnap('place');
|
|
||||||
c.place();
|
c.place();
|
||||||
} else {
|
} else {
|
||||||
// Not exploring: a plain positive reinforcement of the current mapping.
|
// Not exploring: a plain positive reinforcement of the current mapping.
|
||||||
pushSnap('commit +');
|
|
||||||
c?.like(pos, engine?.getOutputs() ?? new Float32Array(0));
|
c?.like(pos, engine?.getOutputs() ?? new Float32Array(0));
|
||||||
pushMarker(pos, 'positive');
|
pushMarker(pos, 'positive');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Geometric dislike: thumbs-up = like + train.
|
// Geometric dislike: thumbs-up = like + train.
|
||||||
pushSnap('like +');
|
|
||||||
c?.like(pos, engine?.getOutputs() ?? new Float32Array(0));
|
c?.like(pos, engine?.getOutputs() ?? new Float32Array(0));
|
||||||
pushMarker(pos, 'positive');
|
pushMarker(pos, 'positive');
|
||||||
}
|
}
|
||||||
|
|
@ -491,8 +453,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
forwardVcvFeedback('up');
|
forwardVcvFeedback('up');
|
||||||
syncController();
|
syncController();
|
||||||
setNoiseCap((n) => Math.max(0.02, n * 0.7));
|
setNoiseCap((n) => Math.max(0.02, n * 0.7));
|
||||||
setHealth((h) => Math.min(1, h + 0.08));
|
|
||||||
setRev((r) => r + 1);
|
|
||||||
const l = engine?.evalLoss();
|
const l = engine?.evalLoss();
|
||||||
setLoss((prev) =>
|
setLoss((prev) =>
|
||||||
[...prev, Number.isFinite(l) ? (l as number) : prev.length ? prev[prev.length - 1] : 0.5].slice(-120),
|
[...prev, Number.isFinite(l) ? (l as number) : prev.length ? prev[prev.length - 1] : 0.5].slice(-120),
|
||||||
|
|
@ -508,10 +468,8 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
// Enter the scratchpad (or, if already exploring, cancel back to the real
|
// Enter the scratchpad (or, if already exploring, cancel back to the real
|
||||||
// net). NEVER a dislike — Mode 2 is positive-only.
|
// net). NEVER a dislike — Mode 2 is positive-only.
|
||||||
if (c?.getState().exploring) {
|
if (c?.getState().exploring) {
|
||||||
pushSnap('cancel explore');
|
|
||||||
c.cancel();
|
c.cancel();
|
||||||
} else {
|
} else {
|
||||||
pushSnap('explore');
|
|
||||||
c?.enterExplore();
|
c?.enterExplore();
|
||||||
// VCV bridged mode: entering explore re-rolls the module's net too.
|
// VCV bridged mode: entering explore re-rolls the module's net too.
|
||||||
forwardVcvFeedback('rand');
|
forwardVcvFeedback('rand');
|
||||||
|
|
@ -520,31 +478,21 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
// Geometric dislike: push the current mapping away from this sound. Pass the
|
// Geometric dislike: push the current mapping away from this sound. Pass the
|
||||||
// HEARD (post-pipeline, routed) vector — NOT the raw MLP output — so the
|
// HEARD (post-pipeline, routed) vector — NOT the raw MLP output — so the
|
||||||
// core has a non-zero MSE derivative (see engine.feedback.dislikeGeometric).
|
// core has a non-zero MSE derivative (see engine.feedback.dislikeGeometric).
|
||||||
pushSnap('dislike −');
|
const action = c?.dislike(engine?.routedOutput() ?? new Float32Array(0));
|
||||||
const action = c?.dislike(
|
|
||||||
pos,
|
|
||||||
engine?.routedOutput() ?? new Float32Array(0),
|
|
||||||
noiseCap,
|
|
||||||
spread ? 1 : 0.6,
|
|
||||||
);
|
|
||||||
// GeometricColdStart (15): no positives yet → show the one-time prompt.
|
// GeometricColdStart (15): no positives yet → show the one-time prompt.
|
||||||
if (action === 15) setColdStart(true);
|
if (action === 15) setColdStart(true);
|
||||||
pushMarker(pos, 'negative');
|
pushMarker(pos, 'negative');
|
||||||
// VCV bridged mode: thumbs-down = negative verdict.
|
// VCV bridged mode: thumbs-down = negative verdict.
|
||||||
forwardVcvFeedback('down');
|
forwardVcvFeedback('down');
|
||||||
setSeed((s) => s + (Math.random() - 0.5) * (noiseCap * 4 + 0.3));
|
|
||||||
setNoiseCap((n) => Math.min(0.5, n + 0.06));
|
setNoiseCap((n) => Math.min(0.5, n + 0.06));
|
||||||
setHealth((h) => Math.max(0.1, h - 0.06));
|
|
||||||
}
|
}
|
||||||
syncController();
|
syncController();
|
||||||
setRev((r) => r + 1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Long-press perturb / explicit re-roll. */
|
/** Long-press perturb / explicit re-roll. */
|
||||||
const reroll = () => {
|
const reroll = () => {
|
||||||
const c = controllerRef.current;
|
const c = controllerRef.current;
|
||||||
setFirstSession(false);
|
setFirstSession(false);
|
||||||
pushSnap('re-roll');
|
|
||||||
if (feedbackMode === 'explore-and-place' && c?.getState().exploring) {
|
if (feedbackMode === 'explore-and-place' && c?.getState().exploring) {
|
||||||
// Re-roll the scratchpad net (undoable) without leaving the session.
|
// Re-roll the scratchpad net (undoable) without leaving the session.
|
||||||
c.reroll();
|
c.reroll();
|
||||||
|
|
@ -555,10 +503,7 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
// VCV bridged mode: re-roll the module's net too.
|
// VCV bridged mode: re-roll the module's net too.
|
||||||
forwardVcvFeedback('rand');
|
forwardVcvFeedback('rand');
|
||||||
syncController();
|
syncController();
|
||||||
setSeed(Math.random() * 6);
|
|
||||||
setNoiseCap(0.4);
|
setNoiseCap(0.4);
|
||||||
setHealth(0.5);
|
|
||||||
setRev((r) => r + 1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -575,51 +520,40 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
engine?.process();
|
engine?.process();
|
||||||
}
|
}
|
||||||
syncController();
|
syncController();
|
||||||
setRev((r) => r + 1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Undo. While exploring (Mode 2) this pops the scratchpad undo ring (reroll /
|
* Undo. Only meaningful while exploring (Mode 2) — pops the scratchpad undo
|
||||||
* nudge). Otherwise it falls back to the UI snapshot stack (visual A/B seed).
|
* ring (reroll / nudge), which is real, engine-backed undo. Geometric dislike
|
||||||
|
* (Mode 1) and the non-exploring case have no real undo in the core, so the
|
||||||
|
* button is simply inactive there (the prior "undo" outside a scratchpad
|
||||||
|
* session only reverted a decorative UI snapshot stack — deleted 2026-07,
|
||||||
|
* simplification audit S16).
|
||||||
*/
|
*/
|
||||||
const undo = () => {
|
const undo = () => {
|
||||||
const c = controllerRef.current;
|
const c = controllerRef.current;
|
||||||
if (feedbackMode === 'explore-and-place' && c?.getState().exploring) {
|
if (feedbackMode === 'explore-and-place' && c?.getState().exploring) {
|
||||||
c.undo();
|
c.undo();
|
||||||
syncController();
|
syncController();
|
||||||
setRev((r) => r + 1);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
setSnapshots((s) => {
|
|
||||||
if (!s.length) return s;
|
|
||||||
const last = s[s.length - 1];
|
|
||||||
setSeed(last.seed);
|
|
||||||
setNoiseCap(last.noise);
|
|
||||||
setRev((r) => r + 1);
|
|
||||||
return s.slice(0, -1);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- Explore-and-place scratchpad ops surfaced to the dock + cluster ----
|
// ---- Explore-and-place scratchpad ops surfaced to the dock + cluster ----
|
||||||
const onExplore = () => {
|
const onExplore = () => {
|
||||||
controllerRef.current?.enterExplore();
|
controllerRef.current?.enterExplore();
|
||||||
syncController();
|
syncController();
|
||||||
setRev((r) => r + 1);
|
|
||||||
};
|
};
|
||||||
const onScratchReroll = () => {
|
const onScratchReroll = () => {
|
||||||
controllerRef.current?.reroll();
|
controllerRef.current?.reroll();
|
||||||
syncController();
|
syncController();
|
||||||
setRev((r) => r + 1);
|
|
||||||
};
|
};
|
||||||
const onScratchNudge = () => {
|
const onScratchNudge = () => {
|
||||||
controllerRef.current?.nudge();
|
controllerRef.current?.nudge();
|
||||||
syncController();
|
syncController();
|
||||||
setRev((r) => r + 1);
|
|
||||||
};
|
};
|
||||||
const onScratchUndo = () => {
|
const onScratchUndo = () => {
|
||||||
controllerRef.current?.undo();
|
controllerRef.current?.undo();
|
||||||
syncController();
|
syncController();
|
||||||
setRev((r) => r + 1);
|
|
||||||
};
|
};
|
||||||
const onPlace = () => {
|
const onPlace = () => {
|
||||||
controllerRef.current?.place();
|
controllerRef.current?.place();
|
||||||
|
|
@ -629,13 +563,11 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
controllerRef.current?.finalise();
|
controllerRef.current?.finalise();
|
||||||
syncController();
|
syncController();
|
||||||
setRev((r) => r + 1);
|
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
};
|
};
|
||||||
const onCancelExplore = () => {
|
const onCancelExplore = () => {
|
||||||
controllerRef.current?.cancel();
|
controllerRef.current?.cancel();
|
||||||
syncController();
|
syncController();
|
||||||
setRev((r) => r + 1);
|
|
||||||
};
|
};
|
||||||
const train = () => {
|
const train = () => {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
|
|
@ -655,30 +587,11 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
// Snapshot the current input → current (shaped) output as a training example.
|
// Snapshot the current input → current (shaped) output as a training example.
|
||||||
engine?.addExample([pos[0], pos[1]], Array.from(values));
|
engine?.addExample([pos[0], pos[1]], Array.from(values));
|
||||||
setExamples((e) => e + 1);
|
setExamples((e) => e + 1);
|
||||||
pushSnap('example');
|
|
||||||
train();
|
train();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setParam = (i: number, patch: Partial<MFParam>) =>
|
const setParam = (i: number, patch: Partial<MFParam>) =>
|
||||||
setParams((ps) => ps.map((p, j) => (j === i ? { ...p, ...patch } : p)));
|
setParams((ps) => ps.map((p, j) => (j === i ? { ...p, ...patch } : p)));
|
||||||
const cycleStatus = (i: number) =>
|
|
||||||
setParams((ps) =>
|
|
||||||
ps.map((p, j) =>
|
|
||||||
j === i
|
|
||||||
? { ...p, status: ({ off: 'fixed', fixed: 'live', live: 'off' } as const)[p.status] }
|
|
||||||
: p,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleAB = () => {
|
|
||||||
if (ab === 'B') {
|
|
||||||
aRef.current = { seed };
|
|
||||||
setAB('A');
|
|
||||||
} else {
|
|
||||||
if (aRef.current) setSeed(aRef.current.seed);
|
|
||||||
setAB('B');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// keyboard accelerators
|
// keyboard accelerators
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -702,13 +615,13 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
setActive((a) => (a === map[e.key] ? null : map[e.key]));
|
setActive((a) => (a === map[e.key] ? null : map[e.key]));
|
||||||
setDepth('condensed');
|
setDepth('condensed');
|
||||||
} else if (e.key === '\\') setDepth((d) => (d === 'expanded' ? 'condensed' : 'expanded'));
|
} else if (e.key === '\\') setDepth((d) => (d === 'expanded' ? 'condensed' : 'expanded'));
|
||||||
else if (focus === 'composite' && e.key === '[') {
|
else if (e.key === '[') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSplit((s) => Math.max(0, s - 0.04));
|
setSplit((s) => Math.max(0, s - 0.04));
|
||||||
} else if (focus === 'composite' && e.key === ']') {
|
} else if (e.key === ']') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSplit((s) => Math.min(1, s + 0.04));
|
setSplit((s) => Math.min(1, s + 0.04));
|
||||||
} else if (focus === 'composite' && (e.key === '=' || e.key === '0')) {
|
} else if (e.key === '=' || e.key === '0') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSplit(0.5);
|
setSplit(0.5);
|
||||||
} else if (e.key === ' ' || e.key === 'ArrowUp') {
|
} else if (e.key === ' ' || e.key === 'ArrowUp') {
|
||||||
|
|
@ -791,11 +704,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
modeId,
|
modeId,
|
||||||
setModeId,
|
setModeId,
|
||||||
mode,
|
mode,
|
||||||
axes,
|
|
||||||
setAxis: (k, v) => setAxes((s) => ({ ...s, [k]: v })),
|
|
||||||
preset,
|
|
||||||
setPreset,
|
|
||||||
offsetActive: preset !== 'Sculpt',
|
|
||||||
datasetCount: examples,
|
datasetCount: examples,
|
||||||
loss,
|
loss,
|
||||||
busy,
|
busy,
|
||||||
|
|
@ -812,25 +720,8 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
setMarkers([]);
|
setMarkers([]);
|
||||||
setPins([]);
|
setPins([]);
|
||||||
},
|
},
|
||||||
snapshots,
|
|
||||||
onJump: (id) => {
|
|
||||||
const s = snapshots.find((x) => x.id === id);
|
|
||||||
if (s) {
|
|
||||||
setSeed(s.seed);
|
|
||||||
setNoiseCap(s.noise);
|
|
||||||
setRev((r) => r + 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
params,
|
params,
|
||||||
cycleStatus,
|
|
||||||
setParam,
|
setParam,
|
||||||
outputBackend,
|
|
||||||
setOutputBackend: (b) => {
|
|
||||||
// Map a backend id back onto the active Mode (the Mode is the source of
|
|
||||||
// truth; the Outputs drawer drives it via setOutputMode).
|
|
||||||
const m = OUTPUT_MODES.find((om) => om.backend === b);
|
|
||||||
if (m) setOutputMode(m.id);
|
|
||||||
},
|
|
||||||
outputMode,
|
outputMode,
|
||||||
setOutputMode,
|
setOutputMode,
|
||||||
// ---- output backend transport ----
|
// ---- output backend transport ----
|
||||||
|
|
@ -853,16 +744,9 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
cvIdentify,
|
cvIdentify,
|
||||||
cvDisconnect,
|
cvDisconnect,
|
||||||
setParams: (next: MFParam[]) => setParams(next),
|
setParams: (next: MFParam[]) => setParams(next),
|
||||||
markers,
|
|
||||||
inputs,
|
inputs,
|
||||||
health,
|
|
||||||
gradient: gradient.norms,
|
|
||||||
gradientStatus: gradient.status,
|
|
||||||
weightsRevision: rev,
|
|
||||||
spread,
|
spread,
|
||||||
setSpread,
|
setSpread,
|
||||||
tame,
|
|
||||||
setTame,
|
|
||||||
noiseCap,
|
noiseCap,
|
||||||
setNoiseCap,
|
setNoiseCap,
|
||||||
// learning-behaviour
|
// learning-behaviour
|
||||||
|
|
@ -874,12 +758,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
learningPaused,
|
learningPaused,
|
||||||
armedCount: params.filter((p) => p.armed).length,
|
armedCount: params.filter((p) => p.armed).length,
|
||||||
clearArmed: () => setParams((ps) => ps.map((p) => (p.armed ? { ...p, armed: false } : p))),
|
clearArmed: () => setParams((ps) => ps.map((p) => (p.armed ? { ...p, armed: false } : p))),
|
||||||
learningRate,
|
|
||||||
setLearningRate,
|
|
||||||
decay,
|
|
||||||
setDecay,
|
|
||||||
spreadLevel,
|
|
||||||
setSpreadLevel,
|
|
||||||
// exploration gestures (Jolt + OU explore)
|
// exploration gestures (Jolt + OU explore)
|
||||||
joltActive,
|
joltActive,
|
||||||
onJoltPress,
|
onJoltPress,
|
||||||
|
|
@ -889,10 +767,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
// synth
|
// synth
|
||||||
audioStarted,
|
audioStarted,
|
||||||
onToggleAudio,
|
onToggleAudio,
|
||||||
volume,
|
|
||||||
setVolume,
|
|
||||||
bpm,
|
|
||||||
setBpm,
|
|
||||||
// explore-and-place scratchpad session (workstream B)
|
// explore-and-place scratchpad session (workstream B)
|
||||||
picking,
|
picking,
|
||||||
anchorCount,
|
anchorCount,
|
||||||
|
|
@ -909,8 +783,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
// Resolve the effective input-map shape from Settings + the mode's declared input.
|
// Resolve the effective input-map shape from Settings + the mode's declared input.
|
||||||
const inputMapVariant = resolveInputMap(settings.inputMap, mode.input);
|
const inputMapVariant = resolveInputMap(settings.inputMap, mode.input);
|
||||||
|
|
||||||
const healthColor =
|
|
||||||
health > 0.66 ? 'rgba(107,194,107,' : health > 0.33 ? 'rgba(245,196,94,' : 'rgba(255,68,102,';
|
|
||||||
const addPin = (p: [number, number]) =>
|
const addPin = (p: [number, number]) =>
|
||||||
setPins((ps) => [...ps, { x: p[0], y: p[1], color: 'rgba(255,106,0,0.16)' }]);
|
setPins((ps) => [...ps, { x: p[0], y: p[1], color: 'rgba(255,106,0,0.16)' }]);
|
||||||
|
|
||||||
|
|
@ -926,41 +798,9 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
>
|
>
|
||||||
<style>{`@keyframes mfDrawerIn{from{transform:translateX(16px)}to{transform:translateX(0)}}`}</style>
|
<style>{`@keyframes mfDrawerIn{from{transform:translateX(16px)}to{transform:translateX(0)}}`}</style>
|
||||||
|
|
||||||
{/* ambient health glow at the screen edge */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
inset: 0,
|
|
||||||
pointerEvents: 'none',
|
|
||||||
zIndex: 25,
|
|
||||||
boxShadow: `inset 0 0 120px ${healthColor}${0.05 + (1 - health) * 0.12})`,
|
|
||||||
transition: 'box-shadow var(--dur-slow) var(--ease-console)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* stage = manifold area (left of dock) */}
|
{/* stage = manifold area (left of dock) */}
|
||||||
<div style={{ position: 'absolute', top: 0, left: 0, right: 48, bottom: 0 }}>
|
<div style={{ position: 'absolute', top: 0, left: 0, right: 48, bottom: 0 }}>
|
||||||
{focus === 'in' && (stripPinned || mode.cls !== 'Synth') && (
|
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}>
|
||||||
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 30 }}>
|
|
||||||
<ReadoutStrip
|
|
||||||
params={params}
|
|
||||||
values={values}
|
|
||||||
onChange={setParam}
|
|
||||||
pinned={stripPinned}
|
|
||||||
onTogglePin={() => setStripPinned((p) => !p)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: focus === 'in' && stripPinned ? 76 : 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{sandwich ? (
|
{sandwich ? (
|
||||||
// Sandwich centre-stage: shrunken input (left) · landscape stack
|
// Sandwich centre-stage: shrunken input (left) · landscape stack
|
||||||
// (centre, fills) · compact outputs (right). Replaces the Mode stage.
|
// (centre, fills) · compact outputs (right). Replaces the Mode stage.
|
||||||
|
|
@ -1011,7 +851,7 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
</div>
|
</div>
|
||||||
) : outputMode === 'particles' ? (
|
) : outputMode === 'particles' ? (
|
||||||
<ParticleStage pos={pos} onMove={onMove} />
|
<ParticleStage pos={pos} onMove={onMove} />
|
||||||
) : focus === 'composite' ? (
|
) : (
|
||||||
<CompositeStage
|
<CompositeStage
|
||||||
split={split}
|
split={split}
|
||||||
onSplit={setSplit}
|
onSplit={setSplit}
|
||||||
|
|
@ -1028,46 +868,6 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
values={values}
|
values={values}
|
||||||
onChange={setParam}
|
onChange={setParam}
|
||||||
/>
|
/>
|
||||||
) : focus === 'split' ? (
|
|
||||||
<SplitStage
|
|
||||||
pos={pos}
|
|
||||||
onMove={onMove}
|
|
||||||
noiseCap={noiseCap}
|
|
||||||
pins={pins}
|
|
||||||
markers={markers}
|
|
||||||
variant={inputMapVariant}
|
|
||||||
follow={follow}
|
|
||||||
onLongPress={addPin}
|
|
||||||
params={params}
|
|
||||||
values={values}
|
|
||||||
onChange={setParam}
|
|
||||||
/>
|
|
||||||
) : focus === 'out' ? (
|
|
||||||
<>
|
|
||||||
<OutputStage params={params} values={values} onChange={setParam} />
|
|
||||||
<InputMini
|
|
||||||
mode={mode}
|
|
||||||
pos={pos}
|
|
||||||
onMove={onMove}
|
|
||||||
noiseCap={noiseCap}
|
|
||||||
corner="bottom-left"
|
|
||||||
variant={inputMapVariant}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Manifold
|
|
||||||
pos={pos}
|
|
||||||
onMove={onMove}
|
|
||||||
noiseCap={noiseCap}
|
|
||||||
pins={pins}
|
|
||||||
markers={markers}
|
|
||||||
variant={inputMapVariant}
|
|
||||||
frozen={false}
|
|
||||||
follow={follow}
|
|
||||||
onLongPress={addPin}
|
|
||||||
picking={picking}
|
|
||||||
onPickLocation={onPickLocation}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* corner overlay — hidden in Particle mode (top axis bar owns that row) */}
|
{/* corner overlay — hidden in Particle mode (top axis bar owns that row) */}
|
||||||
|
|
@ -1092,10 +892,7 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
onReroll={reroll}
|
onReroll={reroll}
|
||||||
onNudge={nudgeNet}
|
onNudge={nudgeNet}
|
||||||
onRandomise={reroll}
|
onRandomise={reroll}
|
||||||
canUndo={feedbackMode === 'explore-and-place' && exploring ? undoDepth > 0 : snapshots.length > 0}
|
canUndo={feedbackMode === 'explore-and-place' && exploring && undoDepth > 0}
|
||||||
ab={ab}
|
|
||||||
onToggleAB={toggleAB}
|
|
||||||
onHoldA={setHoldingA}
|
|
||||||
firstSession={firstSession}
|
firstSession={firstSession}
|
||||||
feedbackMode={feedbackMode}
|
feedbackMode={feedbackMode}
|
||||||
exploring={exploring}
|
exploring={exploring}
|
||||||
|
|
@ -1205,11 +1002,10 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Global PICK-LOCATION capture overlay: works in any focus (composite/
|
{/* Global PICK-LOCATION capture overlay: the CompositeStage/ParticleStage
|
||||||
split stages don't expose picking). The directly-rendered Manifold
|
don't expose picking directly, so this transparent overlay captures
|
||||||
(focus==='in') also handles picks + draws the reticle; this overlay
|
the pointer-down and routes it to onPickLocation everywhere. */}
|
||||||
guarantees the place→pick loop is reachable everywhere. */}
|
{picking && (
|
||||||
{picking && focus !== 'in' && (
|
|
||||||
<div
|
<div
|
||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
const r = e.currentTarget.getBoundingClientRect();
|
const r = e.currentTarget.getBoundingClientRect();
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,7 @@ import { Badge, Button, PillToggle, Slider, Switch } from '../primitives';
|
||||||
import type { ConsoleCtx, DrawerDepth, DrawerKey, FeedbackModeUI, SoloMode } from './types';
|
import type { ConsoleCtx, DrawerDepth, DrawerKey, FeedbackModeUI, SoloMode } from './types';
|
||||||
import type { InputMode } from '../inputs';
|
import type { InputMode } from '../inputs';
|
||||||
import { OutputControlRow } from '../dock/OutputControlRow';
|
import { OutputControlRow } from '../dock/OutputControlRow';
|
||||||
import { BackendAdvanced } from '../dock/BackendAdvanced';
|
|
||||||
import { OutputsBackendConfig, BackendStatusChip } from '../dock/OutputsBackendConfig';
|
import { OutputsBackendConfig, BackendStatusChip } from '../dock/OutputsBackendConfig';
|
||||||
import { BACKENDS } from '../dock/output-state';
|
|
||||||
import { shapeValues } from './model';
|
import { shapeValues } from './model';
|
||||||
import { outputModeDescriptor } from './output-mode';
|
import { outputModeDescriptor } from './output-mode';
|
||||||
import { useSettings, unfocusedIconCss } from '../settings/settings-store';
|
import { useSettings, unfocusedIconCss } from '../settings/settings-store';
|
||||||
|
|
@ -139,6 +137,13 @@ const FEEDBACK_DESC: Record<FeedbackModeUI, string> = {
|
||||||
'explore-and-place':
|
'explore-and-place':
|
||||||
'Down re-rolls the whole net into a scratchpad you audition; + places a liked sound (Mode 2).',
|
'Down re-rolls the whole net into a scratchpad you audition; + places a liked sound (Mode 2).',
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Solo behaviour. `mask-gradients` is the only variant the core actually
|
||||||
|
* implements — `soloMode` has no other observable effect today (the other two
|
||||||
|
* options need the C API's `train_masked` step). Rendered as a fixed,
|
||||||
|
* non-selectable label rather than a picker that offers dead choices
|
||||||
|
* (simplification audit L20).
|
||||||
|
*/
|
||||||
const SOLO_OPTS: { value: SoloMode; label: string }[] = [
|
const SOLO_OPTS: { value: SoloMode; label: string }[] = [
|
||||||
{ value: 'mask-gradients', label: 'Mask gradients' },
|
{ value: 'mask-gradients', label: 'Mask gradients' },
|
||||||
{ value: 'zero-loss', label: 'Zero loss' },
|
{ value: 'zero-loss', label: 'Zero loss' },
|
||||||
|
|
@ -234,25 +239,14 @@ function LearningDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<SectionLabel>Solo behaviour</SectionLabel>
|
<SectionLabel>Solo behaviour</SectionLabel>
|
||||||
<Segmented value={ctx.soloMode} onChange={ctx.setSoloMode} options={SOLO_OPTS} />
|
<Chip tone="var(--accent)">{SOLO_OPTS.find((o) => o.value === ctx.soloMode)?.label}</Chip>
|
||||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||||
{SOLO_DESC[ctx.soloMode]} Solo only freezes the rest as far as a shared network allows.
|
{SOLO_DESC[ctx.soloMode]} Solo only freezes the rest as far as a shared network allows.
|
||||||
|
The other two behaviours need real core support (`train_masked`) and aren't selectable yet.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SectionLabel>Live training params</SectionLabel>
|
<SectionLabel>Live training params</SectionLabel>
|
||||||
<Slider label="noise cap" value={ctx.noiseCap} min={0} max={0.5} step={0.01} onChange={ctx.setNoiseCap} />
|
<Slider label="noise cap" value={ctx.noiseCap} min={0} max={0.5} step={0.01} onChange={ctx.setNoiseCap} />
|
||||||
<Slider label="spread" value={ctx.spreadLevel} min={0} max={1} step={0.01} onChange={ctx.setSpreadLevel} />
|
|
||||||
<Slider label="tame · output limiter" value={ctx.tame} min={0} max={1} step={0.01} onChange={ctx.setTame} />
|
|
||||||
<Slider
|
|
||||||
label="learning rate"
|
|
||||||
value={ctx.learningRate}
|
|
||||||
min={0.000001}
|
|
||||||
max={0.01}
|
|
||||||
step={0.000001}
|
|
||||||
onChange={ctx.setLearningRate}
|
|
||||||
format={(v) => v.toExponential(1)}
|
|
||||||
/>
|
|
||||||
<Slider label="decay" value={ctx.decay} min={0.8} max={1} step={0.001} onChange={ctx.setDecay} />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -569,17 +563,14 @@ function ModeConfig(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||||
</Button>
|
</Button>
|
||||||
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>audio starts on the play gesture</span>
|
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>audio starts on the play gesture</span>
|
||||||
</div>
|
</div>
|
||||||
<Slider label="master volume" value={ctx.volume} min={0} max={1} step={0.01} onChange={ctx.setVolume} />
|
|
||||||
{depth === 'expanded' && (
|
{depth === 'expanded' && (
|
||||||
<>
|
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||||
<SectionLabel>Tempo</SectionLabel>
|
The active engine follows the selected mode ({ctx.mode.label}).
|
||||||
<Slider label="bpm" value={ctx.bpm} min={40} max={220} step={1} onChange={ctx.setBpm} />
|
{/* TODO(dock-spec §5): arpeggiator + tiered synth presets + the
|
||||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
18-section group-override matrix are workstream E. Master
|
||||||
The active engine follows the selected mode ({ctx.mode.label}).
|
volume + bpm sliders were deleted (simplification audit
|
||||||
{/* TODO(dock-spec §5): arpeggiator + tiered synth presets + the
|
S16) — they drove no engine parameter. */}
|
||||||
18-section group-override matrix are workstream E. */}
|
</p>
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
@ -624,7 +615,6 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||||
}, {});
|
}, {});
|
||||||
const mutedN = ctx.params.filter((p) => p.muted).length;
|
const mutedN = ctx.params.filter((p) => p.muted).length;
|
||||||
const modeDesc = outputModeDescriptor(ctx.outputMode);
|
const modeDesc = outputModeDescriptor(ctx.outputMode);
|
||||||
const backend = BACKENDS.find((b) => b.id === modeDesc.backend) ?? BACKENDS[0];
|
|
||||||
// The particle Mode names its outputs; otherwise use the param names.
|
// The particle Mode names its outputs; otherwise use the param names.
|
||||||
const nameFor = (idx: number, fallback: string) =>
|
const nameFor = (idx: number, fallback: string) =>
|
||||||
ctx.outputMode === 'particles' ? VISUAL_NAMES[idx] ?? fallback : fallback;
|
ctx.outputMode === 'particles' ? VISUAL_NAMES[idx] ?? fallback : fallback;
|
||||||
|
|
@ -671,12 +661,6 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||||
{!expanded && ctx.params.length > 6 && (
|
{!expanded && ctx.params.length > 6 && (
|
||||||
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>+{ctx.params.length - 6} more — expand to edit</span>
|
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>+{ctx.params.length - 6} more — expand to edit</span>
|
||||||
)}
|
)}
|
||||||
{expanded && (
|
|
||||||
<>
|
|
||||||
<SectionLabel>Advanced · {modeDesc.label}</SectionLabel>
|
|
||||||
<BackendAdvanced backend={modeDesc.backend} params={ctx.params} setParam={ctx.setParam} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
/**
|
|
||||||
* InputMini — the input demoted to a compact secondary control for output-first
|
|
||||||
* views. Ported from `InputMini.jsx`.
|
|
||||||
*/
|
|
||||||
import { VirtualJoystick, XYPad } from '../primitives';
|
|
||||||
import type { MFMode } from './model';
|
|
||||||
|
|
||||||
export interface InputMiniProps {
|
|
||||||
mode: MFMode;
|
|
||||||
pos: [number, number];
|
|
||||||
onMove: (x: number, y: number) => void;
|
|
||||||
noiseCap?: number;
|
|
||||||
size?: number;
|
|
||||||
corner?: 'bottom-left' | 'bottom-right' | 'top-left';
|
|
||||||
/** Input-map shape override (Settings). Falls back to the mode's input. */
|
|
||||||
variant?: 'rectangular' | 'circular';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InputMini({
|
|
||||||
mode,
|
|
||||||
pos,
|
|
||||||
onMove,
|
|
||||||
size = 132,
|
|
||||||
corner = 'bottom-left',
|
|
||||||
variant,
|
|
||||||
}: InputMiniProps) {
|
|
||||||
const circular = variant ? variant === 'circular' : mode.input === 'joystick';
|
|
||||||
const place = {
|
|
||||||
'bottom-left': { bottom: 14, left: 14 },
|
|
||||||
'bottom-right': { bottom: 14, right: 14 },
|
|
||||||
'top-left': { top: 14, left: 14 },
|
|
||||||
}[corner];
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
zIndex: 22,
|
|
||||||
...place,
|
|
||||||
background: 'var(--glass)',
|
|
||||||
backdropFilter: 'blur(12px)',
|
|
||||||
WebkitBackdropFilter: 'blur(12px)',
|
|
||||||
border: '1px solid var(--glass-line)',
|
|
||||||
borderRadius: 'var(--r-2)',
|
|
||||||
padding: 'var(--sp-2)',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: 6,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 10,
|
|
||||||
color: 'var(--fg-dim)',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.1em',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
input · {circular ? 'joy' : 'xy'}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
style={{ fontSize: 10, color: 'var(--fg-mute)', fontVariantNumeric: 'tabular-nums' }}
|
|
||||||
>
|
|
||||||
{pos[0].toFixed(2)},{pos[1].toFixed(2)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{circular ? (
|
|
||||||
<VirtualJoystick size={size} position={pos} onMove={(x, y) => onMove(x, y)} />
|
|
||||||
) : (
|
|
||||||
<XYPad size={size} position={pos} onMove={(x, y) => onMove(x, y)} showGrid />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,219 +0,0 @@
|
||||||
/**
|
|
||||||
* ReadoutStrip — the output heatmap as a thin top control strip. Same model as
|
|
||||||
* OutputStage. Ported from `ReadoutStrip.jsx`.
|
|
||||||
*/
|
|
||||||
import { useRef, useState } from 'react';
|
|
||||||
import type { PointerEvent as ReactPointerEvent } from 'react';
|
|
||||||
import type { MFParam, ParamStatus } from './model';
|
|
||||||
import { OutputEditor } from './OutputEditor';
|
|
||||||
|
|
||||||
const RS_GROUP_COLOR: Record<string, string> = {
|
|
||||||
formant: '--accent',
|
|
||||||
pitch: '--accent-2',
|
|
||||||
amp: '--good',
|
|
||||||
filter: '--warn',
|
|
||||||
fx: '--info',
|
|
||||||
mod: '--accent-3',
|
|
||||||
};
|
|
||||||
const RS_NEXT: Record<ParamStatus, ParamStatus> = { off: 'fixed', fixed: 'live', live: 'off' };
|
|
||||||
|
|
||||||
export interface ReadoutStripProps {
|
|
||||||
params: MFParam[];
|
|
||||||
values: number[];
|
|
||||||
onChange: (i: number, patch: Partial<MFParam>) => void;
|
|
||||||
pinned: boolean;
|
|
||||||
onTogglePin: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ReadoutStrip({ params, values, onChange, pinned, onTogglePin }: ReadoutStripProps) {
|
|
||||||
const [open, setOpen] = useState<number | null>(null);
|
|
||||||
const timers = useRef<{ open: ReturnType<typeof setTimeout> | null; close: ReturnType<typeof setTimeout> | null }>({
|
|
||||||
open: null,
|
|
||||||
close: null,
|
|
||||||
});
|
|
||||||
const drag = useRef<{ i: number; moved: boolean; startY: number; el: HTMLDivElement | null; alt: boolean }>({
|
|
||||||
i: -1,
|
|
||||||
moved: false,
|
|
||||||
startY: 0,
|
|
||||||
el: null,
|
|
||||||
alt: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const scheduleOpen = (i: number) => {
|
|
||||||
if (timers.current.close) clearTimeout(timers.current.close);
|
|
||||||
if (timers.current.open) clearTimeout(timers.current.open);
|
|
||||||
timers.current.open = setTimeout(() => setOpen(i), 110);
|
|
||||||
};
|
|
||||||
const scheduleClose = () => {
|
|
||||||
if (timers.current.open) clearTimeout(timers.current.open);
|
|
||||||
if (timers.current.close) clearTimeout(timers.current.close);
|
|
||||||
timers.current.close = setTimeout(() => setOpen(null), 280);
|
|
||||||
};
|
|
||||||
const hold = () => {
|
|
||||||
if (timers.current.close) clearTimeout(timers.current.close);
|
|
||||||
};
|
|
||||||
|
|
||||||
const valFromEvent = (el: HTMLDivElement, clientY: number) => {
|
|
||||||
const r = el.getBoundingClientRect();
|
|
||||||
return Math.max(0, Math.min(1, 1 - (clientY - r.top) / r.height));
|
|
||||||
};
|
|
||||||
const down = (e: ReactPointerEvent<HTMLDivElement>, i: number) => {
|
|
||||||
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
||||||
drag.current = {
|
|
||||||
i,
|
|
||||||
moved: false,
|
|
||||||
startY: e.clientY,
|
|
||||||
el: e.currentTarget,
|
|
||||||
alt: e.altKey || e.metaKey,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const move = (e: ReactPointerEvent<HTMLDivElement>, i: number) => {
|
|
||||||
const d = drag.current;
|
|
||||||
if (d.i !== i) return;
|
|
||||||
if (Math.abs(e.clientY - d.startY) > 3) d.moved = true;
|
|
||||||
if (d.moved && !d.alt && d.el) onChange(i, { val: valFromEvent(d.el, e.clientY) });
|
|
||||||
};
|
|
||||||
const up = (e: ReactPointerEvent<HTMLDivElement>, i: number) => {
|
|
||||||
const d = drag.current;
|
|
||||||
if (d.i !== i) return;
|
|
||||||
if (d.alt && !d.moved) onChange(i, { status: RS_NEXT[params[i].status] || 'live' });
|
|
||||||
else if (!d.moved && d.el) onChange(i, { val: valFromEvent(d.el, e.clientY) });
|
|
||||||
drag.current = { i: -1, moved: false, startY: 0, el: null, alt: false };
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'stretch',
|
|
||||||
gap: 2,
|
|
||||||
height: 76,
|
|
||||||
padding: '0 2px',
|
|
||||||
background: 'var(--glass)',
|
|
||||||
backdropFilter: 'blur(12px)',
|
|
||||||
WebkitBackdropFilter: 'blur(12px)',
|
|
||||||
borderBottom: '1px solid var(--glass-line)',
|
|
||||||
position: 'relative',
|
|
||||||
zIndex: 30,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onTogglePin}
|
|
||||||
title="Pin strip open"
|
|
||||||
style={{
|
|
||||||
flex: '0 0 auto',
|
|
||||||
width: 30,
|
|
||||||
border: 0,
|
|
||||||
background: 'transparent',
|
|
||||||
color: pinned ? 'var(--accent)' : 'var(--fg-dim)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: 'var(--fs-md)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{pinned ? '📌' : '▾'}
|
|
||||||
</button>
|
|
||||||
{params.map((p, i) => {
|
|
||||||
const eff = values[i] ?? 0;
|
|
||||||
const gc = `var(${RS_GROUP_COLOR[p.group] || '--accent'})`;
|
|
||||||
const dim = p.status === 'off';
|
|
||||||
const placeRight = i > params.length - 5;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 0,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
}}
|
|
||||||
onPointerLeave={scheduleClose}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
onPointerEnter={() => scheduleOpen(i)}
|
|
||||||
style={{
|
|
||||||
textAlign: 'center',
|
|
||||||
fontSize: 8,
|
|
||||||
fontFamily: 'var(--font-mono)',
|
|
||||||
lineHeight: '11px',
|
|
||||||
cursor: 'help',
|
|
||||||
color: p.status === 'live' ? 'var(--fg-dim)' : gc,
|
|
||||||
overflow: 'hidden',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{p.name}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onPointerDown={(e) => down(e, i)}
|
|
||||||
onPointerMove={(e) => move(e, i)}
|
|
||||||
onPointerUp={(e) => up(e, i)}
|
|
||||||
onPointerCancel={(e) => up(e, i)}
|
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
flex: 1,
|
|
||||||
background: 'var(--bg)',
|
|
||||||
borderRadius: 2,
|
|
||||||
overflow: 'hidden',
|
|
||||||
cursor: 'ns-resize',
|
|
||||||
opacity: dim ? 0.5 : 1,
|
|
||||||
touchAction: 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
height: `${eff * 100}%`,
|
|
||||||
background: gc,
|
|
||||||
opacity: 0.25 + eff * 0.6,
|
|
||||||
transition: 'height 60ms linear',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{p.status === 'live' && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: `${p.val * 100}%`,
|
|
||||||
height: 0,
|
|
||||||
borderTop: '1px dashed rgba(255,255,255,0.3)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{p.status !== 'live' && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 1,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
textAlign: 'center',
|
|
||||||
fontSize: 8,
|
|
||||||
color: p.status === 'fixed' ? 'var(--accent-2)' : 'var(--fg-dim)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{p.status === 'fixed' ? '⊟' : '∅'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{open === i && (
|
|
||||||
<OutputEditor
|
|
||||||
param={p}
|
|
||||||
onChange={(patch) => onChange(i, patch)}
|
|
||||||
onHold={hold}
|
|
||||||
onLeave={scheduleClose}
|
|
||||||
place={{ top: 'calc(100% + 6px)', [placeRight ? 'right' : 'left']: 0 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
/**
|
|
||||||
* SplitStage — input and output given EQUAL prominence, side by side. Left =
|
|
||||||
* Manifold (input), right = OutputStage (output field). Ported from `SplitStage.jsx`.
|
|
||||||
*/
|
|
||||||
import { Manifold } from './Manifold';
|
|
||||||
import { OutputStage } from './OutputStage';
|
|
||||||
import type { MFParam } from './model';
|
|
||||||
import type { FeedbackMarker, Pin } from './types';
|
|
||||||
|
|
||||||
export interface SplitStageProps {
|
|
||||||
pos: [number, number];
|
|
||||||
onMove: (x: number, y: number) => void;
|
|
||||||
noiseCap: number;
|
|
||||||
pins: Pin[];
|
|
||||||
markers?: FeedbackMarker[];
|
|
||||||
variant?: 'rectangular' | 'circular';
|
|
||||||
follow: boolean;
|
|
||||||
onLongPress: (p: [number, number]) => void;
|
|
||||||
params: MFParam[];
|
|
||||||
values: number[];
|
|
||||||
onChange: (i: number, patch: Partial<MFParam>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SplitStage({
|
|
||||||
pos,
|
|
||||||
onMove,
|
|
||||||
noiseCap,
|
|
||||||
pins,
|
|
||||||
markers = [],
|
|
||||||
variant = 'rectangular',
|
|
||||||
follow,
|
|
||||||
onLongPress,
|
|
||||||
params,
|
|
||||||
values,
|
|
||||||
onChange,
|
|
||||||
}: SplitStageProps) {
|
|
||||||
return (
|
|
||||||
<div style={{ position: 'absolute', inset: 0, display: 'flex' }}>
|
|
||||||
<div
|
|
||||||
style={{ flex: 1, position: 'relative', borderRight: '1px solid var(--line)', minWidth: 0 }}
|
|
||||||
>
|
|
||||||
<Manifold
|
|
||||||
pos={pos}
|
|
||||||
onMove={onMove}
|
|
||||||
noiseCap={noiseCap}
|
|
||||||
pins={pins}
|
|
||||||
markers={markers}
|
|
||||||
variant={variant}
|
|
||||||
follow={follow}
|
|
||||||
onLongPress={onLongPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1, position: 'relative', minWidth: 0 }}>
|
|
||||||
<OutputStage params={params} values={values} onChange={onChange} compact />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* VerdictCluster — floating bottom-centre control, the app's main verdict.
|
* VerdictCluster — floating bottom-centre control, the app's main verdict.
|
||||||
* ▽ perturb (thumbs-down) · ↺ undo · △ commit (thumbs-up), + A/B toggle.
|
* ▽ perturb (thumbs-down) · ↺ undo · △ commit (thumbs-up).
|
||||||
* Long-press perturb = full re-roll. Ported from `VerdictCluster.jsx`.
|
* Long-press perturb = full re-roll. Ported from `VerdictCluster.jsx`.
|
||||||
*
|
*
|
||||||
* The cluster reflects the ACTIVE feedback mode (workstream B; rl-feedback §0):
|
* The cluster reflects the ACTIVE feedback mode (workstream B; rl-feedback §0):
|
||||||
|
|
@ -50,9 +50,6 @@ export interface VerdictClusterProps {
|
||||||
/** Full re-roll of the current net (right half of the pill). */
|
/** Full re-roll of the current net (right half of the pill). */
|
||||||
onRandomise: () => void;
|
onRandomise: () => void;
|
||||||
canUndo: boolean;
|
canUndo: boolean;
|
||||||
ab: 'A' | 'B';
|
|
||||||
onToggleAB: () => void;
|
|
||||||
onHoldA: (holding: boolean) => void;
|
|
||||||
firstSession: boolean;
|
firstSession: boolean;
|
||||||
/** Active feedback mode — drives the cluster's labels/tones (rl-feedback §0). */
|
/** Active feedback mode — drives the cluster's labels/tones (rl-feedback §0). */
|
||||||
feedbackMode: FeedbackModeUI;
|
feedbackMode: FeedbackModeUI;
|
||||||
|
|
@ -70,9 +67,6 @@ export function VerdictCluster({
|
||||||
onNudge,
|
onNudge,
|
||||||
onRandomise,
|
onRandomise,
|
||||||
canUndo,
|
canUndo,
|
||||||
ab,
|
|
||||||
onToggleAB,
|
|
||||||
onHoldA,
|
|
||||||
firstSession,
|
firstSession,
|
||||||
feedbackMode,
|
feedbackMode,
|
||||||
exploring,
|
exploring,
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,13 @@
|
||||||
* Console barrel — the convertible Console shell, wired to the real engine.
|
* Console barrel — the convertible Console shell, wired to the real engine.
|
||||||
*/
|
*/
|
||||||
export { ConsoleApp } from './ConsoleApp';
|
export { ConsoleApp } from './ConsoleApp';
|
||||||
export type { ConsoleAppProps } from './ConsoleApp';
|
|
||||||
export { CompositeStage } from './CompositeStage';
|
export { CompositeStage } from './CompositeStage';
|
||||||
export { SplitStage } from './SplitStage';
|
|
||||||
export { OutputStage } from './OutputStage';
|
export { OutputStage } from './OutputStage';
|
||||||
export { ReadoutStrip } from './ReadoutStrip';
|
|
||||||
export { Manifold } from './Manifold';
|
export { Manifold } from './Manifold';
|
||||||
export { InputMini } from './InputMini';
|
|
||||||
export { VerdictCluster } from './VerdictCluster';
|
export { VerdictCluster } from './VerdictCluster';
|
||||||
export { Dock } from './Dock';
|
export { Dock } from './Dock';
|
||||||
export { DRAWERS } from './Drawers';
|
export { DRAWERS } from './Drawers';
|
||||||
export { AltitudeNav, MiniMeters, CompactAxis } from './shared-ui';
|
export { MiniMeters } from './shared-ui';
|
||||||
export { MF_MODES, shapeValues, applyCurve, seededGradient, modeEngineId } from './model';
|
export { MF_MODES, shapeValues, applyCurve, modeEngineId } from './model';
|
||||||
export type { MFMode, MFParam, ParamStatus } from './model';
|
export type { MFMode, MFParam, ParamStatus } from './model';
|
||||||
export type { Focus, ConsoleCtx } from './types';
|
export type { ConsoleCtx } from './types';
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ export const DEFAULT_MODE_ML: ModeML = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-output control row — the unified store used by both the stage
|
* Per-output control row — the unified store used by both the stage
|
||||||
* (OutputStage / ReadoutStrip) and the Outputs/Routing dock. `status` is the
|
* (OutputStage) and the Outputs/Routing dock. `status` is the
|
||||||
* model-control tri-state; `muted` and `armed` are ORTHOGONAL modifiers
|
* model-control tri-state; `muted` and `armed` are ORTHOGONAL modifiers
|
||||||
* (dock-spec §3.2 — the deliberate split of the deployed conflated
|
* (dock-spec §3.2 — the deliberate split of the deployed conflated
|
||||||
* frozen↔muted field). Backend-specific specs are populated by the active
|
* frozen↔muted field). Backend-specific specs are populated by the active
|
||||||
|
|
@ -302,22 +302,6 @@ export function shapeValues(params: MFParam[], engineOut: Float32Array | null):
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Deterministic per-revision gradient-flow stub (visual only; ported as-is). */
|
|
||||||
export function seededGradient(rev: number): {
|
|
||||||
norms: number[];
|
|
||||||
status: string[];
|
|
||||||
} {
|
|
||||||
const n = 4;
|
|
||||||
const norms: number[] = [];
|
|
||||||
const status: string[] = [];
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
const r = Math.abs((Math.sin((rev + 1) * (i + 1) * 12.9898) * 43758.5453) % 1);
|
|
||||||
norms.push(0.2 + r * 0.8);
|
|
||||||
status.push(r > 0.85 ? 'exploding' : r < 0.18 ? 'vanishing' : r < 0.3 ? 'converged' : 'healthy');
|
|
||||||
}
|
|
||||||
return { norms, status };
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Map a mode id → the audio-engine backend id. Mode ids align with engine ids
|
/** Map a mode id → the audio-engine backend id. Mode ids align with engine ids
|
||||||
* except `slp_workshop` (runs the memlcelium engine), the analysis controller,
|
* except `slp_workshop` (runs the memlcelium engine), the analysis controller,
|
||||||
* and the relabelled `c15`. */
|
* and the relabelled `c15`. */
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,6 @@
|
||||||
*/
|
*/
|
||||||
import type { OutputMode } from './types';
|
import type { OutputMode } from './types';
|
||||||
import type { BackendId } from '../dock/output-state';
|
import type { BackendId } from '../dock/output-state';
|
||||||
import type {
|
|
||||||
ParticleIcon,
|
|
||||||
MidiIcon,
|
|
||||||
OscIcon,
|
|
||||||
SynthIcon,
|
|
||||||
EditorIcon,
|
|
||||||
} from './icons';
|
|
||||||
|
|
||||||
export interface OutputModeDescriptor {
|
export interface OutputModeDescriptor {
|
||||||
id: OutputMode;
|
id: OutputMode;
|
||||||
|
|
@ -87,11 +80,3 @@ export const DEFAULT_OUTPUT_MODE: OutputMode = OUTPUT_MODES[0].id;
|
||||||
export function outputModeDescriptor(id: OutputMode): OutputModeDescriptor {
|
export function outputModeDescriptor(id: OutputMode): OutputModeDescriptor {
|
||||||
return OUTPUT_MODES.find((m) => m.id === id) ?? OUTPUT_MODES[0];
|
return OUTPUT_MODES.find((m) => m.id === id) ?? OUTPUT_MODES[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The monochrome icon component for a Mode (resolved by the dock). */
|
|
||||||
export type ModeIconComponent =
|
|
||||||
| typeof ParticleIcon
|
|
||||||
| typeof MidiIcon
|
|
||||||
| typeof OscIcon
|
|
||||||
| typeof SynthIcon
|
|
||||||
| typeof EditorIcon;
|
|
||||||
|
|
|
||||||
|
|
@ -1,85 +1,13 @@
|
||||||
/**
|
/**
|
||||||
* Console — shared chrome for the simpler altitudes. Ported from `shared-ui.jsx`.
|
* Console — shared chrome. Ported from `shared-ui.jsx`.
|
||||||
*
|
*
|
||||||
* AltitudeNav no longer navigates to separate HTML files (the JSX's href model);
|
* AltitudeNav and CompactAxis were deleted 2026-07 (simplification audit S15) —
|
||||||
* the focus switch is driven by React state via `onFocus`. The altitude pills
|
* both were part of the dead focus/altitude system (setFocus was never called;
|
||||||
* (Console / Perform / Zen) are inert here — Manifold ships a single altitude.
|
* Manifold ships a single "composite" altitude). MiniMeters survives — it is
|
||||||
|
* the live glanceable output readout used by CompositeStage's minimap.
|
||||||
*/
|
*/
|
||||||
import type { CSSProperties } from 'react';
|
|
||||||
import type { Focus } from './types';
|
|
||||||
import type { MFParam } from './model';
|
import type { MFParam } from './model';
|
||||||
|
|
||||||
const FOCI: [Focus, string, string][] = [
|
|
||||||
['in', 'IN', 'Input-first'],
|
|
||||||
['split', 'DUAL', 'Input + output equal'],
|
|
||||||
['out', 'OUT', 'Output-first'],
|
|
||||||
['composite', 'FLEX', 'Composite — drag to rebalance'],
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface AltitudeNavProps {
|
|
||||||
current?: string;
|
|
||||||
focus?: Focus;
|
|
||||||
onFocus?: (f: Focus) => void;
|
|
||||||
style?: CSSProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AltitudeNav({ current = 'console', focus = 'in', onFocus, style }: AltitudeNavProps) {
|
|
||||||
const items = [
|
|
||||||
{ id: 'console', dots: '◆◆◆', label: 'Console' },
|
|
||||||
{ id: 'perform', dots: '◆◆', label: 'Perform' },
|
|
||||||
{ id: 'zen', dots: '◆', label: 'Zen' },
|
|
||||||
];
|
|
||||||
const pill = (on: boolean): CSSProperties => ({
|
|
||||||
textDecoration: 'none',
|
|
||||||
fontSize: 11,
|
|
||||||
padding: '2px 8px',
|
|
||||||
borderRadius: 'var(--r-pill)',
|
|
||||||
color: on ? 'var(--accent)' : 'var(--fg-dim)',
|
|
||||||
background: on ? 'rgba(255,106,0,0.14)' : 'transparent',
|
|
||||||
border: 'none',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontFamily: 'var(--font-mono)',
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 12,
|
|
||||||
right: 14,
|
|
||||||
zIndex: 70,
|
|
||||||
display: 'flex',
|
|
||||||
gap: 6,
|
|
||||||
alignItems: 'center',
|
|
||||||
background: 'var(--glass)',
|
|
||||||
backdropFilter: 'blur(10px)',
|
|
||||||
WebkitBackdropFilter: 'blur(10px)',
|
|
||||||
border: '1px solid var(--glass-line)',
|
|
||||||
borderRadius: 'var(--r-pill)',
|
|
||||||
padding: '4px 6px',
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{items.map((it) => (
|
|
||||||
<span key={it.id} title={`${it.label} · ${focus}`} style={pill(it.id === current)}>
|
|
||||||
{it.dots}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
<span style={{ width: 1, height: 16, background: 'var(--glass-line)' }} />
|
|
||||||
{FOCI.map(([f, label, title]) => (
|
|
||||||
<button
|
|
||||||
key={f}
|
|
||||||
type="button"
|
|
||||||
title={title}
|
|
||||||
onClick={() => onFocus?.(f)}
|
|
||||||
style={{ ...pill(focus === f), fontSize: 9, letterSpacing: '0.08em' }}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const MM_GROUP_COLOR: Record<string, string> = {
|
const MM_GROUP_COLOR: Record<string, string> = {
|
||||||
formant: '--accent',
|
formant: '--accent',
|
||||||
pitch: '--accent-2',
|
pitch: '--accent-2',
|
||||||
|
|
@ -122,66 +50,3 @@ export function MiniMeters({ params, values }: { params: MFParam[]; values: numb
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** CompactAxis — slim labelled feel slider (Perform bar; kept for parity). */
|
|
||||||
export function CompactAxis({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
accent = 'var(--accent)',
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: number;
|
|
||||||
onChange: (v: number) => void;
|
|
||||||
accent?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 'var(--sp-2)',
|
|
||||||
fontFamily: 'var(--font-mono)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
width: 64,
|
|
||||||
fontSize: 10,
|
|
||||||
color: 'var(--fg-mute)',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.08em',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="1"
|
|
||||||
step="0.01"
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(parseFloat(e.target.value))}
|
|
||||||
className="mf-slider-input"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
width: 120,
|
|
||||||
['--mf-axis-accent' as string]: accent,
|
|
||||||
['--mf-pct' as string]: `${Math.max(0, Math.min(1, value))}`,
|
|
||||||
} as CSSProperties
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
width: '3ch',
|
|
||||||
fontSize: 10,
|
|
||||||
color: 'var(--fg-dim)',
|
|
||||||
fontVariantNumeric: 'tabular-nums',
|
|
||||||
textAlign: 'right',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{value.toFixed(2)}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
* Console — shared prop/context types used across the stage + dock components.
|
* Console — shared prop/context types used across the stage + dock components.
|
||||||
*/
|
*/
|
||||||
import type { MFMode, MFParam } from './model';
|
import type { MFMode, MFParam } from './model';
|
||||||
import type { BackendId } from '../dock/output-state';
|
|
||||||
import type { FeedbackMode } from '../engine/types';
|
import type { FeedbackMode } from '../engine/types';
|
||||||
import type { BackendStatus } from '../backends/backend';
|
import type { BackendStatus } from '../backends/backend';
|
||||||
import type { UseInputLayer } from '../inputs';
|
import type { UseInputLayer } from '../inputs';
|
||||||
|
|
@ -35,37 +34,34 @@ export interface FeedbackMarker {
|
||||||
polarity: 'positive' | 'negative';
|
polarity: 'positive' | 'negative';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Snapshot {
|
|
||||||
id: number;
|
|
||||||
tag: string;
|
|
||||||
noise: number;
|
|
||||||
seed: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DrawerKey = 'learn' | 'inputs' | 'route' | 'settings' | 'help';
|
export type DrawerKey = 'learn' | 'inputs' | 'route' | 'settings' | 'help';
|
||||||
export type DrawerDepth = 'condensed' | 'expanded';
|
export type DrawerDepth = 'condensed' | 'expanded';
|
||||||
export type Focus = 'in' | 'split' | 'out' | 'composite';
|
|
||||||
|
|
||||||
export interface Axes {
|
/**
|
||||||
boldness: number;
|
* The flat context the Dock + drawers read. Pruned 2026-07 (simplification
|
||||||
memory: number;
|
* audit S19) to the fields Dock/Drawers/OutputsBackendConfig actually consume.
|
||||||
precision: number;
|
* Deleted outright: the Axes type + axes/setAxis state, the
|
||||||
}
|
* preset/setPreset/offsetActive chain (permanently 'Sculpt'/false), the
|
||||||
|
* markers/outputBackend/setOutputBackend/cycleStatus fields (each had a zero-
|
||||||
/** The flat context the Dock + drawers read. */
|
* consumer duplicate elsewhere — see ConsoleApp.tsx), and everything S16/S18/
|
||||||
|
* L1/L20 removed (decorative sliders, snapshots, health/gradient visuals,
|
||||||
|
* BackendAdvanced's catalogue).
|
||||||
|
*
|
||||||
|
* `modes` / `setModeId` are KEPT despite having no renderer today — this is
|
||||||
|
* the exact plumbing the Phase-5 instrument-mode picker is built on (A7/A3),
|
||||||
|
* not dead code. `busy` / `addingExample` / `onAddExample` / `onTrain` / `loss`
|
||||||
|
* are ALSO kept even though no drawer reads them either: unlike the fields
|
||||||
|
* above they drive real engine calls (engine.addExample / engine.train /
|
||||||
|
* engine.evalLoss), so — pending confirmation either way — they read as
|
||||||
|
* unfinished plumbing rather than confirmed-dead decoration; deleting them
|
||||||
|
* was out of this pass's authorized scope.
|
||||||
|
*/
|
||||||
export interface ConsoleCtx {
|
export interface ConsoleCtx {
|
||||||
modes: MFMode[];
|
modes: MFMode[];
|
||||||
modeId: string;
|
modeId: string;
|
||||||
setModeId: (id: string) => void;
|
setModeId: (id: string) => void;
|
||||||
mode: MFMode;
|
mode: MFMode;
|
||||||
|
|
||||||
axes: Axes;
|
|
||||||
setAxis: (k: keyof Axes, v: number) => void;
|
|
||||||
|
|
||||||
preset: string;
|
|
||||||
setPreset: (p: string) => void;
|
|
||||||
offsetActive: boolean;
|
|
||||||
|
|
||||||
datasetCount: number;
|
datasetCount: number;
|
||||||
loss: number[];
|
loss: number[];
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
|
|
@ -74,15 +70,9 @@ export interface ConsoleCtx {
|
||||||
onTrain: () => void;
|
onTrain: () => void;
|
||||||
onClear: () => void;
|
onClear: () => void;
|
||||||
|
|
||||||
snapshots: Snapshot[];
|
|
||||||
onJump: (id: number) => void;
|
|
||||||
|
|
||||||
params: MFParam[];
|
params: MFParam[];
|
||||||
cycleStatus: (i: number) => void;
|
|
||||||
/** Patch one output row in the shared store (drives stage + dock in sync). */
|
/** Patch one output row in the shared store (drives stage + dock in sync). */
|
||||||
setParam: (i: number, patch: Partial<MFParam>) => void;
|
setParam: (i: number, patch: Partial<MFParam>) => void;
|
||||||
outputBackend: BackendId;
|
|
||||||
setOutputBackend: (v: BackendId) => void;
|
|
||||||
|
|
||||||
// ---- Output backend transport (backends-spec §1–§5) ----
|
// ---- Output backend transport (backends-spec §1–§5) ----
|
||||||
/** Live status of the active output backend (MIDI/OSC connect state, etc.). */
|
/** Live status of the active output backend (MIDI/OSC connect state, etc.). */
|
||||||
|
|
@ -117,23 +107,12 @@ export interface ConsoleCtx {
|
||||||
outputMode: OutputMode;
|
outputMode: OutputMode;
|
||||||
setOutputMode: (m: OutputMode) => void;
|
setOutputMode: (m: OutputMode) => void;
|
||||||
|
|
||||||
// ---- Feedback markers on the 2D map (both polarities) ----
|
|
||||||
/** Markers plotted at the input location where each feedback was given. */
|
|
||||||
markers: FeedbackMarker[];
|
|
||||||
|
|
||||||
// ---- Modular input layer (workstream F; inputs-spec) ----
|
// ---- Modular input layer (workstream F; inputs-spec) ----
|
||||||
/** The composed input layer: source enable/config/status + channel layout. */
|
/** The composed input layer: source enable/config/status + channel layout. */
|
||||||
inputs: UseInputLayer;
|
inputs: UseInputLayer;
|
||||||
|
|
||||||
health: number;
|
|
||||||
gradient: number[];
|
|
||||||
gradientStatus: string[];
|
|
||||||
weightsRevision: number;
|
|
||||||
|
|
||||||
spread: boolean;
|
spread: boolean;
|
||||||
setSpread: (v: boolean) => void;
|
setSpread: (v: boolean) => void;
|
||||||
tame: number;
|
|
||||||
setTame: (v: number) => void;
|
|
||||||
noiseCap: number;
|
noiseCap: number;
|
||||||
setNoiseCap: (v: number) => void;
|
setNoiseCap: (v: number) => void;
|
||||||
|
|
||||||
|
|
@ -151,14 +130,6 @@ export interface ConsoleCtx {
|
||||||
/** Clear all arm flags ("Arm all"). */
|
/** Clear all arm flags ("Arm all"). */
|
||||||
clearArmed: () => void;
|
clearArmed: () => void;
|
||||||
|
|
||||||
// ---- Live training params (dock-spec §1.3) ----
|
|
||||||
learningRate: number;
|
|
||||||
setLearningRate: (v: number) => void;
|
|
||||||
decay: number;
|
|
||||||
setDecay: (v: number) => void;
|
|
||||||
spreadLevel: number;
|
|
||||||
setSpreadLevel: (v: number) => void;
|
|
||||||
|
|
||||||
// ---- Exploration gestures (one-core-engine §P1; interim TS shells) ----
|
// ---- Exploration gestures (one-core-engine §P1; interim TS shells) ----
|
||||||
/** True while the Jolt press-and-hold weight-morph is engaged. */
|
/** True while the Jolt press-and-hold weight-morph is engaged. */
|
||||||
joltActive: boolean;
|
joltActive: boolean;
|
||||||
|
|
@ -173,10 +144,6 @@ export interface ConsoleCtx {
|
||||||
// ---- Synth engine (dock-spec §5) ----
|
// ---- Synth engine (dock-spec §5) ----
|
||||||
audioStarted: boolean;
|
audioStarted: boolean;
|
||||||
onToggleAudio: () => void;
|
onToggleAudio: () => void;
|
||||||
volume: number;
|
|
||||||
setVolume: (v: number) => void;
|
|
||||||
bpm: number;
|
|
||||||
setBpm: (v: number) => void;
|
|
||||||
|
|
||||||
// ---- Explore-and-place scratchpad session (workstream B; rl-feedback §2.2) ----
|
// ---- Explore-and-place scratchpad session (workstream B; rl-feedback §2.2) ----
|
||||||
/** True while awaiting a manifold location pick after pressing "place". */
|
/** True while awaiting a manifold location pick after pressing "place". */
|
||||||
|
|
|
||||||
|
|
@ -1,354 +0,0 @@
|
||||||
/**
|
|
||||||
* BackendAdvanced — the FULL-depth advanced backend modal bodies (dock-spec §4).
|
|
||||||
* One editor per backend. All backends share the §3.1 baseline (rendered as
|
|
||||||
* OutputControlRow elsewhere); these add the backend-specific fields.
|
|
||||||
*
|
|
||||||
* The backend transport (backends-spec workstream E) is now LIVE: editing these
|
|
||||||
* fields writes the shared MFParam store, which the BackendManager reads to send
|
|
||||||
* real Web MIDI CC / OSC-over-WS. This modal is the full-depth duplicate of the
|
|
||||||
* inline config in OutputsBackendConfig; both write the same store.
|
|
||||||
*/
|
|
||||||
import type { MFParam } from '../console/model';
|
|
||||||
import type { BackendId, CvChannelId } from './output-state';
|
|
||||||
import { CV_CHANNELS, defaultCvSpec, defaultMidiSpec, defaultOscSpec } from './output-state';
|
|
||||||
|
|
||||||
function num(s: string, fallback: number): number {
|
|
||||||
const v = parseFloat(s);
|
|
||||||
return Number.isFinite(v) ? v : fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cellInput: React.CSSProperties = {
|
|
||||||
width: '100%',
|
|
||||||
background: 'var(--bg-1)',
|
|
||||||
border: '1px solid var(--line)',
|
|
||||||
borderRadius: 'var(--r-1)',
|
|
||||||
color: 'var(--fg)',
|
|
||||||
fontFamily: 'var(--font-mono)',
|
|
||||||
fontSize: 'var(--fs-xs)',
|
|
||||||
padding: '3px 6px',
|
|
||||||
};
|
|
||||||
|
|
||||||
function Th({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<th
|
|
||||||
style={{
|
|
||||||
textAlign: 'left',
|
|
||||||
fontSize: 9,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.06em',
|
|
||||||
color: 'var(--fg-dim)',
|
|
||||||
padding: '4px 6px',
|
|
||||||
borderBottom: '1px solid var(--line)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</th>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BackendAdvancedProps {
|
|
||||||
backend: BackendId;
|
|
||||||
params: MFParam[];
|
|
||||||
setParam: (i: number, patch: Partial<MFParam>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BackendAdvanced({ backend, params, setParam }: BackendAdvancedProps) {
|
|
||||||
switch (backend) {
|
|
||||||
case 'midi':
|
|
||||||
return <MidiCcEditor params={params} setParam={setParam} />;
|
|
||||||
case 'osc':
|
|
||||||
return <OscPathEditor params={params} setParam={setParam} />;
|
|
||||||
case 'vcv':
|
|
||||||
return <VcvChannelEditor params={params} setParam={setParam} />;
|
|
||||||
case 'cvgate':
|
|
||||||
return <CvChannelEditor params={params} setParam={setParam} />;
|
|
||||||
default:
|
|
||||||
return <SynthGroupNote params={params} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- MIDI (dock-spec §4.1) -------------------------------------------------
|
|
||||||
|
|
||||||
function MidiCcEditor({
|
|
||||||
params,
|
|
||||||
setParam,
|
|
||||||
}: {
|
|
||||||
params: MFParam[];
|
|
||||||
setParam: (i: number, patch: Partial<MFParam>) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: '0 0 8px' }}>
|
|
||||||
{params.length} CCs · live Web MIDI out (backends-spec §2.3). Editing the CC map here sends in
|
|
||||||
real time once a MIDI port is selected in the Outputs panel.
|
|
||||||
</p>
|
|
||||||
<div style={{ maxHeight: 360, overflow: 'auto' }}>
|
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<Th>Name</Th>
|
|
||||||
<Th>CC#</Th>
|
|
||||||
<Th>Ch</Th>
|
|
||||||
<Th>State</Th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{params.map((p, i) => {
|
|
||||||
const m = p.midi ?? defaultMidiSpec(i);
|
|
||||||
return (
|
|
||||||
<tr key={i}>
|
|
||||||
<td style={{ padding: '3px 6px' }}>
|
|
||||||
<input
|
|
||||||
style={cellInput}
|
|
||||||
value={m.name}
|
|
||||||
onChange={(e) => setParam(i, { midi: { ...m, name: e.target.value } })}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td style={{ padding: '3px 6px', width: 70 }}>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
max={127}
|
|
||||||
style={cellInput}
|
|
||||||
value={m.cc}
|
|
||||||
onChange={(e) =>
|
|
||||||
setParam(i, {
|
|
||||||
midi: { ...m, cc: Math.max(0, Math.min(127, num(e.target.value, m.cc))) },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td style={{ padding: '3px 6px', width: 60 }}>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={16}
|
|
||||||
style={cellInput}
|
|
||||||
value={m.channel}
|
|
||||||
onChange={(e) =>
|
|
||||||
setParam(i, {
|
|
||||||
midi: {
|
|
||||||
...m,
|
|
||||||
channel: Math.max(1, Math.min(16, num(e.target.value, m.channel))),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td style={{ padding: '3px 6px', fontSize: 'var(--fs-xs)', color: 'var(--fg-mute)' }}>
|
|
||||||
{p.status}
|
|
||||||
{p.muted ? ' · muted' : ''}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- OSC (dock-spec §4.2) --------------------------------------------------
|
|
||||||
|
|
||||||
function OscPathEditor({
|
|
||||||
params,
|
|
||||||
setParam,
|
|
||||||
}: {
|
|
||||||
params: MFParam[];
|
|
||||||
setParam: (i: number, patch: Partial<MFParam>) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: '0 0 8px' }}>
|
|
||||||
Live OSC over the WebSocket bridge (backends-spec §2.4). Set the bridge URL + per-output paths in
|
|
||||||
the Outputs panel; emits only while the bridge process is connected.
|
|
||||||
</p>
|
|
||||||
<div style={{ maxHeight: 360, overflow: 'auto' }}>
|
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<Th>OSC path</Th>
|
|
||||||
<Th>range min</Th>
|
|
||||||
<Th>range max</Th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{params.map((p, i) => {
|
|
||||||
const o = p.osc ?? defaultOscSpec(p.name);
|
|
||||||
return (
|
|
||||||
<tr key={i}>
|
|
||||||
<td style={{ padding: '3px 6px' }}>
|
|
||||||
<input
|
|
||||||
style={cellInput}
|
|
||||||
value={o.path}
|
|
||||||
onChange={(e) => setParam(i, { osc: { ...o, path: e.target.value } })}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td style={{ padding: '3px 6px', width: 90 }}>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
style={cellInput}
|
|
||||||
value={o.rangeMin}
|
|
||||||
onChange={(e) => setParam(i, { osc: { ...o, rangeMin: num(e.target.value, o.rangeMin) } })}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td style={{ padding: '3px 6px', width: 90 }}>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
style={cellInput}
|
|
||||||
value={o.rangeMax}
|
|
||||||
onChange={(e) => setParam(i, { osc: { ...o, rangeMax: num(e.target.value, o.rangeMax) } })}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- VCV / CV (dock-spec §4.3) ---------------------------------------------
|
|
||||||
|
|
||||||
function VcvChannelEditor({
|
|
||||||
params,
|
|
||||||
setParam,
|
|
||||||
}: {
|
|
||||||
params: MFParam[];
|
|
||||||
setParam: (i: number, patch: Partial<MFParam>) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: '0 0 8px' }}>
|
|
||||||
VCV adds nothing beyond the baseline (min/max = range, fixed = freeze) plus per-channel polarity.
|
|
||||||
{/* TODO(backends-spec §2.6): the VCV browser↔module bridge transport is not yet wired here. */}
|
|
||||||
</p>
|
|
||||||
<div style={{ maxHeight: 360, overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
||||||
{params.map((p, i) => {
|
|
||||||
const bipolar = p.vcv?.bipolar ?? false;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 8,
|
|
||||||
background: 'var(--bg-2)',
|
|
||||||
border: '1px solid var(--line)',
|
|
||||||
borderRadius: 'var(--r-1)',
|
|
||||||
padding: '4px 8px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ flex: 1, fontSize: 'var(--fs-xs)', color: 'var(--fg)' }}>{p.name}</span>
|
|
||||||
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>
|
|
||||||
{p.min.toFixed(2)}–{p.max.toFixed(2)} · {p.status === 'fixed' ? 'frozen' : 'live'}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setParam(i, { vcv: { bipolar: !bipolar } })}
|
|
||||||
style={{
|
|
||||||
fontSize: 9,
|
|
||||||
fontFamily: 'var(--font-mono)',
|
|
||||||
padding: '2px 8px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
borderRadius: 'var(--r-pill)',
|
|
||||||
border: `1px solid ${bipolar ? 'var(--danger)' : 'var(--line)'}`,
|
|
||||||
background: 'transparent',
|
|
||||||
color: bipolar ? 'var(--danger)' : 'var(--fg-mute)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{bipolar ? '±5 V' : '0–10 V'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- uSEQ CV / gate (docs/specs/useq-cv-protocol.md) -------------------------------------
|
|
||||||
|
|
||||||
function CvChannelEditor({
|
|
||||||
params,
|
|
||||||
setParam,
|
|
||||||
}: {
|
|
||||||
params: MFParam[];
|
|
||||||
setParam: (i: number, patch: Partial<MFParam>) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: '0 0 8px' }}>
|
|
||||||
Live CV/gate over USB serial to a uSEQ module + expander (docs/specs/useq-cv-protocol.md). Assign each model output to a
|
|
||||||
CV jack or gate; gates threshold the mapped 0–1 value. Connect the device in the Outputs panel.
|
|
||||||
</p>
|
|
||||||
<div style={{ maxHeight: 360, overflow: 'auto' }}>
|
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<Th>Output</Th>
|
|
||||||
<Th>uSEQ channel</Th>
|
|
||||||
<Th>Gate ≥</Th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{params.map((p, i) => {
|
|
||||||
const c = (p.cv as { channel: CvChannelId; gateThreshold: number } | undefined) ?? defaultCvSpec(i);
|
|
||||||
const isGate = c.channel.startsWith('gate');
|
|
||||||
return (
|
|
||||||
<tr key={i}>
|
|
||||||
<td style={{ padding: '3px 6px', fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>{p.name}</td>
|
|
||||||
<td style={{ padding: '3px 6px', width: 150 }}>
|
|
||||||
<select
|
|
||||||
value={c.channel}
|
|
||||||
onChange={(e) => setParam(i, { cv: { ...c, channel: e.target.value as CvChannelId } })}
|
|
||||||
style={{ ...cellInput, cursor: 'pointer' }}
|
|
||||||
>
|
|
||||||
<option value="none">— none —</option>
|
|
||||||
{CV_CHANNELS.map((ch) => (
|
|
||||||
<option key={ch.id} value={ch.id}>
|
|
||||||
{ch.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td style={{ padding: '3px 6px', width: 80 }}>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.05}
|
|
||||||
disabled={!isGate}
|
|
||||||
style={{ ...cellInput, opacity: isGate ? 1 : 0.4 }}
|
|
||||||
value={c.gateThreshold}
|
|
||||||
onChange={(e) =>
|
|
||||||
setParam(i, {
|
|
||||||
cv: { ...c, gateThreshold: Math.max(0, Math.min(1, num(e.target.value, c.gateThreshold))) },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SynthGroupNote({ params }: { params: MFParam[] }) {
|
|
||||||
const groups = Array.from(new Set(params.map((p) => p.group)));
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: '0 0 8px', lineHeight: 1.6 }}>
|
|
||||||
The synth backend's advanced surface is the group-override matrix — see the Powerful Synth Engine
|
|
||||||
drawer's full depth (dock-spec §4.4 / §5). Groups: {groups.join(' · ')}.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -12,8 +12,10 @@
|
||||||
* - Synth/Particle/Editor → handled by ModeConfig in Drawers (no extra config here).
|
* - Synth/Particle/Editor → handled by ModeConfig in Drawers (no extra config here).
|
||||||
*
|
*
|
||||||
* Everything is editable inline; writes go through the shared MFParam store
|
* Everything is editable inline; writes go through the shared MFParam store
|
||||||
* (ctx.setParam) — never a second data path. The full-depth modal reuses the
|
* (ctx.setParam) — never a second data path. This is the ONLY per-backend
|
||||||
* same sections via BackendAdvanced.
|
* config editor (BackendAdvanced.tsx, a full-depth duplicate that rendered
|
||||||
|
* alongside this one in the same expanded drawer, was deleted 2026-07 —
|
||||||
|
* simplification audit S18).
|
||||||
*/
|
*/
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import type { ConsoleCtx } from '../console/types';
|
import type { ConsoleCtx } from '../console/types';
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@
|
||||||
* They compose freely (e.g. an output can be `off` AND `muted` AND `armed`).
|
* They compose freely (e.g. an output can be `off` AND `muted` AND `armed`).
|
||||||
* Recorded in ALIGNMENT.md.
|
* Recorded in ALIGNMENT.md.
|
||||||
*
|
*
|
||||||
* To keep the dock tri-state and the existing OutputStage / ReadoutStrip
|
* To keep the dock tri-state and the existing OutputStage tri-state in sync
|
||||||
* tri-state in sync WITHOUT a second data path, this model is folded onto the
|
* WITHOUT a second data path, this model is folded onto the
|
||||||
* existing `MFParam` (model.ts) — `MFParam.status` carries `state`, and the new
|
* existing `MFParam` (model.ts) — `MFParam.status` carries `state`, and the new
|
||||||
* `muted` / `armed` / backend fields live alongside it. ConsoleApp owns the
|
* `muted` / `armed` / backend fields live alongside it. ConsoleApp owns the
|
||||||
* single `MFParam[]` store; the dock and the stage both read/write it.
|
* single `MFParam[]` store; the dock and the stage both read/write it.
|
||||||
|
|
@ -29,23 +29,6 @@ export type OutputState = ParamStatus; // 'off' | 'fixed' | 'live'
|
||||||
/** The selectable output backend (dock-spec §3.4; backends-spec §1). */
|
/** The selectable output backend (dock-spec §3.4; backends-spec §1). */
|
||||||
export type BackendId = 'synth' | 'particles' | 'midi' | 'osc' | 'cvgate' | 'vcv';
|
export type BackendId = 'synth' | 'particles' | 'midi' | 'osc' | 'cvgate' | 'vcv';
|
||||||
|
|
||||||
export interface BackendDescriptor {
|
|
||||||
id: BackendId;
|
|
||||||
/** Dock label — NEVER "C15" (backends-spec naming guard). */
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The backend roster surfaced in the dock's backend selector. */
|
|
||||||
export const BACKENDS: readonly BackendDescriptor[] = [
|
|
||||||
{ id: 'synth', label: 'Powerful Synth Engine', description: 'Firmware-parity built-in audio engine.' },
|
|
||||||
{ id: 'midi', label: 'MIDI', description: 'Web MIDI CC out — per-output CC#/channel.' },
|
|
||||||
{ id: 'osc', label: 'OSC', description: 'OSC bridge — named paths + physical ranges.' },
|
|
||||||
{ id: 'cvgate', label: 'CV', description: 'uSEQ CV/gate over USB serial — 11 CV + 3 gate.' },
|
|
||||||
{ id: 'vcv', label: 'VCV', description: 'VCV Rack module — 16 CV outs with LED rings.' },
|
|
||||||
{ id: 'particles', label: 'Particle', description: 'Flow-field visualiser (no audio).' },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
// ---- Backend-specific per-output specs (dock-spec §4) ----------------------
|
// ---- Backend-specific per-output specs (dock-spec §4) ----------------------
|
||||||
|
|
||||||
/** MIDI CC backend per-output extras (dock-spec §4.1). */
|
/** MIDI CC backend per-output extras (dock-spec §4.1). */
|
||||||
|
|
@ -111,47 +94,6 @@ export interface CvSpec {
|
||||||
gateThreshold: number;
|
gateThreshold: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The full per-output control. This is the spec's `OutputControl` (dock-spec
|
|
||||||
* §3.2). It is represented on `MFParam` for the shared store; this interface
|
|
||||||
* documents the complete contract and is what {@link toOutputControl} yields.
|
|
||||||
*/
|
|
||||||
export interface OutputControl {
|
|
||||||
index: number;
|
|
||||||
name: string;
|
|
||||||
group: string;
|
|
||||||
state: OutputState; // off | fixed | live
|
|
||||||
muted: boolean; // downstream silence; still computed
|
|
||||||
armed: boolean; // solo / focus-training (=arm)
|
|
||||||
min: number; // [0,1]
|
|
||||||
max: number; // [0,1], min<=max
|
|
||||||
curve: number; // [0,1], 0.5 linear
|
|
||||||
fixedValue: number; // held value when state==='fixed'
|
|
||||||
// backend-specific, populated by the active backend adapter:
|
|
||||||
midi?: MidiCcSpec;
|
|
||||||
osc?: OscSpec;
|
|
||||||
vcv?: VcvSpec;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Project an MFParam (the shared store row) into the full OutputControl view. */
|
|
||||||
export function toOutputControl(p: MFParam, index: number): OutputControl {
|
|
||||||
return {
|
|
||||||
index,
|
|
||||||
name: p.name,
|
|
||||||
group: p.group,
|
|
||||||
state: p.status,
|
|
||||||
muted: p.muted ?? false,
|
|
||||||
armed: p.armed ?? false,
|
|
||||||
min: p.min,
|
|
||||||
max: p.max,
|
|
||||||
curve: p.curve,
|
|
||||||
fixedValue: p.val,
|
|
||||||
midi: p.midi,
|
|
||||||
osc: p.osc,
|
|
||||||
vcv: p.vcv,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the focus / solo mask from the per-row armed flags (dock-spec §1.2).
|
* Build the focus / solo mask from the per-row armed flags (dock-spec §1.2).
|
||||||
* Returns null when nothing is armed (⇒ all outputs active / no focus).
|
* Returns null when nothing is armed (⇒ all outputs active / no focus).
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
* process() — re-run last input after a weight change
|
* process() — re-run last input after a weight change
|
||||||
* addExample([x,y], outVec) — append a training example
|
* addExample([x,y], outVec) — append a training example
|
||||||
* train() — SGD over the dataset
|
* train() — SGD over the dataset
|
||||||
* feedback.{setFocus,thumbsUp,dislikeGeometric,storePositive,…}
|
* feedback.{setFocus,thumbsUp,dislikeGeometric,…}
|
||||||
* — the SHARED C++ core's RL primitives
|
* — the SHARED C++ core's RL primitives
|
||||||
*
|
*
|
||||||
* As of one-core-engine P3 the geometric push-away (Mode 1) is a C++ core
|
* As of one-core-engine P3 the geometric push-away (Mode 1) is a C++ core
|
||||||
|
|
@ -55,9 +55,6 @@ export interface Anchor {
|
||||||
|
|
||||||
/** The minimal engine surface the controller needs (decoupled from EngineApi). */
|
/** The minimal engine surface the controller needs (decoupled from EngineApi). */
|
||||||
export interface ControllerEngine {
|
export interface ControllerEngine {
|
||||||
getWeights(): Float32Array;
|
|
||||||
setWeights(w: Float32Array): void;
|
|
||||||
randomise(spread?: number): void;
|
|
||||||
setInput(x: number, y: number): void;
|
setInput(x: number, y: number): void;
|
||||||
getOutputs(): Float32Array;
|
getOutputs(): Float32Array;
|
||||||
process(): void;
|
process(): void;
|
||||||
|
|
@ -70,8 +67,6 @@ export interface ControllerEngine {
|
||||||
// `heardVec` is the post-pipeline (HEARD) output; returns the FeedbackAction
|
// `heardVec` is the post-pipeline (HEARD) output; returns the FeedbackAction
|
||||||
// int (14=GeometricPush, 15=GeometricColdStart).
|
// int (14=GeometricPush, 15=GeometricColdStart).
|
||||||
dislikeGeometric(heardVec?: Float32Array, lr?: number): number;
|
dislikeGeometric(heardVec?: Float32Array, lr?: number): number;
|
||||||
/** Feed a positive into the k-NN centroid (null → live MLP output). */
|
|
||||||
storePositive(vec?: Float32Array): void;
|
|
||||||
positiveCount(): number;
|
positiveCount(): number;
|
||||||
negativeCount(): number;
|
negativeCount(): number;
|
||||||
// ExploreAndPlace lifecycle — the SHARED C++ core (mode 'explore_and_place').
|
// ExploreAndPlace lifecycle — the SHARED C++ core (mode 'explore_and_place').
|
||||||
|
|
@ -87,8 +82,6 @@ export interface ControllerEngine {
|
||||||
like(): void;
|
like(): void;
|
||||||
commitPlace(): void;
|
commitPlace(): void;
|
||||||
cancelPlace(): void;
|
cancelPlace(): void;
|
||||||
placing(): boolean;
|
|
||||||
exploreState(): number; // 0=Idle 1=Exploring 2=Placing
|
|
||||||
undoDepth(): number;
|
undoDepth(): number;
|
||||||
placedOutput(): Float32Array | null;
|
placedOutput(): Float32Array | null;
|
||||||
};
|
};
|
||||||
|
|
@ -115,24 +108,16 @@ export interface FeedbackControllerState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FeedbackControllerOptions {
|
export interface FeedbackControllerOptions {
|
||||||
/** Seed for the deterministic nudge RNG (NOT Math.random — task constraint). */
|
|
||||||
seed?: number;
|
|
||||||
/** Master spread for randomise / nudge (mirrors the engine spread knob). */
|
/** Master spread for randomise / nudge (mirrors the engine spread knob). */
|
||||||
spread?: number;
|
spread?: number;
|
||||||
/** Nudge perturbation standard deviation (small bounded weight jitter). */
|
/** Nudge perturbation standard deviation (small bounded weight jitter). */
|
||||||
nudgeStddev?: number;
|
nudgeStddev?: number;
|
||||||
/**
|
|
||||||
* Undo-stack depth. WASM D=4, firmware D=2 per rl-feedback-design §2.2; the
|
|
||||||
* prototype defaults to the WASM depth.
|
|
||||||
*/
|
|
||||||
undoDepth?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FeedbackController {
|
export class FeedbackController {
|
||||||
private engine: ControllerEngine;
|
private engine: ControllerEngine;
|
||||||
private spread: number;
|
private spread: number;
|
||||||
private nudgeStddev: number;
|
private nudgeStddev: number;
|
||||||
private maxUndo: number;
|
|
||||||
|
|
||||||
private mode: ProtoFeedbackMode = 'explore-and-place';
|
private mode: ProtoFeedbackMode = 'explore-and-place';
|
||||||
private soloMode: ProtoSoloMode = 'mask-gradients';
|
private soloMode: ProtoSoloMode = 'mask-gradients';
|
||||||
|
|
@ -162,7 +147,6 @@ export class FeedbackController {
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
this.spread = opts.spread ?? 0.6;
|
this.spread = opts.spread ?? 0.6;
|
||||||
this.nudgeStddev = opts.nudgeStddev ?? 0.05;
|
this.nudgeStddev = opts.nudgeStddev ?? 0.05;
|
||||||
this.maxUndo = Math.max(1, opts.undoDepth ?? 4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
|
|
@ -367,23 +351,12 @@ export class FeedbackController {
|
||||||
* 5. cold-start fallback (negative-LR) when there are no positives yet.
|
* 5. cold-start fallback (negative-LR) when there are no positives yet.
|
||||||
* Soloed/active dims come from the core's focus mask (set via setArmMask).
|
* Soloed/active dims come from the core's focus mask (set via setArmMask).
|
||||||
*
|
*
|
||||||
* @param input the control input the disliked sound was heard at (unused by
|
|
||||||
* the core — it reads the MLP's live input — kept for the marker /
|
|
||||||
* call-site symmetry with like()).
|
|
||||||
* @param output the HEARD (post-pipeline) output vector a_neg. MUST be the
|
* @param output the HEARD (post-pipeline) output vector a_neg. MUST be the
|
||||||
* heard vector, not the raw MLP output, or the cold-start MSE
|
* heard vector, not the raw MLP output, or the cold-start MSE
|
||||||
* derivative is zero (see engine.feedback.dislikeGeometric).
|
* derivative is zero (see engine.feedback.dislikeGeometric).
|
||||||
* @param _speed legacy move_weights speed — ignored (geometric path).
|
|
||||||
* @param _spread legacy move_weights spread — ignored (geometric path).
|
|
||||||
* @returns the FeedbackAction int (14=GeometricPush, 15=GeometricColdStart).
|
* @returns the FeedbackAction int (14=GeometricPush, 15=GeometricColdStart).
|
||||||
*/
|
*/
|
||||||
dislike(
|
dislike(output: Float32Array): number {
|
||||||
input: readonly [number, number],
|
|
||||||
output: Float32Array,
|
|
||||||
_speed: number,
|
|
||||||
_spread: number,
|
|
||||||
): number {
|
|
||||||
void input;
|
|
||||||
const action = this.engine.feedback.dislikeGeometric(output);
|
const action = this.engine.feedback.dislikeGeometric(output);
|
||||||
this.engine.process();
|
this.engine.process();
|
||||||
return action;
|
return action;
|
||||||
|
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
import type { CSSProperties, ReactNode } from 'react';
|
|
||||||
|
|
||||||
export interface ControlAxisProps {
|
|
||||||
label?: ReactNode;
|
|
||||||
/** Bipolar endpoint labels, e.g. ['Caution', 'Bold']. */
|
|
||||||
endpoints?: [ReactNode, ReactNode];
|
|
||||||
value?: number;
|
|
||||||
onChange?: (value: number) => void;
|
|
||||||
/** Live preset tag shown next to the label. */
|
|
||||||
preset?: ReactNode;
|
|
||||||
/** Per-axis track/thumb accent colour (any CSS colour or var()). */
|
|
||||||
accent?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
style?: CSSProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manifold ControlAxis — a named macro slider with bipolar endpoint labels
|
|
||||||
* (e.g. Boldness: Caution ↔ Bold). Shows a live preset tag and value. The
|
|
||||||
* track accent can be themed per-axis via `accent`.
|
|
||||||
*
|
|
||||||
* Relies on the `.mf-axis-input` rules in `styles/primitives.css`; the accent
|
|
||||||
* is passed via the inline `--mf-axis-accent` custom property.
|
|
||||||
*/
|
|
||||||
export function ControlAxis({
|
|
||||||
label,
|
|
||||||
endpoints = ['', ''],
|
|
||||||
value = 0.5,
|
|
||||||
onChange,
|
|
||||||
preset,
|
|
||||||
accent = 'var(--accent)',
|
|
||||||
disabled = false,
|
|
||||||
style,
|
|
||||||
}: ControlAxisProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: 'var(--sp-1)',
|
|
||||||
background: 'var(--bg-1)',
|
|
||||||
border: '1px solid var(--line)',
|
|
||||||
borderRadius: 'var(--r-2)',
|
|
||||||
padding: 'var(--sp-2) var(--sp-3)',
|
|
||||||
fontFamily: 'var(--font-mono)',
|
|
||||||
opacity: disabled ? 0.5 : 1,
|
|
||||||
pointerEvents: disabled ? 'none' : 'auto',
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 'var(--sp-2)',
|
|
||||||
fontSize: 'var(--fs-sm)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontWeight: 600,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.08em',
|
|
||||||
color: 'var(--fg)',
|
|
||||||
flex: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
{preset && (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
color: accent,
|
|
||||||
fontSize: 'var(--fs-xs)',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.06em',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{preset}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontVariantNumeric: 'tabular-nums',
|
|
||||||
color: 'var(--fg-mute)',
|
|
||||||
fontSize: 'var(--fs-xs)',
|
|
||||||
minWidth: '4ch',
|
|
||||||
textAlign: 'right',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{value.toFixed(2)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.01}
|
|
||||||
value={value}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={(e) => onChange?.(parseFloat(e.target.value))}
|
|
||||||
className="mf-axis-input"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
WebkitAppearance: 'none',
|
|
||||||
appearance: 'none',
|
|
||||||
width: '100%',
|
|
||||||
height: 24,
|
|
||||||
background: 'transparent',
|
|
||||||
margin: 0,
|
|
||||||
cursor: 'pointer',
|
|
||||||
'--mf-axis-accent': accent,
|
|
||||||
} as CSSProperties
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
fontSize: 10,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.08em',
|
|
||||||
color: 'var(--fg-dim)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>{endpoints[0]}</span>
|
|
||||||
<span>{endpoints[1]}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
import { useEffect, useRef } from 'react';
|
|
||||||
import type { CSSProperties } from 'react';
|
|
||||||
|
|
||||||
export type CurveName =
|
|
||||||
| 'linear'
|
|
||||||
| 'exp'
|
|
||||||
| 'log'
|
|
||||||
| 'square'
|
|
||||||
| 'sqrt'
|
|
||||||
| 'sigmoid'
|
|
||||||
| 'cubic'
|
|
||||||
| 'centered_power';
|
|
||||||
|
|
||||||
export interface CurvePlotProps {
|
|
||||||
/** One of the named response curves. Ignored when `fn` is provided. */
|
|
||||||
curve?: CurveName;
|
|
||||||
/** Custom response function f:[0,1]→[0,1]. Overrides `curve`. */
|
|
||||||
fn?: (x: number) => number;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
/** Stroke colour (any CSS colour or var()). */
|
|
||||||
color?: string;
|
|
||||||
showAxes?: boolean;
|
|
||||||
ariaLabel?: string;
|
|
||||||
style?: CSSProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
const clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v);
|
|
||||||
|
|
||||||
const CURVES: Record<CurveName, (x: number) => number> = {
|
|
||||||
linear: (x) => x,
|
|
||||||
exp: (x) => (Math.exp(4 * x) - 1) / (Math.exp(4) - 1),
|
|
||||||
log: (x) => Math.log(1 + x * (Math.exp(4) - 1)) / 4,
|
|
||||||
square: (x) => x * x,
|
|
||||||
sqrt: (x) => Math.sqrt(clamp01(x)),
|
|
||||||
sigmoid: (x) => {
|
|
||||||
const s = (v: number) => 1 / (1 + Math.exp(-(v - 0.5) * 8));
|
|
||||||
const lo = s(0);
|
|
||||||
const hi = s(1);
|
|
||||||
return (s(x) - lo) / (hi - lo);
|
|
||||||
},
|
|
||||||
cubic: (x) => {
|
|
||||||
const v = clamp01(x);
|
|
||||||
return v * v * (3 - 2 * v);
|
|
||||||
},
|
|
||||||
centered_power: (x) => {
|
|
||||||
const o = x - 0.5;
|
|
||||||
const sg = o < 0 ? -1 : 1;
|
|
||||||
return clamp01((sg * Math.pow(Math.abs(o) * 2, 0.5)) / 2 + 0.5);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manifold CurvePlot — renders one of the named response curves (or a custom
|
|
||||||
* function f:[0,1]→[0,1]) on the dark grid. The brand's straight-line &
|
|
||||||
* parabolic/bézier motif.
|
|
||||||
*/
|
|
||||||
export function CurvePlot({
|
|
||||||
curve = 'cubic',
|
|
||||||
fn,
|
|
||||||
width = 200,
|
|
||||||
height = 120,
|
|
||||||
color = 'var(--accent)',
|
|
||||||
showAxes = true,
|
|
||||||
ariaLabel,
|
|
||||||
style,
|
|
||||||
}: CurvePlotProps) {
|
|
||||||
const ref = useRef<HTMLCanvasElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const cv = ref.current;
|
|
||||||
if (!cv) return;
|
|
||||||
const dpr = window.devicePixelRatio || 1;
|
|
||||||
const w = width * dpr;
|
|
||||||
const h = height * dpr;
|
|
||||||
cv.width = w;
|
|
||||||
cv.height = h;
|
|
||||||
const ctx = cv.getContext('2d');
|
|
||||||
if (!ctx) return;
|
|
||||||
ctx.clearRect(0, 0, w, h);
|
|
||||||
const cs = getComputedStyle(cv);
|
|
||||||
const stroke = color.startsWith('var(')
|
|
||||||
? cs.getPropertyValue(color.slice(4, -1).trim()).trim() || '#ff6a00'
|
|
||||||
: color;
|
|
||||||
const pad = 6 * dpr;
|
|
||||||
|
|
||||||
if (showAxes) {
|
|
||||||
ctx.strokeStyle = 'rgba(255,255,255,0.06)';
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.strokeRect(0.5, 0.5, w - 1, h - 1);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, h / 2);
|
|
||||||
ctx.lineTo(w, h / 2);
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(w / 2, 0);
|
|
||||||
ctx.lineTo(w / 2, h);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
const f = fn || CURVES[curve] || CURVES.linear;
|
|
||||||
ctx.strokeStyle = stroke;
|
|
||||||
ctx.lineWidth = 2 * dpr;
|
|
||||||
ctx.beginPath();
|
|
||||||
for (let p = 0; p <= 120; p++) {
|
|
||||||
const x = p / 120;
|
|
||||||
const y = clamp01(f(x));
|
|
||||||
const px = pad + x * (w - 2 * pad);
|
|
||||||
const py = h - pad - y * (h - 2 * pad);
|
|
||||||
if (p === 0) ctx.moveTo(px, py);
|
|
||||||
else ctx.lineTo(px, py);
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
}, [curve, fn, width, height, color, showAxes]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<canvas
|
|
||||||
ref={ref}
|
|
||||||
role="img"
|
|
||||||
aria-label={ariaLabel || `${curve} curve`}
|
|
||||||
style={{
|
|
||||||
display: 'block',
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
background: 'var(--bg-1)',
|
|
||||||
border: '1px solid var(--line)',
|
|
||||||
borderRadius: 'var(--r-2)',
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
import type { CSSProperties, ReactNode } from 'react';
|
|
||||||
|
|
||||||
export interface PanelProps {
|
|
||||||
title?: ReactNode;
|
|
||||||
/** Small uppercase eyebrow shown before the title. */
|
|
||||||
label?: ReactNode;
|
|
||||||
/** Right-aligned header actions. */
|
|
||||||
actions?: ReactNode;
|
|
||||||
children?: ReactNode;
|
|
||||||
padding?: string;
|
|
||||||
style?: CSSProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manifold Panel — the house surface: bg-1 fill, 1px hairline border, 8px
|
|
||||||
* radius, no shadow. Optional header row with an uppercase title + actions,
|
|
||||||
* separated by a hairline.
|
|
||||||
*/
|
|
||||||
export function Panel({
|
|
||||||
title,
|
|
||||||
label,
|
|
||||||
actions,
|
|
||||||
children,
|
|
||||||
padding = 'var(--sp-3)',
|
|
||||||
style,
|
|
||||||
}: PanelProps) {
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
style={{
|
|
||||||
background: 'var(--bg-1)',
|
|
||||||
border: '1px solid var(--line)',
|
|
||||||
borderRadius: 'var(--r-2)',
|
|
||||||
fontFamily: 'var(--font-mono)',
|
|
||||||
color: 'var(--fg)',
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(title || label || actions) && (
|
|
||||||
<header
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 'var(--sp-2)',
|
|
||||||
padding: 'var(--sp-2) var(--sp-3)',
|
|
||||||
borderBottom: '1px solid var(--line)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label && (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 'var(--fs-xs)',
|
|
||||||
color: 'var(--fg-dim)',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.08em',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{title && (
|
|
||||||
<h3
|
|
||||||
style={{
|
|
||||||
margin: 0,
|
|
||||||
fontSize: 'var(--fs-sm)',
|
|
||||||
fontWeight: 600,
|
|
||||||
color: 'var(--fg)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
)}
|
|
||||||
{actions && (
|
|
||||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: 'var(--sp-2)' }}>
|
|
||||||
{actions}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</header>
|
|
||||||
)}
|
|
||||||
<div style={{ padding }}>{children}</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
import { useEffect, useRef } from 'react';
|
|
||||||
import type { CSSProperties } from 'react';
|
|
||||||
|
|
||||||
export interface SparklineProps {
|
|
||||||
data?: number[];
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
/** Stroke colour (any CSS colour or var()). */
|
|
||||||
color?: string;
|
|
||||||
/** Plot on a log scale (log(max(1e-10, v) + 1)). */
|
|
||||||
log?: boolean;
|
|
||||||
/** Render the last-value readout in the top-right. */
|
|
||||||
showLast?: boolean;
|
|
||||||
/** Custom formatter for the last-value readout. */
|
|
||||||
format?: (value: number) => string;
|
|
||||||
ariaLabel?: string;
|
|
||||||
style?: CSSProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manifold Sparkline — a compact time-series trace (training loss, a feature
|
|
||||||
* envelope). Cyan line on a faint grid, with an optional last-value readout.
|
|
||||||
*/
|
|
||||||
export function Sparkline({
|
|
||||||
data = [],
|
|
||||||
width = 320,
|
|
||||||
height = 70,
|
|
||||||
color = 'var(--accent-2)',
|
|
||||||
log = false,
|
|
||||||
showLast = true,
|
|
||||||
format,
|
|
||||||
ariaLabel = 'time series',
|
|
||||||
style,
|
|
||||||
}: SparklineProps) {
|
|
||||||
const ref = useRef<HTMLCanvasElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const cv = ref.current;
|
|
||||||
if (!cv) return;
|
|
||||||
const dpr = window.devicePixelRatio || 1;
|
|
||||||
const w = width * dpr;
|
|
||||||
const h = height * dpr;
|
|
||||||
cv.width = w;
|
|
||||||
cv.height = h;
|
|
||||||
const ctx = cv.getContext('2d');
|
|
||||||
if (!ctx) return;
|
|
||||||
ctx.clearRect(0, 0, w, h);
|
|
||||||
if (!data.length) return;
|
|
||||||
|
|
||||||
const cs = getComputedStyle(cv);
|
|
||||||
const stroke = color.startsWith('var(')
|
|
||||||
? cs.getPropertyValue(color.slice(4, -1).trim()).trim() || '#00ccff'
|
|
||||||
: color;
|
|
||||||
|
|
||||||
const ys = data.map((v) => (log ? Math.log(Math.max(1e-10, v) + 1) : v));
|
|
||||||
let lo = Infinity;
|
|
||||||
let hi = -Infinity;
|
|
||||||
for (const y of ys) {
|
|
||||||
if (y < lo) lo = y;
|
|
||||||
if (y > hi) hi = y;
|
|
||||||
}
|
|
||||||
if (hi === lo) hi = lo + 1e-6;
|
|
||||||
|
|
||||||
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
for (let i = 1; i < 4; i++) {
|
|
||||||
const y = (i / 4) * h;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, y);
|
|
||||||
ctx.lineTo(w, y);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.strokeStyle = stroke;
|
|
||||||
ctx.lineWidth = 1.5 * dpr;
|
|
||||||
ctx.beginPath();
|
|
||||||
for (let i = 0; i < ys.length; i++) {
|
|
||||||
const x = (i / Math.max(1, ys.length - 1)) * w;
|
|
||||||
const norm = (ys[i] - lo) / (hi - lo);
|
|
||||||
const y = h - norm * h;
|
|
||||||
if (i === 0) ctx.moveTo(x, y);
|
|
||||||
else ctx.lineTo(x, y);
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
if (showLast) {
|
|
||||||
const last = data[data.length - 1];
|
|
||||||
const txt = format
|
|
||||||
? format(last)
|
|
||||||
: typeof last === 'number'
|
|
||||||
? last.toExponential(2)
|
|
||||||
: String(last);
|
|
||||||
ctx.fillStyle = '#9a9a9a';
|
|
||||||
ctx.font = `${10 * dpr}px ui-monospace, monospace`;
|
|
||||||
ctx.textAlign = 'right';
|
|
||||||
ctx.fillText(txt, w - 4 * dpr, 12 * dpr);
|
|
||||||
}
|
|
||||||
}, [data, width, height, color, log, showLast, format]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<canvas
|
|
||||||
ref={ref}
|
|
||||||
role="img"
|
|
||||||
aria-label={ariaLabel}
|
|
||||||
style={{
|
|
||||||
display: 'block',
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
background: 'var(--bg-1)',
|
|
||||||
border: '1px solid var(--line)',
|
|
||||||
borderRadius: 'var(--r-2)',
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
import { Fragment } from 'react';
|
|
||||||
import type { CSSProperties, ReactNode } from 'react';
|
|
||||||
|
|
||||||
export type StatusTone = 'accent' | 'cyan' | 'good' | 'warn' | 'bad';
|
|
||||||
|
|
||||||
export interface StatusItemObject {
|
|
||||||
label?: ReactNode;
|
|
||||||
value: ReactNode;
|
|
||||||
tone?: StatusTone;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type StatusItem = string | StatusItemObject;
|
|
||||||
|
|
||||||
export interface StatusLineProps {
|
|
||||||
items?: StatusItem[];
|
|
||||||
style?: CSSProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TONE_COLORS: Record<StatusTone, string> = {
|
|
||||||
accent: 'var(--accent)',
|
|
||||||
cyan: 'var(--accent-2)',
|
|
||||||
good: 'var(--good)',
|
|
||||||
warn: 'var(--warn)',
|
|
||||||
bad: 'var(--bad)',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manifold StatusLine — the dim mono readout strip at the bottom of a mode.
|
|
||||||
* Pass an array of items; strings render plain, {label,value,tone} render a
|
|
||||||
* labelled readout. Items are joined with the house middle-dot separator.
|
|
||||||
*/
|
|
||||||
export function StatusLine({ items = [], style }: StatusLineProps) {
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 'var(--sp-2)',
|
|
||||||
margin: 0,
|
|
||||||
fontFamily: 'var(--font-mono)',
|
|
||||||
fontSize: 'var(--fs-xs)',
|
|
||||||
color: 'var(--fg-dim)',
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{items.map((it, i) => {
|
|
||||||
const isObj = it !== null && typeof it === 'object';
|
|
||||||
const toneColor =
|
|
||||||
isObj && it.tone ? (TONE_COLORS[it.tone] ?? null) : null;
|
|
||||||
return (
|
|
||||||
<Fragment key={i}>
|
|
||||||
{i > 0 && <span aria-hidden="true">·</span>}
|
|
||||||
{isObj ? (
|
|
||||||
<span style={{ color: toneColor || 'var(--fg-dim)' }}>
|
|
||||||
{it.label && <span style={{ color: 'var(--fg-dim)' }}>{it.label} </span>}
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontVariantNumeric: 'tabular-nums',
|
|
||||||
color: toneColor || 'var(--fg-mute)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{it.value}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span>{it}</span>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -3,9 +3,13 @@
|
||||||
* on the Manifold design tokens. Ported from the window-global JSX reference
|
* on the Manifold design tokens. Ported from the window-global JSX reference
|
||||||
* implementations in docs/redesign/manifold-export/components/.
|
* implementations in docs/redesign/manifold-export/components/.
|
||||||
*
|
*
|
||||||
* Side-effect import: pulls in the `.mf-slider-input` / `.mf-axis-input`
|
* Side-effect import: pulls in the `.mf-slider-input` range-input styling that
|
||||||
* range-input styling that Slider and ControlAxis depend on. Importing this
|
* Slider depends on. Importing this barrel anywhere in the app is enough to
|
||||||
* barrel anywhere in the app is enough to register those rules.
|
* register those rules.
|
||||||
|
*
|
||||||
|
* Panel / StatusLine / ControlAxis / CurvePlot / Sparkline were deleted
|
||||||
|
* 2026-07 (simplification audit L22) — zero consumers repo-wide. Sparkline/
|
||||||
|
* CurvePlot may return with the deferred training-health diagnostics suite.
|
||||||
*/
|
*/
|
||||||
import '../styles/primitives.css';
|
import '../styles/primitives.css';
|
||||||
|
|
||||||
|
|
@ -18,34 +22,14 @@ export type { SliderProps } from './Slider';
|
||||||
export { PillToggle } from './PillToggle';
|
export { PillToggle } from './PillToggle';
|
||||||
export type { PillToggleProps, PillOption } from './PillToggle';
|
export type { PillToggleProps, PillOption } from './PillToggle';
|
||||||
|
|
||||||
export { Panel } from './Panel';
|
|
||||||
export type { PanelProps } from './Panel';
|
|
||||||
|
|
||||||
export { Badge } from './Badge';
|
export { Badge } from './Badge';
|
||||||
export type { BadgeProps, BadgeTone } from './Badge';
|
export type { BadgeProps, BadgeTone } from './Badge';
|
||||||
|
|
||||||
export { Switch } from './Switch';
|
export { Switch } from './Switch';
|
||||||
export type { SwitchProps } from './Switch';
|
export type { SwitchProps } from './Switch';
|
||||||
|
|
||||||
export { StatusLine } from './StatusLine';
|
|
||||||
export type {
|
|
||||||
StatusLineProps,
|
|
||||||
StatusItem,
|
|
||||||
StatusItemObject,
|
|
||||||
StatusTone,
|
|
||||||
} from './StatusLine';
|
|
||||||
|
|
||||||
export { XYPad } from './XYPad';
|
export { XYPad } from './XYPad';
|
||||||
export type { XYPadProps } from './XYPad';
|
export type { XYPadProps } from './XYPad';
|
||||||
|
|
||||||
export { VirtualJoystick } from './VirtualJoystick';
|
export { VirtualJoystick } from './VirtualJoystick';
|
||||||
export type { VirtualJoystickProps } from './VirtualJoystick';
|
export type { VirtualJoystickProps } from './VirtualJoystick';
|
||||||
|
|
||||||
export { ControlAxis } from './ControlAxis';
|
|
||||||
export type { ControlAxisProps } from './ControlAxis';
|
|
||||||
|
|
||||||
export { CurvePlot } from './CurvePlot';
|
|
||||||
export type { CurvePlotProps, CurveName } from './CurvePlot';
|
|
||||||
|
|
||||||
export { Sparkline } from './Sparkline';
|
|
||||||
export type { SparklineProps } from './Sparkline';
|
|
||||||
|
|
|
||||||
|
|
@ -70,37 +70,3 @@
|
||||||
.mf-slider-input:focus::-webkit-slider-thumb {
|
.mf-slider-input:focus::-webkit-slider-thumb {
|
||||||
box-shadow: 0 0 0 3px var(--glow-focus);
|
box-shadow: 0 0 0 3px var(--glow-focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- ControlAxis (.mf-axis-input) ---- */
|
|
||||||
.mf-axis-input::-webkit-slider-runnable-track {
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--bg-3);
|
|
||||||
}
|
|
||||||
.mf-axis-input::-moz-range-track {
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--bg-3);
|
|
||||||
}
|
|
||||||
.mf-axis-input::-webkit-slider-thumb {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--mf-axis-accent, var(--accent));
|
|
||||||
margin-top: -6px;
|
|
||||||
box-shadow: 0 0 10px var(--mf-axis-accent, var(--accent));
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.mf-axis-input::-moz-range-thumb {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--mf-axis-accent, var(--accent));
|
|
||||||
border: none;
|
|
||||||
box-shadow: 0 0 10px var(--mf-axis-accent, var(--accent));
|
|
||||||
}
|
|
||||||
.mf-axis-input:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue