Initial release of 8BitDo Control Center

Provide a Linux desktop controller configuration app with evdev input, uinput remapping, profiles, calibration, and reconnect recovery.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-21 12:18:10 -07:00
commit 6507f5f8b3
121 changed files with 15415 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
import type { PointerEvent as ReactPointerEvent } from "react";
import type { CurvePoint } from "@/types/profile";
import { sampleCurve } from "@/utils/curveMath";
interface CurveEditorProps {
points: CurvePoint[];
onChange: (points: CurvePoint[]) => void;
}
const SIZE = 220;
const PADDING = 18;
const PLOT_SIZE = SIZE - PADDING * 2;
function pointToSvg(point: CurvePoint): { x: number; y: number } {
return { x: PADDING + point.x * PLOT_SIZE, y: PADDING + (1 - point.y) * PLOT_SIZE };
}
function svgToPoint(event: ReactPointerEvent<SVGSVGElement | SVGCircleElement>): CurvePoint {
const svg = event.currentTarget instanceof SVGSVGElement
? event.currentTarget
: event.currentTarget.ownerSVGElement;
if (!svg) return { x: 0, y: 0 };
const bounds = svg.getBoundingClientRect();
return {
x: Math.min(1, Math.max(0, (event.clientX - bounds.left - PADDING) / PLOT_SIZE)),
y: Math.min(1, Math.max(0, 1 - (event.clientY - bounds.top - PADDING) / PLOT_SIZE)),
};
}
/** Interactive, dependency-free custom response-curve point editor. */
export function CurveEditor({ points, onChange }: CurveEditorProps) {
const sorted = [...points].sort((a, b) => a.x - b.x);
const samples = sampleCurve("custom", sorted);
const line = samples.map((point) => {
const svg = pointToSvg(point);
return `${svg.x},${svg.y}`;
}).join(" ");
function addPoint(event: ReactPointerEvent<SVGSVGElement>) {
if ((event.target as Element).tagName !== "svg") return;
const point = svgToPoint(event);
onChange([...sorted, point].sort((a, b) => a.x - b.x));
}
function movePoint(index: number, event: ReactPointerEvent<SVGCircleElement>) {
const next = [...sorted];
const candidate = svgToPoint(event);
// Endpoints remain anchored at input 0/1, preventing invalid curves.
if (index === 0) candidate.x = 0;
if (index === next.length - 1) candidate.x = 1;
const lower = index === 0 ? 0 : next[index - 1].x + 0.01;
const upper = index === next.length - 1 ? 1 : next[index + 1].x - 0.01;
candidate.x = Math.min(upper, Math.max(lower, candidate.x));
next[index] = candidate;
onChange(next);
}
return (
<div>
<svg
viewBox={`0 0 ${SIZE} ${SIZE}`}
className="w-full max-w-[220px] cursor-crosshair rounded-lg border border-border bg-surface-2"
onPointerDown={addPoint}
aria-label="Custom response curve editor"
>
{[0.25, 0.5, 0.75].map((value) => (
<g key={value}>
<line x1={PADDING} x2={SIZE - PADDING} y1={PADDING + value * PLOT_SIZE} y2={PADDING + value * PLOT_SIZE} className="stroke-border" />
<line y1={PADDING} y2={SIZE - PADDING} x1={PADDING + value * PLOT_SIZE} x2={PADDING + value * PLOT_SIZE} className="stroke-border" />
</g>
))}
<polyline points={line} fill="none" className="stroke-accent" strokeWidth={2.5} />
{sorted.map((point, index) => {
const svg = pointToSvg(point);
return (
<circle
key={`${point.x}-${point.y}-${index}`}
cx={svg.x}
cy={svg.y}
r={5}
className="cursor-grab fill-accent stroke-surface active:cursor-grabbing"
strokeWidth={2}
onPointerDown={(event) => {
event.stopPropagation();
event.currentTarget.setPointerCapture(event.pointerId);
}}
onPointerMove={(event) => {
if (event.currentTarget.hasPointerCapture(event.pointerId)) movePoint(index, event);
}}
/>
);
})}
</svg>
<p className="mt-1 text-xs text-fg-subtle">Click to add a point; drag points to adjust the curve.</p>
</div>
);
}