Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,5 @@
class="h-full w-full bg-neutral-100 font-poppins dark:bg-neutral-950 dark:text-white"
>
<script type="module" src="/src/main.ts"></script>
<script>
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("sw.js").then(
(registration) => {
console.log("Service worker registration successful:", registration);
},
(error) => {
console.error(`Service worker registration failed: ${error}`);
},
);
} else {
console.error("Service workers are not supported.");
}
</script>
</body>
</html>
33 changes: 25 additions & 8 deletions public/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ self.addEventListener("activate", (event) => {
// On fetch, intercept server requests
// and respond with cached responses instead of going to network
self.addEventListener("fetch", (event) => {
// Skip service worker for Vite dev server requests
if (event.request.url.includes('/@vite/') ||
event.request.url.includes('/@fs/') ||
event.request.url.includes('/__vite_ping') ||
event.request.url.includes('/node_modules/') ||
event.request.url.includes('?import') ||
event.request.url.includes('.ts') && !event.request.url.includes('/assets/')) {
event.respondWith(fetch(event.request));
return;
}

// As a single page app, direct app to always go to cached home page.
if (event.request.mode === "navigate") {
event.respondWith(caches.match("/"));
Expand All @@ -59,12 +70,12 @@ self.addEventListener("fetch", (event) => {
fetch(event.request).catch(() => {
// If network fails, return a custom offline response
return new Response(
JSON.stringify({
error: 'offline',
message: 'You are offline. Please check your internet connection and try again.'
}),
{
status: 503,
JSON.stringify({
error: 'offline',
message: 'You are offline. Please check your internet connection and try again.'
}),
{
status: 503,
statusText: 'Service Unavailable',
headers: { 'Content-Type': 'application/json' }
}
Expand All @@ -83,8 +94,14 @@ self.addEventListener("fetch", (event) => {
// Return the cached response if it's available.
return cachedResponse;
}
// If resource isn't in the cache, return a 404.
return new Response(null, { status: 404 });
// Try network as fallback
try {
const networkResponse = await fetch(event.request);
return networkResponse;
} catch (error) {
// If resource isn't in cache and network fails, return a 404
return new Response(null, { status: 404 });
}
})(),
);
});
113 changes: 113 additions & 0 deletions src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
radiansToDegrees,
shortestRotation,
} from "./utils";
import {
findPathAroundObstacles,
smoothPath,
isPathClear,
} from "./utils/pathOptimization";
import hotkeys from 'hotkeys-js';

let two: Two;
Expand Down Expand Up @@ -602,6 +607,113 @@
return result;
}

/**
* Optimize all paths to avoid obstacles using A* pathfinding
* Finds the shortest collision-free path around all obstacles
*/
export async function optimizePathsAvoidingObstacles(): Promise<void> {
if (shapes.length === 0) {
alert('⚠️ No obstacles defined. Add obstacles first to use this feature.');
return;
}

// Calculate robot clearance radius using diagonal (worst case rotation)
// This ensures the entire robot body stays clear of obstacles at any angle
const robotDiagonal = Math.sqrt(robotWidth * robotWidth + robotHeight * robotHeight);
const safetyMargin = 6; // Generous 6 inch safety margin
const robotRadius = robotDiagonal / 2 + safetyMargin;

let totalPathsOptimized = 0;
let pathsWithCollisions = 0;

console.log('🔍 Starting obstacle avoidance optimization...');
console.log(`Robot dimensions: ${robotWidth}" × ${robotHeight}"`);
console.log(`Robot diagonal: ${robotDiagonal.toFixed(1)}" (clearance radius: ${robotRadius.toFixed(1)}")`);
console.log(`Obstacles to avoid: ${shapes.length}`);

for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
const line = lines[lineIdx];
const _startPoint = lineIdx === 0 ? startPoint : lines[lineIdx - 1].endPoint;

console.log(`\n📍 Optimizing Path ${lineIdx + 1}: (${_startPoint.x.toFixed(1)}, ${_startPoint.y.toFixed(1)}) → (${line.endPoint.x.toFixed(1)}, ${line.endPoint.y.toFixed(1)})`);

// Check if current path has collisions
const currentPath = [_startPoint, ...line.controlPoints, line.endPoint];
let hasCollision = false;

for (let i = 0; i < currentPath.length - 1; i++) {
if (!isPathClear(currentPath[i], currentPath[i + 1], shapes, robotRadius)) {
hasCollision = true;
pathsWithCollisions++;
console.log('⚠️ Collision detected!');
break;
}
}

if (!hasCollision && line.controlPoints.length === 0) {
console.log('✅ Path is already clear - no optimization needed');
continue;
}

// Use A* to find optimal path around obstacles
const optimalPath = findPathAroundObstacles(
_startPoint,
line.endPoint,
shapes,
robotRadius,
0,
144
);

console.log(`🛤️ A* found path with ${optimalPath.length} waypoints`);

// Smooth the path to reduce unnecessary waypoints
const smoothedPath = smoothPath(optimalPath, shapes, robotRadius);

console.log(`✨ Smoothed to ${smoothedPath.length} waypoints`);

// Validate the smoothed path is actually collision-free
let smoothedPathClear = true;
for (let i = 0; i < smoothedPath.length - 1; i++) {
if (!isPathClear(smoothedPath[i], smoothedPath[i + 1], shapes, robotRadius)) {
console.warn(`⚠️ Smoothed path still has collision between waypoints ${i} and ${i+1}`);
smoothedPathClear = false;
break;
}
}

// If smoothed path has collisions, use the unsmoothed optimal path
const finalPath = smoothedPathClear ? smoothedPath : optimalPath;

if (!smoothedPathClear) {
console.log(`🔄 Using unsmoothed path (${finalPath.length} waypoints) due to collision`);
}

// Update line with new control points (exclude start and end)
const newControlPoints = finalPath.slice(1, -1).map(p => ({ x: p.x, y: p.y }));

// Only update if path changed significantly or had collision
if (hasCollision || newControlPoints.length !== line.controlPoints.length) {
line.controlPoints = newControlPoints;
totalPathsOptimized++;
console.log(`✅ Path optimized with ${newControlPoints.length} control points!`);
} else {
console.log('ℹ️ Path unchanged');
}
}

// Trigger reactivity
lines = lines;

console.log(`\n🎉 Optimization complete! ${totalPathsOptimized}/${lines.length} paths updated`);

if (pathsWithCollisions > 0) {
alert(`✅ Obstacle avoidance complete!\n\n📊 Results:\n• ${pathsWithCollisions} path(s) had collisions\n• ${totalPathsOptimized} path(s) were optimized\n• All paths now avoid obstacles!\n\n🤖 Robot clearance: ${robotRadius.toFixed(1)}" radius\n(${robotWidth}" × ${robotHeight}" with ${robotDiagonal.toFixed(1)}" diagonal + ${safetyMargin}" margin)\n\nThe robot body will NOT touch any obstacles.`);
} else {
alert(`✅ All paths are already clear!\n\nNo collisions were detected with ${robotRadius.toFixed(1)}" clearance radius.\nYour paths already avoid all obstacles with proper robot body clearance.`);
}
}

onMount(() => {
two = new Two({
fitted: true,
Expand Down Expand Up @@ -845,5 +957,6 @@ hotkeys('s', function(event, handler){
{x}
{y}
{fpa}
{optimizePathsAvoidingObstacles}
/>
</div>
26 changes: 24 additions & 2 deletions src/lib/ControlTab.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
export let y: d3.ScaleLinear<number, number, number>;
export let settings: FPASettings;
export let shapes: Shape[];
export let optimizePathsAvoidingObstacles: () => Promise<void>;

function createTriangle(): Shape {
return {
Expand Down Expand Up @@ -106,8 +107,29 @@
</div>

<div class="flex flex-col w-full justify-start items-start gap-0.5 text-sm">
<div class="font-semibold">Obstacles</div>

<div class="flex flex-row w-full justify-between items-center">
<div class="font-semibold">Obstacles</div>
{#if shapes.length > 0}
<button
on:click={async () => {
try {
await optimizePathsAvoidingObstacles();
} catch (error) {
console.error('Obstacle avoidance optimization failed:', error);
alert(`❌ Optimization failed: ${error.message}`);
}
}}
class="px-2 py-1 rounded-md bg-orange-500 hover:bg-orange-600 text-white text-xs font-semibold flex flex-row items-center gap-1 transition-colors"
title="Automatically adjust all paths to avoid obstacles while maintaining the shortest and most efficient route"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width={2} class="size-3.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z" />
</svg>
Avoid Obstacles
</button>
{/if}
</div>

{#each shapes as shape, shapeIdx}
<div class="flex flex-col w-full justify-start items-start gap-1 p-2 border rounded-md border-neutral-300 dark:border-neutral-600">
<div class="flex flex-row w-full justify-between items-center">
Expand Down
23 changes: 23 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,27 @@ const app = new App({
target: document.body!,
});

// Service worker registration
if ("serviceWorker" in navigator) {
if (import.meta.env.PROD) {
// Only register service worker in production
navigator.serviceWorker.register("/sw.js").then(
(registration) => {
console.log("Service worker registration successful:", registration);
},
(error) => {
console.error(`Service worker registration failed: ${error}`);
},
);
} else {
// In development, unregister any existing service workers
navigator.serviceWorker.getRegistrations().then((registrations) => {
for (let registration of registrations) {
registration.unregister();
console.log("Unregistered service worker for development mode");
}
});
}
}

export default app;
Loading