Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/components/recalc/calcHeading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ export default function CalcHeading({
}, [copied]);

return (
<div className="mb-6 flex flex-row items-center justify-between border-b pt-6 pb-5">
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
<div className="mb-6 flex flex-row flex-wrap items-center justify-between gap-x-4 gap-y-2 border-b pt-6 pb-5">
<h1 className="text-xl font-semibold tracking-tight sm:text-2xl">
{title}
</h1>
<div ref={wrapperRef} className="relative inline-flex">
<Button
variant="outline"
Expand Down
113 changes: 92 additions & 21 deletions app/lib/math/chains.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,79 @@ import Measurement from '~/lib/models/Measurement';

describe('calculateCenterDistance', () => {
it('calculates center distance correctly', () => {
const result = calculateCenterDistance('#25', 20, 30, 50);
const result = calculateCenterDistance(
new Measurement(0.25, 'in'),
20,
30,
50,
);

expect(result.to('in').scalar).toBeCloseTo(3.099, 3);
});

it('handles zero teeth for p1', () => {
const result = calculateCenterDistance('#25', 0, 30, 50);
const result = calculateCenterDistance(
new Measurement(0.25, 'in'),
0,
30,
50,
);

expect(result.to('in').scalar).toBeCloseTo(4.206, 3);
});

it('handles zero teeth for p2', () => {
const result = calculateCenterDistance('#25', 20, 0, 50);
const result = calculateCenterDistance(
new Measurement(0.25, 'in'),
20,
0,
50,
);

expect(result.to('in').scalar).toBeCloseTo(4.936, 3);
});

it('handles zero links', () => {
const result = calculateCenterDistance('#25', 20, 30, 0);
const result = calculateCenterDistance(
new Measurement(0.25, 'in'),
20,
30,
0,
);

expect(result.to('in').scalar).toBeCloseTo(-0.026, 3);
});

it('handles equal sprockets', () => {
const result = calculateCenterDistance('#25', 20, 20, 50);
const result = calculateCenterDistance(
new Measurement(0.25, 'in'),
20,
20,
50,
);

expect(result.to('in').scalar).toBeCloseTo(3.75, 3);
});

it('works with different chain types', () => {
const result25 = calculateCenterDistance('#25', 20, 30, 50);
const result35 = calculateCenterDistance('#35', 20, 30, 50);
const result40 = calculateCenterDistance('#40', 20, 30, 50);
const result25 = calculateCenterDistance(
new Measurement(0.25, 'in'),
20,
30,
50,
);
const result35 = calculateCenterDistance(
new Measurement(0.375, 'in'),
20,
30,
50,
);
const result40 = calculateCenterDistance(
new Measurement(0.5, 'in'),
20,
30,
50,
);

expect(result25.to('in').scalar).toBeCloseTo(3.099, 3);
expect(result35.to('in').scalar).toBeCloseTo(4.649, 3);
Expand All @@ -46,13 +86,23 @@ describe('calculateCenterDistance', () => {
});

it('handles very small number of links', () => {
const result = calculateCenterDistance('#25', 20, 30, 10);
const result = calculateCenterDistance(
new Measurement(0.25, 'in'),
20,
30,
10,
);

expect(result.to('in').scalar).toBeCloseTo(-0.043, 3);
});

it('handles large number of links', () => {
const result = calculateCenterDistance('#25', 20, 30, 200);
const result = calculateCenterDistance(
new Measurement(0.25, 'in'),
20,
30,
200,
);

expect(result.to('in').scalar).toBeCloseTo(21.871, 3);
});
Expand All @@ -61,7 +111,7 @@ describe('calculateCenterDistance', () => {
describe('calculateCenters', () => {
it('calculates centers correctly', () => {
const result = calculateCenters(
'#25',
new Measurement(0.25, 'in'),
20,
30,
new Measurement(10, 'in'),
Expand All @@ -76,7 +126,7 @@ describe('calculateCenters', () => {

it('handles zero desired center', () => {
const result = calculateCenters(
'#25',
new Measurement(0.25, 'in'),
20,
30,
new Measurement(0, 'in'),
Expand All @@ -89,9 +139,24 @@ describe('calculateCenters', () => {
expect(result.smaller.links).toBe(0);
});

it('handles zero pitch', () => {
const result = calculateCenters(
new Measurement(0, 'in'),
20,
30,
new Measurement(10, 'in'),
false,
);

expect(result.larger.distance.to('in').scalar).toBe(0);
expect(result.smaller.distance.to('in').scalar).toBe(0);
expect(result.larger.links).toBe(0);
expect(result.smaller.links).toBe(0);
});

it('handles zero teeth for p1', () => {
const result = calculateCenters(
'#25',
new Measurement(0.25, 'in'),
0,
30,
new Measurement(10, 'in'),
Expand All @@ -106,7 +171,7 @@ describe('calculateCenters', () => {

it('handles zero teeth for p2', () => {
const result = calculateCenters(
'#25',
new Measurement(0.25, 'in'),
20,
0,
new Measurement(10, 'in'),
Expand All @@ -121,7 +186,7 @@ describe('calculateCenters', () => {

it('handles invalid configuration (pitch diameter too large)', () => {
const result = calculateCenters(
'#25',
new Measurement(0.25, 'in'),
200,
200,
new Measurement(1, 'in'),
Expand All @@ -136,7 +201,7 @@ describe('calculateCenters', () => {

it('works with allowHalfLinks true', () => {
const result = calculateCenters(
'#25',
new Measurement(0.25, 'in'),
20,
30,
new Measurement(10, 'in'),
Expand All @@ -151,7 +216,7 @@ describe('calculateCenters', () => {

it('works with allowHalfLinks false (rounds to even)', () => {
const result = calculateCenters(
'#25',
new Measurement(0.25, 'in'),
20,
30,
new Measurement(10, 'in'),
Expand All @@ -166,7 +231,7 @@ describe('calculateCenters', () => {

it('handles equal sprockets', () => {
const result = calculateCenters(
'#25',
new Measurement(0.25, 'in'),
20,
20,
new Measurement(10, 'in'),
Expand All @@ -179,7 +244,7 @@ describe('calculateCenters', () => {

it('handles very small desired center', () => {
const result = calculateCenters(
'#25',
new Measurement(0.25, 'in'),
20,
30,
new Measurement(0.1, 'in'),
Expand All @@ -192,7 +257,7 @@ describe('calculateCenters', () => {

it('handles very large desired center', () => {
const result = calculateCenters(
'#25',
new Measurement(0.25, 'in'),
20,
30,
new Measurement(100, 'in'),
Expand All @@ -205,7 +270,13 @@ describe('calculateCenters', () => {

it('calculates difference from target correctly', () => {
const desiredCenter = new Measurement(10, 'in');
const result = calculateCenters('#25', 20, 30, desiredCenter, false);
const result = calculateCenters(
new Measurement(0.25, 'in'),
20,
30,
desiredCenter,
false,
);

expect(result.larger.differenceFromTarget.to('in').scalar).toBeCloseTo(
0.117,
Expand Down
21 changes: 11 additions & 10 deletions app/lib/math/chains.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import Measurement from '~/lib/models/Measurement';
import { SimpleSprocket } from '~/lib/models/Sprocket';

export function calculateCenterDistance(
chainType: string,
pitch: Measurement,
p1Teeth: number,
p2Teeth: number,
links: number,
): Measurement {
const P = new SimpleSprocket(p1Teeth, chainType).pitch;
const P = pitch;
const N = Math.max(p1Teeth, p2Teeth);
const n = Math.min(p1Teeth, p2Teeth);
const t1_ = 2 * links - N - n;
Expand All @@ -26,17 +25,19 @@ export type ChainClosestCentersResult = {
larger: Result;
};
export function calculateCenters(
chainType: string,
pitch: Measurement,
p1Teeth: number,
p2Teeth: number,
desiredCenter: Measurement,
allowHalfLinks: boolean,
): ChainClosestCentersResult {
if (
[desiredCenter.scalar, p1Teeth, p2Teeth].includes(0) ||
new SimpleSprocket(p1Teeth, chainType).pitchDiameter
[desiredCenter.scalar, p1Teeth, p2Teeth, pitch.scalar].includes(0) ||
pitch
.mul(p1Teeth)
.div(Math.PI)
.div(2)
.add(new SimpleSprocket(p2Teeth, chainType).pitchDiameter.div(2))
.add(pitch.mul(p2Teeth).div(Math.PI).div(2))
.gt(desiredCenter)
) {
return {
Expand All @@ -56,7 +57,7 @@ export function calculateCenters(
const z1 = p1Teeth;
const z2 = p2Teeth;
const c0 = desiredCenter;
const p = new SimpleSprocket(p1Teeth, chainType).pitch;
const p = pitch;
const t1 = c0.mul(2).div(p);
const t2 = (z1 + z2) / 2;
const t3 = p.mul(Math.pow(Math.abs(z2 - z1) / (2 * Math.PI), 2)).div(c0);
Expand All @@ -68,13 +69,13 @@ export function calculateCenters(
allowHalfLinks ? Math.floor(n) : Math.floor(n / 2) * 2;

const smallerDistance = calculateCenterDistance(
chainType,
pitch,
z1,
z2,
roundLinksDown(x0),
);
const largerDistance = calculateCenterDistance(
chainType,
pitch,
z1,
z2,
roundLinksUp(x0),
Expand Down
5 changes: 4 additions & 1 deletion app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ export function Layout({ children }: { children: ReactNode }) {
<AppSidebar />
<SidebarInset>
<Nav />
<div className="container mx-auto" data-testid="entrypoint">
<div
className="container mx-auto px-2 sm:px-0"
data-testid="entrypoint"
>
{children}
<footer className="mt-16 border-t py-8">
<Warning />
Expand Down
12 changes: 7 additions & 5 deletions app/routes/arm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,9 @@ export default function Arm() {
<h2 className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">
Motor &amp; Gearing
</h2>
<IOLine>
{/* Motor and Ratio each get their own row on mobile; paired
at md+. */}
<div className="flex flex-col gap-3 *:flex-1 md:flex-row md:gap-x-4">
<MotorInput
stateHook={[motor, setMotor]}
testId="motor"
Expand All @@ -458,7 +460,7 @@ export default function Arm() {
testId="ratio"
labelAbove
/>
</IOLine>
</div>
</div>
<div className="border-t" />

Expand Down Expand Up @@ -824,7 +826,7 @@ export default function Arm() {
<div className="border-t" />

{/* Feedforward */}
<div className="grid grid-cols-3 gap-2 p-4">
<div className="grid grid-cols-1 gap-2 p-4 md:grid-cols-3">
<MeasurementDisplayOutput
state={feedforwardGains.kV}
label="kV"
Expand Down Expand Up @@ -884,7 +886,7 @@ export default function Arm() {

{optimizationEnabled && (
<div className="flex flex-col gap-4 px-1">
<div className="flex flex-row flex-wrap gap-4">
<div className="flex flex-col gap-4 md:flex-row">
{/* Optimal configuration grid */}
<div className="min-w-0 flex-1">
<OptimalConfigGrid
Expand All @@ -897,7 +899,7 @@ export default function Arm() {
</div>

{/* Right column: settings + selected config */}
<div className="flex w-64 shrink-0 flex-col gap-3">
<div className="flex w-full flex-col gap-3 md:w-64 md:shrink-0">
<section className="flex flex-col gap-3 rounded-lg border p-4">
<h2 className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">
Settings
Expand Down
Loading
Loading