Skip to content

Commit 3eba285

Browse files
authored
Optimize button (#24)
* Implement Optimize Button * Moved Optimize Button * Fixed UI Bugs, Temporarily Disabeled Optimize Button, Fixed Code Exporting * Changed autonomousPathUpdate export state * Re enable Optimze Button
1 parent 347a052 commit 3eba285

7 files changed

Lines changed: 422 additions & 101 deletions

File tree

src/App.svelte

Lines changed: 213 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,12 @@
107107
lineId: ln.id!,
108108
}));
109109
let shapes: Shape[] = getDefaultShapes();
110+
let optimizingLineIds: Record<string, boolean> = {};
111+
let optimizingAll = false;
110112
111113
const history = createHistory();
112114
const { canUndoStore, canRedoStore } = history;
115+
const OPTIMIZER_BASE_URL = "https://fpa.pedropathing.com";
113116
114117
function getAppState(): AppState {
115118
return {
@@ -954,6 +957,15 @@
954957
let isDown = false;
955958
let dragOffset = { x: 0, y: 0 }; // Store offset to prevent snapping to center
956959
960+
const isLockedPathElem = (id: string | null): boolean => {
961+
if (!id || !id.startsWith("point")) return false;
962+
const parts = id.split("-");
963+
const lineIdx = Number(parts[1]) - 1;
964+
if (Number.isNaN(lineIdx)) return false;
965+
if (lineIdx < 0) return false; // startPoint currently not lockable
966+
return !!lines[lineIdx]?.locked;
967+
};
968+
957969
two.renderer.domElement.addEventListener("mousemove", (evt: MouseEvent) => {
958970
const elem = document.elementFromPoint(evt.clientX, evt.clientY);
959971
@@ -1028,7 +1040,10 @@
10281040
}
10291041
}
10301042
} else {
1031-
if (elem?.id.startsWith("point") || elem?.id.startsWith("obstacle")) {
1043+
if (
1044+
(elem?.id.startsWith("point") && !isLockedPathElem(elem.id)) ||
1045+
elem?.id.startsWith("obstacle")
1046+
) {
10321047
two.renderer.domElement.style.cursor = "pointer";
10331048
currentElem = elem.id;
10341049
} else {
@@ -1039,6 +1054,11 @@
10391054
});
10401055
10411056
two.renderer.domElement.addEventListener("mousedown", (evt: MouseEvent) => {
1057+
if (currentElem && isLockedPathElem(currentElem)) {
1058+
isDown = false;
1059+
return;
1060+
}
1061+
10421062
isDown = true;
10431063
10441064
if (currentElem) {
@@ -1295,6 +1315,192 @@
12951315
recordChange();
12961316
}
12971317
1318+
function toHeadingDegrees(point: Point, position: "start" | "end"): number {
1319+
if (!point) return 0;
1320+
if (point.heading === "linear") {
1321+
return position === "start" ? point.startDeg ?? 0 : point.endDeg ?? 0;
1322+
}
1323+
if (point.heading === "constant") {
1324+
return point.degrees ?? 0;
1325+
}
1326+
return 0;
1327+
}
1328+
1329+
function buildOptimizationPayload(lineIndex: number) {
1330+
const line = lines[lineIndex];
1331+
if (!line) throw new Error("Line not found");
1332+
1333+
const startPt = lineIndex === 0 ? startPoint : lines[lineIndex - 1]?.endPoint;
1334+
if (!startPt) throw new Error("Missing start point for optimization");
1335+
1336+
const waypoints = [startPt, ...line.controlPoints, line.endPoint].map((p) => [p.x, p.y]);
1337+
1338+
return {
1339+
waypoints,
1340+
start_heading_degrees: toHeadingDegrees(startPt, "start"),
1341+
end_heading_degrees: toHeadingDegrees(line.endPoint, "end"),
1342+
x_velocity: settings.xVelocity,
1343+
y_velocity: settings.yVelocity,
1344+
angular_velocity: settings.aVelocity,
1345+
friction_coefficient: settings.kFriction,
1346+
robot_width: settings.rWidth,
1347+
robot_height: settings.rHeight,
1348+
min_coord_field: 0,
1349+
max_coord_field: FIELD_SIZE,
1350+
interpolation:
1351+
line.endPoint.heading === "tangential"
1352+
? "tangent"
1353+
: line.endPoint.heading === "constant"
1354+
? "constant"
1355+
: "linear",
1356+
};
1357+
}
1358+
1359+
function sleep(ms: number) {
1360+
return new Promise((res) => setTimeout(res, ms));
1361+
}
1362+
1363+
async function createOptimizationTask(payload: any) {
1364+
const response = await fetch(`${OPTIMIZER_BASE_URL}/optimize`, {
1365+
method: "POST",
1366+
headers: { "Content-Type": "application/json" },
1367+
body: JSON.stringify(payload),
1368+
});
1369+
1370+
if (response.status === 503) {
1371+
const errorData = await response.json().catch(() => ({}));
1372+
if ((errorData as any).error === "offline") {
1373+
throw new Error(`OFFLINE: ${(errorData as any).message || "Service unavailable"}`);
1374+
}
1375+
}
1376+
1377+
if (!response.ok) {
1378+
const errorText = await response.text().catch(() => "");
1379+
throw new Error(`Optimizer request failed (${response.status}): ${errorText || response.statusText}`);
1380+
}
1381+
1382+
const data = await response.json();
1383+
if (!data?.job_id) throw new Error("Optimizer did not return a job id");
1384+
return data.job_id as string;
1385+
}
1386+
1387+
async function pollOptimizationResult(jobId: string, pollInterval = 1000, maxTries = 60) {
1388+
for (let i = 0; i < maxTries; i++) {
1389+
const response = await fetch(`${OPTIMIZER_BASE_URL}/job/${jobId}`);
1390+
1391+
if (response.status === 503) {
1392+
const errorData = await response.json().catch(() => ({}));
1393+
if ((errorData as any).error === "offline") {
1394+
throw new Error(`OFFLINE: ${(errorData as any).message || "Service unavailable"}`);
1395+
}
1396+
}
1397+
1398+
if (!response.ok) {
1399+
const errorText = await response.text().catch(() => "");
1400+
throw new Error(`Optimizer status failed (${response.status}): ${errorText || response.statusText}`);
1401+
}
1402+
1403+
const data = await response.json();
1404+
if (data?.status === "completed" && data.result) {
1405+
return data.result;
1406+
}
1407+
if (data?.status === "error") {
1408+
throw new Error("Optimization failed with server error.");
1409+
}
1410+
1411+
await sleep(pollInterval);
1412+
}
1413+
1414+
throw new Error("Timed out waiting for optimization result.");
1415+
}
1416+
1417+
async function runOptimization(payload: any, pollInterval = 1000, maxTries = 60) {
1418+
const jobId = await createOptimizationTask(payload);
1419+
return pollOptimizationResult(jobId, pollInterval, maxTries);
1420+
}
1421+
1422+
async function optimizeLine(lineId: string, targetControlPointIndex?: number) {
1423+
const lineIndex = lines.findIndex((l) => l.id === lineId);
1424+
if (lineIndex === -1) {
1425+
alert("Could not find line to optimize.");
1426+
return;
1427+
}
1428+
1429+
if (optimizingLineIds[lineId]) return;
1430+
optimizingLineIds = { ...optimizingLineIds, [lineId]: true };
1431+
1432+
try {
1433+
const payload = buildOptimizationPayload(lineIndex);
1434+
const result = await runOptimization(payload);
1435+
1436+
const optimizedWaypoints = Array.isArray(result?.optimized_waypoints)
1437+
? result.optimized_waypoints
1438+
: Array.isArray(result)
1439+
? result
1440+
: null;
1441+
1442+
if (!optimizedWaypoints || optimizedWaypoints.length < 2) {
1443+
throw new Error("Unexpected optimizer response format.");
1444+
}
1445+
1446+
const interior = optimizedWaypoints
1447+
.slice(1, optimizedWaypoints.length - 1)
1448+
.map((p: number[]) => ({ x: p[0], y: p[1] }));
1449+
1450+
const newLines = [...lines];
1451+
const current = newLines[lineIndex];
1452+
1453+
if (typeof targetControlPointIndex === "number") {
1454+
// Only replace the targeted control point; keep others and endpoint untouched
1455+
const replacement =
1456+
interior[targetControlPointIndex] ?? interior[interior.length - 1];
1457+
if (replacement) {
1458+
const cps = [...current.controlPoints];
1459+
if (cps[targetControlPointIndex]) {
1460+
cps[targetControlPointIndex] = replacement;
1461+
newLines[lineIndex] = {
1462+
...current,
1463+
controlPoints: cps,
1464+
};
1465+
lines = normalizeLines(newLines);
1466+
recordChange();
1467+
}
1468+
}
1469+
} else {
1470+
// Replace entire line (control points and endpoint)
1471+
newLines[lineIndex] = {
1472+
...current,
1473+
endPoint: {
1474+
...current.endPoint,
1475+
x: optimizedWaypoints[optimizedWaypoints.length - 1][0],
1476+
y: optimizedWaypoints[optimizedWaypoints.length - 1][1],
1477+
},
1478+
controlPoints: interior,
1479+
};
1480+
lines = normalizeLines(newLines);
1481+
recordChange();
1482+
}
1483+
} catch (err) {
1484+
console.error(err);
1485+
alert((err as Error).message || "Optimization failed.");
1486+
} finally {
1487+
optimizingLineIds = { ...optimizingLineIds, [lineId]: false };
1488+
}
1489+
}
1490+
1491+
async function optimizeAllLines() {
1492+
if (optimizingAll) return;
1493+
optimizingAll = true;
1494+
try {
1495+
for (const ln of lines) {
1496+
if (!ln?.id) continue;
1497+
await optimizeLine(ln.id);
1498+
}
1499+
} finally {
1500+
optimizingAll = false;
1501+
}
1502+
}
1503+
12981504
function loadRobot(evt: Event) {
12991505
loadRobotImage(evt, () => updateRobotImageDisplay());
13001506
}
@@ -1452,6 +1658,8 @@
14521658
{recordChange}
14531659
{canUndo}
14541660
{canRedo}
1661+
{optimizeAllLines}
1662+
{optimizingAll}
14551663
/>
14561664
<!-- {saveFile} -->
14571665
<div
@@ -1550,5 +1758,9 @@ pointer-events: none;`}
15501758
bind:loopAnimation
15511759
{resetAnimation}
15521760
{recordChange}
1761+
{optimizeLine}
1762+
{optimizingLineIds}
1763+
{optimizeAllLines}
1764+
{optimizingAll}
15531765
/>
15541766
</div>

src/lib/ControlTab.svelte

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
export let settings: Settings;
3434
export let handleSeek: (percent: number) => void;
3535
export let loopAnimation: boolean;
36+
export let optimizeLine: (lineId: string, targetControlPointIndex?: number) => void;
37+
export let optimizingLineIds: Record<string, boolean> = {};
3638
3739
export let shapes: Shape[];
3840
export let recordChange: () => void;
@@ -68,6 +70,7 @@
6870
return _markers;
6971
})();
7072
73+
7174
// State for collapsed sections
7275
let collapsedSections = {
7376
obstacles: shapes.map(() => true),
@@ -547,6 +550,8 @@
547550
onMoveDown={() => moveSequenceItem(sIdx, 1)}
548551
canMoveUp={sIdx !== 0}
549552
canMoveDown={sIdx !== sequence.length - 1}
553+
optimizeLine={optimizeLine}
554+
optimizing={optimizingLineIds?.[ln.id ?? ""] ?? false}
550555
{recordChange}
551556
/>
552557
{/each}

src/lib/MathTools.svelte

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,12 @@
6969
const rect = twoElement.getBoundingClientRect();
7070
const mouseX = event.clientX - rect.left;
7171
const mouseY = event.clientY - rect.top;
72-
const inchX = x.invert(mouseX);
73-
const inchY = y.invert(mouseY);
72+
let inchX = x.invert(mouseX);
73+
let inchY = y.invert(mouseY);
74+
75+
// Clamp to field boundaries
76+
inchX = Math.max(0, Math.min(FIELD_SIZE, inchX));
77+
inchY = Math.max(0, Math.min(FIELD_SIZE, inchY));
7478
7579
if (rulerDragging === "start") {
7680
rulerStart = { x: inchX, y: inchY };
@@ -96,6 +100,7 @@
96100
);
97101
protractorRadius = clampedRadius;
98102
const angleRadians = Math.atan2(centerY - mouseY, mouseX - centerX);
103+
// keep resize angle updated for handle position
99104
protractorResizeAngle = angleRadians * (180 / Math.PI);
100105
}
101106
}
@@ -382,21 +387,13 @@
382387
: "Drag to move protractor"}
383388
on:mousedown={(e) => {
384389
if ($protractorLockToRobot) {
390+
// Locked: center click does nothing; unlock via navbar toggle only
385391
e.stopPropagation();
386-
protractorLockToRobot.set(false);
387-
} else {
388-
handleMouseDown(e, "protractor-move");
392+
return;
389393
}
394+
handleMouseDown(e, "protractor-move");
390395
}}
391396
/>
392-
{#if $protractorLockToRobot}
393-
<text
394-
x="0"
395-
y="3"
396-
class="fill-white text-[10px] font-bold pointer-events-none"
397-
text-anchor="middle">x</text
398-
>
399-
{/if}
400397
</g>
401398
</svg>
402399
{/if}

0 commit comments

Comments
 (0)