mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 17:40:46 -04:00
top-down roomba view!!! finally!!
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,8 +11,8 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||||
<title>Multi Roomba Rover</title>
|
<title>Multi Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CI7u01Fl.js"></script>
|
<script type="module" crossorigin src="/assets/index-D_fgZoDJ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-K76_qLOR.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BpKtN3Ry.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -72,16 +72,10 @@ export default function TelemetryPanel() {
|
|||||||
</div>
|
</div>
|
||||||
<SituationalMap sensors={sensors} variant="full" />
|
<SituationalMap sensors={sensors} variant="full" />
|
||||||
|
|
||||||
<div className="surface flex flex-col gap-0.5 text-sm">
|
<div className="surface flex gap-0.5 text-sm">
|
||||||
<div className="flex gap-0.5">
|
<IrPill label="IR Omni" value={sensors.infraredCharacterOmni} />
|
||||||
<DirtMeter label="Dirt Detect" value={sensors.dirtDetect} />
|
<IrPill label="IR Left" value={sensors.infraredCharacterLeft} />
|
||||||
<DirtMeter label="Dirt Left" value={sensors.dirtDetectLeft} />
|
<IrPill label="IR Right" value={sensors.infraredCharacterRight} />
|
||||||
</div>
|
|
||||||
<div className="flex gap-0.5">
|
|
||||||
<IrPill label="IR Omni" value={sensors.infraredCharacterOmni} />
|
|
||||||
<IrPill label="IR Left" value={sensors.infraredCharacterLeft} />
|
|
||||||
<IrPill label="IR Right" value={sensors.infraredCharacterRight} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -193,13 +187,13 @@ function SituationalMap({ sensors, variant = 'full' }) {
|
|||||||
const size = variant === 'mini' ? 190 : 240;
|
const size = variant === 'mini' ? 190 : 240;
|
||||||
const center = size / 2;
|
const center = size / 2;
|
||||||
const innerCircle = variant === 'mini' ? 78 : 96;
|
const innerCircle = variant === 'mini' ? 78 : 96;
|
||||||
const lightRingInner = variant === 'mini' ? 68 : 82;
|
const lightRingInner = variant === 'mini' ? 78 : 92;
|
||||||
const lightRingOuter = variant === 'mini' ? 78 : 92;
|
const lightRingOuter = variant === 'mini' ? 86 : 100;
|
||||||
const cliffRingInner = variant === 'mini' ? 58 : 72;
|
const cliffRingInner = variant === 'mini' ? 62 : 76;
|
||||||
const cliffRingOuter = variant === 'mini' ? 68 : 82;
|
const cliffRingOuter = variant === 'mini' ? 70 : 84;
|
||||||
const bumpRingInner = variant === 'mini' ? 82 : 104;
|
const bumpRingInner = variant === 'mini' ? 82 : 104;
|
||||||
const bumpRingOuter = variant === 'mini' ? 90 : 114;
|
const bumpRingOuter = variant === 'mini' ? 90 : 114;
|
||||||
const wheelLineOffset = variant === 'mini' ? 34 : 42;
|
const wheelLineOffset = variant === 'mini' ? 50 : 68;
|
||||||
|
|
||||||
const lightSignals = [
|
const lightSignals = [
|
||||||
sensors?.lightBumpLeftSignal,
|
sensors?.lightBumpLeftSignal,
|
||||||
@@ -218,20 +212,42 @@ function SituationalMap({ sensors, variant = 'full' }) {
|
|||||||
const mainBrushCurrent = sensors?.mainBrushCurrentMa ?? 0;
|
const mainBrushCurrent = sensors?.mainBrushCurrentMa ?? 0;
|
||||||
const wheelOver = sensors?.wheelOvercurrents || {};
|
const wheelOver = sensors?.wheelOvercurrents || {};
|
||||||
|
|
||||||
const lightSegments = [
|
const lightAngles = buildSegments({
|
||||||
{ label: 'L', start: -100, end: -70, value: sensors?.lightBumpLeftSignal, active: sensors?.lightBumper?.left },
|
count: 6,
|
||||||
{ label: 'FL', start: -70, end: -35, value: sensors?.lightBumpFrontLeftSignal, active: sensors?.lightBumper?.frontLeft },
|
totalSpan: 140,
|
||||||
{ label: 'CL', start: -35, end: -5, value: sensors?.lightBumpCenterLeftSignal, active: sensors?.lightBumper?.centerLeft },
|
gap: 6,
|
||||||
{ label: 'CR', start: 5, end: 35, value: sensors?.lightBumpCenterRightSignal, active: sensors?.lightBumper?.centerRight },
|
startAngle: -70,
|
||||||
{ label: 'FR', start: 35, end: 70, value: sensors?.lightBumpFrontRightSignal, active: sensors?.lightBumper?.frontRight },
|
});
|
||||||
{ label: 'R', start: 70, end: 100, value: sensors?.lightBumpRightSignal, active: sensors?.lightBumper?.right },
|
const lightLabels = ['L', 'FL', 'CL', 'CR', 'FR', 'R'];
|
||||||
|
const lightValues = [
|
||||||
|
sensors?.lightBumpLeftSignal,
|
||||||
|
sensors?.lightBumpFrontLeftSignal,
|
||||||
|
sensors?.lightBumpCenterLeftSignal,
|
||||||
|
sensors?.lightBumpCenterRightSignal,
|
||||||
|
sensors?.lightBumpFrontRightSignal,
|
||||||
|
sensors?.lightBumpRightSignal,
|
||||||
];
|
];
|
||||||
|
const lightActive = [
|
||||||
|
sensors?.lightBumper?.left,
|
||||||
|
sensors?.lightBumper?.frontLeft,
|
||||||
|
sensors?.lightBumper?.centerLeft,
|
||||||
|
sensors?.lightBumper?.centerRight,
|
||||||
|
sensors?.lightBumper?.frontRight,
|
||||||
|
sensors?.lightBumper?.right,
|
||||||
|
];
|
||||||
|
const lightSegments = lightAngles.map((ang, idx) => ({
|
||||||
|
label: lightLabels[idx],
|
||||||
|
start: ang.start,
|
||||||
|
end: ang.end,
|
||||||
|
value: lightValues[idx],
|
||||||
|
active: lightActive[idx],
|
||||||
|
}));
|
||||||
|
|
||||||
const cliffSegments = [
|
const cliffSegments = [
|
||||||
{ label: 'Cliff L', start: -95, end: -70, value: sensors?.cliffLeftSignal, active: sensors?.cliffLeft },
|
{ label: 'Cliff L', start: -65, end: -45, value: sensors?.cliffLeftSignal, active: sensors?.cliffLeft },
|
||||||
{ label: 'Cliff FL', start: -45, end: -15, value: sensors?.cliffFrontLeftSignal, active: sensors?.cliffFrontLeft },
|
{ label: 'Cliff FL', start: -25, end: -5, value: sensors?.cliffFrontLeftSignal, active: sensors?.cliffFrontLeft },
|
||||||
{ label: 'Cliff FR', start: 15, end: 45, value: sensors?.cliffFrontRightSignal, active: sensors?.cliffFrontRight },
|
{ label: 'Cliff FR', start: 5, end: 25, value: sensors?.cliffFrontRightSignal, active: sensors?.cliffFrontRight },
|
||||||
{ label: 'Cliff R', start: 70, end: 95, value: sensors?.cliffRightSignal, active: sensors?.cliffRight },
|
{ label: 'Cliff R', start: 45, end: 65, value: sensors?.cliffRightSignal, active: sensors?.cliffRight },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -248,17 +264,31 @@ function SituationalMap({ sensors, variant = 'full' }) {
|
|||||||
overcurrent={wheelOver.leftWheel}
|
overcurrent={wheelOver.leftWheel}
|
||||||
label="L"
|
label="L"
|
||||||
/>
|
/>
|
||||||
<WheelVisual
|
<WheelVisual
|
||||||
cx={center + wheelLineOffset}
|
cx={center + wheelLineOffset}
|
||||||
cy={center}
|
cy={center}
|
||||||
current={wheelCurrentRight}
|
current={wheelCurrentRight}
|
||||||
drop={bumps.wheelDropRight}
|
drop={bumps.wheelDropRight}
|
||||||
overcurrent={wheelOver.rightWheel}
|
overcurrent={wheelOver.rightWheel}
|
||||||
label="R"
|
label="R"
|
||||||
/>
|
/>
|
||||||
|
<SideBrushVisual
|
||||||
|
cx={center + innerCircle * 0.65}
|
||||||
|
cy={center - innerCircle * 0.55}
|
||||||
|
current={sideBrushCurrent}
|
||||||
|
overcurrent={wheelOver.sideBrush}
|
||||||
|
/>
|
||||||
|
<MainBrushVisual
|
||||||
|
cx={center}
|
||||||
|
cy={center}
|
||||||
|
current={mainBrushCurrent}
|
||||||
|
overcurrent={wheelOver.mainBrush}
|
||||||
|
variant={variant}
|
||||||
|
dirtLeft={sensors?.dirtDetectLeft}
|
||||||
|
dirtRight={sensors?.dirtDetect}
|
||||||
|
/>
|
||||||
{lightSegments.map((seg) => {
|
{lightSegments.map((seg) => {
|
||||||
const pct = seg.value == null ? 0 : Math.max(0, Math.min(1, seg.value / effectiveLightMax));
|
const color = lightBumpColor(seg.value);
|
||||||
const color = pct > 0.75 ? '#f59e0b' : '#22c55e';
|
|
||||||
return (
|
return (
|
||||||
<ArcSegment
|
<ArcSegment
|
||||||
key={seg.label}
|
key={seg.label}
|
||||||
@@ -268,15 +298,14 @@ function SituationalMap({ sensors, variant = 'full' }) {
|
|||||||
rOuter={lightRingOuter}
|
rOuter={lightRingOuter}
|
||||||
startDeg={seg.start}
|
startDeg={seg.start}
|
||||||
endDeg={seg.end}
|
endDeg={seg.end}
|
||||||
color={seg.value == null ? '#475569' : color}
|
color={color}
|
||||||
opacity={seg.value == null ? 0.35 : 0.95}
|
opacity={1}
|
||||||
pulse={Boolean(seg.active)}
|
pulse={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{cliffSegments.map((seg) => {
|
{cliffSegments.map((seg) => {
|
||||||
const pct = seg.value == null ? 0 : Math.max(0, Math.min(1, seg.value / 4095));
|
const color = cliffColor(seg.value, seg.active);
|
||||||
const color = pct > 0.6 ? '#f59e0b' : '#22c55e';
|
|
||||||
return (
|
return (
|
||||||
<ArcSegment
|
<ArcSegment
|
||||||
key={seg.label}
|
key={seg.label}
|
||||||
@@ -287,7 +316,7 @@ function SituationalMap({ sensors, variant = 'full' }) {
|
|||||||
startDeg={seg.start}
|
startDeg={seg.start}
|
||||||
endDeg={seg.end}
|
endDeg={seg.end}
|
||||||
color={seg.value == null ? '#475569' : color}
|
color={seg.value == null ? '#475569' : color}
|
||||||
opacity={seg.value == null ? 0.35 : 0.9}
|
opacity={1}
|
||||||
pulse={Boolean(seg.active)}
|
pulse={Boolean(seg.active)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -295,41 +324,25 @@ function SituationalMap({ sensors, variant = 'full' }) {
|
|||||||
<ArcSegment
|
<ArcSegment
|
||||||
cx={center}
|
cx={center}
|
||||||
cy={center}
|
cy={center}
|
||||||
rInner={bumpRingInner}
|
rInner={lightRingOuter + 2}
|
||||||
rOuter={bumpRingOuter}
|
rOuter={lightRingOuter + 8}
|
||||||
startDeg={-80}
|
startDeg={-70}
|
||||||
endDeg={-25}
|
endDeg={-6}
|
||||||
color={bumps.bumpLeft ? '#ef4444' : '#475569'}
|
color={bumps.bumpLeft ? '#ef4444' : '#475569'}
|
||||||
opacity={bumps.bumpLeft ? 0.9 : 0.7}
|
opacity={1}
|
||||||
pulse={Boolean(bumps.bumpLeft)}
|
pulse={Boolean(bumps.bumpLeft)}
|
||||||
/>
|
/>
|
||||||
<ArcSegment
|
<ArcSegment
|
||||||
cx={center}
|
cx={center}
|
||||||
cy={center}
|
cy={center}
|
||||||
rInner={bumpRingInner}
|
rInner={lightRingOuter + 2}
|
||||||
rOuter={bumpRingOuter}
|
rOuter={lightRingOuter + 8}
|
||||||
startDeg={25}
|
startDeg={6}
|
||||||
endDeg={80}
|
endDeg={70}
|
||||||
color={bumps.bumpRight ? '#ef4444' : '#475569'}
|
color={bumps.bumpRight ? '#ef4444' : '#475569'}
|
||||||
opacity={bumps.bumpRight ? 0.9 : 0.7}
|
opacity={1}
|
||||||
pulse={Boolean(bumps.bumpRight)}
|
pulse={Boolean(bumps.bumpRight)}
|
||||||
/>
|
/>
|
||||||
<SideBrushVisual
|
|
||||||
cx={center + innerCircle * 0.45}
|
|
||||||
cy={center - innerCircle * 0.35}
|
|
||||||
current={sideBrushCurrent}
|
|
||||||
overcurrent={wheelOver.sideBrush}
|
|
||||||
/>
|
|
||||||
<MainBrushVisual
|
|
||||||
cx={center}
|
|
||||||
cy={center + innerCircle * 0.25}
|
|
||||||
current={mainBrushCurrent}
|
|
||||||
overcurrent={wheelOver.mainBrush}
|
|
||||||
variant={variant}
|
|
||||||
/>
|
|
||||||
<text x={center} y={18} textAnchor="middle" className="fill-slate-500 text-[0.6rem]">
|
|
||||||
N
|
|
||||||
</text>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -350,7 +363,7 @@ function ArcSegment({ cx, cy, rInner, rOuter, startDeg, endDeg, color, pulse = f
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
fill="none"
|
fill="none"
|
||||||
className="animate-pulse"
|
className="animate-pulse"
|
||||||
opacity={0.6}
|
opacity={1}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
@@ -378,39 +391,51 @@ function toRad(angleDeg) {
|
|||||||
return ((angleDeg - 90) * Math.PI) / 180;
|
return ((angleDeg - 90) * Math.PI) / 180;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clamp01(value) {
|
|
||||||
return Math.max(0, Math.min(1, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function currentColor(value, overcurrent) {
|
function currentColor(value, overcurrent) {
|
||||||
if (overcurrent) return '#ef4444';
|
if (overcurrent) return '#ef4444';
|
||||||
const mag = Math.abs(value);
|
const mag = Math.abs(value);
|
||||||
if (mag > 900) return '#f59e0b';
|
if (mag > 900) return '#f59e0b';
|
||||||
if (mag > 300) return '#22c55e';
|
if (mag > 300) return '#22c55e';
|
||||||
return '#94a3b8';
|
return '#64748b'; // unified idle tone for wheels/brushes
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp01(value) {
|
||||||
|
return Math.max(0, Math.min(1, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function WheelVisual({ cx, cy, current, drop, overcurrent, label }) {
|
function WheelVisual({ cx, cy, current, drop, overcurrent, label }) {
|
||||||
const mag = Math.abs(current);
|
const mag = Math.abs(current);
|
||||||
const pct = clamp01(mag / 1200);
|
const pct = clamp01(mag / 1200);
|
||||||
const color = currentColor(current, overcurrent);
|
const color = currentColor(current, overcurrent);
|
||||||
const barH = 30;
|
const barH = 56;
|
||||||
const currentW = 12;
|
const currentW = 14;
|
||||||
const dropW = 8;
|
const dropW = 9;
|
||||||
const gap = 4;
|
const gap = 3;
|
||||||
const currentFill = barH * pct;
|
const currentFill = barH * pct;
|
||||||
const sign = label === 'L' ? -1 : 1;
|
const sign = label === 'L' ? -1 : 1;
|
||||||
const currentCenterX = sign * (-dropW / 2 - gap / 2);
|
const currentCenterX = sign * (-dropW / 2 - gap / 2);
|
||||||
const dropCenterX = sign * (currentW / 2 + gap / 2);
|
const dropCenterX = sign * (currentW / 2 + gap / 2);
|
||||||
|
const groupWidth = currentW + dropW + gap + 2;
|
||||||
|
const groupHeight = barH + 4;
|
||||||
return (
|
return (
|
||||||
<g transform={`translate(${cx},${cy})`}>
|
<g transform={`translate(${cx},${cy})`}>
|
||||||
|
<rect
|
||||||
|
x={-groupWidth / 2}
|
||||||
|
y={-groupHeight / 2}
|
||||||
|
width={groupWidth}
|
||||||
|
height={groupHeight}
|
||||||
|
rx="4"
|
||||||
|
fill="none"
|
||||||
|
stroke="#64748b"
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
<rect
|
<rect
|
||||||
x={currentCenterX - currentW / 2}
|
x={currentCenterX - currentW / 2}
|
||||||
y={-barH / 2}
|
y={-barH / 2}
|
||||||
width={currentW}
|
width={currentW}
|
||||||
height={barH}
|
height={barH}
|
||||||
fill="#0f172a"
|
fill="#0f172a"
|
||||||
stroke="#1f2937"
|
stroke="#0f172a"
|
||||||
strokeWidth="1"
|
strokeWidth="1"
|
||||||
rx="2"
|
rx="2"
|
||||||
/>
|
/>
|
||||||
@@ -440,10 +465,9 @@ function WheelVisual({ cx, cy, current, drop, overcurrent, label }) {
|
|||||||
|
|
||||||
function SideBrushVisual({ cx, cy, current, overcurrent }) {
|
function SideBrushVisual({ cx, cy, current, overcurrent }) {
|
||||||
const mag = Math.abs(current);
|
const mag = Math.abs(current);
|
||||||
const color = currentColor(current, overcurrent);
|
const color = currentColor(current * 3, overcurrent); // more sensitive; side brush draws little current
|
||||||
const opacity = clamp01(mag / 1000) * 0.6 + 0.3;
|
const armLength = 42;
|
||||||
const armLength = 26;
|
const spinDuration = mag > 0 ? 0.65 : null; // spin only when drawing current
|
||||||
const spinDuration = mag > 10 ? Math.max(0.35, 2.2 - mag / 500) : null;
|
|
||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
style={{
|
style={{
|
||||||
@@ -451,27 +475,27 @@ function SideBrushVisual({ cx, cy, current, overcurrent }) {
|
|||||||
animation: spinDuration ? `spin ${spinDuration}s linear infinite` : 'none',
|
animation: spinDuration ? `spin ${spinDuration}s linear infinite` : 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<circle cx={cx} cy={cy} r={10} fill="#0f172a" stroke={color} strokeWidth="2" opacity={opacity} />
|
<circle cx={cx} cy={cy} r={10} fill="#64748b" stroke="#64748b" strokeWidth="1" />
|
||||||
{[0, 120, 240].map((deg) => {
|
{[0, 120, 240].map((deg) => {
|
||||||
const rad = toRad(deg);
|
const rad = toRad(deg);
|
||||||
const x2 = cx + armLength * Math.cos(rad);
|
const x2 = cx + armLength * Math.cos(rad);
|
||||||
const y2 = cy + armLength * Math.sin(rad);
|
const y2 = cy + armLength * Math.sin(rad);
|
||||||
return <line key={deg} x1={cx} y1={cy} x2={x2} y2={y2} stroke={color} strokeWidth="4" strokeLinecap="round" opacity={opacity} />;
|
return <line key={deg} x1={cx} y1={cy} x2={x2} y2={y2} stroke={color} strokeWidth="4" strokeLinecap="round" />;
|
||||||
})}
|
})}
|
||||||
{overcurrent ? <circle cx={cx} cy={cy} r={armLength + 6} stroke="#ef4444" strokeWidth="3" fill="none" className="animate-pulse" opacity={0.6} /> : null}
|
{overcurrent ? <circle cx={cx} cy={cy} r={armLength + 8} stroke="#ef4444" strokeWidth="3" fill="none" className="animate-pulse" /> : null}
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MainBrushVisual({ cx, cy, current, overcurrent, variant }) {
|
function MainBrushVisual({ cx, cy, current, overcurrent, variant, dirtLeft, dirtRight }) {
|
||||||
const mag = Math.abs(current);
|
const mag = Math.abs(current);
|
||||||
const color = currentColor(current, overcurrent);
|
const color = currentColor(current, overcurrent);
|
||||||
const opacity = clamp01(mag / 1000) * 0.6 + 0.3;
|
const opacity = 1;
|
||||||
const rollerWidth = 44;
|
const rollerWidth = 96;
|
||||||
const rollerHeight = 8;
|
const rollerHeight = 12;
|
||||||
const patternA = `main-brush-pattern-a-${variant}`;
|
const patternA = `main-brush-pattern-a-${variant}`;
|
||||||
const patternB = `main-brush-pattern-b-${variant}`;
|
const patternB = `main-brush-pattern-b-${variant}`;
|
||||||
const dur = mag > 10 ? Math.max(0.35, 2.4 - mag / 500) : null;
|
const dur = mag > 0 ? 0.6 : null; // only scroll when drawing current
|
||||||
const dir = current >= 0 ? 1 : -1;
|
const dir = current >= 0 ? 1 : -1;
|
||||||
return (
|
return (
|
||||||
<g>
|
<g>
|
||||||
@@ -495,24 +519,26 @@ function MainBrushVisual({ cx, cy, current, overcurrent, variant }) {
|
|||||||
</defs>
|
</defs>
|
||||||
<rect
|
<rect
|
||||||
x={cx - rollerWidth / 2}
|
x={cx - rollerWidth / 2}
|
||||||
y={cy - 12}
|
y={cy - 14}
|
||||||
width={rollerWidth}
|
width={rollerWidth}
|
||||||
height={rollerHeight}
|
height={rollerHeight}
|
||||||
rx="3"
|
rx="3"
|
||||||
fill={`url(#${patternA})`}
|
fill={`url(#${patternA})`}
|
||||||
stroke="#0f172a"
|
stroke="#64748b"
|
||||||
strokeWidth="1"
|
strokeWidth="1"
|
||||||
/>
|
/>
|
||||||
<rect
|
<rect
|
||||||
x={cx - rollerWidth / 2}
|
x={cx - rollerWidth / 2}
|
||||||
y={cy + 6}
|
y={cy + 4}
|
||||||
width={rollerWidth}
|
width={rollerWidth}
|
||||||
height={rollerHeight}
|
height={rollerHeight}
|
||||||
rx="3"
|
rx="3"
|
||||||
fill={`url(#${patternB})`}
|
fill={`url(#${patternB})`}
|
||||||
stroke="#0f172a"
|
stroke="#64748b"
|
||||||
strokeWidth="1"
|
strokeWidth="1"
|
||||||
/>
|
/>
|
||||||
|
<circle cx={cx - rollerWidth / 4} cy={cy - 8} r={3.5} fill="#fbbf24" />
|
||||||
|
<circle cx={cx + rollerWidth / 4} cy={cy + 8} r={3.5} fill="#fbbf24" />
|
||||||
{overcurrent ? (
|
{overcurrent ? (
|
||||||
<rect
|
<rect
|
||||||
x={cx - rollerWidth / 2 - 4}
|
x={cx - rollerWidth / 2 - 4}
|
||||||
@@ -530,3 +556,34 @@ function MainBrushVisual({ cx, cy, current, overcurrent, variant }) {
|
|||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildSegments({ count, totalSpan, gap, startAngle }) {
|
||||||
|
const usable = totalSpan - gap * (count - 1);
|
||||||
|
const width = usable / count;
|
||||||
|
const segments = [];
|
||||||
|
let cursor = startAngle;
|
||||||
|
for (let i = 0; i < count; i += 1) {
|
||||||
|
const end = cursor + width;
|
||||||
|
segments.push({ start: cursor, end });
|
||||||
|
cursor = end + gap;
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LIGHT_BUMP_MAX = 2000; // easy sensitivity tuning for light bumps
|
||||||
|
function lightBumpColor(value) {
|
||||||
|
if (value == null || value <= 0) return '#000000';
|
||||||
|
const t = clamp01(value / LIGHT_BUMP_MAX);
|
||||||
|
const hue = 140 - t * 80; // greenish toward amber
|
||||||
|
const lightness = 15 + t * 55;
|
||||||
|
return `hsl(${hue} 90% ${lightness}%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cliffColor(value, active) {
|
||||||
|
if (active) return '#ef4444';
|
||||||
|
const t = clamp01(value == null ? 0 : value / 4095);
|
||||||
|
const start = [47, 55, 69]; // #2f3745-ish dark gray
|
||||||
|
const end = [245, 158, 11]; // amber
|
||||||
|
const [r, g, b] = start.map((s, i) => Math.round(s + (end[i] - s) * t));
|
||||||
|
return `rgb(${r}, ${g}, ${b})`;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user