From f74d68bd177512bdc2c0242ac15962e0c89951d5 Mon Sep 17 00:00:00 2001 From: cavaire Date: Sat, 4 May 2024 10:21:13 -0500 Subject: [PATCH 01/16] =?UTF-8?q?=E2=9C=A8=20Add=20xVeclib=20Boomerang=20f?= =?UTF-8?q?ormat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 2 +- src/format/xVecLibBoomerangFormatV0_1.tsx | 298 ++++++++++++++++++++++ 2 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 src/format/xVecLibBoomerangFormatV0_1.tsx diff --git a/package-lock.json b/package-lock.json index e42cebb..e7ed6a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "path.jerryio", - "version": "0.6.0", + "version": "0.7.0", "dependencies": { "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", diff --git a/src/format/xVecLibBoomerangFormatV0_1.tsx b/src/format/xVecLibBoomerangFormatV0_1.tsx new file mode 100644 index 0000000..d5cefdf --- /dev/null +++ b/src/format/xVecLibBoomerangFormatV0_1.tsx @@ -0,0 +1,298 @@ +import { makeAutoObservable, action } from "mobx"; +import { MainApp, getAppStores } from "@core/MainApp"; +import { EditableNumberRange, IS_MAC_OS, ValidateNumber, getMacHotKeyString, makeId } from "@core/Util"; +import { BentRateApplicationDirection, Path, Segment, Vector } from "@core/Path"; +import { UnitOfLength, UnitConverter, Quantity } from "@core/Unit"; +import { GeneralConfig, PathConfig, convertFormat, initGeneralConfig } from "./Config"; +import { Format, importPDJDataFromTextFile } from "./Format"; +import { Exclude, Expose, Type } from "class-transformer"; +import { IsBoolean, IsObject, IsPositive, IsString, MinLength, ValidateNested } from "class-validator"; +import { PointCalculationResult, getPathPoints, getDiscretePoints, fromDegreeToRadian } from "@core/Calculation"; +import { FieldImageOriginType, FieldImageSignatureAndOrigin, getDefaultBuiltInFieldImage } from "@core/Asset"; +import { UpdateProperties } from "@core/Command"; +import { ObserverInput } from "@app/component.blocks/ObserverInput"; +import { Box, Button, Typography } from "@mui/material"; +import { euclideanRotation } from "@core/Coordinate"; +import { CodePointBuffer, Int } from "../token/Tokens"; +import { observer } from "mobx-react-lite"; +import { enqueueErrorSnackbar, enqueueSuccessSnackbar } from "@app/Notice"; +import { Logger } from "@core/Logger"; +import { getEnableOnNonTextInputFieldsHotkeysOptions, useCustomHotkeys } from "@core/Hook"; +import { ObserverCheckbox } from "@app/component.blocks/ObserverCheckbox"; + +const logger = Logger("xVecLib Boomerang v0.1.0 (inch)"); + +const GeneralConfigPanel = observer((props: { config: GeneralConfigImpl }) => { + const { config } = props; + + const { app, confirmation, modals } = getAppStores(); + + const isUsingEditor = !confirmation.isOpen && !modals.isOpen; + + const onCopyCode = action(() => { + try { + const code = config.format.exportCode(); + + navigator.clipboard.writeText(code); + + enqueueSuccessSnackbar(logger, "Copied"); + } catch (e) { + enqueueErrorSnackbar(logger, e); + } + }); + + useCustomHotkeys("Shift+Mod+C", onCopyCode, getEnableOnNonTextInputFieldsHotkeysOptions(isUsingEditor)); + + const hotkey = IS_MAC_OS ? getMacHotKeyString("Shift+Mod+C") : "Shift+Ctrl+C"; + + return ( + <> + + Export Settings + + config.chassisName} + setValue={(value: string) => { + app.history.execute(`Change chassis variable name`, new UpdateProperties(config, { chassisName: value })); + }} + isValidIntermediate={() => true} + isValidValue={(candidate: string) => candidate !== ""} + sx={{ marginTop: "16px" }} + /> + config.movementTimeout.toString()} + setValue={(value: string) => { + const parsedValue = parseInt(Int.parse(new CodePointBuffer(value))!.value); + app.history.execute( + `Change default movement timeout to ${parsedValue}`, + new UpdateProperties(config, { movementTimeout: parsedValue }) + ); + }} + isValidIntermediate={() => true} + isValidValue={(candidate: string) => Int.parse(new CodePointBuffer(candidate)) !== null} + sx={{ marginTop: "16px" }} + numeric + /> + + + { + app.history.execute( + `Set using relative coordinates to ${value}`, + new UpdateProperties(config, { relativeCoords: value }) + ); + }} + /> + + + + + + + ); +}); + +// observable class +class GeneralConfigImpl implements GeneralConfig { + @IsPositive() + @Expose() + robotWidth: number = 12; + @IsPositive() + @Expose() + robotHeight: number = 12; + @IsBoolean() + @Expose() + robotIsHolonomic: boolean = false; + @IsBoolean() + @Expose() + showRobot: boolean = false; + @ValidateNumber(num => num > 0 && num <= 1000) // Don't use IsEnum + @Expose() + uol: UnitOfLength = UnitOfLength.Inch; + @IsPositive() + @Expose() + pointDensity: number = 2; // inches + @IsPositive() + @Expose() + controlMagnetDistance: number = 5 / 2.54; + @Type(() => FieldImageSignatureAndOrigin) + @ValidateNested() + @IsObject() + @Expose() + fieldImage: FieldImageSignatureAndOrigin = + getDefaultBuiltInFieldImage().getSignatureAndOrigin(); + @IsString() + @MinLength(1) + @Expose() + chassisName: string = "chassis"; + @ValidateNumber(num => num >= 0) + @Expose() + movementTimeout: number = 5000; + @IsBoolean() + @Expose() + relativeCoords: boolean = true; + @Exclude() + private format_: xVecLibBoomerangFormatV0_1; + + constructor(format: xVecLibBoomerangFormatV0_1) { + this.format_ = format; + makeAutoObservable(this); + + initGeneralConfig(this); + } + + get format() { + return this.format_; + } + + getConfigPanel() { + return ; + } +} + +// observable class +class PathConfigImpl implements PathConfig { + @Exclude() + speedLimit: EditableNumberRange = { + minLimit: { value: 0, label: "" }, + maxLimit: { value: 0, label: "" }, + step: 0, + from: 0, + to: 0 + }; + @Exclude() + bentRateApplicableRange: EditableNumberRange = { + minLimit: { value: 0, label: "" }, + maxLimit: { value: 0, label: "" }, + step: 0, + from: 0, + to: 0 + }; + @Exclude() + bentRateApplicationDirection = BentRateApplicationDirection.HighToLow; + + @Exclude() + readonly format: xVecLibBoomerangFormatV0_1; + + @Exclude() + public path!: Path; + + constructor(format: xVecLibBoomerangFormatV0_1) { + this.format = format; + makeAutoObservable(this); + } + + getConfigPanel() { + return ( + <> + (No setting) + + ); + } +} + +// observable class +export class xVecLibBoomerangFormatV0_1 implements Format { + isInit: boolean = false; + uid: string; + + private gc = new GeneralConfigImpl(this); + + constructor() { + this.uid = makeId(10); + makeAutoObservable(this); + } + + createNewInstance(): Format { + return new xVecLibBoomerangFormatV0_1(); + } + + getName(): string { + return "xVecLib Boomerang v0.1.0 (inch)"; + } + + register(app: MainApp): void { + if (this.isInit) return; + this.isInit = true; + } + + unregister(app: MainApp): void {} + + getGeneralConfig(): GeneralConfig { + return this.gc; + } + + createPath(...segments: Segment[]): Path { + return new Path(new PathConfigImpl(this), ...segments); + } + + getPathPoints(path: Path): PointCalculationResult { + const result = getPathPoints(path, new Quantity(this.gc.pointDensity, this.gc.uol)); + return result; + } + + convertFromFormat(oldFormat: Format, oldPaths: Path[]): Path[] { + return convertFormat(this, oldFormat, oldPaths); + } + + importPathsFromFile(buffer: ArrayBuffer): Path[] { + throw new Error("Unable to import paths from this format, try other formats?"); + } + + exportCode(): string { + const { app } = getAppStores(); + + let rtn = ""; + const gc = app.gc as GeneralConfigImpl; + + const path = app.interestedPath(); + if (path === undefined) throw new Error("No path to export"); + if (path.segments.length === 0) throw new Error("No segment to export"); + + const uc = new UnitConverter(this.gc.uol, UnitOfLength.Inch); + const points = getDiscretePoints(path); + + if (points.length > 0) { + const start = points[0]; + let heading = 0; + + if (start.heading !== undefined && gc.relativeCoords) { + heading = fromDegreeToRadian(start.heading); + } + + // ALGO: Offsets to convert the absolute coordinates to the relative coordinates LemLib uses + const offsets = gc.relativeCoords ? new Vector(start.x, start.y) : new Vector(0, 0); + for (const point of points) { + // ALGO: Only coordinate points are supported in LemLibOdom format + const relative = euclideanRotation(heading, point.subtract(offsets)); + rtn += `${gc.chassisName}.moveTo(${uc.fromAtoB(relative.x).toUser()}, ${uc.fromAtoB(relative.y).toUser()}, ${ + gc.movementTimeout + });\n`; + } + } + + return rtn; + } + + importPDJDataFromFile(buffer: ArrayBuffer): Record | undefined { + return importPDJDataFromTextFile(buffer); + } + + exportFile(): ArrayBuffer { + const { app } = getAppStores(); + + let fileContent = this.exportCode(); + + fileContent += "\n"; + + fileContent += "#PATH.JERRYIO-DATA " + JSON.stringify(app.exportPDJData()); + + return new TextEncoder().encode(fileContent); + } +} From 8b270c650e5ff28670df7964fcb2749aefb76f28 Mon Sep 17 00:00:00 2001 From: cavaire Date: Tue, 7 May 2024 18:12:54 -0500 Subject: [PATCH 02/16] :sparkles: Finished Boomerang generation --- .vscode/settings.json | 5 +- src/format/Format.tsx | 3 +- src/format/xVecLibBoomerangFormatV0_1.tsx | 63 ++++++++++++----------- 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 335f886..20c6005 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[typescriptreact]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + } } \ No newline at end of file diff --git a/src/format/Format.tsx b/src/format/Format.tsx index 54b313a..28598a3 100644 --- a/src/format/Format.tsx +++ b/src/format/Format.tsx @@ -11,6 +11,7 @@ import { LemLibFormatV1_0 } from "./LemLibFormatV1_0"; import { isExperimentalFeaturesEnabled } from "@core/Preferences"; import { RigidCodeGenFormatV0_1 } from "./RigidCodeGenFormatV0_1"; import { MoveToPointCodeGenFormatV0_1 } from "./MoveToPointCodeGenFormatV0_1"; +import { xVecLibBoomerangFormatV0_1 } from "./xVecLibBoomerangFormatV0_1"; export interface Format { isInit: boolean; @@ -83,7 +84,7 @@ export function getAllFormats(): Format[] { new LemLibOdomGeneratorFormatV0_4() ], ...(isExperimentalFeaturesEnabled() - ? [new LemLibFormatV1_0(), new RigidCodeGenFormatV0_1(), new MoveToPointCodeGenFormatV0_1()] + ? [new LemLibFormatV1_0(), new RigidCodeGenFormatV0_1(), new MoveToPointCodeGenFormatV0_1(), new xVecLibBoomerangFormatV0_1()] : []), ...[new PathDotJerryioFormatV0_1()] ]; diff --git a/src/format/xVecLibBoomerangFormatV0_1.tsx b/src/format/xVecLibBoomerangFormatV0_1.tsx index d5cefdf..7168e47 100644 --- a/src/format/xVecLibBoomerangFormatV0_1.tsx +++ b/src/format/xVecLibBoomerangFormatV0_1.tsx @@ -1,7 +1,7 @@ import { makeAutoObservable, action } from "mobx"; import { MainApp, getAppStores } from "@core/MainApp"; import { EditableNumberRange, IS_MAC_OS, ValidateNumber, getMacHotKeyString, makeId } from "@core/Util"; -import { BentRateApplicationDirection, Path, Segment, Vector } from "@core/Path"; +import { BentRateApplicationDirection, Control, Path, Segment, Vector } from "@core/Path"; import { UnitOfLength, UnitConverter, Quantity } from "@core/Unit"; import { GeneralConfig, PathConfig, convertFormat, initGeneralConfig } from "./Config"; import { Format, importPDJDataFromTextFile } from "./Format"; @@ -76,18 +76,6 @@ const GeneralConfigPanel = observer((props: { config: GeneralConfigImpl }) => { numeric /> - - { - app.history.execute( - `Set using relative coordinates to ${value}`, - new UpdateProperties(config, { relativeCoords: value }) - ); - }} - /> - ); }); @@ -313,16 +301,8 @@ export const NewFieldImageForm = observer((props: { variables: FieldImageManager return ( - - + draft.name} setValue={(value: string) => { @@ -334,7 +314,7 @@ export const NewFieldImageForm = observer((props: { variables: FieldImageManager sx={{ flexGrow: 1 }} onKeyDown={e => e.stopPropagation()} /> - draft.heightInMM + ""} setValue={(value: string) => { @@ -349,7 +329,7 @@ export const NewFieldImageForm = observer((props: { variables: FieldImageManager onKeyDown={e => e.stopPropagation()} /> - + Source (Choose One) @@ -363,7 +343,7 @@ export const NewFieldImageForm = observer((props: { variables: FieldImageManager value="url" control={} label={ - {draft.urlValidateResult?.[1] && ( - + {draft.urlValidateResult?.[1]} )} - + - @@ -449,22 +429,99 @@ export const NewFieldImageForm = observer((props: { variables: FieldImageManager }); export const FieldImageSection = observer(() => { + const { app, ui, assetManager } = getAppStores(); + const variables = useMobxStorage(() => new FieldImageManagerVariables(), []); const selected = variables.selected; + const hasSelected = selected !== null; + const isAppliedSelected = selected?.signature === app.gc.fieldImage.signature; + + // eslint-disable-next-line react-hooks/exhaustive-deps + React.useEffect( + action(() => { + variables.selected = assetManager.assets[0] ?? null; + }), + [] + ); + + const onApply = action(() => { + if (selected === null) return; + app.history.execute( + `Change field layer`, + new UpdateProperties(app.gc, { fieldImage: selected?.getSignatureAndOrigin() }) + ); + ui.closeModal(AssetManagerModalSymbol); + }); + + const onDelete = action(() => { + if (selected === null) return; + variables.selected = null; + if (app.gc.fieldImage.signature === selected.signature) { + app.history.execute( + `Use default field layer`, + new UpdateProperties(app.gc, { fieldImage: getDefaultBuiltInFieldImage().getSignatureAndOrigin() }) + ); + } + + if (selected.isOriginType(FieldImageOriginType.External) || selected.isOriginType(FieldImageOriginType.Local)) + assetManager.removeAsset(selected); + }); + + const onClose = () => { + ui.closeModal(AssetManagerModalSymbol); + }; + + const isMobileLayout = React.useContext(LayoutContext) === LayoutType.Mobile; return ( - - Field Image - - - + + + Field Image + + { + if (variables.draft === null) variables.newDraft(); + }}> + + + + + {isMobileLayout && ( + + + + )} + + + {!variables.draft && } {variables.draft && } {selected ? : } + {!variables.draft && ( + + + + + )} ); }); diff --git a/src/app/common.blocks/modal/ConfirmationModal.scss b/src/app/common.blocks/modal/ConfirmationModal.scss old mode 100644 new mode 100755 diff --git a/src/app/common.blocks/modal/ConfirmationModal.tsx b/src/app/common.blocks/modal/ConfirmationModal.tsx old mode 100644 new mode 100755 index da5ce5b..38ed7ce --- a/src/app/common.blocks/modal/ConfirmationModal.tsx +++ b/src/app/common.blocks/modal/ConfirmationModal.tsx @@ -4,7 +4,7 @@ import { getAppStores } from "@core/MainApp"; import { makeAutoObservable, action, when, observable, reaction } from "mobx"; import { observer } from "mobx-react-lite"; import { useMobxStorage } from "@core/Hook"; -import { ObserverInput } from "@app/component.blocks/ObserverInput"; +import { FormInputField } from "@app/component.blocks/FormInputField"; import { Modal } from "./Modal"; import "./ConfirmationModal.scss"; @@ -168,7 +168,7 @@ export const ConfirmationModal = observer(() => { {cfm.input !== undefined && ( - cfm.input ?? ""} setValue={value => (cfm.input = value)} diff --git a/src/app/common.blocks/modal/CoordinateSystemModal.scss b/src/app/common.blocks/modal/CoordinateSystemModal.scss new file mode 100755 index 0000000..839905c --- /dev/null +++ b/src/app/common.blocks/modal/CoordinateSystemModal.scss @@ -0,0 +1,73 @@ +#CoordinateSystemModal { + padding: 16px; + width: 768px; + max-width: 80%; + min-height: 96px; + max-height: 80%; + outline: none !important; + overflow-y: auto; + padding-right: calc(16px - 6.4px); + display: flex; + flex-direction: column; + scrollbar-gutter: stable; + + #CoordinateSystem-Body { + display: flex; + width: 100%; + gap: 16px; + flex-wrap: wrap; + + #CoordinateSystems-LeftSide { + flex-grow: 1000; + max-width: 100%; + width: 360px; + + #CoordinateSystemsList { + flex-grow: 1; + overflow-x: hidden; + overflow-y: auto; + padding-bottom: 8px; // maybe 16px + max-height: 400px; + + .CoordinateSystemsList-Item { + .CoordinateSystemsList-ItemApplyButton { + visibility: hidden; + } + } + + .CoordinateSystemsList-Item:hover { + .CoordinateSystemsList-ItemApplyButton { + visibility: inherit; + } + } + } + } + + #CoordinateSystems-PreviewSection { + width: 40%; + min-width: 256px; + flex-grow: 1; + display: flex; + flex-direction: column; + align-items: center; + + #CoordinateSystems-ImagePreview { + width: 100%; + line-height: 0; + border: 1px solid var(--text-primary-color); + position: relative; + + > img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: contain; + user-select: none; + cursor: pointer; + } + } + } + } +} diff --git a/src/app/common.blocks/modal/CoordinateSystemModal.tsx b/src/app/common.blocks/modal/CoordinateSystemModal.tsx new file mode 100755 index 0000000..5283101 --- /dev/null +++ b/src/app/common.blocks/modal/CoordinateSystemModal.tsx @@ -0,0 +1,212 @@ +import { makeAutoObservable, action } from "mobx"; +import { observer } from "mobx-react-lite"; +import { Modal } from "./Modal"; +import { + Box, + Button, + Card, + Chip, + IconButton, + List, + ListItem, + ListItemButton, + ListItemText, + Tooltip, + Typography +} from "@mui/material"; + +import { NamedCoordinateSystem, getNamedCoordinateSystems } from "@core/CoordinateSystem"; +import { getAppStores } from "@core/MainApp"; +import { UpdateProperties } from "@core/Command"; +import { useMobxStorage } from "@core/Hook"; +import { LayoutContext, LayoutType } from "@core/Layout"; +import "./CoordinateSystemModal.scss"; +import InputIcon from "@mui/icons-material/Input"; +import DoneIcon from "@mui/icons-material/Done"; +import React from "react"; + +export const CoordinateSystemModalSymbol = Symbol("CoordinateSystemModal"); + +class CoordinateSystemVariables { + selected: NamedCoordinateSystem | null = null; + + constructor() { + makeAutoObservable(this); + } +} + +export const CoordinateSystemPreview = observer((props: { preview: NamedCoordinateSystem }) => { + const { preview } = props; + + return ( + + + + + + + + + + {preview.name} + + + {preview.description} + + + + ); +}); + +export const CoordinateSystemItem = observer( + (props: { variables: CoordinateSystemVariables; system: NamedCoordinateSystem }) => { + const { variables, system } = props; + const { app, ui } = getAppStores(); + + const isSelected = variables.selected?.name === system.name; + + const isUsing = system.name === app.gc.coordinateSystem; + console.log("isUsing", isUsing, system.name, app.gc.coordinateSystem); + + const onApply = action(() => { + app.history.execute( + `Change coordinate system to ${system.name}`, + new UpdateProperties(app.gc, { coordinateSystem: system.name }) + ); + ui.closeModal(CoordinateSystemModalSymbol); + }); + + const isMobileLayout = React.useContext(LayoutContext) === LayoutType.Mobile; + + return ( + + + + + + ) + }> + { + variables.selected = system; + })}> + + {system.name} + + + } + /> + {isUsing && !isMobileLayout && } + + + ); + } +); + +export const CoordinateSystemList = observer((props: { variables: CoordinateSystemVariables }) => { + const { variables } = props; + const { app } = getAppStores(); + + const systems = getNamedCoordinateSystems(); + + // eslint-disable-next-line react-hooks/exhaustive-deps + React.useEffect( + action(() => { + variables.selected = systems.find(system => system.name === app.gc.coordinateSystem) || systems[0]; + }), + [] + ); + + return ( + + + + {systems.map(system => ( + + ))} + + + + ); +}); + +export const CoordinateSystemSection = observer(() => { + const { app, ui } = getAppStores(); + + const variables = useMobxStorage(() => new CoordinateSystemVariables(), []); + + const selected = variables.selected; + const hasSelected = !!selected; + const isAppliedSelected = selected?.name === getAppStores().app.gc.coordinateSystem; + + const onApply = action(() => { + if (!selected) return; + + app.history.execute( + `Change coordinate system to ${selected.name}`, + new UpdateProperties(app.gc, { coordinateSystem: selected.name }) + ); + + ui.closeModal(CoordinateSystemModalSymbol); + }); + + const onClose = () => { + ui.closeModal(CoordinateSystemModalSymbol); + }; + + const isMobileLayout = React.useContext(LayoutContext) === LayoutType.Mobile; + + return ( + + + + Frontend Coordinate System + + {isMobileLayout && ( + + + + )} + + + + + + {selected && } + + + + Note: the coordinate system you choose does not affect the output file. + + + + + ); +}); + +export const CoordinateSystemModal = observer(() => { + return ( + + + + + + ); +}); diff --git a/src/app/common.blocks/modal/Modal.scss b/src/app/common.blocks/modal/Modal.scss old mode 100644 new mode 100755 diff --git a/src/app/common.blocks/modal/Modal.tsx b/src/app/common.blocks/modal/Modal.tsx old mode 100644 new mode 100755 diff --git a/src/app/common.blocks/modal/PreferencesModal.scss b/src/app/common.blocks/modal/PreferencesModal.scss old mode 100644 new mode 100755 index d59c14b..522a3e0 --- a/src/app/common.blocks/modal/PreferencesModal.scss +++ b/src/app/common.blocks/modal/PreferencesModal.scss @@ -8,13 +8,4 @@ hr { margin: 16px 0; } - - .PreferencesModal-Title { - margin-top: 16px; - margin-bottom: 16px; - } - - .PreferencesModal-Title:first-child { - margin-top: 0; - } } diff --git a/src/app/common.blocks/modal/PreferencesModal.tsx b/src/app/common.blocks/modal/PreferencesModal.tsx old mode 100644 new mode 100755 index e9b8405..c8a69fa --- a/src/app/common.blocks/modal/PreferencesModal.tsx +++ b/src/app/common.blocks/modal/PreferencesModal.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react-lite"; import { getAppStores } from "@core/MainApp"; import { AppThemeType } from "@app/Theme"; import { clamp } from "@core/Util"; -import { ObserverEnumSelect } from "@app/component.blocks/ObserverEnumSelect"; -import { ObserverCheckbox } from "@app/component.blocks/ObserverCheckbox"; -import { ObserverInput } from "@app/component.blocks/ObserverInput"; +import { FormEnumSelect } from "@app/component.blocks/FormEnumSelect"; +import { FormCheckbox } from "@app/component.blocks/FormCheckbox"; +import { FormInputField } from "@app/component.blocks/FormInputField"; import { Modal } from "./Modal"; import { enqueueInfoSnackbar } from "@app/Notice"; import { Logger } from "@core/Logger"; @@ -21,8 +21,10 @@ export const PreferencesModal = observer(() => { return ( - General - + General + + appPreferences.maxHistory.toString()} @@ -34,8 +36,8 @@ export const PreferencesModal = observer(() => { - Appearance - Appearance + { - Other - Other + (appPreferences.isGoogleAnalyticsEnabled = v)} /> - { diff --git a/src/app/common.blocks/modal/RequireLocalFieldImageModal.scss b/src/app/common.blocks/modal/RequireLocalFieldImageModal.scss old mode 100644 new mode 100755 diff --git a/src/app/common.blocks/modal/RequireLocalFieldImageModal.tsx b/src/app/common.blocks/modal/RequireLocalFieldImageModal.tsx old mode 100644 new mode 100755 diff --git a/src/app/common.blocks/modal/WelcomeForBrave.mdx b/src/app/common.blocks/modal/WelcomeForBrave.mdx old mode 100644 new mode 100755 diff --git a/src/app/common.blocks/modal/WelcomeForOthers.mdx b/src/app/common.blocks/modal/WelcomeForOthers.mdx old mode 100644 new mode 100755 index 2003694..2e92666 --- a/src/app/common.blocks/modal/WelcomeForOthers.mdx +++ b/src/app/common.blocks/modal/WelcomeForOthers.mdx @@ -1,4 +1,4 @@ -import { ObserverCheckbox } from "@app/component.blocks/ObserverCheckbox"; +import { FormCheckbox } from "@app/component.blocks/FormCheckbox"; app logo @@ -21,7 +21,7 @@ This app uses Google Analytics to collect anonymous usage data after leaving thi You can disable it now or at any time on the Preference Page. Please read our [Privacy Policy](https://github.com/Jerrylum/path.jerryio/blob/main/PRIVACY.md) for more information. - + ## Support diff --git a/src/app/common.blocks/modal/WelcomeModal.scss b/src/app/common.blocks/modal/WelcomeModal.scss old mode 100644 new mode 100755 diff --git a/src/app/common.blocks/modal/WelcomeModal.tsx b/src/app/common.blocks/modal/WelcomeModal.tsx old mode 100644 new mode 100755 index c6fc5a0..b82e5c0 --- a/src/app/common.blocks/modal/WelcomeModal.tsx +++ b/src/app/common.blocks/modal/WelcomeModal.tsx @@ -41,7 +41,7 @@ export const WelcomeModal = observer(() => { {isMobileLayout && ( - + )} @@ -51,7 +51,7 @@ export const WelcomeModal = observer(() => { )} {isMobileLayout && ( - + )} diff --git a/src/app/common.blocks/panel/ControlConfigPanel.scss b/src/app/common.blocks/panel/ControlConfigPanel.scss old mode 100644 new mode 100755 diff --git a/src/app/common.blocks/panel/ControlConfigPanel.tsx b/src/app/common.blocks/panel/ControlConfigPanel.tsx old mode 100644 new mode 100755 index 669f4f6..8c49f96 --- a/src/app/common.blocks/panel/ControlConfigPanel.tsx +++ b/src/app/common.blocks/panel/ControlConfigPanel.tsx @@ -2,10 +2,10 @@ import { Box, IconButton, Tooltip } from "@mui/material"; import { action } from "mobx"; import { observer } from "mobx-react-lite"; import { AnyControl, Control, EndControl } from "@core/Path"; -import { ObserverInput, clampQuantity } from "@app/component.blocks/ObserverInput"; +import { FormInputField, clampQuantity } from "@app/component.blocks/FormInputField"; import { Quantity, UnitOfAngle, UnitOfLength } from "@core/Unit"; import { boundHeading, findCentralPoint } from "@core/Calculation"; -import { Coordinate, CoordinateWithHeading, EuclideanTransformation } from "@core/Coordinate"; +import { Coordinate, CoordinateWithHeading, EuclideanTransformation, isCoordinateWithHeading } from "@core/Coordinate"; import { PanelBuilderProps, PanelInstanceProps } from "@core/Layout"; import { UpdatePathTreeItems } from "@core/Command"; import { getAppStores } from "@core/MainApp"; @@ -17,6 +17,8 @@ import RotateLeftIcon from "@mui/icons-material/RotateLeft"; import RotateRightIcon from "@mui/icons-material/RotateRight"; import "./ControlConfigPanel.scss"; +import { PanelBox } from "@src/app/component.blocks/PanelBox"; +import { CoordinateSystemTransformation } from "@src/core/CoordinateSystem"; const ControlConfigPanelBody = observer((props: {}) => { const { app } = getAppStores(); @@ -82,34 +84,78 @@ const ControlConfigPanelBody = observer((props: {}) => { app.history.execute("Rotate controls right", new UpdatePathTreeItems(selectedControls, updates), 0); }; + const clampQuantityValue = function (value: number) { + return clampQuantity( + value, + app.gc.uol, + new Quantity(-1000, UnitOfLength.Centimeter), + new Quantity(1000, UnitOfLength.Centimeter) + ); + }; + + const cst: CoordinateSystemTransformation | undefined = (() => { + if (app.selectedEntityCount !== 1) return undefined; + const control = app.selectedControl; + if (control === undefined) return undefined; + + const referencedPath = app.interestedPath(); + if (referencedPath === undefined) return undefined; + + const cs = app.coordinateSystem; + if (cs === undefined) return undefined; + + const fieldDimension = app.fieldDimension; + + const firstControl = referencedPath.segments[0].controls[0]; + + return new CoordinateSystemTransformation(cs, fieldDimension, firstControl); + })(); + + let xDisplayValue: string; + let yDisplayValue: string; + let headingDisplayValue: string; + + if (app.selectedEntityCount > 1) { + xDisplayValue = "(mixed)"; + yDisplayValue = "(mixed)"; + headingDisplayValue = "(mixed)"; + } else if (cst === undefined) { + xDisplayValue = ""; + yDisplayValue = ""; + headingDisplayValue = ""; + } else { + const control = app.selectedControl!; + const coordInFCS = cst.transform(control); + xDisplayValue = coordInFCS.x.toUser().toString(); + yDisplayValue = coordInFCS.y.toUser().toString(); + if (isCoordinateWithHeading(coordInFCS)) { + headingDisplayValue = coordInFCS.heading.toUser().toString(); + } else { + headingDisplayValue = ""; + } + } + return ( - - + { - if (app.selectedEntityCount === 0) return ""; - if (app.selectedEntityCount > 1) return "(mixed)"; - const control = app.selectedControl; - if (control === undefined) return ""; - return control.x.toUser().toString(); - }} + getValue={() => xDisplayValue} setValue={(value: string) => { - if (app.selectedEntityCount !== 1) return; + if (cst === undefined) return; const control = app.selectedControl; if (control === undefined) return; - const controlUid = control.uid; - const finalVal = clampQuantity( - parseFormula(value, NumberUOL.parse)!.compute(app.gc.uol), - app.gc.uol, - new Quantity(-1000, UnitOfLength.Centimeter), - new Quantity(1000, UnitOfLength.Centimeter) - ); + const coordInFCS = cst.transform(control); + const xValueInFCS = parseFormula(value, NumberUOL.parse)!.compute(app.gc.uol); + const newCoord = cst.inverseTransform({ ...coordInFCS, x: xValueInFCS }); + + newCoord.x = clampQuantityValue(newCoord.x); + newCoord.y = clampQuantityValue(newCoord.y); app.history.execute( - `Update control ${controlUid} x value`, - new UpdatePathTreeItems([control], { x: finalVal }) + `Update control ${control.uid} coordinate`, + new UpdatePathTreeItems([control], newCoord) ); }} isValidIntermediate={() => true} @@ -117,31 +163,24 @@ const ControlConfigPanelBody = observer((props: {}) => { disabled={app.selectedEntityCount !== 1 || app.selectedControl === undefined} numeric /> - { - if (app.selectedEntityCount === 0) return ""; - if (app.selectedEntityCount > 1) return "(mixed)"; - const control = app.selectedControl; - if (control === undefined) return ""; - return control.y.toUser().toString(); - }} + getValue={() => yDisplayValue} setValue={(value: string) => { - if (app.selectedEntityCount !== 1) return; + if (cst === undefined) return; const control = app.selectedControl; if (control === undefined) return; - const controlUid = control.uid; - const finalVal = clampQuantity( - parseFormula(value, NumberUOL.parse)!.compute(app.gc.uol), - app.gc.uol, - new Quantity(-1000, UnitOfLength.Centimeter), - new Quantity(1000, UnitOfLength.Centimeter) - ); + const coordInFCS = cst.transform(control); + const yValueInFCS = parseFormula(value, NumberUOL.parse)!.compute(app.gc.uol); + const newCoord = cst.inverseTransform({ ...coordInFCS, y: yValueInFCS }); + + newCoord.x = clampQuantityValue(newCoord.x); + newCoord.y = clampQuantityValue(newCoord.y); app.history.execute( - `Update control ${controlUid} y value`, - new UpdatePathTreeItems([control], { y: finalVal }) + `Update control ${control.uid} coordinate`, + new UpdatePathTreeItems([control], newCoord) ); }} isValidIntermediate={() => true} @@ -149,22 +188,20 @@ const ControlConfigPanelBody = observer((props: {}) => { disabled={app.selectedEntityCount !== 1 || app.selectedControl === undefined} numeric /> - { - if (app.selectedEntityCount === 0) return ""; - if (app.selectedEntityCount > 1) return "(mixed)"; - const control = app.selectedControl; - if (!(control instanceof EndControl)) return ""; - return control.heading.toUser().toString(); - }} + getValue={() => headingDisplayValue} setValue={(value: string) => { - if (app.selectedEntityCount !== 1) return; + if (cst === undefined) return; const control = app.selectedControl; if (!(control instanceof EndControl)) return; + const coordInFCS = cst.transform(control); + const headingValueInFCS = parseFormula(value, NumberUOA.parse)!.compute(UnitOfAngle.Degree); + const newCoord = cst.inverseTransform({ ...coordInFCS, heading: headingValueInFCS }); + const controlUid = control.uid; - const finalVal = parseFormula(value, NumberUOA.parse)!.compute(UnitOfAngle.Degree).toUser(); + const finalVal = newCoord.heading; app.history.execute( `Update control ${controlUid} heading value`, @@ -179,8 +216,8 @@ const ControlConfigPanelBody = observer((props: {}) => { }} numeric /> - - + + { - + ); }); diff --git a/src/app/common.blocks/panel/GeneralConfigPanel.scss b/src/app/common.blocks/panel/GeneralConfigPanel.scss old mode 100644 new mode 100755 diff --git a/src/app/common.blocks/panel/GeneralConfigPanel.tsx b/src/app/common.blocks/panel/GeneralConfigPanel.tsx old mode 100644 new mode 100755 index ab654ef..ebf2cd1 --- a/src/app/common.blocks/panel/GeneralConfigPanel.tsx +++ b/src/app/common.blocks/panel/GeneralConfigPanel.tsx @@ -2,21 +2,21 @@ import { Box, ListSubheader, MenuItem, MenuItemProps, Select, SelectChangeEvent, import { action } from "mobx"; import { observer } from "mobx-react-lite"; import { Format, getAllDeprecatedFormats, getAllExperimentalFormats, getAllGeneralFormats } from "@format/Format"; -import { ObserverInput, clampQuantity } from "@app/component.blocks/ObserverInput"; +import { FormInputField, clampQuantity } from "@app/component.blocks/FormInputField"; import { Quantity, UnitOfLength } from "@core/Unit"; import { UpdateProperties } from "@core/Command"; import { getAppStores } from "@core/MainApp"; -import { ObserverEnumSelect } from "@app/component.blocks/ObserverEnumSelect"; -import { ObserverCheckbox } from "@app/component.blocks/ObserverCheckbox"; +import { FormEnumSelect } from "@app/component.blocks/FormEnumSelect"; +import { FormCheckbox } from "@app/component.blocks/FormCheckbox"; import { NumberUOL } from "@token/Tokens"; import { parseFormula } from "@core/Util"; -import { ObserverItemsSelect } from "@app/component.blocks/ObserverItemsSelect"; -import { FieldImageAsset, FieldImageOriginType } from "@core/Asset"; import { AssetManagerModalSymbol } from "../modal/AssetManagerModal"; import { PanelBuilderProps, PanelInstanceProps } from "@core/Layout"; import TuneIcon from "@mui/icons-material/Tune"; -import "./GeneralConfigPanel.scss"; import { isExperimentalFeaturesEnabled } from "@src/core/Preferences"; +import { OpenModalButton } from "@src/app/component.blocks/OpenModalButton"; +import { PanelBox } from "@src/app/component.blocks/PanelBox"; +import { CoordinateSystemModalSymbol } from "../modal/CoordinateSystemModal"; const FormatMenuItem = (props: { format: Format } & MenuItemProps) => { const { format, ...rests } = props; @@ -25,7 +25,7 @@ const FormatMenuItem = (props: { format: Format } & MenuItemProps) => { {format.getName()} - + {format.getDescription()} @@ -34,7 +34,7 @@ const FormatMenuItem = (props: { format: Format } & MenuItemProps) => { }; const GeneralConfigPanelBody = observer((props: {}) => { - const { app, assetManager, confirmation, ui, appPreferences } = getAppStores(); + const { app, confirmation, ui, appPreferences } = getAppStores(); const gc = app.gc; @@ -73,7 +73,7 @@ const GeneralConfigPanelBody = observer((props: {}) => { return ( <> Format - + - - - + + app.history.execute(`Set Unit of Length`, new UpdateProperties(gc, { uol: v }))} enumType={UnitOfLength} /> - gc.pointDensity.toUser() + ""} @@ -121,12 +121,12 @@ const GeneralConfigPanelBody = observer((props: {}) => { isValidValue={(candidate: string) => parseFormula(candidate, NumberUOL.parse) !== null} numeric /> - - + + Robot Visualize - - + gc.robotWidth.toUser() + ""} setValue={(value: string) => @@ -146,7 +146,7 @@ const GeneralConfigPanelBody = observer((props: {}) => { isValidValue={(candidate: string) => parseFormula(candidate, NumberUOL.parse) !== null} numeric /> - gc.robotHeight.toUser() + ""} setValue={(value: string) => @@ -166,16 +166,16 @@ const GeneralConfigPanelBody = observer((props: {}) => { isValidValue={(candidate: string) => parseFormula(candidate, NumberUOL.parse) !== null} numeric /> - (gc.showRobot = c)} /> - - + + {typeof gc.robotIsHolonomic === "boolean" && ( - { @@ -183,31 +183,35 @@ const GeneralConfigPanelBody = observer((props: {}) => { }} /> )} - - - Field Layer + + + Field & Coordinate - - ({ key: asset.signature, value: asset, label: asset.displayName })), - { key: "open-asset-manager", value: "open-asset-manager", label: "(Custom)" } - ]} - onSelectItem={(asset: FieldImageAsset | string | undefined) => { - if (asset === "open-asset-manager") { - ui.openModal(AssetManagerModalSymbol); - } else if (asset instanceof FieldImageAsset) { - app.history.execute( - `Change field layer`, - new UpdateProperties(gc, { fieldImage: asset?.getSignatureAndOrigin() }) - ); - } - }} - /> - + + ui.openModal(AssetManagerModalSymbol)}> + {gc.fieldImage.displayName} + + + + ui.openModal(CoordinateSystemModalSymbol)}> + {gc.coordinateSystem} + + + {/* {appPreferences.isExperimentalFeaturesEnabled && ( + + { + const canvas = document.querySelector(".FieldCanvas canvas") as HTMLCanvasElement; + canvas.toBlob(blob => { + if (!blob) return; + const item = new ClipboardItem({ "image/png": blob }); + navigator.clipboard.write([item]); + }); + }}> + Capture Canvas + + + )} */} {gc.getAdditionalConfigUI()} ); diff --git a/src/app/common.blocks/panel/MenuPanel.scss b/src/app/common.blocks/panel/MenuPanel.scss old mode 100644 new mode 100755 diff --git a/src/app/common.blocks/panel/MenuPanel.tsx b/src/app/common.blocks/panel/MenuPanel.tsx old mode 100644 new mode 100755 index e5feaf0..6a00491 --- a/src/app/common.blocks/panel/MenuPanel.tsx +++ b/src/app/common.blocks/panel/MenuPanel.tsx @@ -49,11 +49,9 @@ const HotkeyTypography = observer((props: { hotkey: string | undefined }) => { key={index} variant="body2" color="text.secondary" - sx={{ - width: "1em", - textAlign: "center", - fontFamily: '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif' - }} + width="1em" + textAlign="center" + fontFamily="-apple-system,system-ui,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif" children={char} /> ); diff --git a/src/app/common.blocks/panel/Panel.scss b/src/app/common.blocks/panel/Panel.scss old mode 100644 new mode 100755 index 4451dfb..80f19c6 --- a/src/app/common.blocks/panel/Panel.scss +++ b/src/app/common.blocks/panel/Panel.scss @@ -1,12 +1,3 @@ -.Panel-FlexBox { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - justify-content: flex-start; - align-items: center; - gap: 8px; -} - .Panel-Header { margin: 0; font-family: "Roboto", "Helvetica", "Arial", sans-serif; diff --git a/src/app/common.blocks/panel/Panel.tsx b/src/app/common.blocks/panel/Panel.tsx old mode 100644 new mode 100755 diff --git a/src/app/common.blocks/panel/PathTreePanel.scss b/src/app/common.blocks/panel/PathTreePanel.scss old mode 100644 new mode 100755 diff --git a/src/app/common.blocks/panel/PathTreePanel.tsx b/src/app/common.blocks/panel/PathTreePanel.tsx old mode 100644 new mode 100755 diff --git a/src/app/common.blocks/speed-canvas/SpeedCanvasElement.scss b/src/app/common.blocks/speed-canvas/SpeedCanvasElement.scss old mode 100644 new mode 100755 diff --git a/src/app/common.blocks/speed-canvas/SpeedCanvasElement.tsx b/src/app/common.blocks/speed-canvas/SpeedCanvasElement.tsx old mode 100644 new mode 100755 index 853ce70..941f6ae --- a/src/app/common.blocks/speed-canvas/SpeedCanvasElement.tsx +++ b/src/app/common.blocks/speed-canvas/SpeedCanvasElement.tsx @@ -39,7 +39,7 @@ const SpeedCanvasTooltipContent = observer((props: {}) => { const speed = (speedFrom + pos.yPos * (speedTo - speedFrom)).toUser(); const postfix = interaction.keyframe.followBentRate ? " (Bent Rate Interruption)" : " (Linear Interpolation)"; return ( - + {speed} {postfix} diff --git a/src/app/component.blocks/CanvasTooltip.scss b/src/app/component.blocks/CanvasTooltip.scss old mode 100644 new mode 100755 diff --git a/src/app/component.blocks/CanvasTooltip.tsx b/src/app/component.blocks/CanvasTooltip.tsx old mode 100644 new mode 100755 diff --git a/src/app/component.blocks/FormButton.scss b/src/app/component.blocks/FormButton.scss new file mode 100755 index 0000000..daff593 --- /dev/null +++ b/src/app/component.blocks/FormButton.scss @@ -0,0 +1,37 @@ +.FormButton-Button { + all: unset; + margin: 0; + border-radius: 4px; + border-style: solid; + border-width: 1px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + min-width: 0%; + border-color: rgba(255, 255, 255, 0.23); + line-height: 1.4375em; + letter-spacing: 0.00938em; + align-items: center; + padding: 8.5px 14px; + // box-sizing: content-box; + box-sizing: border-box; + cursor: pointer; + display: flex; + gap: 8px; + + &:hover { + border-color: var(--text-primary-color); + } + + &:focus { + border-color: var(--primary-main-color); + border-width: 2px; + padding: 7.5px 13px; + } + + > svg { + font-size: 1em; + width: 1em; + height: 1em; + } +} diff --git a/src/app/component.blocks/FormButton.tsx b/src/app/component.blocks/FormButton.tsx new file mode 100755 index 0000000..66bcfc0 --- /dev/null +++ b/src/app/component.blocks/FormButton.tsx @@ -0,0 +1,9 @@ +import { Typography, TypographyProps } from "@mui/material"; +import { observer } from "mobx-react-lite"; +import "./FormButton.scss"; + +export const FormButton = observer((props: {} & TypographyProps) => { + const { ...rest } = props; + + return ; +}); diff --git a/src/app/component.blocks/FormCheckbox.tsx b/src/app/component.blocks/FormCheckbox.tsx new file mode 100755 index 0000000..e40ab2a --- /dev/null +++ b/src/app/component.blocks/FormCheckbox.tsx @@ -0,0 +1,26 @@ +import { action } from "mobx"; +import { observer } from "mobx-react-lite"; +import { Checkbox, FormControlLabel, FormControlLabelProps } from "@mui/material"; + +const FormCheckbox = observer( + ( + props: Omit & { + label: string; + checked: boolean; + onCheckedChange: (value: boolean) => void; + } + ) => { + const { label, checked, onCheckedChange, ...rest } = props; + + return ( + onCheckedChange(c))} />} + label={label} + sx={{ whiteSpace: "nowrap" }} + {...rest} + /> + ); + } +); + +export { FormCheckbox }; diff --git a/src/app/component.blocks/FormEnumSelect.tsx b/src/app/component.blocks/FormEnumSelect.tsx new file mode 100755 index 0000000..2a90af8 --- /dev/null +++ b/src/app/component.blocks/FormEnumSelect.tsx @@ -0,0 +1,42 @@ +import { action } from "mobx"; +import { observer } from "mobx-react-lite"; +import { FormControlProps, FormControl, InputLabel, Select, SelectChangeEvent, MenuItem } from "@mui/material"; +import React from "react"; +import { makeId } from "@core/Util"; + +const FormEnumSelect = observer( + ( + props: FormControlProps & { + label: string; + enumValue: T; + onEnumChange: (value: T) => void; + enumType: any; + } + ) => { + const { label, enumValue, onEnumChange, enumType, ...rest } = props; + + const uid = React.useRef(makeId(10)).current; + + return ( + + {label} + + + ); + } +); + +export { FormEnumSelect }; diff --git a/src/app/component.blocks/FormInputField.tsx b/src/app/component.blocks/FormInputField.tsx new file mode 100755 index 0000000..495e4a9 --- /dev/null +++ b/src/app/component.blocks/FormInputField.tsx @@ -0,0 +1,126 @@ +import { TextField, TextFieldProps } from "@mui/material"; +import { action } from "mobx"; +import { observer } from "mobx-react-lite"; +import { Quantity, UnitConverter, UnitOfLength } from "@core/Unit"; +import { clamp } from "@core/Util"; +import React, { forwardRef } from "react"; + +export function clampQuantity( + value: number, + uol: UnitOfLength, + min = new Quantity(-Infinity, UnitOfLength.Centimeter), + max = new Quantity(+Infinity, UnitOfLength.Centimeter) +): number { + const minInUOL = new UnitConverter(min.unit, uol).fromAtoB(min.value); + const maxInUOL = new UnitConverter(max.unit, uol).fromAtoB(max.value); + + return clamp(value, minInUOL, maxInUOL).toUser(); +} + +export type FormInputFieldProps = TextFieldProps & { + getValue: () => string; + setValue: (value: string, payload: any) => void; + isValidIntermediate: (candidate: string) => boolean; + isValidValue: (candidate: string) => boolean | [boolean, any]; + numeric?: boolean; // default false +}; + +const FormInputField = observer( + forwardRef((props: FormInputFieldProps, ref) => { + // rest is used to send props to TextField without custom attributes + const { getValue, setValue, isValidIntermediate, isValidValue, numeric: isNumeric, ...rest } = props; + + const initialValue = React.useState(() => getValue())[0]; + const inputRef = React.useRef(null); + const lastValidValue = React.useRef(initialValue); + const lastValidIntermediate = React.useRef(initialValue); + + React.useImperativeHandle(ref, () => inputRef.current!); + + function onChange(event: React.ChangeEvent) { + const element = event.nativeEvent.target as HTMLInputElement; + const candidate = element.value; + + if (!isValidIntermediate(candidate)) { + event.preventDefault(); + + element.value = lastValidIntermediate.current; + } else { + lastValidIntermediate.current = candidate; + } + } + + function onKeyDown(event: React.KeyboardEvent) { + const element = event.nativeEvent.target as HTMLInputElement; + + if (event.code === "Enter" || event.code === "NumpadEnter") { + event.preventDefault(); + element.blur(); + } else if (isNumeric && event.code === "ArrowDown") { + onConfirm(event); + element.value = parseFloat(getValue()) - 1 + ""; + onConfirm(event); + } else if (isNumeric && event.code === "ArrowUp") { + onConfirm(event); + element.value = parseFloat(getValue()) + 1 + ""; + onConfirm(event); + } else if (event.code === "Escape") { + element.value = ""; + element.blur(); + } + + rest.onKeyDown?.(event); + } + + function onBlur(event: React.FocusEvent) { + onConfirm(event); + + rest.onBlur?.(event); + } + + function onConfirm(event: React.SyntheticEvent) { + const element = event.nativeEvent.target as HTMLInputElement; + const candidate = element.value; + let rtn: string; + + const result = isValidValue(candidate); + const isValid = Array.isArray(result) ? result[0] : result; + const payload = Array.isArray(result) ? result[1] : undefined; + if (isValid === false) { + element.value = rtn = lastValidValue.current; + } else { + rtn = candidate; + } + + setValue(rtn, payload); + inputRef.current && + (inputRef.current.value = lastValidValue.current = lastValidIntermediate.current = getValue()); + } + + const value = getValue(); + + React.useEffect(() => { + const value = getValue(); + if (value !== lastValidValue.current) { + lastValidValue.current = value; + lastValidIntermediate.current = value; + inputRef.current!.value = value; + } + }, [value, getValue]); + + return ( + + ); + }) +); + +export { FormInputField }; diff --git a/src/app/component.blocks/FormItemSelect.tsx b/src/app/component.blocks/FormItemSelect.tsx new file mode 100755 index 0000000..4513610 --- /dev/null +++ b/src/app/component.blocks/FormItemSelect.tsx @@ -0,0 +1,46 @@ +import { action } from "mobx"; +import { observer } from "mobx-react-lite"; +import { FormControlProps, FormControl, InputLabel, Select, SelectChangeEvent, MenuItem } from "@mui/material"; +import React from "react"; +import { makeId } from "@core/Util"; + +export type Item = { + key: string | number; + value: TValue; + label: string; +}; + +const FormItemSelect = observer( + , TItems extends TItem[]>( + props: FormControlProps & { + label: string; + selected: TItems[number]["key"]; + items: TItems; + onSelectItem: (item: TItems[number]["value"] | undefined) => void; + } + ) => { + const { label, selected, items, onSelectItem, ...rest } = props; + + const uid = React.useRef(makeId(10)).current; + + return ( + + {label} + + + ); + } +); + +export { FormItemSelect }; diff --git a/src/app/component.blocks/ObserverCheckbox.tsx b/src/app/component.blocks/ObserverCheckbox.tsx old mode 100644 new mode 100755 diff --git a/src/app/component.blocks/ObserverEnumSelect.tsx b/src/app/component.blocks/ObserverEnumSelect.tsx old mode 100644 new mode 100755 diff --git a/src/app/component.blocks/ObserverInput.tsx b/src/app/component.blocks/ObserverInput.tsx old mode 100644 new mode 100755 diff --git a/src/app/component.blocks/ObserverItemsSelect.tsx b/src/app/component.blocks/ObserverItemsSelect.tsx old mode 100644 new mode 100755 diff --git a/src/app/component.blocks/OpenModalButton.tsx b/src/app/component.blocks/OpenModalButton.tsx new file mode 100755 index 0000000..bb84692 --- /dev/null +++ b/src/app/component.blocks/OpenModalButton.tsx @@ -0,0 +1,19 @@ +import { Box } from "@mui/material"; +import { observer } from "mobx-react-lite"; +import { FormButton } from "./FormButton"; +import MenuIcon from "@mui/icons-material/Menu"; + +export const OpenModalButton = observer((props: React.ComponentProps) => { + const { children, ...rest } = props; + + return ( + + + + {children} + + + + + ); +}); diff --git a/src/app/component.blocks/PanelBox.tsx b/src/app/component.blocks/PanelBox.tsx new file mode 100755 index 0000000..c00b567 --- /dev/null +++ b/src/app/component.blocks/PanelBox.tsx @@ -0,0 +1,19 @@ +import { BoxProps, Box } from "@mui/material"; +import { observer } from "mobx-react-lite"; + +export const PanelBox = observer((props: {} & BoxProps) => { + const { ...rest } = props; + + return ( + + ); +}); diff --git a/src/app/component.blocks/RangeSlider.tsx b/src/app/component.blocks/RangeSlider.tsx old mode 100644 new mode 100755 diff --git a/src/app/exclusive.blocks/_index.scss b/src/app/exclusive.blocks/_index.scss old mode 100644 new mode 100755 diff --git a/src/app/exclusive.blocks/_index.tsx b/src/app/exclusive.blocks/_index.tsx old mode 100644 new mode 100755 diff --git a/src/app/exclusive.blocks/panel/PathTreePanel.scss b/src/app/exclusive.blocks/panel/PathTreePanel.scss old mode 100644 new mode 100755 diff --git a/src/app/exclusive.blocks/speed-canvas/SpeedCanvasElement.scss b/src/app/exclusive.blocks/speed-canvas/SpeedCanvasElement.scss old mode 100644 new mode 100755 diff --git a/src/app/mobile.blocks/PathTreePanel.scss b/src/app/mobile.blocks/PathTreePanel.scss old mode 100644 new mode 100755 diff --git a/src/app/mobile.blocks/Welcome.scss b/src/app/mobile.blocks/Welcome.scss old mode 100644 new mode 100755 diff --git a/src/app/mobile.blocks/_index.scss b/src/app/mobile.blocks/_index.scss old mode 100644 new mode 100755 index 2ab8acc..57a71f7 --- a/src/app/mobile.blocks/_index.scss +++ b/src/app/mobile.blocks/_index.scss @@ -97,5 +97,7 @@ } } -@import "./PathTreePanel"; -@import "./Welcome"; +@import "./panel/PathTreePanel"; +@import "./modal/AssetManagerModal"; +@import "./modal/CoordinateSystemModal"; +@import "./modal/WelcomeModal"; diff --git a/src/app/mobile.blocks/_index.tsx b/src/app/mobile.blocks/_index.tsx old mode 100644 new mode 100755 index 276ba2c..65556d3 --- a/src/app/mobile.blocks/_index.tsx +++ b/src/app/mobile.blocks/_index.tsx @@ -91,7 +91,7 @@ export const MobileLayout = observer(() => { {app.interestedPath() ? ( ) : ( - (No path to display) + (No path to display) )} )} diff --git a/src/app/mobile.blocks/modal/AssetManagerModal.scss b/src/app/mobile.blocks/modal/AssetManagerModal.scss new file mode 100755 index 0000000..d664e2f --- /dev/null +++ b/src/app/mobile.blocks/modal/AssetManagerModal.scss @@ -0,0 +1,12 @@ +#AssetManagerModal { + width: 100% !important; + height: 100% !important; + max-width: 100% !important; + max-height: 100% !important; + padding: 8px; + box-sizing: border-box; + + .FieldImageAssets-Title { + padding: 8px; + } +} diff --git a/src/app/mobile.blocks/modal/CoordinateSystemModal.scss b/src/app/mobile.blocks/modal/CoordinateSystemModal.scss new file mode 100755 index 0000000..bdf7976 --- /dev/null +++ b/src/app/mobile.blocks/modal/CoordinateSystemModal.scss @@ -0,0 +1,12 @@ +#CoordinateSystemModal { + width: 100% !important; + height: 100% !important; + max-width: 100% !important; + max-height: 100% !important; + padding: 8px; + box-sizing: border-box; + + .CoordinateSystem-Title { + padding: 8px; + } +} diff --git a/src/app/mobile.blocks/modal/WelcomeModal.scss b/src/app/mobile.blocks/modal/WelcomeModal.scss new file mode 100755 index 0000000..8c54351 --- /dev/null +++ b/src/app/mobile.blocks/modal/WelcomeModal.scss @@ -0,0 +1,7 @@ +#WelcomeModal { + width: 100% !important; + height: 100% !important; + max-width: 100% !important; + padding: 8px; + box-sizing: border-box; +} diff --git a/src/app/mobile.blocks/panel/PathTreePanel.scss b/src/app/mobile.blocks/panel/PathTreePanel.scss new file mode 100755 index 0000000..7ddfe52 --- /dev/null +++ b/src/app/mobile.blocks/panel/PathTreePanel.scss @@ -0,0 +1,9 @@ +.PathTreePanel-Header { + position: relative; + + > div { + position: absolute; + right: -8px; + top: -8px; + } +} diff --git a/src/core/Asset.test.ts b/src/core/Asset.test.ts old mode 100644 new mode 100755 diff --git a/src/core/Asset.ts b/src/core/Asset.ts old mode 100644 new mode 100755 index cffeac3..3e111f6 --- a/src/core/Asset.ts +++ b/src/core/Asset.ts @@ -13,6 +13,7 @@ import { Hash } from "fast-sha256"; import { makeAutoObservable, makeObservable, observable } from "mobx"; import { ValidateNumber, hex, makeId, TextEncoder } from "./Util"; import localforage from "localforage"; +import { Dimension } from "./CoordinateSystem"; export const DEFAULT_ACCEPT_FILE_EXT = [".png", ".jpg", ".jpeg", ".gif"] as const; @@ -152,6 +153,13 @@ export class FieldImageAsset { return this.location; } + async getDimension(): Promise { + const img = new Image(); + img.src = this.location; + await img.decode(); + return { width: img.width, height: img.height }; + } + getOrigin(): FieldImageOriginClass; getOrigin() { switch (this.type) { diff --git a/src/core/Calculation.test.ts b/src/core/Calculation.test.ts old mode 100644 new mode 100755 diff --git a/src/core/Calculation.ts b/src/core/Calculation.ts old mode 100644 new mode 100755 index 3a2a1da..da635be --- a/src/core/Calculation.ts +++ b/src/core/Calculation.ts @@ -115,7 +115,7 @@ export function getPathPoints( } /** - * Process the given points with keyframes. + * Processes the given points with keyframes. * * @param path - The path being processed. * @param points - The points to apply the keyframes to. diff --git a/src/core/Canvas.ts b/src/core/Canvas.ts old mode 100644 new mode 100755 diff --git a/src/core/Clipboard.ts b/src/core/Clipboard.ts old mode 100644 new mode 100755 diff --git a/src/core/Command.test.ts b/src/core/Command.test.ts old mode 100644 new mode 100755 diff --git a/src/core/Command.ts b/src/core/Command.ts old mode 100644 new mode 100755 diff --git a/src/core/Coordinate.test.ts b/src/core/Coordinate.test.ts old mode 100644 new mode 100755 index 13e9fea..76e07f8 --- a/src/core/Coordinate.test.ts +++ b/src/core/Coordinate.test.ts @@ -1,65 +1,132 @@ import { boundHeading } from "./Calculation"; import { EuclideanTransformation, Coordinate, CoordinateWithHeading } from "./Coordinate"; +declare global { + namespace jest { + // ...any other extensions, like "Matchers". + interface Expect { + closeTo(received: { [key: string]: number }, expected: { [key: string]: number }): any; + } + + interface Matchers { + closeTo(expected: { [key: string]: number }): R; + } + } +} + +beforeAll(() => { + expect.extend({ + // obj close to + closeTo: (received: { [key: string]: number }, expected: { [key: string]: number }) => { + for (const key in expected) { + expect(received[key]).toBeCloseTo(expected[key], 0.0001); + } + return { pass: true, message: () => "" }; + } + }); +}); + test("EuclideanTransformation class", () => { let converter = new EuclideanTransformation({ x: 0, y: 0, heading: 0 }); let ans: Coordinate = { x: 0, y: 0 }; let ans2: CoordinateWithHeading = { x: 0, y: 0, heading: 0 }; - ans = converter.transform({ x: 3, y: 4 }); - expect(ans.x).toBeCloseTo(3); - expect(ans.y).toBeCloseTo(4); + expect(converter.transform({ x: 3, y: 4 })).closeTo({ x: 3, y: 4 }); + expect(converter.transform({ x: 3, y: 4, heading: 32.1 })).closeTo({ x: 3, y: 4, heading: 32.1 }); - ans2 = converter.transform({ x: 3, y: 4, heading: 32.1 }); - expect(ans2.x).toBeCloseTo(3); - expect(ans2.y).toBeCloseTo(4); - expect(ans2.heading).toBeCloseTo(32.1); + expect(converter.inverseTransform({ x: 3, y: 4 })).closeTo({ x: 3, y: 4 }); + expect(converter.inverseTransform({ x: 3, y: 4 })).closeTo({ x: 3, y: 4 }); converter = new EuclideanTransformation({ x: 0, y: 0, heading: 45 }); - ans = converter.transform({ x: 3, y: 3 }); - expect(ans.x).toBeCloseTo(0); - expect(ans.y).toBeCloseTo(Math.sqrt(3 ** 2 + 3 ** 2)); - - ans2 = converter.transform({ x: 3, y: 3, heading: 32.1 }); - expect(ans2.x).toBeCloseTo(0); - expect(ans2.y).toBeCloseTo(Math.sqrt(3 ** 2 + 3 ** 2)); - expect(ans2.heading).toBeCloseTo(boundHeading(32.1 - 45)); + expect(converter.transform({ x: 3, y: 3 })).closeTo({ x: 0, y: Math.sqrt(3 ** 2 + 3 ** 2) }); + expect(converter.transform({ x: 3, y: 3, heading: 32.1 })).closeTo({ + x: 0, + y: Math.sqrt(3 ** 2 + 3 ** 2), + heading: boundHeading(32.1 - 45) + }); + expect(converter.transform({ x: 3, y: 3, heading: 330 })).closeTo({ + x: 0, + y: Math.sqrt(3 ** 2 + 3 ** 2), + heading: 285 + }); - ans2 = converter.transform({ x: 3, y: 3, heading: 330 }); - expect(ans2.x).toBeCloseTo(0); - expect(ans2.y).toBeCloseTo(Math.sqrt(3 ** 2 + 3 ** 2)); - expect(ans2.heading).toBeCloseTo(285); + expect(converter.inverseTransform({ x: 0, y: Math.sqrt(3 ** 2 + 3 ** 2) })).closeTo({ x: 3, y: 3 }); + expect(converter.inverseTransform({ x: 0, y: Math.sqrt(3 ** 2 + 3 ** 2), heading: boundHeading(32.1 - 45) })).closeTo( + { + x: 3, + y: 3 + } + ); + expect(converter.inverseTransform({ x: 0, y: Math.sqrt(3 ** 2 + 3 ** 2), heading: 285 })).closeTo({ x: 3, y: 3 }); converter = new EuclideanTransformation({ x: 0, y: 0, heading: -45 }); - ans = converter.transform({ x: 3, y: 3 }); - expect(ans.x).toBeCloseTo(Math.sqrt(3 ** 2 + 3 ** 2)); - expect(ans.y).toBeCloseTo(0); + expect(converter.transform({ x: 3, y: 3 })).closeTo({ x: Math.sqrt(3 ** 2 + 3 ** 2), y: 0 }); + expect(converter.transform({ x: 3, y: 3, heading: 32.1 })).closeTo({ + x: Math.sqrt(3 ** 2 + 3 ** 2), + y: 0, + heading: 45 + 32.1 + }); + expect(converter.transform({ x: 3, y: 3, heading: 330 })).closeTo({ + x: Math.sqrt(3 ** 2 + 3 ** 2), + y: 0, + heading: 15 + }); - ans2 = converter.transform({ x: 3, y: 3, heading: 32.1 }); - expect(ans2.x).toBeCloseTo(Math.sqrt(3 ** 2 + 3 ** 2)); - expect(ans2.y).toBeCloseTo(0); - expect(ans2.heading).toBeCloseTo(45 + 32.1); - - ans2 = converter.transform({ x: 3, y: 3, heading: 330 }); - expect(ans2.x).toBeCloseTo(Math.sqrt(3 ** 2 + 3 ** 2)); - expect(ans2.y).toBeCloseTo(0); - expect(ans2.heading).toBeCloseTo(15); + expect(converter.inverseTransform({ x: Math.sqrt(3 ** 2 + 3 ** 2), y: 0 })).closeTo({ x: 3, y: 3 }); + expect(converter.inverseTransform({ x: Math.sqrt(3 ** 2 + 3 ** 2), y: 0, heading: 45 + 32.1 })).closeTo({ + x: 3, + y: 3, + heading: 32.1 + }); + expect(converter.inverseTransform({ x: Math.sqrt(3 ** 2 + 3 ** 2), y: 0, heading: 15 })).closeTo({ + x: 3, + y: 3, + heading: 330 + }); converter = new EuclideanTransformation({ x: 10, y: 20, heading: -45 }); - ans = converter.transform({ x: 13, y: 23 }); - expect(ans.x).toBeCloseTo(Math.sqrt(3 ** 2 + 3 ** 2)); - expect(ans.y).toBeCloseTo(0); + expect(converter.transform({ x: 13, y: 23 })).closeTo({ x: Math.sqrt(3 ** 2 + 3 ** 2), y: 0 }); + expect(converter.transform({ x: 13, y: 23, heading: 32.1 })).closeTo({ + x: Math.sqrt(3 ** 2 + 3 ** 2), + y: 0, + heading: 45 + 32.1 + }); + expect(converter.transform({ x: 13, y: 23, heading: 330 })).closeTo({ + x: Math.sqrt(3 ** 2 + 3 ** 2), + y: 0, + heading: 15 + }); + + expect(converter.inverseTransform({ x: Math.sqrt(3 ** 2 + 3 ** 2), y: 0 })).closeTo({ x: 13, y: 23 }); + expect(converter.inverseTransform({ x: Math.sqrt(3 ** 2 + 3 ** 2), y: 0, heading: 45 + 32.1 })).closeTo({ + x: 13, + y: 23, + heading: 32.1 + }); + expect(converter.inverseTransform({ x: Math.sqrt(3 ** 2 + 3 ** 2), y: 0, heading: 15 })).closeTo({ + x: 13, + y: 23, + heading: 330 + }); +}); + +test("EuclideanTransformation inverse", () => { + let converter = new EuclideanTransformation({ x: 400, y: 300, heading: 0 }); - ans2 = converter.transform({ x: 13, y: 23, heading: 32.1 }); - expect(ans2.x).toBeCloseTo(Math.sqrt(3 ** 2 + 3 ** 2)); - expect(ans2.y).toBeCloseTo(0); - expect(ans2.heading).toBeCloseTo(45 + 32.1); + expect(converter.transform({ x: 400, y: 300 })).closeTo({ x: 0, y: 0 }); + expect(converter.transform({ x: 401, y: 300 })).closeTo({ x: 1, y: 0 }); + expect(converter.transform({ x: 399, y: 300 })).closeTo({ x: -1, y: 0 }); + expect(converter.transform({ x: 399, y: 302 })).closeTo({ x: -1, y: 2 }); + expect(converter.transform({ x: 399, y: 298 })).closeTo({ x: -1, y: -2 }); + expect(converter.transform({ x: 0, y: 0 })).closeTo({ x: -400, y: -300 }); - ans2 = converter.transform({ x: 13, y: 23, heading: 330 }); - expect(ans2.x).toBeCloseTo(Math.sqrt(3 ** 2 + 3 ** 2)); - expect(ans2.y).toBeCloseTo(0); - expect(ans2.heading).toBeCloseTo(15); + expect(converter.inverseTransform({ x: 0, y: 0 })).closeTo({ x: 400, y: 300 }); + expect(converter.inverseTransform({ x: 1, y: 0 })).closeTo({ x: 401, y: 300 }); + expect(converter.inverseTransform({ x: -1, y: 0 })).closeTo({ x: 399, y: 300 }); + expect(converter.inverseTransform({ x: -1, y: 2 })).closeTo({ x: 399, y: 302 }); + expect(converter.inverseTransform({ x: -1, y: -2 })).closeTo({ x: 399, y: 298 }); + expect(converter.inverseTransform({ x: -400, y: -300 })).closeTo({ x: 0, y: 0 }); }); diff --git a/src/core/Coordinate.ts b/src/core/Coordinate.ts old mode 100644 new mode 100755 index eb8a8f9..2c35ce5 --- a/src/core/Coordinate.ts +++ b/src/core/Coordinate.ts @@ -22,23 +22,39 @@ export class EuclideanTransformation { private sin: number; private cos: number; - constructor(readonly origin: CoordinateWithHeading) { - this.theta = fromHeadingInDegreeToAngleInRadian(boundHeading(-origin.heading + 90)); + constructor(readonly betaOrigin: CoordinateWithHeading) { + this.theta = fromHeadingInDegreeToAngleInRadian(boundHeading(-betaOrigin.heading + 90)); this.sin = Math.sin(this.theta); this.cos = Math.cos(this.theta); } - transform(target: Coordinate): Coordinate; - transform(target: CoordinateWithHeading): CoordinateWithHeading; + transform(alpha: Coordinate): Coordinate; + transform(alpha: CoordinateWithHeading): CoordinateWithHeading; - transform(target: Coordinate | CoordinateWithHeading): Coordinate | CoordinateWithHeading { + transform(alpha: Coordinate | CoordinateWithHeading): Coordinate | CoordinateWithHeading { const rtn: any = { - y: (target.x - this.origin.x) * this.sin + (target.y - this.origin.y) * this.cos, - x: (target.x - this.origin.x) * this.cos - (target.y - this.origin.y) * this.sin + y: (alpha.x - this.betaOrigin.x) * this.sin + (alpha.y - this.betaOrigin.y) * this.cos, + x: (alpha.x - this.betaOrigin.x) * this.cos - (alpha.y - this.betaOrigin.y) * this.sin }; - if (isCoordinateWithHeading(target)) { - rtn.heading = boundHeading(target.heading - this.origin.heading); + if (isCoordinateWithHeading(alpha)) { + rtn.heading = boundHeading(alpha.heading - this.betaOrigin.heading); + } + + return rtn; + } + + inverseTransform(beta: Coordinate): Coordinate; + inverseTransform(beta: CoordinateWithHeading): CoordinateWithHeading; + + inverseTransform(beta: Coordinate | CoordinateWithHeading): Coordinate | CoordinateWithHeading { + const rtn: any = { + y: -beta.x * this.sin + beta.y * this.cos + this.betaOrigin.y, + x: beta.x * this.cos + beta.y * this.sin + this.betaOrigin.x + }; + + if (isCoordinateWithHeading(beta)) { + rtn.heading = boundHeading(beta.heading + this.betaOrigin.heading); } return rtn; diff --git a/src/core/CoordinateSystem.test.ts b/src/core/CoordinateSystem.test.ts new file mode 100755 index 0000000..f3db864 --- /dev/null +++ b/src/core/CoordinateSystem.test.ts @@ -0,0 +1,557 @@ +import { CoordinateWithHeading } from "./Coordinate"; +import { + AxisAnchor, + AxisRotation, + CoordinateSystem, + CoordinateSystemTransformation, + CoordinateSystemUnrelatedToField, + CoordinateSystemUnrelatedToFieldAndPath, + CoordinateSystemUnrelatedToPath, + Dimension, + HeadingAnchor, + HeadingDirection, + HeadingRotation, + OriginAnchor, + YAxisFlip +} from "./CoordinateSystem"; + +declare global { + namespace jest { + // ...any other extensions, like "Matchers". + interface Expect { + closeTo(received: { [key: string]: number }, expected: { [key: string]: number }): any; + } + + interface Matchers { + closeTo(expected: { [key: string]: number }): R; + } + } +} + +beforeAll(() => { + expect.extend({ + // obj close to + closeTo: (received: { [key: string]: number }, expected: { [key: string]: number }) => { + for (const key in expected) { + expect(received[key]).toBeCloseTo(expected[key], 0.0001); + } + return { pass: true, message: () => "" }; + } + }); +}); + +test("CoordinateSystemTransformation original", () => { + let system: CoordinateSystemUnrelatedToFieldAndPath = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XEastYNorth, + yAxisFlip: YAxisFlip.NoFlip, + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.North, + headingDirection: HeadingDirection.Clockwise, + originAnchor: OriginAnchor.FieldCenter, + originOffset: { x: 0, y: 0 } + }; + let cst = CoordinateSystemTransformation.buildWithoutFieldAndBeginningInfo(system); + + expect(cst.transform({ x: 0, y: 0 })).toEqual({ x: 0, y: 0 }); + expect(cst.transform({ x: 1, y: 0 })).toEqual({ x: 1, y: 0 }); + expect(cst.transform({ x: 1, y: 2 })).toEqual({ x: 1, y: 2 }); + expect(cst.transform({ x: 0, y: 2 })).toEqual({ x: 0, y: 2 }); + + expect(cst.transform({ x: 0, y: 0, heading: 0 })).toEqual({ x: 0, y: 0, heading: 0 }); + expect(cst.transform({ x: 0, y: 0, heading: 45 })).toEqual({ x: 0, y: 0, heading: 45 }); + expect(cst.transform({ x: 0, y: 0, heading: 90 })).toEqual({ x: 0, y: 0, heading: 90 }); + expect(cst.transform({ x: 0, y: 0, heading: -45 })).toEqual({ x: 0, y: 0, heading: 360 - 45 }); + expect(cst.transform({ x: 0, y: 0, heading: 270 })).toEqual({ x: 0, y: 0, heading: 270 }); + + expect(cst.inverseTransform({ x: 0, y: 0 })).toEqual({ x: 0, y: 0 }); + expect(cst.inverseTransform({ x: 1, y: 0 })).toEqual({ x: 1, y: 0 }); + expect(cst.inverseTransform({ x: 1, y: 2 })).toEqual({ x: 1, y: 2 }); + expect(cst.inverseTransform({ x: 0, y: 2 })).toEqual({ x: 0, y: 2 }); + + expect(cst.inverseTransform({ x: 0, y: 0, heading: 0 })).toEqual({ x: 0, y: 0, heading: 0 }); + expect(cst.inverseTransform({ x: 0, y: 0, heading: 45 })).toEqual({ x: 0, y: 0, heading: 45 }); + expect(cst.inverseTransform({ x: 0, y: 0, heading: 90 })).toEqual({ x: 0, y: 0, heading: 90 }); + expect(cst.inverseTransform({ x: 0, y: 0, heading: -45 })).toEqual({ x: 0, y: 0, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: 0, y: 0, heading: 270 })).toEqual({ x: 0, y: 0, heading: 270 }); +}); + +test("CoordinateSystemTransformation 90", () => { + let system: CoordinateSystemUnrelatedToFieldAndPath = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XSouthYEast, // Changed + yAxisFlip: YAxisFlip.NoFlip, + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.East, // Changed + headingDirection: HeadingDirection.Clockwise, + originAnchor: OriginAnchor.FieldCenter, + originOffset: { x: 0, y: 0 } + }; + let cst = CoordinateSystemTransformation.buildWithoutFieldAndBeginningInfo(system); + + expect(cst.transform({ x: 1, y: 2, heading: 0 })).closeTo({ x: -2, y: 1, heading: 270 }); + expect(cst.transform({ x: 1, y: 2, heading: 45 })).closeTo({ x: -2, y: 1, heading: 315 }); + expect(cst.transform({ x: 1, y: 2, heading: 90 })).closeTo({ x: -2, y: 1, heading: 0 }); + expect(cst.transform({ x: 1, y: 2, heading: -45 })).closeTo({ x: -2, y: 1, heading: 225 }); + expect(cst.transform({ x: 1, y: 2, heading: 270 })).closeTo({ x: -2, y: 1, heading: 180 }); + + expect(cst.inverseTransform({ x: -2, y: 1, heading: 270 })).closeTo({ x: 1, y: 2, heading: 0 }); + expect(cst.inverseTransform({ x: -2, y: 1, heading: 315 })).closeTo({ x: 1, y: 2, heading: 45 }); + expect(cst.inverseTransform({ x: -2, y: 1, heading: 0 })).closeTo({ x: 1, y: 2, heading: 90 }); + expect(cst.inverseTransform({ x: -2, y: 1, heading: 225 })).closeTo({ x: 1, y: 2, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: -2, y: 1, heading: 180 })).closeTo({ x: 1, y: 2, heading: 270 }); +}); + +test("CoordinateSystemTransformation 180", () => { + let system: CoordinateSystemUnrelatedToFieldAndPath = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XWestYSouth, // Changed + yAxisFlip: YAxisFlip.NoFlip, + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.South, // Changed + headingDirection: HeadingDirection.Clockwise, + originAnchor: OriginAnchor.FieldCenter, + originOffset: { x: 0, y: 0 } + }; + let cst = CoordinateSystemTransformation.buildWithoutFieldAndBeginningInfo(system); + + expect(cst.transform({ x: 1, y: 2, heading: 0 })).closeTo({ x: -1, y: -2, heading: 180 }); + expect(cst.transform({ x: 1, y: 2, heading: 45 })).closeTo({ x: -1, y: -2, heading: 225 }); + expect(cst.transform({ x: 1, y: 2, heading: 90 })).closeTo({ x: -1, y: -2, heading: 270 }); + expect(cst.transform({ x: 1, y: 2, heading: -45 })).closeTo({ x: -1, y: -2, heading: 135 }); + expect(cst.transform({ x: 1, y: 2, heading: 270 })).closeTo({ x: -1, y: -2, heading: 90 }); + + expect(cst.inverseTransform({ x: -1, y: -2, heading: 180 })).closeTo({ x: 1, y: 2, heading: 0 }); + expect(cst.inverseTransform({ x: -1, y: -2, heading: 225 })).closeTo({ x: 1, y: 2, heading: 45 }); + expect(cst.inverseTransform({ x: -1, y: -2, heading: 270 })).closeTo({ x: 1, y: 2, heading: 90 }); + expect(cst.inverseTransform({ x: -1, y: -2, heading: 135 })).closeTo({ x: 1, y: 2, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: -1, y: -2, heading: 90 })).closeTo({ x: 1, y: 2, heading: 270 }); +}); + +test("CoordinateSystemTransformation 270", () => { + let system: CoordinateSystemUnrelatedToFieldAndPath = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XNorthYWest, // Changed + yAxisFlip: YAxisFlip.NoFlip, + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.West, // Changed + headingDirection: HeadingDirection.Clockwise, + originAnchor: OriginAnchor.FieldCenter, + originOffset: { x: 0, y: 0 } + }; + let cst = CoordinateSystemTransformation.buildWithoutFieldAndBeginningInfo(system); + + expect(cst.transform({ x: 1, y: 2, heading: 0 })).closeTo({ x: 2, y: -1, heading: 90 }); + expect(cst.transform({ x: 1, y: 2, heading: 45 })).closeTo({ x: 2, y: -1, heading: 135 }); + expect(cst.transform({ x: 1, y: 2, heading: 90 })).closeTo({ x: 2, y: -1, heading: 180 }); + expect(cst.transform({ x: 1, y: 2, heading: -45 })).closeTo({ x: 2, y: -1, heading: 45 }); + expect(cst.transform({ x: 1, y: 2, heading: 270 })).closeTo({ x: 2, y: -1, heading: 0 }); + + expect(cst.inverseTransform({ x: 2, y: -1, heading: 90 })).closeTo({ x: 1, y: 2, heading: 0 }); + expect(cst.inverseTransform({ x: 2, y: -1, heading: 135 })).closeTo({ x: 1, y: 2, heading: 45 }); + expect(cst.inverseTransform({ x: 2, y: -1, heading: 180 })).closeTo({ x: 1, y: 2, heading: 90 }); + expect(cst.inverseTransform({ x: 2, y: -1, heading: 45 })).closeTo({ x: 1, y: 2, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: 2, y: -1, heading: 0 })).closeTo({ x: 1, y: 2, heading: 270 }); +}); + +test("CoordinateSystemTransformation 90 & 0", () => { + let system: CoordinateSystemUnrelatedToFieldAndPath = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XSouthYEast, // Changed + yAxisFlip: YAxisFlip.NoFlip, + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.North, // Changed + headingDirection: HeadingDirection.Clockwise, + originAnchor: OriginAnchor.FieldCenter, + originOffset: { x: 0, y: 0 } + }; + let cst = CoordinateSystemTransformation.buildWithoutFieldAndBeginningInfo(system); + + expect(cst.transform({ x: 1, y: 2, heading: 0 })).closeTo({ x: -2, y: 1, heading: 0 }); + expect(cst.transform({ x: 1, y: 2, heading: 45 })).closeTo({ x: -2, y: 1, heading: 45 }); + expect(cst.transform({ x: 1, y: 2, heading: 90 })).closeTo({ x: -2, y: 1, heading: 90 }); + expect(cst.transform({ x: 1, y: 2, heading: -45 })).closeTo({ x: -2, y: 1, heading: 315 }); + expect(cst.transform({ x: 1, y: 2, heading: 270 })).closeTo({ x: -2, y: 1, heading: 270 }); + + expect(cst.inverseTransform({ x: -2, y: 1, heading: 0 })).closeTo({ x: 1, y: 2, heading: 0 }); + expect(cst.inverseTransform({ x: -2, y: 1, heading: 45 })).closeTo({ x: 1, y: 2, heading: 45 }); + expect(cst.inverseTransform({ x: -2, y: 1, heading: 90 })).closeTo({ x: 1, y: 2, heading: 90 }); + expect(cst.inverseTransform({ x: -2, y: 1, heading: 315 })).closeTo({ x: 1, y: 2, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: -2, y: 1, heading: 270 })).closeTo({ x: 1, y: 2, heading: 270 }); +}); + +test("CoordinateSystemTransformation 90 & 180", () => { + let system: CoordinateSystemUnrelatedToFieldAndPath = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XSouthYEast, // Changed + yAxisFlip: YAxisFlip.NoFlip, + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.South, // Changed + headingDirection: HeadingDirection.Clockwise, + originAnchor: OriginAnchor.FieldCenter, + originOffset: { x: 0, y: 0 } + }; + let cst = CoordinateSystemTransformation.buildWithoutFieldAndBeginningInfo(system); + + expect(cst.transform({ x: 1, y: 2, heading: 0 })).closeTo({ x: -2, y: 1, heading: 180 }); + expect(cst.transform({ x: 1, y: 2, heading: 45 })).closeTo({ x: -2, y: 1, heading: 225 }); + expect(cst.transform({ x: 1, y: 2, heading: 90 })).closeTo({ x: -2, y: 1, heading: 270 }); + expect(cst.transform({ x: 1, y: 2, heading: -45 })).closeTo({ x: -2, y: 1, heading: 135 }); + expect(cst.transform({ x: 1, y: 2, heading: 270 })).closeTo({ x: -2, y: 1, heading: 90 }); + + expect(cst.inverseTransform({ x: -2, y: 1, heading: 180 })).closeTo({ x: 1, y: 2, heading: 0 }); + expect(cst.inverseTransform({ x: -2, y: 1, heading: 225 })).closeTo({ x: 1, y: 2, heading: 45 }); + expect(cst.inverseTransform({ x: -2, y: 1, heading: 270 })).closeTo({ x: 1, y: 2, heading: 90 }); + expect(cst.inverseTransform({ x: -2, y: 1, heading: 135 })).closeTo({ x: 1, y: 2, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: -2, y: 1, heading: 90 })).closeTo({ x: 1, y: 2, heading: 270 }); +}); + +test("CoordinateSystemTransformation 90 & 270", () => { + let system: CoordinateSystemUnrelatedToFieldAndPath = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XSouthYEast, // Changed + yAxisFlip: YAxisFlip.NoFlip, + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.West, // Changed + headingDirection: HeadingDirection.Clockwise, + originAnchor: OriginAnchor.FieldCenter, + originOffset: { x: 0, y: 0 } + }; + let cst = CoordinateSystemTransformation.buildWithoutFieldAndBeginningInfo(system); + + expect(cst.transform({ x: 1, y: 2, heading: 0 })).closeTo({ x: -2, y: 1, heading: 90 }); + expect(cst.transform({ x: 1, y: 2, heading: 45 })).closeTo({ x: -2, y: 1, heading: 135 }); + expect(cst.transform({ x: 1, y: 2, heading: 90 })).closeTo({ x: -2, y: 1, heading: 180 }); + expect(cst.transform({ x: 1, y: 2, heading: -45 })).closeTo({ x: -2, y: 1, heading: 45 }); + expect(cst.transform({ x: 1, y: 2, heading: 270 })).closeTo({ x: -2, y: 1, heading: 0 }); + + expect(cst.inverseTransform({ x: -2, y: 1, heading: 90 })).closeTo({ x: 1, y: 2, heading: 0 }); + expect(cst.inverseTransform({ x: -2, y: 1, heading: 135 })).closeTo({ x: 1, y: 2, heading: 45 }); + expect(cst.inverseTransform({ x: -2, y: 1, heading: 180 })).closeTo({ x: 1, y: 2, heading: 90 }); + expect(cst.inverseTransform({ x: -2, y: 1, heading: 45 })).closeTo({ x: 1, y: 2, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: -2, y: 1, heading: 0 })).closeTo({ x: 1, y: 2, heading: 270 }); +}); + +test("CoordinateSystemTransformation 180 & flip & 0", () => { + let system: CoordinateSystemUnrelatedToFieldAndPath = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XWestYSouth, // Changed + yAxisFlip: YAxisFlip.Flip, // Changed + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.North, // Changed + headingDirection: HeadingDirection.Clockwise, + originAnchor: OriginAnchor.FieldCenter, + originOffset: { x: 0, y: 0 } + }; + let cst = CoordinateSystemTransformation.buildWithoutFieldAndBeginningInfo(system); + + expect(cst.transform({ x: 1, y: 2, heading: 0 })).closeTo({ x: -1, y: 2, heading: 0 }); + expect(cst.transform({ x: 1, y: 2, heading: 45 })).closeTo({ x: -1, y: 2, heading: 45 }); + expect(cst.transform({ x: 1, y: 2, heading: 90 })).closeTo({ x: -1, y: 2, heading: 90 }); + expect(cst.transform({ x: 1, y: 2, heading: -45 })).closeTo({ x: -1, y: 2, heading: 315 }); + expect(cst.transform({ x: 1, y: 2, heading: 270 })).closeTo({ x: -1, y: 2, heading: 270 }); + + expect(cst.inverseTransform({ x: -1, y: 2, heading: 0 })).closeTo({ x: 1, y: 2, heading: 0 }); + expect(cst.inverseTransform({ x: -1, y: 2, heading: 45 })).closeTo({ x: 1, y: 2, heading: 45 }); + expect(cst.inverseTransform({ x: -1, y: 2, heading: 90 })).closeTo({ x: 1, y: 2, heading: 90 }); + expect(cst.inverseTransform({ x: -1, y: 2, heading: 315 })).closeTo({ x: 1, y: 2, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: -1, y: 2, heading: 270 })).closeTo({ x: 1, y: 2, heading: 270 }); +}); + +test("CoordinateSystemTransformation 180 & flip & 90", () => { + let system: CoordinateSystemUnrelatedToFieldAndPath = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XWestYSouth, // Changed + yAxisFlip: YAxisFlip.Flip, // Changed + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.East, // Changed + headingDirection: HeadingDirection.Clockwise, + originAnchor: OriginAnchor.FieldCenter, + originOffset: { x: 0, y: 0 } + }; + let cst = CoordinateSystemTransformation.buildWithoutFieldAndBeginningInfo(system); + + expect(cst.transform({ x: 1, y: 2, heading: 0 })).closeTo({ x: -1, y: 2, heading: 270 }); + expect(cst.transform({ x: 1, y: 2, heading: 45 })).closeTo({ x: -1, y: 2, heading: 315 }); + expect(cst.transform({ x: 1, y: 2, heading: 90 })).closeTo({ x: -1, y: 2, heading: 0 }); + expect(cst.transform({ x: 1, y: 2, heading: -45 })).closeTo({ x: -1, y: 2, heading: 225 }); + expect(cst.transform({ x: 1, y: 2, heading: 270 })).closeTo({ x: -1, y: 2, heading: 180 }); + + expect(cst.inverseTransform({ x: -1, y: 2, heading: 270 })).closeTo({ x: 1, y: 2, heading: 0 }); + expect(cst.inverseTransform({ x: -1, y: 2, heading: 315 })).closeTo({ x: 1, y: 2, heading: 45 }); + expect(cst.inverseTransform({ x: -1, y: 2, heading: 0 })).closeTo({ x: 1, y: 2, heading: 90 }); + expect(cst.inverseTransform({ x: -1, y: 2, heading: 225 })).closeTo({ x: 1, y: 2, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: -1, y: 2, heading: 180 })).closeTo({ x: 1, y: 2, heading: 270 }); +}); + +test("CoordinateSystemTransformation 180 & flip & 270", () => { + let system: CoordinateSystemUnrelatedToFieldAndPath = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XWestYSouth, // Changed + yAxisFlip: YAxisFlip.Flip, // Changed + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.West, // Changed + headingDirection: HeadingDirection.Clockwise, + originAnchor: OriginAnchor.FieldCenter, + originOffset: { x: 0, y: 0 } + }; + let cst = CoordinateSystemTransformation.buildWithoutFieldAndBeginningInfo(system); + + expect(cst.transform({ x: 1, y: 2, heading: 0 })).closeTo({ x: -1, y: 2, heading: 90 }); + expect(cst.transform({ x: 1, y: 2, heading: 45 })).closeTo({ x: -1, y: 2, heading: 135 }); + expect(cst.transform({ x: 1, y: 2, heading: 90 })).closeTo({ x: -1, y: 2, heading: 180 }); + expect(cst.transform({ x: 1, y: 2, heading: -45 })).closeTo({ x: -1, y: 2, heading: 45 }); + expect(cst.transform({ x: 1, y: 2, heading: 270 })).closeTo({ x: -1, y: 2, heading: 0 }); + + expect(cst.inverseTransform({ x: -1, y: 2, heading: 90 })).closeTo({ x: 1, y: 2, heading: 0 }); + expect(cst.inverseTransform({ x: -1, y: 2, heading: 135 })).closeTo({ x: 1, y: 2, heading: 45 }); + expect(cst.inverseTransform({ x: -1, y: 2, heading: 180 })).closeTo({ x: 1, y: 2, heading: 90 }); + expect(cst.inverseTransform({ x: -1, y: 2, heading: 45 })).closeTo({ x: 1, y: 2, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: -1, y: 2, heading: 0 })).closeTo({ x: 1, y: 2, heading: 270 }); +}); + +test("CoordinateSystemTransformation 270 & flip & 0 & ccw", () => { + let system: CoordinateSystemUnrelatedToFieldAndPath = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XNorthYWest, // Changed + yAxisFlip: YAxisFlip.Flip, // Changed + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.North, // Changed + headingDirection: HeadingDirection.CounterClockwise, // Changed + originAnchor: OriginAnchor.FieldCenter, + originOffset: { x: 0, y: 0 } + }; + let cst = CoordinateSystemTransformation.buildWithoutFieldAndBeginningInfo(system); + + expect(cst.transform({ x: 1, y: 2, heading: 0 })).closeTo({ x: 2, y: 1, heading: 0 }); + expect(cst.transform({ x: 1, y: 2, heading: 45 })).closeTo({ x: 2, y: 1, heading: 315 }); + expect(cst.transform({ x: 1, y: 2, heading: 90 })).closeTo({ x: 2, y: 1, heading: 270 }); + expect(cst.transform({ x: 1, y: 2, heading: -45 })).closeTo({ x: 2, y: 1, heading: 45 }); + expect(cst.transform({ x: 1, y: 2, heading: 270 })).closeTo({ x: 2, y: 1, heading: 90 }); + + expect(cst.inverseTransform({ x: 2, y: 1, heading: 0 })).closeTo({ x: 1, y: 2, heading: 0 }); + expect(cst.inverseTransform({ x: 2, y: 1, heading: 315 })).closeTo({ x: 1, y: 2, heading: 45 }); + expect(cst.inverseTransform({ x: 2, y: 1, heading: 270 })).closeTo({ x: 1, y: 2, heading: 90 }); + expect(cst.inverseTransform({ x: 2, y: 1, heading: 45 })).closeTo({ x: 1, y: 2, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: 2, y: 1, heading: 90 })).closeTo({ x: 1, y: 2, heading: 270 }); +}); + +test("CoordinateSystemTransformation 270 & flip & 90 & ccw", () => { + let system: CoordinateSystemUnrelatedToFieldAndPath = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XNorthYWest, // Changed + yAxisFlip: YAxisFlip.Flip, // Changed + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.East, // Changed + headingDirection: HeadingDirection.CounterClockwise, // Changed + originAnchor: OriginAnchor.FieldCenter, + originOffset: { x: 0, y: 0 } + }; + let cst = CoordinateSystemTransformation.buildWithoutFieldAndBeginningInfo(system); + + expect(cst.transform({ x: 1, y: 2, heading: 0 })).closeTo({ x: 2, y: 1, heading: 90 }); + expect(cst.transform({ x: 1, y: 2, heading: 45 })).closeTo({ x: 2, y: 1, heading: 45 }); + expect(cst.transform({ x: 1, y: 2, heading: 90 })).closeTo({ x: 2, y: 1, heading: 0 }); + expect(cst.transform({ x: 1, y: 2, heading: -45 })).closeTo({ x: 2, y: 1, heading: 135 }); + expect(cst.transform({ x: 1, y: 2, heading: 270 })).closeTo({ x: 2, y: 1, heading: 180 }); + + expect(cst.inverseTransform({ x: 2, y: 1, heading: 90 })).closeTo({ x: 1, y: 2, heading: 0 }); + expect(cst.inverseTransform({ x: 2, y: 1, heading: 45 })).closeTo({ x: 1, y: 2, heading: 45 }); + expect(cst.inverseTransform({ x: 2, y: 1, heading: 0 })).closeTo({ x: 1, y: 2, heading: 90 }); + expect(cst.inverseTransform({ x: 2, y: 1, heading: 135 })).closeTo({ x: 1, y: 2, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: 2, y: 1, heading: 180 })).closeTo({ x: 1, y: 2, heading: 270 }); +}); + +test("CoordinateSystemTransformation 270 & flip & 180 & ccw", () => { + let system: CoordinateSystemUnrelatedToFieldAndPath = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XNorthYWest, // Changed + yAxisFlip: YAxisFlip.Flip, // Changed + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.South, // Changed + headingDirection: HeadingDirection.CounterClockwise, // Changed + originAnchor: OriginAnchor.FieldCenter, + originOffset: { x: 0, y: 0 } + }; + let cst = CoordinateSystemTransformation.buildWithoutFieldAndBeginningInfo(system); + + expect(cst.transform({ x: 1, y: 2, heading: 0 })).closeTo({ x: 2, y: 1, heading: 180 }); + expect(cst.transform({ x: 1, y: 2, heading: 45 })).closeTo({ x: 2, y: 1, heading: 135 }); + expect(cst.transform({ x: 1, y: 2, heading: 90 })).closeTo({ x: 2, y: 1, heading: 90 }); + expect(cst.transform({ x: 1, y: 2, heading: -45 })).closeTo({ x: 2, y: 1, heading: 225 }); + expect(cst.transform({ x: 1, y: 2, heading: 270 })).closeTo({ x: 2, y: 1, heading: 270 }); + + expect(cst.inverseTransform({ x: 2, y: 1, heading: 180 })).closeTo({ x: 1, y: 2, heading: 0 }); + expect(cst.inverseTransform({ x: 2, y: 1, heading: 135 })).closeTo({ x: 1, y: 2, heading: 45 }); + expect(cst.inverseTransform({ x: 2, y: 1, heading: 90 })).closeTo({ x: 1, y: 2, heading: 90 }); + expect(cst.inverseTransform({ x: 2, y: 1, heading: 225 })).closeTo({ x: 1, y: 2, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: 2, y: 1, heading: 270 })).closeTo({ x: 1, y: 2, heading: 270 }); +}); + +test("CoordinateSystemTransformation 0 & no-flip & 0 & cw & TopRight", () => { + let system: CoordinateSystemUnrelatedToPath = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XEastYNorth, // Changed + yAxisFlip: YAxisFlip.NoFlip, // Changed + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.North, // Changed + headingDirection: HeadingDirection.Clockwise, // Changed + originAnchor: OriginAnchor.FieldTopRight, // Changed + originOffset: { x: 0, y: 0 } + }; + let fd: Dimension = { width: 400, height: 300 }; + + let cst = CoordinateSystemTransformation.buildWithoutBeginningInfo(system, fd); + + expect(cst.transform({ x: 1, y: 2, heading: 0 })).closeTo({ x: 1 - 200, y: 2 - 150, heading: 0 }); + expect(cst.transform({ x: 1, y: 2, heading: 45 })).closeTo({ x: 1 - 200, y: 2 - 150, heading: 45 }); + expect(cst.transform({ x: 1, y: 2, heading: 90 })).closeTo({ x: 1 - 200, y: 2 - 150, heading: 90 }); + expect(cst.transform({ x: 1, y: 2, heading: -45 })).closeTo({ x: 1 - 200, y: 2 - 150, heading: 315 }); + expect(cst.transform({ x: 1, y: 2, heading: 270 })).closeTo({ x: 1 - 200, y: 2 - 150, heading: 270 }); + + expect(cst.inverseTransform({ x: 1 - 200, y: 2 - 150, heading: 0 })).closeTo({ x: 1, y: 2, heading: 0 }); + expect(cst.inverseTransform({ x: 1 - 200, y: 2 - 150, heading: 45 })).closeTo({ x: 1, y: 2, heading: 45 }); + expect(cst.inverseTransform({ x: 1 - 200, y: 2 - 150, heading: 90 })).closeTo({ x: 1, y: 2, heading: 90 }); + expect(cst.inverseTransform({ x: 1 - 200, y: 2 - 150, heading: 315 })).closeTo({ x: 1, y: 2, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: 1 - 200, y: 2 - 150, heading: 270 })).closeTo({ x: 1, y: 2, heading: 270 }); +}); + +test("CoordinateSystemTransformation 0 & flip & 90 & cw & BottomRight", () => { + let system: CoordinateSystemUnrelatedToPath = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XEastYNorth, // Changed + yAxisFlip: YAxisFlip.Flip, // Changed + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.East, // Changed + headingDirection: HeadingDirection.Clockwise, // Changed + originAnchor: OriginAnchor.FieldBottomRight, // Changed + originOffset: { x: 0, y: 0 } + }; + let fd: Dimension = { width: 400, height: 300 }; + + let cst = CoordinateSystemTransformation.buildWithoutBeginningInfo(system, fd); + + expect(cst.transform({ x: 1, y: 2, heading: 0 })).closeTo({ x: 1 - 200, y: -2 - 150, heading: 270 }); + expect(cst.transform({ x: 1, y: 2, heading: 45 })).closeTo({ x: 1 - 200, y: -2 - 150, heading: 315 }); + expect(cst.transform({ x: 1, y: 2, heading: 90 })).closeTo({ x: 1 - 200, y: -2 - 150, heading: 0 }); + expect(cst.transform({ x: 1, y: 2, heading: -45 })).closeTo({ x: 1 - 200, y: -2 - 150, heading: 225 }); + expect(cst.transform({ x: 1, y: 2, heading: 270 })).closeTo({ x: 1 - 200, y: -2 - 150, heading: 180 }); + + expect(cst.inverseTransform({ x: 1 - 200, y: -2 - 150, heading: 270 })).closeTo({ x: 1, y: 2, heading: 0 }); + expect(cst.inverseTransform({ x: 1 - 200, y: -2 - 150, heading: 315 })).closeTo({ x: 1, y: 2, heading: 45 }); + expect(cst.inverseTransform({ x: 1 - 200, y: -2 - 150, heading: 0 })).closeTo({ x: 1, y: 2, heading: 90 }); + expect(cst.inverseTransform({ x: 1 - 200, y: -2 - 150, heading: 225 })).closeTo({ x: 1, y: 2, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: 1 - 200, y: -2 - 150, heading: 180 })).closeTo({ x: 1, y: 2, heading: 270 }); +}); + +test("CoordinateSystemTransformation 0 & flip & 180 & cw & BottomLeft", () => { + let system: CoordinateSystemUnrelatedToPath = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XEastYNorth, // Changed + yAxisFlip: YAxisFlip.Flip, // Changed + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.South, // Changed + headingDirection: HeadingDirection.Clockwise, // Changed + originAnchor: OriginAnchor.FieldBottomLeft, // Changed + originOffset: { x: 0, y: 0 } + }; + let fd: Dimension = { width: 400, height: 300 }; + + let cst = CoordinateSystemTransformation.buildWithoutBeginningInfo(system, fd); + + expect(cst.transform({ x: 1, y: 2, heading: 0 })).closeTo({ x: 1 + 200, y: -2 - 150, heading: 180 }); + expect(cst.transform({ x: 1, y: 2, heading: 45 })).closeTo({ x: 1 + 200, y: -2 - 150, heading: 225 }); + expect(cst.transform({ x: 1, y: 2, heading: 90 })).closeTo({ x: 1 + 200, y: -2 - 150, heading: 270 }); + expect(cst.transform({ x: 1, y: 2, heading: -45 })).closeTo({ x: 1 + 200, y: -2 - 150, heading: 135 }); + expect(cst.transform({ x: 1, y: 2, heading: 270 })).closeTo({ x: 1 + 200, y: -2 - 150, heading: 90 }); + + expect(cst.inverseTransform({ x: 1 + 200, y: -2 - 150, heading: 180 })).closeTo({ x: 1, y: 2, heading: 0 }); + expect(cst.inverseTransform({ x: 1 + 200, y: -2 - 150, heading: 225 })).closeTo({ x: 1, y: 2, heading: 45 }); + expect(cst.inverseTransform({ x: 1 + 200, y: -2 - 150, heading: 270 })).closeTo({ x: 1, y: 2, heading: 90 }); + expect(cst.inverseTransform({ x: 1 + 200, y: -2 - 150, heading: 135 })).closeTo({ x: 1, y: 2, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: 1 + 200, y: -2 - 150, heading: 90 })).closeTo({ x: 1, y: 2, heading: 270 }); +}); + +test("CoordinateSystemTransformation 90 & no-flip & 270 & ccw & TopLeft", () => { + let system: CoordinateSystemUnrelatedToPath = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XSouthYEast, // Changed + yAxisFlip: YAxisFlip.NoFlip, // Changed + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.West, // Changed + headingDirection: HeadingDirection.CounterClockwise, // Changed + originAnchor: OriginAnchor.FieldTopLeft, // Changed + originOffset: { x: 0, y: 0 } + }; + let fd: Dimension = { width: 400, height: 300 }; + + let cst = CoordinateSystemTransformation.buildWithoutBeginningInfo(system, fd); + + expect(cst.transform({ x: 1, y: 2, heading: 0 })).closeTo({ x: -2 + 150, y: 1 + 200, heading: 270 }); + expect(cst.transform({ x: 1, y: 2, heading: 45 })).closeTo({ x: -2 + 150, y: 1 + 200, heading: 225 }); + expect(cst.transform({ x: 1, y: 2, heading: 90 })).closeTo({ x: -2 + 150, y: 1 + 200, heading: 180 }); + expect(cst.transform({ x: 1, y: 2, heading: -45 })).closeTo({ x: -2 + 150, y: 1 + 200, heading: 315 }); + expect(cst.transform({ x: 1, y: 2, heading: 270 })).closeTo({ x: -2 + 150, y: 1 + 200, heading: 0 }); + + expect(cst.inverseTransform({ x: -2 + 150, y: 1 + 200, heading: 270 })).closeTo({ x: 1, y: 2, heading: 0 }); + expect(cst.inverseTransform({ x: -2 + 150, y: 1 + 200, heading: 225 })).closeTo({ x: 1, y: 2, heading: 45 }); + expect(cst.inverseTransform({ x: -2 + 150, y: 1 + 200, heading: 180 })).closeTo({ x: 1, y: 2, heading: 90 }); + expect(cst.inverseTransform({ x: -2 + 150, y: 1 + 200, heading: 315 })).closeTo({ x: 1, y: 2, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: -2 + 150, y: 1 + 200, heading: 0 })).closeTo({ x: 1, y: 2, heading: 270 }); +}); + +test("CoordinateSystemTransformation 0 & no-flip & 0 & ccw & path-beginning & offset", () => { + let system: CoordinateSystemUnrelatedToField = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XEastYNorth, + yAxisFlip: YAxisFlip.NoFlip, + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.North, + headingDirection: HeadingDirection.CounterClockwise, // Changed + originAnchor: OriginAnchor.PathBeginning, // Changed + originOffset: { x: 10, y: 20 } // Changed + }; + let beginning = { x: 400, y: 300, heading: 45 }; + + let cst = CoordinateSystemTransformation.buildWithoutFieldInfo(system, beginning); + + expect(cst.transform({ x: 1, y: 2, heading: 0 })).closeTo({ x: -409, y: -318, heading: 0 }); + expect(cst.transform({ x: 1, y: 2, heading: 45 })).closeTo({ x: -409, y: -318, heading: 315 }); + expect(cst.transform({ x: 1, y: 2, heading: 90 })).closeTo({ x: -409, y: -318, heading: 270 }); + expect(cst.transform({ x: 1, y: 2, heading: -45 })).closeTo({ x: -409, y: -318, heading: 45 }); + expect(cst.transform({ x: 1, y: 2, heading: 270 })).closeTo({ x: -409, y: -318, heading: 90 }); + + expect(cst.inverseTransform({ x: -409, y: -318, heading: 0 })).closeTo({ x: 1, y: 2, heading: 0 }); + expect(cst.inverseTransform({ x: -409, y: -318, heading: 315 })).closeTo({ x: 1, y: 2, heading: 45 }); + expect(cst.inverseTransform({ x: -409, y: -318, heading: 270 })).closeTo({ x: 1, y: 2, heading: 90 }); + expect(cst.inverseTransform({ x: -409, y: -318, heading: 45 })).closeTo({ x: 1, y: 2, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: -409, y: -318, heading: 90 })).closeTo({ x: 1, y: 2, heading: 270 }); +}); + +test("CoordinateSystemTransformation 0 & no-flip & path-beginning & ccw & path-beginning & offset", () => { + let system: CoordinateSystemUnrelatedToField = { + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XEastYNorth, + yAxisFlip: YAxisFlip.NoFlip, + headingAnchor: HeadingAnchor.PathBeginning, + headingRotation: HeadingRotation.North, // Changed + headingDirection: HeadingDirection.CounterClockwise, // Changed + originAnchor: OriginAnchor.PathBeginning, // Changed + originOffset: { x: 10, y: 20 } // Changed + }; + let beginning = { x: 400, y: 300, heading: 45 }; + + let cst = CoordinateSystemTransformation.buildWithoutFieldInfo(system, beginning); + + expect(cst.transform({ x: 1, y: 2, heading: 0 })).closeTo({ x: -409, y: -318, heading: 45 }); + expect(cst.transform({ x: 1, y: 2, heading: 45 })).closeTo({ x: -409, y: -318, heading: 0 }); + expect(cst.transform({ x: 1, y: 2, heading: 90 })).closeTo({ x: -409, y: -318, heading: 315 }); + expect(cst.transform({ x: 1, y: 2, heading: -45 })).closeTo({ x: -409, y: -318, heading: 90 }); + expect(cst.transform({ x: 1, y: 2, heading: 270 })).closeTo({ x: -409, y: -318, heading: 135 }); + + expect(cst.inverseTransform({ x: -409, y: -318, heading: 45 })).closeTo({ x: 1, y: 2, heading: 0 }); + expect(cst.inverseTransform({ x: -409, y: -318, heading: 0 })).closeTo({ x: 1, y: 2, heading: 45 }); + expect(cst.inverseTransform({ x: -409, y: -318, heading: 315 })).closeTo({ x: 1, y: 2, heading: 90 }); + expect(cst.inverseTransform({ x: -409, y: -318, heading: 90 })).closeTo({ x: 1, y: 2, heading: 360 - 45 }); + expect(cst.inverseTransform({ x: -409, y: -318, heading: 135 })).closeTo({ x: 1, y: 2, heading: 270 }); +}); diff --git a/src/core/CoordinateSystem.ts b/src/core/CoordinateSystem.ts new file mode 100755 index 0000000..0e8c9d4 --- /dev/null +++ b/src/core/CoordinateSystem.ts @@ -0,0 +1,271 @@ +import { Coordinate, CoordinateWithHeading, EuclideanTransformation, isCoordinateWithHeading } from "./Coordinate"; +import { Vector } from "./Path"; +import { boundHeading } from "./Calculation"; + +export interface Dimension { + width: number; + height: number; +} + +/* +- Axis Rotation: `X-East Y-North` | `X-South Y-East`| `X-West Y-South`| `X-North Y-West` +- Y Axis Flip: `True` | `False` +- Heading Rotation: `Path Beginning` | `East` | `South` | `West` | `North` +- Heading Direction: `CW` | `CCW` +- Origin Position: `Path Beginning` | `Field Top Left` | `Field Top Center` | `Field Top Right` | `Field Left` | `Field Center` | `Field Right` | `Field Bottom Left` | `Field Bottom Center` | `Field Bottom Right` +- Origin X Offset: number (mm) +- Origin Y Offset: number (mm) +*/ + +export enum AxisAnchor { + PathBeginning = "PathBeginning", + Default = "Default" +} + +export enum HeadingAnchor { + PathBeginning = "PathBeginning", + Default = "Default" +} + +export enum AxisRotation { + XEastYNorth = 0, + XSouthYEast = 90, + XWestYSouth = 180, + XNorthYWest = 270 +} + +export enum YAxisFlip { + Flip = -1, + NoFlip = 1 +} + +export enum HeadingRotation { + North = 0, + East = 90, + South = 180, + West = 270 +} + +export enum HeadingDirection { + Clockwise = 1, + CounterClockwise = -1 +} + +export class OriginAnchor { + static PathBeginning = "PathBeginning" as const; + static FieldTopLeft = { x: -1, y: 1 } as const; + static FieldTopCenter = { x: 0, y: 1 } as const; + static FieldTopRight = { x: 1, y: 1 } as const; + static FieldLeft = { x: -1, y: 0 } as const; + static FieldCenter = { x: 0, y: 0 } as const; + static FieldRight = { x: 1, y: 0 } as const; + static FieldBottomLeft = { x: -1, y: -1 } as const; + static FieldBottomCenter = { x: 0, y: -1 } as const; + static FieldBottomRight = { x: 1, y: -1 } as const; +} + +export type OriginAnchorType = + | typeof OriginAnchor.PathBeginning + | typeof OriginAnchor.FieldTopLeft + | typeof OriginAnchor.FieldTopCenter + | typeof OriginAnchor.FieldTopRight + | typeof OriginAnchor.FieldLeft + | typeof OriginAnchor.FieldCenter + | typeof OriginAnchor.FieldRight + | typeof OriginAnchor.FieldBottomLeft + | typeof OriginAnchor.FieldBottomCenter + | typeof OriginAnchor.FieldBottomRight; + +export interface CoordinateSystem { + axisAnchor: AxisAnchor; + axisRotation: AxisRotation; + yAxisFlip: YAxisFlip; + headingAnchor: HeadingAnchor; + headingRotation: HeadingRotation; + headingDirection: HeadingDirection; + originAnchor: OriginAnchorType; + originOffset: Coordinate; // mm +} + +export interface CoordinateSystemUnrelatedToField extends CoordinateSystem { + originAnchor: typeof OriginAnchor.FieldCenter | typeof OriginAnchor.PathBeginning; +} + +export interface CoordinateSystemUnrelatedToPath extends CoordinateSystem { + axisAnchor: AxisAnchor.Default; + headingAnchor: HeadingAnchor.Default; + originAnchor: Exclude; +} + +export interface CoordinateSystemUnrelatedToFieldAndPath extends CoordinateSystem { + axisAnchor: AxisAnchor.Default; + headingAnchor: HeadingAnchor.Default; + originAnchor: typeof OriginAnchor.FieldCenter; +} + +export interface NamedCoordinateSystem extends CoordinateSystem { + name: string; + description: string; + previewImageUrl: string; +} + +export function getNamedCoordinateSystems(): NamedCoordinateSystem[] { + return [ + { + name: "VEX Gaming Positioning System", + description: "The standard coordinate system defined by VEX Robotics.", + previewImageUrl: "static/coordinate-system-preview-vex-gps.png", + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XEastYNorth, + yAxisFlip: YAxisFlip.NoFlip, + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.North, + headingDirection: HeadingDirection.Clockwise, + originAnchor: OriginAnchor.FieldCenter, + originOffset: { x: 0, y: 0 } + }, + { + name: "Cartesian Plane", + description: + "A standard Cartesian coordinate system. Heading is measured in degrees counterclockwise from the positive x-axis.", + previewImageUrl: "static/coordinate-system-preview-cartesian-plane.png", + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XEastYNorth, + yAxisFlip: YAxisFlip.NoFlip, + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.East, + headingDirection: HeadingDirection.CounterClockwise, + originAnchor: OriginAnchor.FieldCenter, + originOffset: { x: 0, y: 0 } + }, + { + name: "Path-Based Coordinates", + description: + "This coordinate system is relative to the beginning of a path. The origin is set at the path's starting point, and the axes are aligned to the field's default orientation.", + previewImageUrl: "static/coordinate-system-preview-path-relative.png", + axisAnchor: AxisAnchor.Default, + axisRotation: AxisRotation.XEastYNorth, + yAxisFlip: YAxisFlip.NoFlip, + headingAnchor: HeadingAnchor.Default, + headingRotation: HeadingRotation.North, + headingDirection: HeadingDirection.Clockwise, + originAnchor: OriginAnchor.PathBeginning, + originOffset: { x: 0, y: 0 } + }, + { + name: "Path-Based Strict Coordinates", + description: + "A strict version of the Path-Based Coordinates system. The origin and axes are both anchored to the beginning of the path.", + previewImageUrl: "static/coordinate-system-preview-path-relative-strict-mode.png", + axisAnchor: AxisAnchor.PathBeginning, + axisRotation: AxisRotation.XEastYNorth, + yAxisFlip: YAxisFlip.NoFlip, + headingAnchor: HeadingAnchor.PathBeginning, + headingRotation: HeadingRotation.North, + headingDirection: HeadingDirection.Clockwise, + originAnchor: OriginAnchor.PathBeginning, + originOffset: { x: 0, y: 0 } + } + ]; +} + +function getOrigin( + system: CoordinateSystem, + fieldHalf: Vector, + pathBeginning: CoordinateWithHeading +): CoordinateWithHeading { + const originPreOffset = + system.originAnchor !== "PathBeginning" + ? fieldHalf.multiply(new Vector(system.originAnchor.x, system.originAnchor.y)) + : new Vector(pathBeginning.x, pathBeginning.y); + + if (system.axisAnchor === "PathBeginning") { + return { x: originPreOffset.x, y: originPreOffset.y, heading: pathBeginning.heading + system.axisRotation }; + } else { + return { x: originPreOffset.x, y: originPreOffset.y, heading: system.axisRotation }; + } +} + +function getHeadingRotation(system: CoordinateSystem, pathBeginning: CoordinateWithHeading) { + if (system.headingAnchor === "PathBeginning") { + return pathBeginning.heading + system.headingRotation; + } else { + return system.headingRotation; + } +} + +export class CoordinateSystemTransformation { + private et: EuclideanTransformation; + private headingRotation: number; + + constructor( + readonly system: CoordinateSystem, + readonly fieldDimension: Dimension, + readonly pathBeginning: CoordinateWithHeading + ) { + const fieldHalf = new Vector(fieldDimension.width / 2, fieldDimension.height / 2); + const originPreOffsetAndAxisRotation = getOrigin(system, fieldHalf, pathBeginning); // coordinate in local system + const tempEt = new EuclideanTransformation(originPreOffsetAndAxisRotation); + const originWithOffset = tempEt.inverseTransform(system.originOffset); // returns coordinate in local system + const originWithOffsetAndAxisRotation = { ...originWithOffset, heading: originPreOffsetAndAxisRotation.heading }; // coordinate in local system + + this.et = new EuclideanTransformation(originWithOffsetAndAxisRotation); + + this.headingRotation = getHeadingRotation(system, pathBeginning); + } + + static buildWithoutFieldInfo( + system: CoordinateSystemUnrelatedToField, + pathBeginning: CoordinateWithHeading + ): CoordinateSystemTransformation { + return new CoordinateSystemTransformation(system, { width: 0, height: 0 }, pathBeginning); + } + + static buildWithoutBeginningInfo( + system: CoordinateSystemUnrelatedToPath, + fieldDimension: Dimension + ): CoordinateSystemTransformation { + return new CoordinateSystemTransformation(system, fieldDimension, { x: 0, y: 0, heading: 0 }); + } + + static buildWithoutFieldAndBeginningInfo( + system: CoordinateSystemUnrelatedToFieldAndPath + ): CoordinateSystemTransformation { + return new CoordinateSystemTransformation(system, { width: 0, height: 0 }, { x: 0, y: 0, heading: 0 }); + } + + transform(alpha: Coordinate): Coordinate; + transform(alpha: CoordinateWithHeading): CoordinateWithHeading; + transform(alpha: Coordinate | CoordinateWithHeading): Coordinate | CoordinateWithHeading { + const transformed = this.et.transform(alpha); + + transformed.y *= this.system.yAxisFlip; + + if (isCoordinateWithHeading(alpha)) { + const temp = boundHeading(alpha.heading - this.headingRotation); + const heading = boundHeading(temp * this.system.headingDirection); + + return { ...transformed, heading }; + } else { + return transformed; + } + } + + inverseTransform(beta: Coordinate): Coordinate; + inverseTransform(beta: CoordinateWithHeading): CoordinateWithHeading; + inverseTransform(beta: Coordinate | CoordinateWithHeading): Coordinate | CoordinateWithHeading { + const temp = { ...beta }; + temp.y *= this.system.yAxisFlip; + + const transformed = this.et.inverseTransform(temp); + + if (isCoordinateWithHeading(beta)) { + const temp = boundHeading(beta.heading * this.system.headingDirection); + const heading = boundHeading(temp + this.headingRotation); + + return { ...transformed, heading }; + } else { + return transformed; + } + } +} diff --git a/src/core/FieldEditor.ts b/src/core/FieldEditor.ts old mode 100644 new mode 100755 diff --git a/src/core/FieldImagePrompt.tsx b/src/core/FieldImagePrompt.tsx old mode 100644 new mode 100755 diff --git a/src/core/GoogleAnalytics.ts b/src/core/GoogleAnalytics.ts old mode 100644 new mode 100755 diff --git a/src/core/Hook.ts b/src/core/Hook.ts old mode 100644 new mode 100755 diff --git a/src/core/InputOutput.ts b/src/core/InputOutput.ts old mode 100644 new mode 100755 diff --git a/src/core/Layout.ts b/src/core/Layout.ts old mode 100644 new mode 100755 index 55d8697..2e1ba10 --- a/src/core/Layout.ts +++ b/src/core/Layout.ts @@ -27,6 +27,11 @@ export enum LayoutType { export const LayoutContext = React.createContext(LayoutType.Classic); export const LayoutProvider = LayoutContext.Provider; +/** + * Get all available layout types based on the current window size + * @param windowSize - The current window size + * @returns All available layout types + */ export function getAvailableLayouts(windowSize: Vector): LayoutType[] { const widthForClassic = 16 + 288 + 16 + getFieldCanvasHalfHeight(windowSize) + 16 + 352 + 16; const heightForClassic = 600; @@ -41,12 +46,22 @@ export function getAvailableLayouts(windowSize: Vector): LayoutType[] { return [LayoutType.Mobile]; } +/** + * Elect the preferred layout from all available layout types, which found based on the current window size + * If the preferred layout is not available, return the first available layout type + * @param windowSize - The current window size + * @param preferred - The preferred layout type + * @returns The elected layout type + */ export function getUsableLayout(windowSize: Vector, preferred: LayoutType): LayoutType { const available = getAvailableLayouts(windowSize); if (available.includes(preferred)) return preferred; return available[0]; } +/** + * The UserInterface class is responsible for managing all overlay and panel components + */ export class UserInterface { private overlayNodeBuilders_: { uid: string; builder: OverlayNodeBuilder }[] = []; private panelBuilders_: { uid: string; builder: PanelBuilder }[] = []; @@ -56,18 +71,37 @@ export class UserInterface { priority: number; } | null = null; + /** + * Get the symbol of the opening modal + * @returns The symbol of the opening modal, or null if no modal is opening + */ get openingModal(): Symbol | null { return this.openingModal_?.symbol ?? null; } + /** + * Get the priority of the opening modal + * @returns The priority of the opening modal, or null if no modal is opening + */ get currentOpeningModalPriority(): number | null { return this.openingModal_?.priority ?? null; } + /** + * Check if a modal is opening + * @returns True if a modal is opening, otherwise false + */ get isOpeningModal() { return this.openingModal_ !== null; } + /** + * Open a modal with a given symbol and priority, the opened modal's symbol and priority will be stored + * If the given priority is higher than the current opening modal's priority, the opening modal will be replaced + * @param symbol - The symbol of the modal to open + * @param priority - The priority of the modal to open, default is 0 + * @returns True if the modal is opened, otherwise false + */ openModal(symbol: Symbol, priority: number = 0): boolean { if (this.openingModal_ === null || this.openingModal_.priority <= priority) { this.openingModal_ = { symbol: symbol, priority }; @@ -77,20 +111,42 @@ export class UserInterface { } } + /** + * Close the opening modal with a given symbol + * If the given symbol is not provided, or the symbol is the same as the opening modal, the opening modal will be closed + * @param symbol - The symbol of the modal to close + */ closeModal(symbol?: Symbol) { if (symbol === undefined || this.openingModal_?.symbol === symbol) this.openingModal_ = null; } + /** + * Register function for register the corresponding builder function of a overlay and ID to the Overlay Builder list. + * Return with the ID and disposer function of the corresponding overlay component + * @param builder - The function to render the overlay component + * @returns The object of ID and disposer function of the corresponding overlay component + */ registerOverlay(builder: OverlayNodeBuilder): { uid: string; disposer: () => void } { const uid = makeId(10); this.overlayNodeBuilders_.push({ uid, builder }); return { uid, disposer: () => this.unregisterOverlay(uid) }; } + /** + * To remove a registered object record of overlay builder via ID from the Overlay Builder list. + * @param uid - The ID of the overlay builder to be removed + */ unregisterOverlay(uid: string): void { this.overlayNodeBuilders_ = this.overlayNodeBuilders_.filter(obj => obj.uid !== uid); } + /** + * Register function for register the corresponding builder function of a panel and ID to the Panel Builder list. + * Return with the ID and disposer function of the corresponding panel component + * @param builder - The function to render the panel component + * @param index - The index of the panel component in the list + * @returns The object of ID and disposer function of the corresponding panel component + */ registerPanel(builder: PanelBuilder, index?: number): { uid: string; disposer: () => void } { const uid = makeId(10); @@ -102,14 +158,26 @@ export class UserInterface { return { uid, disposer: () => this.unregisterPanel(uid) }; } + /** + * To remove a registered object record of panel builder via ID from the Panel Builder list. + * @param uid - The ID of the panel builder to be removed + */ unregisterPanel(uid: string): void { this.panelBuilders_ = this.panelBuilders_.filter(obj => obj.uid !== uid); } + /** + * Get all registered overlay builders + * @returns The list of all registered overlay builders + */ getAllOverlays(): { uid: string; builder: OverlayNodeBuilder }[] { return this.overlayNodeBuilders_; } + /** + * Get all registered panel builders + * @returns The list of all registered panel builders + */ getAllPanels(): { uid: string; builder: PanelBuilder }[] { return this.panelBuilders_; } diff --git a/src/core/Logger.ts b/src/core/Logger.ts old mode 100644 new mode 100755 diff --git a/src/core/LoggerImpl.ts b/src/core/LoggerImpl.ts old mode 100644 new mode 100755 diff --git a/src/core/Magnet.ts b/src/core/Magnet.ts old mode 100644 new mode 100755 diff --git a/src/core/MainApp.ts b/src/core/MainApp.ts old mode 100644 new mode 100755 index 49f0993..439eaef --- a/src/core/MainApp.ts +++ b/src/core/MainApp.ts @@ -23,11 +23,12 @@ import { AppClipboard } from "./Clipboard"; import { validate } from "class-validator"; import { FieldEditor } from "./FieldEditor"; import { SpeedEditor } from "./SpeedEditor"; -import { AssetManager, getDefaultBuiltInFieldImage } from "./Asset"; +import { AssetManager, FieldImageAsset, FieldImageOriginType, getDefaultBuiltInFieldImage } from "./Asset"; import { Preferences, getPreference } from "./Preferences"; import { LemLibFormatV0_4 } from "../format/LemLibFormatV0_4"; import { LemLibFormatV1_0 } from "../format/LemLibFormatV1_0"; import { UserInterface } from "./Layout"; +import { CoordinateSystem, Dimension, getNamedCoordinateSystems } from "./CoordinateSystem"; export const APP_VERSION = new SemVer(APP_VERSION_STRING); @@ -46,6 +47,7 @@ export class MainApp { private selected: string[] = []; // ALGO: Not using Set because order matters private lastInterestedPath: Path | undefined = undefined; // ALGO: For adding controls private expanded: string[] = []; // ALGO: Order doesn't matter but anyway + private fieldDimension_: Dimension = { width: 0, height: 0 }; public robot = { position: new EndControl(0, 0, 0) @@ -119,6 +121,19 @@ export class MainApp { } ); + reaction( + () => this.fieldImageAsset.signature, + () => { + this.fieldDimension_ = { width: 0, height: 0 }; + const signature = this.fieldImageAsset.signature; + this.fieldImageAsset.getDimension().then(dimension => { + if (signature === this.fieldImageAsset.signature) { + runInAction(() => (this.fieldDimension_ = dimension)); + } + }); + } + ); + this.newFile(); } @@ -160,6 +175,20 @@ export class MainApp { return this.format.getGeneralConfig(); } + @computed get fieldImageAsset(): FieldImageAsset { + return assetManager.getAssetBySignature(this.gc.fieldImage.signature) ?? getDefaultBuiltInFieldImage(); + } + + @computed get fieldDimension(): Dimension { + return this.fieldDimension_; + } + + @computed get coordinateSystem(): CoordinateSystem { + return ( + getNamedCoordinateSystems().find(cs => cs.name === this.gc.coordinateSystem) ?? getNamedCoordinateSystems()[0] + ); + } + isSelected(x: PathTreeItem | string): boolean { return typeof x === "string" ? this.selected.includes(x) : this.selected.includes(x.uid); } @@ -359,7 +388,7 @@ export class MainApp { throw new Error("Unable to open the path file due to validation errors."); } - getAppStores().ga.gtag("event", "import_file_format", { format: format.getName() }); + ga.gtag("event", "import_file_format", { format: format.getName() }); const result = await runInActionAsync(() => promptFieldImage(gc.fieldImage)); if (result === false) gc.fieldImage = getDefaultBuiltInFieldImage().getSignatureAndOrigin(); @@ -444,17 +473,15 @@ export class MainApp { export type AppStores = typeof appStores; +const appPreferences = new Preferences(); +const assetManager = new AssetManager(); +const clipboard = new AppClipboard(); +const confirmation = new Confirmation(); +const ga = new GoogleAnalytics(); const ui = new UserInterface(); +const app = new MainApp(); // ALGO: The app must be created last -const appStores = { - app: new MainApp(), - assetManager: new AssetManager(), - confirmation: new Confirmation(), - ui, - appPreferences: new Preferences(), - ga: new GoogleAnalytics(), - clipboard: new AppClipboard() -} as const; +const appStores = { app, appPreferences, assetManager, clipboard, confirmation, ga, ui } as const; export function getAppStores(): AppStores { return appStores; diff --git a/src/core/Path.test.ts b/src/core/Path.test.ts old mode 100644 new mode 100755 diff --git a/src/core/Path.ts b/src/core/Path.ts old mode 100644 new mode 100755 diff --git a/src/core/Preferences.ts b/src/core/Preferences.ts old mode 100644 new mode 100755 diff --git a/src/core/ServiceWorkerMessages.ts b/src/core/ServiceWorkerMessages.ts old mode 100644 new mode 100755 diff --git a/src/core/ServiceWorkerRegistration.ts b/src/core/ServiceWorkerRegistration.ts old mode 100644 new mode 100755 diff --git a/src/core/SpeedEditor.ts b/src/core/SpeedEditor.ts old mode 100644 new mode 100755 diff --git a/src/core/TouchEventListener.ts b/src/core/TouchEventListener.ts old mode 100644 new mode 100755 diff --git a/src/core/Unit.ts b/src/core/Unit.ts old mode 100644 new mode 100755 index fd5d85f..4e792d2 --- a/src/core/Unit.ts +++ b/src/core/Unit.ts @@ -27,21 +27,46 @@ export enum UnitOfAngle { export type Unit = UnitOfLength | UnitOfAngle; +/** + * Quantity class represents a value with a unit + * @param value is a number, represents the value of the quantity + * @param unit is a Unit, can be UnitOfLength or UnitOfAngle + */ export class Quantity { constructor(public value: number, public unit: T) {} + /** + * Converts the quantity to a new unit + * @param unit, the new unit to convert the quantity value to + * @returns the converted value in the new unit + */ to(unit: T): number { return new UnitConverter(this.unit, unit).fromAtoB(this.value); } } +/** + * UnitConverter class converts a value from one unit to another + * @param alpha is a Unit, the unit of the value to be converted + * @param beta is a Unit, the unit to convert the value to + */ export class UnitConverter { constructor(public alpha: T, public beta: T) {} + /** + * Converts a value from unit alpha to unit beta + * @param a, the value to be converted in unit alpha + * @returns the converted value in unit beta + */ fromAtoB(a: number): number { return (a * this.alpha) / this.beta; } + /** + * Converts a value from unit beta to unit alpha + * @param b, the value to be converted in unit beta + * @returns the converted value in unit alpha + */ fromBtoA(b: number): number { return (b * this.beta) / this.alpha; } diff --git a/src/core/Util.test.ts b/src/core/Util.test.ts old mode 100644 new mode 100755 diff --git a/src/core/Util.ts b/src/core/Util.ts old mode 100644 new mode 100755 diff --git a/src/core/Versioning.tsx b/src/core/Versioning.tsx old mode 100644 new mode 100755 diff --git a/src/format/Config.test.ts b/src/format/Config.test.ts old mode 100644 new mode 100755 index ce71645..364f483 --- a/src/format/Config.test.ts +++ b/src/format/Config.test.ts @@ -1,7 +1,7 @@ import { makeAutoObservable } from "mobx"; import { Expose, Exclude, plainToClassFromExist, Type } from "class-transformer"; -import { IsBoolean, IsObject, IsPositive, ValidateNested, validate } from "class-validator"; +import { IsBoolean, IsIn, IsObject, IsPositive, ValidateNested, validate } from "class-validator"; import { BentRateApplicationDirection, Path } from "@core/Path"; import { UnitOfLength } from "@core/Unit"; import { GeneralConfig, PathConfig } from "./Config"; @@ -9,6 +9,7 @@ import { Format } from "./Format"; import { EditableNumberRange, ValidateEditableNumberRange, ValidateNumber } from "@core/Util"; import { CustomFormat } from "./Format.test"; import { FieldImageOriginType, FieldImageSignatureAndOrigin, getDefaultBuiltInFieldImage } from "@core/Asset"; +import { getNamedCoordinateSystems } from "@src/core/CoordinateSystem"; export class CustomGeneralConfig implements GeneralConfig { public custom: string = "custom"; @@ -40,6 +41,9 @@ export class CustomGeneralConfig implements GeneralConfig { @Expose() fieldImage: FieldImageSignatureAndOrigin = getDefaultBuiltInFieldImage().getSignatureAndOrigin(); + @IsIn(getNamedCoordinateSystems().map(s => s.name)) + @Expose() + coordinateSystem: string = "VEX Gaming Positioning System"; constructor() { makeAutoObservable(this); diff --git a/src/format/Config.tsx b/src/format/Config.tsx old mode 100644 new mode 100755 index 5d2e055..a6ab1f0 --- a/src/format/Config.tsx +++ b/src/format/Config.tsx @@ -74,6 +74,20 @@ export interface ConfigSection { get format(): Format; } +/** + * Common configuration params for all formats + * @param robotWidth Width of the robot in the unit of length + * @param robotHeight Height of the robot in the unit of length + * @param robotIsHolonomic Whether the robot is holonomic or not, or force the robot to be static + * Force Static - The robot's heading aligns with the first end control of the current segment where the robot is located + * Holonomic robot - The robot that can move in any direction without turning + * @param showRobot Whether to show the robot on the field + * @param uol Unit of length + * @param pointDensity The spacing between two waypoints on the path + * @param controlMagnetDistance The minimal distance for the dragging control to get magnetized to the Magnet Reference Line + * @param fieldImage The field image using for the format + * @param coordinateSystem The coordinate system used for the format + */ export interface GeneralConfig extends ConfigSection { robotWidth: number; robotHeight: number; @@ -83,9 +97,22 @@ export interface GeneralConfig extends ConfigSection { pointDensity: number; controlMagnetDistance: number; fieldImage: FieldImageSignatureAndOrigin; + coordinateSystem: string; + /** + * Get the react components as customized additional configuration UI for the format + * The customized additional configuration UI will be render at the end of GeneralConfigPanelBody + * @returns The customized additional configuration UI as react components + */ getAdditionalConfigUI(): React.ReactNode; } +/** Common Path Configuration params for all formats + * @param path The path to configure + * @param lookaheadLimit The lookahead limit of the path, used for determining the lookahead of each points + * @param speedLimit The configurable range of speed of the path + * @param bentRateApplicableRange The configurable range of bent rate of the path + * @param bentRateApplicationDirection The direction of the bent rate range on the speed canvas + */ export interface PathConfig extends ConfigSection { path: Path; lookaheadLimit?: NumberRange; diff --git a/src/format/Format.test.ts b/src/format/Format.test.ts old mode 100644 new mode 100755 diff --git a/src/format/Format.tsx b/src/format/Format.tsx old mode 100644 new mode 100755 index c4a443d..67570f5 --- a/src/format/Format.tsx +++ b/src/format/Format.tsx @@ -13,7 +13,6 @@ import { isExperimentalFeaturesEnabled } from "@core/Preferences"; import { RigidCodeGenFormatV0_1 } from "./RigidCodeGenFormatV0_1"; import { MoveToPointCodeGenFormatV0_1 } from "./MoveToPointCodeGenFormatV0_1"; import { xVecLibBoomerangFormatV0_1 } from "./xVecLibBoomerangFormatV0_1"; - export interface Format { isInit: boolean; uid: string; @@ -22,16 +21,48 @@ export interface Format { getDescription(): string; + /** + * Registers the format with the provided application and user interface. + * This method should set up any necessary event listeners, hooks, or UI components specific to the format. + * The `register` function should be the first function to call on the format. This is triggered when MainApp changes format. + * @param app The MainApp instance + * @param ui The UserInterface instances + */ register(app: MainApp, ui: UserInterface): void; + /** + * Unregisters the format from the application. + * This should clean up and remove any hooks, event listeners, or UI components that were added during the registration process. + * The `unregister` function should be the last function to call on the format. After it is called, the format is expected to be detached from the application. This is triggered when MainApp changes format. + */ unregister(): void; + /** + * Creates a new instance of this format + * @returns a new instance of this format + */ createNewInstance(): Format; + /** + * Gets the general configuration for this format + * This method provides access to the general configuration settings to this format. + * @returns the general configuration for this format + */ getGeneralConfig(): GeneralConfig; + /** + * Creates a path instance with the given segments + * @param segments the segments to create the path + * @returns the created path instance + */ createPath(...segments: Segment[]): Path; + /** + * Calculates the waypoints along a given path + * The points' speed may recalculated based on each format + * @param path the path to get the points + * @returns the calculation result points along the given path + */ getPathPoints(path: Path): PointCalculationResult; /** @@ -110,7 +141,7 @@ interface PathFileDataConverter { const convertFromV0_1_0ToV0_2_0: PathFileDataConverter = { version: new Range("~0.1"), convert: (data: Record): void => { - // Covert old enum number to new enum ratio + // Convert old enum number to new enum ratio data.gc.uol = { 1: UnitOfLength.Millimeter, 2: UnitOfLength.Centimeter, @@ -177,10 +208,32 @@ const convertFromV0_6_0ToV0_7_0: PathFileDataConverter = { } }; -const convertFromV0_7_0ToCurrentAppVersion: PathFileDataConverter = { +const convertFromV0_7_0ToV0_8_0: PathFileDataConverter = { version: new Range("~0.7"), convert: (data: Record): void => { - // From v0.7.0 to current app version + if (data.format === "LemLib v0.4.x (inch, byte-voltage)") { + data.format = "LemLib v0.5"; + } else if (data.format === "path.jerryio v0.1.x (cm, rpm)") { + data.format = "path.jerryio v0.1"; + } else if (data.format === "LemLib Odom Code Gen v0.4.x (inch)") { + data.format = "LemLib Odom Code Gen v0.4"; + } else if (data.format === "LemLib v1.0.0 (mm, m/s)") { + data.format = "LemLib v1.0"; + } else if (data.format === "Move-to-Point Code Gen v0.1.x") { + data.format = "Move-to-Point Code Gen v0.1"; + } else if (data.format === "Rigid Code Gen v0.1.x") { + data.format = "Rigid Code Gen v0.1"; + } + + // From v0.7.0 to v0.8.0 + data.appVersion = "0.8.0"; + } +}; + +const convertFromV0_8_0ToCurrentAppVersion: PathFileDataConverter = { + version: new Range("~0.8"), + convert: (data: Record): void => { + // From v0.8.0 to current app version data.appVersion = APP_VERSION.version; } }; @@ -193,7 +246,8 @@ export function convertPathFileData(data: Record): boolean { convertFromV0_4_0ToV0_5_0, convertFromV0_5_0ToV0_6_0, convertFromV0_6_0ToV0_7_0, - convertFromV0_7_0ToCurrentAppVersion + convertFromV0_7_0ToV0_8_0, + convertFromV0_8_0ToCurrentAppVersion ]) { if (version.test(data.appVersion)) { convert(data); diff --git a/src/format/LemLibFormatV0_4/GeneralConfig.tsx b/src/format/LemLibFormatV0_4/GeneralConfig.tsx old mode 100644 new mode 100755 index 6d600ea..6980d24 --- a/src/format/LemLibFormatV0_4/GeneralConfig.tsx +++ b/src/format/LemLibFormatV0_4/GeneralConfig.tsx @@ -3,9 +3,10 @@ import { FieldImageSignatureAndOrigin, FieldImageOriginType, getDefaultBuiltInFi import { UnitOfLength } from "@core/Unit"; import { ValidateNumber } from "@core/Util"; import { Expose, Type, Exclude } from "class-transformer"; -import { IsPositive, IsBoolean, ValidateNested, IsObject } from "class-validator"; +import { IsPositive, IsBoolean, ValidateNested, IsObject, IsIn } from "class-validator"; import { GeneralConfig, initGeneralConfig } from "../Config"; import { Format } from "../Format"; +import { getNamedCoordinateSystems } from "@src/core/CoordinateSystem"; // observable class export class GeneralConfigImpl implements GeneralConfig { @@ -36,7 +37,9 @@ export class GeneralConfigImpl implements GeneralConfig { @Expose() fieldImage: FieldImageSignatureAndOrigin = getDefaultBuiltInFieldImage().getSignatureAndOrigin(); - + @IsIn(getNamedCoordinateSystems().map(s => s.name)) + @Expose() + coordinateSystem: string = "VEX Gaming Positioning System"; @Exclude() private format_: Format; diff --git a/src/format/LemLibFormatV0_4/PathConfig.tsx b/src/format/LemLibFormatV0_4/PathConfig.tsx index a8c2b1d..7b09173 100644 --- a/src/format/LemLibFormatV0_4/PathConfig.tsx +++ b/src/format/LemLibFormatV0_4/PathConfig.tsx @@ -1,5 +1,5 @@ import { makeAutoObservable, action } from "mobx"; -import { Typography, Box, Slider } from "@mui/material"; +import { Typography, Slider } from "@mui/material"; import { RangeSlider } from "@src/app/component.blocks/RangeSlider"; import { UpdateProperties } from "@core/Command"; import { LayoutContext, LayoutType, PanelBuilderProps, PanelInstanceProps } from "@core/Layout"; @@ -12,6 +12,7 @@ import React from "react"; import { PathConfig } from "../Config"; import { Format } from "../Format"; import LinearScaleIcon from "@mui/icons-material/LinearScale"; +import { PanelBox } from "@src/app/component.blocks/PanelBox"; // observable class export class PathConfigImpl implements PathConfig { @@ -64,8 +65,8 @@ const PathConfigPanelBody = observer((props: {}) => { return ( <> - - Min/Max Speed + Min/Max Speed + @@ -75,9 +76,9 @@ const PathConfigPanelBody = observer((props: {}) => { ) } /> - - - Bent Rate Applicable Range + + Bent Rate Applicable Range + @@ -87,9 +88,9 @@ const PathConfigPanelBody = observer((props: {}) => { ) } /> - - - Max Deceleration Rate + + Max Deceleration Rate + { if (Array.isArray(value)) value = value[0]; app.history.execute( `Change path ${pc.path.uid} max deceleration rate`, - new UpdateProperties(this as any, { maxDecelerationRate: value }) + new UpdateProperties(pc, { maxDecelerationRate: value }) ); })} /> - + ); }); diff --git a/src/format/LemLibFormatV0_4/index.tsx b/src/format/LemLibFormatV0_4/index.tsx old mode 100644 new mode 100755 index 7b14f63..05530a1 --- a/src/format/LemLibFormatV0_4/index.tsx +++ b/src/format/LemLibFormatV0_4/index.tsx @@ -30,11 +30,11 @@ export class LemLibFormatV0_4 implements Format { } getName(): string { - return "LemLib v0.4.x (inch, byte-voltage)"; + return "LemLib v0.5"; } getDescription(): string { - return "Path file format for LemLib v0.4 (or higher)"; + return "Path file format for LemLib v0.4 or higher, using 0 to 127 as the speed unit."; } register(app: MainApp, ui: UserInterface): void { diff --git a/src/format/LemLibFormatV1_0/GeneralConfig.tsx b/src/format/LemLibFormatV1_0/GeneralConfig.tsx old mode 100644 new mode 100755 index 3f6caaa..b116d5e --- a/src/format/LemLibFormatV1_0/GeneralConfig.tsx +++ b/src/format/LemLibFormatV1_0/GeneralConfig.tsx @@ -3,9 +3,10 @@ import { FieldImageSignatureAndOrigin, FieldImageOriginType, getDefaultBuiltInFi import { UnitOfLength } from "@core/Unit"; import { ValidateNumber } from "@core/Util"; import { Expose, Type, Exclude } from "class-transformer"; -import { IsPositive, IsBoolean, ValidateNested, IsObject } from "class-validator"; +import { IsPositive, IsBoolean, ValidateNested, IsObject, IsIn } from "class-validator"; import { GeneralConfig, initGeneralConfig } from "../Config"; import { Format } from "../Format"; +import { getNamedCoordinateSystems } from "@src/core/CoordinateSystem"; // observable class export class GeneralConfigImpl implements GeneralConfig { @@ -36,7 +37,9 @@ export class GeneralConfigImpl implements GeneralConfig { @Expose() fieldImage: FieldImageSignatureAndOrigin = getDefaultBuiltInFieldImage().getSignatureAndOrigin(); - + @IsIn(getNamedCoordinateSystems().map(s => s.name)) + @Expose() + coordinateSystem: string = "VEX Gaming Positioning System"; @Exclude() private format_: Format; diff --git a/src/format/LemLibFormatV1_0/PathConfig.tsx b/src/format/LemLibFormatV1_0/PathConfig.tsx index d948423..fae5a72 100644 --- a/src/format/LemLibFormatV1_0/PathConfig.tsx +++ b/src/format/LemLibFormatV1_0/PathConfig.tsx @@ -1,5 +1,5 @@ import { makeAutoObservable, action } from "mobx"; -import { Typography, Box, Slider } from "@mui/material"; +import { Typography, Slider } from "@mui/material"; import { RangeSlider } from "@src/app/component.blocks/RangeSlider"; import { UpdateProperties } from "@core/Command"; import { LayoutContext, LayoutType, PanelBuilderProps, PanelInstanceProps } from "@core/Layout"; @@ -12,6 +12,7 @@ import React from "react"; import { PathConfig } from "../Config"; import { Format } from "../Format"; import LinearScaleIcon from "@mui/icons-material/LinearScale"; +import { PanelBox } from "@src/app/component.blocks/PanelBox"; export interface LemLibPathConfig extends PathConfig {} @@ -76,8 +77,8 @@ const PathConfigPanelBody = observer((props: {}) => { return ( <> - - Min/Max Speed + Min/Max Speed + @@ -87,9 +88,9 @@ const PathConfigPanelBody = observer((props: {}) => { ) } /> - - - Bent Rate Applicable Range + + Bent Rate Applicable Range + @@ -99,9 +100,9 @@ const PathConfigPanelBody = observer((props: {}) => { ) } /> - - - Max Deceleration Rate + + Max Deceleration Rate + { if (Array.isArray(value)) value = value[0]; app.history.execute( `Change path ${pc.path.uid} max deceleration rate`, - new UpdateProperties(this as any, { maxDecelerationRate: value }) + new UpdateProperties(pc, { maxDecelerationRate: value }) ); })} /> - + {/* TODO show button to show Lookahead Graph */} ); diff --git a/src/format/LemLibFormatV1_0/Serialization.ts b/src/format/LemLibFormatV1_0/Serialization.ts old mode 100644 new mode 100755 diff --git a/src/format/LemLibFormatV1_0/index.test.tsx b/src/format/LemLibFormatV1_0/index.test.tsx old mode 100644 new mode 100755 diff --git a/src/format/LemLibFormatV1_0/index.tsx b/src/format/LemLibFormatV1_0/index.tsx old mode 100644 new mode 100755 index dfbe7a3..05573d5 --- a/src/format/LemLibFormatV1_0/index.tsx +++ b/src/format/LemLibFormatV1_0/index.tsx @@ -32,11 +32,11 @@ export class LemLibFormatV1_0 implements Format { } getName(): string { - return "LemLib v1.0.0 (mm, m/s)"; + return "LemLib v1.0"; } getDescription(): string { - return "Path file for LemLib v1.0 (Experimental)"; + return "Path file for LemLib v1.0, using mm as the unit of length and m/s as the speed unit."; } register(app: MainApp, ui: UserInterface): void { diff --git a/src/format/LemLibOdomGeneratorFormatV0_4/GeneralConfig.tsx b/src/format/LemLibOdomGeneratorFormatV0_4/GeneralConfig.tsx old mode 100644 new mode 100755 index 184a634..fda9441 --- a/src/format/LemLibOdomGeneratorFormatV0_4/GeneralConfig.tsx +++ b/src/format/LemLibOdomGeneratorFormatV0_4/GeneralConfig.tsx @@ -1,8 +1,8 @@ import { makeAutoObservable, action } from "mobx"; -import { Box, Typography, Button } from "@mui/material"; +import { Typography, Button } from "@mui/material"; import { enqueueSuccessSnackbar, enqueueErrorSnackbar } from "@src/app/Notice"; -import { ObserverCheckbox } from "@src/app/component.blocks/ObserverCheckbox"; -import { ObserverInput } from "@src/app/component.blocks/ObserverInput"; +import { FormCheckbox } from "@src/app/component.blocks/FormCheckbox"; +import { FormInputField } from "@src/app/component.blocks/FormInputField"; import { FieldImageSignatureAndOrigin, FieldImageOriginType, getDefaultBuiltInFieldImage } from "@core/Asset"; import { UpdateProperties } from "@core/Command"; import { useCustomHotkeys, getEnableOnNonTextInputFieldsHotkeysOptions } from "@core/Hook"; @@ -12,10 +12,12 @@ import { UnitOfLength } from "@core/Unit"; import { IS_MAC_OS, getMacHotKeyString, ValidateNumber } from "@core/Util"; import { Int, CodePointBuffer } from "@src/token/Tokens"; import { Expose, Type, Exclude } from "class-transformer"; -import { IsPositive, IsBoolean, ValidateNested, IsObject, IsString, MinLength } from "class-validator"; +import { IsPositive, IsBoolean, ValidateNested, IsObject, IsString, MinLength, IsIn } from "class-validator"; import { observer } from "mobx-react-lite"; import { GeneralConfig, initGeneralConfig } from "../Config"; import { Format } from "../Format"; +import { PanelBox } from "@src/app/component.blocks/PanelBox"; +import { getNamedCoordinateSystems } from "@src/core/CoordinateSystem"; interface FormatWithExportCode extends Format { exportCode(): string; @@ -48,53 +50,51 @@ const GeneralConfigPanel = observer((props: { config: GeneralConfigImpl }) => { return ( <> - - Export Settings - - config.chassisName} - setValue={(value: string) => { - app.history.execute(`Change chassis variable name`, new UpdateProperties(config, { chassisName: value })); - }} - isValidIntermediate={() => true} - isValidValue={(candidate: string) => candidate !== ""} - sx={{ marginTop: "16px" }} - /> - config.movementTimeout.toString()} - setValue={(value: string) => { - const parsedValue = parseInt(Int.parse(new CodePointBuffer(value))!.value); - app.history.execute( - `Change default movement timeout to ${parsedValue}`, - new UpdateProperties(config, { movementTimeout: parsedValue }) - ); - }} - isValidIntermediate={() => true} - isValidValue={(candidate: string) => Int.parse(new CodePointBuffer(candidate)) !== null} - sx={{ marginTop: "16px" }} - numeric - /> - - - { - app.history.execute( - `Set using relative coordinates to ${value}`, - new UpdateProperties(config, { relativeCoords: value }) - ); - }} - /> - - - - - + Export Settings + + config.chassisName} + setValue={(value: string) => { + app.history.execute(`Change chassis variable name`, new UpdateProperties(config, { chassisName: value })); + }} + isValidIntermediate={() => true} + isValidValue={(candidate: string) => candidate !== ""} + sx={{ marginTop: "16px" }} + /> + config.movementTimeout.toString()} + setValue={(value: string) => { + const parsedValue = parseInt(Int.parse(new CodePointBuffer(value))!.value); + app.history.execute( + `Change default movement timeout to ${parsedValue}`, + new UpdateProperties(config, { movementTimeout: parsedValue }) + ); + }} + isValidIntermediate={() => true} + isValidValue={(candidate: string) => Int.parse(new CodePointBuffer(candidate)) !== null} + sx={{ marginTop: "16px" }} + numeric + /> + + + { + app.history.execute( + `Set using relative coordinates to ${value}`, + new UpdateProperties(config, { relativeCoords: value }) + ); + }} + /> + + + + ); }); @@ -138,6 +138,9 @@ export class GeneralConfigImpl implements GeneralConfig { @IsBoolean() @Expose() relativeCoords: boolean = true; + @IsIn(getNamedCoordinateSystems().map(s => s.name)) + @Expose() + coordinateSystem: string = "VEX Gaming Positioning System"; @Exclude() private format_: FormatWithExportCode; diff --git a/src/format/LemLibOdomGeneratorFormatV0_4/PathConfig.tsx b/src/format/LemLibOdomGeneratorFormatV0_4/PathConfig.tsx old mode 100644 new mode 100755 diff --git a/src/format/LemLibOdomGeneratorFormatV0_4/index.tsx b/src/format/LemLibOdomGeneratorFormatV0_4/index.tsx old mode 100644 new mode 100755 index f3ecb67..04165c5 --- a/src/format/LemLibOdomGeneratorFormatV0_4/index.tsx +++ b/src/format/LemLibOdomGeneratorFormatV0_4/index.tsx @@ -28,7 +28,7 @@ export class LemLibOdomGeneratorFormatV0_4 implements Format { } getName(): string { - return "LemLib Odom Code Gen v0.4.x (inch)"; + return "LemLib Odom Code Gen v0.4"; } getDescription(): string { diff --git a/src/format/MoveToPointCodeGenFormatV0_1/GeneralConfig.tsx b/src/format/MoveToPointCodeGenFormatV0_1/GeneralConfig.tsx old mode 100644 new mode 100755 index 93bcad4..dbe0583 --- a/src/format/MoveToPointCodeGenFormatV0_1/GeneralConfig.tsx +++ b/src/format/MoveToPointCodeGenFormatV0_1/GeneralConfig.tsx @@ -1,5 +1,5 @@ import { makeAutoObservable, action, observable, IObservableValue } from "mobx"; -import { Typography, TextField, Box, Button } from "@mui/material"; +import { Typography, TextField, Button } from "@mui/material"; import { enqueueSuccessSnackbar, enqueueErrorSnackbar } from "@src/app/Notice"; import { FieldImageSignatureAndOrigin, FieldImageOriginType, getDefaultBuiltInFieldImage } from "@core/Asset"; import { useCustomHotkeys, getEnableOnNonTextInputFieldsHotkeysOptions } from "@core/Hook"; @@ -8,10 +8,12 @@ import { Logger } from "@core/Logger"; import { UnitOfLength } from "@core/Unit"; import { IS_MAC_OS, getMacHotKeyString, ValidateNumber } from "@core/Util"; import { Expose, Exclude, Type } from "class-transformer"; -import { IsPositive, IsBoolean, ValidateNested, IsObject, IsString } from "class-validator"; +import { IsPositive, IsBoolean, ValidateNested, IsObject, IsString, IsIn } from "class-validator"; import { observer } from "mobx-react-lite"; import { GeneralConfig, initGeneralConfig } from "../Config"; import { Format } from "../Format"; +import { PanelBox } from "@src/app/component.blocks/PanelBox"; +import { getNamedCoordinateSystems } from "@src/core/CoordinateSystem"; interface FormatWithExportCode extends Format { exportCode(): string; @@ -83,16 +85,14 @@ const GeneralConfigPanel = observer((props: { config: GeneralConfigImpl }) => { return ( <> - - - - - - + + + + ); }); @@ -125,6 +125,9 @@ export class GeneralConfigImpl implements GeneralConfig { @Expose() fieldImage: FieldImageSignatureAndOrigin = getDefaultBuiltInFieldImage().getSignatureAndOrigin(); + @IsIn(getNamedCoordinateSystems().map(s => s.name)) + @Expose() + coordinateSystem: string = "VEX Gaming Positioning System"; @IsString() @Expose() outputTemplate: string = `path: \`// \${name} diff --git a/src/format/MoveToPointCodeGenFormatV0_1/PathConfig.tsx b/src/format/MoveToPointCodeGenFormatV0_1/PathConfig.tsx old mode 100644 new mode 100755 index c23ed8d..5388799 --- a/src/format/MoveToPointCodeGenFormatV0_1/PathConfig.tsx +++ b/src/format/MoveToPointCodeGenFormatV0_1/PathConfig.tsx @@ -1,6 +1,6 @@ import { makeAutoObservable } from "mobx"; -import { Typography, Box } from "@mui/material"; -import { ObserverInput } from "@src/app/component.blocks/ObserverInput"; +import { Typography } from "@mui/material"; +import { FormInputField } from "@src/app/component.blocks/FormInputField"; import { LayoutContext, LayoutType, PanelBuilderProps, PanelInstanceProps } from "@core/Layout"; import { getAppStores } from "@core/MainApp"; import { BentRateApplicationDirection, Path } from "@core/Path"; @@ -13,6 +13,7 @@ import React from "react"; import { PathConfig } from "../Config"; import { Format } from "../Format"; import LinearScaleIcon from "@mui/icons-material/LinearScale"; +import { PanelBox } from "@src/app/component.blocks/PanelBox"; // observable class export class PathConfigImpl implements PathConfig { @@ -62,8 +63,8 @@ const PathConfigPanelBody = observer((props: {}) => { return ( <> - - + pc.speed.toUser() + ""} @@ -74,7 +75,7 @@ const PathConfigPanelBody = observer((props: {}) => { isValidValue={(candidate: string) => NumberT.parse(new CodePointBuffer(candidate)) !== null} numeric /> - + ); }); diff --git a/src/format/MoveToPointCodeGenFormatV0_1/index.tsx b/src/format/MoveToPointCodeGenFormatV0_1/index.tsx old mode 100644 new mode 100755 index ade604e..d2fc7ba --- a/src/format/MoveToPointCodeGenFormatV0_1/index.tsx +++ b/src/format/MoveToPointCodeGenFormatV0_1/index.tsx @@ -31,7 +31,7 @@ export class MoveToPointCodeGenFormatV0_1 implements Format { } getName(): string { - return "Move-to-Point Code Gen v0.1.x"; + return "Move-to-Point Code Gen v0.1"; } getDescription(): string { diff --git a/src/format/PathDotJerryioFormatV0_1/GeneralConfig.tsx b/src/format/PathDotJerryioFormatV0_1/GeneralConfig.tsx old mode 100644 new mode 100755 index 2e81c2b..74e6511 --- a/src/format/PathDotJerryioFormatV0_1/GeneralConfig.tsx +++ b/src/format/PathDotJerryioFormatV0_1/GeneralConfig.tsx @@ -3,9 +3,10 @@ import { FieldImageSignatureAndOrigin, FieldImageOriginType, getDefaultBuiltInFi import { UnitOfLength } from "@core/Unit"; import { ValidateNumber } from "@core/Util"; import { Expose, Type, Exclude } from "class-transformer"; -import { IsPositive, IsBoolean, ValidateNested, IsObject } from "class-validator"; +import { IsPositive, IsBoolean, ValidateNested, IsObject, IsIn } from "class-validator"; import { GeneralConfig, initGeneralConfig } from "../Config"; import { Format } from "../Format"; +import { getNamedCoordinateSystems } from "@src/core/CoordinateSystem"; // observable class export class GeneralConfigImpl implements GeneralConfig { @@ -36,7 +37,9 @@ export class GeneralConfigImpl implements GeneralConfig { @Expose() fieldImage: FieldImageSignatureAndOrigin = getDefaultBuiltInFieldImage().getSignatureAndOrigin(); - + @IsIn(getNamedCoordinateSystems().map(s => s.name)) + @Expose() + coordinateSystem: string = "VEX Gaming Positioning System"; @Exclude() private format_: Format; diff --git a/src/format/PathDotJerryioFormatV0_1/PathConfig.tsx b/src/format/PathDotJerryioFormatV0_1/PathConfig.tsx old mode 100644 new mode 100755 index f59b9fd..ba6a265 --- a/src/format/PathDotJerryioFormatV0_1/PathConfig.tsx +++ b/src/format/PathDotJerryioFormatV0_1/PathConfig.tsx @@ -1,5 +1,5 @@ import { makeAutoObservable } from "mobx"; -import { Typography, Box } from "@mui/material"; +import { Typography } from "@mui/material"; import { RangeSlider } from "@src/app/component.blocks/RangeSlider"; import { UpdateProperties } from "@core/Command"; import { LayoutContext, LayoutType, PanelBuilderProps, PanelInstanceProps } from "@core/Layout"; @@ -12,6 +12,7 @@ import React from "react"; import { PathConfig } from "../Config"; import { Format } from "../Format"; import LinearScaleIcon from "@mui/icons-material/LinearScale"; +import { PanelBox } from "@src/app/component.blocks/PanelBox"; // observable class export class PathConfigImpl implements PathConfig { @@ -60,8 +61,8 @@ const PathConfigPanelBody = observer((props: {}) => { return ( <> - - Min/Max Speed + Min/Max Speed + @@ -71,9 +72,9 @@ const PathConfigPanelBody = observer((props: {}) => { ) } /> - - - Bent Rate Applicable Range + + Bent Rate Applicable Range + @@ -83,7 +84,7 @@ const PathConfigPanelBody = observer((props: {}) => { ) } /> - + ); }); diff --git a/src/format/PathDotJerryioFormatV0_1/index.tsx b/src/format/PathDotJerryioFormatV0_1/index.tsx old mode 100644 new mode 100755 index c9b2092..7d99dcd --- a/src/format/PathDotJerryioFormatV0_1/index.tsx +++ b/src/format/PathDotJerryioFormatV0_1/index.tsx @@ -30,11 +30,11 @@ export class PathDotJerryioFormatV0_1 implements Format { } getName(): string { - return "path.jerryio v0.1.x (cm, rpm)"; + return "path.jerryio v0.1"; } getDescription(): string { - return "The default and official format for path planning purposes and custom library using algorithms like pure pursuit."; + return "The default and official format for path planning purposes and custom library. Output is in cm, rpm."; } register(app: MainApp, ui: UserInterface): void { diff --git a/src/format/RigidCodeGenFormatV0_1/GeneralConfig.tsx b/src/format/RigidCodeGenFormatV0_1/GeneralConfig.tsx old mode 100644 new mode 100755 index 070522a..ca3be60 --- a/src/format/RigidCodeGenFormatV0_1/GeneralConfig.tsx +++ b/src/format/RigidCodeGenFormatV0_1/GeneralConfig.tsx @@ -1,7 +1,7 @@ import { makeAutoObservable, action, observable, IObservableValue } from "mobx"; -import { Box, Typography, Button, TextField } from "@mui/material"; +import { Typography, Button, TextField } from "@mui/material"; import { enqueueSuccessSnackbar, enqueueErrorSnackbar } from "@src/app/Notice"; -import { ObserverEnumSelect } from "@src/app/component.blocks/ObserverEnumSelect"; +import { FormEnumSelect } from "@src/app/component.blocks/FormEnumSelect"; import { FieldImageSignatureAndOrigin, FieldImageOriginType, getDefaultBuiltInFieldImage } from "@core/Asset"; import { UpdateProperties } from "@core/Command"; import { useCustomHotkeys, getEnableOnNonTextInputFieldsHotkeysOptions } from "@core/Hook"; @@ -10,10 +10,12 @@ import { Logger } from "@core/Logger"; import { UnitOfLength } from "@core/Unit"; import { IS_MAC_OS, getMacHotKeyString, ValidateNumber } from "@core/Util"; import { Expose, Exclude, Type } from "class-transformer"; -import { IsPositive, IsBoolean, ValidateNested, IsObject, IsEnum, IsString } from "class-validator"; +import { IsPositive, IsBoolean, ValidateNested, IsObject, IsEnum, IsString, IsIn } from "class-validator"; import { observer } from "mobx-react-lite"; import { GeneralConfig, initGeneralConfig } from "../Config"; import { Format } from "../Format"; +import { PanelBox } from "@src/app/component.blocks/PanelBox"; +import { getNamedCoordinateSystems } from "@src/core/CoordinateSystem"; interface FormatWithExportCode extends Format { exportCode(): string; @@ -91,31 +93,29 @@ const GeneralConfigPanel = observer((props: { config: GeneralConfigImpl }) => { return ( <> - - Export Settings - - { - app.history.execute( - `Set using heading output type to ${value}`, - new UpdateProperties(config, { headingOutputType: value }) - ); - }} - enumType={HeadingOutputType} - /> - - - - - - + Export Settings + + { + app.history.execute( + `Set using heading output type to ${value}`, + new UpdateProperties(config, { headingOutputType: value }) + ); + }} + enumType={HeadingOutputType} + /> + + + + + ); }); @@ -148,6 +148,9 @@ export class GeneralConfigImpl implements GeneralConfig { @Expose() fieldImage: FieldImageSignatureAndOrigin = getDefaultBuiltInFieldImage().getSignatureAndOrigin(); + @IsIn(getNamedCoordinateSystems().map(s => s.name)) + @Expose() + coordinateSystem: string = "VEX Gaming Positioning System"; @IsEnum(HeadingOutputType) @Expose() headingOutputType: HeadingOutputType = HeadingOutputType.Absolute; diff --git a/src/format/RigidCodeGenFormatV0_1/PathConfig.tsx b/src/format/RigidCodeGenFormatV0_1/PathConfig.tsx old mode 100644 new mode 100755 index bead0a3..55ce473 --- a/src/format/RigidCodeGenFormatV0_1/PathConfig.tsx +++ b/src/format/RigidCodeGenFormatV0_1/PathConfig.tsx @@ -1,6 +1,6 @@ import { makeAutoObservable } from "mobx"; -import { Box, Typography } from "@mui/material"; -import { ObserverInput } from "@src/app/component.blocks/ObserverInput"; +import { Typography } from "@mui/material"; +import { FormInputField } from "@src/app/component.blocks/FormInputField"; import { BentRateApplicationDirection, Path } from "@core/Path"; import { EditableNumberRange } from "@core/Util"; import { NumberT, CodePointBuffer } from "@src/token/Tokens"; @@ -13,6 +13,7 @@ import { getAppStores } from "@core/MainApp"; import { observer } from "mobx-react-lite"; import React from "react"; import LinearScaleIcon from "@mui/icons-material/LinearScale"; +import { PanelBox } from "@src/app/component.blocks/PanelBox"; // observable class export class PathConfigImpl implements PathConfig { @@ -62,8 +63,8 @@ const PathConfigPanelBody = observer((props: {}) => { return ( <> - - + pc.speed.toUser() + ""} @@ -74,7 +75,7 @@ const PathConfigPanelBody = observer((props: {}) => { isValidValue={(candidate: string) => NumberT.parse(new CodePointBuffer(candidate)) !== null} numeric /> - + ); }); diff --git a/src/format/RigidCodeGenFormatV0_1/index.tsx b/src/format/RigidCodeGenFormatV0_1/index.tsx old mode 100644 new mode 100755 index 7c5c22e..1704bd2 --- a/src/format/RigidCodeGenFormatV0_1/index.tsx +++ b/src/format/RigidCodeGenFormatV0_1/index.tsx @@ -39,7 +39,7 @@ export class RigidCodeGenFormatV0_1 implements Format { } getName(): string { - return "Rigid Code Gen v0.1.x"; + return "Rigid Code Gen v0.1"; } getDescription(): string { diff --git a/src/format/xVecLibBoomerangFormatV0_1/GeneralConfig.tsx b/src/format/xVecLibBoomerangFormatV0_1/GeneralConfig.tsx old mode 100644 new mode 100755 index 5aab5a3..def1c55 --- a/src/format/xVecLibBoomerangFormatV0_1/GeneralConfig.tsx +++ b/src/format/xVecLibBoomerangFormatV0_1/GeneralConfig.tsx @@ -1,7 +1,6 @@ import { makeAutoObservable, action } from "mobx"; -import { Box, Typography, Button } from "@mui/material"; +import { Typography, Button } from "@mui/material"; import { enqueueSuccessSnackbar, enqueueErrorSnackbar } from "@src/app/Notice"; -import { ObserverInput } from "@src/app/component.blocks/ObserverInput"; import { FieldImageSignatureAndOrigin, FieldImageOriginType, getDefaultBuiltInFieldImage } from "@core/Asset"; import { UpdateProperties } from "@core/Command"; import { useCustomHotkeys, getEnableOnNonTextInputFieldsHotkeysOptions } from "@core/Hook"; @@ -11,11 +10,13 @@ import { UnitOfLength } from "@core/Unit"; import { IS_MAC_OS, getMacHotKeyString, ValidateNumber } from "@core/Util"; import { Int, CodePointBuffer } from "@src/token/Tokens"; import { Expose, Type, Exclude } from "class-transformer"; -import { IsPositive, IsBoolean, ValidateNested, IsObject, IsString, MinLength } from "class-validator"; +import { IsPositive, IsBoolean, ValidateNested, IsObject, IsString, MinLength, IsIn } from "class-validator"; import { observer } from "mobx-react-lite"; import { GeneralConfig, initGeneralConfig } from "../Config"; import { Format } from "../Format"; -import { ObserverCheckbox } from "@src/app/component.blocks/ObserverCheckbox"; +import { PanelBox } from "@src/app/component.blocks/PanelBox"; +import { FormInputField } from "@src/app/component.blocks/FormInputField"; +import { getNamedCoordinateSystems } from "@src/core/CoordinateSystem"; interface FormatWithExportCode extends Format { exportCode(): string; } @@ -47,47 +48,43 @@ const GeneralConfigPanel = observer((props: { config: GeneralConfigImpl }) => { return ( <> - - Export Settings - - config.chassisName} - setValue={(value: string) => { - app.history.execute(`Change chassis variable name`, new UpdateProperties(config, { chassisName: value })); - }} - isValidIntermediate={() => true} - isValidValue={(candidate: string) => candidate !== ""} - sx={{ marginTop: "16px" }} - /> - config.movementTimeout.toString()} - setValue={(value: string) => { - const parsedValue = parseInt(Int.parse(new CodePointBuffer(value))!.value); - app.history.execute( - `Change default movement timeout to ${parsedValue}`, - new UpdateProperties(config, { movementTimeout: parsedValue }) - ); - }} - isValidIntermediate={() => true} - isValidValue={(candidate: string) => Int.parse(new CodePointBuffer(candidate)) !== null} - sx={{ marginTop: "16px" }} - numeric - /> - - - - - - + Export Settings + + config.chassisName} + setValue={(value: string) => { + app.history.execute(`Change chassis variable name`, new UpdateProperties(config, { chassisName: value })); + }} + isValidIntermediate={() => true} + isValidValue={(candidate: string) => candidate !== ""} + sx={{ marginTop: "16px" }} + /> + config.movementTimeout.toString()} + setValue={(value: string) => { + const parsedValue = parseInt(Int.parse(new CodePointBuffer(value))!.value); + app.history.execute( + `Change default movement timeout to ${parsedValue}`, + new UpdateProperties(config, { movementTimeout: parsedValue }) + ); + }} + isValidIntermediate={() => true} + isValidValue={(candidate: string) => Int.parse(new CodePointBuffer(candidate)) !== null} + sx={{ marginTop: "16px" }} + numeric + /> + + + + + ); }); - -// observable class export class GeneralConfigImpl implements GeneralConfig { @IsPositive() @Expose() @@ -96,11 +93,11 @@ export class GeneralConfigImpl implements GeneralConfig { @Expose() robotHeight: number = 12; @IsBoolean() - @Exclude() + @Expose() robotIsHolonomic: boolean = false; @IsBoolean() @Expose() - showRobot: boolean = true; + showRobot: boolean = false; @ValidateNumber(num => num > 0 && num <= 1000) // Don't use IsEnum @Expose() uol: UnitOfLength = UnitOfLength.Inch; @@ -126,6 +123,9 @@ export class GeneralConfigImpl implements GeneralConfig { @IsBoolean() @Expose() relativeCoords: boolean = true; + @IsIn(getNamedCoordinateSystems().map(s => s.name)) + @Expose() + coordinateSystem: string = "VEX Gaming Positioning System"; @Exclude() private format_: FormatWithExportCode; diff --git a/src/format/xVecLibBoomerangFormatV0_1/PathConfig.tsx b/src/format/xVecLibBoomerangFormatV0_1/PathConfig.tsx old mode 100644 new mode 100755 index 8e18012..49a2f2f --- a/src/format/xVecLibBoomerangFormatV0_1/PathConfig.tsx +++ b/src/format/xVecLibBoomerangFormatV0_1/PathConfig.tsx @@ -1,8 +1,7 @@ import { makeAutoObservable } from "mobx"; -import { Box, Typography } from "@mui/material"; +import { Typography } from "@mui/material"; import { BentRateApplicationDirection, Path } from "@core/Path"; import { EditableNumberRange } from "@core/Util"; -import { NumberT, CodePointBuffer } from "@src/token/Tokens"; import { Exclude, Expose } from "class-transformer"; import { IsNumber } from "class-validator"; import { PathConfig } from "../Config"; diff --git a/src/format/xVecLibBoomerangFormatV0_1/index.test.tsx b/src/format/xVecLibBoomerangFormatV0_1/index.test.tsx old mode 100644 new mode 100755 index 447b72c..a05bba7 --- a/src/format/xVecLibBoomerangFormatV0_1/index.test.tsx +++ b/src/format/xVecLibBoomerangFormatV0_1/index.test.tsx @@ -2,23 +2,39 @@ import { xVecLibBoomerangFormatV0_1 } from "."; import { MainApp, getAppStores } from "@core/MainApp"; import { Control, EndControl, Segment } from "@core/Path"; import { GeneralConfigImpl } from "./GeneralConfig"; +import { SmartBuffer } from "smart-buffer"; +import { LemLibV1_0 } from "../LemLibFormatV1_0/Serialization"; test("dummy", () => { const { app } = getAppStores(); // suppress constructor error }); -test("read write path", () => { +test("read write path file", () => { const format = new xVecLibBoomerangFormatV0_1(); const path = format.createPath(); - path.segments.push( - new Segment(new EndControl(0, 0, 0.1), new Control(70, 70), new Control(80, 80), new EndControl(62, 60, 90)) - ); - /* - chassis.setPos(0, 0,0.1); -chassis.printCoords(); -chassis.moveToBoom( 24, 24, 133.6, 0.7095089932546322,5000); -chassis.moveToBoom( 24, -24, -116.82300000000001, 0.7511567373713952,5000); -chassis.moveToBoom( -24, -24, -17.854025751516758, -0.9970639976227197,5000); -chassis.moveToBoom( -24, 24, 20.008747952175142, 0.8558511879438432,5000); - - -*/ + + const buffer1 = SmartBuffer.fromSize(1024); // auto resize + path.segments.push(new Segment(new EndControl(1, 1, 0), new EndControl(60, 60, 90))); + path.segments.push(new Segment(path.segments[path.segments.length - 1].last, new EndControl(63, 60, 180))); + path.segments.push(new Segment(path.segments[path.segments.length - 1].last, new EndControl(64, 60, 270))); + + LemLibV1_0.writePath(buffer1, path); + + const result = LemLibV1_0.readPath(buffer1); + + expect(result.name).toBe(path.name); + + const points = path.cachedResult.points; + + expect(result.waypoints.length).toBe(points.length); + + for (let i = 0; i < points.length; i++) { + const point1 = points[i]; + const point2 = result.waypoints[i]; + + expect(point2.x).toBeCloseTo(point1.x, 0.1); + expect(point2.y).toBeCloseTo(point1.y, 0.1); + expect(point2.heading ?? 0).toBeCloseTo(point1.heading ?? 0); + } + + // let code = format.exportCode(); + // console.log(code); }); diff --git a/src/format/xVecLibBoomerangFormatV0_1/index.tsx b/src/format/xVecLibBoomerangFormatV0_1/index.tsx old mode 100644 new mode 100755 index c3c66fb..7eb0720 --- a/src/format/xVecLibBoomerangFormatV0_1/index.tsx +++ b/src/format/xVecLibBoomerangFormatV0_1/index.tsx @@ -8,13 +8,14 @@ import { Format, importPDJDataFromTextFile } from "../Format"; import { PointCalculationResult, getPathPoints } from "@core/Calculation"; import { GeneralConfigImpl } from "./GeneralConfig"; import { PathConfigImpl } from "./PathConfig"; -import { SmartBuffer } from "smart-buffer"; // observable class export class xVecLibBoomerangFormatV0_1 implements Format { isInit: boolean = false; uid: string; - protected gc = new GeneralConfigImpl(this); + private gc = new GeneralConfigImpl(this); + + private readonly disposers: (() => void)[] = []; constructor() { this.uid = makeId(10); @@ -34,9 +35,12 @@ export class xVecLibBoomerangFormatV0_1 implements Format { register(app: MainApp): void { if (this.isInit) return; this.isInit = true; + this.disposers.push(); } - unregister(): void {} + unregister(): void { + this.disposers.forEach(disposer => disposer()); + } getGeneralConfig(): GeneralConfig { return this.gc; @@ -83,14 +87,32 @@ export class xVecLibBoomerangFormatV0_1 implements Format { for (const seg of segments) { let lead = 0; let last = path.controls.at(path.controls.length - 1); + let first = path.controls.at(0); let dis; - if (last != null) { + if (last != null && first != null) { dis = path.controls.at(0)?.distance(last); if (seg.isCubic() && dis != null) { let closestContol = seg.controls[1].distance(seg.first) > seg.controls[2].distance(seg.last) ? seg.controls[1] : seg.controls[2]; + if (seg.first === path.controls.at(0)) { + seg.first.heading = + 180 + Math.atan2(seg.first.x - closestContol.x, seg.first.y - closestContol.y) * (180 / Math.PI); + } + if ( + seg.controls[1] === closestContol && + (seg.first.heading === 0 || seg.first.heading === 180 || seg.last.heading === 0 || seg.last.heading === 0) + ) { + seg.controls[2].setXY(seg.last.x, seg.last.y); + } else if ( + seg.first.heading === 0 || + seg.first.heading === 180 || + seg.last.heading === 0 || + seg.last.heading === 0 + ) { + seg.controls[1].setXY(seg.first.x, seg.first.y); + } if (seg.first.heading === 0 || seg.first.heading === 180) { seg.first.heading = Math.atan2(seg.first.x - closestContol.x, seg.first.y - closestContol.y) * (180 / Math.PI); @@ -117,8 +139,6 @@ export class xVecLibBoomerangFormatV0_1 implements Format { } } else { lead = (seg.last.x - closestContol.x) / (dis * Math.sin(seg.last.heading)); - console.log("lead", lead); - console.log("seg.last.heading", seg.last.heading); while (Math.abs(lead) > 1) { numIterations++; if (lead > 1) { diff --git a/src/index.tsx b/src/index.tsx old mode 100644 new mode 100755 diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts old mode 100644 new mode 100755 diff --git a/src/service-worker.ts b/src/service-worker.ts old mode 100644 new mode 100755 diff --git a/src/setupTests.ts b/src/setupTests.ts old mode 100644 new mode 100755 diff --git a/src/token/Tokens.test.ts b/src/token/Tokens.test.ts old mode 100644 new mode 100755 diff --git a/src/token/Tokens.ts b/src/token/Tokens.ts old mode 100644 new mode 100755 diff --git a/tsconfig.json b/tsconfig.json old mode 100644 new mode 100755 diff --git a/tsconfig.paths.json b/tsconfig.paths.json old mode 100644 new mode 100755 From a81628248f63945ad7a9c60d20ee075740aa40b3 Mon Sep 17 00:00:00 2001 From: Cavaire Date: Tue, 22 Oct 2024 19:15:05 -0500 Subject: [PATCH 13/16] :lipstick:Added more configurations for lead --- .../GeneralConfig.tsx | 39 ++++++++++++++++++- .../xVecLibBoomerangFormatV0_1/index.tsx | 11 ++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/format/xVecLibBoomerangFormatV0_1/GeneralConfig.tsx b/src/format/xVecLibBoomerangFormatV0_1/GeneralConfig.tsx index def1c55..96a9eb6 100755 --- a/src/format/xVecLibBoomerangFormatV0_1/GeneralConfig.tsx +++ b/src/format/xVecLibBoomerangFormatV0_1/GeneralConfig.tsx @@ -17,11 +17,12 @@ import { Format } from "../Format"; import { PanelBox } from "@src/app/component.blocks/PanelBox"; import { FormInputField } from "@src/app/component.blocks/FormInputField"; import { getNamedCoordinateSystems } from "@src/core/CoordinateSystem"; +import { FormCheckbox } from "@src/app/component.blocks/FormCheckbox"; interface FormatWithExportCode extends Format { exportCode(): string; } -const logger = Logger("xVecLib Boomerang v0.1.0 (inch)"); +const logger = Logger("xVecLib Boomerang v1.0.0 (inch)"); const GeneralConfigPanel = observer((props: { config: GeneralConfigImpl }) => { const { config } = props; @@ -75,13 +76,42 @@ const GeneralConfigPanel = observer((props: { config: GeneralConfigImpl }) => { sx={{ marginTop: "16px" }} numeric /> + - + + Lead Settings + + config.maxIterations.toString()} + setValue={(value: string) => { + const parsedValue = parseInt(Int.parse(new CodePointBuffer(value))!.value); + app.history.execute( + `Change max iterations for lead to ${parsedValue}`, + new UpdateProperties(config, { maxIterations: parsedValue }) + ); + }} + isValidIntermediate={() => true} + isValidValue={(candidate: string) => Int.parse(new CodePointBuffer(candidate)) !== null} + sx={{ marginTop: "16px" }} + numeric + /> + { + app.history.execute( + `Using real(bad) lead's is ${value}`, + new UpdateProperties(config, { badLead: value }) + ); + }} + /> + ); }); @@ -120,6 +150,11 @@ export class GeneralConfigImpl implements GeneralConfig { @ValidateNumber(num => num >= 0) @Expose() movementTimeout: number = 5000; + @Expose() + maxIterations: number = 200; + @IsBoolean() + @Expose() + badLead: boolean = false; @IsBoolean() @Expose() relativeCoords: boolean = true; diff --git a/src/format/xVecLibBoomerangFormatV0_1/index.tsx b/src/format/xVecLibBoomerangFormatV0_1/index.tsx index 7eb0720..80732a5 100755 --- a/src/format/xVecLibBoomerangFormatV0_1/index.tsx +++ b/src/format/xVecLibBoomerangFormatV0_1/index.tsx @@ -27,7 +27,7 @@ export class xVecLibBoomerangFormatV0_1 implements Format { } getName(): string { - return "xVecLib Boomerang v0.1.0 (inch)"; + return "xVecLib Boomerang v1.0.0 (inch)"; } getDescription(): string { return "Generates a sequence of xVecLib .moveToBoom function calls."; @@ -133,7 +133,7 @@ export class xVecLibBoomerangFormatV0_1 implements Format { seg.first.heading -= 0.5; } lead = (seg.first.x - closestContol.x) / (dis * Math.sin(seg.first.heading)); - if (numIterations > 200) { + if (numIterations > gc.maxIterations) { break; } } @@ -147,11 +147,16 @@ export class xVecLibBoomerangFormatV0_1 implements Format { seg.last.heading -= 0.5; } lead = (seg.last.x - closestContol.x) / (dis * Math.sin(seg.last.heading)); - if (numIterations > 200) { + if (numIterations > gc.maxIterations) { break; } } } + if (lead > 1 && !gc.badLead) { + lead = 1; + } else if (lead < -1 && !gc.badLead) { + lead = -1; + } } } let tmpp = seg.last.heading > 180 ? seg.last.heading - 360 : seg.last.heading; From 38cec8b7afc3b8dfb0f9f504943a6a3f7de4341a Mon Sep 17 00:00:00 2001 From: Cavaire Date: Wed, 4 Dec 2024 16:38:06 -0600 Subject: [PATCH 14/16] :children_crossing: Added a link to the new documentation --- .../xVecLibBoomerangFormatV0_1/GeneralConfig.tsx | 12 +++++------- src/format/xVecLibBoomerangFormatV0_1/index.tsx | 4 +++- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/format/xVecLibBoomerangFormatV0_1/GeneralConfig.tsx b/src/format/xVecLibBoomerangFormatV0_1/GeneralConfig.tsx index 96a9eb6..0d511ec 100755 --- a/src/format/xVecLibBoomerangFormatV0_1/GeneralConfig.tsx +++ b/src/format/xVecLibBoomerangFormatV0_1/GeneralConfig.tsx @@ -76,17 +76,18 @@ const GeneralConfigPanel = observer((props: { config: GeneralConfigImpl }) => { sx={{ marginTop: "16px" }} numeric /> - - + +

Documentation

+
Lead Settings - config.maxIterations.toString()} setValue={(value: string) => { @@ -105,10 +106,7 @@ const GeneralConfigPanel = observer((props: { config: GeneralConfigImpl }) => { label="Use broken lead" checked={config.badLead} onCheckedChange={value => { - app.history.execute( - `Using real(bad) lead's is ${value}`, - new UpdateProperties(config, { badLead: value }) - ); + app.history.execute(`Using real(bad) lead's is ${value}`, new UpdateProperties(config, { badLead: value })); }} /> diff --git a/src/format/xVecLibBoomerangFormatV0_1/index.tsx b/src/format/xVecLibBoomerangFormatV0_1/index.tsx index 80732a5..b9dcd2e 100755 --- a/src/format/xVecLibBoomerangFormatV0_1/index.tsx +++ b/src/format/xVecLibBoomerangFormatV0_1/index.tsx @@ -29,9 +29,11 @@ export class xVecLibBoomerangFormatV0_1 implements Format { getName(): string { return "xVecLib Boomerang v1.0.0 (inch)"; } - getDescription(): string { + + getDescription(): string { return "Generates a sequence of xVecLib .moveToBoom function calls."; } + register(app: MainApp): void { if (this.isInit) return; this.isInit = true; From 5e1122ac5ffa665d3636e0e368f1b6d99e6b07bd Mon Sep 17 00:00:00 2001 From: Logieboyyyyyyyyy <156133855+Logieboyyyyyyyyy@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:31:44 -0800 Subject: [PATCH 15/16] Fix type error in exportFile return type. --- src/format/xVecLibBoomerangFormatV0_1/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/format/xVecLibBoomerangFormatV0_1/index.tsx b/src/format/xVecLibBoomerangFormatV0_1/index.tsx index b9dcd2e..0c1694e 100755 --- a/src/format/xVecLibBoomerangFormatV0_1/index.tsx +++ b/src/format/xVecLibBoomerangFormatV0_1/index.tsx @@ -176,7 +176,7 @@ export class xVecLibBoomerangFormatV0_1 implements Format { return importPDJDataFromTextFile(buffer); } - exportFile(): ArrayBuffer { + exportFile(): ArrayBufferView { const { app } = getAppStores(); let fileContent = this.exportCode(); From 9c954fa3c72be47d081b0aafcd06930b50c4ec4f Mon Sep 17 00:00:00 2001 From: Logieboyyyyyyyyy <156133855+Logieboyyyyyyyyy@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:32:46 -0800 Subject: [PATCH 16/16] Revert accidental file permission changes --- .gitignore | 0 .prettierignore | 0 CODE_OF_CONDUCT.md | 0 CONTRIBUTING.md | 0 LICENSE | 0 5 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 .gitignore mode change 100755 => 100644 .prettierignore mode change 100755 => 100644 CODE_OF_CONDUCT.md mode change 100755 => 100644 CONTRIBUTING.md mode change 100755 => 100644 LICENSE diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 diff --git a/.prettierignore b/.prettierignore old mode 100755 new mode 100644 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md old mode 100755 new mode 100644 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md old mode 100755 new mode 100644 diff --git a/LICENSE b/LICENSE old mode 100755 new mode 100644