From d533f4e04b7472886b799b288270fe1e43074f99 Mon Sep 17 00:00:00 2001 From: Daniel1464 Date: Fri, 15 Aug 2025 10:32:45 -0400 Subject: [PATCH 1/4] horray --- src-tauri/src/api.rs | 24 +++++++++++++ src-tauri/src/tauri.rs | 2 ++ src/AppMenu.tsx | 31 ++++++++++++++++- src/codegen/genConstsFile.ts | 61 +++++++++++++++++++++++++++++++++ src/codegen/genVarsFile.ts | 45 ++++++++++++++++++++++++ src/codegen/internals.ts | 42 +++++++++++++++++++++++ src/document/DocumentManager.ts | 58 +++++++++++++++++++++++++++++-- src/document/tauriCommands.ts | 13 ++++++- src/util/LocalStorageKeys.ts | 4 ++- 9 files changed, 274 insertions(+), 6 deletions(-) create mode 100644 src/codegen/genConstsFile.ts create mode 100644 src/codegen/genVarsFile.ts create mode 100644 src/codegen/internals.ts diff --git a/src-tauri/src/api.rs b/src-tauri/src/api.rs index ee2dbcb9b4..44e9b89abe 100644 --- a/src-tauri/src/api.rs +++ b/src-tauri/src/api.rs @@ -1,6 +1,7 @@ #![allow(clippy::needless_pass_by_value)] use std::path::PathBuf; +use std::fs; use crate::tauri::TauriResult; use choreo_core::{ @@ -55,6 +56,23 @@ pub async fn open_in_explorer(path: String) -> TauriResult<()> { debug_result!(open::that(path).map_err(ChoreoError::from)); } +#[tauri::command] +pub async fn select_codegen_folder(app_handle: tauri::AppHandle) -> TauriResult { + Ok( + app_handle + .dialog() + .file() + .set_title("Select a folder to output generated java files") + .blocking_pick_folder() + .ok_or(ChoreoError::FileNotFound(None))? + .as_path() + .ok_or(ChoreoError::FileNotFound(None))? + .to_str() + .ok_or(ChoreoError::FileNotFound(None))? + .to_string() + ) +} + #[tauri::command] pub async fn open_project_dialog(app_handle: tauri::AppHandle) -> TauriResult { app_handle @@ -86,6 +104,12 @@ pub async fn open_project_dialog(app_handle: tauri::AppHandle) -> TauriResult ChoreoResult<()> { + fs::write(file_path, content.as_bytes())?; + Ok(()) +} + #[tauri::command] pub async fn default_project() -> TauriResult { Ok(ProjectFile::default()) diff --git a/src-tauri/src/tauri.rs b/src-tauri/src/tauri.rs index 358e449bd9..fe52e2ea1d 100644 --- a/src-tauri/src/tauri.rs +++ b/src-tauri/src/tauri.rs @@ -203,11 +203,13 @@ pub fn run_tauri(project: Option) { guess_control_interval_counts, open_in_explorer, default_project, + write_raw_file, read_project, write_project, write_trajectory, read_all_trajectory, open_project_dialog, + select_codegen_folder, read_trajectory, rename_trajectory, trajectory_up_to_date, diff --git a/src/AppMenu.tsx b/src/AppMenu.tsx index 1eec669f2a..80ab94b7a3 100644 --- a/src/AppMenu.tsx +++ b/src/AppMenu.tsx @@ -2,6 +2,7 @@ import { CopyAll, NoteAddOutlined, OpenInNew, + RemoveCircle, Settings } from "@mui/icons-material"; import MenuIcon from "@mui/icons-material/Menu"; @@ -28,7 +29,10 @@ import { saveProjectDialog, uiState, openDiagnosticZipWithInfo, - openProjectSelectFeedback + openProjectSelectFeedback, + codeGenDialog, + disableCodegen, + codegenEnabled } from "./document/DocumentManager"; import SettingsModal from "./components/config/SettingsModal"; @@ -189,6 +193,31 @@ class AppMenu extends Component { + + { + codeGenDialog(); + }} + > + + + + + + + {codegenEnabled() && { + disableCodegen(); + }} + > + + + + + } {/* Info about save locations */} diff --git a/src/codegen/genConstsFile.ts b/src/codegen/genConstsFile.ts new file mode 100644 index 0000000000..01bf4ddfff --- /dev/null +++ b/src/codegen/genConstsFile.ts @@ -0,0 +1,61 @@ +import { Project } from "../document/2025/DocumentTypes"; +import { writeConst } from "./internals"; + +export function genConstsFile(project: Project, packageName: string): string { + let out: string[] = []; + out.push(`package ${packageName};`); + out.push(` +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.units.measure.*; +import static edu.wpi.first.units.Units.*; + +/** + * Generated file containing document settings for your choreo project. + * This allows for modifying constants in choreo while keeping your robot code up-to-date. + * DO NOT MODIFY this file yourself, as it is auto-generated. + */ +public final class ChoreoConsts {`); + + const config = project.config; + const maxLinearVel = config.vmax.val / config.gearing.val * config.radius.val; + const maxWheelForce = config.tmax.val * config.gearing.val * 4 / config.radius.val; + const maxLinearAccel = maxWheelForce / config.mass.val; + + out.push(writeConst(config.gearing, 'gearing', 'Number')); + out.push(writeConst(config.cof, 'frictionCoefficient', 'Number')); + out.push(writeConst(config.radius, 'wheelRadius', 'Length')); + out.push(writeConst(config.inertia, "moi", 'MoI')); + out.push(writeConst(config.mass, 'mass', 'Mass')); + out.push(writeConst(config.tmax, 'driveMotorMaxTorque', 'Torque')); + + const bumperWB = config.bumper.front.val + config.bumper.back.val; + const bumperTW = config.bumper.side.val * 2; + out.push(writeConst(bumperWB, "wheelBaseWithBumpers", 'Length')); + out.push(writeConst(bumperTW, "trackWidthWithBumpers", 'Length')); + + if (project.type === 'Swerve') { + out.push(` + public static final Translation2d[] moduleTranslations = { + new Translation2d(${config.frontLeft.x.val}, ${config.frontLeft.y.val}), + new Translation2d(${config.frontLeft.x.val}, ${-config.frontLeft.y.val}), + new Translation2d(${config.backLeft.x.val}, ${config.backLeft.y.val}), + new Translation2d(${config.backLeft.x.val}, ${-config.backLeft.y.val}), + };`); + const drivebaseRadius = Math.hypot(config.frontLeft.x.val, config.frontLeft.y.val); + const frictionFloorForce = config.mass.val * 9.81 * config.cof.val; + const minLinearForce = Math.min(frictionFloorForce, maxWheelForce); + const maxAngularVel = maxLinearVel / drivebaseRadius; + const maxAngularAccel = minLinearForce * drivebaseRadius / config.inertia.val; + out.push(writeConst(maxAngularVel, 'maxAngularVel', 'AngVel')); + out.push(writeConst(maxAngularAccel, 'maxAngularAccel', 'AngAcc')); + } else { + out.push(writeConst(config.differentialTrackWidth, 'trackWidth', 'Length')); + out.push(''); + } + out.push(writeConst(maxLinearVel, 'maxLinearVel', 'LinVel')); + out.push(writeConst(maxLinearAccel, 'maxLinearAccel', 'LinAcc')); + out.push(''); + out.push(' private ChoreoConsts() {}'); + out.push('}'); + return out.join('\n'); +} \ No newline at end of file diff --git a/src/codegen/genVarsFile.ts b/src/codegen/genVarsFile.ts new file mode 100644 index 0000000000..c298ab65b2 --- /dev/null +++ b/src/codegen/genVarsFile.ts @@ -0,0 +1,45 @@ +import { Project } from "../document/2025/DocumentTypes"; +import { round, writeConst } from "./internals"; + +export function genVarsFile(project: Project, packageName: string): string { + const expressions = project.variables.expressions + const poses = project.variables.poses + let out: string[] = []; + out.push(`package ${packageName};\n`); + out.push(` +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.units.measure.*; +import static edu.wpi.first.units.Units.*; + +/** + * Generated file containing variables defined in choreo. + * DO NOT MODIFY THIS YOURSELF; instead, change these values + * in the choreo GUI. + */ +public final class ChoreoVars {`); + for (const varName in project.variables.expressions) { + const data = expressions[varName]; + out.push(writeConst(data.var, varName, data.dimension)) + } + out.push(''); + out.push(' public static final class Poses {'); + for (const poseName in poses) { + const pose = poses[poseName]; + const heading = Math.abs(pose.heading.val) < 1e-5 + ? 'Rotation2d.kZero' + : `Rotation2d.fromRadians(${round(pose.heading.val, 3)})`; + const x = round(pose.x.val, 3) + const y = round(pose.y.val, 3) + out.push( + ` public static final Pose2d ${poseName} = new Pose2d(${x}, ${y}, ${heading});` + ); + } + out.push(''); + out.push(' private Poses() {}'); + out.push(' }'); + out.push(''); + out.push(' private ChoreoVars() {}'); + out.push('}'); + return out.join('\n'); +} \ No newline at end of file diff --git a/src/codegen/internals.ts b/src/codegen/internals.ts new file mode 100644 index 0000000000..d5ae4f3ad7 --- /dev/null +++ b/src/codegen/internals.ts @@ -0,0 +1,42 @@ +import { DimensionName } from "../document/ExpressionStore"; +import { Expr, Variable } from "../document/2025/DocumentTypes"; + +export interface UnitData { + type: string; + baseUnit: string; +} + +export function unitDataFrom(choreoDimensionName: DimensionName): UnitData | null { + switch (choreoDimensionName) { + case "LinAcc": return { type: "LinearAcceleration", baseUnit: "MetersPerSecondPerSecond" }; + case "LinVel": return { type: "LinearVelocity", baseUnit: "MetersPerSecond" }; + case "Length": return { type: "Distance", baseUnit: "Meters" }; + case "Angle": return { type: "Angle", baseUnit: "Radians" }; + case "AngVel": return { type: "AngularVelocity", baseUnit: "RadiansPerSecond" }; + case "AngAcc": return { type: "AngularAcceleration", baseUnit: "RadiansPerSecondPerSecond" }; + case "Time": return { type: "Time", baseUnit: "Seconds" }; + case "Mass": return { type: "Mass", baseUnit: "Kilograms" }; + case "Torque": return { type: "Torque", baseUnit: "NewtonMeters" }; + case "MoI": return { type: "MomentOfInertia", baseUnit: "KilogramSquareMeters" }; + default: return null; + } +} + +export function writeConst(expr: Expr | number, variableName: string, dimension: DimensionName): string { + const unitData = unitDataFrom(dimension) + let val = typeof expr === 'number' ? expr : expr.val + val = round(val, dimension === "MoI" ? 5 : 3) + if (!dimension || !unitData) { + return ` public static final double ${variableName} = ${val};`; + } + return ` public static final ${unitData.type} ${variableName} = ${unitData.baseUnit}.of(${val});`; +} + +export function writeVar(variable: Variable, name: string): string { + return writeConst(variable.var, name, variable.dimension) +} + +export function round(val: number, digits: number) { + const roundingFactor = Math.pow(10, digits) + return Math.round(val * roundingFactor) / roundingFactor +} \ No newline at end of file diff --git a/src/document/DocumentManager.ts b/src/document/DocumentManager.ts index 5e74e724bf..16d34e26d6 100644 --- a/src/document/DocumentManager.ts +++ b/src/document/DocumentManager.ts @@ -64,6 +64,8 @@ import { SavingState, UIStateStore } from "./UIStateStore"; import { findUUIDIndex } from "./path/utils"; import { ChoreoError, Commands } from "./tauriCommands"; import { tracing } from "./tauriTracing"; +import { genConstsFile } from "../codegen/genConstsFile"; +import { genVarsFile } from "../codegen/genVarsFile"; export type OpenFilePayload = { name: string; @@ -275,7 +277,7 @@ function renameVariable(find: string, replace: string) { export function setup() { doc.history.clear(); setupEventListeners() - .then(() => newProject()) + .then(() => newProject(false)) .then(() => uiState.updateWindowTitle()) .then(() => openProjectFile()); } @@ -690,7 +692,7 @@ function getSelectedConstraint() { }); } -export async function newProject() { +export async function newProject(promptForCodegen: boolean = true) { applySnapshot(uiState, { settingsTab: 0, layers: ViewLayerDefaults, @@ -705,6 +707,22 @@ export async function newProject() { uiState.setProjectSavingState(SavingState.NO_LOCATION); uiState.setProjectSavingTime(new Date()); doc.history.clear(); + if (!promptForCodegen) { + return + } + const msg = codegenEnabled() + ? ` + Change the folder in which java file generation occurs? + (Do this if you're switching to a new codebase) + ` + : ` + Choreo can now output java files + containing variables and constants defined in the GUI. + Select a folder to enable this feature? + ` + if (await ask(msg, { title: "Choreo", kind: "warning" })) { + await codeGenDialog(); + } } export function select(item: SelectableItemTypes) { doc.setSelectedSidebarItem(item); @@ -776,10 +794,27 @@ export async function writeAllTrajectories() { } } +export function codegenEnabled() { + return localStorage.getItem(LocalStorageKeys.JAVA_ROOT) != null +} + export async function saveProject() { if (await canSave()) { try { - await toast.promise(Commands.writeProject(doc.serializeChor()), { + const data = doc.serializeChor() + const javaRoot = localStorage.getItem(LocalStorageKeys.JAVA_ROOT) + const packageName = localStorage.getItem(LocalStorageKeys.CODE_GEN_PACKAGE) + let tasks = [Commands.writeProject(data)] + if (javaRoot && packageName) { + const constsFile = genConstsFile(data, packageName) + const varsFile = genVarsFile(data, packageName) + tasks = [ + ...tasks, + Commands.writeRawFile(constsFile, javaRoot + packageName + "/ChoreoConsts.java"), + Commands.writeRawFile(varsFile, javaRoot + packageName + "/ChoreoVars.java") + ] + } + await toast.promise(Promise.all(tasks), { error: { render(toastProps: ToastContentProps) { return `Project save fail. Alert developers: (${toastProps.data!.type}) ${toastProps.data!.content}`; @@ -797,6 +832,23 @@ export async function saveProject() { } } +export async function codeGenDialog() { + const filePath = await Commands.selectCodegenFolder(); + const splitPath = filePath.split(path.sep() + "java" + path.sep()) + if (splitPath.length === 1) { + toast.error("Invalid path: make sure your code generation root points to a java directory.") + return + } + localStorage.setItem(LocalStorageKeys.JAVA_ROOT, splitPath[0] + "/java/") + localStorage.setItem(LocalStorageKeys.CODE_GEN_PACKAGE, splitPath[1]) +} + +export async function disableCodegen() { + localStorage.removeItem(LocalStorageKeys.JAVA_ROOT) + localStorage.removeItem(LocalStorageKeys.CODE_GEN_PACKAGE) + toast.info("Choreo Codegen was disabled.") +} + export async function saveProjectDialog() { const filePath = await save({ title: "Save Document", diff --git a/src/document/tauriCommands.ts b/src/document/tauriCommands.ts index 2cf1460b44..2ace9cc27a 100644 --- a/src/document/tauriCommands.ts +++ b/src/document/tauriCommands.ts @@ -141,5 +141,16 @@ export const Commands = { * Opens the platforms file explorer to the directory holding a newly generated diagnostic zip file. */ openDiagnosticZip: (project: Project, trajectories: Trajectory[]) => - invoke("open_diagnostic_file", { project, trajectories }) + invoke("open_diagnostic_file", { project, trajectories }), + + /** + * Writes raw file content to a specified path. + */ + writeRawFile: (content: string, filePath: string) => + invoke("write_raw_file", { content, filePath }), + + /** + * Sets the directory path to push generated java files to. + */ + selectCodegenFolder: () => invoke("select_codegen_folder") }; diff --git a/src/util/LocalStorageKeys.ts b/src/util/LocalStorageKeys.ts index 05d97f8c42..289c496807 100644 --- a/src/util/LocalStorageKeys.ts +++ b/src/util/LocalStorageKeys.ts @@ -1,6 +1,8 @@ enum LocalStorageKeys { LAST_OPENED_FILE_LOCATION = "last_opened_project_payload", - PATH_GRADIENT = "path_gradient" + PATH_GRADIENT = "path_gradient", + JAVA_ROOT = "java_root", + CODE_GEN_PACKAGE = "code_gen_package", } export default LocalStorageKeys; From 0e9b8faa2d900e70f33d81ee8315cefbe85df252 Mon Sep 17 00:00:00 2001 From: Daniel1464 Date: Fri, 15 Aug 2025 14:41:57 -0400 Subject: [PATCH 2/4] lint --- src-tauri/src/api.rs | 26 +++++------ src/AppMenu.tsx | 22 ++++----- src/codegen/genConstsFile.ts | 80 ++++++++++++++++++--------------- src/codegen/genVarsFile.ts | 61 ++++++++++++------------- src/codegen/internals.ts | 65 ++++++++++++++++++--------- src/document/DocumentManager.ts | 50 ++++++++++++--------- src/document/tauriCommands.ts | 4 +- src/util/LocalStorageKeys.ts | 2 +- 8 files changed, 175 insertions(+), 135 deletions(-) diff --git a/src-tauri/src/api.rs b/src-tauri/src/api.rs index 44e9b89abe..f9b97a80ba 100644 --- a/src-tauri/src/api.rs +++ b/src-tauri/src/api.rs @@ -1,7 +1,7 @@ #![allow(clippy::needless_pass_by_value)] -use std::path::PathBuf; use std::fs; +use std::path::PathBuf; use crate::tauri::TauriResult; use choreo_core::{ @@ -58,19 +58,17 @@ pub async fn open_in_explorer(path: String) -> TauriResult<()> { #[tauri::command] pub async fn select_codegen_folder(app_handle: tauri::AppHandle) -> TauriResult { - Ok( - app_handle - .dialog() - .file() - .set_title("Select a folder to output generated java files") - .blocking_pick_folder() - .ok_or(ChoreoError::FileNotFound(None))? - .as_path() - .ok_or(ChoreoError::FileNotFound(None))? - .to_str() - .ok_or(ChoreoError::FileNotFound(None))? - .to_string() - ) + Ok(app_handle + .dialog() + .file() + .set_title("Select a folder to output generated java files") + .blocking_pick_folder() + .ok_or(ChoreoError::FileNotFound(None))? + .as_path() + .ok_or(ChoreoError::FileNotFound(None))? + .to_str() + .ok_or(ChoreoError::FileNotFound(None))? + .to_string()) } #[tauri::command] diff --git a/src/AppMenu.tsx b/src/AppMenu.tsx index 80ab94b7a3..add3a59071 100644 --- a/src/AppMenu.tsx +++ b/src/AppMenu.tsx @@ -208,16 +208,18 @@ class AppMenu extends Component { - {codegenEnabled() && { - disableCodegen(); - }} - > - - - - - } + {codegenEnabled() && ( + { + disableCodegen(); + }} + > + + + + + + )} {/* Info about save locations */} diff --git a/src/codegen/genConstsFile.ts b/src/codegen/genConstsFile.ts index 01bf4ddfff..07df4762b3 100644 --- a/src/codegen/genConstsFile.ts +++ b/src/codegen/genConstsFile.ts @@ -2,9 +2,9 @@ import { Project } from "../document/2025/DocumentTypes"; import { writeConst } from "./internals"; export function genConstsFile(project: Project, packageName: string): string { - let out: string[] = []; - out.push(`package ${packageName};`); - out.push(` + const out: string[] = []; + out.push(`package ${packageName};`); + out.push(` import edu.wpi.first.math.geometry.Translation2d; import edu.wpi.first.units.measure.*; import static edu.wpi.first.units.Units.*; @@ -16,46 +16,52 @@ import static edu.wpi.first.units.Units.*; */ public final class ChoreoConsts {`); - const config = project.config; - const maxLinearVel = config.vmax.val / config.gearing.val * config.radius.val; - const maxWheelForce = config.tmax.val * config.gearing.val * 4 / config.radius.val; - const maxLinearAccel = maxWheelForce / config.mass.val; + const config = project.config; + const maxLinearVel = + (config.vmax.val / config.gearing.val) * config.radius.val; + const maxWheelForce = + (config.tmax.val * config.gearing.val * 4) / config.radius.val; + const maxLinearAccel = maxWheelForce / config.mass.val; - out.push(writeConst(config.gearing, 'gearing', 'Number')); - out.push(writeConst(config.cof, 'frictionCoefficient', 'Number')); - out.push(writeConst(config.radius, 'wheelRadius', 'Length')); - out.push(writeConst(config.inertia, "moi", 'MoI')); - out.push(writeConst(config.mass, 'mass', 'Mass')); - out.push(writeConst(config.tmax, 'driveMotorMaxTorque', 'Torque')); + out.push(writeConst(config.gearing, "gearing", "Number")); + out.push(writeConst(config.cof, "frictionCoefficient", "Number")); + out.push(writeConst(config.radius, "wheelRadius", "Length")); + out.push(writeConst(config.inertia, "moi", "MoI")); + out.push(writeConst(config.mass, "mass", "Mass")); + out.push(writeConst(config.tmax, "driveMotorMaxTorque", "Torque")); - const bumperWB = config.bumper.front.val + config.bumper.back.val; - const bumperTW = config.bumper.side.val * 2; - out.push(writeConst(bumperWB, "wheelBaseWithBumpers", 'Length')); - out.push(writeConst(bumperTW, "trackWidthWithBumpers", 'Length')); + const bumperWB = config.bumper.front.val + config.bumper.back.val; + const bumperTW = config.bumper.side.val * 2; + out.push(writeConst(bumperWB, "wheelBaseWithBumpers", "Length")); + out.push(writeConst(bumperTW, "trackWidthWithBumpers", "Length")); - if (project.type === 'Swerve') { - out.push(` + if (project.type === "Swerve") { + out.push(` public static final Translation2d[] moduleTranslations = { new Translation2d(${config.frontLeft.x.val}, ${config.frontLeft.y.val}), new Translation2d(${config.frontLeft.x.val}, ${-config.frontLeft.y.val}), new Translation2d(${config.backLeft.x.val}, ${config.backLeft.y.val}), new Translation2d(${config.backLeft.x.val}, ${-config.backLeft.y.val}), };`); - const drivebaseRadius = Math.hypot(config.frontLeft.x.val, config.frontLeft.y.val); - const frictionFloorForce = config.mass.val * 9.81 * config.cof.val; - const minLinearForce = Math.min(frictionFloorForce, maxWheelForce); - const maxAngularVel = maxLinearVel / drivebaseRadius; - const maxAngularAccel = minLinearForce * drivebaseRadius / config.inertia.val; - out.push(writeConst(maxAngularVel, 'maxAngularVel', 'AngVel')); - out.push(writeConst(maxAngularAccel, 'maxAngularAccel', 'AngAcc')); - } else { - out.push(writeConst(config.differentialTrackWidth, 'trackWidth', 'Length')); - out.push(''); - } - out.push(writeConst(maxLinearVel, 'maxLinearVel', 'LinVel')); - out.push(writeConst(maxLinearAccel, 'maxLinearAccel', 'LinAcc')); - out.push(''); - out.push(' private ChoreoConsts() {}'); - out.push('}'); - return out.join('\n'); -} \ No newline at end of file + const drivebaseRadius = Math.hypot( + config.frontLeft.x.val, + config.frontLeft.y.val + ); + const frictionFloorForce = config.mass.val * 9.81 * config.cof.val; + const minLinearForce = Math.min(frictionFloorForce, maxWheelForce); + const maxAngularVel = maxLinearVel / drivebaseRadius; + const maxAngularAccel = + (minLinearForce * drivebaseRadius) / config.inertia.val; + out.push(writeConst(maxAngularVel, "maxAngularVel", "AngVel")); + out.push(writeConst(maxAngularAccel, "maxAngularAccel", "AngAcc")); + } else { + out.push(writeConst(config.differentialTrackWidth, "trackWidth", "Length")); + out.push(""); + } + out.push(writeConst(maxLinearVel, "maxLinearVel", "LinVel")); + out.push(writeConst(maxLinearAccel, "maxLinearAccel", "LinAcc")); + out.push(""); + out.push(" private ChoreoConsts() {}"); + out.push("}"); + return out.join("\n"); +} diff --git a/src/codegen/genVarsFile.ts b/src/codegen/genVarsFile.ts index c298ab65b2..8e609a544f 100644 --- a/src/codegen/genVarsFile.ts +++ b/src/codegen/genVarsFile.ts @@ -2,11 +2,11 @@ import { Project } from "../document/2025/DocumentTypes"; import { round, writeConst } from "./internals"; export function genVarsFile(project: Project, packageName: string): string { - const expressions = project.variables.expressions - const poses = project.variables.poses - let out: string[] = []; - out.push(`package ${packageName};\n`); - out.push(` + const expressions = project.variables.expressions; + const poses = project.variables.poses; + const out: string[] = []; + out.push(`package ${packageName};\n`); + out.push(` import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.units.measure.*; @@ -18,28 +18,29 @@ import static edu.wpi.first.units.Units.*; * in the choreo GUI. */ public final class ChoreoVars {`); - for (const varName in project.variables.expressions) { - const data = expressions[varName]; - out.push(writeConst(data.var, varName, data.dimension)) - } - out.push(''); - out.push(' public static final class Poses {'); - for (const poseName in poses) { - const pose = poses[poseName]; - const heading = Math.abs(pose.heading.val) < 1e-5 - ? 'Rotation2d.kZero' - : `Rotation2d.fromRadians(${round(pose.heading.val, 3)})`; - const x = round(pose.x.val, 3) - const y = round(pose.y.val, 3) - out.push( - ` public static final Pose2d ${poseName} = new Pose2d(${x}, ${y}, ${heading});` - ); - } - out.push(''); - out.push(' private Poses() {}'); - out.push(' }'); - out.push(''); - out.push(' private ChoreoVars() {}'); - out.push('}'); - return out.join('\n'); -} \ No newline at end of file + for (const varName in project.variables.expressions) { + const data = expressions[varName]; + out.push(writeConst(data.var, varName, data.dimension)); + } + out.push(""); + out.push(" public static final class Poses {"); + for (const poseName in poses) { + const pose = poses[poseName]; + const heading = + Math.abs(pose.heading.val) < 1e-5 + ? "Rotation2d.kZero" + : `Rotation2d.fromRadians(${round(pose.heading.val, 3)})`; + const x = round(pose.x.val, 3); + const y = round(pose.y.val, 3); + out.push( + ` public static final Pose2d ${poseName} = new Pose2d(${x}, ${y}, ${heading});` + ); + } + out.push(""); + out.push(" private Poses() {}"); + out.push(" }"); + out.push(""); + out.push(" private ChoreoVars() {}"); + out.push("}"); + return out.join("\n"); +} diff --git a/src/codegen/internals.ts b/src/codegen/internals.ts index d5ae4f3ad7..d844666937 100644 --- a/src/codegen/internals.ts +++ b/src/codegen/internals.ts @@ -6,37 +6,60 @@ export interface UnitData { baseUnit: string; } -export function unitDataFrom(choreoDimensionName: DimensionName): UnitData | null { +export function unitDataFrom( + choreoDimensionName: DimensionName +): UnitData | null { switch (choreoDimensionName) { - case "LinAcc": return { type: "LinearAcceleration", baseUnit: "MetersPerSecondPerSecond" }; - case "LinVel": return { type: "LinearVelocity", baseUnit: "MetersPerSecond" }; - case "Length": return { type: "Distance", baseUnit: "Meters" }; - case "Angle": return { type: "Angle", baseUnit: "Radians" }; - case "AngVel": return { type: "AngularVelocity", baseUnit: "RadiansPerSecond" }; - case "AngAcc": return { type: "AngularAcceleration", baseUnit: "RadiansPerSecondPerSecond" }; - case "Time": return { type: "Time", baseUnit: "Seconds" }; - case "Mass": return { type: "Mass", baseUnit: "Kilograms" }; - case "Torque": return { type: "Torque", baseUnit: "NewtonMeters" }; - case "MoI": return { type: "MomentOfInertia", baseUnit: "KilogramSquareMeters" }; - default: return null; + case "LinAcc": + return { + type: "LinearAcceleration", + baseUnit: "MetersPerSecondPerSecond" + }; + case "LinVel": + return { type: "LinearVelocity", baseUnit: "MetersPerSecond" }; + case "Length": + return { type: "Distance", baseUnit: "Meters" }; + case "Angle": + return { type: "Angle", baseUnit: "Radians" }; + case "AngVel": + return { type: "AngularVelocity", baseUnit: "RadiansPerSecond" }; + case "AngAcc": + return { + type: "AngularAcceleration", + baseUnit: "RadiansPerSecondPerSecond" + }; + case "Time": + return { type: "Time", baseUnit: "Seconds" }; + case "Mass": + return { type: "Mass", baseUnit: "Kilograms" }; + case "Torque": + return { type: "Torque", baseUnit: "NewtonMeters" }; + case "MoI": + return { type: "MomentOfInertia", baseUnit: "KilogramSquareMeters" }; + default: + return null; } } -export function writeConst(expr: Expr | number, variableName: string, dimension: DimensionName): string { - const unitData = unitDataFrom(dimension) - let val = typeof expr === 'number' ? expr : expr.val - val = round(val, dimension === "MoI" ? 5 : 3) +export function writeConst( + expr: Expr | number, + variableName: string, + dimension: DimensionName +): string { + const unitData = unitDataFrom(dimension); + let val = typeof expr === "number" ? expr : expr.val; + val = round(val, dimension === "MoI" ? 5 : 3); if (!dimension || !unitData) { - return ` public static final double ${variableName} = ${val};`; + return ` public static final double ${variableName} = ${val};`; } return ` public static final ${unitData.type} ${variableName} = ${unitData.baseUnit}.of(${val});`; } export function writeVar(variable: Variable, name: string): string { - return writeConst(variable.var, name, variable.dimension) + return writeConst(variable.var, name, variable.dimension); } export function round(val: number, digits: number) { - const roundingFactor = Math.pow(10, digits) - return Math.round(val * roundingFactor) / roundingFactor -} \ No newline at end of file + const roundingFactor = Math.pow(10, digits); + return Math.round(val * roundingFactor) / roundingFactor; +} diff --git a/src/document/DocumentManager.ts b/src/document/DocumentManager.ts index 16d34e26d6..f053ff098a 100644 --- a/src/document/DocumentManager.ts +++ b/src/document/DocumentManager.ts @@ -708,7 +708,7 @@ export async function newProject(promptForCodegen: boolean = true) { uiState.setProjectSavingTime(new Date()); doc.history.clear(); if (!promptForCodegen) { - return + return; } const msg = codegenEnabled() ? ` @@ -719,7 +719,7 @@ export async function newProject(promptForCodegen: boolean = true) { Choreo can now output java files containing variables and constants defined in the GUI. Select a folder to enable this feature? - ` + `; if (await ask(msg, { title: "Choreo", kind: "warning" })) { await codeGenDialog(); } @@ -795,24 +795,32 @@ export async function writeAllTrajectories() { } export function codegenEnabled() { - return localStorage.getItem(LocalStorageKeys.JAVA_ROOT) != null + return localStorage.getItem(LocalStorageKeys.JAVA_ROOT) != null; } export async function saveProject() { if (await canSave()) { try { - const data = doc.serializeChor() - const javaRoot = localStorage.getItem(LocalStorageKeys.JAVA_ROOT) - const packageName = localStorage.getItem(LocalStorageKeys.CODE_GEN_PACKAGE) - let tasks = [Commands.writeProject(data)] + const data = doc.serializeChor(); + const javaRoot = localStorage.getItem(LocalStorageKeys.JAVA_ROOT); + const packageName = localStorage.getItem( + LocalStorageKeys.CODE_GEN_PACKAGE + ); + let tasks = [Commands.writeProject(data)]; if (javaRoot && packageName) { - const constsFile = genConstsFile(data, packageName) - const varsFile = genVarsFile(data, packageName) + const constsFile = genConstsFile(data, packageName); + const varsFile = genVarsFile(data, packageName); tasks = [ ...tasks, - Commands.writeRawFile(constsFile, javaRoot + packageName + "/ChoreoConsts.java"), - Commands.writeRawFile(varsFile, javaRoot + packageName + "/ChoreoVars.java") - ] + Commands.writeRawFile( + constsFile, + javaRoot + packageName + "/ChoreoConsts.java" + ), + Commands.writeRawFile( + varsFile, + javaRoot + packageName + "/ChoreoVars.java" + ) + ]; } await toast.promise(Promise.all(tasks), { error: { @@ -834,19 +842,21 @@ export async function saveProject() { export async function codeGenDialog() { const filePath = await Commands.selectCodegenFolder(); - const splitPath = filePath.split(path.sep() + "java" + path.sep()) + const splitPath = filePath.split(path.sep() + "java" + path.sep()); if (splitPath.length === 1) { - toast.error("Invalid path: make sure your code generation root points to a java directory.") - return + toast.error( + "Invalid path: make sure your code generation root points to a java directory." + ); + return; } - localStorage.setItem(LocalStorageKeys.JAVA_ROOT, splitPath[0] + "/java/") - localStorage.setItem(LocalStorageKeys.CODE_GEN_PACKAGE, splitPath[1]) + localStorage.setItem(LocalStorageKeys.JAVA_ROOT, splitPath[0] + "/java/"); + localStorage.setItem(LocalStorageKeys.CODE_GEN_PACKAGE, splitPath[1]); } export async function disableCodegen() { - localStorage.removeItem(LocalStorageKeys.JAVA_ROOT) - localStorage.removeItem(LocalStorageKeys.CODE_GEN_PACKAGE) - toast.info("Choreo Codegen was disabled.") + localStorage.removeItem(LocalStorageKeys.JAVA_ROOT); + localStorage.removeItem(LocalStorageKeys.CODE_GEN_PACKAGE); + toast.info("Choreo Codegen was disabled."); } export async function saveProjectDialog() { diff --git a/src/document/tauriCommands.ts b/src/document/tauriCommands.ts index 2ace9cc27a..cc3a81ebbc 100644 --- a/src/document/tauriCommands.ts +++ b/src/document/tauriCommands.ts @@ -146,11 +146,11 @@ export const Commands = { /** * Writes raw file content to a specified path. */ - writeRawFile: (content: string, filePath: string) => + writeRawFile: (content: string, filePath: string) => invoke("write_raw_file", { content, filePath }), /** - * Sets the directory path to push generated java files to. + * Sets the directory path to push generated java files to. */ selectCodegenFolder: () => invoke("select_codegen_folder") }; diff --git a/src/util/LocalStorageKeys.ts b/src/util/LocalStorageKeys.ts index 289c496807..dacc4d5dc4 100644 --- a/src/util/LocalStorageKeys.ts +++ b/src/util/LocalStorageKeys.ts @@ -2,7 +2,7 @@ enum LocalStorageKeys { LAST_OPENED_FILE_LOCATION = "last_opened_project_payload", PATH_GRADIENT = "path_gradient", JAVA_ROOT = "java_root", - CODE_GEN_PACKAGE = "code_gen_package", + CODE_GEN_PACKAGE = "code_gen_package" } export default LocalStorageKeys; From eca4d2c1378c91cf1976ecae34e8dc4cf4c72bdd Mon Sep 17 00:00:00 2001 From: Daniel Chen <108989218+Daniel1464@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:45:54 -0400 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: Tyler Veness --- src/AppMenu.tsx | 2 +- src/codegen/genConstsFile.ts | 2 +- src/codegen/genVarsFile.ts | 2 +- src/document/DocumentManager.ts | 4 ++-- src/document/tauriCommands.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/AppMenu.tsx b/src/AppMenu.tsx index add3a59071..2d4d91bf2f 100644 --- a/src/AppMenu.tsx +++ b/src/AppMenu.tsx @@ -195,7 +195,7 @@ class AppMenu extends Component { { diff --git a/src/codegen/genConstsFile.ts b/src/codegen/genConstsFile.ts index 07df4762b3..65182bb89d 100644 --- a/src/codegen/genConstsFile.ts +++ b/src/codegen/genConstsFile.ts @@ -10,7 +10,7 @@ import edu.wpi.first.units.measure.*; import static edu.wpi.first.units.Units.*; /** - * Generated file containing document settings for your choreo project. + * Generated file containing document settings for your Choreo project. * This allows for modifying constants in choreo while keeping your robot code up-to-date. * DO NOT MODIFY this file yourself, as it is auto-generated. */ diff --git a/src/codegen/genVarsFile.ts b/src/codegen/genVarsFile.ts index 8e609a544f..6a68b708e7 100644 --- a/src/codegen/genVarsFile.ts +++ b/src/codegen/genVarsFile.ts @@ -13,7 +13,7 @@ import edu.wpi.first.units.measure.*; import static edu.wpi.first.units.Units.*; /** - * Generated file containing variables defined in choreo. + * Generated file containing variables defined in Choreo. * DO NOT MODIFY THIS YOURSELF; instead, change these values * in the choreo GUI. */ diff --git a/src/document/DocumentManager.ts b/src/document/DocumentManager.ts index f053ff098a..3b18e6419c 100644 --- a/src/document/DocumentManager.ts +++ b/src/document/DocumentManager.ts @@ -712,7 +712,7 @@ export async function newProject(promptForCodegen: boolean = true) { } const msg = codegenEnabled() ? ` - Change the folder in which java file generation occurs? + Change the folder in which Java file generation occurs? (Do this if you're switching to a new codebase) ` : ` @@ -845,7 +845,7 @@ export async function codeGenDialog() { const splitPath = filePath.split(path.sep() + "java" + path.sep()); if (splitPath.length === 1) { toast.error( - "Invalid path: make sure your code generation root points to a java directory." + "Invalid path: make sure your code generation root points to a \"java\" directory." ); return; } diff --git a/src/document/tauriCommands.ts b/src/document/tauriCommands.ts index cc3a81ebbc..68c57e4baa 100644 --- a/src/document/tauriCommands.ts +++ b/src/document/tauriCommands.ts @@ -150,7 +150,7 @@ export const Commands = { invoke("write_raw_file", { content, filePath }), /** - * Sets the directory path to push generated java files to. + * Sets the directory path to push generated Java files to. */ selectCodegenFolder: () => invoke("select_codegen_folder") }; From c71b4023ce5befad263da51b29c10813a5dedb3d Mon Sep 17 00:00:00 2001 From: Daniel Chen <108989218+Daniel1464@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:47:04 -0400 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Tyler Veness --- src-tauri/src/api.rs | 2 +- src/document/DocumentManager.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/api.rs b/src-tauri/src/api.rs index f9b97a80ba..723de767eb 100644 --- a/src-tauri/src/api.rs +++ b/src-tauri/src/api.rs @@ -61,7 +61,7 @@ pub async fn select_codegen_folder(app_handle: tauri::AppHandle) -> TauriResult< Ok(app_handle .dialog() .file() - .set_title("Select a folder to output generated java files") + .set_title("Select a folder to output generated Java files") .blocking_pick_folder() .ok_or(ChoreoError::FileNotFound(None))? .as_path() diff --git a/src/document/DocumentManager.ts b/src/document/DocumentManager.ts index 3b18e6419c..c8a8ea458d 100644 --- a/src/document/DocumentManager.ts +++ b/src/document/DocumentManager.ts @@ -716,7 +716,7 @@ export async function newProject(promptForCodegen: boolean = true) { (Do this if you're switching to a new codebase) ` : ` - Choreo can now output java files + Choreo can now output Java files containing variables and constants defined in the GUI. Select a folder to enable this feature? `;