Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a visual roadrunner path editor #134

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import com.acmerobotics.dashboard.message.redux.ReceiveConfig;
import com.acmerobotics.dashboard.message.redux.ReceiveTelemetry;
import com.acmerobotics.dashboard.message.redux.SaveConfig;
import com.acmerobotics.dashboard.message.redux.UploadPath;
import com.acmerobotics.dashboard.path.reflection.FieldProvider;
import com.acmerobotics.dashboard.telemetry.TelemetryPacket;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
Expand Down Expand Up @@ -42,6 +44,10 @@ public class DashboardCore {

private final Mutex<CustomVariable> configRoot = new Mutex<>(new CustomVariable());

private final Object pathLock = new Object();
public ArrayList<FieldProvider> pathFields = new ArrayList<>(); // guarded by pathLock


// NOTE: Helps to have this here for testing
public static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(Message.class, new MessageDeserializer())
Expand Down Expand Up @@ -124,13 +130,14 @@ public boolean onMessage(Message message) {
return true;
}
case SAVE_CONFIG: {
withConfigRoot(new CustomVariableConsumer() {
@Override
public void accept(CustomVariable configRoot) {
configRoot.update(((SaveConfig) message).getConfigDiff());
}
});

withConfigRoot(configRoot -> configRoot.update(((SaveConfig) message).getConfigDiff()));
return true;
}
case UPLOAD_PATH: {
synchronized (pathLock) {
for (FieldProvider field : pathFields)
field.set((UploadPath) message);
}
return true;
}
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.acmerobotics.dashboard.message.redux.SaveConfig;
import com.acmerobotics.dashboard.message.redux.StartOpMode;
import com.acmerobotics.dashboard.message.redux.StopOpMode;
import com.acmerobotics.dashboard.message.redux.UploadPath;

/**
* Dashboard message types. These values match the corresponding Redux actions in the frontend.
Expand Down Expand Up @@ -39,8 +40,10 @@ public enum MessageType {
RECEIVE_IMAGE(ReceiveImage.class),

/* gamepad */
RECEIVE_GAMEPAD_STATE(ReceiveGamepadState.class);
RECEIVE_GAMEPAD_STATE(ReceiveGamepadState.class),

/* path */
UPLOAD_PATH(UploadPath.class);
final Class<? extends Message> msgClass;

MessageType(Class<? extends Message> msgClass) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.acmerobotics.dashboard.message.redux;

import com.acmerobotics.dashboard.path.PathSegment;
import com.acmerobotics.dashboard.message.Message;
import com.acmerobotics.dashboard.message.MessageType;

import java.util.Arrays;
import java.util.List;

public class UploadPath extends Message {
public PathSegment start;
public PathSegment[] segments;

public UploadPath(PathSegment start, PathSegment[] segments) {
super(MessageType.UPLOAD_PATH);

this.start = start;
this.segments = segments;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.acmerobotics.dashboard.path;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DashboardPath {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.acmerobotics.dashboard.path;

import com.google.gson.annotations.SerializedName;

public enum HeadingType {
@SerializedName("Tangent")
TANGENT,
@SerializedName("Constant")
CONSTANT,
@SerializedName("Linear")
LINEAR,
@SerializedName("Spline")
SPLINE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.acmerobotics.dashboard.path;

public class PathSegment {
public SegmentType type;
public double x;
public double y;
public double tangent;
public double time;
public double heading;
public HeadingType headingType;

public PathSegment() {
this(SegmentType.SPLINE, 0, 0, 0, 0, 0, HeadingType.TANGENT);
System.out.println("PathSegment Noarg constructor");
}

public PathSegment(SegmentType type, double x, double y, double tangent, double time, double heading, HeadingType headingType) {
this.type = type;
this.x = x;
this.y = y;
this.tangent = tangent;
this.time = time;
this.heading = heading;
this.headingType = headingType;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.acmerobotics.dashboard.path;

import com.google.gson.annotations.SerializedName;

public enum SegmentType {
@SerializedName("Line")
LINE(),
@SerializedName("Spline")
SPLINE(),
@SerializedName("Wait")
WAIT()

// final Class<? extends Message> msgClass;
//
// MessageType(Class<? extends Message> msgClass) {
// this.msgClass = msgClass;
// }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.acmerobotics.dashboard.path.reflection;

import com.acmerobotics.dashboard.config.ValueProvider;

import java.lang.reflect.Field;

/**
* Value provider backed by a class field.
* @param <T> type of the class field
*/
public class FieldProvider<T> implements ValueProvider<T> {
private final Field field;
private final Object parent;

public FieldProvider(Field field, Object parent) {
this.field = field;
this.parent = parent;
}

@SuppressWarnings("unchecked")
@Override
public T get() {
try {
return (T) field.get(parent);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}

@Override
public void set(T value) {
try {
field.set(parent, value);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.acmerobotics.dashboard.path.reflection;

import com.acmerobotics.dashboard.path.DashboardPath;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Set;

public class ReflectionPath {
private ReflectionPath() {}

}
6 changes: 6 additions & 0 deletions FtcDashboard/dash/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@
"@headlessui/react": "^1.7.2",
"@tailwindcss/forms": "^0.5.3",
"clsx": "^1.2.1",
"konva": "^8.4.2",
"lodash": "4.17.21",
"prop-types": "15.7.2",
"react": "18.2.0",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.5",
"react-grid-layout": "^1.3.4",
"react-konva": "^18.2.5",
"react-redux": "^8.0.4",
"react-resizable": "1.11.1",
"react-virtualized-auto-sizer": "^1.0.7",
"react-window": "^1.8.7",
"redux": "4.2.0",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.4.1",
"use-image": "^1.0.12",
"uuid": "^9.0.0"
},
"scripts": {
Expand Down
5 changes: 4 additions & 1 deletion FtcDashboard/dash/src/assets/icons/delete.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion FtcDashboard/dash/src/assets/icons/file_download.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 15 additions & 9 deletions FtcDashboard/dash/src/components/views/BaseView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import clsx from 'clsx';
import { forwardRef, PropsWithChildren } from 'react';
import { ForwardedRef, forwardRef, PropsWithChildren } from 'react';

type BaseViewProps = PropsWithChildren<{
isUnlocked?: boolean;
Expand Down Expand Up @@ -47,15 +47,21 @@ const BaseViewHeading = ({
</h2>
);

const BaseViewBody = ({
children,
className,
...props
}: JSX.IntrinsicElements['div']) => (
<div className={`flex-1 overflow-auto px-4 ${className}`} {...props}>
{children}
</div>
const BaseViewBody = forwardRef(
(
{ children, className, ...props }: JSX.IntrinsicElements['div'],
ref: ForwardedRef<HTMLDivElement>,
) => (
<div
ref={ref}
className={`flex-1 overflow-auto px-4 ${className}`}
{...props}
>
{children}
</div>
),
);
BaseViewBody.displayName = 'BaseViewBody';

const BaseViewIcons = ({
className,
Expand Down
101 changes: 101 additions & 0 deletions FtcDashboard/dash/src/components/views/PathView/PathSegment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import PropTypes from 'prop-types';

import { segmentTypes, headingTypes, SegmentData } from '@/store/types';

import PointInput from './inputs/PointInput';
import AngleInput from './inputs/AngleInput';

const PathSegment = ({
data,
onChange,
}: {
data: SegmentData;
onChange: (val: Partial<SegmentData>) => void;
}) => (
<li className="my-4 pl-2">
<div className="mb-2 flex gap-2">
<select
className="valid flex-grow rounded py-0 dark:border-slate-500/80 dark:bg-slate-700 dark:text-slate-200"
value={data.type}
onChange={(e) =>
onChange({
type: e.target.value as typeof segmentTypes[number],
})
}
>
{segmentTypes.map((enumValue) => (
<option key={enumValue} value={enumValue}>
{enumValue}
</option>
))}
</select>
{data.type === 'Wait' ? (
<>
<div className="self-center">for</div>
<input
type="number"
min={0}
step={0.5}
value={data.time}
onChange={(evt) => onChange({ time: +evt.target.value })}
className="h-8 w-16 p-2 dark:border-slate-500/80 dark:bg-slate-700 dark:text-slate-200"
title="Time in Seconds"
/>
</>
) : (
<>
<div className="self-center">to</div>
<PointInput
valueX={data.x}
valueY={data.y}
onChange={(newVals) => onChange(newVals)}
/>
</>
)}
</div>
{data.type === 'Spline' && (
<div className="mb-2 flex gap-2 self-center">
<div className="flex-grow self-center">End Tangent:</div>
<AngleInput
name="tangent"
value={data.tangent}
onChange={(newVals) => onChange(newVals)}
/>
</div>
)}
{data.type !== 'Wait' && (
<div className="mb-2 flex gap-2 self-center">
<div className="self-center">Heading:</div>
<select
className="valid h-8 flex-grow rounded py-0 dark:border-slate-500/80 dark:bg-slate-700 dark:text-slate-200"
value={data.headingType}
onChange={(e) =>
onChange({
headingType: e.target.value as typeof headingTypes[number],
})
}
>
{headingTypes.map((enumValue) => (
<option key={enumValue} value={enumValue}>
{enumValue}
</option>
))}
</select>
{headingTypes.slice(2).includes(data.headingType) && (
<AngleInput
name="heading"
value={data.heading}
onChange={(newVals) => onChange(newVals)}
/>
)}
</div>
)}
</li>
);

PathSegment.propTypes = {
data: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
};

export default PathSegment;
Loading