Skip to content

Commit 2ce326c

Browse files
committed
Dynamic spline desnity and image dimensions for mm conversion
1 parent 697fbbc commit 2ce326c

1 file changed

Lines changed: 43 additions & 27 deletions

File tree

src/pages/index.tsx

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { Separator } from "@/components/ui/separator";
33
import { Card } from "@/components/ui/card";
44
import { Button } from "@/components/ui/button";
5-
import { useState, useEffect } from "react"
5+
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";
@@ -19,7 +19,9 @@ from pybricks.parameters import Port
1919
motor = Motor(Port.A)
2020
motor.run_time(100, 2000)
2121
`
22-
const motorOptions = ["A", "B", "C", "D"]
22+
const motor_options = ["A", "B", "C", "D"]
23+
const game_board_width = 2362.2 // mm
24+
const game_board_height = 1143.0 // mm
2325

2426
class DriveBase {
2527
left_motor: "A" | "B" | "C" | "D";
@@ -70,10 +72,10 @@ class Run {
7072
points: Point[];
7173
actions: Action[];
7274

73-
constructor(name: string, points: [number, number][], actions: { point: [number, number], function: string, args: any[] }[] = []) {
75+
constructor(name: string, points: Point[], actions: Action[] = []) {
7476
this.name = name;
75-
this.points = points.map((point) => new Point(point[0], point[1]));
76-
this.actions = actions.map((action) => new Action(new Point(action.point[0], action.point[1]), action.function, action.args));
77+
this.points = points;
78+
this.actions = actions
7779
}
7880
}
7981

@@ -85,49 +87,52 @@ class SplanContent {
8587
constructor(data: any) {
8688
this.name = data.name;
8789
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);
88-
this.runs = data.runs.map((run: any) => new Run(run.name, run.points, run.actions));
90+
this.runs = data.runs.map((run: any) => new Run(run.name, run.points.map((point: any) => new Point(point[0], point[1])), run.actions.map((action: any) => new Action(new Point(action.point[0], action.point[1]), action.function, action.args))));
8991
}
9092
}
9193

9294
// Custom PySplanner B-Spline algorithm
93-
const GetCurvePoints = (pts: number[], tension = 0.5, isClosed = false, numOfSegments = 16) => {
95+
const GetCurvePoints = (pts: number[], tension = 0.5, isClosed = false, segmentFactor = 0.035) => {
9496
let _pts = pts.slice(0); // Copy the array of points
9597
let res = [], x, y, t1x, t2x, t1y, t2y, c1, c2, c3, c4, st, t;
9698

99+
// Helper function to compute Euclidean distance
100+
const getDistance = (x1: number, y1: number, x2: number, y2: number) =>
101+
Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
102+
97103
// Handle closed vs open curves by adding control points at the ends
98104
if (isClosed) {
99-
_pts.unshift(pts[pts.length - 2], pts[pts.length - 1]); // Repeat last point at the start
100-
_pts.push(pts[0], pts[1]); // Repeat first point at the end
105+
_pts.unshift(pts[pts.length - 2], pts[pts.length - 1]);
106+
_pts.push(pts[0], pts[1]);
101107
} else {
102-
// Add mirrored control points at start and end to prevent sharp edges
103-
_pts.unshift(2 * pts[0] - pts[2], 2 * pts[1] - pts[3]);
104-
_pts.push(2 * pts[pts.length - 2] - pts[pts.length - 4], 2 * pts[pts.length - 1] - pts[pts.length - 3]);
108+
_pts.unshift(2 * pts[0] - pts[2], 2 * pts[1] - pts[3]);
109+
_pts.push(2 * pts[pts.length - 2] - pts[pts.length - 4], 2 * pts[pts.length - 1] - pts[pts.length - 3]);
105110
}
106111

107112
// Loop through each segment of the points
108113
for (let i = 2; i < (_pts.length - 4); i += 2) {
109-
for (t = 0; t <= numOfSegments; t++) { // Interpolate points for each segment
110-
// Calculate tangents at the start and end of the segment
114+
const segmentLength = getDistance(_pts[i], _pts[i + 1], _pts[i + 2], _pts[i + 3]);
115+
const numOfSegments = Math.max(2, Math.floor(segmentLength * segmentFactor)); // At least 2 segments per section
116+
117+
for (t = 0; t <= numOfSegments; t++) {
111118
t1x = (_pts[i + 2] - _pts[i - 2]) * tension;
112119
t2x = (_pts[i + 4] - _pts[i]) * tension;
113120
t1y = (_pts[i + 3] - _pts[i - 1]) * tension;
114121
t2y = (_pts[i + 5] - _pts[i + 1]) * tension;
115-
st = t / numOfSegments; // Parameter for interpolation between 0 and 1
116-
117-
// Catmull-Rom spline basis functions
122+
st = t / numOfSegments;
123+
118124
c1 = 2 * st ** 3 - 3 * st ** 2 + 1;
119125
c2 = -2 * st ** 3 + 3 * st ** 2;
120126
c3 = st ** 3 - 2 * st ** 2 + st;
121127
c4 = st ** 3 - st ** 2;
122128

123-
// Calculate the x and y coordinates using the basis functions
124129
x = c1 * _pts[i] + c2 * _pts[i + 2] + c3 * t1x + c4 * t2x;
125130
y = c1 * _pts[i + 1] + c2 * _pts[i + 3] + c3 * t1y + c4 * t2y;
126-
127-
res.push({ x, y }); // Store the interpolated point
131+
132+
res.push({ x, y });
128133
}
129134
}
130-
return res; // Return the array of curve points
135+
return res;
131136
};
132137

133138
export default function App() {
@@ -137,6 +142,17 @@ export default function App() {
137142
const [spike_server, SetSpikeServer] = useState<BluetoothRemoteGATTServer | null>(null)
138143
const [pysplan_handler, SetPySplanHandler] = useState<SplanContent | null>(null)
139144
const [run_index, SetRunIndex] = useState(0)
145+
const img_size_ref = useRef(null)
146+
const [img_dimensions, SetImageDimensions] = useState({ width: 0, height: 0 });
147+
148+
useEffect(() => {
149+
if (img_size_ref.current) {
150+
// @ts-ignore | Fix getBoundingClient due to initial 'null' value
151+
const { width, height } = img_size_ref.current.getBoundingClientRect();
152+
SetImageDimensions({ width, height });
153+
console.log(img_dimensions.width, img_dimensions.height);
154+
}
155+
}, [pysplan_handler]);
140156

141157
const HandleLoadSplan = () => {
142158
const input = document.createElement('input');
@@ -180,7 +196,7 @@ export default function App() {
180196
const AddPoint = (e: React.MouseEvent) => {
181197
if (!pysplan_handler) { toast.error("No run selected", {duration: 5000}); return; }
182198
const rect = e.currentTarget.getBoundingClientRect();
183-
const new_point = new Point(e.clientX - rect.left, e.clientY - rect.top);
199+
const new_point = new Point(e.clientX - rect.left, rect.height - (e.clientY - rect.top));
184200
const new_points = [...pysplan_handler.runs[run_index].points, new_point];
185201

186202
if (new_points.length === 25) {
@@ -190,8 +206,8 @@ export default function App() {
190206
return
191207
}
192208

193-
pysplan_handler.runs[run_index].points = new_points;
194-
SetPySplanHandler(pysplan_handler);
209+
const new_run = new Run(pysplan_handler.runs[run_index].name, new_points, pysplan_handler.runs[run_index].actions);
210+
SetPySplanHandler({ ...pysplan_handler, runs: [...pysplan_handler.runs.slice(0, run_index), new_run, ...pysplan_handler.runs.slice(run_index + 1)] });
195211
};
196212

197213
const flat_points = pysplan_handler?.runs[run_index].points.flatMap(p => [p.x, p.y])
@@ -282,14 +298,14 @@ export default function App() {
282298
{pysplan_handler ? (
283299
<div className="relative flex items-center justify-center w-full h-full border rounded-lg ml-4">
284300
<div className="relative">
285-
<img src={mat_img} className="w-auto h-auto max-h-[85vh] max-w-[85vw] object-contain"/>
301+
<img src={mat_img} className="w-auto h-auto max-h-[85vh] max-w-[85vw] object-contain" ref={img_size_ref}/>
286302
<div className="absolute inset-0 flex items-center justify-center">
287303
<div className="w-full h-full relative" onClick={AddPoint}>
288304
{pysplan_handler.runs[run_index].points.map((p, idx) => (
289-
<div key={idx} className="absolute bg-green-500 w-2 h-2 rounded-full" style={{ left: `${p.x}px`, top: `${p.y}px` }}/>
305+
<div key={idx} className="absolute bg-green-500 w-2 h-2 rounded-full" style={{ left: `${p.x}px`, bottom: `${p.y}px` }}/>
290306
))}
291307
{spline_points.map((p, idx) => (
292-
<div key={idx} className="absolute bg-green-400 w-1 h-1 rounded-full" style={{ left: `${p.x}px`, top: `${p.y}px` }}/>
308+
<div key={idx} className="absolute bg-green-400 w-1 h-1 rounded-full" style={{ left: `${p.x}px`, bottom: `${p.y}px` }}/>
293309
))}
294310
</div>
295311
</div>

0 commit comments

Comments
 (0)