fix(manifold): clamp temporary output slider overrides
This commit is contained in:
parent
6e71ad7d35
commit
3879d57b4c
4 changed files with 95 additions and 8 deletions
|
|
@ -115,8 +115,9 @@ Manifold ships a single "composite" altitude. Selection is now a plain three-way
|
|||
CompositeStage/SandwichStage. `Manifold.tsx` is the full-bleed 2D input surface (canvas trail + pins
|
||||
+ feedback markers; pointer → `onMove`; **double-click the input mark → follow-mouse mode**, a window
|
||||
`pointermove` listener mapping the whole viewport onto this surface's space, Esc or a second
|
||||
double-click exits). `OutputStage.tsx` is the output columns; drag a bar to set value, and it takes a
|
||||
`compact` prop for the narrow pane.
|
||||
double-click exits). `OutputStage.tsx` is the output columns; click or drag a bar to temporarily
|
||||
audition a clamped value (the next engine tick restores the live MLP), and it takes a `compact` prop
|
||||
for the narrow pane.
|
||||
|
||||
- **Output modes** (the TOP dock selector, NOT the same axis as `focus`): `src/console/output-mode.ts`
|
||||
defines `OUTPUT_MODES` = **particles** (default) / midi / osc / cv / synth / editor, each mapping to a
|
||||
|
|
|
|||
|
|
@ -609,7 +609,32 @@ export function ConsoleApp() {
|
|||
};
|
||||
|
||||
const setParam = (i: number, patch: Partial<MFParam>) =>
|
||||
setParams((ps) => ps.map((p, j) => (j === i ? { ...p, ...patch } : p)));
|
||||
setParams((ps) =>
|
||||
ps.map((p, j) => {
|
||||
if (j !== i) return p;
|
||||
const next = { ...p, ...patch };
|
||||
// Any explicit edit other than the main-view gesture cancels the
|
||||
// transient override, so normal dock/editor controls remain authoritative.
|
||||
if (!Object.prototype.hasOwnProperty.call(patch, 'manualOverride')) {
|
||||
next.manualOverride = undefined;
|
||||
}
|
||||
return next;
|
||||
}),
|
||||
);
|
||||
|
||||
// A direct slider gesture is only an audition value. The next engine tick
|
||||
// (normally caused by moving the input) returns that param to the live MLP.
|
||||
useEffect(() => {
|
||||
setParams((ps) => {
|
||||
let changed = false;
|
||||
const next = ps.map((p) => {
|
||||
if (!p.manualOverride) return p;
|
||||
changed = true;
|
||||
return { ...p, status: 'live' as const, manualOverride: undefined };
|
||||
});
|
||||
return changed ? next : ps;
|
||||
});
|
||||
}, [version]);
|
||||
|
||||
// Ref-mirror of everything the two global-listener effects below close over
|
||||
// that is NOT already React-stable (verdict/navigation handlers are plain
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* columns. Drag/click a bar sets value; ⌥/alt-click cycles state; hover opens
|
||||
* the OutputEditor. Ported from `OutputStage.jsx`.
|
||||
*/
|
||||
import { useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { PointerEvent as ReactPointerEvent } from 'react';
|
||||
import type { MFParam, ParamStatus } from './model';
|
||||
import { GROUP_COLOR } from './model';
|
||||
|
|
@ -20,10 +20,12 @@ export interface OutputStageProps {
|
|||
|
||||
export function OutputStage({ params, values, onChange, compact = false }: OutputStageProps) {
|
||||
const [open, setOpen] = useState<number | null>(null);
|
||||
const [clampMarker, setClampMarker] = useState<{ id: string; value: number } | null>(null);
|
||||
const timers = useRef<{ open: ReturnType<typeof setTimeout> | null; close: ReturnType<typeof setTimeout> | null }>({
|
||||
open: null,
|
||||
close: null,
|
||||
});
|
||||
const clampTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const drag = useRef<{ i: number; moved: boolean; startY: number; el: HTMLDivElement | null; alt: boolean }>({
|
||||
i: -1,
|
||||
moved: false,
|
||||
|
|
@ -46,10 +48,48 @@ export function OutputStage({ params, values, onChange, compact = false }: Outpu
|
|||
if (timers.current.close) clearTimeout(timers.current.close);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (clampTimer.current) clearTimeout(clampTimer.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const valFromEvent = (el: HTMLDivElement, clientY: number) => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return Math.max(0, Math.min(1, 1 - (clientY - r.top) / r.height));
|
||||
};
|
||||
const showClampMarker = (id: string, value: number) => {
|
||||
if (clampTimer.current) clearTimeout(clampTimer.current);
|
||||
setClampMarker({ id, value });
|
||||
clampTimer.current = setTimeout(() => {
|
||||
setClampMarker(null);
|
||||
clampTimer.current = null;
|
||||
}, 700);
|
||||
};
|
||||
const clearClampMarker = () => {
|
||||
if (clampTimer.current) clearTimeout(clampTimer.current);
|
||||
clampTimer.current = null;
|
||||
setClampMarker(null);
|
||||
};
|
||||
const setSliderValue = (i: number, raw: number) => {
|
||||
const param = params[i];
|
||||
if (!param) return;
|
||||
const min = Math.max(0, Math.min(1, Math.min(param.min, param.max)));
|
||||
const max = Math.max(0, Math.min(1, Math.max(param.min, param.max)));
|
||||
const value = Math.max(min, Math.min(max, raw));
|
||||
const wasClamped = value !== raw;
|
||||
if (wasClamped) showClampMarker(param.id, value);
|
||||
else clearClampMarker();
|
||||
|
||||
const span = max - min;
|
||||
const fixedValue = span > 0 ? (value - min) / span : 0;
|
||||
const temporary = param.manualOverride || param.status === 'live';
|
||||
onChange(i, {
|
||||
status: 'fixed',
|
||||
val: fixedValue,
|
||||
manualOverride: temporary ? true : undefined,
|
||||
});
|
||||
};
|
||||
const down = (e: ReactPointerEvent<HTMLDivElement>, i: number) => {
|
||||
e.currentTarget.setPointerCapture?.(e.pointerId);
|
||||
drag.current = {
|
||||
|
|
@ -64,13 +104,15 @@ export function OutputStage({ params, values, onChange, compact = false }: Outpu
|
|||
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) });
|
||||
if (d.moved && !d.alt && d.el) setSliderValue(i, 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: OUT_NEXT[params[i].status] || 'live' });
|
||||
else if (!d.moved && d.el) onChange(i, { val: valFromEvent(d.el, e.clientY) });
|
||||
if (d.alt && !d.moved) {
|
||||
clearClampMarker();
|
||||
onChange(i, { status: OUT_NEXT[params[i].status] || 'live', manualOverride: undefined });
|
||||
} else if (!d.moved && d.el) setSliderValue(i, valFromEvent(d.el, e.clientY));
|
||||
drag.current = { i: -1, moved: false, startY: 0, el: null, alt: false };
|
||||
};
|
||||
|
||||
|
|
@ -202,6 +244,21 @@ export function OutputStage({ params, values, onChange, compact = false }: Outpu
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{clampMarker?.id === p.id && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 1,
|
||||
right: 1,
|
||||
bottom: `${clampMarker.value * 100}%`,
|
||||
height: 0,
|
||||
borderTop: '1px dashed #ff4466',
|
||||
boxShadow: '0 0 5px rgba(255,68,102,0.8)',
|
||||
zIndex: 3,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
|
|
|
|||
|
|
@ -105,6 +105,8 @@ export interface MFParam {
|
|||
group: string;
|
||||
status: ParamStatus;
|
||||
val: number;
|
||||
/** Temporary value from a direct main-view slider gesture; released on the next engine tick. */
|
||||
manualOverride?: boolean;
|
||||
min: number;
|
||||
max: number;
|
||||
curve: number;
|
||||
|
|
@ -364,7 +366,9 @@ export { applyCurve };
|
|||
export function shapeValues(params: MFParam[], engineOut: Float32Array | null): number[] {
|
||||
return params.map((p, i) => {
|
||||
if (p.status === 'off') return 0;
|
||||
if (p.status === 'fixed') return p.val ?? 0.5;
|
||||
if (p.status === 'fixed') {
|
||||
return p.min + Math.max(0, Math.min(1, p.val ?? 0.5)) * (p.max - p.min);
|
||||
}
|
||||
const raw = engineOut && i < engineOut.length ? engineOut[i] : 0.5;
|
||||
const v = p.min + applyCurve(raw, p.curve) * (p.max - p.min);
|
||||
return Math.max(0, Math.min(1, v));
|
||||
|
|
|
|||
Loading…
Reference in a new issue