11/// <reference types="@types/web-bluetooth" />
22import { Separator } from "@/components/ui/separator" ;
33import { Card } from "@/components/ui/card" ;
4- import { Button } from "@/components/ui/button"
5- import { useState } from "react"
4+ import { Button } from "@/components/ui/button" ;
5+ import { useState , useEffect } from "react"
66import { useTheme } from "next-themes" ;
7- import PointPlotter from "@/components/spline_plotter" ;
87import { Toaster , toast } from "sonner" ;
98import { connectToSpike , sendCodeToSpike , readResponseFromSpike } from "@/components/pybricks/tools" ;
109import {
@@ -20,6 +19,7 @@ from pybricks.parameters import Port
2019motor = Motor(Port.A)
2120motor.run_time(100, 2000)
2221`
22+ const motorOptions = [ "A" , "B" , "C" , "D" ]
2323
2424class DriveBase {
2525 left_motor : "A" | "B" | "C" | "D" ;
@@ -40,10 +40,12 @@ class DriveBase {
4040}
4141
4242class Action {
43+ point : Point ;
4344 function : string ;
4445 args : any [ ] ;
45-
46- constructor ( function_name : string , args : any [ ] = [ ] ) {
46+
47+ constructor ( point : Point , function_name : string , args : any [ ] = [ ] ) {
48+ this . point = point ;
4749 this . function = function_name ;
4850 this . args = args ;
4951 }
@@ -52,26 +54,34 @@ class Action {
5254class Point {
5355 x : number ;
5456 y : number ;
55- actions : Action [ ] ;
56- actions_are_blocking : boolean
5757
58- constructor ( x : number , y : number , actions : Action [ ] = [ ] , actions_are_blocking : boolean = false ) {
58+ constructor ( x : number , y : number ) {
5959 this . x = x ;
6060 this . y = y ;
61+ }
62+ }
63+
64+ class Run {
65+ name : string ;
66+ points : Point [ ] ;
67+ actions : Action [ ] ;
68+
69+ constructor ( name : string , points : Point [ ] , actions : Action [ ] = [ ] ) {
70+ this . name = name ;
71+ this . points = points ;
6172 this . actions = actions ;
62- this . actions_are_blocking = actions_are_blocking ;
6373 }
6474}
6575
66- class PySplanContent {
76+ class SplanContent {
6777 name : string ;
6878 drive_base : DriveBase ;
69- runs : Point [ ] [ ] ;
79+ runs : Run [ ] ;
7080
7181 constructor ( data : any ) {
7282 this . name = data . name ;
7383 this . drive_base = new DriveBase ( data . drive_base . left_motor , data . drive_base . right_motor , data . drive_base . wheel_diameter , data . drive_base . axle_track ) ;
74- this . runs = data . runs ;
84+ this . runs = data . runs . map ( ( run : any ) => new Run ( run . name , run . points , run . actions ) ) ;
7585 }
7686
7787 save_file ( ) {
@@ -93,22 +103,138 @@ class PySplanContent {
93103 }
94104}
95105
106+ // Custom PySplanner B-Spline algorithm
107+ const getCurvePoints = ( pts : number [ ] , tension = 0.5 , isClosed = false , numOfSegments = 16 ) => {
108+ let _pts = pts . slice ( 0 ) ; // Copy the array of points
109+ let res = [ ] , x , y , t1x , t2x , t1y , t2y , c1 , c2 , c3 , c4 , st , t ;
110+
111+ // Handle closed vs open curves by adding control points at the ends
112+ if ( isClosed ) {
113+ _pts . unshift ( pts [ pts . length - 2 ] , pts [ pts . length - 1 ] ) ; // Repeat last point at the start
114+ _pts . push ( pts [ 0 ] , pts [ 1 ] ) ; // Repeat first point at the end
115+ } else {
116+ // Add mirrored control points at start and end to prevent sharp edges
117+ _pts . unshift ( 2 * pts [ 0 ] - pts [ 2 ] , 2 * pts [ 1 ] - pts [ 3 ] ) ;
118+ _pts . push ( 2 * pts [ pts . length - 2 ] - pts [ pts . length - 4 ] , 2 * pts [ pts . length - 1 ] - pts [ pts . length - 3 ] ) ;
119+ }
120+
121+ // Loop through each segment of the points
122+ for ( let i = 2 ; i < ( _pts . length - 4 ) ; i += 2 ) {
123+ for ( t = 0 ; t <= numOfSegments ; t ++ ) { // Interpolate points for each segment
124+ // Calculate tangents at the start and end of the segment
125+ t1x = ( _pts [ i + 2 ] - _pts [ i - 2 ] ) * tension ;
126+ t2x = ( _pts [ i + 4 ] - _pts [ i ] ) * tension ;
127+ t1y = ( _pts [ i + 3 ] - _pts [ i - 1 ] ) * tension ;
128+ t2y = ( _pts [ i + 5 ] - _pts [ i + 1 ] ) * tension ;
129+ st = t / numOfSegments ; // Parameter for interpolation between 0 and 1
130+
131+ // Catmull-Rom spline basis functions
132+ c1 = 2 * st ** 3 - 3 * st ** 2 + 1 ;
133+ c2 = - 2 * st ** 3 + 3 * st ** 2 ;
134+ c3 = st ** 3 - 2 * st ** 2 + st ;
135+ c4 = st ** 3 - st ** 2 ;
136+
137+ // Calculate the x and y coordinates using the basis functions
138+ x = c1 * _pts [ i ] + c2 * _pts [ i + 2 ] + c3 * t1x + c4 * t2x ;
139+ y = c1 * _pts [ i + 1 ] + c2 * _pts [ i + 3 ] + c3 * t1y + c4 * t2y ;
140+
141+ res . push ( { x, y } ) ; // Store the interpolated point
142+ }
143+ }
144+ return res ; // Return the array of curve points
145+ } ;
146+
96147export default function App ( ) {
97148 const { theme } = useTheme ( )
98149 const mat_img = `./game_board_${ theme ? theme : "dark" } .png`
99150 const [ settings_active , SetSettingsActive ] = useState ( false )
100151 const [ spike_server , SetSpikeServer ] = useState < BluetoothRemoteGATTServer | null > ( null )
152+ const [ pysplan_handloer , SetPySplanHandler ] = useState < SplanContent | null > ( null )
153+ const [ points , setPoints ] = useState < Point [ ] > ( [ ] ) ;
154+ const [ history , setHistory ] = useState < Point [ ] [ ] > ( [ [ ] ] ) ;
155+ const [ currentIndex , setCurrentIndex ] = useState ( 0 ) ;
156+
157+ const HandleLoadSplan = ( ) => {
158+ const input = document . createElement ( 'input' ) ;
159+ input . type = 'file' ;
160+ input . accept = '.pysplan' ;
161+ input . onchange = async ( ) => {
162+ const file = input . files ?. [ 0 ] ;
163+ if ( ! file ) return ;
164+ const reader = new FileReader ( ) ;
165+ reader . onload = async ( ) => {
166+ const data = JSON . parse ( reader . result as string ) ;
167+ try { const splan = new SplanContent ( data ) ; SetPySplanHandler ( splan ) ; } catch ( e ) { toast . error ( "Failed to load Splan file, check the console for more info" , { duration : 5000 } ) ; console . error ( e ) ; }
168+ }
169+ reader . readAsText ( file ) ;
170+ }
171+ input . click ( ) ;
172+ }
173+
174+ const HandleSaveSplan = ( ) => {
175+ if ( ! pysplan_handloer ) { toast . error ( "No Splan to save" , { duration : 5000 } ) ; return ; }
176+ const data = JSON . stringify ( pysplan_handloer ) ;
177+ const blob = new Blob ( [ data ] , { type : "application/json" } ) ;
178+ const url = URL . createObjectURL ( blob ) ;
179+ const link = document . createElement ( "a" ) ;
180+ link . href = url ;
181+ link . download = `${ pysplan_handloer . name } .pysplan` ;
182+ link . click ( ) ;
183+ URL . revokeObjectURL ( url ) ;
184+ }
185+
186+ const GenerateCode = async ( ) => {
187+ const github_url = "https://raw.githubusercontent.com/PySplanner/PySplanner/refs/heads/main/pysplanner.py"
188+ const response = await fetch ( github_url ) ;
189+ const code = await response . text ( ) ;
190+ // TODO: Add the stuff to the code
191+ }
192+
193+ const addPoint = ( e : React . MouseEvent ) => {
194+ const rect = e . currentTarget . getBoundingClientRect ( ) ;
195+ const newPoint = { x : e . clientX - rect . left , y : e . clientY - rect . top } ;
196+ const newPoints = [ ...points , newPoint ] ;
197+
198+ if ( newPoints . length === 25 ) {
199+ toast . warning ( "WARNING: Exceeding 25 points may cause lagging/crashing of the Spike or EV3 robot." , { duration : 10000 } ) ;
200+ } else if ( newPoints . length === 50 ) {
201+ toast . error ( "You have reached the maximum number of points, which is 50." , { duration : 10000 } ) ;
202+ return
203+ }
204+
205+ setPoints ( newPoints ) ;
206+ setHistory ( history . slice ( 0 , currentIndex + 1 ) . concat ( [ newPoints ] ) ) ;
207+ setCurrentIndex ( currentIndex + 1 ) ;
208+ } ;
209+
210+ const handleKeyDown = ( e : KeyboardEvent ) => {
211+ if ( e . ctrlKey && e . key === 'z' && currentIndex > 0 ) {
212+ setCurrentIndex ( currentIndex - 1 ) ;
213+ setPoints ( history [ currentIndex - 1 ] ) ;
214+ } else if ( e . ctrlKey && e . key === 'y' && currentIndex < history . length - 1 ) {
215+ setCurrentIndex ( currentIndex + 1 ) ;
216+ setPoints ( history [ currentIndex + 1 ] ) ;
217+ }
218+ } ;
219+
220+ useEffect ( ( ) => {
221+ window . addEventListener ( 'keydown' , handleKeyDown ) ;
222+ return ( ) => window . removeEventListener ( 'keydown' , handleKeyDown ) ;
223+ } , [ points , history , currentIndex ] ) ;
224+
225+ const flatPoints = points . flatMap ( p => [ p . x , p . y ] ) ;
226+ const splinePoints = getCurvePoints ( flatPoints ) ;
101227
102228 const GetSpikeServer = async ( ) => {
103229 toast . promise (
104- async ( ) => { SetSpikeServer ( await connectToSpike ( ) ) } ,
105- {
106- loading : "Connecting to Spike..." ,
107- success : "Connected to Spike!" ,
108- error : "Failed to connect to Spike."
109- }
230+ async ( ) => { SetSpikeServer ( await connectToSpike ( ) ) } ,
231+ {
232+ loading : "Connecting to Spike..." ,
233+ success : "Connected to Spike!" ,
234+ error : "Failed to connect to Spike."
235+ }
110236 )
111- } ;
237+ } ;
112238
113239 const Sidebar = ( ) => {
114240 return (
@@ -141,13 +267,13 @@ export default function App() {
141267 EV3 is not yet supported.
142268 </ AccordionContent >
143269 </ AccordionItem >
144- < AccordionItem value = "Paths " >
145- < AccordionTrigger > Paths </ AccordionTrigger >
270+ < AccordionItem value = "Splans " >
271+ < AccordionTrigger > Splans </ AccordionTrigger >
146272 < AccordionContent >
147273 < div className = "flex flex-col gap-2" >
148- < Button variant = "secondary" className = "w-full" > Create Path </ Button >
149- < Button variant = "secondary" className = "w-full" > Load Paths </ Button >
150- < Button variant = "secondary" className = "w-full" > Save Paths </ Button >
274+ < Button variant = "secondary" className = "w-full" > Create Splan </ Button >
275+ < Button variant = "secondary" className = "w-full" onClick = { ( ) => HandleLoadSplan ( ) } > Load Splan </ Button >
276+ < Button variant = "secondary" className = "w-full" onClick = { ( ) => HandleSaveSplan ( ) } > Save Splan </ Button >
151277 </ div >
152278 </ AccordionContent >
153279 </ AccordionItem >
@@ -166,20 +292,36 @@ export default function App() {
166292
167293 const Home = ( ) => {
168294 return (
169- < div className = "relative flex items-center justify-center w-full h-full border rounded-lg ml-4" >
170- < div className = "relative" >
171- < img src = { mat_img } className = "w-auto h-auto max-h-[85vh] max-w-[85vw] object-contain" />
172- < div className = "absolute inset-0 flex items-center justify-center" >
173- < PointPlotter />
295+ < div className = "flex items-center justify-center w-full h-full" >
296+ { pysplan_handloer ? (
297+ < div className = "relative flex items-center justify-center w-full h-full border rounded-lg ml-4" >
298+ < div className = "relative" >
299+ < img src = { mat_img } className = "w-auto h-auto max-h-[85vh] max-w-[85vw] object-contain" />
300+ < div className = "absolute inset-0 flex items-center justify-center" >
301+ < div className = "w-full h-full relative" onClick = { addPoint } >
302+ { points . map ( ( p , idx ) => (
303+ < div key = { idx } className = "absolute bg-green-500 w-2 h-2 rounded-full" style = { { left : `${ p . x } px` , top : `${ p . y } px` } } />
304+ ) ) }
305+ { splinePoints . map ( ( p , idx ) => (
306+ < div key = { idx } className = "absolute bg-green-400 w-1 h-1 rounded-full" style = { { left : `${ p . x } px` , top : `${ p . y } px` } } />
307+ ) ) }
308+ </ div >
309+ </ div >
310+ </ div >
174311 </ div >
175- </ div >
312+ ) : (
313+ < div className = "flex flex-col items-center justify-center w-full h-full border rounded-lg ml-4" >
314+ < p className = "text-center font-bold text-xl" > Create a new Splan or load an existing one on the sidebar under Splans</ p >
315+ < p className = "text-center mt-2 text-md text-zinc-500" > This path planner is currently in development, please check back later</ p >
316+ </ div >
317+ ) }
176318 </ div >
177319 ) ;
178320 } ;
179321
180322 const Settings = ( ) => {
181323 return (
182- < div className = "relative flex w-full border rounded-lg ml-4 p-8" >
324+ < div className = "relative flex w-full border rounded-lg ml-4 p-8" >
183325 < h1 > Settings</ h1 >
184326 < Button variant = { "secondary" } className = "absolute top-2 right-2 m-2" onClick = { ( ) => SetSettingsActive ( false ) } >
185327 ✕
0 commit comments