Skip to content

Commit 40324d3

Browse files
committed
Splan functionality is complete
1 parent 2ce326c commit 40324d3

1 file changed

Lines changed: 164 additions & 13 deletions

File tree

src/pages/index.tsx

Lines changed: 164 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@ import { useState, useRef, useEffect } from "react"
66
import { useTheme } from "next-themes";
77
import { Toaster, toast } from "sonner";
88
import { connectToSpike, sendCodeToSpike, readResponseFromSpike } from "@/components/pybricks/tools";
9-
import {
10-
Accordion,
11-
AccordionContent,
12-
AccordionItem,
13-
AccordionTrigger,
14-
} from "@/components/ui/accordion"
9+
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
10+
import { Dialog, DialogTrigger, DialogTitle, DialogDescription, DialogHeader, DialogFooter, DialogContent } from "@/components/ui/dialog";
11+
import { Input } from "@/components/ui/input";
12+
import { Label } from "@/components/ui/label";
13+
import { Select, SelectItem, SelectTrigger, SelectValue, SelectContent, SelectGroup } from "@/components/ui/select";
1514

1615
const sample_code = `
1716
from pybricks.pupdevices import Motor
@@ -24,12 +23,12 @@ const game_board_width = 2362.2 // mm
2423
const game_board_height = 1143.0 // mm
2524

2625
class DriveBase {
27-
left_motor: "A" | "B" | "C" | "D";
28-
right_motor: "A" | "B" | "C" | "D";
26+
left_motor: "A" | "B" | "C" | "D" | "E" | "F";
27+
right_motor: "A" | "B" | "C" | "D" | "E" | "F";
2928
wheel_diameter: number;
3029
axle_track: number;
3130

32-
constructor(left_motor: "A" | "B" | "C" | "D", right_motor: "A" | "B" | "C" | "D", wheel_diameter: number, axle_track: number) {
31+
constructor(left_motor: "A" | "B" | "C" | "D" | "E" | "F", right_motor: "A" | "B" | "C" | "D" | "E" | "F", wheel_diameter: number, axle_track: number) {
3332
if (left_motor === right_motor) {
3433
throw new Error("Left and right motors must be different");
3534
}
@@ -144,6 +143,8 @@ export default function App() {
144143
const [run_index, SetRunIndex] = useState(0)
145144
const img_size_ref = useRef(null)
146145
const [img_dimensions, SetImageDimensions] = useState({ width: 0, height: 0 });
146+
const [run_creator_open, SetRunCreatorOpen] = useState(false)
147+
const [splan_creator_open, SetSplanCreatorOpen] = useState(false)
147148

148149
useEffect(() => {
149150
if (img_size_ref.current) {
@@ -185,7 +186,143 @@ export default function App() {
185186
link.click();
186187
URL.revokeObjectURL(url);
187188
}
188-
189+
190+
function HandleCreateRun() {
191+
if (!pysplan_handler) { toast.error("No Splan to add run to", {duration: 5000}); return; }
192+
SetRunCreatorOpen(true);
193+
}
194+
195+
const CreateRun = (name: string) => {
196+
if (!pysplan_handler) { toast.error("No Splan to add run to", {duration: 5000}); return; }
197+
const new_run = new Run(name, [], []);
198+
SetPySplanHandler({ ...pysplan_handler, runs: [...pysplan_handler.runs, new_run] });
199+
SetRunCreatorOpen(false);
200+
}
201+
202+
const CreateRunDialog = () => {
203+
const [new_run_name, SetNewRunName] = useState<string>("New Run")
204+
return (
205+
<Dialog open={run_creator_open} onOpenChange={SetRunCreatorOpen}>
206+
<DialogTrigger asChild></DialogTrigger>
207+
<DialogContent>
208+
<DialogHeader>
209+
<DialogTitle>Create Run</DialogTitle>
210+
<DialogDescription>
211+
Enter a name for the new run.
212+
</DialogDescription>
213+
</DialogHeader>
214+
<Input onChange={(e) => SetNewRunName(e.target.value)} value={new_run_name} />
215+
<DialogFooter>
216+
<Button onClick={() => CreateRun(new_run_name)} className="w-full">Create</Button>
217+
</DialogFooter>
218+
</DialogContent>
219+
</Dialog>
220+
)
221+
}
222+
223+
function HandleCreateSplan() {
224+
SetSplanCreatorOpen(true);
225+
}
226+
227+
const CreateSplan = (name: string, left_motor: string, right_motor: string, wheel_diameter: number, axle_track: number) => {
228+
if (name === "") { toast.error("Splan name cannot be empty", {duration: 5000}); return; }
229+
if (left_motor === "None" || right_motor === "None") { toast.error("Left and right motors cannot be empty", {duration: 5000}); return; }
230+
if (left_motor === right_motor) { toast.error("Left and right motors must be different", {duration: 5000}); return; }
231+
if (wheel_diameter <= 0 || axle_track <= 0) { toast.error("Wheel diameter and axle track must be greater than 0", {duration: 5000}); return; }
232+
233+
const splan_data = {"name": name, "drive_base": {"left_motor": left_motor, "right_motor": right_motor, "wheel_diameter": wheel_diameter, "axle_track": axle_track}, "runs": []}
234+
const new_splan = new SplanContent(splan_data);
235+
SetPySplanHandler(new_splan);
236+
SetSplanCreatorOpen(false);
237+
}
238+
239+
const CreateSplanDialog = () => {
240+
const [new_splan_name, SetNewSplanName] = useState<string>("New Splan")
241+
const [left_motor, SetLeftMotor] = useState<"A" | "B" | "C" | "D" | "E" | "F" | "None">("None")
242+
const [right_motor, SetRightMotor] = useState<"A" | "B" | "C" | "D" | "E" | "F" | "None">("None")
243+
const [wheel_diameter, SetWheelDiameter] = useState<number>(0)
244+
const [axle_track, SetAxleTrack] = useState<number>(0)
245+
246+
return (
247+
<Dialog open={splan_creator_open} onOpenChange={SetSplanCreatorOpen}>
248+
<DialogTrigger asChild></DialogTrigger>
249+
<DialogContent>
250+
<DialogHeader>
251+
<DialogTitle>Create Splan</DialogTitle>
252+
<DialogDescription>
253+
Enter the following data to create a new Splan.
254+
</DialogDescription>
255+
</DialogHeader>
256+
<Label>Splan Name</Label>
257+
<Input onChange={(e) => SetNewSplanName(e.target.value)} value={new_splan_name} />
258+
<Separator />
259+
<div className="grid grid-cols-2 gap-4">
260+
<div className="flex flex-col gap-2">
261+
<Label>Wheel Diameter</Label>
262+
<Input type="number" step="any" onChange={(e) => { const value = e.target.value;
263+
if (value === "") {
264+
SetWheelDiameter(0);
265+
} else {
266+
const num = parseFloat(value);
267+
if (!isNaN(num)) SetWheelDiameter(num);
268+
}
269+
}} value={wheel_diameter} />
270+
</div>
271+
<div className="flex flex-col gap-2">
272+
<Label>Axle Track</Label>
273+
<Input type="number" step="any" onChange={(e) => { const value = e.target.value;
274+
if (value === "") {
275+
SetAxleTrack(0);
276+
} else {
277+
const num = parseFloat(value);
278+
if (!isNaN(num)) SetAxleTrack(num);
279+
}
280+
}} value={axle_track} />
281+
</div>
282+
</div>
283+
<Separator />
284+
<div className="grid grid-cols-2 gap-4">
285+
<div className="flex flex-col gap-2">
286+
<Label>Left Motor</Label>
287+
<Select onValueChange={(e) => SetLeftMotor(e as "A" | "B" | "C" | "D" | "E" | "F")}>
288+
<SelectTrigger>
289+
<SelectValue placeholder="Select a motor" />
290+
</SelectTrigger>
291+
<SelectContent>
292+
<SelectItem value="A">A</SelectItem>
293+
<SelectItem value="B">B</SelectItem>
294+
<SelectItem value="C">C</SelectItem>
295+
<SelectItem value="D">D</SelectItem>
296+
<SelectItem value="E">E</SelectItem>
297+
<SelectItem value="F">F</SelectItem>
298+
</SelectContent>
299+
</Select>
300+
</div>
301+
<div className="flex flex-col gap-2">
302+
<Label>Right Motor</Label>
303+
<Select onValueChange={(e) => SetRightMotor(e as "A" | "B" | "C" | "D" | "E" | "F")}>
304+
<SelectTrigger>
305+
<SelectValue placeholder="Select a motor" />
306+
</SelectTrigger>
307+
<SelectContent>
308+
<SelectItem value="A">A</SelectItem>
309+
<SelectItem value="B">B</SelectItem>
310+
<SelectItem value="C">C</SelectItem>
311+
<SelectItem value="D">D</SelectItem>
312+
<SelectItem value="E">E</SelectItem>
313+
<SelectItem value="F">F</SelectItem>
314+
</SelectContent>
315+
</Select>
316+
</div>
317+
</div>
318+
<DialogFooter>
319+
<Button onClick={() => CreateSplan(new_splan_name, left_motor, right_motor, wheel_diameter, axle_track)} className="w-full">Create</Button>
320+
</DialogFooter>
321+
</DialogContent>
322+
</Dialog>
323+
)
324+
}
325+
189326
const GenerateCode = async () => {
190327
const github_url = "https://raw.githubusercontent.com/PySplanner/PySplanner/refs/heads/main/pysplanner.py"
191328
const response = await fetch(github_url);
@@ -210,7 +347,7 @@ export default function App() {
210347
SetPySplanHandler({ ...pysplan_handler, runs: [...pysplan_handler.runs.slice(0, run_index), new_run, ...pysplan_handler.runs.slice(run_index + 1)] });
211348
};
212349

213-
const flat_points = pysplan_handler?.runs[run_index].points.flatMap(p => [p.x, p.y])
350+
const flat_points = pysplan_handler?.runs[run_index]?.points.flatMap(p => [p.x, p.y])
214351
const spline_points = GetCurvePoints(flat_points ?? []);
215352

216353
const GetSpikeServer = async () => {
@@ -259,7 +396,7 @@ export default function App() {
259396
<AccordionTrigger>Splans</AccordionTrigger>
260397
<AccordionContent>
261398
<div className="flex flex-col gap-2">
262-
<Button variant="secondary" className="w-full">Create Splan</Button>
399+
<Button variant="secondary" className="w-full" onClick={ () => HandleCreateSplan() }>Create Splan</Button>
263400
<Button variant="secondary" className="w-full" onClick={ () => HandleLoadSplan() }>Load Splan</Button>
264401
<Button variant="secondary" className="w-full" onClick={ () => HandleSaveSplan() }>Save Splan</Button>
265402
</div>
@@ -273,9 +410,21 @@ export default function App() {
273410
<div className="flex flex-col flex-grow overflow-y-auto">
274411
{pysplan_handler ? (
275412
<div className="flex flex-col gap-2 w-[calc(100%-30px)] ml-[15px]">
413+
<Button variant="secondary" className="w-full" onClick={ () => HandleCreateRun() }>Create Run</Button>
414+
<Separator orientation="horizontal" className="bg-zinc-600 w-[calc(100%-30px)] ml-[15px]" />
276415
{pysplan_handler.runs.map((run, idx) => (
277416
<Button key={idx} variant={run_index === idx ? "secondary" : "outline"} className="w-full" onClick={() => SetRunIndex(idx)}>{run.name}</Button>
278417
))}
418+
{pysplan_handler.runs.length === 0 && (
419+
<div>
420+
<p className="text-center mt-4 w-[calc(100%-30px)] ml-[15px] font-bold text-lg">
421+
No runs found
422+
</p>
423+
<p className="text-center mt-1 w-[calc(100%-30px)] ml-[15px] text-sm text-zinc-500">
424+
Create a run in this Splan with the button above
425+
</p>
426+
</div>
427+
)}
279428
</div>
280429
) : (
281430
<div>
@@ -295,7 +444,7 @@ export default function App() {
295444
const Home = () => {
296445
return (
297446
<div className="flex items-center justify-center w-full h-full">
298-
{pysplan_handler ? (
447+
{pysplan_handler && pysplan_handler.runs.length > 0 ? (
299448
<div className="relative flex items-center justify-center w-full h-full border rounded-lg ml-4">
300449
<div className="relative">
301450
<img src={mat_img} className="w-auto h-auto max-h-[85vh] max-w-[85vw] object-contain" ref={img_size_ref}/>
@@ -337,6 +486,8 @@ export default function App() {
337486
<Toaster richColors position="bottom-right" theme="dark" />
338487
<Sidebar />
339488
{settings_active ? <Settings /> : <Home />}
489+
{run_creator_open && <CreateRunDialog />}
490+
{splan_creator_open && <CreateSplanDialog />}
340491
</div>
341492
);
342493
}

0 commit comments

Comments
 (0)