Skip to content
Open
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
51 changes: 42 additions & 9 deletions src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,21 @@
}));
}

function getRobotDisplayXY(xy: BasePoint, headingDeg: number): BasePoint {
const headingRad = (headingDeg * Math.PI) / 180;
const offsetX = x(settings.rOffsetX) - x(0);
const offsetY = y(settings.rOffsetY) - y(0);
const offsetXRot =
offsetX * Math.cos(headingRad) - offsetY * Math.sin(headingRad);
const offsetYRot =
offsetX * Math.sin(headingRad) + offsetY * Math.cos(headingRad);

return {
x: xy.x + offsetXRot,
y: xy.y + offsetYRot,
};
}

// Canvas state
let two: Two;
let twoElement: HTMLDivElement;
Expand Down Expand Up @@ -1038,6 +1053,8 @@
settings.rWidth,
settings.rHeight,
50,
settings.rOffsetX,
settings.rOffsetY,
);

if (ghostPoints.length >= 3) {
Expand Down Expand Up @@ -1112,6 +1129,8 @@
settings.rWidth,
settings.rHeight,
50,
settings.rOffsetX,
settings.rOffsetY,
);

if (ghostPoints.length >= 3) {
Expand Down Expand Up @@ -1184,6 +1203,8 @@
settings.rWidth,
settings.rHeight,
50,
settings.rOffsetX,
settings.rOffsetY,
);

if (ghostPoints.length >= 3) {
Expand Down Expand Up @@ -1257,6 +1278,8 @@
settings.rWidth,
settings.rHeight,
spacing,
settings.rOffsetX,
settings.rOffsetY,
);

// If user requested onion layers only for the next point, filter to the relevant line
Expand Down Expand Up @@ -1369,6 +1392,8 @@
settings.rWidth,
settings.rHeight,
spacing,
settings.rOffsetX,
settings.rOffsetY,
);

// If user requested onion layers only for the next point, filter to the relevant line
Expand Down Expand Up @@ -1640,6 +1665,11 @@
ctx.globalAlpha = opacity;
ctx.translate(xy.x * scale, xy.y * scale);
ctx.rotate((headingDeg * Math.PI) / 180);
// Apply robot center offset (rotated with the robot)
ctx.translate(
settings.rOffsetX * scale,
settings.rOffsetY * scale
);
ctx.drawImage(
robotImage,
(-robotPixelWidth * scale) / 2,
Expand Down Expand Up @@ -3094,11 +3124,12 @@
<MathTools {x} {y} {twoElement} {robotXY} />
<!-- Main robot: only show in normal mode -->
{#if $activePaths.length === 0}
{@const mainRobotDisplayXY = getRobotDisplayXY(robotXY, robotHeading)}
<img
src={settings.robotImage || "/robot.png"}
alt="Robot"
style={`position: absolute; top: ${robotXY.y}px;
left: ${robotXY.x}px; transform: translate(-50%, -50%) rotate(${robotHeading}deg); z-index: 20; width: ${x(robotWidth)}px; height: ${x(robotHeight)}px;user-select: none; -webkit-user-select: none; -moz-user-select: none;-ms-user-select: none;
style={`position: absolute; top: ${mainRobotDisplayXY.y}px;
left: ${mainRobotDisplayXY.x}px; transform: translate(-50%, -50%) rotate(${robotHeading}deg); z-index: 20; width: ${x(robotWidth)}px; height: ${x(robotHeight)}px;user-select: none; -webkit-user-select: none; -moz-user-select: none;-ms-user-select: none;
pointer-events: none;`}
draggable="false"
on:error={(e) => {
Expand All @@ -3111,7 +3142,7 @@ pointer-events: none;`}
<!-- Heading arrow for main robot -->
{#if settings.showHeadingArrow}
<svg
style={`position: absolute; top: ${robotXY.y}px; left: ${robotXY.x}px; z-index: 21; pointer-events: none; overflow: visible;`}
style={`position: absolute; top: ${mainRobotDisplayXY.y}px; left: ${mainRobotDisplayXY.x}px; z-index: 21; pointer-events: none; overflow: visible;`}
width="1"
height="1"
>
Expand Down Expand Up @@ -3144,11 +3175,12 @@ pointer-events: none;`}
{/if}
<!-- Second robot: only show in dual path mode (not multi-path mode) -->
{#if $activePaths.length === 0 && $dualPathMode}
{@const secondRobotDisplayXY = getRobotDisplayXY(secondRobotXY, secondRobotHeading)}
<img
src={settings.robotImage || "/robot.png"}
alt="Robot 2"
style={`position: absolute; top: ${secondRobotXY.y}px;
left: ${secondRobotXY.x}px; transform: translate(-50%, -50%) rotate(${secondRobotHeading}deg); z-index: 19; width: ${x(robotWidth)}px; height: ${x(robotHeight)}px;user-select: none; -webkit-user-select: none; -moz-user-select: none;-ms-user-select: none;
style={`position: absolute; top: ${secondRobotDisplayXY.y}px;
left: ${secondRobotDisplayXY.x}px; transform: translate(-50%, -50%) rotate(${secondRobotHeading}deg); z-index: 19; width: ${x(robotWidth)}px; height: ${x(robotHeight)}px;user-select: none; -webkit-user-select: none; -moz-user-select: none;-ms-user-select: none;
pointer-events: none; opacity: 0.8;`}
draggable="false"
on:error={(e) => {
Expand All @@ -3161,7 +3193,7 @@ pointer-events: none; opacity: 0.8;`}
<!-- Heading arrow for second robot -->
{#if settings.showHeadingArrow}
<svg
style={`position: absolute; top: ${secondRobotXY.y}px; left: ${secondRobotXY.x}px; z-index: 19; pointer-events: none; overflow: visible; opacity: 0.8;`}
style={`position: absolute; top: ${secondRobotDisplayXY.y}px; left: ${secondRobotDisplayXY.x}px; z-index: 19; pointer-events: none; overflow: visible; opacity: 0.8;`}
width="1"
height="1"
>
Expand Down Expand Up @@ -3195,11 +3227,12 @@ pointer-events: none; opacity: 0.8;`}
<!-- Additional robots: only show in multi-path mode -->
{#if $activePaths.length > 0}
{#each additionalRobotStates as robotState, idx}
{@const displayXY = getRobotDisplayXY(robotState.xy, robotState.heading)}
<img
src={settings.robotImage || "/robot.png"}
alt="Robot {idx + 1}"
style={`position: absolute; top: ${robotState.xy.y}px;
left: ${robotState.xy.x}px; transform: translate(-50%, -50%) rotate(${robotState.heading}deg); z-index: ${20 - idx}; width: ${x(robotWidth)}px; height: ${x(robotHeight)}px;user-select: none; -webkit-user-select: none; -moz-user-select: none;-ms-user-select: none;
style={`position: absolute; top: ${displayXY.y}px;
left: ${displayXY.x}px; transform: translate(-50%, -50%) rotate(${robotState.heading}deg); z-index: ${20 - idx}; width: ${x(robotWidth)}px; height: ${x(robotHeight)}px;user-select: none; -webkit-user-select: none; -moz-user-select: none;-ms-user-select: none;
pointer-events: none; opacity: ${1.0 - idx * 0.15};`}
draggable="false"
on:error={(e) => {
Expand All @@ -3212,7 +3245,7 @@ pointer-events: none; opacity: ${1.0 - idx * 0.15};`}
<!-- Heading arrow for additional robots -->
{#if settings.showHeadingArrow}
<svg
style={`position: absolute; top: ${robotState.xy.y}px; left: ${robotState.xy.x}px; z-index: ${20 - idx}; pointer-events: none; overflow: visible; opacity: ${1.0 - idx * 0.15};`}
style={`position: absolute; top: ${displayXY.y}px; left: ${displayXY.x}px; z-index: ${20 - idx}; pointer-events: none; overflow: visible; opacity: ${1.0 - idx * 0.15};`}
width="1"
height="1"
>
Expand Down
4 changes: 4 additions & 0 deletions src/config/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { getRandomColor } from "../utils";
*/
export const DEFAULT_ROBOT_WIDTH = 16;
export const DEFAULT_ROBOT_HEIGHT = 16;
export const DEFAULT_ROBOT_OFFSET_X = 0;
export const DEFAULT_ROBOT_OFFSET_Y = 0;

/**
* Default canvas drawing settings
Expand Down Expand Up @@ -34,6 +36,8 @@ export const DEFAULT_SETTINGS: Settings = {
kFriction: 0.1,
rWidth: DEFAULT_ROBOT_WIDTH,
rHeight: DEFAULT_ROBOT_HEIGHT,
rOffsetX: DEFAULT_ROBOT_OFFSET_X,
rOffsetY: DEFAULT_ROBOT_OFFSET_Y,
safetyMargin: 1,
maxVelocity: 40,
maxAcceleration: 30,
Expand Down
44 changes: 43 additions & 1 deletion src/lib/components/SettingsDialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

// Track which sections are collapsed
let collapsedSections = {
robot: true,
robot: false,
motion: true,
advanced: true,
theme: true,
Expand Down Expand Up @@ -258,6 +258,48 @@
/>
</div>

<div>
<label
for="robot-offset-x"
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1"
>
Robot Offset X (in)
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Horizontal offset from the robot center to the wheel center
</div>
</label>
<input
id="robot-offset-x"
type="number"
value={settings.rOffsetX}
step="0.1"
on:input={(e) =>
handleNumberInput(e.target.value, "rOffsetX")}
class="w-full px-3 py-2 rounded-md border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>

<div>
<label
for="robot-offset-y"
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1"
>
Robot Offset Y (in)
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Vertical offset from the robot center to the wheel center
</div>
</label>
<input
id="robot-offset-y"
type="number"
value={settings.rOffsetY}
step="0.1"
on:input={(e) =>
handleNumberInput(e.target.value, "rOffsetY")}
class="w-full px-3 py-2 rounded-md border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>

<div>
<label
for="safety-margin"
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ export interface Settings {
kFriction: number;
rWidth: number;
rHeight: number;
rOffsetX: number;
rOffsetY: number;
safetyMargin: number;
maxVelocity: number; // inches/sec
maxAcceleration: number; // inches/sec²
Expand Down
28 changes: 22 additions & 6 deletions src/utils/animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,8 @@ export function generateGhostPathPoints(
robotWidth: number,
robotHeight: number,
samples: number = 200, // Higher default for smoother turns
offsetX: number = 0,
offsetY: number = 0,
): BasePoint[] {
if (lines.length === 0) return [];

Expand Down Expand Up @@ -474,23 +476,30 @@ export function generateGhostPathPoints(
}
}

// Build left/right rails directly from center + normal offsets
const headingRad = (heading * Math.PI) / 180;
const offsetXRot = offsetX * Math.cos(headingRad) - offsetY * Math.sin(headingRad);
const offsetYRot = offsetX * Math.sin(headingRad) + offsetY * Math.cos(headingRad);
const centerPoint = {
x: robotPosInches.x + offsetXRot,
y: robotPosInches.y + offsetYRot,
};

// Build left/right rails directly from center + normal offsets
const nx = -Math.sin(headingRad);
const ny = Math.cos(headingRad);
const halfW = robotWidth / 2;

const leftPoint = {
x: robotPosInches.x + nx * halfW,
y: robotPosInches.y + ny * halfW,
x: centerPoint.x + nx * halfW,
y: centerPoint.y + ny * halfW,
};
const rightPoint = {
x: robotPosInches.x - nx * halfW,
y: robotPosInches.y - ny * halfW,
x: centerPoint.x - nx * halfW,
y: centerPoint.y - ny * halfW,
};

robotStates.push({
center: { x: robotPosInches.x, y: robotPosInches.y },
center: centerPoint,
heading,
left: leftPoint,
right: rightPoint,
Expand All @@ -511,6 +520,8 @@ export function generateGhostPathPoints(
heading,
robotWidth,
robotHeight,
offsetX,
offsetY,
);
return corners;
}
Expand Down Expand Up @@ -592,6 +603,8 @@ export function generateOnionLayers(
robotWidth: number,
robotHeight: number,
spacing: number = 6,
offsetX: number = 0,
offsetY: number = 0,
): Array<{ x: number; y: number; heading: number; corners: BasePoint[]; lineIndex: number }> {
if (lines.length === 0) return [];

Expand All @@ -600,6 +613,7 @@ export function generateOnionLayers(
y: number;
heading: number;
corners: BasePoint[];
lineIndex: number;
}> = [];

// Calculate total path length
Expand Down Expand Up @@ -702,6 +716,8 @@ export function generateOnionLayers(
heading,
robotWidth,
robotHeight,
offsetX,
offsetY,
);

layers.push({
Expand Down
10 changes: 8 additions & 2 deletions src/utils/geometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export function getRobotCorners(
heading: number,
width: number,
height: number,
offsetX: number = 0,
offsetY: number = 0,
): BasePoint[] {
// Convert heading from degrees to radians
const headingRad = (heading * Math.PI) / 180;
Expand All @@ -128,6 +130,10 @@ export function getRobotCorners(
const cos = Math.cos(headingRad);
const sin = Math.sin(headingRad);

// Apply center offset (rotated with the robot)
const offsetXRot = offsetX * cos - offsetY * sin;
const offsetYRot = offsetX * sin + offsetY * cos;

// Corner offsets relative to center (before rotation)
// Define corners in local robot frame:
// - width extends perpendicular to heading direction (left/right)
Expand All @@ -142,8 +148,8 @@ export function getRobotCorners(
// Rotate and translate corners
// Using standard 2D rotation matrix for screen coordinates
return corners.map((corner) => ({
x: x + corner.dx * cos - corner.dy * sin,
y: y + corner.dx * sin + corner.dy * cos,
x: x + offsetXRot + corner.dx * cos - corner.dy * sin,
y: y + offsetYRot + corner.dx * sin + corner.dy * cos,
}));
}

Expand Down
Loading