/**
* The hidden force tuner panel (docs/weave-workspace.md §15.7).
*
* Props in, JSX out. Every range, label, parse and clamp comes from
* `tuner.model.ts`; what is left here is seven `` elements
* and two buttons. Native range inputs rather than a slider component, for the
* obvious reason: the platform has had this element since 2011.
*
* Rendered only when `Graph.tsx` was handed `tuner` — that is, only when
* `?sliders=1` was on the URL. See `slidersFlag`.
*/
import { useState } from "preact/hooks";
import { FORCES, FORCE_DEFAULTS, setForces } from "../../shared/layout";
import type { SliderSpec } from "./tuner.model";
import { FORCE_SLIDERS, HAIRBALL, forcesSnippet, formatValue, isDefault, parseSlider, sliderValue } from "./tuner.model";
export interface ForceTunerProps {
/**
* Called after every write to `FORCES`, so the column can drop its cached
* layout and re-run the simulation. The panel never lays out anything
* itself — it mutates the constants and says so.
*/
onChange: () => void;
/** Whether nodes are coloured by group hue (§15.8). */
groupColors: boolean;
/** Toggle that. Owned by the column, which persists it. */
onGroupColors: (next: boolean) => void;
}
export function ForceTuner(props: ForceTunerProps) {
// The panel's own re-render trigger. `FORCES` is a plain mutable object, so
// preact cannot observe it; a counter is the whole subscription.
const [, bump] = useState(0);
const [copied, setCopied] = useState(false);
const apply = (spec: SliderSpec, raw: string): void => {
setForces({ [spec.key]: parseSlider(spec, raw) });
setCopied(false);
bump((n) => n + 1);
props.onChange();
};
const applyAll = (next: Partial): void => {
setForces(next);
setCopied(false);
bump((n) => n + 1);
props.onChange();
};
return (