From 80cfcc0f830b3f89549f0e5d9e32d690add39109 Mon Sep 17 00:00:00 2001 From: Nash Pillai Date: Sun, 26 Feb 2023 15:44:01 -0500 Subject: [PATCH 1/6] it builds --- .../acmerobotics/dashboard/DashboardCore.java | 21 +- .../dashboard/message/MessageType.java | 5 +- .../dashboard/message/redux/UploadPath.java | 20 ++ .../dashboard/path/DashboardPath.java | 10 + .../dashboard/path/HeadingType.java | 14 ++ .../dashboard/path/PathSegment.java | 26 +++ .../dashboard/path/SegmentType.java | 18 ++ .../path/reflection/FieldProvider.java | 38 ++++ .../path/reflection/ReflectionPath.java | 13 ++ FtcDashboard/dash/package.json | 3 + .../components/views/PathView/PathSegment.tsx | 208 ++++++++++++++++++ .../views/PathView/PathSegmentView.tsx | 153 +++++++++++++ .../components/views/PathView/PathView.jsx | 63 ++++++ FtcDashboard/dash/src/enums/LayoutPreset.tsx | 22 ++ FtcDashboard/dash/src/index.css | 12 + FtcDashboard/dash/src/store/actions/path.ts | 6 + .../src/store/middleware/socketMiddleware.ts | 1 + FtcDashboard/dash/src/store/reducers/index.ts | 2 + FtcDashboard/dash/src/store/reducers/path.ts | 39 ++++ FtcDashboard/dash/src/store/types/index.ts | 3 + FtcDashboard/dash/src/store/types/path.ts | 16 ++ FtcDashboard/dash/vite.config.ts | 4 + FtcDashboard/dash/yarn.lock | 40 ++-- build.gradle | 2 +- 24 files changed, 716 insertions(+), 23 deletions(-) create mode 100644 DashboardCore/src/main/java/com/acmerobotics/dashboard/message/redux/UploadPath.java create mode 100644 DashboardCore/src/main/java/com/acmerobotics/dashboard/path/DashboardPath.java create mode 100644 DashboardCore/src/main/java/com/acmerobotics/dashboard/path/HeadingType.java create mode 100644 DashboardCore/src/main/java/com/acmerobotics/dashboard/path/PathSegment.java create mode 100644 DashboardCore/src/main/java/com/acmerobotics/dashboard/path/SegmentType.java create mode 100644 DashboardCore/src/main/java/com/acmerobotics/dashboard/path/reflection/FieldProvider.java create mode 100644 DashboardCore/src/main/java/com/acmerobotics/dashboard/path/reflection/ReflectionPath.java create mode 100644 FtcDashboard/dash/src/components/views/PathView/PathSegment.tsx create mode 100644 FtcDashboard/dash/src/components/views/PathView/PathSegmentView.tsx create mode 100644 FtcDashboard/dash/src/components/views/PathView/PathView.jsx create mode 100644 FtcDashboard/dash/src/store/actions/path.ts create mode 100644 FtcDashboard/dash/src/store/reducers/path.ts create mode 100644 FtcDashboard/dash/src/store/types/path.ts diff --git a/DashboardCore/src/main/java/com/acmerobotics/dashboard/DashboardCore.java b/DashboardCore/src/main/java/com/acmerobotics/dashboard/DashboardCore.java index 80d2f7d3a..9595efc0e 100644 --- a/DashboardCore/src/main/java/com/acmerobotics/dashboard/DashboardCore.java +++ b/DashboardCore/src/main/java/com/acmerobotics/dashboard/DashboardCore.java @@ -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; @@ -41,6 +43,10 @@ public class DashboardCore { private final Object configLock = new Object(); private CustomVariable configRoot = new CustomVariable(); // guarded by configLock + private final Object pathLock = new Object(); + private ArrayList pathFields; // guarded by pathLock + + // TODO: this doensn't make a ton of sense here, though it needs to go in this module for testing public static final Gson GSON = new GsonBuilder() .registerTypeAdapter(Message.class, new MessageDeserializer()) @@ -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: diff --git a/DashboardCore/src/main/java/com/acmerobotics/dashboard/message/MessageType.java b/DashboardCore/src/main/java/com/acmerobotics/dashboard/message/MessageType.java index fa35e441d..f7c376c07 100644 --- a/DashboardCore/src/main/java/com/acmerobotics/dashboard/message/MessageType.java +++ b/DashboardCore/src/main/java/com/acmerobotics/dashboard/message/MessageType.java @@ -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. @@ -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 msgClass; MessageType(Class msgClass) { diff --git a/DashboardCore/src/main/java/com/acmerobotics/dashboard/message/redux/UploadPath.java b/DashboardCore/src/main/java/com/acmerobotics/dashboard/message/redux/UploadPath.java new file mode 100644 index 000000000..9de770517 --- /dev/null +++ b/DashboardCore/src/main/java/com/acmerobotics/dashboard/message/redux/UploadPath.java @@ -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 List segments; + + public UploadPath(PathSegment start, PathSegment[] segments) { + super(MessageType.UPLOAD_PATH); + + this.start = start; + this.segments = Arrays.asList(segments); + } +} \ No newline at end of file diff --git a/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/DashboardPath.java b/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/DashboardPath.java new file mode 100644 index 000000000..5bfb7f6ab --- /dev/null +++ b/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/DashboardPath.java @@ -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 {} \ No newline at end of file diff --git a/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/HeadingType.java b/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/HeadingType.java new file mode 100644 index 000000000..96ab20103 --- /dev/null +++ b/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/HeadingType.java @@ -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 +} \ No newline at end of file diff --git a/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/PathSegment.java b/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/PathSegment.java new file mode 100644 index 000000000..c4269490e --- /dev/null +++ b/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/PathSegment.java @@ -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; + } +} \ No newline at end of file diff --git a/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/SegmentType.java b/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/SegmentType.java new file mode 100644 index 000000000..85fc55b5d --- /dev/null +++ b/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/SegmentType.java @@ -0,0 +1,18 @@ +package com.acmerobotics.dashboard.path; + +import com.google.gson.annotations.SerializedName; + +public enum SegmentType { + @SerializedName("Line") + LINE(), + @SerializedName("Spine") + SPLINE(), + @SerializedName("Wait") + WAIT() + +// final Class msgClass; +// +// MessageType(Class msgClass) { +// this.msgClass = msgClass; +// } +} \ No newline at end of file diff --git a/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/reflection/FieldProvider.java b/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/reflection/FieldProvider.java new file mode 100644 index 000000000..3846c5e33 --- /dev/null +++ b/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/reflection/FieldProvider.java @@ -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 type of the class field + */ +public class FieldProvider implements ValueProvider { + 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); + } + } +} \ No newline at end of file diff --git a/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/reflection/ReflectionPath.java b/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/reflection/ReflectionPath.java new file mode 100644 index 000000000..ee65b504c --- /dev/null +++ b/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/reflection/ReflectionPath.java @@ -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() {} + +} \ No newline at end of file diff --git a/FtcDashboard/dash/package.json b/FtcDashboard/dash/package.json index 968e3bb49..9791cd6da 100644 --- a/FtcDashboard/dash/package.json +++ b/FtcDashboard/dash/package.json @@ -6,11 +6,14 @@ "@headlessui/react": "^1.7.2", "@tailwindcss/forms": "^0.5.3", "clsx": "^1.2.1", + "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-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", diff --git a/FtcDashboard/dash/src/components/views/PathView/PathSegment.tsx b/FtcDashboard/dash/src/components/views/PathView/PathSegment.tsx new file mode 100644 index 000000000..51dd839db --- /dev/null +++ b/FtcDashboard/dash/src/components/views/PathView/PathSegment.tsx @@ -0,0 +1,208 @@ +import React, { createRef, MouseEventHandler, useState } from 'react'; +import PropTypes from 'prop-types'; + +import { segmentTypes, headingTypes, SegmentData } from '@/store/types'; + +export const PointInput = ({ + valueX, + valueY, + onChange, +}: { + valueX: number; + valueY: number; + onChange: (_: Partial) => void; +}) => ( + <> + onChange({ x: +evt.target.value })} + className="w-16 h-8 p-2" + title="x-coordinate in inches" + /> + onChange({ y: +evt.target.value })} + className="w-16 h-8 p-2" + title="y-coordinate in inches" + /> + +); + +export function AngleInput({ + value, + name, + onChange, +}: { + value: number; + name: string; + onChange: (_: Partial) => void; +}) { + const [isPickingAngle, setIsPickingAngle] = useState(false); + const angleSelecter = createRef(); + const pickAngle: MouseEventHandler = (e) => { + e.preventDefault(); + e.persist(); + setIsPickingAngle(true); + + if (!angleSelecter.current) return; + const a = angleSelecter.current; + + a.style.left = e.pageX - 32 + 'px'; + a.style.top = e.pageY - 32 + 'px'; + + window.addEventListener('mousemove', ({ x, y }) => + requestAnimationFrame(() => { + const angle = (Math.atan2(y - e.pageY, x - e.pageX) * 180) / Math.PI; + a.parentElement?.style.setProperty('--angle', angle.toFixed()); + }), + ); + }; + + return ( + <> + onChange({ [name]: +e.target.value })} + className="w-16 h-8 p-2" + title={`${name} in degrees`} + onContextMenu={pickAngle} + /> +
{ + onChange({ + [name]: + (+getComputedStyle(e.target as HTMLElement).getPropertyValue( + '--angle', + ) + + 360) % + 360, + }); + setIsPickingAngle(false); + }} + onContextMenu={(e) => { + setIsPickingAngle(false); + e.preventDefault(); + }} + > +
+
+ + ); +} + +const PathSegment = ({ + index, + data, + onChange, +}: { + index: number; + data: SegmentData; + onChange: (i: number, val: Partial) => void; +}) => ( +
  • +
    + + {data.type === 'Wait' ? ( + <> +
    for
    + onChange(index, { time: +evt.target.value })} + className="w-16 h-8 p-2" + title="Time in Seconds" + /> + + ) : ( + <> +
    to
    + onChange(index, newVals)} + /> + + )} +
    + {data.type === 'Spline' && ( +
    +
    End Tangent:
    + onChange(index, newVals)} + /> +
    + )} + {data.type !== 'Wait' && ( +
    +
    Heading:
    + + {headingTypes.slice(2).includes(data.headingType) && ( + onChange(index, newVals)} + /> + )} +
    + )} +
  • +); + +PathSegment.propTypes = { + index: PropTypes.number.isRequired, + data: PropTypes.object.isRequired, + onChange: PropTypes.func.isRequired, +}; + +export default PathSegment; diff --git a/FtcDashboard/dash/src/components/views/PathView/PathSegmentView.tsx b/FtcDashboard/dash/src/components/views/PathView/PathSegmentView.tsx new file mode 100644 index 000000000..5640ca58c --- /dev/null +++ b/FtcDashboard/dash/src/components/views/PathView/PathSegmentView.tsx @@ -0,0 +1,153 @@ +import React, { useState } from 'react'; + +import BaseView, { + BaseViewHeading, + BaseViewBody, + BaseViewProps, + BaseViewHeadingProps, + BaseViewIconButton, + BaseViewIcons, +} from '@/components/views/BaseView'; +import { ReactComponent as AddIcon } from '@/assets/icons/add.svg'; +import { ReactComponent as SaveIcon } from '@/assets/icons/save.svg'; +import { ReactComponent as DownloadIcon } from '@/assets/icons/file_download.svg'; +import { ReactComponent as DeleteIcon } from '@/assets/icons/delete.svg'; +import PathSegment, { PointInput, AngleInput } from '@/components/views/PathView/PathSegment'; +import { SegmentData } from '@/store/types'; +import { useDispatch } from 'react-redux'; +import { uploadPathAction } from '@/store/actions/path'; + +type PathSegmentViewProps = BaseViewProps & BaseViewHeadingProps; + +const exportPath = ( + start: Omit, + segments: SegmentData[], +) => `--- +startPose: + x: ${start.x} + y: ${start.y} + heading: ${start.heading} +startTangent: ${start.tangent} +waypoints: +${segments + .map( + (segment) => `- position: + x: ${segment.x} + y: ${segment.y} + interpolationType: "${segment.headingType.toUpperCase()}" + heading: ${segment.heading} + tangent: ${segment.tangent} + type: ${segment.type}`, + ) + .join('\n')} +resolution: 0.25 +version: 1 +`; + +const PathSegmentView = ({ + isDraggable = false, + isUnlocked = false, +}: PathSegmentViewProps) => { + const dispatch = useDispatch(); + const [startPose, setStartPose] = useState({ + x: 0, + y: 0, + tangent: 0, + heading: 0, + }); + const [segments, setSegments] = useState([] as SegmentData[]); + const changeSegment = (i: number, val: Partial) => + setSegments((prev) => { + Object.assign(prev[i], val); + return [...prev]; + }); + return ( + +
    + + Path Segments + + + setSegments([])}> + + + console.log(exportPath(startPose, segments))} + > + + + dispatch(uploadPathAction(startPose, segments))} + > + + + + setSegments((prev) => + prev.concat([ + { + type: 'Spline', + x: 0, + y: 0, + tangent: 0, + time: 0, + heading: 0, + headingType: 'Tangent', + }, + ]), + ) + } + > + + + +
    + +
    +
    +
    Start at
    + + setStartPose((prev) => ({ ...prev, ...newVals })) + } + /> +
    +
    +
    Start Tangent:
    + + setStartPose((prev) => ({ ...prev, ...newVals })) + } + /> +
    +
    +
    Start Heading:
    + + setStartPose((prev) => ({ ...prev, ...newVals })) + } + /> +
    +
      + {segments.map((segment, i) => ( + + ))} +
    +
    +
    +
    + ); +}; + +export default PathSegmentView; diff --git a/FtcDashboard/dash/src/components/views/PathView/PathView.jsx b/FtcDashboard/dash/src/components/views/PathView/PathView.jsx new file mode 100644 index 000000000..baebba262 --- /dev/null +++ b/FtcDashboard/dash/src/components/views/PathView/PathView.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import BaseView, { BaseViewHeading } from '@/components/views/BaseView'; +import Field from '@/components/views/FieldView/Field'; +import AutoFitCanvas from '@/components/Canvas/AutoFitCanvas'; + +class PathView extends React.Component { + constructor(props) { + super(props); + + this.canvasRef = React.createRef(); + + this.renderField = this.renderField.bind(this); + } + + componentDidMount() { + this.field = new Field(this.canvasRef.current); + this.renderField(); + } + + componentDidUpdate() { + this.field.setOverlay(this.props.overlay); + this.renderField(); + } + + renderField() { + if (this.field) { + this.field.render(); + } + } + + render() { + return ( + + + Draw a Path + + + + ); + } +} + +PathView.propTypes = { + overlay: PropTypes.shape({ + ops: PropTypes.array.isRequired, + }).isRequired, + + isDraggable: PropTypes.bool, + isUnlocked: PropTypes.bool, +}; + +const mapStateToProps = ({ telemetry }) => ({ + overlay: telemetry[telemetry.length - 1].fieldOverlay, +}); + +export default connect(mapStateToProps)(PathView); diff --git a/FtcDashboard/dash/src/enums/LayoutPreset.tsx b/FtcDashboard/dash/src/enums/LayoutPreset.tsx index cbdf60ef1..2c43f756e 100644 --- a/FtcDashboard/dash/src/enums/LayoutPreset.tsx +++ b/FtcDashboard/dash/src/enums/LayoutPreset.tsx @@ -10,10 +10,13 @@ import GraphView from '@/components/views/GraphView/GraphView'; import ConfigView from '@/components/views/ConfigView/ConfigView'; import TelemetryView from '@/components/views/TelemetryView'; import FieldView from '@/components/views/FieldView/FieldView'; +import PathView from '@/components/views/PathView/PathView'; +import PathSegmentView from '@/components/views/PathView/PathSegmentView'; const LayoutPreset = { DEFAULT: 'DEFAULT', FIELD: 'FIELD', + PATH: 'PATH', GRAPH: 'GRAPH', ORIGINAL: 'ORIGINAL', CONFIGURABLE: 'CONFIGURABLE', @@ -69,6 +72,25 @@ const LAYOUT_DETAILS: { [key in Values]: Layout } = { ), }, + [LayoutPreset.PATH]: { + name: 'Path', + content: ( + + + + + + + + + + + + + + + ) + }, [LayoutPreset.GRAPH]: { name: 'Graph', content: ( diff --git a/FtcDashboard/dash/src/index.css b/FtcDashboard/dash/src/index.css index a05a6b015..e10308277 100644 --- a/FtcDashboard/dash/src/index.css +++ b/FtcDashboard/dash/src/index.css @@ -122,3 +122,15 @@ input { select { background-position: right 0.15rem center; } + +.direction-selector { + background-image: conic-gradient( + from 90deg, + blue 5%, + #e2e8f0 5.5%, + #e2e8f0 94.5%, + blue 95% + ); + mask-image: radial-gradient(transparent 1rem, #000 1.1rem); + transform: /*translate(-50%, -50%)*/ rotate(calc(var(--angle) * 1deg)); +} \ No newline at end of file diff --git a/FtcDashboard/dash/src/store/actions/path.ts b/FtcDashboard/dash/src/store/actions/path.ts new file mode 100644 index 000000000..5a4ba81f1 --- /dev/null +++ b/FtcDashboard/dash/src/store/actions/path.ts @@ -0,0 +1,6 @@ +import { SegmentData } from '../types/path'; + +export const uploadPathAction = ( + start: Omit, + segments: SegmentData[], +) => ({ type: 'UPLOAD_PATH', start, segments }); \ No newline at end of file diff --git a/FtcDashboard/dash/src/store/middleware/socketMiddleware.ts b/FtcDashboard/dash/src/store/middleware/socketMiddleware.ts index 4a81de103..18b2f5ef2 100644 --- a/FtcDashboard/dash/src/store/middleware/socketMiddleware.ts +++ b/FtcDashboard/dash/src/store/middleware/socketMiddleware.ts @@ -80,6 +80,7 @@ const socketMiddleware: Middleware, RootState> = break; } // messages forwarded to the server + case 'UPLOAD_PATH': case RECEIVE_GAMEPAD_STATE: case GET_ROBOT_STATUS: case 'SAVE_CONFIG': diff --git a/FtcDashboard/dash/src/store/reducers/index.ts b/FtcDashboard/dash/src/store/reducers/index.ts index dea48f86c..245f05e02 100644 --- a/FtcDashboard/dash/src/store/reducers/index.ts +++ b/FtcDashboard/dash/src/store/reducers/index.ts @@ -8,6 +8,7 @@ import statusReducer from './status'; import cameraReducer from './camera'; import settingsReducer from './settings'; import gamepadReducer from './gamepad'; +import pathReducer from './path'; import { createDispatchHook } from 'react-redux'; const rootReducer = combineReducers({ @@ -18,6 +19,7 @@ const rootReducer = combineReducers({ camera: cameraReducer, settings: settingsReducer, gamepad: gamepadReducer, + path: pathReducer, }); export type RootState = ReturnType; diff --git a/FtcDashboard/dash/src/store/reducers/path.ts b/FtcDashboard/dash/src/store/reducers/path.ts new file mode 100644 index 000000000..f12383d51 --- /dev/null +++ b/FtcDashboard/dash/src/store/reducers/path.ts @@ -0,0 +1,39 @@ +import { SegmentData } from '../types'; +import { uploadPathAction } from '../actions/path'; + +const initialState = { + start: {} as Omit, + segments: [] as SegmentData[], +}; + +const pathReducer = ( + state = initialState, + action: ReturnType, +) => { + switch (action.type) { + // case RECEIVE_CONFIG: + // return { + // ...state, + // configRoot: receiveConfig(state.configRoot, action.configRoot), + // }; + // case UPDATE_CONFIG: + // return { + // ...state, + // configRoot: updateConfig(state.configRoot, action.configDiff, true), + // }; + // case SAVE_CONFIG: + // return { + // ...state, + // configRoot: updateConfig(state.configRoot, action.configDiff, false), + // }; + // case REFRESH_CONFIG: + // return { + // ...state, + // configRoot: refreshConfig(state.configRoot), + // }; + default: + return action; + } +}; + +export default pathReducer; \ No newline at end of file diff --git a/FtcDashboard/dash/src/store/types/index.ts b/FtcDashboard/dash/src/store/types/index.ts index 0f1594a51..0342af5db 100644 --- a/FtcDashboard/dash/src/store/types/index.ts +++ b/FtcDashboard/dash/src/store/types/index.ts @@ -82,3 +82,6 @@ export type { TelemetryItem, ReceiveTelemetryAction, } from './telemetry'; + +export { segmentTypes, headingTypes } from './path'; +export type { SegmentData } from './path'; \ No newline at end of file diff --git a/FtcDashboard/dash/src/store/types/path.ts b/FtcDashboard/dash/src/store/types/path.ts new file mode 100644 index 000000000..140523701 --- /dev/null +++ b/FtcDashboard/dash/src/store/types/path.ts @@ -0,0 +1,16 @@ +export const segmentTypes = ['Line', 'Spline', 'Wait'] as const; +export const headingTypes = [ + 'Tangent', + 'Constant', + 'Linear', + 'Spline', +] as const; +export type SegmentData = { + type: typeof segmentTypes[number]; + x: number; + y: number; + tangent: number; + time: number; + heading: number; + headingType: typeof headingTypes[number]; +}; \ No newline at end of file diff --git a/FtcDashboard/dash/vite.config.ts b/FtcDashboard/dash/vite.config.ts index 78718c2df..eeb0068ed 100644 --- a/FtcDashboard/dash/vite.config.ts +++ b/FtcDashboard/dash/vite.config.ts @@ -6,6 +6,10 @@ import path from 'path'; export default defineConfig({ base: '/dash/', plugins: [react(), svgr()], + server: { + port: 8000, + hmr: { host: 'localhost' }, + }, resolve: { alias: { '@': path.resolve(__dirname, './src'), diff --git a/FtcDashboard/dash/yarn.lock b/FtcDashboard/dash/yarn.lock index 23ba8220c..78ae89130 100644 --- a/FtcDashboard/dash/yarn.lock +++ b/FtcDashboard/dash/yarn.lock @@ -999,7 +999,7 @@ client-only@^0.0.1: clsx@^1.1.1, clsx@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== color-convert@^1.9.0: @@ -1270,7 +1270,7 @@ esbuild-darwin-64@0.15.15: esbuild-darwin-arm64@0.15.15: version "0.15.15" - resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.15.tgz" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.15.tgz#e922ec387c00fa84d664e14b5722fe13613f4adc" integrity sha512-P8jOZ5zshCNIuGn+9KehKs/cq5uIniC+BeCykvdVhx/rBXSxmtj3CUIKZz4sDCuESMbitK54drf/2QX9QHG5Ag== esbuild-freebsd-64@0.15.15: @@ -1290,7 +1290,7 @@ esbuild-linux-32@0.15.15: esbuild-linux-64@0.15.15: version "0.15.15" - resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.15.15.tgz#11a430a86403b0411ca0a355b891f1cb8c4c4ec6" + resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.15.tgz" integrity sha512-217KPmWMirkf8liO+fj2qrPwbIbhNTGNVtvqI1TnOWJgcMjUWvd677Gq3fTzXEjilkx2yWypVnTswM2KbXgoAg== esbuild-linux-arm64@0.15.15: @@ -1756,7 +1756,7 @@ fs.realpath@^1.0.0: fsevents@~2.3.2: version "2.3.2" - resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== function-bind@^1.1.1: @@ -2247,9 +2247,9 @@ lodash.merge@^4.6.2: resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.2.1: +lodash@4.17.21, lodash@^4.2.1: version "4.17.21" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== log-update@^4.0.0: @@ -2338,16 +2338,11 @@ ms@2.0.0: resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== -ms@2.1.2: +ms@2.1.2, ms@^2.1.1: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.1: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - nanoid@^3.3.4: version "3.3.4" resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz" @@ -2646,6 +2641,15 @@ prettier@^2.8.0: resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.0.tgz" integrity sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA== +prop-types@15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + prop-types@15.x, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" @@ -2697,9 +2701,9 @@ react-grid-layout@^1.3.4: react-draggable "^4.0.0" react-resizable "^3.0.4" -react-is@^16.13.1, react-is@^16.7.0: +react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" - resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== react-is@^18.0.0: @@ -2724,6 +2728,14 @@ react-refresh@^0.14.0: resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz" integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== +react-resizable@1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.11.1.tgz#02ca6850afa7a22c1b3e623e64aef71ee252af69" + integrity sha512-S70gbLaAYqjuAd49utRHibtHLrHXInh7GuOR+6OO6RO6uleQfuBnWmZjRABfqNEx3C3Z6VPLg0/0uOYFrkfu9Q== + dependencies: + prop-types "15.x" + react-draggable "^4.0.3" + react-resizable@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.4.tgz" diff --git a/build.gradle b/build.gradle index fd7c8ea22..968755f99 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ */ buildscript { - ext.dashboard_version = '0.4.8' + ext.dashboard_version = '0.4.8-SNAPSHOT' ext.checkstyle_version = '8.18' repositories { From bba3dfd06c799763504e35a4029c053da71e5f42 Mon Sep 17 00:00:00 2001 From: Nash Pillai Date: Sun, 26 Feb 2023 18:22:29 -0500 Subject: [PATCH 2/6] visual path editor (with vim keys) --- .../dashboard/message/redux/UploadPath.java | 4 +- .../dashboard/path/SegmentType.java | 2 +- FtcDashboard/dash/package.json | 3 + .../components/views/PathView/PathSegment.tsx | 141 +----- .../views/PathView/PathSegmentView.tsx | 109 +++-- .../components/views/PathView/PathView.jsx | 63 --- .../components/views/PathView/PathView.tsx | 428 ++++++++++++++++++ .../views/PathView/inputs/AngleInput.tsx | 72 +++ .../views/PathView/inputs/PointInput.tsx | 37 ++ FtcDashboard/dash/src/store/actions/path.ts | 31 +- FtcDashboard/dash/src/store/reducers/path.ts | 71 +-- FtcDashboard/dash/src/store/types/index.ts | 3 +- FtcDashboard/dash/src/store/types/path.ts | 7 +- .../dash/src/store/types/telemetry.ts | 4 +- FtcDashboard/dash/yarn.lock | 44 +- 15 files changed, 735 insertions(+), 284 deletions(-) delete mode 100644 FtcDashboard/dash/src/components/views/PathView/PathView.jsx create mode 100644 FtcDashboard/dash/src/components/views/PathView/PathView.tsx create mode 100644 FtcDashboard/dash/src/components/views/PathView/inputs/AngleInput.tsx create mode 100644 FtcDashboard/dash/src/components/views/PathView/inputs/PointInput.tsx diff --git a/DashboardCore/src/main/java/com/acmerobotics/dashboard/message/redux/UploadPath.java b/DashboardCore/src/main/java/com/acmerobotics/dashboard/message/redux/UploadPath.java index 9de770517..de1c80cf0 100644 --- a/DashboardCore/src/main/java/com/acmerobotics/dashboard/message/redux/UploadPath.java +++ b/DashboardCore/src/main/java/com/acmerobotics/dashboard/message/redux/UploadPath.java @@ -9,12 +9,12 @@ public class UploadPath extends Message { public PathSegment start; - public List segments; + public PathSegment[] segments; public UploadPath(PathSegment start, PathSegment[] segments) { super(MessageType.UPLOAD_PATH); this.start = start; - this.segments = Arrays.asList(segments); + this.segments = segments; } } \ No newline at end of file diff --git a/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/SegmentType.java b/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/SegmentType.java index 85fc55b5d..f0be0b05b 100644 --- a/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/SegmentType.java +++ b/DashboardCore/src/main/java/com/acmerobotics/dashboard/path/SegmentType.java @@ -5,7 +5,7 @@ public enum SegmentType { @SerializedName("Line") LINE(), - @SerializedName("Spine") + @SerializedName("Spline") SPLINE(), @SerializedName("Wait") WAIT() diff --git a/FtcDashboard/dash/package.json b/FtcDashboard/dash/package.json index 9791cd6da..80995d836 100644 --- a/FtcDashboard/dash/package.json +++ b/FtcDashboard/dash/package.json @@ -6,12 +6,14 @@ "@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", @@ -19,6 +21,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": { diff --git a/FtcDashboard/dash/src/components/views/PathView/PathSegment.tsx b/FtcDashboard/dash/src/components/views/PathView/PathSegment.tsx index 51dd839db..5d8816560 100644 --- a/FtcDashboard/dash/src/components/views/PathView/PathSegment.tsx +++ b/FtcDashboard/dash/src/components/views/PathView/PathSegment.tsx @@ -1,131 +1,25 @@ -import React, { createRef, MouseEventHandler, useState } from 'react'; import PropTypes from 'prop-types'; import { segmentTypes, headingTypes, SegmentData } from '@/store/types'; -export const PointInput = ({ - valueX, - valueY, - onChange, -}: { - valueX: number; - valueY: number; - onChange: (_: Partial) => void; -}) => ( - <> - onChange({ x: +evt.target.value })} - className="w-16 h-8 p-2" - title="x-coordinate in inches" - /> - onChange({ y: +evt.target.value })} - className="w-16 h-8 p-2" - title="y-coordinate in inches" - /> - -); - -export function AngleInput({ - value, - name, - onChange, -}: { - value: number; - name: string; - onChange: (_: Partial) => void; -}) { - const [isPickingAngle, setIsPickingAngle] = useState(false); - const angleSelecter = createRef(); - const pickAngle: MouseEventHandler = (e) => { - e.preventDefault(); - e.persist(); - setIsPickingAngle(true); - - if (!angleSelecter.current) return; - const a = angleSelecter.current; - - a.style.left = e.pageX - 32 + 'px'; - a.style.top = e.pageY - 32 + 'px'; - - window.addEventListener('mousemove', ({ x, y }) => - requestAnimationFrame(() => { - const angle = (Math.atan2(y - e.pageY, x - e.pageX) * 180) / Math.PI; - a.parentElement?.style.setProperty('--angle', angle.toFixed()); - }), - ); - }; - - return ( - <> - onChange({ [name]: +e.target.value })} - className="w-16 h-8 p-2" - title={`${name} in degrees`} - onContextMenu={pickAngle} - /> -
    { - onChange({ - [name]: - (+getComputedStyle(e.target as HTMLElement).getPropertyValue( - '--angle', - ) + - 360) % - 360, - }); - setIsPickingAngle(false); - }} - onContextMenu={(e) => { - setIsPickingAngle(false); - e.preventDefault(); - }} - > -
    -
    - - ); -} +import PointInput from './inputs/PointInput'; +import AngleInput from './inputs/AngleInput'; const PathSegment = ({ - index, data, onChange, }: { - index: number; data: SegmentData; - onChange: (i: number, val: Partial) => void; + onChange: (val: Partial) => void; }) => (
  • -
    +
    - onChange(index, { - headingType: e.target.value as (typeof headingTypes)[number], + onChange({ + headingType: e.target.value as typeof headingTypes[number], }) } > @@ -191,7 +85,7 @@ const PathSegment = ({ onChange(index, newVals)} + onChange={(newVals) => onChange(newVals)} /> )}
    @@ -200,7 +94,6 @@ const PathSegment = ({ ); PathSegment.propTypes = { - index: PropTypes.number.isRequired, data: PropTypes.object.isRequired, onChange: PropTypes.func.isRequired, }; diff --git a/FtcDashboard/dash/src/components/views/PathView/PathSegmentView.tsx b/FtcDashboard/dash/src/components/views/PathView/PathSegmentView.tsx index 5640ca58c..9db02e2e5 100644 --- a/FtcDashboard/dash/src/components/views/PathView/PathSegmentView.tsx +++ b/FtcDashboard/dash/src/components/views/PathView/PathSegmentView.tsx @@ -12,10 +12,19 @@ import { ReactComponent as AddIcon } from '@/assets/icons/add.svg'; import { ReactComponent as SaveIcon } from '@/assets/icons/save.svg'; import { ReactComponent as DownloadIcon } from '@/assets/icons/file_download.svg'; import { ReactComponent as DeleteIcon } from '@/assets/icons/delete.svg'; -import PathSegment, { PointInput, AngleInput } from '@/components/views/PathView/PathSegment'; +import PathSegment from '@/components/views/PathView/PathSegment'; import { SegmentData } from '@/store/types'; -import { useDispatch } from 'react-redux'; -import { uploadPathAction } from '@/store/actions/path'; +import { useDispatch, useSelector } from 'react-redux'; +import { + addSegmentPathAction, + clearSegmentsPathAction, + setSegmentPathAction, + setStartPathAction, + uploadPathAction, +} from '@/store/actions/path'; +import PointInput from '@/components/views/PathView/inputs/PointInput'; +import AngleInput from '@/components/views/PathView/inputs/AngleInput'; +import { RootState } from '@/store/reducers'; type PathSegmentViewProps = BaseViewProps & BaseViewHeadingProps; @@ -49,18 +58,9 @@ const PathSegmentView = ({ isUnlocked = false, }: PathSegmentViewProps) => { const dispatch = useDispatch(); - const [startPose, setStartPose] = useState({ - x: 0, - y: 0, - tangent: 0, - heading: 0, - }); - const [segments, setSegments] = useState([] as SegmentData[]); - const changeSegment = (i: number, val: Partial) => - setSegments((prev) => { - Object.assign(prev[i], val); - return [...prev]; - }); + const { start, segments } = useSelector((state: RootState) => ({ + ...state.path, + })); return (
    @@ -68,78 +68,73 @@ const PathSegmentView = ({ Path Segments - setSegments([])}> - - console.log(exportPath(startPose, segments))} + onClick={() => dispatch(clearSegmentsPathAction())} > - + dispatch(uploadPathAction(startPose, segments))} + onClick={() => { + const file = new Blob([exportPath(start, segments)], { + type: 'yaml', + }), + a = document.createElement('a'), + url = URL.createObjectURL(file); + a.href = url; + a.download = 'path.yaml'; + document.body.appendChild(a); + a.click(); + setTimeout(function () { + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }, 0); + }} > - + - setSegments((prev) => - prev.concat([ - { - type: 'Spline', - x: 0, - y: 0, - tangent: 0, - time: 0, - heading: 0, - headingType: 'Tangent', - }, - ]), - ) - } + onClick={() => dispatch(uploadPathAction(start, segments))} > - + + + dispatch(addSegmentPathAction())}> +
    -
    +
    Start at
    - setStartPose((prev) => ({ ...prev, ...newVals })) - } + valueX={start.x} + valueY={start.y} + onChange={(newVals) => dispatch(setStartPathAction(newVals))} />
    -
    +
    Start Tangent:
    - setStartPose((prev) => ({ ...prev, ...newVals })) - } + onChange={(newVals) => dispatch(setStartPathAction(newVals))} />
    -
    +
    Start Heading:
    - setStartPose((prev) => ({ ...prev, ...newVals })) - } + onChange={(newVals) => dispatch(setStartPathAction(newVals))} />
    -
      +
        {segments.map((segment, i) => ( + dispatch(setSegmentPathAction(i, newVals)) + } data={segment} /> ))} diff --git a/FtcDashboard/dash/src/components/views/PathView/PathView.jsx b/FtcDashboard/dash/src/components/views/PathView/PathView.jsx deleted file mode 100644 index baebba262..000000000 --- a/FtcDashboard/dash/src/components/views/PathView/PathView.jsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import BaseView, { BaseViewHeading } from '@/components/views/BaseView'; -import Field from '@/components/views/FieldView/Field'; -import AutoFitCanvas from '@/components/Canvas/AutoFitCanvas'; - -class PathView extends React.Component { - constructor(props) { - super(props); - - this.canvasRef = React.createRef(); - - this.renderField = this.renderField.bind(this); - } - - componentDidMount() { - this.field = new Field(this.canvasRef.current); - this.renderField(); - } - - componentDidUpdate() { - this.field.setOverlay(this.props.overlay); - this.renderField(); - } - - renderField() { - if (this.field) { - this.field.render(); - } - } - - render() { - return ( - - - Draw a Path - - - - ); - } -} - -PathView.propTypes = { - overlay: PropTypes.shape({ - ops: PropTypes.array.isRequired, - }).isRequired, - - isDraggable: PropTypes.bool, - isUnlocked: PropTypes.bool, -}; - -const mapStateToProps = ({ telemetry }) => ({ - overlay: telemetry[telemetry.length - 1].fieldOverlay, -}); - -export default connect(mapStateToProps)(PathView); diff --git a/FtcDashboard/dash/src/components/views/PathView/PathView.tsx b/FtcDashboard/dash/src/components/views/PathView/PathView.tsx new file mode 100644 index 000000000..26471cf59 --- /dev/null +++ b/FtcDashboard/dash/src/components/views/PathView/PathView.tsx @@ -0,0 +1,428 @@ +import React, { + KeyboardEventHandler, + useEffect, + useRef, + useState, +} from 'react'; +import PropTypes from 'prop-types'; +import { useDispatch, useSelector } from 'react-redux'; +import fieldImageName from '@/assets/field.png'; + +import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg'; +import BaseView, { + BaseViewBody, + BaseViewHeading, + BaseViewHeadingProps, + BaseViewProps, +} from '@/components/views/BaseView'; +import { Stage, Layer, Image, Circle, Line, Text, Group } from 'react-konva'; + +import { RootState } from '@/store/reducers'; +import useImage from 'use-image'; +import { + addSegmentPathAction, + setSegmentPathAction, + setStartPathAction, + uploadPathAction, +} from '@/store/actions/path'; +import { headingTypes, DrawOp } from '@/store/types'; +import { zip } from 'lodash'; + +const mod = (val: number, base: number) => (val + base) % base; +const deg2rad = (deg: number) => mod((deg / 180) * Math.PI, 2 * Math.PI); +const rad2deg = (rad: number) => mod((rad / Math.PI) * 180, 360); + +const clamp = (min: number, val: number, max: number) => + Math.min(Math.max(min, val), max); + +type PathSegmentViewProps = BaseViewProps & BaseViewHeadingProps; + +function PathView({ isUnlocked, isDraggable }: PathSegmentViewProps) { + const dispatch = useDispatch(); + const container = useRef(null); + const [canvasSize, setCanvasSize] = useState(0); + const { + path: { start, segments }, + overlay, + } = useSelector(({ path, telemetry }: RootState) => ({ + path, + overlay: + telemetry.length === 0 + ? { ops: [] } + : telemetry[telemetry.length - 1].fieldOverlay, + })); + const points = [start].concat(segments); + const [image] = useImage(fieldImageName); + useEffect(() => { + if (container.current) + new ResizeObserver(([{ contentRect }]) => { + setCanvasSize(Math.min(contentRect.width, contentRect.height - 24)); + }).observe(container.current); + }, []); + + const [multiplier, setMultiplier] = useState(''); + const [selected, setSelected] = useState(0); + const [pickingAngle, setPickingAngle] = useState(''); + const angleSelecter = useRef(null); + const pickAngle = (name: string) => { + if (!angleSelecter.current || !container.current) return; + setPickingAngle(name); + const rect = + container.current.firstElementChild?.getBoundingClientRect() ?? { + x: 0, + y: 0, + }; + const pageX = rect.x + canvasSize * (points[selected].x / 144 + 0.5); + const pageY = rect.y + canvasSize * (-points[selected].y / 144 + 0.5); + + const a = angleSelecter.current; + a.style.left = pageX - 32 + 'px'; + a.style.top = pageY - 32 + 'px'; + + window.addEventListener('mousemove', ({ x, y }) => { + const angle = rad2deg(Math.atan2(y - pageY, x - pageX)); + a.parentElement?.style.setProperty('--angle', angle.toFixed()); + }); + }; + + const movePoint = (i: number, dx = 0, dy = 0) => + dispatch( + i + ? setSegmentPathAction(i - 1, { + x: clamp(-72, points[i].x + dx * (+multiplier || 1), 72), + y: clamp(-72, points[i].y + dy * (+multiplier || 1), 72), + }) + : setStartPathAction({ + x: clamp(-72, start.x + dx * (+multiplier || 1), 72), + y: clamp(-72, start.y + dy * (+multiplier || 1), 72), + }), + ); + const handleShortcuts: KeyboardEventHandler = (e) => { + const key = `${e.nativeEvent.ctrlKey ? '^' : ''}${e.nativeEvent.key}`; + if (/^[0-9]$/.test(key)) setMultiplier((prev) => prev + key); + else if (pickingAngle) { + if (key === 'j') 0; + else if (key === 'J') 0; + else if (key === 'k') 0; + else if (key === 'K') 0; + else return; + } else { + if (key === 'h') movePoint(selected, -4); + else if (key === 'j') movePoint(selected, 0, -4); + else if (key === 'k') movePoint(selected, 0, 4); + else if (key === 'l') movePoint(selected, 4); + else if (key === 'H') movePoint(selected, -1); + else if (key === 'J') movePoint(selected, 0, -1); + else if (key === 'K') movePoint(selected, 0, 1); + else if (key === 'L') movePoint(selected, 1); + else if (key === '^j') + setSelected((prev) => clamp(0, prev - 1, segments.length)); + else if (key === '^k') + setSelected((prev) => clamp(0, prev + 1, segments.length)); + else if (key === 'a') { + dispatch(addSegmentPathAction()); + setSelected(points.length); + } else if (key === 't') + dispatch( + selected + ? setSegmentPathAction(selected - 1, { + tangent: deg2rad(+multiplier), + }) + : setStartPathAction({ tangent: deg2rad(+multiplier) }), + ); + else if (key === 'T') pickAngle('tangent'); + else if (key === 'f') + dispatch( + selected + ? setSegmentPathAction(selected - 1, { + heading: deg2rad(+multiplier), + }) + : setStartPathAction({ heading: deg2rad(+multiplier) }), + ); + else if (key === 'F') pickAngle('heading'); + else if (key === 'i') + dispatch( + setSegmentPathAction(selected - 1, { + headingType: + headingTypes[ + multiplier + ? clamp(0, +multiplier - 1, 3) + : (headingTypes.indexOf(segments[selected - 1].headingType) + + 1) % + 4 + ], + }), + ); + else if (key === 'd') + dispatch(setSegmentPathAction(selected - 1, { time: +multiplier })); + else if (key === 's') { + if (selected) + dispatch(setSegmentPathAction(selected - 1, { type: 'Spline' })); + } else if (key === 'S') { + if (selected) + dispatch(setSegmentPathAction(selected - 1, { type: 'Line' })); + } else if (key === 'w') { + if (selected) + dispatch(setSegmentPathAction(selected - 1, { type: 'Wait' })); + } else if (key === 'g') + setSelected(clamp(0, +multiplier, points.length - 1)); + else if (key === 'G') setSelected(points.length - 1); + else if (key === 'r') dispatch(uploadPathAction(start, segments)); + else if (key === 'Escape') 0; + else return; + + setMultiplier(''); + } + e.preventDefault(); + }; + + const drawOverlay = ( + acc: { + fill?: string; + stroke?: string; + strokeWidth?: number; + shapes: { type: 'Line' }[]; + }, + op: DrawOp, + i: number, + ) => { + if (op.type === 'fill') acc.fill = op.color; + else if (op.type === 'stroke') acc.stroke = op.color; + else if (op.type === 'strokeWidth') acc.strokeWidth = op.width; + else if (op.type === 'circle') + acc.shapes.push( + , + ); + else if (op.type === 'polygon' || op.type === 'polyline') + acc.shapes.push( + , + ); + else if (op.type === 'spline') + acc.shapes.push( + { + const { ax, bx, cx, dx, ex, fx, ay, by, cy, dy, ey, fy } = op; + const t = i / 200; + return [ + (ax * t + bx) * (t * t * t * t) + + cx * (t * t * t) + + dx * (t * t) + + ex * t + + fx, + (ay * t + by) * (t * t * t * t) + + cy * (t * t * t) + + dy * (t * t) + + ey * t + + fy, + ]; + }) + .flat()} + fill={acc.fill} + stroke={acc.stroke} + strokeWidth={acc.strokeWidth} + />, + ); + return acc; + }; + + return ( + +
        + Draw a Path + {multiplier} +
        + + + + + + {[0, 1].map((dir) => + Array(18) + .fill(0) + .map((_, i) => [(i - 9) / 18, i % 3 === 0] as const) + .map(([i, primary]) => ( + + )), + )} + + + { + overlay.ops.reduce(drawOverlay, { + fill: undefined, + stroke: undefined, + strokeWidth: undefined, + shapes: [], + }).shapes + } + + + + dispatch( + setStartPathAction({ + x: Math.round(target.x()), + y: Math.round(target.y()), + }), + ) + } + onDragMove={({ target }) => { + target.x(clamp(-72, target.x(), 72)); + target.y(clamp(-72, target.y(), 72)); + }} + onMouseOver={() => + container.current?.style.setProperty('cursor', 'pointer') + } + onMouseOut={() => + container.current?.style.setProperty('cursor', 'default') + } + /> + {segments.map((s, i) => ( + setSelected(i + 1)} + onDragEnd={({ target }) => + dispatch( + setSegmentPathAction(i, { + x: Math.round(target.x()), + y: Math.round(target.y()), + }), + ) + } + onDragMove={({ target }) => { + target.x(clamp(-72, target.x(), 72)); + target.y(clamp(-72, target.y(), 72)); + }} + onMouseOver={() => + container.current?.style.setProperty('cursor', 'pointer') + } + onMouseOut={() => + container.current?.style.setProperty('cursor', 'default') + } + > + + + + ))} + + +
        { + dispatch( + selected + ? setSegmentPathAction(selected - 1, { + [pickingAngle]: deg2rad( + +getComputedStyle( + e.target as HTMLElement, + ).getPropertyValue('--angle') + 180, + ), + }) + : setStartPathAction({ + [pickingAngle]: deg2rad( + +getComputedStyle( + e.target as HTMLElement, + ).getPropertyValue('--angle') + 180, + ), + }), + ); + setPickingAngle(''); + }} + onContextMenu={(e) => { + setPickingAngle(''); + e.preventDefault(); + }} + > +
        +
        +
        +
        + ); +} + +PathView.propTypes = { + isDraggable: PropTypes.bool, + isUnlocked: PropTypes.bool, +}; + +export default PathView; diff --git a/FtcDashboard/dash/src/components/views/PathView/inputs/AngleInput.tsx b/FtcDashboard/dash/src/components/views/PathView/inputs/AngleInput.tsx new file mode 100644 index 000000000..3bdab2557 --- /dev/null +++ b/FtcDashboard/dash/src/components/views/PathView/inputs/AngleInput.tsx @@ -0,0 +1,72 @@ +import React, { createRef, MouseEventHandler, useState } from 'react'; +import { SegmentData } from '@/store/types'; + +const mod = (val: number, base: number) => (val + base) % base; +const deg2rad = (deg: number) => mod((deg / 180) * Math.PI, 2 * Math.PI); +const rad2deg = (rad: number) => mod((rad / Math.PI) * 180, 360); + +export default function AngleInput({ + value, + name, + onChange, +}: { + value: number; + name: string; + onChange: (_: Partial) => void; +}) { + const [isPickingAngle, setIsPickingAngle] = useState(false); + const angleSelecter = createRef(); + const pickAngle: MouseEventHandler = (e) => { + e.preventDefault(); + e.persist(); + setIsPickingAngle(true); + + if (!angleSelecter.current) return; + const a = angleSelecter.current; + + a.style.left = e.pageX - 32 + 'px'; + a.style.top = e.pageY - 32 + 'px'; + + window.addEventListener('mousemove', ({ x, y }) => { + const angle = rad2deg(Math.atan2(y - e.pageY, x - e.pageX)); + a.parentElement?.style.setProperty('--angle', angle.toFixed()); + }); + }; + + return ( + <> + onChange({ [name]: deg2rad(+e.target.value) })} + className="h-8 w-16 p-2" + title={`${name} in degrees`} + onContextMenu={pickAngle} + /> +
        { + onChange({ + [name]: deg2rad( + +getComputedStyle(e.target as HTMLElement).getPropertyValue('--angle') + 180, + ), + }); + setIsPickingAngle(false); + }} + onContextMenu={(e) => { + setIsPickingAngle(false); + e.preventDefault(); + }} + > +
        +
        + + ); +} diff --git a/FtcDashboard/dash/src/components/views/PathView/inputs/PointInput.tsx b/FtcDashboard/dash/src/components/views/PathView/inputs/PointInput.tsx new file mode 100644 index 000000000..ea7b1b57a --- /dev/null +++ b/FtcDashboard/dash/src/components/views/PathView/inputs/PointInput.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { SegmentData } from '@/store/types'; + +const PointInput = ({ + valueX, + valueY, + onChange, +}: { + valueX: number; + valueY: number; + onChange: (_: Partial) => void; +}) => ( + <> + onChange({ x: +evt.target.value })} + className="h-8 w-16 p-2" + title="x-coordinate in inches" + /> + onChange({ y: +evt.target.value })} + className="h-8 w-16 p-2" + title="y-coordinate in inches" + /> + +); + +export default PointInput; diff --git a/FtcDashboard/dash/src/store/actions/path.ts b/FtcDashboard/dash/src/store/actions/path.ts index 5a4ba81f1..668091386 100644 --- a/FtcDashboard/dash/src/store/actions/path.ts +++ b/FtcDashboard/dash/src/store/actions/path.ts @@ -1,6 +1,29 @@ -import { SegmentData } from '../types/path'; +import { Path, SegmentData } from '@/store/types/path'; export const uploadPathAction = ( - start: Omit, - segments: SegmentData[], -) => ({ type: 'UPLOAD_PATH', start, segments }); \ No newline at end of file + start: Path['start'], + segments: Path['segments'], +) => ({ type: 'UPLOAD_PATH' as const, start, segments }); + +export const setStartPathAction = (newVals: Partial) => ({ + type: 'SET_START_PATH' as const, + newVals, +}); + +export const setSegmentPathAction = ( + i: number, + newVals: Partial, +) => ({ + type: 'SET_SEGMENT_PATH' as const, + i, + newVals, +}); + +export const clearSegmentsPathAction = () => ({ + type: 'SET_PATH' as const, + newVals: { segments: [] }, +}); + +export const addSegmentPathAction = () => ({ + type: 'ADD_SEGMENT_PATH' as const, +}); diff --git a/FtcDashboard/dash/src/store/reducers/path.ts b/FtcDashboard/dash/src/store/reducers/path.ts index f12383d51..4cb4a5be8 100644 --- a/FtcDashboard/dash/src/store/reducers/path.ts +++ b/FtcDashboard/dash/src/store/reducers/path.ts @@ -1,39 +1,54 @@ -import { SegmentData } from '../types'; -import { uploadPathAction } from '../actions/path'; +/* eslint-disable no-fallthrough */ +import { Path, SegmentData } from '@/store/types'; +import { + addSegmentPathAction, + clearSegmentsPathAction, + setSegmentPathAction, + setStartPathAction, + uploadPathAction, +} from '@/store/actions/path'; -const initialState = { - start: {} as Omit, +const initialState: Path = { + start: { + x: 0, + y: 0, + tangent: 0, + heading: 0, + }, // as Omit, segments: [] as SegmentData[], }; const pathReducer = ( state = initialState, - action: ReturnType, + action: + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType, ) => { switch (action.type) { - // case RECEIVE_CONFIG: - // return { - // ...state, - // configRoot: receiveConfig(state.configRoot, action.configRoot), - // }; - // case UPDATE_CONFIG: - // return { - // ...state, - // configRoot: updateConfig(state.configRoot, action.configDiff, true), - // }; - // case SAVE_CONFIG: - // return { - // ...state, - // configRoot: updateConfig(state.configRoot, action.configDiff, false), - // }; - // case REFRESH_CONFIG: - // return { - // ...state, - // configRoot: refreshConfig(state.configRoot), - // }; - default: - return action; + case 'SET_PATH': + return { ...state, ...action }; + case 'ADD_SEGMENT_PATH': + state.segments.push({ + type: 'Spline', + x: 0, + y: 0, + tangent: 0, + time: 0, + heading: 0, + headingType: 'Tangent', + }); + return state; + case 'SET_START_PATH': + Object.assign(state.start, action.newVals); + return state; + case 'SET_SEGMENT_PATH': + Object.assign(state.segments[action.i], action.newVals); + // case 'UPLOAD_PATH': } + return state; }; -export default pathReducer; \ No newline at end of file +export default pathReducer; diff --git a/FtcDashboard/dash/src/store/types/index.ts b/FtcDashboard/dash/src/store/types/index.ts index 0342af5db..c8f433a3b 100644 --- a/FtcDashboard/dash/src/store/types/index.ts +++ b/FtcDashboard/dash/src/store/types/index.ts @@ -81,7 +81,8 @@ export type { Telemetry, TelemetryItem, ReceiveTelemetryAction, + DrawOp, } from './telemetry'; export { segmentTypes, headingTypes } from './path'; -export type { SegmentData } from './path'; \ No newline at end of file +export type { SegmentData, Path } from './path'; diff --git a/FtcDashboard/dash/src/store/types/path.ts b/FtcDashboard/dash/src/store/types/path.ts index 140523701..7650bc753 100644 --- a/FtcDashboard/dash/src/store/types/path.ts +++ b/FtcDashboard/dash/src/store/types/path.ts @@ -13,4 +13,9 @@ export type SegmentData = { time: number; heading: number; headingType: typeof headingTypes[number]; -}; \ No newline at end of file +}; + +export type Path = { + start: Omit; + segments: SegmentData[]; +}; diff --git a/FtcDashboard/dash/src/store/types/telemetry.ts b/FtcDashboard/dash/src/store/types/telemetry.ts index 6660018ef..1e1d3f547 100644 --- a/FtcDashboard/dash/src/store/types/telemetry.ts +++ b/FtcDashboard/dash/src/store/types/telemetry.ts @@ -14,7 +14,7 @@ type Stroke = { type StrokeWidth = { type: 'strokeWidth'; - lineWidth: number; + width: number; }; type Circle = { @@ -53,7 +53,7 @@ type Spline = { fy: number; }; -type DrawOp = +export type DrawOp = | Fill | Stroke | StrokeWidth diff --git a/FtcDashboard/dash/yarn.lock b/FtcDashboard/dash/yarn.lock index 78ae89130..933b0575b 100644 --- a/FtcDashboard/dash/yarn.lock +++ b/FtcDashboard/dash/yarn.lock @@ -523,6 +523,13 @@ dependencies: "@types/react" "*" +"@types/react-reconciler@^0.28.0", "@types/react-reconciler@^0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@types/react-reconciler/-/react-reconciler-0.28.2.tgz#f16b0e8cc4748af70ca975eaaace0d79582c71fa" + integrity sha512-8tu6lHzEgYPlfDf/J6GOQdIc+gs+S2yAqlby3zTsB3SP2svlqTYe5fwZNtZyfactP74ShooP2vvi1BOp9ZemWw== + dependencies: + "@types/react" "*" + "@types/react-virtualized-auto-sizer@^1.0.1": version "1.0.1" resolved "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz" @@ -2105,6 +2112,13 @@ isexe@^2.0.0: resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +its-fine@^1.0.6: + version "1.0.9" + resolved "https://registry.yarnpkg.com/its-fine/-/its-fine-1.0.9.tgz#f4ca0ad5bdbf896764d35f7cf24c16287b6c6d31" + integrity sha512-Ph+vcp1R100JOM4raXmDx/wCTi4kMkMXiFE108qGzsLdghXFPqad82UJJtqT1jwdyWYkTU6eDpDnol/ZIzW+1g== + dependencies: + "@types/react-reconciler" "^0.28.0" + js-sdsl@^4.1.4: version "4.2.0" resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz" @@ -2162,6 +2176,11 @@ json5@^2.2.1: array-includes "^3.1.5" object.assign "^4.1.3" +konva@^8.4.2: + version "8.4.2" + resolved "https://registry.yarnpkg.com/konva/-/konva-8.4.2.tgz#6de2b9d54f2b56b8c7c76eba66955f7255dd9afb" + integrity sha512-4VQcrgj/PI8ydJjtLcTuinHBE8o0WGX0YoRwbiN5mpYQiC52aOzJ0XbpKNDJdRvORQphK5LP+jeM0hQJEYIuUA== + language-subtag-registry@~0.3.2: version "0.3.22" resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz" @@ -2708,9 +2727,27 @@ react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: react-is@^18.0.0: version "18.2.0" - resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +react-konva@^18.2.5: + version "18.2.5" + resolved "https://registry.yarnpkg.com/react-konva/-/react-konva-18.2.5.tgz#592692619c5f4a9c14726e146574ddc8bc468a7c" + integrity sha512-lTqJStcHnpGSXB9RlV7p5at3MpRML/TujzbuUDZRIInsLocJ/I4Nhhg3w6yJm9UV05kcwr88OY6LO+2zRyzXog== + dependencies: + "@types/react-reconciler" "^0.28.2" + its-fine "^1.0.6" + react-reconciler "~0.29.0" + scheduler "^0.23.0" + +react-reconciler@~0.29.0: + version "0.29.0" + resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.29.0.tgz#ee769bd362915076753f3845822f2d1046603de7" + integrity sha512-wa0fGj7Zht1EYMRhKWwoo1H9GApxYLBuhoAuXN0TlltESAjDssB+Apf0T/DngVqaMyPypDmabL37vw/2aRM98Q== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + react-redux@^8.0.4: version "8.0.5" resolved "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz" @@ -3228,6 +3265,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +use-image@^1.0.12: + version "1.1.0" + resolved "https://registry.yarnpkg.com/use-image/-/use-image-1.1.0.tgz#dc244c34506d3cf3a8177c1f0bbfb158b9beefe5" + integrity sha512-+cBHRR/44ZyMUS873O0vbVylgMM0AbdTunEplAWXvIQ2p69h2sIo2Qq74zeUsq6AMo+27e5lERQvXzd1crGiMg== + use-sync-external-store@^1.0.0: version "1.2.0" resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz" From 7d054126949528f53456055194682243a59cb703 Mon Sep 17 00:00:00 2001 From: Nash Pillai Date: Sun, 26 Feb 2023 19:37:14 -0500 Subject: [PATCH 3/6] fix ref for visual path editor --- .../dash/src/components/views/BaseView.tsx | 24 ++++++++++++------- .../components/views/PathView/PathView.tsx | 14 +++++------ .../views/PathView/inputs/AngleInput.tsx | 12 ++++++---- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/FtcDashboard/dash/src/components/views/BaseView.tsx b/FtcDashboard/dash/src/components/views/BaseView.tsx index 0e44f6c9e..2c767ccdd 100644 --- a/FtcDashboard/dash/src/components/views/BaseView.tsx +++ b/FtcDashboard/dash/src/components/views/BaseView.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import { forwardRef, PropsWithChildren } from 'react'; +import { ForwardedRef, forwardRef, PropsWithChildren } from 'react'; type BaseViewProps = PropsWithChildren<{ isUnlocked?: boolean; @@ -47,15 +47,21 @@ const BaseViewHeading = ({ ); -const BaseViewBody = ({ - children, - className, - ...props -}: JSX.IntrinsicElements['div']) => ( -
        - {children} -
        +const BaseViewBody = forwardRef( + ( + { children, className, ...props }: JSX.IntrinsicElements['div'], + ref: ForwardedRef, + ) => ( +
        + {children} +
        + ), ); +BaseViewBody.displayName = 'BaseViewBody'; const BaseViewIcons = ({ className, diff --git a/FtcDashboard/dash/src/components/views/PathView/PathView.tsx b/FtcDashboard/dash/src/components/views/PathView/PathView.tsx index 26471cf59..775371ad8 100644 --- a/FtcDashboard/dash/src/components/views/PathView/PathView.tsx +++ b/FtcDashboard/dash/src/components/views/PathView/PathView.tsx @@ -1,4 +1,5 @@ -import React, { +import { + createRef, KeyboardEventHandler, useEffect, useRef, @@ -8,7 +9,6 @@ import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; import fieldImageName from '@/assets/field.png'; -import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg'; import BaseView, { BaseViewBody, BaseViewHeading, @@ -39,7 +39,7 @@ type PathSegmentViewProps = BaseViewProps & BaseViewHeadingProps; function PathView({ isUnlocked, isDraggable }: PathSegmentViewProps) { const dispatch = useDispatch(); - const container = useRef(null); + const container = createRef(); const [canvasSize, setCanvasSize] = useState(0); const { path: { start, segments }, @@ -63,9 +63,9 @@ function PathView({ isUnlocked, isDraggable }: PathSegmentViewProps) { const [multiplier, setMultiplier] = useState(''); const [selected, setSelected] = useState(0); const [pickingAngle, setPickingAngle] = useState(''); - const angleSelecter = useRef(null); + const angleSelector = useRef(null); const pickAngle = (name: string) => { - if (!angleSelecter.current || !container.current) return; + if (!angleSelector.current || !container.current) return; setPickingAngle(name); const rect = container.current.firstElementChild?.getBoundingClientRect() ?? { @@ -75,7 +75,7 @@ function PathView({ isUnlocked, isDraggable }: PathSegmentViewProps) { const pageX = rect.x + canvasSize * (points[selected].x / 144 + 0.5); const pageY = rect.y + canvasSize * (-points[selected].y / 144 + 0.5); - const a = angleSelecter.current; + const a = angleSelector.current; a.style.left = pageX - 32 + 'px'; a.style.top = pageY - 32 + 'px'; @@ -411,7 +411,7 @@ function PathView({ isUnlocked, isDraggable }: PathSegmentViewProps) { }} >
    diff --git a/FtcDashboard/dash/src/components/views/PathView/inputs/AngleInput.tsx b/FtcDashboard/dash/src/components/views/PathView/inputs/AngleInput.tsx index 3bdab2557..bd3bc8fd7 100644 --- a/FtcDashboard/dash/src/components/views/PathView/inputs/AngleInput.tsx +++ b/FtcDashboard/dash/src/components/views/PathView/inputs/AngleInput.tsx @@ -15,14 +15,14 @@ export default function AngleInput({ onChange: (_: Partial) => void; }) { const [isPickingAngle, setIsPickingAngle] = useState(false); - const angleSelecter = createRef(); + const angleSelector = createRef(); const pickAngle: MouseEventHandler = (e) => { e.preventDefault(); e.persist(); setIsPickingAngle(true); - if (!angleSelecter.current) return; - const a = angleSelecter.current; + if (!angleSelector.current) return; + const a = angleSelector.current; a.style.left = e.pageX - 32 + 'px'; a.style.top = e.pageY - 32 + 'px'; @@ -52,7 +52,9 @@ export default function AngleInput({ onClick={(e) => { onChange({ [name]: deg2rad( - +getComputedStyle(e.target as HTMLElement).getPropertyValue('--angle') + 180, + +getComputedStyle(e.target as HTMLElement).getPropertyValue( + '--angle', + ) + 180, ), }); setIsPickingAngle(false); @@ -63,7 +65,7 @@ export default function AngleInput({ }} >
    From fac2d3f433ffc9ac6aae09223e065fec6f8a378a Mon Sep 17 00:00:00 2001 From: Nash Pillai Date: Sun, 26 Feb 2023 21:21:44 -0500 Subject: [PATCH 4/6] fix reflection getting list of classes --- .../acmerobotics/dashboard/DashboardCore.java | 2 +- .../acmerobotics/dashboard/FtcDashboard.java | 55 +++++++++++++++++-- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/DashboardCore/src/main/java/com/acmerobotics/dashboard/DashboardCore.java b/DashboardCore/src/main/java/com/acmerobotics/dashboard/DashboardCore.java index 9595efc0e..a8aed6ad3 100644 --- a/DashboardCore/src/main/java/com/acmerobotics/dashboard/DashboardCore.java +++ b/DashboardCore/src/main/java/com/acmerobotics/dashboard/DashboardCore.java @@ -44,7 +44,7 @@ public class DashboardCore { private CustomVariable configRoot = new CustomVariable(); // guarded by configLock private final Object pathLock = new Object(); - private ArrayList pathFields; // guarded by pathLock + public ArrayList pathFields = new ArrayList<>(); // guarded by pathLock // TODO: this doensn't make a ton of sense here, though it needs to go in this module for testing diff --git a/FtcDashboard/src/main/java/com/acmerobotics/dashboard/FtcDashboard.java b/FtcDashboard/src/main/java/com/acmerobotics/dashboard/FtcDashboard.java index 0bc9770d1..3d72ec7e6 100644 --- a/FtcDashboard/src/main/java/com/acmerobotics/dashboard/FtcDashboard.java +++ b/FtcDashboard/src/main/java/com/acmerobotics/dashboard/FtcDashboard.java @@ -27,6 +27,8 @@ import com.acmerobotics.dashboard.message.redux.ReceiveOpModeList; import com.acmerobotics.dashboard.message.redux.ReceiveRobotStatus; import com.acmerobotics.dashboard.message.redux.SaveConfig; +import com.acmerobotics.dashboard.path.DashboardPath; +import com.acmerobotics.dashboard.path.reflection.FieldProvider; import com.acmerobotics.dashboard.telemetry.TelemetryPacket; import com.qualcomm.ftccommon.FtcEventLoop; import com.qualcomm.robotcore.eventloop.opmode.Disabled; @@ -62,6 +64,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -541,6 +545,49 @@ private static void addConfigClasses(CustomVariable customVariable) { throw new RuntimeException(e); } } + private static void addPathClasses(ArrayList pathFields) { + ClassLoader classLoader = FtcDashboard.class.getClassLoader(); + + Context context = AppUtil.getInstance().getApplication(); + try { + DexFile dexFile = new DexFile(context.getPackageCodePath()); + + List classNames = Collections.list(dexFile.entries()); + + for (String className : classNames) { + boolean skip = false; + for (String prefix : IGNORED_PACKAGES) if (className.startsWith(prefix)) { + skip = true; + break; + } + + if (skip) continue; + + try { + Class pathClass = Class.forName(className, false, classLoader); + + if ( + !pathClass.isAnnotationPresent(DashboardPath.class) + || pathClass.isAnnotationPresent(Disabled.class) + ) continue; + + + for (Field field : pathClass.getFields()) { + if (Modifier.isStatic(field.getModifiers()) + && !Modifier.isFinal(field.getModifiers()) + && field.getName().equals("dashboardPath") + ) pathFields.add(new FieldProvider(field, null)); + } + } catch (ClassNotFoundException | NoClassDefFoundError ignored) { + // dash is unable to access many classes and reporting every instance + // only clutters the logs + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + private class DashWebSocket extends NanoWSD.WebSocket implements SendFun { final SocketHandler sh = core.newSocket(this); @@ -631,12 +678,8 @@ protected void onException(IOException exception) { } private FtcDashboard() { - core.withConfigRoot(new CustomVariableConsumer() { - @Override - public void accept(CustomVariable configRoot) { - addConfigClasses(configRoot); - } - }); + core.withConfigRoot(FtcDashboard::addConfigClasses); + addPathClasses(core.pathFields); try { server.start(); From e59e6bbf62ce143f6c518a05005850b0e3450748 Mon Sep 17 00:00:00 2001 From: Nash Pillai Date: Wed, 1 Mar 2023 20:41:51 -0500 Subject: [PATCH 5/6] fix inputs in dark mode --- FtcDashboard/dash/src/assets/icons/delete.svg | 5 ++++- FtcDashboard/dash/src/assets/icons/file_download.svg | 5 ++++- .../dash/src/components/views/PathView/PathSegment.tsx | 6 +++--- .../dash/src/components/views/PathView/PathSegmentView.tsx | 2 -- .../src/components/views/PathView/inputs/AngleInput.tsx | 2 +- .../src/components/views/PathView/inputs/PointInput.tsx | 4 ++-- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/FtcDashboard/dash/src/assets/icons/delete.svg b/FtcDashboard/dash/src/assets/icons/delete.svg index 3ea375972..fe4568c14 100644 --- a/FtcDashboard/dash/src/assets/icons/delete.svg +++ b/FtcDashboard/dash/src/assets/icons/delete.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/FtcDashboard/dash/src/assets/icons/file_download.svg b/FtcDashboard/dash/src/assets/icons/file_download.svg index c4ec1c354..ce4d932e4 100644 --- a/FtcDashboard/dash/src/assets/icons/file_download.svg +++ b/FtcDashboard/dash/src/assets/icons/file_download.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/FtcDashboard/dash/src/components/views/PathView/PathSegment.tsx b/FtcDashboard/dash/src/components/views/PathView/PathSegment.tsx index 5d8816560..163d43adf 100644 --- a/FtcDashboard/dash/src/components/views/PathView/PathSegment.tsx +++ b/FtcDashboard/dash/src/components/views/PathView/PathSegment.tsx @@ -15,7 +15,7 @@ const PathSegment = ({
  • onChange({ diff --git a/FtcDashboard/dash/src/components/views/PathView/PathSegmentView.tsx b/FtcDashboard/dash/src/components/views/PathView/PathSegmentView.tsx index 9db02e2e5..acbc52094 100644 --- a/FtcDashboard/dash/src/components/views/PathView/PathSegmentView.tsx +++ b/FtcDashboard/dash/src/components/views/PathView/PathSegmentView.tsx @@ -1,5 +1,3 @@ -import React, { useState } from 'react'; - import BaseView, { BaseViewHeading, BaseViewBody, diff --git a/FtcDashboard/dash/src/components/views/PathView/inputs/AngleInput.tsx b/FtcDashboard/dash/src/components/views/PathView/inputs/AngleInput.tsx index bd3bc8fd7..1113d91f1 100644 --- a/FtcDashboard/dash/src/components/views/PathView/inputs/AngleInput.tsx +++ b/FtcDashboard/dash/src/components/views/PathView/inputs/AngleInput.tsx @@ -41,7 +41,7 @@ export default function AngleInput({ step={15} value={rad2deg(value).toFixed()} onChange={(e) => onChange({ [name]: deg2rad(+e.target.value) })} - className="h-8 w-16 p-2" + className="h-8 w-16 p-2 dark:border-slate-500/80 dark:bg-slate-700 dark:text-slate-200" title={`${name} in degrees`} onContextMenu={pickAngle} /> diff --git a/FtcDashboard/dash/src/components/views/PathView/inputs/PointInput.tsx b/FtcDashboard/dash/src/components/views/PathView/inputs/PointInput.tsx index ea7b1b57a..58068604c 100644 --- a/FtcDashboard/dash/src/components/views/PathView/inputs/PointInput.tsx +++ b/FtcDashboard/dash/src/components/views/PathView/inputs/PointInput.tsx @@ -18,7 +18,7 @@ const PointInput = ({ step={4} value={valueX} onChange={(evt) => onChange({ x: +evt.target.value })} - className="h-8 w-16 p-2" + className="h-8 w-16 p-2 dark:border-slate-500/80 dark:bg-slate-700 dark:text-slate-200" title="x-coordinate in inches" /> onChange({ y: +evt.target.value })} - className="h-8 w-16 p-2" + className="h-8 w-16 p-2 dark:border-slate-500/80 dark:bg-slate-700 dark:text-slate-200 " title="y-coordinate in inches" /> From a731288db79262504ab929c52e629d714ef09a29 Mon Sep 17 00:00:00 2001 From: Nash Pillai Date: Fri, 3 Mar 2023 21:51:51 -0500 Subject: [PATCH 6/6] replace redundant field view with config view --- FtcDashboard/dash/src/enums/LayoutPreset.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FtcDashboard/dash/src/enums/LayoutPreset.tsx b/FtcDashboard/dash/src/enums/LayoutPreset.tsx index 2c43f756e..c877bc760 100644 --- a/FtcDashboard/dash/src/enums/LayoutPreset.tsx +++ b/FtcDashboard/dash/src/enums/LayoutPreset.tsx @@ -80,7 +80,7 @@ const LAYOUT_DETAILS: { [key in Values]: Layout } = { - + @@ -89,7 +89,7 @@ const LAYOUT_DETAILS: { [key in Values]: Layout } = { - ) + ), }, [LayoutPreset.GRAPH]: { name: 'Graph',