Skip to content
Merged
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
10 changes: 10 additions & 0 deletions docs/docs/tab-reference/2d-field.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ To zoom, place the cursor over the timeline and scroll up or down. A range can a

</details>

## Camera Controls

Scroll up and down over the field to zoom in and out. Click and drag to pan across the field. Right-click anywhere on the field to open the camera menu with the following options:

- **Unlocked**: Pan and zoom are manual.
- **Locked to Robot**: The camera automatically pans to track the robot.
- **Locked to Robot & Rotation**: The camera automatically pans and rotates to track the robot and its rotation.

In either locked mode, the camera can be freely panned away from the robot. The orientation buttons in the control pane can be used to rotate the camera in any mode.

## Adding Objects

To get started, drag a field to the "Poses" section. Delete an object using the X button, or hide it temporarily by clicking the eye icon or double-clicking the field name. To remove all objects, click the trash can near the axis title and then `Clear All`. Objects can be rearranged in the list by clicking and dragging.
Expand Down
43 changes: 37 additions & 6 deletions src/hub/ScrollSensor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default class ScrollSensor {
private RESET_MS = 1000;

private container: HTMLElement;
private callback: (x: number, y: number) => void;
private callback: (dx: number, dy: number, isPan: boolean, cursorX: number, cursorY: number) => void;

private lastScrollUpdate = 0;
private resetNext = false;
Expand All @@ -19,13 +19,22 @@ export default class ScrollSensor {

private panActive = false;
private panLastCursorX = 0;
private panLastCursorY = 0;

private lastCursorX = 0;
private lastCursorY = 0;

/**
* Creates a new ScrollSensor.
* @param container The container element. The overflow should be "scroll" and the scrollbar should be hidden. The child element should have the dimensions 1000000x1000000px.
* @param callback A function to be called after each scroll event, with the relative change in x and y.
* @param mouseControl The mouse control mode for drag-panning.
*/
constructor(container: HTMLElement, callback: (dx: number, dy: number) => void, enableMouseControls = true) {
constructor(
container: HTMLElement,
callback: (dx: number, dy: number, isPan: boolean, cursorX: number, cursorY: number) => void,
mouseControl: ScrollSensorMouseControl = ScrollSensorMouseControl.PanX
) {
this.container = container;
this.callback = callback;

Expand All @@ -35,13 +44,22 @@ export default class ScrollSensor {
this.update();
});

// Prevent capturing spacebar
this.container.addEventListener("keydown", (event) => {
if (event.code === "Space") {
event.preventDefault();
}
});

// Mouse controls
if (enableMouseControls) {
if (mouseControl !== ScrollSensorMouseControl.None) {
container.addEventListener("mousedown", (event) => {
if (event.shiftKey) return;
this.panActive = true;
let x = event.clientX - container.getBoundingClientRect().x;
let y = event.clientY - container.getBoundingClientRect().y;
this.panLastCursorX = x;
this.panLastCursorY = y;
});
container.addEventListener("mouseleave", () => {
this.panActive = false;
Expand All @@ -50,10 +68,17 @@ export default class ScrollSensor {
this.panActive = false;
});
container.addEventListener("mousemove", (event) => {
let cursorX = event.clientX - container.getBoundingClientRect().x;
let cursorY = event.clientY - container.getBoundingClientRect().y;
this.lastCursorX = cursorX;
this.lastCursorY = cursorY;

if (this.panActive) {
let cursorX = event.clientX - container.getBoundingClientRect().x;
callback(this.panLastCursorX - cursorX, 0);
let dx = this.panLastCursorX - cursorX;
let dy = mouseControl === ScrollSensorMouseControl.PanXY ? this.panLastCursorY - cursorY : 0;
callback(dx, dy, true, cursorX, cursorY);
this.panLastCursorX = cursorX;
this.panLastCursorY = cursorY;
}
});
}
Expand Down Expand Up @@ -89,7 +114,7 @@ export default class ScrollSensor {
let dy = this.container.scrollTop - this.lastScrollTop;
this.lastScrollLeft = this.container.scrollLeft;
this.lastScrollTop = this.container.scrollTop;
this.callback(dx, dy);
this.callback(dx, dy, false, this.lastCursorX, this.lastCursorY);
}

/** Moves the scroll position to the center. */
Expand All @@ -101,3 +126,9 @@ export default class ScrollSensor {
this.lastScrollTop = middle;
}
}

export enum ScrollSensorMouseControl {
None,
PanX,
PanXY
}
8 changes: 7 additions & 1 deletion src/hub/SelectionImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,13 @@ export default class SelectionImpl implements Selection {
});

window.addEventListener("keydown", (event) => {
if (event.target !== document.body && event.target !== window) return;
if (
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement ||
(event.target instanceof HTMLElement && event.target.isContentEditable)
) {
return;
}
switch (event.code) {
case "Space":
event.preventDefault();
Expand Down
11 changes: 9 additions & 2 deletions src/hub/Tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import TableRenderer from "../shared/renderers/TableRenderer";
import VideoRenderer from "../shared/renderers/VideoRenderer";
import { Units } from "../shared/units";
import { clampValue } from "../shared/util";
import ScrollSensor from "./ScrollSensor";
import ScrollSensor, { ScrollSensorMouseControl } from "./ScrollSensor";
import Timeline from "./Timeline";
import ConsoleController from "./controllers/ConsoleController";
import Field2dController from "./controllers/Field2dController";
Expand Down Expand Up @@ -325,7 +325,7 @@ export default class Tabs {
(dx: number, dy: number) => {
this.TAB_BAR.scrollLeft += dx + dy;
},
false
ScrollSensorMouseControl.None
);

// Add timeline
Expand Down Expand Up @@ -747,6 +747,13 @@ export default class Tabs {
}
}

/** Switches the selected camera mode for the selected 2D field. */
set2DCamera(index: number) {
if (this.tabList[this.selectedTab].type === TabType.Field2d) {
(this.tabList[this.selectedTab].renderer as Field2dRenderer).set2DCamera(index);
}
}

/** Switches the orbit FOV for the selected 3D field. */
setFov(fov: number) {
if (this.tabList[this.selectedTab].type === TabType.Field3d) {
Expand Down
4 changes: 3 additions & 1 deletion src/hub/controllers/VideoController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@ export default class VideoController implements TabController {
if (
root === null ||
root.hidden ||
event.target !== document.body ||
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement ||
(event.target instanceof HTMLElement && event.target.isContentEditable) ||
(window.platform === "darwin" ? event.metaKey : event.ctrlKey)
)
return;
Expand Down
4 changes: 4 additions & 0 deletions src/hub/hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,10 @@ async function handleMainMessage(message: NamedMessage) {
window.tabs.set3DCamera(message.data);
break;

case "set-2d-camera":
window.tabs.set2DCamera(message.data);
break;

case "edit-fov":
window.tabs.setFov(message.data);
break;
Expand Down
57 changes: 55 additions & 2 deletions src/main/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { AdvantageScopeAssets } from "../../shared/AdvantageScopeAssets";
import ButtonRect from "../../shared/ButtonRect";
import { ensureThemeContrast } from "../../shared/Colors";
import ExportOptions from "../../shared/ExportOptions";
import { Field2dCameraMode } from "../../shared/Field2dCameraMode";
import LineGraphFilter from "../../shared/LineGraphFilter";
import NamedMessage from "../../shared/NamedMessage";
import Preferences, { DEFAULT_PREFS, getLiveModeName, LiveMode, mergePreferences } from "../../shared/Preferences";
Expand Down Expand Up @@ -1211,6 +1212,10 @@ async function handleHubMessage(window: BrowserWindow, message: NamedMessage) {
);
break;

case "ask-2d-camera":
select2DCameraPopup(window, message.data.position, message.data.selectedIndex);
break;

case "export-console":
dialog
.showSaveDialog(window, {
Expand Down Expand Up @@ -1516,6 +1521,45 @@ function select3DCameraPopup(
});
}

function select2DCameraPopup(window: BrowserWindow, position: [number, number], selectedIndex: Field2dCameraMode) {
const cameraMenu = new Menu();
cameraMenu.append(
new MenuItem({
label: "Unlocked",
type: "checkbox",
checked: selectedIndex === Field2dCameraMode.Unlocked,
click() {
sendMessage(window, "set-2d-camera", Field2dCameraMode.Unlocked);
}
})
);
cameraMenu.append(
new MenuItem({
label: "Locked to Robot",
type: "checkbox",
checked: selectedIndex === Field2dCameraMode.Robot,
click() {
sendMessage(window, "set-2d-camera", Field2dCameraMode.Robot);
}
})
);
cameraMenu.append(
new MenuItem({
label: "Locked to Robot && Rotation",
type: "checkbox",
checked: selectedIndex === Field2dCameraMode.RobotAndRotation,
click() {
sendMessage(window, "set-2d-camera", Field2dCameraMode.RobotAndRotation);
}
})
);
cameraMenu.popup({
window: window,
x: Math.round(position[0]),
y: Math.round(position[1])
});
}

/**
* Process a message from a download window.
* @param message The received message
Expand Down Expand Up @@ -3113,15 +3157,20 @@ function createSatellite(
let message: NamedMessage = event.data;
switch (message.name) {
case "set-aspect-ratio":
let aspectRatio = message.data;
let aspectRatio = message.data.aspectRatio;
let lock = message.data.lock;
if (aspectRatio === null) {
satellite.setAspectRatio(0);
} else {
let originalSize = satellite.getContentSize();
let originalArea = originalSize[0] * originalSize[1];
let newY = Math.sqrt(originalArea / aspectRatio);
let newX = aspectRatio * newY;
satellite.setAspectRatio(aspectRatio);
if (lock) {
satellite.setAspectRatio(aspectRatio);
} else {
satellite.setAspectRatio(0);
}
satellite.setContentSize(Math.round(newX), Math.round(newY));
}
break;
Expand All @@ -3137,6 +3186,10 @@ function createSatellite(
);
break;

case "ask-2d-camera":
select2DCameraPopup(satellite, message.data.position, message.data.selectedIndex);
break;

case "add-table-range":
hubWindows.forEach((window) => {
sendMessage(window, "add-table-range", {
Expand Down
30 changes: 30 additions & 0 deletions src/main/lite/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AdvantageScopeAssets } from "../../shared/AdvantageScopeAssets";
import { BUILD_DATE, COPYRIGHT, Distribution, DISTRIBUTION, LITE_VERSION } from "../../shared/buildConstants";
import ButtonRect from "../../shared/ButtonRect";
import { ensureThemeContrast } from "../../shared/Colors";
import { Field2dCameraMode } from "../../shared/Field2dCameraMode";
import { HubState } from "../../shared/HubState";
import LineGraphFilter from "../../shared/LineGraphFilter";
import NamedMessage from "../../shared/NamedMessage";
Expand Down Expand Up @@ -1205,6 +1206,35 @@ async function handleHubMessage(message: NamedMessage) {
}
break;

case "ask-2d-camera":
{
let position: [number, number] = message.data.position;
let selectedIndex: Field2dCameraMode = message.data.selectedIndex;
let menuItems: (MenuItem | Submenu | "-")[] = [
{
content: (selectedIndex === Field2dCameraMode.Unlocked ? "\u2714 " : "") + "Unlocked",
callback() {
sendMessage(hubPort, "set-2d-camera", Field2dCameraMode.Unlocked);
}
},
{
content: (selectedIndex === Field2dCameraMode.Robot ? "\u2714 " : "") + "Locked to Robot",
callback() {
sendMessage(hubPort, "set-2d-camera", Field2dCameraMode.Robot);
}
},
{
content:
(selectedIndex === Field2dCameraMode.RobotAndRotation ? "\u2714 " : "") + "Locked to Robot & Rotation",
callback() {
sendMessage(hubPort, "set-2d-camera", Field2dCameraMode.RobotAndRotation);
}
}
];
openMenu({ x: position[0], y: position[1], width: 0, height: 0 }, menuItems);
}
break;

default:
console.warn("Unknown message from hub", message);
break;
Expand Down
16 changes: 14 additions & 2 deletions src/satellite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@ window.addEventListener("message", (event) => {
}
break;

case "set-2d-camera":
if (type === TabType.Field2d) {
(renderer as Field2dRenderer).set2DCamera(message.data);
}
break;

case "edit-fov":
if (type === TabType.Field3d) {
(renderer as Field3dRenderer).setFov(message.data);
Expand Down Expand Up @@ -205,7 +211,7 @@ function processAspectRatio(aspectRatio: number | null) {
if (aspectRatio > MAX_ASPECT_RATIO) aspectRatio = MAX_ASPECT_RATIO;
if (aspectRatio < 1 / MAX_ASPECT_RATIO) aspectRatio = 1 / MAX_ASPECT_RATIO;
}
window.sendMainMessage("set-aspect-ratio", aspectRatio);
window.sendMainMessage("set-aspect-ratio", { aspectRatio, lock: type !== TabType.Field2d });
}
}

Expand All @@ -220,7 +226,13 @@ window.addEventListener("beforeunload", () => {
});

window.addEventListener("keydown", (event) => {
if (event.target !== document.body) return;
if (
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement ||
(event.target instanceof HTMLElement && event.target.isContentEditable)
) {
return;
}
switch (event.code) {
case "Space":
event.preventDefault();
Expand Down
12 changes: 12 additions & 0 deletions src/shared/Field2dCameraMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) 2021-2026 Littleton Robotics
// http://github.com/Mechanical-Advantage
//
// Use of this source code is governed by a BSD
// license that can be found in the LICENSE file
// at the root directory of this project.

export enum Field2dCameraMode {
Unlocked = 0,
Robot = 1,
RobotAndRotation = 2
}
10 changes: 9 additions & 1 deletion src/shared/renderers/ConsoleRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,15 @@ export default class ConsoleRenderer implements TabRenderer {

// Select filter
window.addEventListener("keydown", (event) => {
if (root === null || root.hidden || (event.target !== document.body && event.target !== window)) return;
if (
root === null ||
root.hidden ||
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement ||
(event.target instanceof HTMLElement && event.target.isContentEditable)
) {
return;
}
if ((window.platform === "darwin" ? event.metaKey : event.ctrlKey) && event.key === "f") {
this.FILTER_INPUT.select();
}
Expand Down
Loading