Skip to content

Commit db864dd

Browse files
committed
Bug fixes, sanity checks, QOL features
1 parent 6acd3ef commit db864dd

6 files changed

Lines changed: 112 additions & 62 deletions

File tree

src/app/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/componen
44
import PathControls from "@/components/path-controls";
55
import { Paths, Poses } from "@/hooks/use-visualizer";
66
import PoseControls from "@/components/pose-controls";
7+
import DrawPaths from "@/components/path-overlay";
78

89
export default function Home() {
910
const {
@@ -53,8 +54,10 @@ export default function Home() {
5354
className="max-h-full max-w-full object-contain"
5455
alt="Decode Field"
5556
draggable="false"
57+
id="field-canvas"
5658
/>
5759
</div>
60+
<DrawPaths poses={poses} />
5861
</ResizablePanel>
5962
<ResizableHandle withHandle />
6063
<ResizablePanel defaultSize="27.5%" maxSize="40%" minSize="20%">

src/components/path-controls.tsx

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export default function PathControls ({
104104
<div className="flex flex-row w-full items-center">
105105
<AccordionTrigger className="hover:no-underline">
106106
<div className="flex w-fit text-xs flex-row mr-2">
107-
Control Points
107+
Control Points ({path.controlPoints?.length || 0})
108108
</div>
109109
</AccordionTrigger>
110110
<Button
@@ -183,7 +183,7 @@ export default function PathControls ({
183183
<div className="flex flex-row w-full items-center">
184184
<AccordionTrigger className="hover:no-underline">
185185
<div className="flex w-fit flex-row text-xs gap-2 mr-2">
186-
Callbacks
186+
Callbacks ({path.callbacks?.length || 0})
187187
</div>
188188
</AccordionTrigger>
189189

@@ -193,7 +193,7 @@ export default function PathControls ({
193193
</div>
194194
<AccordionContent className="flex h-full flex-col gap-2">
195195
{ /* TODO: When we add support for converting distance to s value, make sure that they are compared by s value, not just the raw distance */ }
196-
{(path.callbacks.sort((a, b) => a.value - b.value) || []).map((callback) => (
196+
{(path.callbacks.sort((a, b) => (a.value ?? 0) - (b.value ?? 0)) || []).map((callback) => (
197197
<div className="flex flex-row mt-2 items-center gap-2 text-2xl" key={callback.id}>
198198
<Button className="bg-transparent hover:bg-transparent p-0 h-auto" onClick={() => deleteCallback(path.id, path.callbacks, callback.id)}>
199199
<CircleMinus color="#C00000" />
@@ -202,16 +202,23 @@ export default function PathControls ({
202202
<Input
203203
id="callback-input"
204204
type="number"
205-
placeholder="Dist"
206-
value={callback.value}
207-
onChange={(e) => {
208-
updateCallback(path.id, path.callbacks, callback.id, {
209-
value: parseFloat(e.target.value) || 0
210-
});
205+
placeholder={callback.distValue ? "Dist" : "S"}
206+
defaultValue={callback.value ?? 0}
207+
onChange={(e) => { // TODO: Handle distance values with units (currently just behaves like an s value regardless)
208+
let final = null;
209+
if (e.target.value !== "") {
210+
const parsed = parseFloat(e.target.value);
211+
if (!isNaN(parsed)) { final = Math.max(0, Math.min(1, parsed)); }
212+
}
213+
updateCallback(path.id, path.callbacks, callback.id, { value: final});
211214
}}
212-
onClick={() => {
213-
if (callback.value == 0) {
214-
updateCallback(path.id, path.callbacks, callback.id, { value: 0 });
215+
onBlur={(e) => {
216+
if (e.target.value !== "") {
217+
const parsed = parseFloat(e.target.value);
218+
if (!isNaN(parsed)) {
219+
const final = Math.max(0, Math.min(1, parsed));
220+
e.target.value = final.toString();
221+
}
215222
}
216223
}}
217224
className="min-w-16 max-w-20 transition-colors focus-visible:border-red-500 focus-visible:ring-red-500 bg-zinc-900"
@@ -228,7 +235,6 @@ export default function PathControls ({
228235
}}
229236
>
230237
<ComboboxInput
231-
placeholder=""
232238
className="min-w-14 max-w-14 focus-visible:border-red-500 focus-visible:ring-red-500 bg-zinc-900"
233239
/>
234240
<ComboboxContent>
@@ -248,7 +254,7 @@ export default function PathControls ({
248254
<Input
249255
id={`callback-method-${callback.id}`}
250256
type="text"
251-
value={callback.method}
257+
value={callback.method ?? ""}
252258
onChange={(e) => {
253259
updateCallback(path.id, path.callbacks, callback.id, {
254260
method: e.target.value

src/components/path-overlay.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use client';
2+
3+
import { useEffect, useRef } from "react";
4+
5+
interface PathDrawProps {
6+
poses: Pose[];
7+
}
8+
9+
export default function DrawPaths({ poses }: PathDrawProps) {
10+
const canvasRef = useRef<HTMLCanvasElement>(null);
11+
12+
useEffect(() => {
13+
const canvas = canvasRef.current;
14+
if (!canvas) return;
15+
16+
const ctx = canvas.getContext("2d");
17+
if (!ctx) return;
18+
19+
ctx.clearRect(0, 0, canvas.width, canvas.height);
20+
const centerX = canvas.width / 2;
21+
const centerY = canvas.height / 2;
22+
const scale = canvas.width / 141.5;
23+
24+
poses.forEach((pose) => {
25+
if (pose.x === null || pose.y === null || pose.heading === null) return;
26+
const posX = centerX + pose.x * scale;
27+
const posY = centerY - pose.y * scale;
28+
29+
ctx.beginPath();
30+
ctx.arc(posX, posY, 2, 0, 2 * Math.PI);
31+
ctx.fillStyle = 'black';
32+
ctx.fill();
33+
});
34+
}, [poses]);
35+
36+
return (
37+
<canvas
38+
ref={canvasRef}
39+
id="field-canvas"
40+
className="absolute top-0 left-0 w-full h-full pointer-events-none"
41+
/>
42+
)
43+
}

src/components/pose-controls.tsx

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,25 @@ export default function PoseControls ({
3636
}
3737
}
3838

39+
function handleInputChange(e: React.ChangeEvent<HTMLInputElement>, poseId: number, field: keyof Pose) {
40+
let final = null;
41+
if (e.target.value !== "") {
42+
const parsed = parseFloat(e.target.value);
43+
if (!isNaN(parsed)) { final = Math.max(-70.75, Math.min(70.75, parsed)); }
44+
}
45+
updatePose(poseId, { [field]: final });
46+
}
47+
48+
function handleInputBlur(e: React.FocusEvent<HTMLInputElement>, poseId: number, field: keyof Pose, min: number, max: number) {
49+
if (e.target.value !== "") {
50+
const parsed = parseFloat(e.target.value);
51+
if (!isNaN(parsed)) {
52+
const final = Math.max(min, Math.min(max, parsed));
53+
e.target.value = final.toString();
54+
}
55+
}
56+
}
57+
3958
return (
4059
<div className="flex h-full flex-col">
4160
<Button className="flex mt-4 mx-4" onClick={addPose}>
@@ -106,17 +125,12 @@ export default function PoseControls ({
106125
id={`x-${pose.id}`}
107126
type="number"
108127
placeholder="X"
109-
min={-70.5}
110-
max={70.5}
111-
className="w-20 h-7 transition-colors focus-visible:border-red-500 focus-visible:ring-red-500 bg-zinc-900"
112-
value={pose.x}
113-
onChange={(e) => {
114-
const val = e.target.value;
115-
updatePose(pose.id, { x: val === "" ? 0 : Math.max(-70.5, Math.min(70.5, parseFloat(val))) });
116-
}}
117-
onClick={() => {
118-
if (pose.x === 0) updatePose(pose.id, { x: 0 });
119-
}}
128+
min={-70.75}
129+
max={70.75}
130+
className="w-20 transition-colors focus-visible:border-red-500 focus-visible:ring-red-500 bg-zinc-900"
131+
defaultValue={pose.x ?? 0}
132+
onChange={(e) => handleInputChange(e, pose.id, 'x')}
133+
onBlur={(e) => handleInputBlur(e, pose.id, 'x', -70.75, 70.75)}
120134
/>
121135
</Field>
122136

@@ -128,17 +142,12 @@ export default function PoseControls ({
128142
id={`y-${pose.id}`}
129143
type="number"
130144
placeholder="Y"
131-
min={-70.5}
132-
max={70.5}
133-
className="w-20 h-7 transition-colors focus-visible:border-red-500 focus-visible:ring-red-500 bg-zinc-900"
134-
value={pose.y}
135-
onClick={() => {
136-
if (pose.y === 0) updatePose(pose.id, { y: 0 });
137-
}}
138-
onChange={(e) => {
139-
const val = e.target.value;
140-
updatePose(pose.id, { y: val === "" ? 0 : Math.max(-70.5, Math.min(70.5, parseFloat(val))) });
141-
}}
145+
min={-70.75}
146+
max={70.75}
147+
className="w-20 transition-colors focus-visible:border-red-500 focus-visible:ring-red-500 bg-zinc-900"
148+
defaultValue={pose.y ?? 0}
149+
onChange={(e) => handleInputChange(e, pose.id, 'y')}
150+
onBlur={(e) => handleInputBlur(e, pose.id, 'y', -70.75, 70.75)}
142151
/>
143152
</Field>
144153
</div>
@@ -154,15 +163,10 @@ export default function PoseControls ({
154163
placeholder="Heading"
155164
min={0}
156165
max={360}
157-
className="w-20 h-7 transition-colors focus-visible:border-red-500 focus-visible:ring-red-500 bg-zinc-900"
158-
value={pose.heading}
159-
onClick={() => {
160-
if (pose.heading === 0) updatePose(pose.id, { heading: 0 });
161-
}}
162-
onChange={(e) => {
163-
const val = e.target.value;
164-
updatePose(pose.id, { heading: val === "" ? 0 : Number(val) % 360 });
165-
}}
166+
className="w-20 transition-colors focus-visible:border-red-500 focus-visible:ring-red-500 bg-zinc-900"
167+
defaultValue={pose.heading ?? 0}
168+
onChange={(e) => handleInputChange(e, pose.id, 'heading')}
169+
onBlur={(e) => handleInputBlur(e, pose.id, 'heading', 0, 360)}
166170
/>
167171
</Field>
168172

@@ -174,17 +178,11 @@ export default function PoseControls ({
174178
id={`radius-${pose.id}`}
175179
type="number"
176180
placeholder="Radius"
177-
disabled={!pose.arcPose}
178-
min={2}
179-
className="w-20 h-7 transition-all duration-300 ease-in-out focus-visible:border-red-500 focus-visible:ring-red-500 disabled:cursor-not-allowed disabled:opacity-40 bg-zinc-900"
180-
value={pose.radius}
181-
onClick={() => {
182-
if (pose.radius === 2) updatePose(pose.id, { radius: 0 });
183-
}}
184-
onChange={(e) => {
185-
const val = e.target.value;
186-
updatePose(pose.id, { radius: val === "" ? 0 : (Number(val) <= 2 && pose.arcPose) ? 2 : Number(val) });
187-
}}
181+
disabled={!pose.arcPose} // TODO: Add limits and clamping for radius (make sure to account for units)
182+
className="w-20 transition-all duration-300 ease-in-out focus-visible:border-red-500 focus-visible:ring-red-500 disabled:cursor-not-allowed disabled:opacity-40 bg-zinc-900"
183+
defaultValue={pose.radius ?? 0}
184+
onChange={(e) => handleInputChange(e, pose.id, 'radius')}
185+
onBlur={(e) => handleInputBlur(e, pose.id, 'radius', 0, 100)} // TODO: Proper limits
188186
/>
189187
</Field>
190188
</div>

types/paths.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
interface Callback {
22
id: number
3-
method: string
4-
value: number // TODO: Replace with just s and convert from distance to s when creating
3+
method: string | null
4+
value: number | null // TODO: Replace with just s and convert from distance to s when creating
55
distValue: boolean // true if the value is a distance, false if it is an s value
66
}
77

types/poses.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
interface Pose {
22
id: number
33
name: string
4-
x: number
5-
y: number
6-
heading: number
7-
radius: number
4+
x: number | null
5+
y: number | null
6+
heading: number | null
7+
radius: number | null
88
arcPose: boolean
99
local: boolean
1010
}

0 commit comments

Comments
 (0)