@@ -6,12 +6,11 @@ import { useState, useRef, useEffect } from "react"
66import { useTheme } from "next-themes" ;
77import { Toaster , toast } from "sonner" ;
88import { 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
1615const sample_code = `
1716from pybricks.pupdevices import Motor
@@ -24,12 +23,12 @@ const game_board_width = 2362.2 // mm
2423const game_board_height = 1143.0 // mm
2524
2625class 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