yansari's picture
feat: graph-editor UX overhaul and headless matplotlib fix
ef4c917
Raw
History Blame Contribute Delete
4.51 kB
import type { RouteEndpoint, RouteInfo, InteractionMode } from '../types';
import { NODE_TYPE_LABELS, ROUTE_COLORS } from '../constants';
interface Props {
endpoints: RouteEndpoint[];
source: string | null;
target: string | null;
onSourceChange: (id: string | null) => void;
onTargetChange: (id: string | null) => void;
onSwap: () => void;
onClear: () => void;
info: RouteInfo | null;
mode: InteractionMode;
onArmStart: () => void;
onArmEnd: () => void;
}
function labelFor(endpoints: RouteEndpoint[], id: string | null): string {
if (!id) return 'not set';
const ep = endpoints.find((e) => e.id === id);
return ep ? ep.label : id;
}
function optionLabel(ep: RouteEndpoint): string {
const typeLabel = NODE_TYPE_LABELS[ep.type] || ep.type;
return ep.type === 'room' ? ep.label : `${ep.label} (${typeLabel})`;
}
export default function RoutePanel({
endpoints,
source,
target,
onSourceChange,
onTargetChange,
onSwap,
onClear,
info,
mode,
onArmStart,
onArmEnd,
}: Props) {
const hasRoute = !!source && !!target;
return (
<div className="panel">
<h3>Navigation</h3>
<p className="route-hint">
Click <strong>Set Start</strong>, then click a node on the graph. Do the
same for <strong>Set Destination</strong>. The shortest route is computed
with A* search.
</p>
<div className="route-pick">
<button
className={`btn route-pick-btn${mode === 'route-start' ? ' arming' : ''}`}
onClick={onArmStart}
>
<span className="route-dot" style={{ background: ROUTE_COLORS.source }} />
{mode === 'route-start' ? 'Click a node…' : 'Set Start'}
</button>
<div className="route-current">{labelFor(endpoints, source)}</div>
</div>
<div className="route-pick">
<button
className={`btn route-pick-btn${mode === 'route-end' ? ' arming' : ''}`}
onClick={onArmEnd}
>
<span className="route-dot" style={{ background: ROUTE_COLORS.target }} />
{mode === 'route-end' ? 'Click a node…' : 'Set Destination'}
</button>
<div className="route-current">{labelFor(endpoints, target)}</div>
</div>
<div className="route-buttons">
<button className="btn" onClick={onSwap} disabled={!hasRoute}>Swap</button>
<button className="btn" onClick={onClear} disabled={!source && !target}>Clear</button>
</div>
{hasRoute && info && (
<div className={`route-result${info.found ? '' : ' route-result-fail'}`}>
{info.found ? (
<>
<div className="route-result-row">
<span className="label">Path length</span>
<span className="value">{info.distance.toLocaleString()} px</span>
</div>
<div className="route-result-row">
<span className="label">Segments</span>
<span className="value">{info.segments}</span>
</div>
{info.crossFloor && <span className="route-badge">Crosses floors</span>}
</>
) : (
<span>No route found between these points.</span>
)}
</div>
)}
<details className="route-advanced">
<summary>Pick from list instead</summary>
<label className="route-field">
<span className="route-dot" style={{ background: ROUTE_COLORS.source }} />
<select
className="route-select"
value={source ?? ''}
onChange={(e) => onSourceChange(e.target.value || null)}
>
<option value="">Start…</option>
{endpoints.map((ep) => (
<option key={ep.id} value={ep.id} disabled={ep.id === target}>
{optionLabel(ep)}
</option>
))}
</select>
</label>
<label className="route-field">
<span className="route-dot" style={{ background: ROUTE_COLORS.target }} />
<select
className="route-select"
value={target ?? ''}
onChange={(e) => onTargetChange(e.target.value || null)}
>
<option value="">Destination…</option>
{endpoints.map((ep) => (
<option key={ep.id} value={ep.id} disabled={ep.id === source}>
{optionLabel(ep)}
</option>
))}
</select>
</label>
</details>
</div>
);
}