From 85be1887bdace47888219bd0dac30e5367ae98ed Mon Sep 17 00:00:00 2001 From: awesumdude42 Date: Tue, 30 Jun 2026 07:40:55 -0700 Subject: [PATCH 1/2] bspline class created, with a Vector interface! --- src/app/page.tsx | 3 +- src/components/path-overlay.tsx | 8 +- src/lib/bspline.tsx | 201 ++++++++++++++++++++++++++++++++ types/poses.ts | 7 ++ 4 files changed, 216 insertions(+), 3 deletions(-) create mode 100644 src/lib/bspline.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index 4ab7067..c5aa705 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -6,6 +6,7 @@ import { Paths, Poses } from "@/hooks/use-visualizer"; import PoseControls from "@/components/pose-controls"; import DrawPaths from "@/components/path-overlay"; + export default function Home() { const { poses, @@ -57,7 +58,7 @@ export default function Home() { id="field-canvas" /> - + diff --git a/src/components/path-overlay.tsx b/src/components/path-overlay.tsx index 092c7cc..449ff29 100644 --- a/src/components/path-overlay.tsx +++ b/src/components/path-overlay.tsx @@ -1,12 +1,16 @@ 'use client'; -import { useEffect, useRef } from "react"; +import { bspline } from "@/lib/bspline"; +import { useEffect, useRef, useState } from "react"; interface PathDrawProps { poses: Pose[]; + paths: Path[]; } -export default function DrawPaths({ poses }: PathDrawProps) { +export default function DrawPaths({ poses, paths}: PathDrawProps) { + + const canvasRef = useRef(null); useEffect(() => { diff --git a/src/lib/bspline.tsx b/src/lib/bspline.tsx new file mode 100644 index 0000000..eb850cf --- /dev/null +++ b/src/lib/bspline.tsx @@ -0,0 +1,201 @@ +import { useState } from "react"; + +export function bspline(poses:Pose[]){ + + + const CMatrix = [ + [-1.0 / 6.0, 3.0 / 6.0, -3.0 / 6.0, 1.0 / 6.0], + [3.0 / 6.0, -6.0 / 6.0, 3.0 / 6.0, 0.0], + [-3.0 / 6.0, 0.0, 3.0 / 6.0, 0.0], + [1.0 / 6.0, 4.0 / 6.0, 1.0 / 6.0, 0.0] + ] + + //first, make sure all of the poses have valid values(kinda copied from path-overlay) + + poses.forEach((pose) => { + if (pose.x === null || pose.y === null) return; + }); + + //then, make sure the list has a valid amount of poses + + if(poses.length<2) return; + + //convert all poses to vectors + + const [poseVector,setPoseVector] = useState([]); + poses.forEach((pose) => { + + const temp:Vector = { + x:pose.x ?? 0, // if pose.x is null, give a value of 0. Our filter above will automatically + // return if any of the values are null, so this will never happen, it just lets the Vector class have pure number types instead + // of number | null + y:pose.y ?? 0 + }; + + setPoseVector((prevVectors)=>{ + + return [...prevVectors, temp] + }) + + }); + + + //the first thing we need to do is to get the ghost points + + //the formula is P_0+(-1(P_1-P_0)) where P_0 is the axis point(that you reflect across, otherwise known as the endpoint) + // and P_1 is the point being reflected(otherwise known as the second, or second to last point) + + + + + const [ghostPoints,setGhostPoints] = useState([]);// make a list for the ghost points + + const neg1 = poses.length-1 + const neg2 = poses.length-2 + + //will give error cus the value "might be null" even though we filtered for it already, so just add an if statement + if((poseVector[0].x!=null && poseVector[1].x!=null && poseVector[neg1].x!=null && poseVector[neg2].x!=null + && poseVector[0].y!=null && poseVector[1].y!=null && poseVector[neg1].y!=null && poseVector[neg2].y!=null) + ){ + setGhostPoints([ + { + x:poseVector[0].x+((poseVector[1].x - poseVector[0].x) * -1), + y:poseVector[0].y+((poseVector[1].y - poseVector[0].y) * -1) + }, + { + x:poseVector[neg1].x+((poseVector[neg2].x - poseVector[neg1].x) * -1), + y:poseVector[neg1].y+((poseVector[neg2].y - poseVector[neg1].y) * -1) + + } + ]) + } + + //now that our ghost points are sorted out, we can make the array that will be put into the spline + const [splinePoints,setSplinePoints] = useState([]); + // loop twice more than the poseVector array because we have 2 ghost points + for(let i = 0; i{ + + return[...prevPoints,ghostPoints[0]] + + } + ) + } + //adds ghost points at the end + else if(i == poseVector.length+1){// because its 0 indexed, the last loop will be when i is 1 less than poseVector.length+2 + + setSplinePoints( + (prevPoints)=>{ + + return [...prevPoints,ghostPoints[1]] + + } + ) + } + else{ + + setSplinePoints( + (prevPoints)=>{ + + return [...prevPoints,poseVector[i-1]]//since arrays are 0-indexed, and when the value of i is 0, + // the ghost point is added, we need to subtract one to add all elements in the array + + } + ) + } + } + + + + //now, the control points are in order. Let's make the actual b spline evaluate. + + + const numSegments = splinePoints.length-3 + const [cx,setCx] = useState([]) + const [cy,setCy] = useState([]) + + + //create the b spline segments, 4 control points per segment, each contribute to the whole spline, keeping it 4th degree + //as joel's bspline class states in the pathing repo, "they are evaluated using a sliding 4-point window" + + for(let i = 0; i[...prevCx,tempx] + ) + const tempy:number[] = multiplyMatrices(CMatrix,yWindow) + setCy( + (prevCy)=>[...prevCy,tempy] + ) + + + + } + + + //evaluate function + // copied from joels thing + const evaluate = (t:number)=>{ + + if (t >= 1.0) t = 0.999999; + if (t < 0.0) t = 0.0; + + const continuousIndex = t * numSegments + const segment = Math.trunc(continuousIndex) //should do the same thing as type casting to int + const localT = continuousIndex - segment + + + const cX = cx[segment] + const cY = cy[segment] + + + const x = ((cX[0] * localT + cX[1]) * localT + cX[2]) * localT + cX[3] + const y = ((cY[0] * localT + cY[1]) * localT + cY[2]) * localT + cY[3] + + const returnValue:Vector = { + x:x, + y:y + } + return returnValue; + } + + return {evaluate} +} + + +//this function is made specifically for multiplying the Cmatrix and the windows +function multiplyMatrices(matrix:number[][], window:number[]){ + + + const [result,setResult] = useState([]); + + for (let i = 0; i < matrix.length; i++) { + let sum:number = 0.0; + for (let j = 0; j < matrix[0].length; j++) { + sum += matrix[i][j] * window[j]; + } + setResult((prevResults)=>{ + return[...prevResults,sum] + }) + } + return result; + + +} diff --git a/types/poses.ts b/types/poses.ts index 8a54b50..ea1b11b 100644 --- a/types/poses.ts +++ b/types/poses.ts @@ -7,4 +7,11 @@ interface Pose { radius: number | null arcPose: boolean local: boolean +} + +//this interface is for calculating the bspline, +// as it doesn't need the other things for poses which will just be plugged into the b spline +interface Vector{ + x:number //these two values are null to match the type of the poses, it will never actually be null + y:number } \ No newline at end of file From c30954cc65b680cbb3b616958c70e17bb3681153 Mon Sep 17 00:00:00 2001 From: awesumdude42 Date: Thu, 23 Jul 2026 12:11:46 -0700 Subject: [PATCH 2/2] the bspline is actuallly working lets actually go --- src/components/path-overlay.tsx | 54 ++++++++-- src/lib/bspline.tsx | 2 - src/lib/bsplineClass.tsx | 178 ++++++++++++++++++++++++++++++++ 3 files changed, 226 insertions(+), 8 deletions(-) create mode 100644 src/lib/bsplineClass.tsx diff --git a/src/components/path-overlay.tsx b/src/components/path-overlay.tsx index 449ff29..f336d77 100644 --- a/src/components/path-overlay.tsx +++ b/src/components/path-overlay.tsx @@ -1,18 +1,18 @@ 'use client'; -import { bspline } from "@/lib/bspline"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef } from "react"; +import { bsplineClass } from '@/lib/bsplineClass'; +import Konva from "konva"; interface PathDrawProps { poses: Pose[]; paths: Path[]; } -export default function DrawPaths({ poses, paths}: PathDrawProps) { - - +export default function DrawPaths({ poses, paths }: PathDrawProps) { const canvasRef = useRef(null); + useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; @@ -22,6 +22,7 @@ export default function DrawPaths({ poses, paths}: PathDrawProps) { canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; + const ctx = canvas.getContext("2d"); if (!ctx) return; @@ -33,6 +34,7 @@ export default function DrawPaths({ poses, paths}: PathDrawProps) { const REAL_WIDTH_INCHES = 141.5; const pixelsPerInch = rect.width / REAL_WIDTH_INCHES; + // 1. Draw your Pose circles poses.forEach((pose) => { if (pose.x === null || pose.y === null || pose.heading === null) return; @@ -44,7 +46,47 @@ export default function DrawPaths({ poses, paths}: PathDrawProps) { ctx.fillStyle = 'green'; ctx.fill(); }); - }, [poses]); + + + + + + + + paths.forEach((path)=>{ + const spline = new bsplineClass(path,poses); + + const points: Vector[] = []; + const numPoints = 50; + + for (let i = 0; i <= numPoints; i++) { + const t = i / numPoints; + const point = spline.evaluate(t); + points.push(point); + } + + //this was kinda ai-ed mb + if (points.length > 0) { + ctx.beginPath(); + ctx.strokeStyle = '#2563eb'; + ctx.lineWidth = 3; + + points.forEach((pt, index) => { + + const canvasX = centerX + (pt.x * pixelsPerInch); + const canvasY = centerY - (pt.y * pixelsPerInch); + + if (index === 0) { + ctx.moveTo(canvasX, canvasY); + } else { + ctx.lineTo(canvasX, canvasY); + } + }); + + ctx.stroke(); + } + }) + }, [poses, paths]); return ( { + + const currentPose = fullPoses.find((pose)=> pose.id === cPoint.poseId) + + if(currentPose){ + poses.push(currentPose) + } + }) + + + + + //this is where all of the conversions will happen + + + //first, make sure the list has a valid amount of poses + + if(poses.length < 2) return; + + //then, make sure all values are valid + + + if (poses.some(pose => pose.x == null || pose.y == null)) { + return; + } + + //convert all poses to vectors + + let poseVector:Vector[] = [] + poses.forEach((pose) => { + + const temp:Vector = { + x:pose.x ?? 0, // if pose.x is null, give a value of 0. Our filter above will automatically + // return if any of the values are null, so this will never happen, it just lets the Vector class have + // pure number types instead of number | null + y:pose.y ?? 0 + }; + + poseVector.push(temp) + }); + + //assign posevectors to our object + + + //the first thing we need to do is to get the ghost points + + //the formula is P_0+(-1(P_1-P_0)) where P_0 is the axis point(that you reflect across, otherwise known as the endpoint) + // and P_1 is the point being reflected(otherwise known as the second, or second to last point) + + + + + let ghostPoints:Vector[] = []// make a list for the ghost points + + const neg1 = poses.length-1 + const neg2 = poses.length-2 + + //will give error cus the value "might be null" even though we filtered for it already, so just add an if statement + if((poseVector[0].x!=null && poseVector[1].x!=null && poseVector[neg1].x!=null && poseVector[neg2].x!=null + && poseVector[0].y!=null && poseVector[1].y!=null && poseVector[neg1].y!=null && poseVector[neg2].y!=null) + ){ + ghostPoints = [ + { + x:poseVector[0].x+((poseVector[1].x - poseVector[0].x) * -1), + y:poseVector[0].y+((poseVector[1].y - poseVector[0].y) * -1) + }, + { + x:poseVector[neg1].x+((poseVector[neg2].x - poseVector[neg1].x) * -1), + y:poseVector[neg1].y+((poseVector[neg2].y - poseVector[neg1].y) * -1) + + } + ] + } + + + //combine ghost points and posevectors into splinePoints + + //now that our ghost points are sorted out, we can make the array that will be put into the spline + let splinePoints:Vector[] = []; + + splinePoints.push(ghostPoints[0]) + splinePoints.push(...poseVector) + splinePoints.push(ghostPoints[1]) + + + //now for the actual calculations for the bspline + + + const numSegments = splinePoints.length-3 + this.numSegments = numSegments + let cx:number[][] = [] + let cy:number[][] = [] + + //create the b spline segments, 4 control points per segment, each contribute to the whole spline, keeping it 4th degree + //as joel's bspline class states in the pathing repo, "they are evaluated using a sliding 4-point window" + + for(let i = 0; i= 1.0) t = 0.999999; + if (t < 0.0) t = 0.0; + + const continuousIndex = t * this.numSegments + const segment = Math.trunc(continuousIndex) //should do the same thing as type casting to int + const localT = continuousIndex - segment + + + const cX = this.cx[segment] + const cY = this.cy[segment] + + + const x = ((cX[0] * localT + cX[1]) * localT + cX[2]) * localT + cX[3] + const y = ((cY[0] * localT + cY[1]) * localT + cY[2]) * localT + cY[3] + + const returnValue:Vector = { + x:x, + y:y + } + return returnValue; + } + +} + +function multiplyMatrices(matrix:number[][], window:number[]){ + + + const result:number[] = []; + + for (let i = 0; i < matrix.length; i++) { + let sum:number = 0.0; + for (let j = 0; j < matrix[0].length; j++) { + sum += matrix[i][j] * window[j]; + } + result.push(sum) + } + return result; +} \ No newline at end of file