From 3cc003d961def33b209ab685aa2eb6b94ec9bb4a Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Mon, 10 Jun 2024 03:57:29 -0600 Subject: [PATCH 001/121] Starting WS connection --- fission/package-lock.json | 10 ++++ fission/package.json | 1 + fission/src/components/MainHUD.tsx | 6 +++ .../systems/simulation/SimulationSystem.ts | 3 ++ .../simulation/wpilib_brain/WPILibBrain.ts | 16 +++++++ .../wpilib_brain/WPILibConnector.ts | 48 +++++++++++++++++++ 6 files changed, 84 insertions(+) create mode 100644 fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts create mode 100644 fission/src/systems/simulation/wpilib_brain/WPILibConnector.ts diff --git a/fission/package-lock.json b/fission/package-lock.json index a287e56772..adc9a867c7 100644 --- a/fission/package-lock.json +++ b/fission/package-lock.json @@ -11,6 +11,7 @@ "@barclah/jolt-physics": "^0.19.3", "@react-three/drei": "^9.96.5", "@react-three/fiber": "^8.15.15", + "async-mutex": "^0.5.0", "colord": "^2.9.3", "framer-motion": "^10.13.1", "react": "^18.2.0", @@ -2959,6 +2960,15 @@ "node": "*" } }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/autoprefixer": { "version": "10.4.19", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", diff --git a/fission/package.json b/fission/package.json index 76547f1c81..73a57cb2aa 100644 --- a/fission/package.json +++ b/fission/package.json @@ -19,6 +19,7 @@ "@barclah/jolt-physics": "^0.19.3", "@react-three/drei": "^9.96.5", "@react-three/fiber": "^8.15.15", + "async-mutex": "^0.5.0", "colord": "^2.9.3", "framer-motion": "^10.13.1", "react": "^18.2.0", diff --git a/fission/src/components/MainHUD.tsx b/fission/src/components/MainHUD.tsx index 48e20d215b..551c808834 100644 --- a/fission/src/components/MainHUD.tsx +++ b/fission/src/components/MainHUD.tsx @@ -12,6 +12,7 @@ import { motion } from "framer-motion" import logo from "../assets/autodesk_logo.png" import { ToastType, useToastContext } from "../ToastContext" import { Random } from "@/util/Random" +import WPILibConnector from "@/systems/simulation/wpilib_brain/WPILibConnector" type ButtonProps = { value: string @@ -150,6 +151,11 @@ const MainHUD: React.FC = () => { icon={} onClick={() => openModal("drivetrain")} /> + } + onClick={() => WPILibConnector.getInstance().then(_ => console.debug('WS connector loaded'))} + /> } diff --git a/fission/src/systems/simulation/SimulationSystem.ts b/fission/src/systems/simulation/SimulationSystem.ts index ce95d15d46..5c0c429786 100644 --- a/fission/src/systems/simulation/SimulationSystem.ts +++ b/fission/src/systems/simulation/SimulationSystem.ts @@ -11,6 +11,7 @@ import HingeStimulus from "./stimulus/HingeStimulus"; import WheelRotationStimulus from "./stimulus/WheelStimulus"; import SliderStimulus from "./stimulus/SliderStimulus"; import ChassisStimulus from "./stimulus/ChassisStimulus"; +// import WPILibConnector from "./wpilib_brain/WPILibConnector"; class SimulationSystem extends WorldSystem { @@ -20,6 +21,8 @@ class SimulationSystem extends WorldSystem { super(); this._simMechanisms = new Map(); + + // WPILibConnector.getInstance() } public RegisterMechanism(mechanism: Mechanism) { diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts new file mode 100644 index 0000000000..6521b8a990 --- /dev/null +++ b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts @@ -0,0 +1,16 @@ +import Mechanism from "@/systems/physics/Mechanism"; +import Brain from "../Brain"; + +class WPILibBrain extends Brain { + + constructor(mech: Mechanism) { + super(mech) + } + + public Update(deltaT: number): void { } + public Enable(): void { } + public Disable(): void { } + +} + +export default WPILibBrain \ No newline at end of file diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibConnector.ts b/fission/src/systems/simulation/wpilib_brain/WPILibConnector.ts new file mode 100644 index 0000000000..9cc31038a7 --- /dev/null +++ b/fission/src/systems/simulation/wpilib_brain/WPILibConnector.ts @@ -0,0 +1,48 @@ +/* +WPILib Web Socket Simulation Spec +https://github.com/wpilibsuite/allwpilib/blob/main/simulation/halsim_ws_core/doc/hardware_ws_api.md +*/ + +import { Mutex } from 'async-mutex' + +const instanceMutex = new Mutex() + +class WPILibConnector { + + private static _connector: WPILibConnector | undefined + + private _socket: WebSocket + + private constructor() { + this._socket = new WebSocket('ws://localhost:3300/wpilibws') + + this._socket.addEventListener('open', () => { + this._socket.send('hello') + }) + + this._socket.addEventListener('message', (e) => { + console.debug(`WS MESSAGE: ${e.data}`) + }) + } + + private teardown() { + + } + + public static async getInstance(): Promise { + return instanceMutex.runExclusive(() => { + if (!this._connector) { + this._connector = new WPILibConnector() + } + return this._connector + }) + } + + public static killInstance() { + if (this._connector) { + this._connector.teardown() + } + } +} + +export default WPILibConnector \ No newline at end of file From 26d8c33978a67abefa1335f7a26d6563c2db7e7b Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Thu, 13 Jun 2024 16:55:38 -0600 Subject: [PATCH 002/121] Can now receive ws messages, need to parse --- fission/src/Synthesis.tsx | 7 ++ fission/src/components/MainHUD.tsx | 22 ++++-- fission/src/mirabuf/MirabufSceneObject.ts | 2 + .../simulation/wpilib_brain/WPILibBrain.ts | 67 ++++++++++++++++++- .../wpilib_brain/WPILibConnector.ts | 48 ------------- .../simulation/wpilib_brain/WPILibWSWorker.ts | 53 +++++++++++++++ 6 files changed, 144 insertions(+), 55 deletions(-) delete mode 100644 fission/src/systems/simulation/wpilib_brain/WPILibConnector.ts create mode 100644 fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index b6746f0aff..06f0ad6cda 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -53,12 +53,16 @@ import ManageAssembliesModal from './modals/spawning/ManageAssembliesModal.tsx'; import World from './systems/World.ts'; import { AddRobotsModal, AddFieldsModal, SpawningModal } from './modals/spawning/SpawningModals.tsx'; +import WPILibWSWorker from '@/systems/simulation/wpilib_brain/WPILibWSWorker.ts?worker' + const DEFAULT_MIRA_PATH = 'test_mira/Team_2471_(2018)_v7.mira'; // const DEFAULT_MIRA_PATH = 'test_mira/Dozer_v2.mira'; // const DEFAULT_MIRA_PATH = 'test_mira/PhysicsSpikeTest_v1.mira'; // const DEFAULT_MIRA_PATH = 'test_mira/SliderTestFission_v2.mira'; // const DEFAULT_MIRA_PATH = 'test_mira/HingeTestFission_v1.mira'; +export let worker: Worker | undefined = undefined + function Synthesis() { const { openModal, closeModal, getActiveModalElement } = useModalManager(initialModals) @@ -149,6 +153,9 @@ function Synthesis() { World.InitWorld(); + // worker = new Worker(new URL('systems/simulation/wpilib_brain/Worker.ts', import.meta.url)) + worker = new WPILibWSWorker() + let mira_path = DEFAULT_MIRA_PATH; const urlParams = new URLSearchParams(document.location.search); diff --git a/fission/src/components/MainHUD.tsx b/fission/src/components/MainHUD.tsx index 551c808834..e993ac2a13 100644 --- a/fission/src/components/MainHUD.tsx +++ b/fission/src/components/MainHUD.tsx @@ -12,7 +12,11 @@ import { motion } from "framer-motion" import logo from "../assets/autodesk_logo.png" import { ToastType, useToastContext } from "../ToastContext" import { Random } from "@/util/Random" -import WPILibConnector from "@/systems/simulation/wpilib_brain/WPILibConnector" +import { worker } from "@/Synthesis" +import World from "@/systems/World" +import SceneObject from "@/systems/scene/SceneObject" +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import WPILibBrain from "@/systems/simulation/wpilib_brain/WPILibBrain" type ButtonProps = { value: string @@ -37,7 +41,7 @@ const MainHUDButton: React.FC = ({ {larger && icon} {!larger && ( {icon} @@ -48,7 +52,7 @@ const MainHUDButton: React.FC = ({ className={`px-2 ${larger ? "py-2" : "py-1 ml-6" } text-main-text cursor-pointer`} value={value} - onClick={onClick} + // onClick={onClick} /> ) @@ -154,7 +158,17 @@ const MainHUD: React.FC = () => { } - onClick={() => WPILibConnector.getInstance().then(_ => console.debug('WS connector loaded'))} + onClick={() => { + worker?.postMessage({ command: 'connect' }); + const miraObjs = [...World.SceneRenderer.sceneObjects.entries()] + .filter(x => x[1] instanceof MirabufSceneObject) + console.log(`Number of mirabuf scene objects: ${miraObjs.length}`) + if (miraObjs.length > 0) { + const mechanism = (miraObjs[0][1] as MirabufSceneObject).mechanism + const simLayer = World.SimulationSystem.GetSimulationLayer(mechanism) + simLayer?.SetBrain(new WPILibBrain(mechanism)) + } + }} /> = new Map() + +worker.addEventListener('message', (eventData: MessageEvent) => { + let data: any | undefined; + try { + data = JSON.parse(eventData.data) + } catch (e) { + console.warn(`Failed to parse data:\n${JSON.stringify(eventData.data)}`) + } + + if (!data) { + // console.log('No data, bailing out') + return + } + + switch (data.type.toLowerCase()) { + case 'solenoid': + if (!solenoids.has(data.device)) { + solenoids.set(data.device, new Solenoid(data.device)) + } + solenoids.get(data.device)?.Update(data.data) + break + case 'simdevice': + console.log(`SimDevice:\n${JSON.stringify(data, null, '\t')}`) + break + default: + // console.debug('Skipping Message') + break + } +}) + class WPILibBrain extends Brain { constructor(mech: Mechanism) { super(mech) } - public Update(deltaT: number): void { } - public Enable(): void { } - public Disable(): void { } + public Update(_: number): void { } + + public Enable(): void { + worker.postMessage({ command: 'connect' }) + } + + public Disable(): void { + worker.postMessage({ command: 'disconnect' }) + } } diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibConnector.ts b/fission/src/systems/simulation/wpilib_brain/WPILibConnector.ts deleted file mode 100644 index 9cc31038a7..0000000000 --- a/fission/src/systems/simulation/wpilib_brain/WPILibConnector.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* -WPILib Web Socket Simulation Spec -https://github.com/wpilibsuite/allwpilib/blob/main/simulation/halsim_ws_core/doc/hardware_ws_api.md -*/ - -import { Mutex } from 'async-mutex' - -const instanceMutex = new Mutex() - -class WPILibConnector { - - private static _connector: WPILibConnector | undefined - - private _socket: WebSocket - - private constructor() { - this._socket = new WebSocket('ws://localhost:3300/wpilibws') - - this._socket.addEventListener('open', () => { - this._socket.send('hello') - }) - - this._socket.addEventListener('message', (e) => { - console.debug(`WS MESSAGE: ${e.data}`) - }) - } - - private teardown() { - - } - - public static async getInstance(): Promise { - return instanceMutex.runExclusive(() => { - if (!this._connector) { - this._connector = new WPILibConnector() - } - return this._connector - }) - } - - public static killInstance() { - if (this._connector) { - this._connector.teardown() - } - } -} - -export default WPILibConnector \ No newline at end of file diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts b/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts new file mode 100644 index 0000000000..203e5c29a0 --- /dev/null +++ b/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts @@ -0,0 +1,53 @@ +import { Mutex } from "async-mutex" + +let socket: WebSocket | undefined = undefined + +const connectMutex = new Mutex() + +async function tryConnect(port: number | undefined): Promise { + await connectMutex.runExclusive(() => { + if ((socket?.readyState ?? WebSocket.CLOSED) == WebSocket.OPEN) { + socket?.close() + socket = undefined + } + + socket = new WebSocket(`ws://localhost:${port ?? 3300}/wpilibws`) + + socket.addEventListener('open', () => { console.log('WS Opened'); self.postMessage({ status: 'open' }) }) + socket.addEventListener('error', () => { console.log('WS Could not open'); self.postMessage({ status: 'error' }) }) + + socket.addEventListener('message', onMessage) + }) +} + +async function tryDisconnect(): Promise { + await connectMutex.runExclusive(() => { + if (!socket) { + return + } + + socket.close() + socket = undefined + }) +} + +function onMessage(event: MessageEvent) { + // console.log(`${JSON.stringify(JSON.parse(event.data), null, '\t')}`) + self.postMessage(event.data) +} + +self.addEventListener('message', e => { + switch (e.data.command) { + case 'connect': + tryConnect(e.data.port) + break + case 'disconnect': + tryDisconnect() + break + default: + console.warn(`Unrecognized command '${e.data.command}'`) + break + } +}) + +console.log('Worker started') \ No newline at end of file From a491e3855b6f83f3a9130aa572e1f5578cc21cd5 Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Fri, 21 Jun 2024 07:56:47 -0700 Subject: [PATCH 003/121] Non-deterministic behaviour because of course --- .../simulation/wpilib_brain/WPILibBrain.ts | 70 ++++++++++++++++++- .../simulation/wpilib_brain/WPILibWSWorker.ts | 33 +++++---- fission/src/ui/components/MainHUD.tsx | 2 +- 3 files changed, 89 insertions(+), 16 deletions(-) diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts index a942c63c79..a758549910 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts @@ -27,6 +27,64 @@ class Solenoid extends DeviceType { } const solenoids: Map = new Map() +class SimDevice extends DeviceType { + constructor(device: string) { + super(device) + } + + public Update(data: any): void { + + } +} +const simDevices: Map = new Map() + +class SparkMax extends SimDevice { + + private _sparkMaxId: number; + + constructor(device: string) { + super(device) + + console.debug('Spark Max Constructed') + + if (device.match(/spark max/i)?.length ?? 0 > 0) { + const endPos = device.indexOf(']') + const startPos = device.indexOf('[') + this._sparkMaxId = parseInt(device.substring(startPos + 1, endPos)) + } else { + throw new Error('Unrecognized Device ID') + } + } + + public Update(data: any): void { + super.Update(data) + + Object.entries(data).forEach(x => { + // if (x[0].startsWith('<')) { + // console.debug(`${x[0]} -> ${x[1]}`) + // } + + switch (x[0]) { + case '': + + break + default: + console.debug(`[${this._sparkMaxId}] ${x[0]} -> ${x[1]}`) + break + } + }) + } + + public SetPosition(val: number) { + worker.postMessage( + { + command: 'update', + data: { type: 'simdevice', device: this._device, data: { '>Position': val } } + } + ) + } +} + worker.addEventListener('message', (eventData: MessageEvent) => { let data: any | undefined; try { @@ -48,10 +106,18 @@ worker.addEventListener('message', (eventData: MessageEvent) => { solenoids.get(data.device)?.Update(data.data) break case 'simdevice': - console.log(`SimDevice:\n${JSON.stringify(data, null, '\t')}`) + // console.debug(`SimDevice:\n${JSON.stringify(data, null, '\t')}`) + if (!simDevices.has(data.device)) { + if (data.device.match(/spark max/i)) { + simDevices.set(data.device, new SparkMax(data.device)) + } else { + simDevices.set(data.device, new SimDevice(data.device)) + } + } + simDevices.get(data.device)?.Update(data.data) break default: - // console.debug('Skipping Message') + // console.debug(`Unrecognized Message:\n${data}`) break } }) diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts b/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts index 203e5c29a0..632eb8f99d 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts @@ -5,19 +5,21 @@ let socket: WebSocket | undefined = undefined const connectMutex = new Mutex() async function tryConnect(port: number | undefined): Promise { - await connectMutex.runExclusive(() => { - if ((socket?.readyState ?? WebSocket.CLOSED) == WebSocket.OPEN) { - socket?.close() - socket = undefined - } - - socket = new WebSocket(`ws://localhost:${port ?? 3300}/wpilibws`) - - socket.addEventListener('open', () => { console.log('WS Opened'); self.postMessage({ status: 'open' }) }) - socket.addEventListener('error', () => { console.log('WS Could not open'); self.postMessage({ status: 'error' }) }) - - socket.addEventListener('message', onMessage) - }) + if (!connectMutex.isLocked()) { + await connectMutex.runExclusive(() => { + if ((socket?.readyState ?? WebSocket.CLOSED) == WebSocket.OPEN) { + socket?.close() + socket = undefined + } + + socket = new WebSocket(`ws://localhost:${port ?? 3300}/wpilibws`) + + socket.addEventListener('open', () => { console.log('WS Opened'); self.postMessage({ status: 'open' }); }) + socket.addEventListener('error', () => { console.log('WS Could not open'); self.postMessage({ status: 'error' }) }) + + socket.addEventListener('message', onMessage) + }) + } } async function tryDisconnect(): Promise { @@ -44,6 +46,11 @@ self.addEventListener('message', e => { case 'disconnect': tryDisconnect() break + case 'update': + if (socket) { + socket.send(JSON.stringify(e.data.data)) + } + break default: console.warn(`Unrecognized command '${e.data.command}'`) break diff --git a/fission/src/ui/components/MainHUD.tsx b/fission/src/ui/components/MainHUD.tsx index e1b42622f0..f81b3dde21 100644 --- a/fission/src/ui/components/MainHUD.tsx +++ b/fission/src/ui/components/MainHUD.tsx @@ -36,7 +36,7 @@ const MainHUDButton: React.FC = ({ value, icon, onClick, larger }) > {larger && icon} {!larger && ( - + {icon} )} From 55acf65509f33f4498643760364783cd121fa26b Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Tue, 25 Jun 2024 14:04:56 -0700 Subject: [PATCH 004/121] Working new joint config tab --- .../src/UI/ConfigCommand.py | 837 ++++++++++-------- .../src/UI/CreateCommandInputsHelper.py | 93 ++ .../src/UI/CustomGraphics.py | 5 +- .../src/UI/FileDialogConfig.py | 3 +- .../src/UI/JointConfigTab.py | 305 +++++++ .../src/UI/MarkingMenu.py | 7 +- .../SynthesisFusionAddin/src/UI/OsHelper.py | 5 +- .../src/UI/TableUtilities.py | 10 +- 8 files changed, 861 insertions(+), 404 deletions(-) create mode 100644 exporter/SynthesisFusionAddin/src/UI/CreateCommandInputsHelper.py create mode 100644 exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 4c540f5bd5..a682ef8556 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -20,7 +20,24 @@ ) from .Configuration.SerialCommand import SerialCommand -import adsk.core, adsk.fusion, traceback, logging, os +# Transition: AARD-1685 +from .CreateCommandInputsHelper import ( + createTextBoxInput, + createTableInput, +) +from .JointConfigTab import ( + createJointConfigTab, + addJointToConfigTab, + removeIndexedJointFromConfigTab, + getSelectedJoints, + resetSelectedJoints +) + +import adsk.core +import adsk.fusion +import traceback +import logging +import os from types import SimpleNamespace # ====================================== CONFIG COMMAND ====================================== @@ -241,7 +258,7 @@ def notify(self, args): # ~~~~~~~~~~~~~~~~ WEIGHT CONFIGURATION ~~~~~~~~~~~~~~~~ """ - Table for weight config. + Table for weight config. - Used this to align multiple commandInputs on the same row """ weightTableInput = self.createTableInput( @@ -387,17 +404,17 @@ def notify(self, args): algorithmicIndicator.formattedText = "🟢" algorithmicIndicator.tooltipDescription = ( "(enabled)" + - "
If a sub-part of a wheel is selected (eg. a roller of an omni wheel), an algorithm will traverse the assembly to best determine the entire wheel component.
" + - "
This traversal operates on how the wheel is jointed and where the joint is placed. If the automatic selection fails, try:" + + "
If a sub-part of a wheel is selected (eg. a roller of an omni wheel), an algorithm will traverse the assembly to best determine the entire wheel component.
" + + "
This traversal operates on how the wheel is jointed and where the joint is placed. If the automatic selection fails, try:" + "
    " + - "" + - "
  • Jointing the wheel differently, or

  • " + - "
  • Selecting the wheel from the browser while holding down  CTRL , or

  • " + - "
  • Disabling Algorithmic Selection.
  • " + - "
    " + + "" + + "
  • Jointing the wheel differently, or

  • " + + "
  • Selecting the wheel from the browser while holding down  CTRL , or

  • " + + "
  • Disabling Algorithmic Selection.
  • " + + "
    " + "
" ) - + wheelTableInput.addCommandInput( algorithmicIndicator, 0, @@ -437,157 +454,158 @@ def notify(self, args): 3, ) - # AUTOMATICALLY SELECT DUPLICATES - """ - Select duplicates? - - creates a BoolValueCommandInput - """ - # self.createBooleanInput( # create bool value command input using helper - # "duplicate_selection", - # "Select Duplicates", - # wheel_inputs, - # checked=True, - # tooltip="Select duplicate wheel components.", - # tooltipadvanced="""
When this is checked, all duplicate occurrences will be automatically selected. - #
This feature may fail when duplicates are not direct copies.
""", # advanced tooltip - # enabled=True, - # ) + # Transition AARD-1685: + # This should be how all things in this class are handled. - Brandon + createJointConfigTab(args) + for joint in list( + gm.app.activeDocument.design.rootComponent.allJoints + ) + list(gm.app.activeDocument.design.rootComponent.allAsBuiltJoints): + if (joint.jointMotion.jointType == JointMotions.REVOLUTE.value + or joint.jointMotion.jointType == JointMotions.SLIDER.value + ) and not joint.isSuppressed: + addJointToConfigTab(joint) - # ~~~~~~~~~~~~~~~~ JOINT CONFIGURATION ~~~~~~~~~~~~~~~~ - """ - Joint configuration group. Container for joint selection table - """ - jointConfig = inputs.addGroupCommandInput( - "joint_config", "Joint Configuration" - ) - jointConfig.isExpanded = False - jointConfig.isVisible = True - jointConfig.tooltip = "Select and define joint occurrences in your assembly." - joint_inputs = jointConfig.children + # Transition AARD-1685: + # ~~~~~~~~~~~~~~~~ JOINT CONFIGURATION ~~~~~~~~~~~~~~~~ + # """ + # Joint configuration group. Container for joint selection table + # """ + # jointConfig = inputs.addGroupCommandInput( + # "joint_config", "Joint Configuration" + # ) + # jointConfig.isExpanded = False + # jointConfig.isVisible = True + # jointConfig.tooltip = "Select and define joint occurrences in your assembly." + + # joint_inputs = jointConfig.children + + # # JOINT SELECTION TABLE + # """ + # All selection joints appear here. + # """ + # jointTableInput = ( + # self.createTableInput( # create tablecommandinput using helper + # "joint_table", + # "Joint Table", + # joint_inputs, + # 6, + # "1:2:2:2:2:2", + # 50, + # ) + # ) - # JOINT SELECTION TABLE - """ - All selection joints appear here. - """ - jointTableInput = ( - self.createTableInput( # create tablecommandinput using helper - "joint_table", - "Joint Table", - joint_inputs, - 6, - "1:2:2:2:2:2", - 50, - ) - ) + # addJointInput = joint_inputs.addBoolValueInput( + # "joint_add", "Add", False + # ) # add button - addJointInput = joint_inputs.addBoolValueInput( - "joint_add", "Add", False - ) # add button + # removeJointInput = joint_inputs.addBoolValueInput( # remove button + # "joint_delete", "Remove", False + # ) - removeJointInput = joint_inputs.addBoolValueInput( # remove button - "joint_delete", "Remove", False - ) + # addJointInput.isEnabled = removeJointInput.isEnabled = True - addJointInput.isEnabled = removeJointInput.isEnabled = True + # addJointInput.tooltip = "Add a joint selection" # tooltips + # removeJointInput.tooltip = "Remove a joint selection" - addJointInput.tooltip = "Add a joint selection" # tooltips - removeJointInput.tooltip = "Remove a joint selection" + # jointSelectInput = joint_inputs.addSelectionInput( + # "joint_select", + # "Selection", + # "Select a joint in your drive-train assembly.", + # ) - jointSelectInput = joint_inputs.addSelectionInput( - "joint_select", - "Selection", - "Select a joint in your drive-train assembly.", - ) + # jointSelectInput.addSelectionFilter("Joints") # only allow joint selection + # jointSelectInput.setSelectionLimits(0) # set no selection count limits + # jointSelectInput.isEnabled = False + # jointSelectInput.isVisible = False # make selection box invisible + + # jointTableInput.addToolbarCommandInput( + # addJointInput + # ) # add bool inputs to the toolbar + # jointTableInput.addToolbarCommandInput( + # removeJointInput + # ) # add bool inputs to the toolbar + + # jointTableInput.addCommandInput( + # self.createTextBoxInput( # create a textBoxCommandInput for the table header (Joint Motion), using helper + # "motion_header", + # "Motion", + # joint_inputs, + # "Motion", + # bold=False, + # ), + # 0, + # 0, + # ) - jointSelectInput.addSelectionFilter("Joints") # only allow joint selection - jointSelectInput.setSelectionLimits(0) # set no selection count limits - jointSelectInput.isEnabled = False - jointSelectInput.isVisible = False # make selection box invisible - - jointTableInput.addToolbarCommandInput( - addJointInput - ) # add bool inputs to the toolbar - jointTableInput.addToolbarCommandInput( - removeJointInput - ) # add bool inputs to the toolbar - - jointTableInput.addCommandInput( - self.createTextBoxInput( # create a textBoxCommandInput for the table header (Joint Motion), using helper - "motion_header", - "Motion", - joint_inputs, - "Motion", - bold=False, - ), - 0, - 0, - ) + # jointTableInput.addCommandInput( + # self.createTextBoxInput( # textBoxCommandInput for table header (Joint Name), using helper + # "name_header", "Name", joint_inputs, "Joint name", bold=False + # ), + # 0, + # 1, + # ) - jointTableInput.addCommandInput( - self.createTextBoxInput( # textBoxCommandInput for table header (Joint Name), using helper - "name_header", "Name", joint_inputs, "Joint name", bold=False - ), - 0, - 1, - ) + # jointTableInput.addCommandInput( + # self.createTextBoxInput( # another header using helper + # "parent_header", + # "Parent", + # joint_inputs, + # "Parent joint", + # background="#d9d9d9", # background color + # ), + # 0, + # 2, + # ) - jointTableInput.addCommandInput( - self.createTextBoxInput( # another header using helper - "parent_header", - "Parent", - joint_inputs, - "Parent joint", - background="#d9d9d9", # background color - ), - 0, - 2, - ) + # jointTableInput.addCommandInput( + # self.createTextBoxInput( # another header using helper + # "signal_header", + # "Signal", + # joint_inputs, + # "Signal type", + # background="#d9d9d9", # back color + # ), + # 0, + # 3, + # ) - jointTableInput.addCommandInput( - self.createTextBoxInput( # another header using helper - "signal_header", - "Signal", - joint_inputs, - "Signal type", - background="#d9d9d9", # back color - ), - 0, - 3, - ) + # jointTableInput.addCommandInput( + # self.createTextBoxInput( # another header using helper + # "speed_header", + # "Speed", + # joint_inputs, + # "Joint Speed", + # background="#d9d9d9", # back color + # ), + # 0, + # 4, + # ) - jointTableInput.addCommandInput( - self.createTextBoxInput( # another header using helper - "speed_header", - "Speed", - joint_inputs, - "Joint Speed", - background="#d9d9d9", # back color - ), - 0, - 4, - ) + # jointTableInput.addCommandInput( + # self.createTextBoxInput( # another header using helper + # "force_header", + # "Force", + # joint_inputs, + # "Joint Force", + # background="#d9d9d9", # back color + # ), + # 0, + # 5, + # ) - jointTableInput.addCommandInput( - self.createTextBoxInput( # another header using helper - "force_header", - "Force", - joint_inputs, - "Joint Force", - background="#d9d9d9", # back color - ), - 0, - 5, - ) + # Transition: AARD-1685 + # for joint in list( + # gm.app.activeDocument.design.rootComponent.allJoints + # ) + list(gm.app.activeDocument.design.rootComponent.allAsBuiltJoints): + # if ( + # joint.jointMotion.jointType == JointMotions.REVOLUTE.value + # or joint.jointMotion.jointType == JointMotions.SLIDER.value + # ) and not joint.isSuppressed: + # # Transition: AARD-1685 + # # addJointToTable(joint) + # addJointToConfigTab(args, joint) - for joint in list( - gm.app.activeDocument.design.rootComponent.allJoints - ) + list(gm.app.activeDocument.design.rootComponent.allAsBuiltJoints): - if ( - joint.jointMotion.jointType == JointMotions.REVOLUTE.value - or joint.jointMotion.jointType == JointMotions.SLIDER.value - ) and not joint.isSuppressed: - addJointToTable(joint) # ~~~~~~~~~~~~~~~~ GAMEPIECE CONFIGURATION ~~~~~~~~~~~~~~~~ """ @@ -996,6 +1014,7 @@ def notify(self, args): "{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}" ).error("Failed:\n{}".format(traceback.format_exc())) + # Transition: AARD-1685 def createBooleanInput( self, _id: str, @@ -1032,6 +1051,7 @@ def createBooleanInput( "{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}.createBooleanInput()" ).error("Failed:\n{}".format(traceback.format_exc())) + # Transition: AARD-1685 def createTableInput( self, _id: str, @@ -1072,6 +1092,7 @@ def createTableInput( "{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}.createTableInput()" ).error("Failed:\n{}".format(traceback.format_exc())) + # Transition: AARD-1685 def createTextBoxInput( self, _id: str, @@ -1256,7 +1277,7 @@ def notify(self, args): + ".mira" ) - if savepath == False: + if not savepath: # save was canceled return else: @@ -1274,7 +1295,7 @@ def notify(self, args): renderer = 0 _exportWheels = [] # all selected wheels, formatted for parseOptions - _exportJoints = [] # all selected joints, formatted for parseOptions + # _exportJoints = [] # all selected joints, formatted for parseOptions _exportGamepieces = [] # TODO work on the code to populate Gamepiece _robotWeight = float _mode = Mode @@ -1301,70 +1322,74 @@ def notify(self, args): WheelListGlobal[row - 1].entityToken, wheelTypeIndex, signalTypeIndex, - # onSelect.wheelJointList[row-1][0] # GUID of wheel joint. if no joint found, default to None + # onSelect.wheelJointList[row-1][0] # GUID of wheel joint. if no joint + # found, default to None ) ) + # Transition: AARD-1685 + _exportJoints = getSelectedJoints() + """ Loops through all rows in the joint table to extract the input values """ - jointTableInput = jointTable() - for row in range(jointTableInput.rowCount): - if row == 0: - continue - - parentJointIndex = jointTableInput.getInputAtPosition( - row, 2 - ).selectedItem.index # parent joint index, int - - signalTypeIndex = jointTableInput.getInputAtPosition( - row, 3 - ).selectedItem.index # signal type index, int - - # typeString = jointTableInput.getInputAtPosition( - # row, 0 - # ).name - - jointSpeed = jointTableInput.getInputAtPosition(row, 4).value - - jointForce = jointTableInput.getInputAtPosition(row, 5).value - - parentJointToken = "" - - if parentJointIndex == 0: - _exportJoints.append( - _Joint( - JointListGlobal[row - 1].entityToken, - JointParentType.ROOT, - signalTypeIndex, # index of selected signal in dropdown - jointSpeed, - jointForce / 100.0, - ) # parent joint GUID - ) - continue - elif parentJointIndex < row: - parentJointToken = JointListGlobal[ - parentJointIndex - 1 - ].entityToken # parent joint GUID, str - else: - parentJointToken = JointListGlobal[ - parentJointIndex + 1 - ].entityToken # parent joint GUID, str - - # for wheel in _exportWheels: - # find some way to get joint - # 1. Compare Joint occurrence1 to wheel.occurrence_token - # 2. if true set the parent to Root - - _exportJoints.append( - _Joint( - JointListGlobal[row - 1].entityToken, - parentJointToken, - signalTypeIndex, - jointSpeed, - jointForce, - ) - ) + # jointTableInput = jointTable() + # for row in range(jointTableInput.rowCount): + # if row == 0: + # continue + + # parentJointIndex = jointTableInput.getInputAtPosition( + # row, 2 + # ).selectedItem.index # parent joint index, int + + # signalTypeIndex = jointTableInput.getInputAtPosition( + # row, 3 + # ).selectedItem.index # signal type index, int + + # # typeString = jointTableInput.getInputAtPosition( + # # row, 0 + # # ).name + + # jointSpeed = jointTableInput.getInputAtPosition(row, 4).value + + # jointForce = jointTableInput.getInputAtPosition(row, 5).value + + # parentJointToken = "" + + # if parentJointIndex == 0: + # _exportJoints.append( + # _Joint( + # JointListGlobal[row - 1].entityToken, + # JointParentType.ROOT, + # signalTypeIndex, # index of selected signal in dropdown + # jointSpeed, + # jointForce / 100.0, + # ) # parent joint GUID + # ) + # continue + # elif parentJointIndex < row: + # parentJointToken = JointListGlobal[ + # parentJointIndex - 1 + # ].entityToken # parent joint GUID, str + # else: + # parentJointToken = JointListGlobal[ + # parentJointIndex + 1 + # ].entityToken # parent joint GUID, str + + # # for wheel in _exportWheels: + # # find some way to get joint + # # 1. Compare Joint occurrence1 to wheel.occurrence_token + # # 2. if true set the parent to Root + + # _exportJoints.append( + # _Joint( + # JointListGlobal[row - 1].entityToken, + # parentJointToken, + # signalTypeIndex, + # jointSpeed, + # jointForce, + # ) + # ) """ Loops through all rows in the gamepiece table to extract the input values @@ -1431,11 +1456,16 @@ def notify(self, args): if options.parse(False): # success - return + pass else: self.log.error( f"Error: \n\t{name} could not be written to \n {savepath}" ) + + # All selections should be reset AFTER a successful export and save. + # If we run into an exporting error we should return back to the panel with all current options + # still in tact. Even if they did not save. + resetSelectedJoints() except: if gm.ui: gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) @@ -1469,15 +1499,21 @@ def notify(self, args): auto_calc_weight_f = INPUTS_ROOT.itemById("auto_calc_weight_f") removeWheelInput = INPUTS_ROOT.itemById("wheel_delete") - removeJointInput = INPUTS_ROOT.itemById("joint_delete") + removeJointInput = INPUTS_ROOT.itemById("jointRemoveButton") removeFieldInput = INPUTS_ROOT.itemById("field_delete") addWheelInput = INPUTS_ROOT.itemById("wheel_add") - addJointInput = INPUTS_ROOT.itemById("joint_add") + addJointInput = INPUTS_ROOT.itemById("jointAddButton") addFieldInput = INPUTS_ROOT.itemById("field_add") wheelTableInput = wheelTable() - jointTableInput = jointTable() + + # Transition: AARD-1685 + # jointTableInput = jointTable() + + inputs = args.command.commandInputs + jointTableInput: adsk.core.TableCommandInput = inputs.itemById("jointSettings").children.itemById("jointTable") + gamepieceTableInput = gamepieceTable() if wheelTableInput.rowCount <= 1: @@ -1725,10 +1761,12 @@ def notify(self, args: adsk.core.SelectionEventArgs): WheelListGlobal.index(self.selectedJoint) ) else: - if self.selectedJoint not in JointListGlobal: - addJointToTable(self.selectedJoint) - else: - removeJointFromTable(self.selectedJoint) + # Transition: AARD-1685 + # if self.selectedJoint not in JointListGlobal: + addJointToConfigTab(self.selectedJoint) + # addJointToTable(self.selectedJoint) + # else: + # removeJointFromTable(self.selectedJoint) selectionInput.isEnabled = False selectionInput.isVisible = False @@ -1905,11 +1943,16 @@ def notify(self, args): frictionCoeff = INPUTS_ROOT.itemById("friction_coeff_override") wheelSelect = inputs.itemById("wheel_select") - jointSelect = inputs.itemById("joint_select") + jointSelect = inputs.itemById("jointSelection") gamepieceSelect = inputs.itemById("gamepiece_select") wheelTableInput = wheelTable() - jointTableInput = jointTable() + + # Transition: AARD-1685 + # jointTableInput = jointTable() + + jointTableInput: adsk.core.TableCommandInput = args.inputs.itemById("jointTable") + gamepieceTableInput = gamepieceTable() weightTableInput = inputs.itemById("weight_table") @@ -1920,7 +1963,7 @@ def notify(self, args): gamepieceConfig = inputs.itemById("gamepiece_config") addWheelInput = INPUTS_ROOT.itemById("wheel_add") - addJointInput = INPUTS_ROOT.itemById("joint_add") + addJointInput = INPUTS_ROOT.itemById("jointAddButton") addFieldInput = INPUTS_ROOT.itemById("field_add") indicator = INPUTS_ROOT.itemById("algorithmic_indicator") @@ -2099,7 +2142,8 @@ def notify(self, args): addJointInput.isEnabled = True addWheelInput.isEnabled = False - elif cmdInput.id == "joint_add": + # Transition: AARD-1685 + elif cmdInput.id == "jointAddButton": self.reset() addWheelInput.isEnabled = True @@ -2128,7 +2172,7 @@ def notify(self, args): index = wheelTableInput.selectedRow - 1 removeWheelFromTable(index) - elif cmdInput.id == "joint_delete": + elif cmdInput.id == "jointRemoveButton": gm.ui.activeSelections.clear() addJointInput.isEnabled = True @@ -2138,8 +2182,13 @@ def notify(self, args): jointTableInput.selectedRow = jointTableInput.rowCount - 1 gm.ui.messageBox("Select a row to delete.") else: - joint = JointListGlobal[jointTableInput.selectedRow - 1] - removeJointFromTable(joint) + # Transition: AARD-1685 + # joint = JointListGlobal[jointTableInput.selectedRow - 1] + # removeJointFromTable(joint) + # removeJointFromConfigTab(joint) + + # Select Row is 1 indexed + removeIndexedJointFromConfigTab(jointTableInput.selectedRow - 1) elif cmdInput.id == "field_delete": gm.ui.activeSelections.clear() @@ -2159,7 +2208,7 @@ def notify(self, args): elif cmdInput.id == "wheel_select": addWheelInput.isEnabled = True - elif cmdInput.id == "joint_select": + elif cmdInput.id == "jointSelection": addJointInput.isEnabled = True elif cmdInput.id == "gamepiece_select": @@ -2168,7 +2217,7 @@ def notify(self, args): elif cmdInput.id == "friction_override": boolValue = adsk.core.BoolValueCommandInput.cast(cmdInput) - if boolValue.value == True: + if boolValue.value: frictionCoeff.isVisible = True else: frictionCoeff.isVisible = False @@ -2433,140 +2482,141 @@ def notify(self, args): ).error("Failed:\n{}".format(traceback.format_exc())) -def addJointToTable(joint: adsk.fusion.Joint) -> None: - """### Adds a Joint object to its global list and joint table. - - Args: - joint (adsk.fusion.Joint): Joint object to be added - """ - try: - JointListGlobal.append(joint) - jointTableInput = jointTable() - cmdInputs = adsk.core.CommandInputs.cast(jointTableInput.commandInputs) - - # joint type icons - if joint.jointMotion.jointType == adsk.fusion.JointTypes.RigidJointType: - icon = cmdInputs.addImageCommandInput( - "placeholder", "Rigid", IconPaths.jointIcons["rigid"] - ) - icon.tooltip = "Rigid joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: - icon = cmdInputs.addImageCommandInput( - "placeholder", "Revolute", IconPaths.jointIcons["revolute"] - ) - icon.tooltip = "Revolute joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: - icon = cmdInputs.addImageCommandInput( - "placeholder", "Slider", IconPaths.jointIcons["slider"] - ) - icon.tooltip = "Slider joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.PlanarJointType: - icon = cmdInputs.addImageCommandInput( - "placeholder", "Planar", IconPaths.jointIcons["planar"] - ) - icon.tooltip = "Planar joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.PinSlotJointType: - icon = cmdInputs.addImageCommandInput( - "placeholder", "Pin Slot", IconPaths.jointIcons["pin_slot"] - ) - icon.tooltip = "Pin slot joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.CylindricalJointType: - icon = cmdInputs.addImageCommandInput( - "placeholder", "Cylindrical", IconPaths.jointIcons["cylindrical"] - ) - icon.tooltip = "Cylindrical joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.BallJointType: - icon = cmdInputs.addImageCommandInput( - "placeholder", "Ball", IconPaths.jointIcons["ball"] - ) - icon.tooltip = "Ball joint" - - # joint name - name = cmdInputs.addTextBoxCommandInput("name_j", "Occurrence name", "", 1, True) - name.tooltip = joint.name - name.formattedText = "

{}

".format(joint.name) - - jointType = cmdInputs.addDropDownCommandInput( - "joint_parent", - "Joint Type", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) - jointType.isFullWidth = True - jointType.listItems.add("Root", True) - - # after each additional joint added, add joint to the dropdown of all preview rows/joints - for row in range(jointTableInput.rowCount): - if row != 0: - dropDown = jointTableInput.getInputAtPosition(row, 2) - dropDown.listItems.add(JointListGlobal[-1].name, False) - - # add all parent joint options to added joint dropdown - for j in range(len(JointListGlobal) - 1): - jointType.listItems.add(JointListGlobal[j].name, False) - - jointType.tooltip = "Possible parent joints" - jointType.tooltipDescription = "
The root component is usually the parent." - - signalType = cmdInputs.addDropDownCommandInput( - "signal_type", - "Signal Type", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) - signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) - signalType.tooltip = "Signal type" - - row = jointTableInput.rowCount - - jointTableInput.addCommandInput(icon, row, 0) - jointTableInput.addCommandInput(name, row, 1) - jointTableInput.addCommandInput(jointType, row, 2) - jointTableInput.addCommandInput(signalType, row, 3) - - if joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: - jointSpeed = cmdInputs.addValueInput( - "joint_speed", - "Speed", - "deg", - adsk.core.ValueInput.createByReal(3.1415926), - ) - jointSpeed.tooltip = "Degrees per second" - jointTableInput.addCommandInput(jointSpeed, row, 4) - - jointForce = cmdInputs.addValueInput( - "joint_force", "Force", "N", adsk.core.ValueInput.createByReal(5000) - ) - jointForce.tooltip = "Newton-Meters***" - jointTableInput.addCommandInput(jointForce, row, 5) - - if joint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: - jointSpeed = cmdInputs.addValueInput( - "joint_speed", - "Speed", - "m", - adsk.core.ValueInput.createByReal(100), - ) - jointSpeed.tooltip = "Meters per second" - jointTableInput.addCommandInput(jointSpeed, row, 4) - - jointForce = cmdInputs.addValueInput( - "joint_force", "Force", "N", adsk.core.ValueInput.createByReal(5000) - ) - jointForce.tooltip = "Newtons" - jointTableInput.addCommandInput(jointForce, row, 5) - - except: - gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.addJointToTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) +# Transition: AARD-1685 +# def addJointToTable(joint: adsk.fusion.Joint) -> None: +# """### Adds a Joint object to its global list and joint table. + +# Args: +# joint (adsk.fusion.Joint): Joint object to be added +# """ +# try: +# JointListGlobal.append(joint) +# jointTableInput = jointTable() +# cmdInputs = adsk.core.CommandInputs.cast(jointTableInput.commandInputs) + +# # joint type icons +# if joint.jointMotion.jointType == adsk.fusion.JointTypes.RigidJointType: +# icon = cmdInputs.addImageCommandInput( +# "placeholder", "Rigid", IconPaths.jointIcons["rigid"] +# ) +# icon.tooltip = "Rigid joint" + +# elif joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: +# icon = cmdInputs.addImageCommandInput( +# "placeholder", "Revolute", IconPaths.jointIcons["revolute"] +# ) +# icon.tooltip = "Revolute joint" + +# elif joint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: +# icon = cmdInputs.addImageCommandInput( +# "placeholder", "Slider", IconPaths.jointIcons["slider"] +# ) +# icon.tooltip = "Slider joint" + +# elif joint.jointMotion.jointType == adsk.fusion.JointTypes.PlanarJointType: +# icon = cmdInputs.addImageCommandInput( +# "placeholder", "Planar", IconPaths.jointIcons["planar"] +# ) +# icon.tooltip = "Planar joint" + +# elif joint.jointMotion.jointType == adsk.fusion.JointTypes.PinSlotJointType: +# icon = cmdInputs.addImageCommandInput( +# "placeholder", "Pin Slot", IconPaths.jointIcons["pin_slot"] +# ) +# icon.tooltip = "Pin slot joint" + +# elif joint.jointMotion.jointType == adsk.fusion.JointTypes.CylindricalJointType: +# icon = cmdInputs.addImageCommandInput( +# "placeholder", "Cylindrical", IconPaths.jointIcons["cylindrical"] +# ) +# icon.tooltip = "Cylindrical joint" + +# elif joint.jointMotion.jointType == adsk.fusion.JointTypes.BallJointType: +# icon = cmdInputs.addImageCommandInput( +# "placeholder", "Ball", IconPaths.jointIcons["ball"] +# ) +# icon.tooltip = "Ball joint" + +# # joint name +# name = cmdInputs.addTextBoxCommandInput("name_j", "Occurrence name", "", 1, True) +# name.tooltip = joint.name +# name.formattedText = "

{}

".format(joint.name) + +# jointType = cmdInputs.addDropDownCommandInput( +# "joint_parent", +# "Joint Type", +# dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, +# ) +# jointType.isFullWidth = True +# jointType.listItems.add("Root", True) + +# # after each additional joint added, add joint to the dropdown of all preview rows/joints +# for row in range(jointTableInput.rowCount): +# if row != 0: +# dropDown = jointTableInput.getInputAtPosition(row, 2) +# dropDown.listItems.add(JointListGlobal[-1].name, False) + +# # add all parent joint options to added joint dropdown +# for j in range(len(JointListGlobal) - 1): +# jointType.listItems.add(JointListGlobal[j].name, False) + +# jointType.tooltip = "Possible parent joints" +# jointType.tooltipDescription = "
The root component is usually the parent." + +# signalType = cmdInputs.addDropDownCommandInput( +# "signal_type", +# "Signal Type", +# dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, +# ) +# signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) +# signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) +# signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) +# signalType.tooltip = "Signal type" + +# row = jointTableInput.rowCount + +# jointTableInput.addCommandInput(icon, row, 0) +# jointTableInput.addCommandInput(name, row, 1) +# jointTableInput.addCommandInput(jointType, row, 2) +# jointTableInput.addCommandInput(signalType, row, 3) + +# if joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: +# jointSpeed = cmdInputs.addValueInput( +# "joint_speed", +# "Speed", +# "deg", +# adsk.core.ValueInput.createByReal(3.1415926), +# ) +# jointSpeed.tooltip = "Degrees per second" +# jointTableInput.addCommandInput(jointSpeed, row, 4) + +# jointForce = cmdInputs.addValueInput( +# "joint_force", "Force", "N", adsk.core.ValueInput.createByReal(5000) +# ) +# jointForce.tooltip = "Newton-Meters***" +# jointTableInput.addCommandInput(jointForce, row, 5) + +# if joint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: +# jointSpeed = cmdInputs.addValueInput( +# "joint_speed", +# "Speed", +# "m", +# adsk.core.ValueInput.createByReal(100), +# ) +# jointSpeed.tooltip = "Meters per second" +# jointTableInput.addCommandInput(jointSpeed, row, 4) + +# jointForce = cmdInputs.addValueInput( +# "joint_force", "Force", "N", adsk.core.ValueInput.createByReal(5000) +# ) +# jointForce.tooltip = "Newtons" +# jointTableInput.addCommandInput(jointForce, row, 5) + +# except: +# gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) + # logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.addJointToTable()").error( + # "Failed:\n{}".format(traceback.format_exc()) + # ) def addWheelToTable(wheel: adsk.fusion.Joint) -> None: @@ -2759,42 +2809,43 @@ def removeWheelFromTable(index: int) -> None: ) -def removeJointFromTable(joint: adsk.fusion.Joint) -> None: - """### Removes a joint occurrence from its global list and joint table. - - Args: - joint (adsk.fusion.Joint): Joint object to be removed - """ - try: - index = JointListGlobal.index(joint) - jointTableInput = jointTable() - JointListGlobal.remove(joint) - - jointTableInput.deleteRow(index + 1) - - for row in range(jointTableInput.rowCount): - if row == 0: - continue - - dropDown = jointTableInput.getInputAtPosition(row, 2) - listItems = dropDown.listItems - - if row > index: - if listItems.item(index + 1).isSelected: - listItems.item(index).isSelected = True - listItems.item(index + 1).deleteMe() - else: - listItems.item(index + 1).deleteMe() - else: - if listItems.item(index).isSelected: - listItems.item(index - 1).isSelected = True - listItems.item(index).deleteMe() - else: - listItems.item(index).deleteMe() - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.removeJointFromTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) +# Transition: AARD-1685 +# def removeJointFromTable(joint: adsk.fusion.Joint) -> None: +# """### Removes a joint occurrence from its global list and joint table. + +# Args: +# joint (adsk.fusion.Joint): Joint object to be removed +# """ +# try: +# index = JointListGlobal.index(joint) +# jointTableInput = jointTable() +# JointListGlobal.remove(joint) + +# jointTableInput.deleteRow(index + 1) + +# for row in range(jointTableInput.rowCount): +# if row == 0: +# continue + +# dropDown = jointTableInput.getInputAtPosition(row, 2) +# listItems = dropDown.listItems + +# if row > index: +# if listItems.item(index + 1).isSelected: +# listItems.item(index).isSelected = True +# listItems.item(index + 1).deleteMe() +# else: +# listItems.item(index + 1).deleteMe() +# else: +# if listItems.item(index).isSelected: +# listItems.item(index - 1).isSelected = True +# listItems.item(index).deleteMe() +# else: +# listItems.item(index).deleteMe() +# except: +# logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.removeJointFromTable()").error( +# "Failed:\n{}".format(traceback.format_exc()) +# ) def removeGamePieceFromTable(index: int) -> None: diff --git a/exporter/SynthesisFusionAddin/src/UI/CreateCommandInputsHelper.py b/exporter/SynthesisFusionAddin/src/UI/CreateCommandInputsHelper.py new file mode 100644 index 0000000000..acdddc5858 --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/UI/CreateCommandInputsHelper.py @@ -0,0 +1,93 @@ +import adsk.core +import traceback +import logging + + +def createTableInput( + id: str, + name: str, + inputs: adsk.core.CommandInputs, + columns: int, + ratio: str, + maxRows: int, + minRows=1, + columnSpacing=0, + rowSpacing=0, +) -> adsk.core.TableCommandInput: + try: + input = inputs.addTableCommandInput(id, name, columns, ratio) + input.minimumVisibleRows = minRows + input.maximumVisibleRows = maxRows + input.columnSpacing = columnSpacing + input.rowSpacing = rowSpacing + + return input + except BaseException: + logging.getLogger( + "{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}.createTableInput()" + ).error("Failed:\n{}".format(traceback.format_exc())) + + +def createBooleanInput( + id: str, + name: str, + inputs: adsk.core.CommandInputs, + tooltip="", + tooltipadvanced="", + checked=True, + enabled=True, + isCheckBox=True, +) -> adsk.core.BoolValueCommandInput: + try: + input = inputs.addBoolValueInput(id, name, isCheckBox) + input.value = checked + input.isEnabled = enabled + input.tooltip = tooltip + input.tooltipDescription = tooltipadvanced + + return input + except BaseException: + logging.getLogger( + "{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}.createBooleanInput()" + ).error("Failed:\n{}".format(traceback.format_exc())) + + +def createTextBoxInput( + id: str, + name: str, + inputs: adsk.core.CommandInputs, + text: str, + italics=True, + bold=True, + fontSize=10, + alignment="center", + rowCount=1, + read=True, + background="whitesmoke", + tooltip="", + advanced_tooltip="", +) -> adsk.core.TextBoxCommandInput: + try: + if bold: + text = f"{text}" + + if italics: + text = f"{text}" + + outputText = f""" +
+

+ {text} +

+ + """ + + input = inputs.addTextBoxCommandInput(id, name, outputText, rowCount, read) + input.tooltip = tooltip + input.tooltipDescription = advanced_tooltip + + return input + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.createTextBoxInput()").error( + "Failed:\n{}".format(traceback.format_exc()) + ) diff --git a/exporter/SynthesisFusionAddin/src/UI/CustomGraphics.py b/exporter/SynthesisFusionAddin/src/UI/CustomGraphics.py index 9b99310657..5906c0d148 100644 --- a/exporter/SynthesisFusionAddin/src/UI/CustomGraphics.py +++ b/exporter/SynthesisFusionAddin/src/UI/CustomGraphics.py @@ -1,4 +1,7 @@ -import adsk.fusion, adsk.core, traceback, logging +import adsk.fusion +import adsk.core +import traceback +import logging from ..general_imports import * diff --git a/exporter/SynthesisFusionAddin/src/UI/FileDialogConfig.py b/exporter/SynthesisFusionAddin/src/UI/FileDialogConfig.py index 0dd7e9d88b..5807fe15c3 100644 --- a/exporter/SynthesisFusionAddin/src/UI/FileDialogConfig.py +++ b/exporter/SynthesisFusionAddin/src/UI/FileDialogConfig.py @@ -4,7 +4,8 @@ from ..Types.OString import OString from typing import Union -import adsk.core, adsk.fusion +import adsk.core +import adsk.fusion def SaveFileDialog( diff --git a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py new file mode 100644 index 0000000000..a360fca8a4 --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py @@ -0,0 +1,305 @@ +import logging +import traceback + +import adsk.core +import adsk.fusion + +from . import IconPaths +from .CreateCommandInputsHelper import createTableInput, createTextBoxInput + +from ..Parser.ParseOptions import _Joint, JointParentType + +# Wish we did not need this. Could look into storing everything within the design every time - Brandon +selectedJointList: list[adsk.fusion.Joint] = [] +jointConfigTable: adsk.core.TableCommandInput + + +def createJointConfigTab(args: adsk.core.CommandCreatedEventArgs) -> None: + try: + inputs = args.command.commandInputs + jointConfigTab = inputs.addTabCommandInput("jointSettings", "Joint Settings") + jointConfigTab.tooltip = "Select and configure robot joints." + jointConfigTabInputs = jointConfigTab.children + + # TODO: Change background colors and such - Brandon + global jointConfigTable + jointConfigTable = createTableInput( + "jointTable", + "Joint Table", + jointConfigTabInputs, + 6, + "1:2:2:2:2:2", + 50, + ) + + jointConfigTable.addCommandInput( + createTextBoxInput( + "motionHeader", + "Motion", + jointConfigTabInputs, + "Motion", + bold=False, + ), + 0, + 0, + ) + + jointConfigTable.addCommandInput( + createTextBoxInput( + "nameHeader", "Name", jointConfigTabInputs, "Joint name", bold=False + ), + 0, + 1, + ) + + jointConfigTable.addCommandInput( + createTextBoxInput( + "parentHeader", + "Parent", + jointConfigTabInputs, + "Parent joint", + background="#d9d9d9", + ), + 0, + 2, + ) + + jointConfigTable.addCommandInput( + createTextBoxInput( + "signalHeader", + "Signal", + jointConfigTabInputs, + "Signal type", + background="#d9d9d9", + ), + 0, + 3, + ) + + jointConfigTable.addCommandInput( + createTextBoxInput( + "speedHeader", + "Speed", + jointConfigTabInputs, + "Joint Speed", + background="#d9d9d9", + ), + 0, + 4, + ) + + jointConfigTable.addCommandInput( + createTextBoxInput( + "forceHeader", + "Force", + jointConfigTabInputs, + "Joint Force", + background="#d9d9d9", + ), + 0, + 5, + ) + + jointSelect = jointConfigTabInputs.addSelectionInput( + "jointSelection", "Selection", "Select a joint in your assembly to add." + ) + jointSelect.addSelectionFilter("Joints") + jointSelect.setSelectionLimits(0) + + # Visibility is triggered by `addJointInputButton` + jointSelect.isEnabled = jointSelect.isVisible = False + + addJointInputButton = jointConfigTabInputs.addBoolValueInput("jointAddButton", "Add", False) + removeJointInputButton = jointConfigTabInputs.addBoolValueInput("jointRemoveButton", "Remove", False) + addJointInputButton.isEnabled = removeJointInputButton.isEnabled = True + + jointConfigTable.addToolbarCommandInput(addJointInputButton) + jointConfigTable.addToolbarCommandInput(removeJointInputButton) + except: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.createJointConfigTab()").error( + "Failed:\n{}".format(traceback.format_exc()) + ) + + +def addJointToConfigTab(joint: adsk.fusion.Joint) -> None: + try: + if joint in selectedJointList: + removeJointFromConfigTab(joint) + return + + selectedJointList.append(joint) + commandInputs = jointConfigTable.commandInputs + + if joint.jointMotion.jointType == adsk.fusion.JointTypes.RigidJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Rigid", IconPaths.jointIcons["rigid"]) + icon.tooltip = "Rigid joint" + + elif joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Revolute", IconPaths.jointIcons["revolute"]) + icon.tooltip = "Revolute joint" + + elif joint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Slider", IconPaths.jointIcons["slider"]) + icon.tooltip = "Slider joint" + + elif joint.jointMotion.jointType == adsk.fusion.JointTypes.PlanarJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Planar", IconPaths.jointIcons["planar"]) + icon.tooltip = "Planar joint" + + elif joint.jointMotion.jointType == adsk.fusion.JointTypes.PinSlotJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Pin Slot", IconPaths.jointIcons["pin_slot"]) + icon.tooltip = "Pin slot joint" + + elif joint.jointMotion.jointType == adsk.fusion.JointTypes.CylindricalJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Cylindrical", IconPaths.jointIcons["cylindrical"]) + icon.tooltip = "Cylindrical joint" + + elif joint.jointMotion.jointType == adsk.fusion.JointTypes.BallJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Ball", IconPaths.jointIcons["ball"]) + icon.tooltip = "Ball joint" + + name = commandInputs.addTextBoxCommandInput("name_j", "Occurrence name", "", 1, True) + name.tooltip = joint.name + name.formattedText = f"

{joint.name}

" + + jointType = commandInputs.addDropDownCommandInput( + "jointParent", + "Joint Type", + dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, + ) + + jointType.isFullWidth = True + jointType.listItems.add("Root", True) + + for row in range(1, jointConfigTable.rowCount): # Row is 1 indexed + dropDown = jointConfigTable.getInputAtPosition(row, 2) + dropDown.listItems.add(selectedJointList[-1].name, False) + + for j in range(len(selectedJointList) - 1): + jointType.listItems.add(selectedJointList[j].name, False) + + jointType.tooltip = "Possible parent joints" + jointType.tooltipDescription = "
The root component is usually the parent." + + signalType = commandInputs.addDropDownCommandInput( + "signalType", + "Signal Type", + dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, + ) + signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) + signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) + signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) + signalType.tooltip = "Signal type" + + row = jointConfigTable.rowCount + jointConfigTable.addCommandInput(icon, row, 0) + jointConfigTable.addCommandInput(name, row, 1) + jointConfigTable.addCommandInput(jointType, row, 2) + jointConfigTable.addCommandInput(signalType, row, 3) + + # Joint speed must be added within an `if` because there is variance between different joint types + if joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: + jointSpeed = commandInputs.addValueInput( + "jointSpeed", + "Speed", + "deg", + adsk.core.ValueInput.createByReal(3.1415926), + ) + jointSpeed.tooltip = "Degrees per second" + jointConfigTable.addCommandInput(jointSpeed, row, 4) + + elif joint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: + jointSpeed = commandInputs.addValueInput( + "jointSpeed", + "Speed", + "m", + adsk.core.ValueInput.createByReal(100), + ) + jointSpeed.tooltip = "Meters per second" + jointConfigTable.addCommandInput(jointSpeed, row, 4) + + jointForce = commandInputs.addValueInput("jointForce", "Force", "N", adsk.core.ValueInput.createByReal(5000)) + jointForce.tooltip = "Newtons" + jointConfigTable.addCommandInput(jointForce, row, 5) + except: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.addJointToConfigTab()").error( + "Failed:\n{}".format(traceback.format_exc()) + ) + + +def removeIndexedJointFromConfigTab(index: int) -> None: + try: + removeJointFromConfigTab(selectedJointList[index]) + except: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.removeIndexedJointFromConfigTab()").error( + "Failed:\n{}".format(traceback.format_exc()) + ) + + +def removeJointFromConfigTab(joint: adsk.fusion.Joint) -> None: + try: + i = selectedJointList.index(joint) + selectedJointList.remove(joint) + + jointConfigTable.deleteRow(i + 1) + for row in range(jointConfigTable.rowCount): # Row is 1 indexed + listItems = jointConfigTable.getInputAtPosition(row, 2).listItems + if row > i: + if listItems.item(i + 1).isSelected: + listItems.item(i).isSelected = True + listItems.item(i + 1).deleteMe() + else: + listItems.item(i + 1).deleteMe() + else: + if listItems.item(i).isSelected: + listItems.item(i - 1).isSelected = True + listItems.item(i).deleteMe() + else: + listItems.item(i).deleteMe() + except: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.removeJointFromConfigTab()").error( + "Failed:\n{}".format(traceback.format_exc()) + ) + + +# Converts the current list of selected adsk.fusion.joints into Synthesis.Joints +def getSelectedJoints() -> list[_Joint]: + joints: list[_Joint] = [] + for row in range(1, jointConfigTable.rowCount): # Row is 1 indexed + parentJointIndex = jointConfigTable.getInputAtPosition(row, 2).selectedItem.index + signalTypeIndex = jointConfigTable.getInputAtPosition(row, 3).selectedItem.index + jointSpeed = jointConfigTable.getInputAtPosition(row, 4).value + jointForce = jointConfigTable.getInputAtPosition(row, 5).value + parentJointToken = "" + + if parentJointIndex == 0: + joints.append( + _Joint( + selectedJointList[row - 1].entityToken, # Row is 1 indexed + JointParentType.ROOT, + signalTypeIndex, + jointSpeed, + jointForce / 100.0, + ) + ) + continue + elif parentJointIndex < row: + parentJointToken = selectedJointList[parentJointIndex - 1].entityToken + else: + parentJointToken = selectedJointList[parentJointIndex + 1].entityToken + + joints.append( + _Joint( + selectedJointList[row - 1].entityToken, + parentJointToken, + signalTypeIndex, + jointSpeed, + jointForce, + ) + ) + + return joints + + +def resetSelectedJoints() -> None: + selectedJointList.clear() diff --git a/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py b/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py index 0a1fbadcbf..5c03d0168f 100644 --- a/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py +++ b/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py @@ -1,7 +1,10 @@ -import adsk.core, adsk.fusion, traceback +import adsk.core +import adsk.fusion +import traceback import logging.handlers -# Ripped all the boiler plate from the example code: https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-c90ce6a2-c282-11e6-a365-3417ebc87622 +# Ripped all the boiler plate from the example code: +# https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-c90ce6a2-c282-11e6-a365-3417ebc87622 # global mapping list of event handlers to keep them referenced for the duration of the command # handlers = {} diff --git a/exporter/SynthesisFusionAddin/src/UI/OsHelper.py b/exporter/SynthesisFusionAddin/src/UI/OsHelper.py index 956c9f6fd1..3c006e3113 100644 --- a/exporter/SynthesisFusionAddin/src/UI/OsHelper.py +++ b/exporter/SynthesisFusionAddin/src/UI/OsHelper.py @@ -1,4 +1,5 @@ -import os, platform +import os +import platform def getOSPath(*argv) -> str: @@ -46,7 +47,7 @@ def getOS(): return platform.system() -""" Old code I believe +""" Old code I believe def openFileLocation(fileLoc: str) -> bool: osName = getOS() if osName == "Windows" or osName == "win32": diff --git a/exporter/SynthesisFusionAddin/src/UI/TableUtilities.py b/exporter/SynthesisFusionAddin/src/UI/TableUtilities.py index 9e274e7148..e972f2ae8e 100644 --- a/exporter/SynthesisFusionAddin/src/UI/TableUtilities.py +++ b/exporter/SynthesisFusionAddin/src/UI/TableUtilities.py @@ -14,7 +14,7 @@ def addWheelToTable(wheel: adsk.fusion.Joint) -> None: try: onSelect = gm.handlers[3] wheelTableInput = INPUTS_ROOT.itemById("wheel_table") - + # def addPreselections(child_occurrences): # for occ in child_occurrences: # onSelect.allWheelPreselections.append(occ.entityToken) @@ -22,7 +22,7 @@ def addWheelToTable(wheel: adsk.fusion.Joint) -> None: # if occ.childOccurrences: # addPreselections(occ.childOccurrences) - # if wheel.childOccurrences: + # if wheel.childOccurrences: # addPreselections(wheel.childOccurrences) # else: # onSelect.allWheelPreselections.append(wheel.entityToken) @@ -198,7 +198,7 @@ def addGamepieceToTable(gamepiece: adsk.fusion.Occurrence) -> None: def addPreselections(child_occurrences): for occ in child_occurrences: onSelect.allGamepiecePreselections.append(occ.entityToken) - + if occ.childOccurrences: addPreselections(occ.childOccurrences) @@ -249,7 +249,7 @@ def addPreselections(child_occurrences): "friction_coeff", "", "", valueList ) friction_coeff.valueOne = 0.5 - + type.tooltip = gamepiece.name weight.tooltip = "Weight of field element" @@ -354,7 +354,7 @@ def removeGamePieceFromTable(index: int) -> None: def removePreselections(child_occurrences): for occ in child_occurrences: onSelect.allGamepiecePreselections.remove(occ.entityToken) - + if occ.childOccurrences: removePreselections(occ.childOccurrences) try: From fdb0076181a0c842ce8003497b463c92a0737ec9 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Tue, 25 Jun 2024 14:11:08 -0700 Subject: [PATCH 005/121] Updated transition comments and removed replaced code --- .../src/UI/ConfigCommand.py | 342 +----------------- 1 file changed, 4 insertions(+), 338 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index a682ef8556..b61e42d1e3 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -21,10 +21,6 @@ from .Configuration.SerialCommand import SerialCommand # Transition: AARD-1685 -from .CreateCommandInputsHelper import ( - createTextBoxInput, - createTableInput, -) from .JointConfigTab import ( createJointConfigTab, addJointToConfigTab, @@ -465,148 +461,6 @@ def notify(self, args): ) and not joint.isSuppressed: addJointToConfigTab(joint) - - # Transition AARD-1685: - # ~~~~~~~~~~~~~~~~ JOINT CONFIGURATION ~~~~~~~~~~~~~~~~ - # """ - # Joint configuration group. Container for joint selection table - # """ - # jointConfig = inputs.addGroupCommandInput( - # "joint_config", "Joint Configuration" - # ) - # jointConfig.isExpanded = False - # jointConfig.isVisible = True - # jointConfig.tooltip = "Select and define joint occurrences in your assembly." - - # joint_inputs = jointConfig.children - - # # JOINT SELECTION TABLE - # """ - # All selection joints appear here. - # """ - # jointTableInput = ( - # self.createTableInput( # create tablecommandinput using helper - # "joint_table", - # "Joint Table", - # joint_inputs, - # 6, - # "1:2:2:2:2:2", - # 50, - # ) - # ) - - # addJointInput = joint_inputs.addBoolValueInput( - # "joint_add", "Add", False - # ) # add button - - # removeJointInput = joint_inputs.addBoolValueInput( # remove button - # "joint_delete", "Remove", False - # ) - - # addJointInput.isEnabled = removeJointInput.isEnabled = True - - # addJointInput.tooltip = "Add a joint selection" # tooltips - # removeJointInput.tooltip = "Remove a joint selection" - - # jointSelectInput = joint_inputs.addSelectionInput( - # "joint_select", - # "Selection", - # "Select a joint in your drive-train assembly.", - # ) - - # jointSelectInput.addSelectionFilter("Joints") # only allow joint selection - # jointSelectInput.setSelectionLimits(0) # set no selection count limits - # jointSelectInput.isEnabled = False - # jointSelectInput.isVisible = False # make selection box invisible - - # jointTableInput.addToolbarCommandInput( - # addJointInput - # ) # add bool inputs to the toolbar - # jointTableInput.addToolbarCommandInput( - # removeJointInput - # ) # add bool inputs to the toolbar - - # jointTableInput.addCommandInput( - # self.createTextBoxInput( # create a textBoxCommandInput for the table header (Joint Motion), using helper - # "motion_header", - # "Motion", - # joint_inputs, - # "Motion", - # bold=False, - # ), - # 0, - # 0, - # ) - - # jointTableInput.addCommandInput( - # self.createTextBoxInput( # textBoxCommandInput for table header (Joint Name), using helper - # "name_header", "Name", joint_inputs, "Joint name", bold=False - # ), - # 0, - # 1, - # ) - - # jointTableInput.addCommandInput( - # self.createTextBoxInput( # another header using helper - # "parent_header", - # "Parent", - # joint_inputs, - # "Parent joint", - # background="#d9d9d9", # background color - # ), - # 0, - # 2, - # ) - - # jointTableInput.addCommandInput( - # self.createTextBoxInput( # another header using helper - # "signal_header", - # "Signal", - # joint_inputs, - # "Signal type", - # background="#d9d9d9", # back color - # ), - # 0, - # 3, - # ) - - # jointTableInput.addCommandInput( - # self.createTextBoxInput( # another header using helper - # "speed_header", - # "Speed", - # joint_inputs, - # "Joint Speed", - # background="#d9d9d9", # back color - # ), - # 0, - # 4, - # ) - - # jointTableInput.addCommandInput( - # self.createTextBoxInput( # another header using helper - # "force_header", - # "Force", - # joint_inputs, - # "Joint Force", - # background="#d9d9d9", # back color - # ), - # 0, - # 5, - # ) - - # Transition: AARD-1685 - # for joint in list( - # gm.app.activeDocument.design.rootComponent.allJoints - # ) + list(gm.app.activeDocument.design.rootComponent.allAsBuiltJoints): - # if ( - # joint.jointMotion.jointType == JointMotions.REVOLUTE.value - # or joint.jointMotion.jointType == JointMotions.SLIDER.value - # ) and not joint.isSuppressed: - # # Transition: AARD-1685 - # # addJointToTable(joint) - # addJointToConfigTab(args, joint) - - # ~~~~~~~~~~~~~~~~ GAMEPIECE CONFIGURATION ~~~~~~~~~~~~~~~~ """ Gamepiece group command input, isVisible=False by default @@ -1015,6 +869,7 @@ def notify(self, args): ).error("Failed:\n{}".format(traceback.format_exc())) # Transition: AARD-1685 + # Functionality will be fully moved to `CreateCommandInputsHelper` in AARD-1683 def createBooleanInput( self, _id: str, @@ -1052,6 +907,7 @@ def createBooleanInput( ).error("Failed:\n{}".format(traceback.format_exc())) # Transition: AARD-1685 + # Functionality will be fully moved to `CreateCommandInputsHelper` in AARD-1683 def createTableInput( self, _id: str, @@ -1093,6 +949,7 @@ def createTableInput( ).error("Failed:\n{}".format(traceback.format_exc())) # Transition: AARD-1685 + # Functionality will be fully moved to `CreateCommandInputsHelper` in AARD-1683 def createTextBoxInput( self, _id: str, @@ -1508,9 +1365,6 @@ def notify(self, args): wheelTableInput = wheelTable() - # Transition: AARD-1685 - # jointTableInput = jointTable() - inputs = args.command.commandInputs jointTableInput: adsk.core.TableCommandInput = inputs.itemById("jointSettings").children.itemById("jointTable") @@ -1761,12 +1615,8 @@ def notify(self, args: adsk.core.SelectionEventArgs): WheelListGlobal.index(self.selectedJoint) ) else: - # Transition: AARD-1685 - # if self.selectedJoint not in JointListGlobal: + # Will prompt for removal if already selected. addJointToConfigTab(self.selectedJoint) - # addJointToTable(self.selectedJoint) - # else: - # removeJointFromTable(self.selectedJoint) selectionInput.isEnabled = False selectionInput.isVisible = False @@ -1948,9 +1798,6 @@ def notify(self, args): wheelTableInput = wheelTable() - # Transition: AARD-1685 - # jointTableInput = jointTable() - jointTableInput: adsk.core.TableCommandInput = args.inputs.itemById("jointTable") gamepieceTableInput = gamepieceTable() @@ -2182,11 +2029,6 @@ def notify(self, args): jointTableInput.selectedRow = jointTableInput.rowCount - 1 gm.ui.messageBox("Select a row to delete.") else: - # Transition: AARD-1685 - # joint = JointListGlobal[jointTableInput.selectedRow - 1] - # removeJointFromTable(joint) - # removeJointFromConfigTab(joint) - # Select Row is 1 indexed removeIndexedJointFromConfigTab(jointTableInput.selectedRow - 1) @@ -2482,143 +2324,6 @@ def notify(self, args): ).error("Failed:\n{}".format(traceback.format_exc())) -# Transition: AARD-1685 -# def addJointToTable(joint: adsk.fusion.Joint) -> None: -# """### Adds a Joint object to its global list and joint table. - -# Args: -# joint (adsk.fusion.Joint): Joint object to be added -# """ -# try: -# JointListGlobal.append(joint) -# jointTableInput = jointTable() -# cmdInputs = adsk.core.CommandInputs.cast(jointTableInput.commandInputs) - -# # joint type icons -# if joint.jointMotion.jointType == adsk.fusion.JointTypes.RigidJointType: -# icon = cmdInputs.addImageCommandInput( -# "placeholder", "Rigid", IconPaths.jointIcons["rigid"] -# ) -# icon.tooltip = "Rigid joint" - -# elif joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: -# icon = cmdInputs.addImageCommandInput( -# "placeholder", "Revolute", IconPaths.jointIcons["revolute"] -# ) -# icon.tooltip = "Revolute joint" - -# elif joint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: -# icon = cmdInputs.addImageCommandInput( -# "placeholder", "Slider", IconPaths.jointIcons["slider"] -# ) -# icon.tooltip = "Slider joint" - -# elif joint.jointMotion.jointType == adsk.fusion.JointTypes.PlanarJointType: -# icon = cmdInputs.addImageCommandInput( -# "placeholder", "Planar", IconPaths.jointIcons["planar"] -# ) -# icon.tooltip = "Planar joint" - -# elif joint.jointMotion.jointType == adsk.fusion.JointTypes.PinSlotJointType: -# icon = cmdInputs.addImageCommandInput( -# "placeholder", "Pin Slot", IconPaths.jointIcons["pin_slot"] -# ) -# icon.tooltip = "Pin slot joint" - -# elif joint.jointMotion.jointType == adsk.fusion.JointTypes.CylindricalJointType: -# icon = cmdInputs.addImageCommandInput( -# "placeholder", "Cylindrical", IconPaths.jointIcons["cylindrical"] -# ) -# icon.tooltip = "Cylindrical joint" - -# elif joint.jointMotion.jointType == adsk.fusion.JointTypes.BallJointType: -# icon = cmdInputs.addImageCommandInput( -# "placeholder", "Ball", IconPaths.jointIcons["ball"] -# ) -# icon.tooltip = "Ball joint" - -# # joint name -# name = cmdInputs.addTextBoxCommandInput("name_j", "Occurrence name", "", 1, True) -# name.tooltip = joint.name -# name.formattedText = "

{}

".format(joint.name) - -# jointType = cmdInputs.addDropDownCommandInput( -# "joint_parent", -# "Joint Type", -# dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, -# ) -# jointType.isFullWidth = True -# jointType.listItems.add("Root", True) - -# # after each additional joint added, add joint to the dropdown of all preview rows/joints -# for row in range(jointTableInput.rowCount): -# if row != 0: -# dropDown = jointTableInput.getInputAtPosition(row, 2) -# dropDown.listItems.add(JointListGlobal[-1].name, False) - -# # add all parent joint options to added joint dropdown -# for j in range(len(JointListGlobal) - 1): -# jointType.listItems.add(JointListGlobal[j].name, False) - -# jointType.tooltip = "Possible parent joints" -# jointType.tooltipDescription = "
The root component is usually the parent." - -# signalType = cmdInputs.addDropDownCommandInput( -# "signal_type", -# "Signal Type", -# dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, -# ) -# signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) -# signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) -# signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) -# signalType.tooltip = "Signal type" - -# row = jointTableInput.rowCount - -# jointTableInput.addCommandInput(icon, row, 0) -# jointTableInput.addCommandInput(name, row, 1) -# jointTableInput.addCommandInput(jointType, row, 2) -# jointTableInput.addCommandInput(signalType, row, 3) - -# if joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: -# jointSpeed = cmdInputs.addValueInput( -# "joint_speed", -# "Speed", -# "deg", -# adsk.core.ValueInput.createByReal(3.1415926), -# ) -# jointSpeed.tooltip = "Degrees per second" -# jointTableInput.addCommandInput(jointSpeed, row, 4) - -# jointForce = cmdInputs.addValueInput( -# "joint_force", "Force", "N", adsk.core.ValueInput.createByReal(5000) -# ) -# jointForce.tooltip = "Newton-Meters***" -# jointTableInput.addCommandInput(jointForce, row, 5) - -# if joint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: -# jointSpeed = cmdInputs.addValueInput( -# "joint_speed", -# "Speed", -# "m", -# adsk.core.ValueInput.createByReal(100), -# ) -# jointSpeed.tooltip = "Meters per second" -# jointTableInput.addCommandInput(jointSpeed, row, 4) - -# jointForce = cmdInputs.addValueInput( -# "joint_force", "Force", "N", adsk.core.ValueInput.createByReal(5000) -# ) -# jointForce.tooltip = "Newtons" -# jointTableInput.addCommandInput(jointForce, row, 5) - -# except: -# gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) - # logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.addJointToTable()").error( - # "Failed:\n{}".format(traceback.format_exc()) - # ) - - def addWheelToTable(wheel: adsk.fusion.Joint) -> None: """### Adds a wheel occurrence to its global list and wheel table. @@ -2809,45 +2514,6 @@ def removeWheelFromTable(index: int) -> None: ) -# Transition: AARD-1685 -# def removeJointFromTable(joint: adsk.fusion.Joint) -> None: -# """### Removes a joint occurrence from its global list and joint table. - -# Args: -# joint (adsk.fusion.Joint): Joint object to be removed -# """ -# try: -# index = JointListGlobal.index(joint) -# jointTableInput = jointTable() -# JointListGlobal.remove(joint) - -# jointTableInput.deleteRow(index + 1) - -# for row in range(jointTableInput.rowCount): -# if row == 0: -# continue - -# dropDown = jointTableInput.getInputAtPosition(row, 2) -# listItems = dropDown.listItems - -# if row > index: -# if listItems.item(index + 1).isSelected: -# listItems.item(index).isSelected = True -# listItems.item(index + 1).deleteMe() -# else: -# listItems.item(index + 1).deleteMe() -# else: -# if listItems.item(index).isSelected: -# listItems.item(index - 1).isSelected = True -# listItems.item(index).deleteMe() -# else: -# listItems.item(index).deleteMe() -# except: -# logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.removeJointFromTable()").error( -# "Failed:\n{}".format(traceback.format_exc()) -# ) - - def removeGamePieceFromTable(index: int) -> None: """### Removes a gamepiece occurrence from its global list and gamepiece table. From 837d83cb9e7a0e38bfccbd85ff949a40f42a4740 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Tue, 25 Jun 2024 14:43:57 -0700 Subject: [PATCH 006/121] Handled transition tags --- .../src/UI/ConfigCommand.py | 200 +----------------- 1 file changed, 4 insertions(+), 196 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 1a940c87f2..b2062cd340 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -419,134 +419,6 @@ def notify(self, args): )[0] addWheelToTable(wheelEntity) - # Transition: AARD-1685 - # ~~~~~~~~~~~~~~~~ JOINT CONFIGURATION ~~~~~~~~~~~~~~~~ - """ - Joint configuration group. Container for joint selection table - """ - # jointConfig = inputs.addGroupCommandInput( - # "joint_config", "Joint Configuration" - # ) - # jointConfig.isExpanded = False - # jointConfig.isVisible = True - # jointConfig.tooltip = "Select and define joint occurrences in your assembly." - - # joint_inputs = jointConfig.children - - # # JOINT SELECTION TABLE - # """ - # All selection joints appear here. - # """ - # jointTableInput = ( - # self.createTableInput( # create tablecommandinput using helper - # "joint_table", - # "Joint Table", - # joint_inputs, - # 6, - # "1:2:2:2:2:2", - # 50, - # ) - # ) - - # addJointInput = joint_inputs.addBoolValueInput( - # "joint_add", "Add", False - # ) # add button - - # removeJointInput = joint_inputs.addBoolValueInput( # remove button - # "joint_delete", "Remove", False - # ) - - # addJointInput.isEnabled = removeJointInput.isEnabled = True - - # addJointInput.tooltip = "Add a joint selection" # tooltips - # removeJointInput.tooltip = "Remove a joint selection" - - # jointSelectInput = joint_inputs.addSelectionInput( - # "joint_select", - # "Selection", - # "Select a joint in your drive-train assembly.", - # ) - - # jointSelectInput.addSelectionFilter("Joints") # only allow joint selection - # jointSelectInput.setSelectionLimits(0) # set no selection count limits - # jointSelectInput.isEnabled = False - # jointSelectInput.isVisible = False # make selection box invisible - - # jointTableInput.addToolbarCommandInput( - # addJointInput - # ) # add bool inputs to the toolbar - # jointTableInput.addToolbarCommandInput( - # removeJointInput - # ) # add bool inputs to the toolbar - - # jointTableInput.addCommandInput( - # self.createTextBoxInput( # create a textBoxCommandInput for the table header (Joint Motion), using helper - # "motion_header", - # "Motion", - # joint_inputs, - # "Motion", - # bold=False, - # ), - # 0, - # 0, - # ) - - # jointTableInput.addCommandInput( - # self.createTextBoxInput( # textBoxCommandInput for table header (Joint Name), using helper - # "name_header", "Name", joint_inputs, "Joint name", bold=False - # ), - # 0, - # 1, - # ) - - # jointTableInput.addCommandInput( - # self.createTextBoxInput( # another header using helper - # "parent_header", - # "Parent", - # joint_inputs, - # "Parent joint", - # background="#d9d9d9", # background color - # ), - # 0, - # 2, - # ) - - # jointTableInput.addCommandInput( - # self.createTextBoxInput( # another header using helper - # "signal_header", - # "Signal", - # joint_inputs, - # "Signal type", - # background="#d9d9d9", # back color - # ), - # 0, - # 3, - # ) - - # jointTableInput.addCommandInput( - # self.createTextBoxInput( # another header using helper - # "speed_header", - # "Speed", - # joint_inputs, - # "Joint Speed", - # background="#d9d9d9", # back color - # ), - # 0, - # 4, - # ) - - # jointTableInput.addCommandInput( - # self.createTextBoxInput( # another header using helper - # "force_header", - # "Force", - # joint_inputs, - # "Joint Force", - # background="#d9d9d9", # back color - # ), - # 0, - # 5, - # ) - # Transition: AARD-1685 createJointConfigTab(args) for joint in list( @@ -1129,9 +1001,7 @@ def notify(self, args): name = design.rootComponent.name.rsplit(" ", 1)[0] version = design.rootComponent.name.rsplit(" ", 1)[1] - # Transition: AARD-1685 _exportWheels = [] # all selected wheels, formatted for parseOptions - _exportJoints = [] # all selected joints, formatted for parseOptions _exportGamepieces = [] # TODO work on the code to populate Gamepiece _robotWeight = float _mode = ExportMode.ROBOT @@ -1162,71 +1032,6 @@ def notify(self, args): ) ) - # Transition: AARD-1685 - _exportJoints = getSelectedJoints() - - # Transition: AARD-1685 - """ - Loops through all rows in the joint table to extract the input values - """ - # jointTableInput = jointTable() - # for row in range(jointTableInput.rowCount): - # if row == 0: - # continue - - # parentJointIndex = jointTableInput.getInputAtPosition( - # row, 2 - # ).selectedItem.index # parent joint index, int - - # signalTypeIndex = jointTableInput.getInputAtPosition( - # row, 3 - # ).selectedItem.index # signal type index, int - - # # typeString = jointTableInput.getInputAtPosition( - # # row, 0 - # # ).name - - # jointSpeed = jointTableInput.getInputAtPosition(row, 4).value - - # jointForce = jointTableInput.getInputAtPosition(row, 5).value - - # parentJointToken = "" - - # if parentJointIndex == 0: - # _exportJoints.append( - # _Joint( - # JointListGlobal[row - 1].entityToken, - # JointParentType.ROOT, - # signalTypeIndex, # index of selected signal in dropdown - # jointSpeed, - # jointForce / 100.0, - # ) # parent joint GUID - # ) - # continue - # elif parentJointIndex < row: - # parentJointToken = JointListGlobal[ - # parentJointIndex - 1 - # ].entityToken # parent joint GUID, str - # else: - # parentJointToken = JointListGlobal[ - # parentJointIndex + 1 - # ].entityToken # parent joint GUID, str - - # # for wheel in _exportWheels: - # # find some way to get joint - # # 1. Compare Joint occurrence1 to wheel.occurrence_token - # # 2. if true set the parent to Root - - # _exportJoints.append( - # _Joint( - # JointListGlobal[row - 1].entityToken, - # parentJointToken, - # signalTypeIndex, - # jointSpeed, - # jointForce, - # ) - # ) - """ Loops through all rows in the gamepiece table to extract the input values """ @@ -1289,7 +1094,7 @@ def notify(self, args): name, version, materials=0, - joints=_exportJoints, + joints=getSelectedJoints(), wheels=_exportWheels, gamepieces=_exportGamepieces, preferredUnits=selectedUnits, @@ -1972,6 +1777,7 @@ def notify(self, args): addWheelInput.isEnabled = False # Transition: AARD-1685 + # Functionality could potentially be moved into `JointConfigTab.py` elif cmdInput.id == "jointAddButton": self.reset() @@ -2001,6 +1807,8 @@ def notify(self, args): index = wheelTableInput.selectedRow - 1 removeWheelFromTable(index) + # Transition: AARD-1685 + # Functionality could potentially be moved into `JointConfigTab.py` elif cmdInput.id == "jointRemoveButton": gm.ui.activeSelections.clear() From 90f96c923f32af0a127a45c393490a5cee2e29d3 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Tue, 25 Jun 2024 15:12:08 -0700 Subject: [PATCH 007/121] Readabliity cleanups --- .../src/Parser/ExporterOptions.py | 2 +- .../src/UI/ConfigCommand.py | 25 ++++++++++--------- .../src/UI/JointConfigTab.py | 6 ++--- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py index 65dfbab62c..0f56b4338f 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py +++ b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py @@ -102,7 +102,7 @@ class ExporterOptions: default=CalculationAccuracy.LowCalculationAccuracy ) - def readFromDesign(self) -> None: + def readFromDesign(self) -> "ExporterOptions": designAttributes = adsk.core.Application.get().activeProduct.attributes for field in fields(self): attribute = designAttributes.itemByName(INTERNAL_ID, field.name) diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index b2062cd340..5a6e8fb480 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -14,16 +14,15 @@ Gamepiece, ExportMode, ExporterOptions, - Joint, Wheel, WheelType, SignalType, - JointParentType, PreferredUnits, ) from .Configuration.SerialCommand import SerialCommand # Transition: AARD-1685 +# In the future all components should be handled in this way. from .JointConfigTab import ( createJointConfigTab, addJointToConfigTab, @@ -414,20 +413,22 @@ def notify(self, args): if exporterOptions.wheels: for wheel in exporterOptions.wheels: - wheelEntity = gm.app.activeDocument.design.findEntityByToken( - wheel.jointToken - )[0] + wheelEntity = gm.app.activeDocument.design.findEntityByToken(wheel.jointToken)[0] addWheelToTable(wheelEntity) # Transition: AARD-1685 createJointConfigTab(args) - for joint in list( - gm.app.activeDocument.design.rootComponent.allJoints - ) + list(gm.app.activeDocument.design.rootComponent.allAsBuiltJoints): - if (joint.jointMotion.jointType == JointMotions.REVOLUTE.value - or joint.jointMotion.jointType == JointMotions.SLIDER.value - ) and not joint.isSuppressed: - addJointToConfigTab(joint) + if exporterOptions.joints: + for joint in exporterOptions.joints: + jointEntity = gm.app.activeDocument.design.findEntityByToken(joint.jointToken)[0] + addJointToConfigTab(jointEntity) + else: + for joint in [ + *gm.app.activeDocument.design.rootComponent.allJoints, + *gm.app.activeDocument.design.rootComponent.allAsBuiltJoints + ]: + if joint.jointMotion.jointType in (JointMotions.REVOLUTE.value, JointMotions.SLIDER.value) and not joint.isSuppressed: + addJointToConfigTab(joint) # ~~~~~~~~~~~~~~~~ GAMEPIECE CONFIGURATION ~~~~~~~~~~~~~~~~ """ diff --git a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py index 27e342149a..03b111e93e 100644 --- a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py @@ -175,8 +175,8 @@ def addJointToConfigTab(joint: adsk.fusion.Joint) -> None: dropDown = jointConfigTable.getInputAtPosition(row, 2) dropDown.listItems.add(selectedJointList[-1].name, False) - for j in range(len(selectedJointList) - 1): - jointType.listItems.add(selectedJointList[j].name, False) + for joint in selectedJointList: + jointType.listItems.add(joint.name, False) jointType.tooltip = "Possible parent joints" jointType.tooltipDescription = "
The root component is usually the parent." @@ -262,7 +262,7 @@ def removeJointFromConfigTab(joint: adsk.fusion.Joint) -> None: ) -# Converts the current list of selected adsk.fusion.joints into Synthesis.Joints +# Converts the current list of selected adsk.fusion.joints into list[Synthesis.Joint] def getSelectedJoints() -> list[Joint]: joints: list[Joint] = [] for row in range(1, jointConfigTable.rowCount): # Row is 1 indexed From f77570ecececb437bc2de3a7b9421e601e880362 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Tue, 25 Jun 2024 17:29:15 -0700 Subject: [PATCH 008/121] Load saved joint settings --- .../src/UI/ConfigCommand.py | 20 ++--- .../src/UI/JointConfigTab.py | 73 +++++++++++++------ 2 files changed, 56 insertions(+), 37 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 5a6e8fb480..b921f306eb 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -50,11 +50,9 @@ """ These lists are crucial, and contain all of the relevant object selections. - WheelListGlobal: list of wheels (adsk.fusion.Occurrence) -- JointListGlobal: list of joints (adsk.fusion.Joint) - GamepieceListGlobal: list of gamepieces (adsk.fusion.Occurrence) """ WheelListGlobal = [] -JointListGlobal = [] GamepieceListGlobal = [] # Default to compressed files @@ -177,7 +175,6 @@ def __init__(self, configure): def notify(self, args): try: exporterOptions = ExporterOptions().readFromDesign() - # exporterOptions = ExporterOptions() if not Helper.check_solid_open(): return @@ -295,12 +292,8 @@ def notify(self, args): adsk.core.DropDownStyles.LabeledIconDropDownStyle, ) - weight_unit.listItems.add( - "‎", imperialUnits, IconPaths.massIcons["LBS"] - ) # add listdropdown mass options - weight_unit.listItems.add( - "‎", not imperialUnits, IconPaths.massIcons["KG"] - ) # add listdropdown mass options + weight_unit.listItems.add("‎", imperialUnits, IconPaths.massIcons["LBS"]) + weight_unit.listItems.add("‎", not imperialUnits, IconPaths.massIcons["KG"]) weight_unit.tooltip = "Unit of mass" weight_unit.tooltipDescription = ( "
Configure the unit of mass for the weight calculation." @@ -419,9 +412,9 @@ def notify(self, args): # Transition: AARD-1685 createJointConfigTab(args) if exporterOptions.joints: - for joint in exporterOptions.joints: - jointEntity = gm.app.activeDocument.design.findEntityByToken(joint.jointToken)[0] - addJointToConfigTab(jointEntity) + for synJoint in exporterOptions.joints: + fusionJoint = gm.app.activeDocument.design.findEntityByToken(synJoint.jointToken)[0] + addJointToConfigTab(fusionJoint, synJoint) else: for joint in [ *gm.app.activeDocument.design.rootComponent.allJoints, @@ -954,7 +947,6 @@ def __init__(self): super().__init__() self.log = logging.getLogger(f"{INTERNAL_ID}.UI.{self.__class__.__name__}") self.current = SerialCommand() - self.designAttrs = adsk.core.Application.get().activeProduct.attributes def notify(self, args): try: @@ -1967,7 +1959,7 @@ def notify(self, args): onSelect = gm.handlers[3] WheelListGlobal.clear() - JointListGlobal.clear() + resetSelectedJoints() GamepieceListGlobal.clear() onSelect.allWheelPreselections.clear() onSelect.wheelJointList.clear() diff --git a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py index 03b111e93e..891ad08fc4 100644 --- a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py @@ -121,46 +121,46 @@ def createJointConfigTab(args: adsk.core.CommandCreatedEventArgs) -> None: ) -def addJointToConfigTab(joint: adsk.fusion.Joint) -> None: +def addJointToConfigTab(fusionJoint: adsk.fusion.Joint, synJoint: Joint = None) -> None: try: - if joint in selectedJointList: - removeJointFromConfigTab(joint) + if fusionJoint in selectedJointList: + removeJointFromConfigTab(fusionJoint) return - selectedJointList.append(joint) + selectedJointList.append(fusionJoint) commandInputs = jointConfigTable.commandInputs - if joint.jointMotion.jointType == adsk.fusion.JointTypes.RigidJointType: + if fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.RigidJointType: icon = commandInputs.addImageCommandInput("placeholder", "Rigid", IconPaths.jointIcons["rigid"]) icon.tooltip = "Rigid joint" - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: icon = commandInputs.addImageCommandInput("placeholder", "Revolute", IconPaths.jointIcons["revolute"]) icon.tooltip = "Revolute joint" - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: icon = commandInputs.addImageCommandInput("placeholder", "Slider", IconPaths.jointIcons["slider"]) icon.tooltip = "Slider joint" - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.PlanarJointType: + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.PlanarJointType: icon = commandInputs.addImageCommandInput("placeholder", "Planar", IconPaths.jointIcons["planar"]) icon.tooltip = "Planar joint" - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.PinSlotJointType: + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.PinSlotJointType: icon = commandInputs.addImageCommandInput("placeholder", "Pin Slot", IconPaths.jointIcons["pin_slot"]) icon.tooltip = "Pin slot joint" - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.CylindricalJointType: + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.CylindricalJointType: icon = commandInputs.addImageCommandInput("placeholder", "Cylindrical", IconPaths.jointIcons["cylindrical"]) icon.tooltip = "Cylindrical joint" - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.BallJointType: + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.BallJointType: icon = commandInputs.addImageCommandInput("placeholder", "Ball", IconPaths.jointIcons["ball"]) icon.tooltip = "Ball joint" name = commandInputs.addTextBoxCommandInput("name_j", "Occurrence name", "", 1, True) - name.tooltip = joint.name - name.formattedText = f"

{joint.name}

" + name.tooltip = fusionJoint.name + name.formattedText = f"

{fusionJoint.name}

" jointType = commandInputs.addDropDownCommandInput( "jointParent", @@ -169,14 +169,17 @@ def addJointToConfigTab(joint: adsk.fusion.Joint) -> None: ) jointType.isFullWidth = True + + # Transition: AARD-1685 + # Implementation of joint parent system needs to be revisited. jointType.listItems.add("Root", True) for row in range(1, jointConfigTable.rowCount): # Row is 1 indexed dropDown = jointConfigTable.getInputAtPosition(row, 2) dropDown.listItems.add(selectedJointList[-1].name, False) - for joint in selectedJointList: - jointType.listItems.add(joint.name, False) + for fusionJoint in selectedJointList: + jointType.listItems.add(fusionJoint.name, False) jointType.tooltip = "Possible parent joints" jointType.tooltipDescription = "
The root component is usually the parent." @@ -186,9 +189,17 @@ def addJointToConfigTab(joint: adsk.fusion.Joint) -> None: "Signal Type", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, ) - signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) + + # TODO: Make this better, this is bad bad bad - Brandon + if synJoint: + signalType.listItems.add("‎", synJoint.signalType is SignalType.PWM, IconPaths.signalIcons["PWM"]) + signalType.listItems.add("‎", synJoint.signalType is SignalType.CAN, IconPaths.signalIcons["CAN"]) + signalType.listItems.add("‎", synJoint.signalType is SignalType.PASSIVE, IconPaths.signalIcons["PASSIVE"]) + else: + signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) + signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) + signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) + signalType.tooltip = "Signal type" row = jointConfigTable.rowCount @@ -198,27 +209,43 @@ def addJointToConfigTab(joint: adsk.fusion.Joint) -> None: jointConfigTable.addCommandInput(signalType, row, 3) # Joint speed must be added within an `if` because there is variance between different joint types - if joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: + # Comparison by `==` over `is` because the Autodesk API does not use `Enum` for their enum classes + if fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: + if synJoint: + jointSpeedValue = synJoint.speed + else: + jointSpeedValue = 3.1415926 + jointSpeed = commandInputs.addValueInput( "jointSpeed", "Speed", "deg", - adsk.core.ValueInput.createByReal(3.1415926), + adsk.core.ValueInput.createByReal(jointSpeedValue), ) jointSpeed.tooltip = "Degrees per second" jointConfigTable.addCommandInput(jointSpeed, row, 4) - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: + if synJoint: + jointSpeedValue = synJoint.speed + else: + jointSpeedValue = 100 + jointSpeed = commandInputs.addValueInput( "jointSpeed", "Speed", "m", - adsk.core.ValueInput.createByReal(100), + adsk.core.ValueInput.createByReal(jointSpeedValue), ) jointSpeed.tooltip = "Meters per second" jointConfigTable.addCommandInput(jointSpeed, row, 4) - jointForce = commandInputs.addValueInput("jointForce", "Force", "N", adsk.core.ValueInput.createByReal(5000)) + if synJoint: + jointForceValue = synJoint.force * 100 # Currently a factor of 100 - Should be investigated + else: + jointForceValue = 5 + + jointForce = commandInputs.addValueInput("jointForce", "Force", "N", adsk.core.ValueInput.createByReal(jointForceValue)) jointForce.tooltip = "Newtons" jointConfigTable.addCommandInput(jointForce, row, 5) except: From 57d6b931a8d24a361cbeeea71b371f7ba44d3910 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Tue, 25 Jun 2024 17:40:54 -0700 Subject: [PATCH 009/121] Restored unintentional formatting changes --- exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py | 7 ++----- exporter/SynthesisFusionAddin/src/UI/OsHelper.py | 5 ++--- exporter/SynthesisFusionAddin/src/UI/TableUtilities.py | 10 +++++----- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py b/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py index 5c03d0168f..0a1fbadcbf 100644 --- a/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py +++ b/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py @@ -1,10 +1,7 @@ -import adsk.core -import adsk.fusion -import traceback +import adsk.core, adsk.fusion, traceback import logging.handlers -# Ripped all the boiler plate from the example code: -# https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-c90ce6a2-c282-11e6-a365-3417ebc87622 +# Ripped all the boiler plate from the example code: https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-c90ce6a2-c282-11e6-a365-3417ebc87622 # global mapping list of event handlers to keep them referenced for the duration of the command # handlers = {} diff --git a/exporter/SynthesisFusionAddin/src/UI/OsHelper.py b/exporter/SynthesisFusionAddin/src/UI/OsHelper.py index 3c006e3113..956c9f6fd1 100644 --- a/exporter/SynthesisFusionAddin/src/UI/OsHelper.py +++ b/exporter/SynthesisFusionAddin/src/UI/OsHelper.py @@ -1,5 +1,4 @@ -import os -import platform +import os, platform def getOSPath(*argv) -> str: @@ -47,7 +46,7 @@ def getOS(): return platform.system() -""" Old code I believe +""" Old code I believe def openFileLocation(fileLoc: str) -> bool: osName = getOS() if osName == "Windows" or osName == "win32": diff --git a/exporter/SynthesisFusionAddin/src/UI/TableUtilities.py b/exporter/SynthesisFusionAddin/src/UI/TableUtilities.py index e972f2ae8e..9e274e7148 100644 --- a/exporter/SynthesisFusionAddin/src/UI/TableUtilities.py +++ b/exporter/SynthesisFusionAddin/src/UI/TableUtilities.py @@ -14,7 +14,7 @@ def addWheelToTable(wheel: adsk.fusion.Joint) -> None: try: onSelect = gm.handlers[3] wheelTableInput = INPUTS_ROOT.itemById("wheel_table") - + # def addPreselections(child_occurrences): # for occ in child_occurrences: # onSelect.allWheelPreselections.append(occ.entityToken) @@ -22,7 +22,7 @@ def addWheelToTable(wheel: adsk.fusion.Joint) -> None: # if occ.childOccurrences: # addPreselections(occ.childOccurrences) - # if wheel.childOccurrences: + # if wheel.childOccurrences: # addPreselections(wheel.childOccurrences) # else: # onSelect.allWheelPreselections.append(wheel.entityToken) @@ -198,7 +198,7 @@ def addGamepieceToTable(gamepiece: adsk.fusion.Occurrence) -> None: def addPreselections(child_occurrences): for occ in child_occurrences: onSelect.allGamepiecePreselections.append(occ.entityToken) - + if occ.childOccurrences: addPreselections(occ.childOccurrences) @@ -249,7 +249,7 @@ def addPreselections(child_occurrences): "friction_coeff", "", "", valueList ) friction_coeff.valueOne = 0.5 - + type.tooltip = gamepiece.name weight.tooltip = "Weight of field element" @@ -354,7 +354,7 @@ def removeGamePieceFromTable(index: int) -> None: def removePreselections(child_occurrences): for occ in child_occurrences: onSelect.allGamepiecePreselections.remove(occ.entityToken) - + if occ.childOccurrences: removePreselections(occ.childOccurrences) try: From 5adcd67edf1a88fbcb06adc425b4c4b0e038c9ed Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Wed, 26 Jun 2024 09:03:44 -0700 Subject: [PATCH 010/121] Added confirm joint removal prompt --- .../SynthesisFusionAddin/src/UI/ConfigCommand.py | 16 ++++++++++++++-- .../src/UI/JointConfigTab.py | 7 ++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index b921f306eb..7a13b9687a 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -26,6 +26,7 @@ from .JointConfigTab import ( createJointConfigTab, addJointToConfigTab, + removeJointFromConfigTab, removeIndexedJointFromConfigTab, getSelectedJoints, resetSelectedJoints @@ -1395,8 +1396,19 @@ def notify(self, args: adsk.core.SelectionEventArgs): WheelListGlobal.index(self.selectedJoint) ) else: - # Will prompt for removal if already selected. - addJointToConfigTab(self.selectedJoint) + # Transition: AARD-1685 + # Should move selection handles into respective UI modules + if not addJointToConfigTab(self.selectedJoint): + result = gm.ui.messageBox( + "You have already selected this joint.\n" + "Would you like to remove it?", + "Synthesis: Remove Joint Confirmation", + adsk.core.MessageBoxButtonTypes.YesNoButtonType, + adsk.core.MessageBoxIconTypes.QuestionIconType, + ) + + if result == adsk.core.DialogResults.DialogYes: + removeJointFromConfigTab(self.selectedJoint) selectionInput.isEnabled = False selectionInput.isVisible = False diff --git a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py index 891ad08fc4..ff85fe5158 100644 --- a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py @@ -121,11 +121,10 @@ def createJointConfigTab(args: adsk.core.CommandCreatedEventArgs) -> None: ) -def addJointToConfigTab(fusionJoint: adsk.fusion.Joint, synJoint: Joint = None) -> None: +def addJointToConfigTab(fusionJoint: adsk.fusion.Joint, synJoint: Joint = None) -> bool: try: if fusionJoint in selectedJointList: - removeJointFromConfigTab(fusionJoint) - return + return False selectedJointList.append(fusionJoint) commandInputs = jointConfigTable.commandInputs @@ -253,6 +252,8 @@ def addJointToConfigTab(fusionJoint: adsk.fusion.Joint, synJoint: Joint = None) "Failed:\n{}".format(traceback.format_exc()) ) + return True + def removeIndexedJointFromConfigTab(index: int) -> None: try: From 7c2c2a936c5fc59753bd93c358314e7c485dfd4a Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Wed, 26 Jun 2024 10:26:43 -0700 Subject: [PATCH 011/121] Added joint select cancel button --- .../src/UI/ConfigCommand.py | 43 +++++++++++-------- .../src/UI/JointConfigTab.py | 4 ++ 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 7a13b9687a..3af0c1ef7b 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -1134,7 +1134,8 @@ def notify(self, args): auto_calc_weight_f = INPUTS_ROOT.itemById("auto_calc_weight_f") removeWheelInput = INPUTS_ROOT.itemById("wheel_delete") - removeJointInput = INPUTS_ROOT.itemById("jointRemoveButton") + jointRemoveButton = INPUTS_ROOT.itemById("jointRemoveButton") + jointSelection = INPUTS_ROOT.itemById("jointSelection") removeFieldInput = INPUTS_ROOT.itemById("field_delete") addWheelInput = INPUTS_ROOT.itemById("wheel_add") @@ -1154,9 +1155,9 @@ def notify(self, args): removeWheelInput.isEnabled = True if jointTableInput.rowCount <= 1: - removeJointInput.isEnabled = False - else: - removeJointInput.isEnabled = True + jointRemoveButton.isEnabled = False + elif not jointSelection.isEnabled: + jointRemoveButton.isEnabled = True if gamepieceTableInput.rowCount <= 1: removeFieldInput.isEnabled = auto_calc_weight_f.isEnabled = False @@ -1177,7 +1178,7 @@ def notify(self, args): group.deleteMe() if ( - not addJointInput.isEnabled or not removeJointInput + not addJointInput.isEnabled or not jointRemoveButton.isEnabled ): # TODO: improve joint highlighting # for joint in JointListGlobal: # CustomGraphics.highlightJointedOccurrences(joint) @@ -1585,7 +1586,8 @@ def notify(self, args): frictionCoeff = INPUTS_ROOT.itemById("friction_coeff_override") wheelSelect = inputs.itemById("wheel_select") - jointSelect = inputs.itemById("jointSelection") + jointSelection = inputs.itemById("jointSelection") + jointSelectCancelButton = INPUTS_ROOT.itemById("jointSelectCancelButton") gamepieceSelect = inputs.itemById("gamepiece_select") wheelTableInput = wheelTable() @@ -1602,7 +1604,8 @@ def notify(self, args): gamepieceConfig = inputs.itemById("gamepiece_config") addWheelInput = INPUTS_ROOT.itemById("wheel_add") - addJointInput = INPUTS_ROOT.itemById("jointAddButton") + addJointButton = INPUTS_ROOT.itemById("jointAddButton") + removeJointButton = INPUTS_ROOT.itemById("jointRemoveButton") addFieldInput = INPUTS_ROOT.itemById("field_add") indicator = INPUTS_ROOT.itemById("algorithmic_indicator") @@ -1631,7 +1634,7 @@ def notify(self, args): gm.ui.activeSelections.clear() gm.app.activeDocument.design.rootComponent.opacity = 1 - addWheelInput.isEnabled = addJointInput.isEnabled = ( + addWheelInput.isEnabled = addJointButton.isEnabled = ( gamepieceConfig.isVisible ) = True @@ -1685,8 +1688,8 @@ def notify(self, args): or cmdInput.id == "signal_type" ): self.reset() - jointSelect.isEnabled = False - addJointInput.isEnabled = True + jointSelection.isEnabled = False + addJointButton.isEnabled = True elif ( cmdInput.id == "blank_gp" @@ -1778,7 +1781,7 @@ def notify(self, args): wheelSelect.isVisible = True wheelSelect.isEnabled = True wheelSelect.clearSelection() - addJointInput.isEnabled = True + addJointButton.isEnabled = True addWheelInput.isEnabled = False # Transition: AARD-1685 @@ -1787,10 +1790,10 @@ def notify(self, args): self.reset() addWheelInput.isEnabled = True - jointSelect.isVisible = True - jointSelect.isEnabled = True - jointSelect.clearSelection() - addJointInput.isEnabled = False + jointSelection.isVisible = jointSelection.isEnabled = True + jointSelection.clearSelection() + addJointButton.isEnabled = removeJointButton.isEnabled = False + jointSelectCancelButton.isVisible = jointSelectCancelButton.isEnabled = True elif cmdInput.id == "field_add": self.reset() @@ -1817,7 +1820,7 @@ def notify(self, args): elif cmdInput.id == "jointRemoveButton": gm.ui.activeSelections.clear() - addJointInput.isEnabled = True + addJointButton.isEnabled = True addWheelInput.isEnabled = True if jointTableInput.selectedRow == -1 or jointTableInput.selectedRow == 0: @@ -1846,7 +1849,13 @@ def notify(self, args): addWheelInput.isEnabled = True elif cmdInput.id == "jointSelection": - addJointInput.isEnabled = True + addJointButton.isEnabled = removeJointButton.isEnabled = True + jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False + + elif cmdInput.id == "jointSelectCancelButton": + jointSelection.isEnabled = jointSelection.isVisible = False + jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False + addJointButton.isEnabled = removeJointButton.isEnabled = True elif cmdInput.id == "gamepiece_select": addFieldInput.isEnabled = True diff --git a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py index ff85fe5158..ed2f4a7d37 100644 --- a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py @@ -109,12 +109,16 @@ def createJointConfigTab(args: adsk.core.CommandCreatedEventArgs) -> None: # Visibility is triggered by `addJointInputButton` jointSelect.isEnabled = jointSelect.isVisible = False + jointSelectCancelButton = jointConfigTabInputs.addBoolValueInput("jointSelectCancelButton", "Cancel", False) + jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False + addJointInputButton = jointConfigTabInputs.addBoolValueInput("jointAddButton", "Add", False) removeJointInputButton = jointConfigTabInputs.addBoolValueInput("jointRemoveButton", "Remove", False) addJointInputButton.isEnabled = removeJointInputButton.isEnabled = True jointConfigTable.addToolbarCommandInput(addJointInputButton) jointConfigTable.addToolbarCommandInput(removeJointInputButton) + jointConfigTable.addToolbarCommandInput(jointSelectCancelButton) except: logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.createJointConfigTab()").error( "Failed:\n{}".format(traceback.format_exc()) From 818e784083636524575c7f5a205b66e196988aa2 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Thu, 27 Jun 2024 11:14:45 -0700 Subject: [PATCH 012/121] Created new table under joint select that autopopulates with selected wheels --- .../src/Parser/ExporterOptions.py | 29 +- .../src/UI/ConfigCommand.py | 523 +++++++++--------- .../src/UI/CreateCommandInputsHelper.py | 36 +- .../src/UI/JointConfigTab.py | 187 ++++++- 4 files changed, 480 insertions(+), 295 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py index 0f56b4338f..4f7983ee4d 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py +++ b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py @@ -39,6 +39,12 @@ class Joint: speed: float = field(default=None) force: float = field(default=None) + # Transition: AARD-1865 + # Should consider changing how the parser handles wheels and joints as there is overlap between + # `Joint` and `Wheel` that should be avoided + # This overlap also presents itself in 'ConfigCommand.py' and 'JointConfigTab.py' + isWheel: bool = field(default=False) + @dataclass class Gamepiece: @@ -103,17 +109,20 @@ class ExporterOptions: ) def readFromDesign(self) -> "ExporterOptions": - designAttributes = adsk.core.Application.get().activeProduct.attributes - for field in fields(self): - attribute = designAttributes.itemByName(INTERNAL_ID, field.name) - if attribute: - setattr( - self, - field.name, - self._makeObjectFromJson(field.type, json.loads(attribute.value)), - ) + try: + designAttributes = adsk.core.Application.get().activeProduct.attributes + for field in fields(self): + attribute = designAttributes.itemByName(INTERNAL_ID, field.name) + if attribute: + setattr( + self, + field.name, + self._makeObjectFromJson(field.type, json.loads(attribute.value)), + ) - return self + return self + except: + return ExporterOptions() def writeToDesign(self) -> None: designAttributes = adsk.core.Application.get().activeProduct.attributes diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 3af0c1ef7b..9f59245836 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -26,9 +26,10 @@ from .JointConfigTab import ( createJointConfigTab, addJointToConfigTab, + addWheelToConfigTab, removeJointFromConfigTab, removeIndexedJointFromConfigTab, - getSelectedJoints, + getSelectedJointsAndWheels, resetSelectedJoints ) @@ -76,22 +77,23 @@ def GUID(arg): return arg.entityToken -def wheelTable(): - """### Returns the wheel table command input +# Transition: AARD-1865 +# def wheelTable(): +# """### Returns the wheel table command input - Returns: - adsk.fusion.TableCommandInput - """ - return INPUTS_ROOT.itemById("wheel_table") +# Returns: +# adsk.fusion.TableCommandInput +# """ +# return INPUTS_ROOT.itemById("wheel_table") -def jointTable(): - """### Returns the joint table command input +# def jointTable(): +# """### Returns the joint table command input - Returns: - adsk.fusion.TableCommandInput - """ - return INPUTS_ROOT.itemById("joint_table") +# Returns: +# adsk.fusion.TableCommandInput +# """ +# return INPUTS_ROOT.itemById("joint_table") def gamepieceTable(): @@ -313,105 +315,109 @@ def notify(self, args): weight_unit, 0, 3 ) # add command inputs to table + # Transition: AARD-1685 # ~~~~~~~~~~~~~~~~ WHEEL CONFIGURATION ~~~~~~~~~~~~~~~~ """ Wheel configuration command input group - Container for wheel selection Table """ - wheelConfig = inputs.addGroupCommandInput( - "wheel_config", "Wheel Configuration" - ) - wheelConfig.isExpanded = True - wheelConfig.isEnabled = True - wheelConfig.tooltip = ( - "Select and define the drive-train wheels in your assembly." - ) - - wheel_inputs = wheelConfig.children + # wheelConfig = inputs.addGroupCommandInput( + # "wheel_config", "Wheel Configuration" + # ) + # wheelConfig.isExpanded = True + # wheelConfig.isEnabled = True + # wheelConfig.tooltip = ( + # "Select and define the drive-train wheels in your assembly." + # ) - # WHEEL SELECTION TABLE - """ - All selected wheel occurrences appear here. - """ - wheelTableInput = self.createTableInput( - "wheel_table", - "Wheel Table", - wheel_inputs, - 4, - "1:4:2:2", - 50, - ) + # wheel_inputs = wheelConfig.children + + # # WHEEL SELECTION TABLE + # """ + # All selected wheel occurrences appear here. + # """ + # wheelTableInput = self.createTableInput( + # "wheel_table", + # "Wheel Table", + # wheel_inputs, + # 4, + # "1:4:2:2", + # 50, + # ) - addWheelInput = wheel_inputs.addBoolValueInput( - "wheel_add", "Add", False - ) # add button + # addWheelInput = wheel_inputs.addBoolValueInput( + # "wheel_add", "Add", False + # ) # add button - removeWheelInput = wheel_inputs.addBoolValueInput( # remove button - "wheel_delete", "Remove", False - ) + # removeWheelInput = wheel_inputs.addBoolValueInput( # remove button + # "wheel_delete", "Remove", False + # ) - addWheelInput.tooltip = "Add a wheel joint" # tooltips - removeWheelInput.tooltip = "Remove a wheel joint" + # addWheelInput.tooltip = "Add a wheel joint" # tooltips + # removeWheelInput.tooltip = "Remove a wheel joint" - wheelSelectInput = wheel_inputs.addSelectionInput( - "wheel_select", - "Selection", - "Select the wheels joints in your drive-train assembly.", - ) - wheelSelectInput.addSelectionFilter( - "Joints" - ) # filter selection to only occurrences - - wheelSelectInput.setSelectionLimits(0) # no selection count limit - wheelSelectInput.isEnabled = False - wheelSelectInput.isVisible = False - - wheelTableInput.addToolbarCommandInput( - addWheelInput - ) # add buttons to the toolbar - wheelTableInput.addToolbarCommandInput( - removeWheelInput - ) # add buttons to the toolbar - - wheelTableInput.addCommandInput( # create textbox input using helper (component name) - self.createTextBoxInput( - "name_header", "Name", wheel_inputs, "Joint name", bold=False - ), - 0, - 1, - ) + # wheelSelectInput = wheel_inputs.addSelectionInput( + # "wheel_select", + # "Selection", + # "Select the wheels joints in your drive-train assembly.", + # ) + # wheelSelectInput.addSelectionFilter( + # "Joints" + # ) # filter selection to only occurrences + + # wheelSelectInput.setSelectionLimits(0) # no selection count limit + # wheelSelectInput.isEnabled = False + # wheelSelectInput.isVisible = False + + # wheelTableInput.addToolbarCommandInput( + # addWheelInput + # ) # add buttons to the toolbar + # wheelTableInput.addToolbarCommandInput( + # removeWheelInput + # ) # add buttons to the toolbar + + # wheelTableInput.addCommandInput( # create textbox input using helper (component name) + # self.createTextBoxInput( + # "name_header", "Name", wheel_inputs, "Joint name", bold=False + # ), + # 0, + # 1, + # ) - wheelTableInput.addCommandInput( - self.createTextBoxInput( # wheel type header - "parent_header", - "Parent", - wheel_inputs, - "Wheel type", - background="#d9d9d9", # textbox header background color - ), - 0, - 2, - ) + # wheelTableInput.addCommandInput( + # self.createTextBoxInput( # wheel type header + # "parent_header", + # "Parent", + # wheel_inputs, + # "Wheel type", + # background="#d9d9d9", # textbox header background color + # ), + # 0, + # 2, + # ) - wheelTableInput.addCommandInput( - self.createTextBoxInput( # Signal type header - "signal_header", - "Signal", - wheel_inputs, - "Signal type", - background="#d9d9d9", # textbox header background color - ), - 0, - 3, - ) + # wheelTableInput.addCommandInput( + # self.createTextBoxInput( # Signal type header + # "signal_header", + # "Signal", + # wheel_inputs, + # "Signal type", + # background="#d9d9d9", # textbox header background color + # ), + # 0, + # 3, + # ) + # Transition: AARD-1685 + # There remains some overlap between adding joints as wheels. + # Should investigate changes to improve performance. + createJointConfigTab(args) if exporterOptions.wheels: + pass for wheel in exporterOptions.wheels: - wheelEntity = gm.app.activeDocument.design.findEntityByToken(wheel.jointToken)[0] - addWheelToTable(wheelEntity) + fusionJoint = gm.app.activeDocument.design.findEntityByToken(wheel.jointToken)[0] + addWheelToConfigTab(fusionJoint, wheel) - # Transition: AARD-1685 - createJointConfigTab(args) if exporterOptions.joints: for synJoint in exporterOptions.joints: fusionJoint = gm.app.activeDocument.design.findEntityByToken(synJoint.jointToken)[0] @@ -1000,31 +1006,32 @@ def notify(self, args): _robotWeight = float _mode = ExportMode.ROBOT + # Transition: AARD-1865 """ Loops through all rows in the wheel table to extract all the input values """ - onSelect = gm.handlers[3] - wheelTableInput = wheelTable() - for row in range(wheelTableInput.rowCount): - if row == 0: - continue - - wheelTypeIndex = wheelTableInput.getInputAtPosition( - row, 2 - ).selectedItem.index # This must be either 0 or 1 for standard or omni - - signalTypeIndex = wheelTableInput.getInputAtPosition( - row, 3 - ).selectedItem.index - - _exportWheels.append( - Wheel( - WheelListGlobal[row - 1].entityToken, - WheelType(wheelTypeIndex + 1), - SignalType(signalTypeIndex + 1), - # onSelect.wheelJointList[row-1][0] # GUID of wheel joint. if no joint found, default to None - ) - ) + # onSelect = gm.handlers[3] + # wheelTableInput = wheelTable() + # for row in range(wheelTableInput.rowCount): + # if row == 0: + # continue + + # wheelTypeIndex = wheelTableInput.getInputAtPosition( + # row, 2 + # ).selectedItem.index # This must be either 0 or 1 for standard or omni + + # signalTypeIndex = wheelTableInput.getInputAtPosition( + # row, 3 + # ).selectedItem.index + + # _exportWheels.append( + # Wheel( + # WheelListGlobal[row - 1].entityToken, + # WheelType(wheelTypeIndex + 1), + # SignalType(signalTypeIndex + 1), + # # onSelect.wheelJointList[row-1][0] # GUID of wheel joint. if no joint found, default to None + # ) + # ) """ Loops through all rows in the gamepiece table to extract the input values @@ -1083,13 +1090,15 @@ def notify(self, args): .children.itemById("compress") ).value + selectedJoints, selectedWheels = getSelectedJointsAndWheels() + exporterOptions = ExporterOptions( savepath, name, version, materials=0, - joints=getSelectedJoints(), - wheels=_exportWheels, + joints=selectedJoints, + wheels=selectedWheels, gamepieces=_exportGamepieces, preferredUnits=selectedUnits, robotWeight=_robotWeight, @@ -1142,17 +1151,17 @@ def notify(self, args): addJointInput = INPUTS_ROOT.itemById("jointAddButton") addFieldInput = INPUTS_ROOT.itemById("field_add") - wheelTableInput = wheelTable() + # wheelTableInput = wheelTable() inputs = args.command.commandInputs jointTableInput: adsk.core.TableCommandInput = inputs.itemById("jointSettings").children.itemById("jointTable") gamepieceTableInput = gamepieceTable() - if wheelTableInput.rowCount <= 1: - removeWheelInput.isEnabled = False - else: - removeWheelInput.isEnabled = True + # if wheelTableInput.rowCount <= 1: + # removeWheelInput.isEnabled = False + # else: + # removeWheelInput.isEnabled = True if jointTableInput.rowCount <= 1: jointRemoveButton.isEnabled = False @@ -1383,33 +1392,34 @@ def notify(self, args: adsk.core.SelectionEventArgs): jointType == JointMotions.REVOLUTE.value or jointType == JointMotions.SLIDER.value ): - if ( - jointType == JointMotions.REVOLUTE.value - and MySelectHandler.lastInputCmd.id == "wheel_select" - ): - addWheelToTable(self.selectedJoint) - elif ( - jointType == JointMotions.REVOLUTE.value - and MySelectHandler.lastInputCmd.id == "wheel_remove" - ): - if self.selectedJoint in WheelListGlobal: - removeWheelFromTable( - WheelListGlobal.index(self.selectedJoint) - ) - else: + # Transition: AARD-1685 + # if ( + # jointType == JointMotions.REVOLUTE.value + # and MySelectHandler.lastInputCmd.id == "wheel_select" + # ): + # addWheelToTable(self.selectedJoint) + # elif ( + # jointType == JointMotions.REVOLUTE.value + # and MySelectHandler.lastInputCmd.id == "wheel_remove" + # ): + # if self.selectedJoint in WheelListGlobal: + # removeWheelFromTable( + # WheelListGlobal.index(self.selectedJoint) + # ) + # else: # Transition: AARD-1685 # Should move selection handles into respective UI modules - if not addJointToConfigTab(self.selectedJoint): - result = gm.ui.messageBox( - "You have already selected this joint.\n" - "Would you like to remove it?", - "Synthesis: Remove Joint Confirmation", - adsk.core.MessageBoxButtonTypes.YesNoButtonType, - adsk.core.MessageBoxIconTypes.QuestionIconType, - ) + if not addJointToConfigTab(self.selectedJoint): + result = gm.ui.messageBox( + "You have already selected this joint.\n" + "Would you like to remove it?", + "Synthesis: Remove Joint Confirmation", + adsk.core.MessageBoxButtonTypes.YesNoButtonType, + adsk.core.MessageBoxIconTypes.QuestionIconType, + ) - if result == adsk.core.DialogResults.DialogYes: - removeJointFromConfigTab(self.selectedJoint) + if result == adsk.core.DialogResults.DialogYes: + removeJointFromConfigTab(self.selectedJoint) selectionInput.isEnabled = False selectionInput.isVisible = False @@ -1590,7 +1600,7 @@ def notify(self, args): jointSelectCancelButton = INPUTS_ROOT.itemById("jointSelectCancelButton") gamepieceSelect = inputs.itemById("gamepiece_select") - wheelTableInput = wheelTable() + # wheelTableInput = wheelTable() jointTableInput: adsk.core.TableCommandInput = args.inputs.itemById("jointTable") @@ -1657,27 +1667,28 @@ def notify(self, args): cmdInput_str = cmdInput.id - if cmdInput_str == "placeholder_w": - position = ( - wheelTableInput.getPosition( - adsk.core.ImageCommandInput.cast(cmdInput) - )[1] - - 1 - ) - elif cmdInput_str == "name_w": - position = ( - wheelTableInput.getPosition( - adsk.core.TextBoxCommandInput.cast(cmdInput) - )[1] - - 1 - ) - elif cmdInput_str == "signal_type_w": - position = ( - wheelTableInput.getPosition( - adsk.core.DropDownCommandInput.cast(cmdInput) - )[1] - - 1 - ) + # Transition: AARD-1685 + # if cmdInput_str == "placeholder_w": + # position = ( + # wheelTableInput.getPosition( + # adsk.core.ImageCommandInput.cast(cmdInput) + # )[1] + # - 1 + # ) + # elif cmdInput_str == "name_w": + # position = ( + # wheelTableInput.getPosition( + # adsk.core.TextBoxCommandInput.cast(cmdInput) + # )[1] + # - 1 + # ) + # elif cmdInput_str == "signal_type_w": + # position = ( + # wheelTableInput.getPosition( + # adsk.core.DropDownCommandInput.cast(cmdInput) + # )[1] + # - 1 + # ) gm.ui.activeSelections.add(WheelListGlobal[position]) @@ -1734,44 +1745,45 @@ def notify(self, args): gm.ui.activeSelections.add(GamepieceListGlobal[position]) - elif cmdInput.id == "wheel_type_w": - self.reset() - - wheelSelect.isEnabled = False - addWheelInput.isEnabled = True - - cmdInput_str = cmdInput.id - position = ( - wheelTableInput.getPosition( - adsk.core.DropDownCommandInput.cast(cmdInput) - )[1] - - 1 - ) - wheelDropdown = adsk.core.DropDownCommandInput.cast(cmdInput) - - if wheelDropdown.selectedItem.index == 0: - getPosition = wheelTableInput.getPosition( - adsk.core.DropDownCommandInput.cast(cmdInput) - ) - iconInput = wheelTableInput.getInputAtPosition(getPosition[1], 0) - iconInput.imageFile = IconPaths.wheelIcons["standard"] - iconInput.tooltip = "Standard wheel" - - elif wheelDropdown.selectedItem.index == 1: - getPosition = wheelTableInput.getPosition( - adsk.core.DropDownCommandInput.cast(cmdInput) - ) - iconInput = wheelTableInput.getInputAtPosition(getPosition[1], 0) - iconInput.imageFile = IconPaths.wheelIcons["omni"] - iconInput.tooltip = "Omni wheel" - - elif wheelDropdown.selectedItem.index == 2: - getPosition = wheelTableInput.getPosition( - adsk.core.DropDownCommandInput.cast(cmdInput) - ) - iconInput = wheelTableInput.getInputAtPosition(getPosition[1], 0) - iconInput.imageFile = IconPaths.wheelIcons["mecanum"] - iconInput.tooltip = "Mecanum wheel" + # Transition: AARD-1685 + # elif cmdInput.id == "wheel_type_w": + # self.reset() + + # wheelSelect.isEnabled = False + # addWheelInput.isEnabled = True + + # cmdInput_str = cmdInput.id + # position = ( + # wheelTableInput.getPosition( + # adsk.core.DropDownCommandInput.cast(cmdInput) + # )[1] + # - 1 + # ) + # wheelDropdown = adsk.core.DropDownCommandInput.cast(cmdInput) + + # if wheelDropdown.selectedItem.index == 0: + # getPosition = wheelTableInput.getPosition( + # adsk.core.DropDownCommandInput.cast(cmdInput) + # ) + # iconInput = wheelTableInput.getInputAtPosition(getPosition[1], 0) + # iconInput.imageFile = IconPaths.wheelIcons["standard"] + # iconInput.tooltip = "Standard wheel" + + # elif wheelDropdown.selectedItem.index == 1: + # getPosition = wheelTableInput.getPosition( + # adsk.core.DropDownCommandInput.cast(cmdInput) + # ) + # iconInput = wheelTableInput.getInputAtPosition(getPosition[1], 0) + # iconInput.imageFile = IconPaths.wheelIcons["omni"] + # iconInput.tooltip = "Omni wheel" + + # elif wheelDropdown.selectedItem.index == 2: + # getPosition = wheelTableInput.getPosition( + # adsk.core.DropDownCommandInput.cast(cmdInput) + # ) + # iconInput = wheelTableInput.getInputAtPosition(getPosition[1], 0) + # iconInput.imageFile = IconPaths.wheelIcons["mecanum"] + # iconInput.tooltip = "Mecanum wheel" gm.ui.activeSelections.add(WheelListGlobal[position]) @@ -1803,17 +1815,18 @@ def notify(self, args): gamepieceSelect.clearSelection() addFieldInput.isEnabled = False - elif cmdInput.id == "wheel_delete": - # Currently causes Internal Autodesk Error - # gm.ui.activeSelections.clear() - - addWheelInput.isEnabled = True - if wheelTableInput.selectedRow == -1 or wheelTableInput.selectedRow == 0: - wheelTableInput.selectedRow = wheelTableInput.rowCount - 1 - gm.ui.messageBox("Select a row to delete.") - else: - index = wheelTableInput.selectedRow - 1 - removeWheelFromTable(index) + # Transition: AARD-1685 + # elif cmdInput.id == "wheel_delete": + # # Currently causes Internal Autodesk Error + # # gm.ui.activeSelections.clear() + + # addWheelInput.isEnabled = True + # if wheelTableInput.selectedRow == -1 or wheelTableInput.selectedRow == 0: + # wheelTableInput.selectedRow = wheelTableInput.rowCount - 1 + # gm.ui.messageBox("Select a row to delete.") + # else: + # index = wheelTableInput.selectedRow - 1 + # removeWheelFromTable(index) # Transition: AARD-1685 # Functionality could potentially be moved into `JointConfigTab.py` @@ -1999,6 +2012,7 @@ def notify(self, args): ).error("Failed:\n{}".format(traceback.format_exc())) +# Transition: AARD-1685 def addWheelToTable(wheel: adsk.fusion.Joint) -> None: """### Adds a wheel occurrence to its global list and wheel table. @@ -2015,7 +2029,7 @@ def addWheelToTable(wheel: adsk.fusion.Joint) -> None: # we do this before the initialization of gm.handlers[] pass - wheelTableInput = wheelTable() + wheelTableInput = None # def addPreselections(child_occurrences): # for occ in child_occurrences: # onSelect.allWheelPreselections.append(occ.entityToken) @@ -2161,39 +2175,40 @@ def addPreselections(child_occurrences): ) -def removeWheelFromTable(index: int) -> None: - """### Removes a wheel joint from its global list and wheel table. - - Args: - index (int): index of wheel item in its global list - """ - try: - onSelect = gm.handlers[3] - wheelTableInput = wheelTable() - wheel = WheelListGlobal[index] - - # def removePreselections(child_occurrences): - # for occ in child_occurrences: - # onSelect.allWheelPreselections.remove(occ.entityToken) - - # if occ.childOccurrences: - # removePreselections(occ.childOccurrences) - - # if wheel.childOccurrences: - # removePreselections(wheel.childOccurrences) - # else: - onSelect.allWheelPreselections.remove(wheel.entityToken) - - del WheelListGlobal[index] - wheelTableInput.deleteRow(index + 1) - - # updateJointTable(wheel) - except IndexError: - pass - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.removeWheelFromTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) +# Transition: AARD-1685 +# def removeWheelFromTable(index: int) -> None: +# """### Removes a wheel joint from its global list and wheel table. + +# Args: +# index (int): index of wheel item in its global list +# """ +# try: +# onSelect = gm.handlers[3] +# wheelTableInput = wheelTable() +# wheel = WheelListGlobal[index] + +# # def removePreselections(child_occurrences): +# # for occ in child_occurrences: +# # onSelect.allWheelPreselections.remove(occ.entityToken) + +# # if occ.childOccurrences: +# # removePreselections(occ.childOccurrences) + +# # if wheel.childOccurrences: +# # removePreselections(wheel.childOccurrences) +# # else: +# onSelect.allWheelPreselections.remove(wheel.entityToken) + +# del WheelListGlobal[index] +# wheelTableInput.deleteRow(index + 1) + +# # updateJointTable(wheel) +# except IndexError: +# pass +# except: +# logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.removeWheelFromTable()").error( +# "Failed:\n{}".format(traceback.format_exc()) +# ) def removeGamePieceFromTable(index: int) -> None: diff --git a/exporter/SynthesisFusionAddin/src/UI/CreateCommandInputsHelper.py b/exporter/SynthesisFusionAddin/src/UI/CreateCommandInputsHelper.py index acdddc5858..81a0530409 100644 --- a/exporter/SynthesisFusionAddin/src/UI/CreateCommandInputsHelper.py +++ b/exporter/SynthesisFusionAddin/src/UI/CreateCommandInputsHelper.py @@ -9,10 +9,10 @@ def createTableInput( inputs: adsk.core.CommandInputs, columns: int, ratio: str, - maxRows: int, - minRows=1, - columnSpacing=0, - rowSpacing=0, + minRows: int = 1, + maxRows: int = 50, + columnSpacing: int = 0, + rowSpacing: int = 0, ) -> adsk.core.TableCommandInput: try: input = inputs.addTableCommandInput(id, name, columns, ratio) @@ -32,11 +32,11 @@ def createBooleanInput( id: str, name: str, inputs: adsk.core.CommandInputs, - tooltip="", - tooltipadvanced="", - checked=True, - enabled=True, - isCheckBox=True, + tooltip: str = "", + tooltipadvanced: str = "", + checked: bool = True, + enabled: bool = True, + isCheckBox: bool = True, ) -> adsk.core.BoolValueCommandInput: try: input = inputs.addBoolValueInput(id, name, isCheckBox) @@ -57,15 +57,15 @@ def createTextBoxInput( name: str, inputs: adsk.core.CommandInputs, text: str, - italics=True, - bold=True, - fontSize=10, - alignment="center", - rowCount=1, - read=True, - background="whitesmoke", - tooltip="", - advanced_tooltip="", + italics: bool = True, + bold: bool = True, + fontSize: int = 10, + alignment: str = "center", + rowCount: int = 1, + read: bool = True, + background: str = "whitesmoke", + tooltip: str = "", + advanced_tooltip: str = "", ) -> adsk.core.TextBoxCommandInput: try: if bold: diff --git a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py index ed2f4a7d37..2dc628a1a6 100644 --- a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py @@ -5,13 +5,15 @@ import adsk.fusion from . import IconPaths -from .CreateCommandInputsHelper import createTableInput, createTextBoxInput +from .CreateCommandInputsHelper import createTableInput, createTextBoxInput, createBooleanInput -from ..Parser.ExporterOptions import JointParentType, Joint, SignalType +from ..Parser.ExporterOptions import JointParentType, Joint, Wheel, SignalType, WheelType # Wish we did not need this. Could look into storing everything within the design every time - Brandon selectedJointList: list[adsk.fusion.Joint] = [] +jointWheelIndexMap: dict[str, int] = {} jointConfigTable: adsk.core.TableCommandInput +wheelConfigTable: adsk.core.TableCommandInput def createJointConfigTab(args: adsk.core.CommandCreatedEventArgs) -> None: @@ -27,14 +29,13 @@ def createJointConfigTab(args: adsk.core.CommandCreatedEventArgs) -> None: "jointTable", "Joint Table", jointConfigTabInputs, - 6, - "1:2:2:2:2:2", - 50, + 7, + "1:2:2:2:2:2:2", ) jointConfigTable.addCommandInput( createTextBoxInput( - "motionHeader", + "jointMotionHeader", "Motion", jointConfigTabInputs, "Motion", @@ -100,6 +101,73 @@ def createJointConfigTab(args: adsk.core.CommandCreatedEventArgs) -> None: 5, ) + jointConfigTable.addCommandInput( + createTextBoxInput( + "wheelHeader", + "Is Wheel", + jointConfigTabInputs, + "Is Wheel", + background="#d9d9d9", + ), + 0, + 6, + ) + + jointConfigTabInputs.addTextBoxCommandInput("jointTabBlankSpacer", "", "", 1, True) + + global wheelConfigTable + wheelConfigTable = createTableInput( + "wheelTable", + "Wheel Table", + jointConfigTabInputs, + 4, + "1:2:2:2", + ) + + wheelConfigTable.addCommandInput( + createTextBoxInput( + "wheelMotionHeader", + "Motion", + jointConfigTabInputs, + "Motion", + bold=False, + ), + 0, + 0, + ) + + wheelConfigTable.addCommandInput( + createTextBoxInput( + "name_header", "Name", jointConfigTabInputs, "Joint name", bold=False + ), + 0, + 1, + ) + + wheelConfigTable.addCommandInput( + createTextBoxInput( + "wheelTypeHeader", + "WheelType", + jointConfigTabInputs, + "Wheel type", + background="#d9d9d9", + ), + 0, + 2, + ) + + wheelConfigTable.addCommandInput( + createTextBoxInput( + "signalTypeHeader", + "SignalType", + jointConfigTabInputs, + "Signal type", + background="#d9d9d9", + ), + 0, + 3, + ) + jointSelect = jointConfigTabInputs.addSelectionInput( "jointSelection", "Selection", "Select a joint in your assembly to add." ) @@ -125,7 +193,7 @@ def createJointConfigTab(args: adsk.core.CommandCreatedEventArgs) -> None: ) -def addJointToConfigTab(fusionJoint: adsk.fusion.Joint, synJoint: Joint = None) -> bool: +def addJointToConfigTab(fusionJoint: adsk.fusion.Joint, synJoint: Joint | None = None) -> bool: try: if fusionJoint in selectedJointList: return False @@ -251,6 +319,26 @@ def addJointToConfigTab(fusionJoint: adsk.fusion.Joint, synJoint: Joint = None) jointForce = commandInputs.addValueInput("jointForce", "Force", "N", adsk.core.ValueInput.createByReal(jointForceValue)) jointForce.tooltip = "Newtons" jointConfigTable.addCommandInput(jointForce, row, 5) + + if fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: + wheelCheckboxEnabled = True + wheelCheckboxTooltip = "Determines if this joint should be counted as a wheel." + else: + wheelCheckboxEnabled = False + wheelCheckboxTooltip = "Only Revolute joints can be treated as wheels." + + isWheel = synJoint.isWheel if synJoint else False + + # Transition: AARD-1685 + # All command inputs should be created using the helpers. + jointConfigTable.addCommandInput(createBooleanInput( + "isWheel", + "Is Wheel", + commandInputs, + wheelCheckboxTooltip, + checked=isWheel, + enabled=wheelCheckboxEnabled, + ), row, 6) except: logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.addJointToConfigTab()").error( "Failed:\n{}".format(traceback.format_exc()) @@ -259,6 +347,47 @@ def addJointToConfigTab(fusionJoint: adsk.fusion.Joint, synJoint: Joint = None) return True +def addWheelToConfigTab(joint: adsk.fusion.Joint, wheel: Wheel | None = None) -> None: + jointWheelIndexMap[joint.entityToken] = wheelConfigTable.rowCount + + commandInputs = wheelConfigTable.commandInputs + wheelIcon = commandInputs.addImageCommandInput("wheelPlaceholder", "Placeholder", IconPaths.wheelIcons["standard"]) + wheelName = commandInputs.addTextBoxCommandInput("wheelName", "Joint Name", joint.name, 1, True) + wheelName.tooltip = joint.name # TODO: Should this be the same? + wheelType = commandInputs.addDropDownCommandInput("wheelType", "Wheel Type", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle) + + if wheel: + standardWheelType = wheel.wheelType is WheelType.STANDARD + else: + standardWheelType = True + + wheelType.listItems.add("Standard", standardWheelType, "") + wheelType.listItems.add("OMNI", not standardWheelType, "") + wheelType.tooltip = "Wheel type" + wheelType.tooltipDescription = "".join(["
Omni-directional wheels can be used just like regular drive wheels", + "but they have the advantage of being able to roll freely perpendicular to", + "the drive direction.
"]) + + signalType = commandInputs.addDropDownCommandInput("wheelSignalType", "Signal Type", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle) + signalType.isFullWidth = True + signalType.isEnabled = False + signalType.tooltip = "Wheel signal type is linked with the respective joint signal type." + if wheel: + signalType.listItems.add("‎", wheel.signalType is SignalType.PWM, IconPaths.signalIcons["PWM"]) + signalType.listItems.add("‎", wheel.signalType is SignalType.CAN, IconPaths.signalIcons["CAN"]) + signalType.listItems.add("‎", wheel.signalType is SignalType.PASSIVE, IconPaths.signalIcons["PASSIVE"]) + else: + signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) + signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) + signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) + + row = wheelConfigTable.rowCount + wheelConfigTable.addCommandInput(wheelIcon, row, 0) + wheelConfigTable.addCommandInput(wheelName, row, 1) + wheelConfigTable.addCommandInput(wheelType, row, 2) + wheelConfigTable.addCommandInput(signalType, row, 3) + + def removeIndexedJointFromConfigTab(index: int) -> None: try: removeJointFromConfigTab(selectedJointList[index]) @@ -270,12 +399,16 @@ def removeIndexedJointFromConfigTab(index: int) -> None: def removeJointFromConfigTab(joint: adsk.fusion.Joint) -> None: try: + if jointWheelIndexMap.get(joint.entityToken): + removeWheelFromConfigTab(joint) + i = selectedJointList.index(joint) selectedJointList.remove(joint) - jointConfigTable.deleteRow(i + 1) for row in range(jointConfigTable.rowCount): # Row is 1 indexed + # TODO: Step through this in the debugger and figure out if this is all necessary. listItems = jointConfigTable.getInputAtPosition(row, 2).listItems + logging.getLogger(type(listItems)) if row > i: if listItems.item(i + 1).isSelected: listItems.item(i).isSelected = True @@ -294,25 +427,53 @@ def removeJointFromConfigTab(joint: adsk.fusion.Joint) -> None: ) -# Converts the current list of selected adsk.fusion.joints into list[Synthesis.Joint] -def getSelectedJoints() -> list[Joint]: +# Transition: AARD-1685 +# Remove wheel by joint name to avoid storing additional data about selected wheels. +# Should investigate finding a better way of linking joints and wheels. +def removeWheelFromConfigTab(joint: adsk.fusion.Joint) -> None: + try: + row = jointWheelIndexMap[joint.entityToken] + wheelConfigTable.deleteRow(row) + except: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.removeJointFromConfigTab()").error( + "Failed:\n{}".format(traceback.format_exc()) + ) + + +def getSelectedJointsAndWheels() -> tuple[list[Joint], list[Wheel]]: joints: list[Joint] = [] + wheels: list[Wheel] = [] for row in range(1, jointConfigTable.rowCount): # Row is 1 indexed + jointEntityToken = selectedJointList[row - 1].entityToken signalTypeIndex = jointConfigTable.getInputAtPosition(row, 3).selectedItem.index + signalType = SignalType(signalTypeIndex + 1) jointSpeed = jointConfigTable.getInputAtPosition(row, 4).value jointForce = jointConfigTable.getInputAtPosition(row, 5).value + isWheel = jointConfigTable.getInputAtPosition(row, 6).value joints.append( Joint( - selectedJointList[row - 1].entityToken, # Row is 1 indexed + jointEntityToken, JointParentType.ROOT, - SignalType(signalTypeIndex + 1), + signalType, jointSpeed, jointForce / 100.0, + isWheel, ) ) - return joints + if isWheel: + wheelRow = jointWheelIndexMap[jointEntityToken] + wheelTypeIndex = wheelConfigTable.getInputAtPosition(wheelRow, 2).selectedItem.index + wheels.append( + Wheel( + jointEntityToken, + WheelType(wheelTypeIndex + 1), + signalType, + ) + ) + + return (joints, wheels) def resetSelectedJoints() -> None: From d769a9e4d8fbbba60b7390dcc6fbf092663a0111 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Fri, 28 Jun 2024 12:26:16 -0700 Subject: [PATCH 013/121] added docstring standard --- exporter/SynthesisFusionAddin/README.md | 59 +++++++++++++++++++++---- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/exporter/SynthesisFusionAddin/README.md b/exporter/SynthesisFusionAddin/README.md index 8f40acf4cd..4fc4933373 100644 --- a/exporter/SynthesisFusionAddin/README.md +++ b/exporter/SynthesisFusionAddin/README.md @@ -1,7 +1,9 @@ # Synthesis Exporter + This is a Addin for Autodesk® Fusion™ that will export a [Mirabuf](https://github.com/HiceS/mirabuf) usable by the Synthesis simulator. ## Features + - [x] Materials - [x] Apperances - [x] Instances @@ -30,16 +32,17 @@ We use `VSCode` Primarily, download it to interact with our code or use your own --- ### How to Build + Run + 1. See root [`README`](/README.md) on how to run `init` script 2. Open `Autodesk Fusion` 3. Select `UTILITIES` from the top bar 4. Click `ADD-INS` Button 5. Click `Add-Ins` tab at the top of Scripts and Add-Ins dialog -6. Press + Button under **My Add-Ins** +6. Press + Button under **My Add-Ins** 7. Navigate to the containing folder for this Addin and click open at bottom - _clone-directory_/synthesis/exporters/SynthesisFusionAddin 8. Synthesis should be an option - select it and click run at the bottom of the dialog 9. There should now be a button that says Synthesis in your utilities menu - - If there is no button there may be a problem - see below for [checking log file](#debug-non-start) + - If there is no button there may be a problem - see below for [checking log file](#debug-non-start) --- @@ -50,8 +53,8 @@ We use `VSCode` Primarily, download it to interact with our code or use your own Most of the runtime for the addin is saved under the `logs` directory in this folder - Open `logs/synthesis.log` - - If nothing appears something went very wrong (make a issue on this github) - - If something appears and you cannot solve it feel free to make an issue anyway and include the file + - If nothing appears something went very wrong (make a issue on this github) + - If something appears and you cannot solve it feel free to make an issue anyway and include the file #### General Debugging @@ -59,12 +62,12 @@ Most of the runtime for the addin is saved under the `logs` directory in this fo 2. Select `UTILITIES` from the top bar 3. Click `ADD-INS` Button 4. Click `Add-Ins` tab at the top of Scripts and Add-Ins dialog -5. Press + Button under **My Add-Ins** +5. Press + Button under **My Add-Ins** 6. Navigate to the containing folder for this Addin and click open at bottom - _clone-directory_/synthesis/exporters/SynthesisFusionAddin 7. Synthesis should be an option - select it and click `Debug` at the bottom of the dialog - - This is in a dropdown with the Run Button + - This is in a dropdown with the Run Button 8. This should open VSCode - Now run with `FN+5` - - Now you may add break points or debug at will + - Now you may add break points or debug at will --- @@ -84,4 +87,44 @@ We format using a Python formatter called `black` [![Code style: black](https:// - use `isort .` followed by `black .` to format all relevant exporter python files. - or, alternatively, run `python ./tools/format.py` to do this for you! -__Note: black will always ignore files in the proto/proto_out folder since google formats those__ +**Note: black will always ignore files in the proto/proto_out folder since google formats those** + +### Docstring standard + +This standard is inconsistently applied, and that's ok + +```python +def foo(bar: fizz="flower") -> Result[walrus, None]: + """ + Turns a fizz into a walrus + + Parameters: + bar - The fizz to be transformed (default = "flower") ; fizz standards are subject to change, old fizzes may no longer be valid + + Returns: + Success - She new walrus + Failure - None if the summoning fails ; the cause of failure will be printed, not returned + + Notes: + - Only works as expected if the bar arg isn't a palindrome or an anagram of coffee. Otherwise unexpected (but still valid) walruses may be returned + - Please do not name your fizz "rizz" either, it hurts the walrus's feelings + + TODO: Consult witch about inconsistent alchemical methods + """ + # More alchemical fizz -> walrus code + some_walrus = bar + "_coffee" + return some_walrus + +``` + +Note that not this much detail is necessary when writing function documentation, notes, defaults, and a differentiation between sucess and failure aren't always necessary. + +#### Where to list potential causes of failure? + +It depends on how many you can list + +- 1: In the failure return case +- 2-3: In the notes section +- 4+: In a dedicated "potential causes of failure section" between the "returns" and "notes" sections + +Additionally, printing the error instead of returning it is bad practice From 08d08ead763255b561eeae262ab9f642a45d8881 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Fri, 28 Jun 2024 12:27:14 -0700 Subject: [PATCH 014/121] initial exporter data management interface From 5cd297e4d1782f06adda6dc3d481b05f8b98a6d4 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Fri, 28 Jun 2024 15:28:44 -0700 Subject: [PATCH 015/121] Working link between joints and wheels --- .../src/Parser/ExporterOptions.py | 2 +- .../src/UI/ConfigCommand.py | 122 ++++++++++-------- .../src/UI/JointConfigTab.py | 75 ++++++++--- 3 files changed, 127 insertions(+), 72 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py index 4f7983ee4d..3ff717062a 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py +++ b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py @@ -18,7 +18,7 @@ # Not 100% sure what this is for - Brandon JointParentType = Enum("JointParentType", ["ROOT", "END"]) -WheelType = Enum("WheelType", ["STANDARD", "OMNI"]) +WheelType = Enum("WheelType", ["STANDARD", "OMNI", "MECANUM"]) SignalType = Enum("SignalType", ["PWM", "CAN", "PASSIVE"]) ExportMode = Enum("ExportMode", ["ROBOT", "FIELD"]) # Dynamic / Static export PreferredUnits = Enum("PreferredUnits", ["METRIC", "IMPERIAL"]) diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 9f59245836..4f6d378b6b 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -15,8 +15,6 @@ ExportMode, ExporterOptions, Wheel, - WheelType, - SignalType, PreferredUnits, ) from .Configuration.SerialCommand import SerialCommand @@ -30,7 +28,8 @@ removeJointFromConfigTab, removeIndexedJointFromConfigTab, getSelectedJointsAndWheels, - resetSelectedJoints + resetSelectedJoints, + handleJointConfigTabInputChanged, ) import adsk.core @@ -412,12 +411,6 @@ def notify(self, args): # There remains some overlap between adding joints as wheels. # Should investigate changes to improve performance. createJointConfigTab(args) - if exporterOptions.wheels: - pass - for wheel in exporterOptions.wheels: - fusionJoint = gm.app.activeDocument.design.findEntityByToken(wheel.jointToken)[0] - addWheelToConfigTab(fusionJoint, wheel) - if exporterOptions.joints: for synJoint in exporterOptions.joints: fusionJoint = gm.app.activeDocument.design.findEntityByToken(synJoint.jointToken)[0] @@ -430,6 +423,15 @@ def notify(self, args): if joint.jointMotion.jointType in (JointMotions.REVOLUTE.value, JointMotions.SLIDER.value) and not joint.isSuppressed: addJointToConfigTab(joint) + # Adding saved wheels must take place after joints are added as a result of how the two types are connected. + # Transition: AARD-1685 + # Should consider changing how the parser handles wheels and joints to avoid overlap + if exporterOptions.wheels: + pass + for wheel in exporterOptions.wheels: + fusionJoint = gm.app.activeDocument.design.findEntityByToken(wheel.jointToken)[0] + addWheelToConfigTab(fusionJoint, wheel) + # ~~~~~~~~~~~~~~~~ GAMEPIECE CONFIGURATION ~~~~~~~~~~~~~~~~ """ Gamepiece group command input, isVisible=False by default @@ -1539,6 +1541,7 @@ def __init__(self, cmd): self.allWeights = [None, None] # [lbs, kg] self.isLbs = True self.isLbs_f = True + self.called = False def reset(self): """### Process: @@ -1589,13 +1592,22 @@ def notify(self, args): try: eventArgs = adsk.core.InputChangedEventArgs.cast(args) cmdInput = eventArgs.input + + # Transition: AARD-1685 + # This should be how all events are handled in separate files + # if not self.called: + handleJointConfigTabInputChanged(cmdInput) + # self.called = True + # else: + # self.called = False + MySelectHandler.lastInputCmd = cmdInput inputs = cmdInput.commandInputs onSelect = gm.handlers[3] frictionCoeff = INPUTS_ROOT.itemById("friction_coeff_override") - wheelSelect = inputs.itemById("wheel_select") + # wheelSelect = inputs.itemById("wheel_select") jointSelection = inputs.itemById("jointSelection") jointSelectCancelButton = INPUTS_ROOT.itemById("jointSelectCancelButton") gamepieceSelect = inputs.itemById("gamepiece_select") @@ -1609,11 +1621,11 @@ def notify(self, args): weight_input = INPUTS_ROOT.itemById("weight_input") - wheelConfig = inputs.itemById("wheel_config") - jointConfig = inputs.itemById("joint_config") + # wheelConfig = inputs.itemById("wheel_config") + # jointConfig = inputs.itemById("joint_config") gamepieceConfig = inputs.itemById("gamepiece_config") - addWheelInput = INPUTS_ROOT.itemById("wheel_add") + # addWheelInput = INPUTS_ROOT.itemById("wheel_add") addJointButton = INPUTS_ROOT.itemById("jointAddButton") removeJointButton = INPUTS_ROOT.itemById("jointRemoveButton") addFieldInput = INPUTS_ROOT.itemById("field_add") @@ -1635,37 +1647,37 @@ def notify(self, args): gamepieceConfig.isVisible = False weightTableInput.isVisible = True - addFieldInput.isEnabled = wheelConfig.isVisible = ( - jointConfig.isVisible - ) = True + # addFieldInput.isEnabled = wheelConfig.isVisible = ( + # jointConfig.isVisible + # ) = True elif modeDropdown.selectedItem.index == 1: if gamepieceConfig: gm.ui.activeSelections.clear() gm.app.activeDocument.design.rootComponent.opacity = 1 - addWheelInput.isEnabled = addJointButton.isEnabled = ( - gamepieceConfig.isVisible - ) = True + # addWheelInput.isEnabled = addJointButton.isEnabled = ( + # gamepieceConfig.isVisible + # ) = True - jointConfig.isVisible = wheelConfig.isVisible = ( - weightTableInput.isVisible - ) = False + # jointConfig.isVisible = wheelConfig.isVisible = ( + # weightTableInput.isVisible + # ) = False - elif cmdInput.id == "joint_config": - gm.app.activeDocument.design.rootComponent.opacity = 1 + # elif cmdInput.id == "joint_config": + # gm.app.activeDocument.design.rootComponent.opacity = 1 - elif ( - cmdInput.id == "placeholder_w" - or cmdInput.id == "name_w" - or cmdInput.id == "signal_type_w" - ): - self.reset() + # elif ( + # cmdInput.id == "placeholder_w" + # or cmdInput.id == "name_w" + # or cmdInput.id == "signal_type_w" + # ): + # self.reset() - wheelSelect.isEnabled = False - addWheelInput.isEnabled = True + # wheelSelect.isEnabled = False + # addWheelInput.isEnabled = True - cmdInput_str = cmdInput.id + # cmdInput_str = cmdInput.id # Transition: AARD-1685 # if cmdInput_str == "placeholder_w": @@ -1690,17 +1702,17 @@ def notify(self, args): # - 1 # ) - gm.ui.activeSelections.add(WheelListGlobal[position]) + # gm.ui.activeSelections.add(WheelListGlobal[position]) - elif ( - cmdInput.id == "placeholder" - or cmdInput.id == "name_j" - or cmdInput.id == "joint_parent" - or cmdInput.id == "signal_type" - ): - self.reset() - jointSelection.isEnabled = False - addJointButton.isEnabled = True + # elif ( + # cmdInput.id == "placeholder" + # or cmdInput.id == "name_j" + # or cmdInput.id == "joint_parent" + # or cmdInput.id == "signal_type" + # ): + # self.reset() + # jointSelection.isEnabled = False + # addJointButton.isEnabled = True elif ( cmdInput.id == "blank_gp" @@ -1785,23 +1797,23 @@ def notify(self, args): # iconInput.imageFile = IconPaths.wheelIcons["mecanum"] # iconInput.tooltip = "Mecanum wheel" - gm.ui.activeSelections.add(WheelListGlobal[position]) + # gm.ui.activeSelections.add(WheelListGlobal[position]) - elif cmdInput.id == "wheel_add": - self.reset() + # elif cmdInput.id == "wheel_add": + # self.reset() - wheelSelect.isVisible = True - wheelSelect.isEnabled = True - wheelSelect.clearSelection() - addJointButton.isEnabled = True - addWheelInput.isEnabled = False + # wheelSelect.isVisible = True + # wheelSelect.isEnabled = True + # wheelSelect.clearSelection() + # addJointButton.isEnabled = True + # addWheelInput.isEnabled = False # Transition: AARD-1685 # Functionality could potentially be moved into `JointConfigTab.py` elif cmdInput.id == "jointAddButton": self.reset() - addWheelInput.isEnabled = True + # addWheelInput.isEnabled = True jointSelection.isVisible = jointSelection.isEnabled = True jointSelection.clearSelection() addJointButton.isEnabled = removeJointButton.isEnabled = False @@ -1834,7 +1846,7 @@ def notify(self, args): gm.ui.activeSelections.clear() addJointButton.isEnabled = True - addWheelInput.isEnabled = True + # addWheelInput.isEnabled = True if jointTableInput.selectedRow == -1 or jointTableInput.selectedRow == 0: jointTableInput.selectedRow = jointTableInput.rowCount - 1 @@ -1858,8 +1870,8 @@ def notify(self, args): index = gamepieceTableInput.selectedRow - 1 removeGamePieceFromTable(index) - elif cmdInput.id == "wheel_select": - addWheelInput.isEnabled = True + # elif cmdInput.id == "wheel_select": + # addWheelInput.isEnabled = True elif cmdInput.id == "jointSelection": addJointButton.isEnabled = removeJointButton.isEnabled = True diff --git a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py index 2dc628a1a6..4776935374 100644 --- a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py @@ -11,6 +11,7 @@ # Wish we did not need this. Could look into storing everything within the design every time - Brandon selectedJointList: list[adsk.fusion.Joint] = [] +previousWheelCheckboxState: list[bool] = [] jointWheelIndexMap: dict[str, int] = {} jointConfigTable: adsk.core.TableCommandInput wheelConfigTable: adsk.core.TableCommandInput @@ -256,7 +257,7 @@ def addJointToConfigTab(fusionJoint: adsk.fusion.Joint, synJoint: Joint | None = jointType.tooltipDescription = "
The root component is usually the parent." signalType = commandInputs.addDropDownCommandInput( - "signalType", + "signalTypeJoint", "Signal Type", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, ) @@ -339,6 +340,8 @@ def addJointToConfigTab(fusionJoint: adsk.fusion.Joint, synJoint: Joint | None = checked=isWheel, enabled=wheelCheckboxEnabled, ), row, 6) + + previousWheelCheckboxState.append(isWheel) except: logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.addJointToConfigTab()").error( "Failed:\n{}".format(traceback.format_exc()) @@ -356,13 +359,10 @@ def addWheelToConfigTab(joint: adsk.fusion.Joint, wheel: Wheel | None = None) -> wheelName.tooltip = joint.name # TODO: Should this be the same? wheelType = commandInputs.addDropDownCommandInput("wheelType", "Wheel Type", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle) - if wheel: - standardWheelType = wheel.wheelType is WheelType.STANDARD - else: - standardWheelType = True - - wheelType.listItems.add("Standard", standardWheelType, "") - wheelType.listItems.add("OMNI", not standardWheelType, "") + selectedWheelType = wheel.wheelType if wheel else WheelType.STANDARD + wheelType.listItems.add("Standard", selectedWheelType is WheelType.STANDARD, "") + wheelType.listItems.add("OMNI", selectedWheelType is WheelType.OMNI, "") + wheelType.listItems.add("Mecanum", selectedWheelType is WheelType.MECANUM, "") wheelType.tooltip = "Wheel type" wheelType.tooltipDescription = "".join(["
Omni-directional wheels can be used just like regular drive wheels", "but they have the advantage of being able to roll freely perpendicular to", @@ -372,14 +372,11 @@ def addWheelToConfigTab(joint: adsk.fusion.Joint, wheel: Wheel | None = None) -> signalType.isFullWidth = True signalType.isEnabled = False signalType.tooltip = "Wheel signal type is linked with the respective joint signal type." - if wheel: - signalType.listItems.add("‎", wheel.signalType is SignalType.PWM, IconPaths.signalIcons["PWM"]) - signalType.listItems.add("‎", wheel.signalType is SignalType.CAN, IconPaths.signalIcons["CAN"]) - signalType.listItems.add("‎", wheel.signalType is SignalType.PASSIVE, IconPaths.signalIcons["PASSIVE"]) - else: - signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) + i = selectedJointList.index(joint) + jointSignalType = SignalType(jointConfigTable.getInputAtPosition(i + 1, 3).selectedItem.index + 1) + signalType.listItems.add("‎", jointSignalType is SignalType.PWM, IconPaths.signalIcons["PWM"]) + signalType.listItems.add("‎", jointSignalType is SignalType.CAN, IconPaths.signalIcons["CAN"]) + signalType.listItems.add("‎", jointSignalType is SignalType.PASSIVE, IconPaths.signalIcons["PASSIVE"]) row = wheelConfigTable.rowCount wheelConfigTable.addCommandInput(wheelIcon, row, 0) @@ -404,6 +401,7 @@ def removeJointFromConfigTab(joint: adsk.fusion.Joint) -> None: i = selectedJointList.index(joint) selectedJointList.remove(joint) + previousWheelCheckboxState.pop(i) jointConfigTable.deleteRow(i + 1) for row in range(jointConfigTable.rowCount): # Row is 1 indexed # TODO: Step through this in the debugger and figure out if this is all necessary. @@ -434,6 +432,10 @@ def removeWheelFromConfigTab(joint: adsk.fusion.Joint) -> None: try: row = jointWheelIndexMap[joint.entityToken] wheelConfigTable.deleteRow(row) + del jointWheelIndexMap[joint.entityToken] + for key, value in jointWheelIndexMap.items(): + if value > row - 1: + jointWheelIndexMap[key] -= 1 except: logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.removeJointFromConfigTab()").error( "Failed:\n{}".format(traceback.format_exc()) @@ -478,3 +480,44 @@ def getSelectedJointsAndWheels() -> tuple[list[Joint], list[Wheel]]: def resetSelectedJoints() -> None: selectedJointList.clear() + previousWheelCheckboxState.clear() + jointWheelIndexMap.clear() + + +def handleJointConfigTabInputChanged(commandInput: adsk.core.CommandInput) -> None: + if commandInput.id == "wheelType": + wheelTypeDropdown = adsk.core.DropDownCommandInput.cast(commandInput) + position = wheelConfigTable.getPosition(wheelTypeDropdown)[1] + iconInput = wheelConfigTable.getInputAtPosition(position, 0) + + if wheelTypeDropdown.selectedItem.index == 0: + iconInput.imageFile = IconPaths.wheelIcons["standard"] + iconInput.tooltip = "Standard wheel" + elif wheelTypeDropdown.selectedItem.index == 1: + iconInput.imageFile = IconPaths.wheelIcons["omni"] + iconInput.tooltip = "Omni wheel" + elif wheelTypeDropdown.selectedItem.index == 2: + iconInput.imageFile = IconPaths.wheelIcons["mecanum"] + iconInput.tooltip = "Mecanum wheel" + + elif commandInput.id == "isWheel": + isWheelCheckbox = adsk.core.BoolValueCommandInput.cast(commandInput) + position = jointConfigTable.getPosition(isWheelCheckbox)[1] - 1 + isAlreadyWheel = bool(jointWheelIndexMap.get(selectedJointList[position].entityToken)) + + if isWheelCheckbox.value != previousWheelCheckboxState[position]: + if not isAlreadyWheel: + addWheelToConfigTab(selectedJointList[position]) + else: + removeWheelFromConfigTab(selectedJointList[position]) + + previousWheelCheckboxState[position] = isWheelCheckbox.value + + elif commandInput.id == "signalTypeJoint": + signalTypeDropdown = adsk.core.DropDownCommandInput.cast(commandInput) + position = jointConfigTable.getPosition(signalTypeDropdown)[1] # 1 indexed + wheelTabPosition = jointWheelIndexMap.get(selectedJointList[position - 1].entityToken) + + if wheelTabPosition: + wheelSignalItems = wheelConfigTable.getInputAtPosition(position, 3).listItems + wheelSignalItems.item(signalTypeDropdown.selectedItem.index).isSelected = True From 4ca5692668846939154c08f07d52b32140e601c5 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Mon, 1 Jul 2024 12:22:47 -0700 Subject: [PATCH 016/121] Moved event handles into seperate files --- .../src/UI/ConfigCommand.py | 180 +++++++++--------- .../src/UI/JointConfigTab.py | 101 ++++++++-- 2 files changed, 175 insertions(+), 106 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 68d80258aa..7f5fdc88f2 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -28,14 +28,14 @@ # Transition: AARD-1685 # In the future all components should be handled in this way. -from .JointConfigTab import ( +from .JointConfigTab import ( # removeIndexedJointFromConfigTab,; removeJointFromConfigTab, addJointToConfigTab, addWheelToConfigTab, createJointConfigTab, getSelectedJointsAndWheels, handleJointConfigTabInputChanged, - removeIndexedJointFromConfigTab, - removeJointFromConfigTab, + handleJointConfigTabPreviewEvent, + handleJointConfigTabSelectionEvent, resetSelectedJoints, ) @@ -1098,21 +1098,24 @@ def notify(self, args): auto_calc_weight_f = INPUTS_ROOT.itemById("auto_calc_weight_f") - removeWheelInput = INPUTS_ROOT.itemById("wheel_delete") + # removeWheelInput = INPUTS_ROOT.itemById("wheel_delete") jointRemoveButton = INPUTS_ROOT.itemById("jointRemoveButton") jointSelection = INPUTS_ROOT.itemById("jointSelection") removeFieldInput = INPUTS_ROOT.itemById("field_delete") - addWheelInput = INPUTS_ROOT.itemById("wheel_add") + # addWheelInput = INPUTS_ROOT.itemById("wheel_add") addJointInput = INPUTS_ROOT.itemById("jointAddButton") addFieldInput = INPUTS_ROOT.itemById("field_add") # wheelTableInput = wheelTable() - inputs = args.command.commandInputs - jointTableInput: adsk.core.TableCommandInput = inputs.itemById("jointSettings").children.itemById( - "jointTable" - ) + # inputs = args.command.commandInputs + # jointTableInput: adsk.core.TableCommandInput = inputs.itemById("jointSettings").children.itemById( + # "jointTable" + # ) + + # Transition: AARD-1685 + handleJointConfigTabPreviewEvent(args) gamepieceTableInput = gamepieceTable() @@ -1121,26 +1124,26 @@ def notify(self, args): # else: # removeWheelInput.isEnabled = True - if jointTableInput.rowCount <= 1: - jointRemoveButton.isEnabled = False - elif not jointSelection.isEnabled: - jointRemoveButton.isEnabled = True + # if jointTableInput.rowCount <= 1: + # jointRemoveButton.isEnabled = False + # elif not jointSelection.isEnabled: + # jointRemoveButton.isEnabled = True if gamepieceTableInput.rowCount <= 1: removeFieldInput.isEnabled = auto_calc_weight_f.isEnabled = False else: removeFieldInput.isEnabled = auto_calc_weight_f.isEnabled = True - if not addWheelInput.isEnabled or not removeWheelInput: - # for wheel in WheelListGlobal: - # wheel.component.opacity = 0.25 - # CustomGraphics.createTextGraphics(wheel, WheelListGlobal) + # if not addWheelInput.isEnabled or not removeWheelInput: + # for wheel in WheelListGlobal: + # wheel.component.opacity = 0.25 + # CustomGraphics.createTextGraphics(wheel, WheelListGlobal) - gm.app.activeViewport.refresh() - else: - gm.app.activeDocument.design.rootComponent.opacity = 1 - for group in gm.app.activeDocument.design.rootComponent.customGraphicsGroups: - group.deleteMe() + # gm.app.activeViewport.refresh() + # else: + # gm.app.activeDocument.design.rootComponent.opacity = 1 + # for group in gm.app.activeDocument.design.rootComponent.customGraphicsGroups: + # group.deleteMe() if not addJointInput.isEnabled or not jointRemoveButton.isEnabled: # TODO: improve joint highlighting # for joint in JointListGlobal: @@ -1328,40 +1331,42 @@ def notify(self, args: adsk.core.SelectionEventArgs): selectionInput.isEnabled = False selectionInput.isVisible = False + # Transition: AARD-1685 elif self.selectedJoint: - self.cmd.setCursor("", 0, 0) - jointType = self.selectedJoint.jointMotion.jointType - if jointType == JointMotions.REVOLUTE.value or jointType == JointMotions.SLIDER.value: - # Transition: AARD-1685 - # if ( - # jointType == JointMotions.REVOLUTE.value - # and MySelectHandler.lastInputCmd.id == "wheel_select" - # ): - # addWheelToTable(self.selectedJoint) - # elif ( - # jointType == JointMotions.REVOLUTE.value - # and MySelectHandler.lastInputCmd.id == "wheel_remove" - # ): - # if self.selectedJoint in WheelListGlobal: - # removeWheelFromTable( - # WheelListGlobal.index(self.selectedJoint) - # ) - # else: - # Transition: AARD-1685 - # Should move selection handles into respective UI modules - if not addJointToConfigTab(self.selectedJoint): - result = gm.ui.messageBox( - "You have already selected this joint.\n" "Would you like to remove it?", - "Synthesis: Remove Joint Confirmation", - adsk.core.MessageBoxButtonTypes.YesNoButtonType, - adsk.core.MessageBoxIconTypes.QuestionIconType, - ) - - if result == adsk.core.DialogResults.DialogYes: - removeJointFromConfigTab(self.selectedJoint) - - selectionInput.isEnabled = False - selectionInput.isVisible = False + handleJointConfigTabSelectionEvent(args, self.selectedJoint) + # self.cmd.setCursor("", 0, 0) + # jointType = self.selectedJoint.jointMotion.jointType + # if jointType == JointMotions.REVOLUTE.value or jointType == JointMotions.SLIDER.value: + # Transition: AARD-1685 + # if ( + # jointType == JointMotions.REVOLUTE.value + # and MySelectHandler.lastInputCmd.id == "wheel_select" + # ): + # addWheelToTable(self.selectedJoint) + # elif ( + # jointType == JointMotions.REVOLUTE.value + # and MySelectHandler.lastInputCmd.id == "wheel_remove" + # ): + # if self.selectedJoint in WheelListGlobal: + # removeWheelFromTable( + # WheelListGlobal.index(self.selectedJoint) + # ) + # else: + # Transition: AARD-1685 + # Should move selection handles into respective UI modules + # if not addJointToConfigTab(self.selectedJoint): + # result = gm.ui.messageBox( + # "You have already selected this joint.\n" "Would you like to remove it?", + # "Synthesis: Remove Joint Confirmation", + # adsk.core.MessageBoxButtonTypes.YesNoButtonType, + # adsk.core.MessageBoxIconTypes.QuestionIconType, + # ) + + # if result == adsk.core.DialogResults.DialogYes: + # removeJointFromConfigTab(self.selectedJoint) + + # selectionInput.isEnabled = False + # selectionInput.isVisible = False except: if gm.ui: gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) @@ -1529,12 +1534,7 @@ def notify(self, args): cmdInput = eventArgs.input # Transition: AARD-1685 - # This should be how all events are handled in separate files - # if not self.called: - handleJointConfigTabInputChanged(cmdInput) - # self.called = True - # else: - # self.called = False + handleJointConfigTabInputChanged(args, INPUTS_ROOT) MySelectHandler.lastInputCmd = cmdInput inputs = cmdInput.commandInputs @@ -1543,13 +1543,13 @@ def notify(self, args): frictionCoeff = INPUTS_ROOT.itemById("friction_coeff_override") # wheelSelect = inputs.itemById("wheel_select") - jointSelection = inputs.itemById("jointSelection") - jointSelectCancelButton = INPUTS_ROOT.itemById("jointSelectCancelButton") + # jointSelection = inputs.itemById("jointSelection") + # jointSelectCancelButton = INPUTS_ROOT.itemById("jointSelectCancelButton") gamepieceSelect = inputs.itemById("gamepiece_select") # wheelTableInput = wheelTable() - jointTableInput: adsk.core.TableCommandInput = args.inputs.itemById("jointTable") + # jointTableInput: adsk.core.TableCommandInput = args.inputs.itemById("jointTable") gamepieceTableInput = gamepieceTable() weightTableInput = inputs.itemById("weight_table") @@ -1561,8 +1561,8 @@ def notify(self, args): gamepieceConfig = inputs.itemById("gamepiece_config") # addWheelInput = INPUTS_ROOT.itemById("wheel_add") - addJointButton = INPUTS_ROOT.itemById("jointAddButton") - removeJointButton = INPUTS_ROOT.itemById("jointRemoveButton") + # addJointButton = INPUTS_ROOT.itemById("jointAddButton") + # removeJointButton = INPUTS_ROOT.itemById("jointRemoveButton") addFieldInput = INPUTS_ROOT.itemById("field_add") indicator = INPUTS_ROOT.itemById("algorithmic_indicator") @@ -1721,14 +1721,14 @@ def notify(self, args): # Transition: AARD-1685 # Functionality could potentially be moved into `JointConfigTab.py` - elif cmdInput.id == "jointAddButton": - self.reset() + # elif cmdInput.id == "jointAddButton": + # self.reset() - # addWheelInput.isEnabled = True - jointSelection.isVisible = jointSelection.isEnabled = True - jointSelection.clearSelection() - addJointButton.isEnabled = removeJointButton.isEnabled = False - jointSelectCancelButton.isVisible = jointSelectCancelButton.isEnabled = True + # # addWheelInput.isEnabled = True + # jointSelection.isVisible = jointSelection.isEnabled = True + # jointSelection.clearSelection() + # addJointButton.isEnabled = removeJointButton.isEnabled = False + # jointSelectCancelButton.isVisible = jointSelectCancelButton.isEnabled = True elif cmdInput.id == "field_add": self.reset() @@ -1753,18 +1753,18 @@ def notify(self, args): # Transition: AARD-1685 # Functionality could potentially be moved into `JointConfigTab.py` - elif cmdInput.id == "jointRemoveButton": - gm.ui.activeSelections.clear() + # elif cmdInput.id == "jointRemoveButton": + # gm.ui.activeSelections.clear() - addJointButton.isEnabled = True - # addWheelInput.isEnabled = True + # addJointButton.isEnabled = True + # # addWheelInput.isEnabled = True - if jointTableInput.selectedRow == -1 or jointTableInput.selectedRow == 0: - jointTableInput.selectedRow = jointTableInput.rowCount - 1 - gm.ui.messageBox("Select a row to delete.") - else: - # Select Row is 1 indexed - removeIndexedJointFromConfigTab(jointTableInput.selectedRow - 1) + # if jointTableInput.selectedRow == -1 or jointTableInput.selectedRow == 0: + # jointTableInput.selectedRow = jointTableInput.rowCount - 1 + # gm.ui.messageBox("Select a row to delete.") + # else: + # # Select Row is 1 indexed + # removeIndexedJointFromConfigTab(jointTableInput.selectedRow - 1) elif cmdInput.id == "field_delete": gm.ui.activeSelections.clear() @@ -1781,14 +1781,14 @@ def notify(self, args): # elif cmdInput.id == "wheel_select": # addWheelInput.isEnabled = True - elif cmdInput.id == "jointSelection": - addJointButton.isEnabled = removeJointButton.isEnabled = True - jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False + # elif cmdInput.id == "jointSelection": + # addJointButton.isEnabled = removeJointButton.isEnabled = True + # jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False - elif cmdInput.id == "jointSelectCancelButton": - jointSelection.isEnabled = jointSelection.isVisible = False - jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False - addJointButton.isEnabled = removeJointButton.isEnabled = True + # elif cmdInput.id == "jointSelectCancelButton": + # jointSelection.isEnabled = jointSelection.isVisible = False + # jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False + # addJointButton.isEnabled = removeJointButton.isEnabled = True elif cmdInput.id == "gamepiece_select": addFieldInput.isEnabled = True diff --git a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py index a8a6507b6a..a644eee76f 100644 --- a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py @@ -121,6 +121,15 @@ def createJointConfigTab(args: adsk.core.CommandCreatedEventArgs) -> None: 6, ) + jointSelect = jointConfigTabInputs.addSelectionInput( + "jointSelection", "Selection", "Select a joint in your assembly to add." + ) + jointSelect.addSelectionFilter("Joints") + jointSelect.setSelectionLimits(0) + + # Visibility is triggered by `addJointInputButton` + jointSelect.isEnabled = jointSelect.isVisible = False + jointConfigTabInputs.addTextBoxCommandInput("jointTabBlankSpacer", "", "", 1, True) global wheelConfigTable @@ -174,15 +183,6 @@ def createJointConfigTab(args: adsk.core.CommandCreatedEventArgs) -> None: 3, ) - jointSelect = jointConfigTabInputs.addSelectionInput( - "jointSelection", "Selection", "Select a joint in your assembly to add." - ) - jointSelect.addSelectionFilter("Joints") - jointSelect.setSelectionLimits(0) - - # Visibility is triggered by `addJointInputButton` - jointSelect.isEnabled = jointSelect.isVisible = False - jointSelectCancelButton = jointConfigTabInputs.addBoolValueInput("jointSelectCancelButton", "Cancel", False) jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False @@ -285,7 +285,6 @@ def addJointToConfigTab(fusionJoint: adsk.fusion.Joint, synJoint: Joint | None = jointConfigTable.addCommandInput(jointType, row, 2) jointConfigTable.addCommandInput(signalType, row, 3) - # Joint speed must be added within an `if` because there is variance between different joint types # Comparison by `==` over `is` because the Autodesk API does not use `Enum` for their enum classes if fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: if synJoint: @@ -422,10 +421,9 @@ def removeJointFromConfigTab(joint: adsk.fusion.Joint) -> None: selectedJointList.remove(joint) previousWheelCheckboxState.pop(i) jointConfigTable.deleteRow(i + 1) - for row in range(jointConfigTable.rowCount): # Row is 1 indexed + for row in range(1, jointConfigTable.rowCount): # Row is 1 indexed # TODO: Step through this in the debugger and figure out if this is all necessary. listItems = jointConfigTable.getInputAtPosition(row, 2).listItems - logging.getLogger(type(listItems)) if row > i: if listItems.item(i + 1).isSelected: listItems.item(i).isSelected = True @@ -444,9 +442,6 @@ def removeJointFromConfigTab(joint: adsk.fusion.Joint) -> None: ) -# Transition: AARD-1685 -# Remove wheel by joint name to avoid storing additional data about selected wheels. -# Should investigate finding a better way of linking joints and wheels. def removeWheelFromConfigTab(joint: adsk.fusion.Joint) -> None: try: row = jointWheelIndexMap[joint.entityToken] @@ -503,7 +498,15 @@ def resetSelectedJoints() -> None: jointWheelIndexMap.clear() -def handleJointConfigTabInputChanged(commandInput: adsk.core.CommandInput) -> None: +# Transition: AARD-1685 +# Find a way to not pass the global commandInputs into this function +# Perhaps get the joint tab from the args then get what we want? +def handleJointConfigTabInputChanged( + args: adsk.core.InputChangedEventArgs, globalCommandInputs: adsk.core.CommandInputs +) -> None: + commandInput = args.input + + # TODO: Reorder if commandInput.id == "wheelType": wheelTypeDropdown = adsk.core.DropDownCommandInput.cast(commandInput) position = wheelConfigTable.getPosition(wheelTypeDropdown)[1] @@ -540,3 +543,69 @@ def handleJointConfigTabInputChanged(commandInput: adsk.core.CommandInput) -> No if wheelTabPosition: wheelSignalItems = wheelConfigTable.getInputAtPosition(position, 3).listItems wheelSignalItems.item(signalTypeDropdown.selectedItem.index).isSelected = True + + elif commandInput.id == "jointAddButton": + jointAddButton = globalCommandInputs.itemById("jointAddButton") + jointRemoveButton = globalCommandInputs.itemById("jointRemoveButton") + jointSelectCancelButton = globalCommandInputs.itemById("jointSelectCancelButton") + jointSelection = globalCommandInputs.itemById("jointSelection") + + jointSelection.isVisible = jointSelection.isEnabled = True + jointSelection.clearSelection() + jointAddButton.isEnabled = jointRemoveButton.isEnabled = False + jointSelectCancelButton.isVisible = jointSelectCancelButton.isEnabled = True + + elif commandInput.id == "jointRemoveButton": + jointAddButton = globalCommandInputs.itemById("jointAddButton") + jointTable = args.inputs.itemById("jointTable") + + jointAddButton.isEnabled = True + + if jointTable.selectedRow == -1 or jointTable.selectedRow == 0: + ui = adsk.core.Application.get().userInterface + ui.messageBox("Select a row to delete.") + else: + # Select Row is 1 indexed + removeIndexedJointFromConfigTab(jointTable.selectedRow - 1) + + elif commandInput.id == "jointSelectCancelButton": + jointAddButton = globalCommandInputs.itemById("jointAddButton") + jointRemoveButton = globalCommandInputs.itemById("jointRemoveButton") + jointSelectCancelButton = globalCommandInputs.itemById("jointSelectCancelButton") + jointSelection = globalCommandInputs.itemById("jointSelection") + jointSelection.isEnabled = jointSelection.isVisible = False + jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False + jointAddButton.isEnabled = jointRemoveButton.isEnabled = True + + +def handleJointConfigTabSelectionEvent(args: adsk.core.SelectionEventArgs, selectedJoint: adsk.fusion.Joint) -> None: + selectionInput = args.activeInput + jointType = selectedJoint.jointMotion.jointType + if jointType == adsk.fusion.JointTypes.RevoluteJointType or jointType == adsk.fusion.JointTypes.SliderJointType: + if not addJointToConfigTab(selectedJoint): + ui = adsk.core.Application.get().userInterface + result = ui.messageBox( + "You have already selected this joint.\n" "Would you like to remove it?", + "Synthesis: Remove Joint Confirmation", + adsk.core.MessageBoxButtonTypes.YesNoButtonType, + adsk.core.MessageBoxIconTypes.QuestionIconType, + ) + + if result == adsk.core.DialogResults.DialogYes: + removeJointFromConfigTab(selectedJoint) + + selectionInput.isEnabled = selectionInput.isVisible = False + + +def handleJointConfigTabPreviewEvent(args: adsk.core.CommandEventArgs) -> None: + jointAddButton = args.command.commandInputs.itemById("jointAddButton") + jointRemoveButton = args.command.commandInputs.itemById("jointRemoveButton") + jointSelectCancelButton = args.command.commandInputs.itemById("jointSelectCancelButton") + jointSelection = args.command.commandInputs.itemById("jointSelection") + + if jointConfigTable.rowCount <= 1: + jointRemoveButton.isEnabled = False + + if not jointSelection.isEnabled: + jointAddButton.isEnabled = jointRemoveButton.isEnabled = True + jointSelectCancelButton.isVisible = jointSelectCancelButton.isEnabled = False From 0f88cee64286f9c758cc8690182a49eba8e90361 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Mon, 1 Jul 2024 14:37:48 -0700 Subject: [PATCH 017/121] Classified Joint config table --- .../src/UI/ConfigCommand.py | 170 ++- .../src/UI/JointConfigTab.py | 1032 ++++++++--------- 2 files changed, 596 insertions(+), 606 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 7f5fdc88f2..5f723463d9 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -4,7 +4,6 @@ import logging import os -import platform import traceback from enum import Enum @@ -19,28 +18,20 @@ ExportMode, Gamepiece, PreferredUnits, - Wheel, ) from ..Parser.SynthesisParser.Parser import Parser from ..Parser.SynthesisParser.Utilities import guid_occurrence -from . import CustomGraphics, FileDialogConfig, Helper, IconPaths, OsHelper +from . import CustomGraphics, FileDialogConfig, Helper, IconPaths from .Configuration.SerialCommand import SerialCommand # Transition: AARD-1685 # In the future all components should be handled in this way. -from .JointConfigTab import ( # removeIndexedJointFromConfigTab,; removeJointFromConfigTab, - addJointToConfigTab, - addWheelToConfigTab, - createJointConfigTab, - getSelectedJointsAndWheels, - handleJointConfigTabInputChanged, - handleJointConfigTabPreviewEvent, - handleJointConfigTabSelectionEvent, - resetSelectedJoints, -) +from .JointConfigTab import JointConfigTab # ====================================== CONFIG COMMAND ====================================== +jointConfigTab: JointConfigTab + """ INPUTS_ROOT (adsk.fusion.CommandInputs): - Provides access to the set of all commandInput UI elements in the panel @@ -52,7 +43,7 @@ - WheelListGlobal: list of wheels (adsk.fusion.Occurrence) - GamepieceListGlobal: list of gamepieces (adsk.fusion.Occurrence) """ -WheelListGlobal = [] +# WheelListGlobal = [] GamepieceListGlobal = [] # Default to compressed files @@ -388,14 +379,17 @@ def notify(self, args): # 3, # ) + global jointConfigTab + jointConfigTab = JointConfigTab(args) + # Transition: AARD-1685 # There remains some overlap between adding joints as wheels. # Should investigate changes to improve performance. - createJointConfigTab(args) + # createJointConfigTab(args) if exporterOptions.joints: for synJoint in exporterOptions.joints: fusionJoint = gm.app.activeDocument.design.findEntityByToken(synJoint.jointToken)[0] - addJointToConfigTab(fusionJoint, synJoint) + jointConfigTab.addJoint(fusionJoint, synJoint) else: for joint in [ *gm.app.activeDocument.design.rootComponent.allJoints, @@ -405,7 +399,7 @@ def notify(self, args): joint.jointMotion.jointType in (JointMotions.REVOLUTE.value, JointMotions.SLIDER.value) and not joint.isSuppressed ): - addJointToConfigTab(joint) + jointConfigTab.addJoint(joint) # Adding saved wheels must take place after joints are added as a result of how the two types are connected. # Transition: AARD-1685 @@ -414,7 +408,7 @@ def notify(self, args): pass for wheel in exporterOptions.wheels: fusionJoint = gm.app.activeDocument.design.findEntityByToken(wheel.jointToken)[0] - addWheelToConfigTab(fusionJoint, wheel) + jointConfigTab.addWheel(fusionJoint, wheel) # ~~~~~~~~~~~~~~~~ GAMEPIECE CONFIGURATION ~~~~~~~~~~~~~~~~ """ @@ -1046,7 +1040,7 @@ def notify(self, args): .children.itemById("compress") ).value - selectedJoints, selectedWheels = getSelectedJointsAndWheels() + selectedJoints, selectedWheels = jointConfigTab.getSelectedJointsAndWheels() exporterOptions = ExporterOptions( savepath, @@ -1069,7 +1063,7 @@ def notify(self, args): # All selections should be reset AFTER a successful export and save. # If we run into an exporting error we should return back to the panel with all current options # still in tact. Even if they did not save. - resetSelectedJoints() + jointConfigTab.reset() except: if gm.ui: gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) @@ -1115,7 +1109,8 @@ def notify(self, args): # ) # Transition: AARD-1685 - handleJointConfigTabPreviewEvent(args) + # handleJointConfigTabPreviewEvent(args) + jointConfigTab.handlePreviewEvent(args) gamepieceTableInput = gamepieceTable() @@ -1333,7 +1328,8 @@ def notify(self, args: adsk.core.SelectionEventArgs): # Transition: AARD-1685 elif self.selectedJoint: - handleJointConfigTabSelectionEvent(args, self.selectedJoint) + jointConfigTab.handleSelectionEvent(args, self.selectedJoint) + # handleJointConfigTabSelectionEvent(args, self.selectedJoint) # self.cmd.setCursor("", 0, 0) # jointType = self.selectedJoint.jointMotion.jointType # if jointType == JointMotions.REVOLUTE.value or jointType == JointMotions.SLIDER.value: @@ -1534,7 +1530,8 @@ def notify(self, args): cmdInput = eventArgs.input # Transition: AARD-1685 - handleJointConfigTabInputChanged(args, INPUTS_ROOT) + # handleJointConfigTabInputChanged(args) + jointConfigTab.handleInputChanged(args, INPUTS_ROOT) MySelectHandler.lastInputCmd = cmdInput inputs = cmdInput.commandInputs @@ -1910,8 +1907,9 @@ def notify(self, args): try: onSelect = gm.handlers[3] - WheelListGlobal.clear() - resetSelectedJoints() + # WheelListGlobal.clear() + # resetSelectedJoints() + jointConfigTab.reset() GamepieceListGlobal.clear() onSelect.allWheelPreselections.clear() onSelect.wheelJointList.clear() @@ -1930,78 +1928,78 @@ def notify(self, args): ) -# Transition: AARD-1685 -def addWheelToTable(wheel: adsk.fusion.Joint) -> None: - """### Adds a wheel occurrence to its global list and wheel table. +# # Transition: AARD-1685 +# def addWheelToTable(wheel: adsk.fusion.Joint) -> None: +# """### Adds a wheel occurrence to its global list and wheel table. - Args: - wheel (adsk.fusion.Occurrence): wheel Occurrence object to be added. - """ - try: - try: - onSelect = gm.handlers[3] - onSelect.allWheelPreselections.append(wheel.entityToken) - except IndexError: - # Not 100% sure what we need the select handler here for however it should not run when - # first populating the saved wheel configs. This will naturally throw a IndexError as - # we do this before the initialization of gm.handlers[] - pass - - wheelTableInput = None - # def addPreselections(child_occurrences): - # for occ in child_occurrences: - # onSelect.allWheelPreselections.append(occ.entityToken) +# Args: +# wheel (adsk.fusion.Occurrence): wheel Occurrence object to be added. +# """ +# try: +# try: +# onSelect = gm.handlers[3] +# onSelect.allWheelPreselections.append(wheel.entityToken) +# except IndexError: +# # Not 100% sure what we need the select handler here for however it should not run when +# # first populating the saved wheel configs. This will naturally throw a IndexError as +# # we do this before the initialization of gm.handlers[] +# pass + +# wheelTableInput = None +# # def addPreselections(child_occurrences): +# # for occ in child_occurrences: +# # onSelect.allWheelPreselections.append(occ.entityToken) - # if occ.childOccurrences: - # addPreselections(occ.childOccurrences) +# # if occ.childOccurrences: +# # addPreselections(occ.childOccurrences) - # if wheel.childOccurrences: - # addPreselections(wheel.childOccurrences) - # else: +# # if wheel.childOccurrences: +# # addPreselections(wheel.childOccurrences) +# # else: - WheelListGlobal.append(wheel) - cmdInputs = adsk.core.CommandInputs.cast(wheelTableInput.commandInputs) +# # WheelListGlobal.append(wheel) +# cmdInputs = adsk.core.CommandInputs.cast(wheelTableInput.commandInputs) - icon = cmdInputs.addImageCommandInput("placeholder_w", "Placeholder", IconPaths.wheelIcons["standard"]) +# icon = cmdInputs.addImageCommandInput("placeholder_w", "Placeholder", IconPaths.wheelIcons["standard"]) - name = cmdInputs.addTextBoxCommandInput("name_w", "Joint name", wheel.name, 1, True) - name.tooltip = wheel.name +# name = cmdInputs.addTextBoxCommandInput("name_w", "Joint name", wheel.name, 1, True) +# name.tooltip = wheel.name - wheelType = cmdInputs.addDropDownCommandInput( - "wheel_type_w", - "Wheel Type", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) - wheelType.listItems.add("Standard", True, "") - wheelType.listItems.add("Omni", False, "") - wheelType.tooltip = "Wheel type" - wheelType.tooltipDescription = "
Omni-directional wheels can be used just like regular drive wheels but they have the advantage of being able to roll freely perpendicular to the drive direction.
" - wheelType.toolClipFilename = OsHelper.getOSPath(".", "src", "Resources") + os.path.join( - "WheelIcons", "omni-wheel-preview.png" - ) +# wheelType = cmdInputs.addDropDownCommandInput( +# "wheel_type_w", +# "Wheel Type", +# dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, +# ) +# wheelType.listItems.add("Standard", True, "") +# wheelType.listItems.add("Omni", False, "") +# wheelType.tooltip = "Wheel type" +# wheelType.tooltipDescription = "
Omni-directional wheels can be used just like regular drive wheels but they have the advantage of being able to roll freely perpendicular to the drive direction.
" +# wheelType.toolClipFilename = OsHelper.getOSPath(".", "src", "Resources") + os.path.join( +# "WheelIcons", "omni-wheel-preview.png" +# ) - signalType = cmdInputs.addDropDownCommandInput( - "signal_type_w", - "Signal Type", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) - signalType.isFullWidth = True - signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) - signalType.tooltip = "Signal type" +# signalType = cmdInputs.addDropDownCommandInput( +# "signal_type_w", +# "Signal Type", +# dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, +# ) +# signalType.isFullWidth = True +# signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) +# signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) +# signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) +# signalType.tooltip = "Signal type" - row = wheelTableInput.rowCount +# row = wheelTableInput.rowCount - wheelTableInput.addCommandInput(icon, row, 0) - wheelTableInput.addCommandInput(name, row, 1) - wheelTableInput.addCommandInput(wheelType, row, 2) - wheelTableInput.addCommandInput(signalType, row, 3) +# wheelTableInput.addCommandInput(icon, row, 0) +# wheelTableInput.addCommandInput(name, row, 1) +# wheelTableInput.addCommandInput(wheelType, row, 2) +# wheelTableInput.addCommandInput(signalType, row, 3) - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.addWheelToTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) +# except: +# logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.addWheelToTable()").error( +# "Failed:\n{}".format(traceback.format_exc()) +# ) def addGamepieceToTable(gamepiece: adsk.fusion.Occurrence) -> None: diff --git a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py index a644eee76f..8ad7f1893c 100644 --- a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py @@ -18,594 +18,586 @@ createTextBoxInput, ) -# Wish we did not need this. Could look into storing everything within the design every time - Brandon -selectedJointList: list[adsk.fusion.Joint] = [] -previousWheelCheckboxState: list[bool] = [] -jointWheelIndexMap: dict[str, int] = {} -jointConfigTable: adsk.core.TableCommandInput -wheelConfigTable: adsk.core.TableCommandInput - - -def createJointConfigTab(args: adsk.core.CommandCreatedEventArgs) -> None: - try: - inputs = args.command.commandInputs - jointConfigTab = inputs.addTabCommandInput("jointSettings", "Joint Settings") - jointConfigTab.tooltip = "Select and configure robot joints." - jointConfigTabInputs = jointConfigTab.children - - # TODO: Change background colors and such - Brandon - global jointConfigTable - jointConfigTable = createTableInput( - "jointTable", - "Joint Table", - jointConfigTabInputs, - 7, - "1:2:2:2:2:2:2", - ) - jointConfigTable.addCommandInput( - createTextBoxInput( - "jointMotionHeader", - "Motion", +class JointConfigTab: + selectedJointList: list[adsk.fusion.Joint] = [] + previousWheelCheckboxState: list[bool] = [] + jointWheelIndexMap: dict[str, int] = {} + jointConfigTable: adsk.core.TableCommandInput + wheelConfigTable: adsk.core.TableCommandInput + + def __init__(self, args: adsk.core.CommandCreatedEventArgs) -> None: + try: + inputs = args.command.commandInputs + jointConfigTab = inputs.addTabCommandInput("jointSettings", "Joint Settings") + jointConfigTab.tooltip = "Select and configure robot joints." + jointConfigTabInputs = jointConfigTab.children + + # TODO: Change background colors and such - Brandon + self.jointConfigTable = createTableInput( + "jointTable", + "Joint Table", jointConfigTabInputs, - "Motion", - bold=False, - ), - 0, - 0, - ) + 7, + "1:2:2:2:2:2:2", + ) - jointConfigTable.addCommandInput( - createTextBoxInput("nameHeader", "Name", jointConfigTabInputs, "Joint name", bold=False), - 0, - 1, - ) + self.jointConfigTable.addCommandInput( + createTextBoxInput( + "jointMotionHeader", + "Motion", + jointConfigTabInputs, + "Motion", + bold=False, + ), + 0, + 0, + ) - jointConfigTable.addCommandInput( - createTextBoxInput( - "parentHeader", - "Parent", - jointConfigTabInputs, - "Parent joint", - background="#d9d9d9", - ), - 0, - 2, - ) + self.jointConfigTable.addCommandInput( + createTextBoxInput("nameHeader", "Name", jointConfigTabInputs, "Joint name", bold=False), + 0, + 1, + ) - jointConfigTable.addCommandInput( - createTextBoxInput( - "signalHeader", - "Signal", - jointConfigTabInputs, - "Signal type", - background="#d9d9d9", - ), - 0, - 3, - ) + self.jointConfigTable.addCommandInput( + createTextBoxInput( + "parentHeader", + "Parent", + jointConfigTabInputs, + "Parent joint", + background="#d9d9d9", + ), + 0, + 2, + ) - jointConfigTable.addCommandInput( - createTextBoxInput( - "speedHeader", - "Speed", - jointConfigTabInputs, - "Joint Speed", - background="#d9d9d9", - ), - 0, - 4, - ) + self.jointConfigTable.addCommandInput( + createTextBoxInput( + "signalHeader", + "Signal", + jointConfigTabInputs, + "Signal type", + background="#d9d9d9", + ), + 0, + 3, + ) - jointConfigTable.addCommandInput( - createTextBoxInput( - "forceHeader", - "Force", - jointConfigTabInputs, - "Joint Force", - background="#d9d9d9", - ), - 0, - 5, - ) + self.jointConfigTable.addCommandInput( + createTextBoxInput( + "speedHeader", + "Speed", + jointConfigTabInputs, + "Joint Speed", + background="#d9d9d9", + ), + 0, + 4, + ) - jointConfigTable.addCommandInput( - createTextBoxInput( - "wheelHeader", - "Is Wheel", - jointConfigTabInputs, - "Is Wheel", - background="#d9d9d9", - ), - 0, - 6, - ) + self.jointConfigTable.addCommandInput( + createTextBoxInput( + "forceHeader", + "Force", + jointConfigTabInputs, + "Joint Force", + background="#d9d9d9", + ), + 0, + 5, + ) - jointSelect = jointConfigTabInputs.addSelectionInput( - "jointSelection", "Selection", "Select a joint in your assembly to add." - ) - jointSelect.addSelectionFilter("Joints") - jointSelect.setSelectionLimits(0) + self.jointConfigTable.addCommandInput( + createTextBoxInput( + "wheelHeader", + "Is Wheel", + jointConfigTabInputs, + "Is Wheel", + background="#d9d9d9", + ), + 0, + 6, + ) - # Visibility is triggered by `addJointInputButton` - jointSelect.isEnabled = jointSelect.isVisible = False + jointSelect = jointConfigTabInputs.addSelectionInput( + "jointSelection", "Selection", "Select a joint in your assembly to add." + ) + jointSelect.addSelectionFilter("Joints") + jointSelect.setSelectionLimits(0) - jointConfigTabInputs.addTextBoxCommandInput("jointTabBlankSpacer", "", "", 1, True) + # Visibility is triggered by `addJointInputButton` + jointSelect.isEnabled = jointSelect.isVisible = False - global wheelConfigTable - wheelConfigTable = createTableInput( - "wheelTable", - "Wheel Table", - jointConfigTabInputs, - 4, - "1:2:2:2", - ) + jointConfigTabInputs.addTextBoxCommandInput("jointTabBlankSpacer", "", "", 1, True) - wheelConfigTable.addCommandInput( - createTextBoxInput( - "wheelMotionHeader", - "Motion", + self.wheelConfigTable = createTableInput( + "wheelTable", + "Wheel Table", jointConfigTabInputs, - "Motion", - bold=False, - ), - 0, - 0, - ) + 4, + "1:2:2:2", + ) - wheelConfigTable.addCommandInput( - createTextBoxInput("name_header", "Name", jointConfigTabInputs, "Joint name", bold=False), - 0, - 1, - ) + self.wheelConfigTable.addCommandInput( + createTextBoxInput( + "wheelMotionHeader", + "Motion", + jointConfigTabInputs, + "Motion", + bold=False, + ), + 0, + 0, + ) - wheelConfigTable.addCommandInput( - createTextBoxInput( - "wheelTypeHeader", - "WheelType", - jointConfigTabInputs, - "Wheel type", - background="#d9d9d9", - ), - 0, - 2, - ) + self.wheelConfigTable.addCommandInput( + createTextBoxInput("name_header", "Name", jointConfigTabInputs, "Joint name", bold=False), + 0, + 1, + ) - wheelConfigTable.addCommandInput( - createTextBoxInput( - "signalTypeHeader", - "SignalType", - jointConfigTabInputs, - "Signal type", - background="#d9d9d9", - ), - 0, - 3, - ) + self.wheelConfigTable.addCommandInput( + createTextBoxInput( + "wheelTypeHeader", + "WheelType", + jointConfigTabInputs, + "Wheel type", + background="#d9d9d9", + ), + 0, + 2, + ) - jointSelectCancelButton = jointConfigTabInputs.addBoolValueInput("jointSelectCancelButton", "Cancel", False) - jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False + self.wheelConfigTable.addCommandInput( + createTextBoxInput( + "signalTypeHeader", + "SignalType", + jointConfigTabInputs, + "Signal type", + background="#d9d9d9", + ), + 0, + 3, + ) - addJointInputButton = jointConfigTabInputs.addBoolValueInput("jointAddButton", "Add", False) - removeJointInputButton = jointConfigTabInputs.addBoolValueInput("jointRemoveButton", "Remove", False) - addJointInputButton.isEnabled = removeJointInputButton.isEnabled = True + jointSelectCancelButton = jointConfigTabInputs.addBoolValueInput("jointSelectCancelButton", "Cancel", False) + jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False - jointConfigTable.addToolbarCommandInput(addJointInputButton) - jointConfigTable.addToolbarCommandInput(removeJointInputButton) - jointConfigTable.addToolbarCommandInput(jointSelectCancelButton) - except: - logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.createJointConfigTab()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + addJointInputButton = jointConfigTabInputs.addBoolValueInput("jointAddButton", "Add", False) + removeJointInputButton = jointConfigTabInputs.addBoolValueInput("jointRemoveButton", "Remove", False) + addJointInputButton.isEnabled = removeJointInputButton.isEnabled = True + self.jointConfigTable.addToolbarCommandInput(addJointInputButton) + self.jointConfigTable.addToolbarCommandInput(removeJointInputButton) + self.jointConfigTable.addToolbarCommandInput(jointSelectCancelButton) + except: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.createJointConfigTab()").error( + "Failed:\n{}".format(traceback.format_exc()) + ) -def addJointToConfigTab(fusionJoint: adsk.fusion.Joint, synJoint: Joint | None = None) -> bool: - try: - if fusionJoint in selectedJointList: - return False + def addJoint(self, fusionJoint: adsk.fusion.Joint, synJoint: Joint | None = None) -> bool: + try: + if fusionJoint in self.selectedJointList: + return False - selectedJointList.append(fusionJoint) - commandInputs = jointConfigTable.commandInputs + self.selectedJointList.append(fusionJoint) + commandInputs = self.jointConfigTable.commandInputs - if fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.RigidJointType: - icon = commandInputs.addImageCommandInput("placeholder", "Rigid", IconPaths.jointIcons["rigid"]) - icon.tooltip = "Rigid joint" + if fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.RigidJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Rigid", IconPaths.jointIcons["rigid"]) + icon.tooltip = "Rigid joint" - elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: - icon = commandInputs.addImageCommandInput("placeholder", "Revolute", IconPaths.jointIcons["revolute"]) - icon.tooltip = "Revolute joint" + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Revolute", IconPaths.jointIcons["revolute"]) + icon.tooltip = "Revolute joint" - elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: - icon = commandInputs.addImageCommandInput("placeholder", "Slider", IconPaths.jointIcons["slider"]) - icon.tooltip = "Slider joint" + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Slider", IconPaths.jointIcons["slider"]) + icon.tooltip = "Slider joint" - elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.PlanarJointType: - icon = commandInputs.addImageCommandInput("placeholder", "Planar", IconPaths.jointIcons["planar"]) - icon.tooltip = "Planar joint" + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.PlanarJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Planar", IconPaths.jointIcons["planar"]) + icon.tooltip = "Planar joint" - elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.PinSlotJointType: - icon = commandInputs.addImageCommandInput("placeholder", "Pin Slot", IconPaths.jointIcons["pin_slot"]) - icon.tooltip = "Pin slot joint" + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.PinSlotJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Pin Slot", IconPaths.jointIcons["pin_slot"]) + icon.tooltip = "Pin slot joint" - elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.CylindricalJointType: - icon = commandInputs.addImageCommandInput("placeholder", "Cylindrical", IconPaths.jointIcons["cylindrical"]) - icon.tooltip = "Cylindrical joint" + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.CylindricalJointType: + icon = commandInputs.addImageCommandInput( + "placeholder", "Cylindrical", IconPaths.jointIcons["cylindrical"] + ) + icon.tooltip = "Cylindrical joint" - elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.BallJointType: - icon = commandInputs.addImageCommandInput("placeholder", "Ball", IconPaths.jointIcons["ball"]) - icon.tooltip = "Ball joint" + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.BallJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Ball", IconPaths.jointIcons["ball"]) + icon.tooltip = "Ball joint" - name = commandInputs.addTextBoxCommandInput("name_j", "Occurrence name", "", 1, True) - name.tooltip = fusionJoint.name - name.formattedText = f"

{fusionJoint.name}

" + name = commandInputs.addTextBoxCommandInput("name_j", "Occurrence name", "", 1, True) + name.tooltip = fusionJoint.name + name.formattedText = f"

{fusionJoint.name}

" - jointType = commandInputs.addDropDownCommandInput( - "jointParent", - "Joint Type", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) + jointType = commandInputs.addDropDownCommandInput( + "jointParent", + "Joint Type", + dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, + ) - jointType.isFullWidth = True + jointType.isFullWidth = True - # Transition: AARD-1685 - # Implementation of joint parent system needs to be revisited. - jointType.listItems.add("Root", True) + # Transition: AARD-1685 + # Implementation of joint parent system needs to be revisited. + jointType.listItems.add("Root", True) - for row in range(1, jointConfigTable.rowCount): # Row is 1 indexed - dropDown = jointConfigTable.getInputAtPosition(row, 2) - dropDown.listItems.add(selectedJointList[-1].name, False) + for row in range(1, self.jointConfigTable.rowCount): # Row is 1 indexed + dropDown = self.jointConfigTable.getInputAtPosition(row, 2) + dropDown.listItems.add(self.selectedJointList[-1].name, False) - for fusionJoint in selectedJointList: - jointType.listItems.add(fusionJoint.name, False) + for fusionJoint in self.selectedJointList: + jointType.listItems.add(fusionJoint.name, False) - jointType.tooltip = "Possible parent joints" - jointType.tooltipDescription = "
The root component is usually the parent." + jointType.tooltip = "Possible parent joints" + jointType.tooltipDescription = "
The root component is usually the parent." - signalType = commandInputs.addDropDownCommandInput( - "signalTypeJoint", - "Signal Type", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) + signalType = commandInputs.addDropDownCommandInput( + "signalTypeJoint", + "Signal Type", + dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, + ) + + # TODO: Make this better, this is bad bad bad - Brandon + # if synJoint: + # signalType.listItems.add("", synJoint.signalType is SignalType.PWM, IconPaths.signalIcons["PWM"]) + # signalType.listItems.add("", synJoint.signalType is SignalType.CAN, IconPaths.signalIcons["CAN"]) + # signalType.listItems.add("", synJoint.signalType is SignalType.PASSIVE, IconPaths.signalIcons["PASSIVE"]) + # else: + signalType.listItems.add("", True, IconPaths.signalIcons["PWM"]) + signalType.listItems.add("", False, IconPaths.signalIcons["CAN"]) + signalType.listItems.add("", False, IconPaths.signalIcons["PASSIVE"]) + + signalType.tooltip = "Signal type" + + row = self.jointConfigTable.rowCount + self.jointConfigTable.addCommandInput(icon, row, 0) + self.jointConfigTable.addCommandInput(name, row, 1) + self.jointConfigTable.addCommandInput(jointType, row, 2) + self.jointConfigTable.addCommandInput(signalType, row, 3) + + # Comparison by `==` over `is` because the Autodesk API does not use `Enum` for their enum classes + if fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: + if synJoint: + jointSpeedValue = synJoint.speed + else: + jointSpeedValue = 3.1415926 + + jointSpeed = commandInputs.addValueInput( + "jointSpeed", + "Speed", + "deg", + adsk.core.ValueInput.createByReal(jointSpeedValue), + ) + jointSpeed.tooltip = "Degrees per second" + self.jointConfigTable.addCommandInput(jointSpeed, row, 4) + + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: + if synJoint: + jointSpeedValue = synJoint.speed + else: + jointSpeedValue = 100 + + jointSpeed = commandInputs.addValueInput( + "jointSpeed", + "Speed", + "m", + adsk.core.ValueInput.createByReal(jointSpeedValue), + ) + jointSpeed.tooltip = "Meters per second" + self.jointConfigTable.addCommandInput(jointSpeed, row, 4) - # TODO: Make this better, this is bad bad bad - Brandon - if synJoint: - signalType.listItems.add("‎", synJoint.signalType is SignalType.PWM, IconPaths.signalIcons["PWM"]) - signalType.listItems.add("‎", synJoint.signalType is SignalType.CAN, IconPaths.signalIcons["CAN"]) - signalType.listItems.add("‎", synJoint.signalType is SignalType.PASSIVE, IconPaths.signalIcons["PASSIVE"]) - else: - signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) - - signalType.tooltip = "Signal type" - - row = jointConfigTable.rowCount - jointConfigTable.addCommandInput(icon, row, 0) - jointConfigTable.addCommandInput(name, row, 1) - jointConfigTable.addCommandInput(jointType, row, 2) - jointConfigTable.addCommandInput(signalType, row, 3) - - # Comparison by `==` over `is` because the Autodesk API does not use `Enum` for their enum classes - if fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: if synJoint: - jointSpeedValue = synJoint.speed + jointForceValue = synJoint.force * 100 # Currently a factor of 100 - Should be investigated else: - jointSpeedValue = 3.1415926 + jointForceValue = 5 - jointSpeed = commandInputs.addValueInput( - "jointSpeed", - "Speed", - "deg", - adsk.core.ValueInput.createByReal(jointSpeedValue), + jointForce = commandInputs.addValueInput( + "jointForce", "Force", "N", adsk.core.ValueInput.createByReal(jointForceValue) ) - jointSpeed.tooltip = "Degrees per second" - jointConfigTable.addCommandInput(jointSpeed, row, 4) + jointForce.tooltip = "Newtons" + self.jointConfigTable.addCommandInput(jointForce, row, 5) - elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: - if synJoint: - jointSpeedValue = synJoint.speed + if fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: + wheelCheckboxEnabled = True + wheelCheckboxTooltip = "Determines if this joint should be counted as a wheel." else: - jointSpeedValue = 100 + wheelCheckboxEnabled = False + wheelCheckboxTooltip = "Only Revolute joints can be treated as wheels." + + isWheel = synJoint.isWheel if synJoint else False + + # Transition: AARD-1685 + # All command inputs should be created using the helpers. + self.jointConfigTable.addCommandInput( + createBooleanInput( + "isWheel", + "Is Wheel", + commandInputs, + wheelCheckboxTooltip, + checked=isWheel, + enabled=wheelCheckboxEnabled, + ), + row, + 6, + ) - jointSpeed = commandInputs.addValueInput( - "jointSpeed", - "Speed", - "m", - adsk.core.ValueInput.createByReal(jointSpeedValue), + self.previousWheelCheckboxState.append(isWheel) + except: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.addJointToConfigTab()").error( + "Failed:\n{}".format(traceback.format_exc()) ) - jointSpeed.tooltip = "Meters per second" - jointConfigTable.addCommandInput(jointSpeed, row, 4) - if synJoint: - jointForceValue = synJoint.force * 100 # Currently a factor of 100 - Should be investigated - else: - jointForceValue = 5 + return True - jointForce = commandInputs.addValueInput( - "jointForce", "Force", "N", adsk.core.ValueInput.createByReal(jointForceValue) - ) - jointForce.tooltip = "Newtons" - jointConfigTable.addCommandInput(jointForce, row, 5) - - if fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: - wheelCheckboxEnabled = True - wheelCheckboxTooltip = "Determines if this joint should be counted as a wheel." - else: - wheelCheckboxEnabled = False - wheelCheckboxTooltip = "Only Revolute joints can be treated as wheels." - - isWheel = synJoint.isWheel if synJoint else False - - # Transition: AARD-1685 - # All command inputs should be created using the helpers. - jointConfigTable.addCommandInput( - createBooleanInput( - "isWheel", - "Is Wheel", - commandInputs, - wheelCheckboxTooltip, - checked=isWheel, - enabled=wheelCheckboxEnabled, - ), - row, - 6, - ) + def addWheel(self, joint: adsk.fusion.Joint, wheel: Wheel | None = None) -> None: + self.jointWheelIndexMap[joint.entityToken] = self.wheelConfigTable.rowCount - previousWheelCheckboxState.append(isWheel) - except: - logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.addJointToConfigTab()").error( - "Failed:\n{}".format(traceback.format_exc()) + commandInputs = self.wheelConfigTable.commandInputs + wheelIcon = commandInputs.addImageCommandInput( + "wheelPlaceholder", "Placeholder", IconPaths.wheelIcons["standard"] ) - - return True - - -def addWheelToConfigTab(joint: adsk.fusion.Joint, wheel: Wheel | None = None) -> None: - jointWheelIndexMap[joint.entityToken] = wheelConfigTable.rowCount - - commandInputs = wheelConfigTable.commandInputs - wheelIcon = commandInputs.addImageCommandInput("wheelPlaceholder", "Placeholder", IconPaths.wheelIcons["standard"]) - wheelName = commandInputs.addTextBoxCommandInput("wheelName", "Joint Name", joint.name, 1, True) - wheelName.tooltip = joint.name # TODO: Should this be the same? - wheelType = commandInputs.addDropDownCommandInput( - "wheelType", "Wheel Type", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle - ) - - selectedWheelType = wheel.wheelType if wheel else WheelType.STANDARD - wheelType.listItems.add("Standard", selectedWheelType is WheelType.STANDARD, "") - wheelType.listItems.add("OMNI", selectedWheelType is WheelType.OMNI, "") - wheelType.listItems.add("Mecanum", selectedWheelType is WheelType.MECANUM, "") - wheelType.tooltip = "Wheel type" - wheelType.tooltipDescription = "".join( - [ - "
Omni-directional wheels can be used just like regular drive wheels", - "but they have the advantage of being able to roll freely perpendicular to", - "the drive direction.
", - ] - ) - - signalType = commandInputs.addDropDownCommandInput( - "wheelSignalType", "Signal Type", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle - ) - signalType.isFullWidth = True - signalType.isEnabled = False - signalType.tooltip = "Wheel signal type is linked with the respective joint signal type." - i = selectedJointList.index(joint) - jointSignalType = SignalType(jointConfigTable.getInputAtPosition(i + 1, 3).selectedItem.index + 1) - signalType.listItems.add("‎", jointSignalType is SignalType.PWM, IconPaths.signalIcons["PWM"]) - signalType.listItems.add("‎", jointSignalType is SignalType.CAN, IconPaths.signalIcons["CAN"]) - signalType.listItems.add("‎", jointSignalType is SignalType.PASSIVE, IconPaths.signalIcons["PASSIVE"]) - - row = wheelConfigTable.rowCount - wheelConfigTable.addCommandInput(wheelIcon, row, 0) - wheelConfigTable.addCommandInput(wheelName, row, 1) - wheelConfigTable.addCommandInput(wheelType, row, 2) - wheelConfigTable.addCommandInput(signalType, row, 3) - - -def removeIndexedJointFromConfigTab(index: int) -> None: - try: - removeJointFromConfigTab(selectedJointList[index]) - except: - logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.removeIndexedJointFromConfigTab()").error( - "Failed:\n{}".format(traceback.format_exc()) + wheelName = commandInputs.addTextBoxCommandInput("wheelName", "Joint Name", joint.name, 1, True) + wheelName.tooltip = joint.name # TODO: Should this be the same? + wheelType = commandInputs.addDropDownCommandInput( + "wheelType", "Wheel Type", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle ) - -def removeJointFromConfigTab(joint: adsk.fusion.Joint) -> None: - try: - if jointWheelIndexMap.get(joint.entityToken): - removeWheelFromConfigTab(joint) - - i = selectedJointList.index(joint) - selectedJointList.remove(joint) - previousWheelCheckboxState.pop(i) - jointConfigTable.deleteRow(i + 1) - for row in range(1, jointConfigTable.rowCount): # Row is 1 indexed - # TODO: Step through this in the debugger and figure out if this is all necessary. - listItems = jointConfigTable.getInputAtPosition(row, 2).listItems - if row > i: - if listItems.item(i + 1).isSelected: - listItems.item(i).isSelected = True - listItems.item(i + 1).deleteMe() - else: - listItems.item(i + 1).deleteMe() - else: - if listItems.item(i).isSelected: - listItems.item(i - 1).isSelected = True - listItems.item(i).deleteMe() - else: - listItems.item(i).deleteMe() - except: - logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.removeJointFromConfigTab()").error( - "Failed:\n{}".format(traceback.format_exc()) + selectedWheelType = wheel.wheelType if wheel else WheelType.STANDARD + wheelType.listItems.add("Standard", selectedWheelType is WheelType.STANDARD, "") + wheelType.listItems.add("OMNI", selectedWheelType is WheelType.OMNI, "") + wheelType.listItems.add("Mecanum", selectedWheelType is WheelType.MECANUM, "") + wheelType.tooltip = "Wheel type" + wheelType.tooltipDescription = "".join( + [ + "
Omni-directional wheels can be used just like regular drive wheels", + "but they have the advantage of being able to roll freely perpendicular to", + "the drive direction.
", + ] ) - -def removeWheelFromConfigTab(joint: adsk.fusion.Joint) -> None: - try: - row = jointWheelIndexMap[joint.entityToken] - wheelConfigTable.deleteRow(row) - del jointWheelIndexMap[joint.entityToken] - for key, value in jointWheelIndexMap.items(): - if value > row - 1: - jointWheelIndexMap[key] -= 1 - except: - logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.removeJointFromConfigTab()").error( - "Failed:\n{}".format(traceback.format_exc()) + signalType = commandInputs.addDropDownCommandInput( + "wheelSignalType", "Signal Type", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle ) + signalType.isFullWidth = True + signalType.isEnabled = False + signalType.tooltip = "Wheel signal type is linked with the respective joint signal type." + i = self.selectedJointList.index(joint) + jointSignalType = SignalType(self.jointConfigTable.getInputAtPosition(i + 1, 3).selectedItem.index + 1) + signalType.listItems.add("‎", jointSignalType is SignalType.PWM, IconPaths.signalIcons["PWM"]) + signalType.listItems.add("‎", jointSignalType is SignalType.CAN, IconPaths.signalIcons["CAN"]) + signalType.listItems.add("‎", jointSignalType is SignalType.PASSIVE, IconPaths.signalIcons["PASSIVE"]) + + row = self.wheelConfigTable.rowCount + self.wheelConfigTable.addCommandInput(wheelIcon, row, 0) + self.wheelConfigTable.addCommandInput(wheelName, row, 1) + self.wheelConfigTable.addCommandInput(wheelType, row, 2) + self.wheelConfigTable.addCommandInput(signalType, row, 3) + + def removeIndexedJoint(self, index: int) -> None: + try: + self.removeJoint(self.selectedJointList[index]) + except: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.removeIndexedJointFromConfigTab()").error( + "Failed:\n{}".format(traceback.format_exc()) + ) + def removeJoint(self, joint: adsk.fusion.Joint) -> None: + try: + if self.jointWheelIndexMap.get(joint.entityToken): + self.removeWheel(joint) + + i = self.selectedJointList.index(joint) + self.selectedJointList.remove(joint) + self.previousWheelCheckboxState.pop(i) + self.jointConfigTable.deleteRow(i + 1) + for row in range(1, self.jointConfigTable.rowCount): # Row is 1 indexed + # TODO: Step through this in the debugger and figure out if this is all necessary. + listItems = self.jointConfigTable.getInputAtPosition(row, 2).listItems + if row > i: + if listItems.item(i + 1).isSelected: + listItems.item(i).isSelected = True + listItems.item(i + 1).deleteMe() + else: + listItems.item(i + 1).deleteMe() + else: + if listItems.item(i).isSelected: + listItems.item(i - 1).isSelected = True + listItems.item(i).deleteMe() + else: + listItems.item(i).deleteMe() + except: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.removeJointFromConfigTab()").error( + "Failed:\n{}".format(traceback.format_exc()) + ) -def getSelectedJointsAndWheels() -> tuple[list[Joint], list[Wheel]]: - joints: list[Joint] = [] - wheels: list[Wheel] = [] - for row in range(1, jointConfigTable.rowCount): # Row is 1 indexed - jointEntityToken = selectedJointList[row - 1].entityToken - signalTypeIndex = jointConfigTable.getInputAtPosition(row, 3).selectedItem.index - signalType = SignalType(signalTypeIndex + 1) - jointSpeed = jointConfigTable.getInputAtPosition(row, 4).value - jointForce = jointConfigTable.getInputAtPosition(row, 5).value - isWheel = jointConfigTable.getInputAtPosition(row, 6).value - - joints.append( - Joint( - jointEntityToken, - JointParentType.ROOT, - signalType, - jointSpeed, - jointForce / 100.0, - isWheel, + def removeWheel(self, joint: adsk.fusion.Joint) -> None: + try: + row = self.jointWheelIndexMap[joint.entityToken] + self.wheelConfigTable.deleteRow(row) + del self.jointWheelIndexMap[joint.entityToken] + for key, value in self.jointWheelIndexMap.items(): + if value > row - 1: + self.jointWheelIndexMap[key] -= 1 + except: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.removeJointFromConfigTab()").error( + "Failed:\n{}".format(traceback.format_exc()) ) - ) - if isWheel: - wheelRow = jointWheelIndexMap[jointEntityToken] - wheelTypeIndex = wheelConfigTable.getInputAtPosition(wheelRow, 2).selectedItem.index - wheels.append( - Wheel( + def getSelectedJointsAndWheels(self) -> tuple[list[Joint], list[Wheel]]: + joints: list[Joint] = [] + wheels: list[Wheel] = [] + for row in range(1, self.jointConfigTable.rowCount): # Row is 1 indexed + jointEntityToken = self.selectedJointList[row - 1].entityToken + signalTypeIndex = self.jointConfigTable.getInputAtPosition(row, 3).selectedItem.index + signalType = SignalType(signalTypeIndex + 1) + jointSpeed = self.jointConfigTable.getInputAtPosition(row, 4).value + jointForce = self.jointConfigTable.getInputAtPosition(row, 5).value + isWheel = self.jointConfigTable.getInputAtPosition(row, 6).value + + joints.append( + Joint( jointEntityToken, - WheelType(wheelTypeIndex + 1), + JointParentType.ROOT, signalType, + jointSpeed, + jointForce / 100.0, + isWheel, ) ) - return (joints, wheels) - - -def resetSelectedJoints() -> None: - selectedJointList.clear() - previousWheelCheckboxState.clear() - jointWheelIndexMap.clear() - - -# Transition: AARD-1685 -# Find a way to not pass the global commandInputs into this function -# Perhaps get the joint tab from the args then get what we want? -def handleJointConfigTabInputChanged( - args: adsk.core.InputChangedEventArgs, globalCommandInputs: adsk.core.CommandInputs -) -> None: - commandInput = args.input - - # TODO: Reorder - if commandInput.id == "wheelType": - wheelTypeDropdown = adsk.core.DropDownCommandInput.cast(commandInput) - position = wheelConfigTable.getPosition(wheelTypeDropdown)[1] - iconInput = wheelConfigTable.getInputAtPosition(position, 0) - - if wheelTypeDropdown.selectedItem.index == 0: - iconInput.imageFile = IconPaths.wheelIcons["standard"] - iconInput.tooltip = "Standard wheel" - elif wheelTypeDropdown.selectedItem.index == 1: - iconInput.imageFile = IconPaths.wheelIcons["omni"] - iconInput.tooltip = "Omni wheel" - elif wheelTypeDropdown.selectedItem.index == 2: - iconInput.imageFile = IconPaths.wheelIcons["mecanum"] - iconInput.tooltip = "Mecanum wheel" - - elif commandInput.id == "isWheel": - isWheelCheckbox = adsk.core.BoolValueCommandInput.cast(commandInput) - position = jointConfigTable.getPosition(isWheelCheckbox)[1] - 1 - isAlreadyWheel = bool(jointWheelIndexMap.get(selectedJointList[position].entityToken)) - - if isWheelCheckbox.value != previousWheelCheckboxState[position]: - if not isAlreadyWheel: - addWheelToConfigTab(selectedJointList[position]) - else: - removeWheelFromConfigTab(selectedJointList[position]) - - previousWheelCheckboxState[position] = isWheelCheckbox.value - - elif commandInput.id == "signalTypeJoint": - signalTypeDropdown = adsk.core.DropDownCommandInput.cast(commandInput) - position = jointConfigTable.getPosition(signalTypeDropdown)[1] # 1 indexed - wheelTabPosition = jointWheelIndexMap.get(selectedJointList[position - 1].entityToken) - - if wheelTabPosition: - wheelSignalItems = wheelConfigTable.getInputAtPosition(position, 3).listItems - wheelSignalItems.item(signalTypeDropdown.selectedItem.index).isSelected = True - - elif commandInput.id == "jointAddButton": - jointAddButton = globalCommandInputs.itemById("jointAddButton") - jointRemoveButton = globalCommandInputs.itemById("jointRemoveButton") - jointSelectCancelButton = globalCommandInputs.itemById("jointSelectCancelButton") - jointSelection = globalCommandInputs.itemById("jointSelection") - - jointSelection.isVisible = jointSelection.isEnabled = True - jointSelection.clearSelection() - jointAddButton.isEnabled = jointRemoveButton.isEnabled = False - jointSelectCancelButton.isVisible = jointSelectCancelButton.isEnabled = True - - elif commandInput.id == "jointRemoveButton": - jointAddButton = globalCommandInputs.itemById("jointAddButton") - jointTable = args.inputs.itemById("jointTable") - - jointAddButton.isEnabled = True - - if jointTable.selectedRow == -1 or jointTable.selectedRow == 0: - ui = adsk.core.Application.get().userInterface - ui.messageBox("Select a row to delete.") - else: - # Select Row is 1 indexed - removeIndexedJointFromConfigTab(jointTable.selectedRow - 1) - - elif commandInput.id == "jointSelectCancelButton": - jointAddButton = globalCommandInputs.itemById("jointAddButton") - jointRemoveButton = globalCommandInputs.itemById("jointRemoveButton") - jointSelectCancelButton = globalCommandInputs.itemById("jointSelectCancelButton") - jointSelection = globalCommandInputs.itemById("jointSelection") - jointSelection.isEnabled = jointSelection.isVisible = False - jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False - jointAddButton.isEnabled = jointRemoveButton.isEnabled = True - - -def handleJointConfigTabSelectionEvent(args: adsk.core.SelectionEventArgs, selectedJoint: adsk.fusion.Joint) -> None: - selectionInput = args.activeInput - jointType = selectedJoint.jointMotion.jointType - if jointType == adsk.fusion.JointTypes.RevoluteJointType or jointType == adsk.fusion.JointTypes.SliderJointType: - if not addJointToConfigTab(selectedJoint): - ui = adsk.core.Application.get().userInterface - result = ui.messageBox( - "You have already selected this joint.\n" "Would you like to remove it?", - "Synthesis: Remove Joint Confirmation", - adsk.core.MessageBoxButtonTypes.YesNoButtonType, - adsk.core.MessageBoxIconTypes.QuestionIconType, - ) + if isWheel: + wheelRow = self.jointWheelIndexMap[jointEntityToken] + wheelTypeIndex = self.wheelConfigTable.getInputAtPosition(wheelRow, 2).selectedItem.index + wheels.append( + Wheel( + jointEntityToken, + WheelType(wheelTypeIndex + 1), + signalType, + ) + ) + + return (joints, wheels) + + def reset(self) -> None: + self.selectedJointList.clear() + self.previousWheelCheckboxState.clear() + self.jointWheelIndexMap.clear() + + # Transition: AARD-1685 + # Find a way to not pass the global commandInputs into this function + # Perhaps get the joint tab from the args then get what we want? + def handleInputChanged( + self, args: adsk.core.InputChangedEventArgs, globalCommandInputs: adsk.core.CommandInputs + ) -> None: + commandInput = args.input + + # TODO: Reorder + if commandInput.id == "wheelType": + wheelTypeDropdown = adsk.core.DropDownCommandInput.cast(commandInput) + position = self.wheelConfigTable.getPosition(wheelTypeDropdown)[1] + iconInput = self.wheelConfigTable.getInputAtPosition(position, 0) + + if wheelTypeDropdown.selectedItem.index == 0: + iconInput.imageFile = IconPaths.wheelIcons["standard"] + iconInput.tooltip = "Standard wheel" + elif wheelTypeDropdown.selectedItem.index == 1: + iconInput.imageFile = IconPaths.wheelIcons["omni"] + iconInput.tooltip = "Omni wheel" + elif wheelTypeDropdown.selectedItem.index == 2: + iconInput.imageFile = IconPaths.wheelIcons["mecanum"] + iconInput.tooltip = "Mecanum wheel" + + elif commandInput.id == "isWheel": + isWheelCheckbox = adsk.core.BoolValueCommandInput.cast(commandInput) + position = self.jointConfigTable.getPosition(isWheelCheckbox)[1] - 1 + isAlreadyWheel = bool(self.jointWheelIndexMap.get(self.selectedJointList[position].entityToken)) + + if isWheelCheckbox.value != self.previousWheelCheckboxState[position]: + if not isAlreadyWheel: + self.addWheel(self.selectedJointList[position]) + else: + self.removeWheel(self.selectedJointList[position]) + + self.previousWheelCheckboxState[position] = isWheelCheckbox.value + + elif commandInput.id == "signalTypeJoint": + signalTypeDropdown = adsk.core.DropDownCommandInput.cast(commandInput) + position = self.jointConfigTable.getPosition(signalTypeDropdown)[1] # 1 indexed + wheelTabPosition = self.jointWheelIndexMap.get(self.selectedJointList[position - 1].entityToken) + + if wheelTabPosition: + wheelSignalItems = self.wheelConfigTable.getInputAtPosition(position, 3).listItems + wheelSignalItems.item(signalTypeDropdown.selectedItem.index).isSelected = True + + elif commandInput.id == "jointAddButton": + jointAddButton = globalCommandInputs.itemById("jointAddButton") + jointRemoveButton = globalCommandInputs.itemById("jointRemoveButton") + jointSelectCancelButton = globalCommandInputs.itemById("jointSelectCancelButton") + jointSelection = globalCommandInputs.itemById("jointSelection") - if result == adsk.core.DialogResults.DialogYes: - removeJointFromConfigTab(selectedJoint) + jointSelection.isVisible = jointSelection.isEnabled = True + jointSelection.clearSelection() + jointAddButton.isEnabled = jointRemoveButton.isEnabled = False + jointSelectCancelButton.isVisible = jointSelectCancelButton.isEnabled = True + + elif commandInput.id == "jointRemoveButton": + jointAddButton = globalCommandInputs.itemById("jointAddButton") + jointTable = args.inputs.itemById("jointTable") + + jointAddButton.isEnabled = True + + if jointTable.selectedRow == -1 or jointTable.selectedRow == 0: + ui = adsk.core.Application.get().userInterface + ui.messageBox("Select a row to delete.") + else: + # Select Row is 1 indexed + self.removeIndexedJoint(jointTable.selectedRow - 1) + + elif commandInput.id == "jointSelectCancelButton": + jointAddButton = globalCommandInputs.itemById("jointAddButton") + jointRemoveButton = globalCommandInputs.itemById("jointRemoveButton") + jointSelectCancelButton = globalCommandInputs.itemById("jointSelectCancelButton") + jointSelection = globalCommandInputs.itemById("jointSelection") + jointSelection.isEnabled = jointSelection.isVisible = False + jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False + jointAddButton.isEnabled = jointRemoveButton.isEnabled = True + + def handleSelectionEvent(self, args: adsk.core.SelectionEventArgs, selectedJoint: adsk.fusion.Joint) -> None: + selectionInput = args.activeInput + jointType = selectedJoint.jointMotion.jointType + if jointType == adsk.fusion.JointTypes.RevoluteJointType or jointType == adsk.fusion.JointTypes.SliderJointType: + if not self.addJoint(selectedJoint): + ui = adsk.core.Application.get().userInterface + result = ui.messageBox( + "You have already selected this joint.\n" "Would you like to remove it?", + "Synthesis: Remove Joint Confirmation", + adsk.core.MessageBoxButtonTypes.YesNoButtonType, + adsk.core.MessageBoxIconTypes.QuestionIconType, + ) - selectionInput.isEnabled = selectionInput.isVisible = False + if result == adsk.core.DialogResults.DialogYes: + self.removeJoint(selectedJoint) + selectionInput.isEnabled = selectionInput.isVisible = False -def handleJointConfigTabPreviewEvent(args: adsk.core.CommandEventArgs) -> None: - jointAddButton = args.command.commandInputs.itemById("jointAddButton") - jointRemoveButton = args.command.commandInputs.itemById("jointRemoveButton") - jointSelectCancelButton = args.command.commandInputs.itemById("jointSelectCancelButton") - jointSelection = args.command.commandInputs.itemById("jointSelection") + def handlePreviewEvent(self, args: adsk.core.CommandEventArgs) -> None: + jointAddButton = args.command.commandInputs.itemById("jointAddButton") + jointRemoveButton = args.command.commandInputs.itemById("jointRemoveButton") + jointSelectCancelButton = args.command.commandInputs.itemById("jointSelectCancelButton") + jointSelection = args.command.commandInputs.itemById("jointSelection") - if jointConfigTable.rowCount <= 1: - jointRemoveButton.isEnabled = False + if self.jointConfigTable.rowCount <= 1: + jointRemoveButton.isEnabled = False - if not jointSelection.isEnabled: - jointAddButton.isEnabled = jointRemoveButton.isEnabled = True - jointSelectCancelButton.isVisible = jointSelectCancelButton.isEnabled = False + if not jointSelection.isEnabled: + jointAddButton.isEnabled = jointRemoveButton.isEnabled = True + jointSelectCancelButton.isVisible = jointSelectCancelButton.isEnabled = False From f1c318b6ca809b63e480a384401f0d0a2ebde608 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Mon, 1 Jul 2024 14:43:56 -0700 Subject: [PATCH 018/121] Removed replaced code and handled transition tags --- .../src/UI/ConfigCommand.py | 482 +----------------- 1 file changed, 5 insertions(+), 477 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 5f723463d9..936d15ea4c 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -40,10 +40,8 @@ """ These lists are crucial, and contain all of the relevant object selections. -- WheelListGlobal: list of wheels (adsk.fusion.Occurrence) - GamepieceListGlobal: list of gamepieces (adsk.fusion.Occurrence) """ -# WheelListGlobal = [] GamepieceListGlobal = [] # Default to compressed files @@ -66,25 +64,6 @@ def GUID(arg): return arg.entityToken -# Transition: AARD-1865 -# def wheelTable(): -# """### Returns the wheel table command input - -# Returns: -# adsk.fusion.TableCommandInput -# """ -# return INPUTS_ROOT.itemById("wheel_table") - - -# def jointTable(): -# """### Returns the joint table command input - -# Returns: -# adsk.fusion.TableCommandInput -# """ -# return INPUTS_ROOT.itemById("joint_table") - - def gamepieceTable(): """### Returns the gamepiece table command input @@ -286,106 +265,12 @@ def notify(self, args): weightTableInput.addCommandInput(weight_input, 0, 2) # add command inputs to table weightTableInput.addCommandInput(weight_unit, 0, 3) # add command inputs to table - # Transition: AARD-1685 - # ~~~~~~~~~~~~~~~~ WHEEL CONFIGURATION ~~~~~~~~~~~~~~~~ - """ - Wheel configuration command input group - - Container for wheel selection Table - """ - # wheelConfig = inputs.addGroupCommandInput( - # "wheel_config", "Wheel Configuration" - # ) - # wheelConfig.isExpanded = True - # wheelConfig.isEnabled = True - # wheelConfig.tooltip = ( - # "Select and define the drive-train wheels in your assembly." - # ) - - # wheel_inputs = wheelConfig.children - - # # WHEEL SELECTION TABLE - # """ - # All selected wheel occurrences appear here. - # """ - # wheelTableInput = self.createTableInput( - # "wheel_table", - # "Wheel Table", - # wheel_inputs, - # 4, - # "1:4:2:2", - # 50, - # ) - - # addWheelInput = wheel_inputs.addBoolValueInput( - # "wheel_add", "Add", False - # ) # add button - - # removeWheelInput = wheel_inputs.addBoolValueInput( # remove button - # "wheel_delete", "Remove", False - # ) - - # addWheelInput.tooltip = "Add a wheel joint" # tooltips - # removeWheelInput.tooltip = "Remove a wheel joint" - - # wheelSelectInput = wheel_inputs.addSelectionInput( - # "wheel_select", - # "Selection", - # "Select the wheels joints in your drive-train assembly.", - # ) - # wheelSelectInput.addSelectionFilter( - # "Joints" - # ) # filter selection to only occurrences - - # wheelSelectInput.setSelectionLimits(0) # no selection count limit - # wheelSelectInput.isEnabled = False - # wheelSelectInput.isVisible = False - - # wheelTableInput.addToolbarCommandInput( - # addWheelInput - # ) # add buttons to the toolbar - # wheelTableInput.addToolbarCommandInput( - # removeWheelInput - # ) # add buttons to the toolbar - - # wheelTableInput.addCommandInput( # create textbox input using helper (component name) - # self.createTextBoxInput( - # "name_header", "Name", wheel_inputs, "Joint name", bold=False - # ), - # 0, - # 1, - # ) - - # wheelTableInput.addCommandInput( - # self.createTextBoxInput( # wheel type header - # "parent_header", - # "Parent", - # wheel_inputs, - # "Wheel type", - # background="#d9d9d9", # textbox header background color - # ), - # 0, - # 2, - # ) - - # wheelTableInput.addCommandInput( - # self.createTextBoxInput( # Signal type header - # "signal_header", - # "Signal", - # wheel_inputs, - # "Signal type", - # background="#d9d9d9", # textbox header background color - # ), - # 0, - # 3, - # ) - global jointConfigTab jointConfigTab = JointConfigTab(args) # Transition: AARD-1685 # There remains some overlap between adding joints as wheels. # Should investigate changes to improve performance. - # createJointConfigTab(args) if exporterOptions.joints: for synJoint in exporterOptions.joints: fusionJoint = gm.app.activeDocument.design.findEntityByToken(synJoint.jointToken)[0] @@ -1092,65 +977,19 @@ def notify(self, args): auto_calc_weight_f = INPUTS_ROOT.itemById("auto_calc_weight_f") - # removeWheelInput = INPUTS_ROOT.itemById("wheel_delete") - jointRemoveButton = INPUTS_ROOT.itemById("jointRemoveButton") - jointSelection = INPUTS_ROOT.itemById("jointSelection") - removeFieldInput = INPUTS_ROOT.itemById("field_delete") - - # addWheelInput = INPUTS_ROOT.itemById("wheel_add") - addJointInput = INPUTS_ROOT.itemById("jointAddButton") addFieldInput = INPUTS_ROOT.itemById("field_add") - - # wheelTableInput = wheelTable() - - # inputs = args.command.commandInputs - # jointTableInput: adsk.core.TableCommandInput = inputs.itemById("jointSettings").children.itemById( - # "jointTable" - # ) + removeFieldInput = INPUTS_ROOT.itemById("field_delete") # Transition: AARD-1685 - # handleJointConfigTabPreviewEvent(args) + # This is how all preview handles should be done in the future jointConfigTab.handlePreviewEvent(args) gamepieceTableInput = gamepieceTable() - - # if wheelTableInput.rowCount <= 1: - # removeWheelInput.isEnabled = False - # else: - # removeWheelInput.isEnabled = True - - # if jointTableInput.rowCount <= 1: - # jointRemoveButton.isEnabled = False - # elif not jointSelection.isEnabled: - # jointRemoveButton.isEnabled = True - if gamepieceTableInput.rowCount <= 1: removeFieldInput.isEnabled = auto_calc_weight_f.isEnabled = False else: removeFieldInput.isEnabled = auto_calc_weight_f.isEnabled = True - # if not addWheelInput.isEnabled or not removeWheelInput: - # for wheel in WheelListGlobal: - # wheel.component.opacity = 0.25 - # CustomGraphics.createTextGraphics(wheel, WheelListGlobal) - - # gm.app.activeViewport.refresh() - # else: - # gm.app.activeDocument.design.rootComponent.opacity = 1 - # for group in gm.app.activeDocument.design.rootComponent.customGraphicsGroups: - # group.deleteMe() - - if not addJointInput.isEnabled or not jointRemoveButton.isEnabled: # TODO: improve joint highlighting - # for joint in JointListGlobal: - # CustomGraphics.highlightJointedOccurrences(joint) - - # gm.app.activeViewport.refresh() - gm.app.activeDocument.design.rootComponent.opacity = 0.15 - # else: - # for group in gm.app.activeDocument.design.rootComponent.customGraphicsGroups: - # group.deleteMe() - # gm.app.activeDocument.design.rootComponent.opacity = 1 - if not addFieldInput.isEnabled or not removeFieldInput: for gamepiece in GamepieceListGlobal: gamepiece.component.opacity = 0.25 @@ -1327,42 +1166,10 @@ def notify(self, args: adsk.core.SelectionEventArgs): selectionInput.isVisible = False # Transition: AARD-1685 + # This is how all handle selection events should be done in the future although it will look + # slightly differently for each type of handle. elif self.selectedJoint: jointConfigTab.handleSelectionEvent(args, self.selectedJoint) - # handleJointConfigTabSelectionEvent(args, self.selectedJoint) - # self.cmd.setCursor("", 0, 0) - # jointType = self.selectedJoint.jointMotion.jointType - # if jointType == JointMotions.REVOLUTE.value or jointType == JointMotions.SLIDER.value: - # Transition: AARD-1685 - # if ( - # jointType == JointMotions.REVOLUTE.value - # and MySelectHandler.lastInputCmd.id == "wheel_select" - # ): - # addWheelToTable(self.selectedJoint) - # elif ( - # jointType == JointMotions.REVOLUTE.value - # and MySelectHandler.lastInputCmd.id == "wheel_remove" - # ): - # if self.selectedJoint in WheelListGlobal: - # removeWheelFromTable( - # WheelListGlobal.index(self.selectedJoint) - # ) - # else: - # Transition: AARD-1685 - # Should move selection handles into respective UI modules - # if not addJointToConfigTab(self.selectedJoint): - # result = gm.ui.messageBox( - # "You have already selected this joint.\n" "Would you like to remove it?", - # "Synthesis: Remove Joint Confirmation", - # adsk.core.MessageBoxButtonTypes.YesNoButtonType, - # adsk.core.MessageBoxIconTypes.QuestionIconType, - # ) - - # if result == adsk.core.DialogResults.DialogYes: - # removeJointFromConfigTab(self.selectedJoint) - - # selectionInput.isEnabled = False - # selectionInput.isVisible = False except: if gm.ui: gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) @@ -1530,7 +1337,7 @@ def notify(self, args): cmdInput = eventArgs.input # Transition: AARD-1685 - # handleJointConfigTabInputChanged(args) + # Should be how all input changed handles are done in the future jointConfigTab.handleInputChanged(args, INPUTS_ROOT) MySelectHandler.lastInputCmd = cmdInput @@ -1539,27 +1346,12 @@ def notify(self, args): frictionCoeff = INPUTS_ROOT.itemById("friction_coeff_override") - # wheelSelect = inputs.itemById("wheel_select") - # jointSelection = inputs.itemById("jointSelection") - # jointSelectCancelButton = INPUTS_ROOT.itemById("jointSelectCancelButton") gamepieceSelect = inputs.itemById("gamepiece_select") - - # wheelTableInput = wheelTable() - - # jointTableInput: adsk.core.TableCommandInput = args.inputs.itemById("jointTable") - gamepieceTableInput = gamepieceTable() weightTableInput = inputs.itemById("weight_table") weight_input = INPUTS_ROOT.itemById("weight_input") - - # wheelConfig = inputs.itemById("wheel_config") - # jointConfig = inputs.itemById("joint_config") gamepieceConfig = inputs.itemById("gamepiece_config") - - # addWheelInput = INPUTS_ROOT.itemById("wheel_add") - # addJointButton = INPUTS_ROOT.itemById("jointAddButton") - # removeJointButton = INPUTS_ROOT.itemById("jointRemoveButton") addFieldInput = INPUTS_ROOT.itemById("field_add") indicator = INPUTS_ROOT.itemById("algorithmic_indicator") @@ -1596,56 +1388,6 @@ def notify(self, args): # weightTableInput.isVisible # ) = False - # elif cmdInput.id == "joint_config": - # gm.app.activeDocument.design.rootComponent.opacity = 1 - - # elif ( - # cmdInput.id == "placeholder_w" - # or cmdInput.id == "name_w" - # or cmdInput.id == "signal_type_w" - # ): - # self.reset() - - # wheelSelect.isEnabled = False - # addWheelInput.isEnabled = True - - # cmdInput_str = cmdInput.id - - # Transition: AARD-1685 - # if cmdInput_str == "placeholder_w": - # position = ( - # wheelTableInput.getPosition( - # adsk.core.ImageCommandInput.cast(cmdInput) - # )[1] - # - 1 - # ) - # elif cmdInput_str == "name_w": - # position = ( - # wheelTableInput.getPosition( - # adsk.core.TextBoxCommandInput.cast(cmdInput) - # )[1] - # - 1 - # ) - # elif cmdInput_str == "signal_type_w": - # position = ( - # wheelTableInput.getPosition( - # adsk.core.DropDownCommandInput.cast(cmdInput) - # )[1] - # - 1 - # ) - - # gm.ui.activeSelections.add(WheelListGlobal[position]) - - # elif ( - # cmdInput.id == "placeholder" - # or cmdInput.id == "name_j" - # or cmdInput.id == "joint_parent" - # or cmdInput.id == "signal_type" - # ): - # self.reset() - # jointSelection.isEnabled = False - # addJointButton.isEnabled = True - elif cmdInput.id == "blank_gp" or cmdInput.id == "name_gp" or cmdInput.id == "weight_gp": self.reset() @@ -1665,68 +1407,6 @@ def notify(self, args): gm.ui.activeSelections.add(GamepieceListGlobal[position]) - # Transition: AARD-1685 - # elif cmdInput.id == "wheel_type_w": - # self.reset() - - # wheelSelect.isEnabled = False - # addWheelInput.isEnabled = True - - # cmdInput_str = cmdInput.id - # position = ( - # wheelTableInput.getPosition( - # adsk.core.DropDownCommandInput.cast(cmdInput) - # )[1] - # - 1 - # ) - # wheelDropdown = adsk.core.DropDownCommandInput.cast(cmdInput) - - # if wheelDropdown.selectedItem.index == 0: - # getPosition = wheelTableInput.getPosition( - # adsk.core.DropDownCommandInput.cast(cmdInput) - # ) - # iconInput = wheelTableInput.getInputAtPosition(getPosition[1], 0) - # iconInput.imageFile = IconPaths.wheelIcons["standard"] - # iconInput.tooltip = "Standard wheel" - - # elif wheelDropdown.selectedItem.index == 1: - # getPosition = wheelTableInput.getPosition( - # adsk.core.DropDownCommandInput.cast(cmdInput) - # ) - # iconInput = wheelTableInput.getInputAtPosition(getPosition[1], 0) - # iconInput.imageFile = IconPaths.wheelIcons["omni"] - # iconInput.tooltip = "Omni wheel" - - # elif wheelDropdown.selectedItem.index == 2: - # getPosition = wheelTableInput.getPosition( - # adsk.core.DropDownCommandInput.cast(cmdInput) - # ) - # iconInput = wheelTableInput.getInputAtPosition(getPosition[1], 0) - # iconInput.imageFile = IconPaths.wheelIcons["mecanum"] - # iconInput.tooltip = "Mecanum wheel" - - # gm.ui.activeSelections.add(WheelListGlobal[position]) - - # elif cmdInput.id == "wheel_add": - # self.reset() - - # wheelSelect.isVisible = True - # wheelSelect.isEnabled = True - # wheelSelect.clearSelection() - # addJointButton.isEnabled = True - # addWheelInput.isEnabled = False - - # Transition: AARD-1685 - # Functionality could potentially be moved into `JointConfigTab.py` - # elif cmdInput.id == "jointAddButton": - # self.reset() - - # # addWheelInput.isEnabled = True - # jointSelection.isVisible = jointSelection.isEnabled = True - # jointSelection.clearSelection() - # addJointButton.isEnabled = removeJointButton.isEnabled = False - # jointSelectCancelButton.isVisible = jointSelectCancelButton.isEnabled = True - elif cmdInput.id == "field_add": self.reset() @@ -1735,34 +1415,6 @@ def notify(self, args): gamepieceSelect.clearSelection() addFieldInput.isEnabled = False - # Transition: AARD-1685 - # elif cmdInput.id == "wheel_delete": - # # Currently causes Internal Autodesk Error - # # gm.ui.activeSelections.clear() - - # addWheelInput.isEnabled = True - # if wheelTableInput.selectedRow == -1 or wheelTableInput.selectedRow == 0: - # wheelTableInput.selectedRow = wheelTableInput.rowCount - 1 - # gm.ui.messageBox("Select a row to delete.") - # else: - # index = wheelTableInput.selectedRow - 1 - # removeWheelFromTable(index) - - # Transition: AARD-1685 - # Functionality could potentially be moved into `JointConfigTab.py` - # elif cmdInput.id == "jointRemoveButton": - # gm.ui.activeSelections.clear() - - # addJointButton.isEnabled = True - # # addWheelInput.isEnabled = True - - # if jointTableInput.selectedRow == -1 or jointTableInput.selectedRow == 0: - # jointTableInput.selectedRow = jointTableInput.rowCount - 1 - # gm.ui.messageBox("Select a row to delete.") - # else: - # # Select Row is 1 indexed - # removeIndexedJointFromConfigTab(jointTableInput.selectedRow - 1) - elif cmdInput.id == "field_delete": gm.ui.activeSelections.clear() @@ -1775,18 +1427,6 @@ def notify(self, args): index = gamepieceTableInput.selectedRow - 1 removeGamePieceFromTable(index) - # elif cmdInput.id == "wheel_select": - # addWheelInput.isEnabled = True - - # elif cmdInput.id == "jointSelection": - # addJointButton.isEnabled = removeJointButton.isEnabled = True - # jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False - - # elif cmdInput.id == "jointSelectCancelButton": - # jointSelection.isEnabled = jointSelection.isVisible = False - # jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False - # addJointButton.isEnabled = removeJointButton.isEnabled = True - elif cmdInput.id == "gamepiece_select": addFieldInput.isEnabled = True @@ -1907,8 +1547,6 @@ def notify(self, args): try: onSelect = gm.handlers[3] - # WheelListGlobal.clear() - # resetSelectedJoints() jointConfigTab.reset() GamepieceListGlobal.clear() onSelect.allWheelPreselections.clear() @@ -1928,80 +1566,6 @@ def notify(self, args): ) -# # Transition: AARD-1685 -# def addWheelToTable(wheel: adsk.fusion.Joint) -> None: -# """### Adds a wheel occurrence to its global list and wheel table. - -# Args: -# wheel (adsk.fusion.Occurrence): wheel Occurrence object to be added. -# """ -# try: -# try: -# onSelect = gm.handlers[3] -# onSelect.allWheelPreselections.append(wheel.entityToken) -# except IndexError: -# # Not 100% sure what we need the select handler here for however it should not run when -# # first populating the saved wheel configs. This will naturally throw a IndexError as -# # we do this before the initialization of gm.handlers[] -# pass - -# wheelTableInput = None -# # def addPreselections(child_occurrences): -# # for occ in child_occurrences: -# # onSelect.allWheelPreselections.append(occ.entityToken) - -# # if occ.childOccurrences: -# # addPreselections(occ.childOccurrences) - -# # if wheel.childOccurrences: -# # addPreselections(wheel.childOccurrences) -# # else: - -# # WheelListGlobal.append(wheel) -# cmdInputs = adsk.core.CommandInputs.cast(wheelTableInput.commandInputs) - -# icon = cmdInputs.addImageCommandInput("placeholder_w", "Placeholder", IconPaths.wheelIcons["standard"]) - -# name = cmdInputs.addTextBoxCommandInput("name_w", "Joint name", wheel.name, 1, True) -# name.tooltip = wheel.name - -# wheelType = cmdInputs.addDropDownCommandInput( -# "wheel_type_w", -# "Wheel Type", -# dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, -# ) -# wheelType.listItems.add("Standard", True, "") -# wheelType.listItems.add("Omni", False, "") -# wheelType.tooltip = "Wheel type" -# wheelType.tooltipDescription = "
Omni-directional wheels can be used just like regular drive wheels but they have the advantage of being able to roll freely perpendicular to the drive direction.
" -# wheelType.toolClipFilename = OsHelper.getOSPath(".", "src", "Resources") + os.path.join( -# "WheelIcons", "omni-wheel-preview.png" -# ) - -# signalType = cmdInputs.addDropDownCommandInput( -# "signal_type_w", -# "Signal Type", -# dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, -# ) -# signalType.isFullWidth = True -# signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) -# signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) -# signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) -# signalType.tooltip = "Signal type" - -# row = wheelTableInput.rowCount - -# wheelTableInput.addCommandInput(icon, row, 0) -# wheelTableInput.addCommandInput(name, row, 1) -# wheelTableInput.addCommandInput(wheelType, row, 2) -# wheelTableInput.addCommandInput(signalType, row, 3) - -# except: -# logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.addWheelToTable()").error( -# "Failed:\n{}".format(traceback.format_exc()) -# ) - - def addGamepieceToTable(gamepiece: adsk.fusion.Occurrence) -> None: """### Adds a gamepiece occurrence to its global list and gamepiece table. @@ -2077,42 +1641,6 @@ def addPreselections(child_occurrences): ) -# Transition: AARD-1685 -# def removeWheelFromTable(index: int) -> None: -# """### Removes a wheel joint from its global list and wheel table. - -# Args: -# index (int): index of wheel item in its global list -# """ -# try: -# onSelect = gm.handlers[3] -# wheelTableInput = wheelTable() -# wheel = WheelListGlobal[index] - -# # def removePreselections(child_occurrences): -# # for occ in child_occurrences: -# # onSelect.allWheelPreselections.remove(occ.entityToken) - -# # if occ.childOccurrences: -# # removePreselections(occ.childOccurrences) - -# # if wheel.childOccurrences: -# # removePreselections(wheel.childOccurrences) -# # else: -# onSelect.allWheelPreselections.remove(wheel.entityToken) - -# del WheelListGlobal[index] -# wheelTableInput.deleteRow(index + 1) - -# # updateJointTable(wheel) -# except IndexError: -# pass -# except: -# logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.removeWheelFromTable()").error( -# "Failed:\n{}".format(traceback.format_exc()) -# ) - - def removeGamePieceFromTable(index: int) -> None: """### Removes a gamepiece occurrence from its global list and gamepiece table. From 3bfb556f8898dbdb98b9c5dab72ed75b1ac1c3b1 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Mon, 1 Jul 2024 15:04:34 -0700 Subject: [PATCH 019/121] Bogus bug fixes --- .../src/UI/JointConfigTab.py | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py index 8ad7f1893c..d9b7aaaad7 100644 --- a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py @@ -191,6 +191,8 @@ def __init__(self, args: adsk.core.CommandCreatedEventArgs) -> None: self.jointConfigTable.addToolbarCommandInput(addJointInputButton) self.jointConfigTable.addToolbarCommandInput(removeJointInputButton) self.jointConfigTable.addToolbarCommandInput(jointSelectCancelButton) + + self.reset() except: logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.createJointConfigTab()").error( "Failed:\n{}".format(traceback.format_exc()) @@ -266,15 +268,18 @@ def addJoint(self, fusionJoint: adsk.fusion.Joint, synJoint: Joint | None = None dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, ) - # TODO: Make this better, this is bad bad bad - Brandon - # if synJoint: - # signalType.listItems.add("", synJoint.signalType is SignalType.PWM, IconPaths.signalIcons["PWM"]) - # signalType.listItems.add("", synJoint.signalType is SignalType.CAN, IconPaths.signalIcons["CAN"]) - # signalType.listItems.add("", synJoint.signalType is SignalType.PASSIVE, IconPaths.signalIcons["PASSIVE"]) - # else: - signalType.listItems.add("", True, IconPaths.signalIcons["PWM"]) - signalType.listItems.add("", False, IconPaths.signalIcons["CAN"]) - signalType.listItems.add("", False, IconPaths.signalIcons["PASSIVE"]) + # Invisible white space characters are required in the list item name field to make this work. + # I have no idea why, Fusion API needs some special education help - Brandon + if synJoint: + signalType.listItems.add("‎", synJoint.signalType is SignalType.PWM, IconPaths.signalIcons["PWM"]) + signalType.listItems.add("‎", synJoint.signalType is SignalType.CAN, IconPaths.signalIcons["CAN"]) + signalType.listItems.add( + "‎", synJoint.signalType is SignalType.PASSIVE, IconPaths.signalIcons["PASSIVE"] + ) + else: + signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) + signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) + signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) signalType.tooltip = "Signal type" @@ -531,12 +536,13 @@ def handleInputChanged( elif commandInput.id == "signalTypeJoint": signalTypeDropdown = adsk.core.DropDownCommandInput.cast(commandInput) - position = self.jointConfigTable.getPosition(signalTypeDropdown)[1] # 1 indexed - wheelTabPosition = self.jointWheelIndexMap.get(self.selectedJointList[position - 1].entityToken) + jointTabPosition = self.jointConfigTable.getPosition(signalTypeDropdown)[1] # 1 indexed + wheelTabPosition = self.jointWheelIndexMap.get(self.selectedJointList[jointTabPosition - 1].entityToken) if wheelTabPosition: - wheelSignalItems = self.wheelConfigTable.getInputAtPosition(position, 3).listItems - wheelSignalItems.item(signalTypeDropdown.selectedItem.index).isSelected = True + wheelSignalItems: adsk.core.DropDownCommandInput + wheelSignalItems = self.wheelConfigTable.getInputAtPosition(wheelTabPosition, 3) + wheelSignalItems.listItems.item(signalTypeDropdown.selectedItem.index).isSelected = True elif commandInput.id == "jointAddButton": jointAddButton = globalCommandInputs.itemById("jointAddButton") From 35c3820ba93eb2f0d8b41d7658adb6e89d909016 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Mon, 1 Jul 2024 15:40:18 -0700 Subject: [PATCH 020/121] Added typing and improved comments + formatting --- .../src/UI/ConfigCommand.py | 1 + .../src/UI/JointConfigTab.py | 162 ++++++------------ 2 files changed, 49 insertions(+), 114 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 936d15ea4c..0ecdc91faa 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -26,6 +26,7 @@ # Transition: AARD-1685 # In the future all components should be handled in this way. +# This import broke everything when attempting to use absolute imports??? Investigate? from .JointConfigTab import JointConfigTab # ====================================== CONFIG COMMAND ====================================== diff --git a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py index d9b7aaaad7..f734c3171c 100644 --- a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py @@ -23,99 +23,57 @@ class JointConfigTab: selectedJointList: list[adsk.fusion.Joint] = [] previousWheelCheckboxState: list[bool] = [] jointWheelIndexMap: dict[str, int] = {} + jointConfigTab: adsk.core.TabCommandInput jointConfigTable: adsk.core.TableCommandInput wheelConfigTable: adsk.core.TableCommandInput def __init__(self, args: adsk.core.CommandCreatedEventArgs) -> None: try: inputs = args.command.commandInputs - jointConfigTab = inputs.addTabCommandInput("jointSettings", "Joint Settings") - jointConfigTab.tooltip = "Select and configure robot joints." - jointConfigTabInputs = jointConfigTab.children + self.jointConfigTab = inputs.addTabCommandInput("jointSettings", "Joint Settings") + self.jointConfigTab.tooltip = "Select and configure robot joints." + jointConfigTabInputs = self.jointConfigTab.children - # TODO: Change background colors and such - Brandon self.jointConfigTable = createTableInput( - "jointTable", - "Joint Table", - jointConfigTabInputs, - 7, - "1:2:2:2:2:2:2", + "jointTable", "Joint Table", jointConfigTabInputs, 7, "1:2:2:2:2:2:2" ) self.jointConfigTable.addCommandInput( - createTextBoxInput( - "jointMotionHeader", - "Motion", - jointConfigTabInputs, - "Motion", - bold=False, - ), - 0, - 0, + createTextBoxInput("jointMotionHeader", "Motion", jointConfigTabInputs, "Motion", bold=False), 0, 0 ) self.jointConfigTable.addCommandInput( - createTextBoxInput("nameHeader", "Name", jointConfigTabInputs, "Joint name", bold=False), - 0, - 1, + createTextBoxInput("nameHeader", "Name", jointConfigTabInputs, "Joint name", bold=False), 0, 1 ) self.jointConfigTable.addCommandInput( createTextBoxInput( - "parentHeader", - "Parent", - jointConfigTabInputs, - "Parent joint", - background="#d9d9d9", + "parentHeader", "Parent", jointConfigTabInputs, "Parent joint", background="#d9d9d9" ), 0, 2, ) self.jointConfigTable.addCommandInput( - createTextBoxInput( - "signalHeader", - "Signal", - jointConfigTabInputs, - "Signal type", - background="#d9d9d9", - ), + createTextBoxInput("signalHeader", "Signal", jointConfigTabInputs, "Signal type", background="#d9d9d9"), 0, 3, ) self.jointConfigTable.addCommandInput( - createTextBoxInput( - "speedHeader", - "Speed", - jointConfigTabInputs, - "Joint Speed", - background="#d9d9d9", - ), + createTextBoxInput("speedHeader", "Speed", jointConfigTabInputs, "Joint Speed", background="#d9d9d9"), 0, 4, ) self.jointConfigTable.addCommandInput( - createTextBoxInput( - "forceHeader", - "Force", - jointConfigTabInputs, - "Joint Force", - background="#d9d9d9", - ), + createTextBoxInput("forceHeader", "Force", jointConfigTabInputs, "Joint Force", background="#d9d9d9"), 0, 5, ) self.jointConfigTable.addCommandInput( - createTextBoxInput( - "wheelHeader", - "Is Wheel", - jointConfigTabInputs, - "Is Wheel", - background="#d9d9d9", - ), + createTextBoxInput("wheelHeader", "Is Wheel", jointConfigTabInputs, "Is Wheel", background="#d9d9d9"), 0, 6, ) @@ -125,57 +83,27 @@ def __init__(self, args: adsk.core.CommandCreatedEventArgs) -> None: ) jointSelect.addSelectionFilter("Joints") jointSelect.setSelectionLimits(0) - - # Visibility is triggered by `addJointInputButton` - jointSelect.isEnabled = jointSelect.isVisible = False + jointSelect.isEnabled = jointSelect.isVisible = False # Visibility is triggered by `addJointInputButton` jointConfigTabInputs.addTextBoxCommandInput("jointTabBlankSpacer", "", "", 1, True) - self.wheelConfigTable = createTableInput( - "wheelTable", - "Wheel Table", - jointConfigTabInputs, - 4, - "1:2:2:2", - ) - + self.wheelConfigTable = createTableInput("wheelTable", "Wheel Table", jointConfigTabInputs, 4, "1:2:2:2") self.wheelConfigTable.addCommandInput( - createTextBoxInput( - "wheelMotionHeader", - "Motion", - jointConfigTabInputs, - "Motion", - bold=False, - ), - 0, - 0, + createTextBoxInput("wheelMotionHeader", "Motion", jointConfigTabInputs, "Motion", bold=False), 0, 0 ) - self.wheelConfigTable.addCommandInput( - createTextBoxInput("name_header", "Name", jointConfigTabInputs, "Joint name", bold=False), - 0, - 1, + createTextBoxInput("name_header", "Name", jointConfigTabInputs, "Joint name", bold=False), 0, 1 ) - self.wheelConfigTable.addCommandInput( createTextBoxInput( - "wheelTypeHeader", - "WheelType", - jointConfigTabInputs, - "Wheel type", - background="#d9d9d9", + "wheelTypeHeader", "WheelType", jointConfigTabInputs, "Wheel type", background="#d9d9d9" ), 0, 2, ) - self.wheelConfigTable.addCommandInput( createTextBoxInput( - "signalTypeHeader", - "SignalType", - jointConfigTabInputs, - "Signal type", - background="#d9d9d9", + "signalTypeHeader", "SignalType", jointConfigTabInputs, "Signal type", background="#d9d9d9" ), 0, 3, @@ -370,6 +298,7 @@ def addWheel(self, joint: adsk.fusion.Joint, wheel: Wheel | None = None) -> None wheelIcon = commandInputs.addImageCommandInput( "wheelPlaceholder", "Placeholder", IconPaths.wheelIcons["standard"] ) + wheelIcon.tooltip = "Standard wheel" wheelName = commandInputs.addTextBoxCommandInput("wheelName", "Joint Name", joint.name, 1, True) wheelName.tooltip = joint.name # TODO: Should this be the same? wheelType = commandInputs.addDropDownCommandInput( @@ -464,9 +393,9 @@ def getSelectedJointsAndWheels(self) -> tuple[list[Joint], list[Wheel]]: jointEntityToken = self.selectedJointList[row - 1].entityToken signalTypeIndex = self.jointConfigTable.getInputAtPosition(row, 3).selectedItem.index signalType = SignalType(signalTypeIndex + 1) - jointSpeed = self.jointConfigTable.getInputAtPosition(row, 4).value - jointForce = self.jointConfigTable.getInputAtPosition(row, 5).value - isWheel = self.jointConfigTable.getInputAtPosition(row, 6).value + jointSpeed: float = self.jointConfigTable.getInputAtPosition(row, 4).value + jointForce: float = self.jointConfigTable.getInputAtPosition(row, 5).value + isWheel: bool = self.jointConfigTable.getInputAtPosition(row, 6).value joints.append( Joint( @@ -500,16 +429,16 @@ def reset(self) -> None: # Transition: AARD-1685 # Find a way to not pass the global commandInputs into this function # Perhaps get the joint tab from the args then get what we want? + # Idk the Fusion API seems to think that you would never need to change anything other than the effected + # commandInput in a input changed handle for some reason. def handleInputChanged( self, args: adsk.core.InputChangedEventArgs, globalCommandInputs: adsk.core.CommandInputs ) -> None: commandInput = args.input - - # TODO: Reorder if commandInput.id == "wheelType": wheelTypeDropdown = adsk.core.DropDownCommandInput.cast(commandInput) position = self.wheelConfigTable.getPosition(wheelTypeDropdown)[1] - iconInput = self.wheelConfigTable.getInputAtPosition(position, 0) + iconInput: adsk.core.ImageCommandInput = self.wheelConfigTable.getInputAtPosition(position, 0) if wheelTypeDropdown.selectedItem.index == 0: iconInput.imageFile = IconPaths.wheelIcons["standard"] @@ -540,15 +469,18 @@ def handleInputChanged( wheelTabPosition = self.jointWheelIndexMap.get(self.selectedJointList[jointTabPosition - 1].entityToken) if wheelTabPosition: - wheelSignalItems: adsk.core.DropDownCommandInput - wheelSignalItems = self.wheelConfigTable.getInputAtPosition(wheelTabPosition, 3) + wheelSignalItems: adsk.core.DropDownCommandInput = self.wheelConfigTable.getInputAtPosition( + wheelTabPosition, 3 + ) wheelSignalItems.listItems.item(signalTypeDropdown.selectedItem.index).isSelected = True elif commandInput.id == "jointAddButton": - jointAddButton = globalCommandInputs.itemById("jointAddButton") - jointRemoveButton = globalCommandInputs.itemById("jointRemoveButton") - jointSelectCancelButton = globalCommandInputs.itemById("jointSelectCancelButton") - jointSelection = globalCommandInputs.itemById("jointSelection") + jointAddButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointAddButton") + jointRemoveButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointRemoveButton") + jointSelectCancelButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById( + "jointSelectCancelButton" + ) + jointSelection: adsk.core.SelectionCommandInput = globalCommandInputs.itemById("jointSelection") jointSelection.isVisible = jointSelection.isEnabled = True jointSelection.clearSelection() @@ -556,8 +488,8 @@ def handleInputChanged( jointSelectCancelButton.isVisible = jointSelectCancelButton.isEnabled = True elif commandInput.id == "jointRemoveButton": - jointAddButton = globalCommandInputs.itemById("jointAddButton") - jointTable = args.inputs.itemById("jointTable") + jointAddButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointAddButton") + jointTable: adsk.core.TableCommandInput = args.inputs.itemById("jointTable") jointAddButton.isEnabled = True @@ -565,14 +497,15 @@ def handleInputChanged( ui = adsk.core.Application.get().userInterface ui.messageBox("Select a row to delete.") else: - # Select Row is 1 indexed - self.removeIndexedJoint(jointTable.selectedRow - 1) + self.removeIndexedJoint(jointTable.selectedRow - 1) # selectedRow is 1 indexed elif commandInput.id == "jointSelectCancelButton": - jointAddButton = globalCommandInputs.itemById("jointAddButton") - jointRemoveButton = globalCommandInputs.itemById("jointRemoveButton") - jointSelectCancelButton = globalCommandInputs.itemById("jointSelectCancelButton") - jointSelection = globalCommandInputs.itemById("jointSelection") + jointAddButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointAddButton") + jointRemoveButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointRemoveButton") + jointSelectCancelButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById( + "jointSelectCancelButton" + ) + jointSelection: adsk.core.SelectionCommandInput = globalCommandInputs.itemById("jointSelection") jointSelection.isEnabled = jointSelection.isVisible = False jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False jointAddButton.isEnabled = jointRemoveButton.isEnabled = True @@ -596,10 +529,11 @@ def handleSelectionEvent(self, args: adsk.core.SelectionEventArgs, selectedJoint selectionInput.isEnabled = selectionInput.isVisible = False def handlePreviewEvent(self, args: adsk.core.CommandEventArgs) -> None: - jointAddButton = args.command.commandInputs.itemById("jointAddButton") - jointRemoveButton = args.command.commandInputs.itemById("jointRemoveButton") - jointSelectCancelButton = args.command.commandInputs.itemById("jointSelectCancelButton") - jointSelection = args.command.commandInputs.itemById("jointSelection") + commandInputs = args.command.commandInputs + jointAddButton: adsk.core.BoolValueCommandInput = commandInputs.itemById("jointAddButton") + jointRemoveButton: adsk.core.BoolValueCommandInput = commandInputs.itemById("jointRemoveButton") + jointSelectCancelButton: adsk.core.BoolValueCommandInput = commandInputs.itemById("jointSelectCancelButton") + jointSelection: adsk.core.SelectionCommandInput = commandInputs.itemById("jointSelection") if self.jointConfigTable.rowCount <= 1: jointRemoveButton.isEnabled = False From 19abf609093e54823fec6a931a05a0f00772c484 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Mon, 1 Jul 2024 15:41:30 -0700 Subject: [PATCH 021/121] Last bit of formatting --- exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py index f734c3171c..f59e96eab7 100644 --- a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py @@ -33,19 +33,15 @@ def __init__(self, args: adsk.core.CommandCreatedEventArgs) -> None: self.jointConfigTab = inputs.addTabCommandInput("jointSettings", "Joint Settings") self.jointConfigTab.tooltip = "Select and configure robot joints." jointConfigTabInputs = self.jointConfigTab.children - self.jointConfigTable = createTableInput( "jointTable", "Joint Table", jointConfigTabInputs, 7, "1:2:2:2:2:2:2" ) - self.jointConfigTable.addCommandInput( createTextBoxInput("jointMotionHeader", "Motion", jointConfigTabInputs, "Motion", bold=False), 0, 0 ) - self.jointConfigTable.addCommandInput( createTextBoxInput("nameHeader", "Name", jointConfigTabInputs, "Joint name", bold=False), 0, 1 ) - self.jointConfigTable.addCommandInput( createTextBoxInput( "parentHeader", "Parent", jointConfigTabInputs, "Parent joint", background="#d9d9d9" @@ -53,25 +49,21 @@ def __init__(self, args: adsk.core.CommandCreatedEventArgs) -> None: 0, 2, ) - self.jointConfigTable.addCommandInput( createTextBoxInput("signalHeader", "Signal", jointConfigTabInputs, "Signal type", background="#d9d9d9"), 0, 3, ) - self.jointConfigTable.addCommandInput( createTextBoxInput("speedHeader", "Speed", jointConfigTabInputs, "Joint Speed", background="#d9d9d9"), 0, 4, ) - self.jointConfigTable.addCommandInput( createTextBoxInput("forceHeader", "Force", jointConfigTabInputs, "Joint Force", background="#d9d9d9"), 0, 5, ) - self.jointConfigTable.addCommandInput( createTextBoxInput("wheelHeader", "Is Wheel", jointConfigTabInputs, "Is Wheel", background="#d9d9d9"), 0, From 004f857b93f97d8ad32f5266d823e682ccfab855 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Mon, 1 Jul 2024 15:50:00 -0700 Subject: [PATCH 022/121] Cleanup catches and try except logging --- .../src/UI/ConfigCommand.py | 42 +- .../src/UI/JointConfigTab.py | 374 +++++++++--------- 2 files changed, 192 insertions(+), 224 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 0ecdc91faa..b2632902a1 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -291,7 +291,6 @@ def notify(self, args): # Transition: AARD-1685 # Should consider changing how the parser handles wheels and joints to avoid overlap if exporterOptions.wheels: - pass for wheel in exporterOptions.wheels: fusionJoint = gm.app.activeDocument.design.findEntityByToken(wheel.jointToken)[0] jointConfigTab.addWheel(fusionJoint, wheel) @@ -841,38 +840,10 @@ def notify(self, args): name = design.rootComponent.name.rsplit(" ", 1)[0] version = design.rootComponent.name.rsplit(" ", 1)[1] - _exportWheels = [] # all selected wheels, formatted for parseOptions _exportGamepieces = [] # TODO work on the code to populate Gamepiece _robotWeight = float _mode = ExportMode.ROBOT - # Transition: AARD-1865 - """ - Loops through all rows in the wheel table to extract all the input values - """ - # onSelect = gm.handlers[3] - # wheelTableInput = wheelTable() - # for row in range(wheelTableInput.rowCount): - # if row == 0: - # continue - - # wheelTypeIndex = wheelTableInput.getInputAtPosition( - # row, 2 - # ).selectedItem.index # This must be either 0 or 1 for standard or omni - - # signalTypeIndex = wheelTableInput.getInputAtPosition( - # row, 3 - # ).selectedItem.index - - # _exportWheels.append( - # Wheel( - # WheelListGlobal[row - 1].entityToken, - # WheelType(wheelTypeIndex + 1), - # SignalType(signalTypeIndex + 1), - # # onSelect.wheelJointList[row-1][0] # GUID of wheel joint. if no joint found, default to None - # ) - # ) - """ Loops through all rows in the gamepiece table to extract the input values """ @@ -1285,7 +1256,6 @@ def __init__(self, cmd): self.allWeights = [None, None] # [lbs, kg] self.isLbs = True self.isLbs_f = True - self.called = False def reset(self): """### Process: @@ -1372,23 +1342,13 @@ def notify(self, args): gamepieceConfig.isVisible = False weightTableInput.isVisible = True - # addFieldInput.isEnabled = wheelConfig.isVisible = ( - # jointConfig.isVisible - # ) = True + addFieldInput.isEnabled = True elif modeDropdown.selectedItem.index == 1: if gamepieceConfig: gm.ui.activeSelections.clear() gm.app.activeDocument.design.rootComponent.opacity = 1 - # addWheelInput.isEnabled = addJointButton.isEnabled = ( - # gamepieceConfig.isVisible - # ) = True - - # jointConfig.isVisible = wheelConfig.isVisible = ( - # weightTableInput.isVisible - # ) = False - elif cmdInput.id == "blank_gp" or cmdInput.id == "name_gp" or cmdInput.id == "weight_gp": self.reset() diff --git a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py index f59e96eab7..e6f79b0f77 100644 --- a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py @@ -113,10 +113,8 @@ def __init__(self, args: adsk.core.CommandCreatedEventArgs) -> None: self.jointConfigTable.addToolbarCommandInput(jointSelectCancelButton) self.reset() - except: - logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.createJointConfigTab()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) def addJoint(self, fusionJoint: adsk.fusion.Joint, synJoint: Joint | None = None) -> bool: try: @@ -276,65 +274,64 @@ def addJoint(self, fusionJoint: adsk.fusion.Joint, synJoint: Joint | None = None ) self.previousWheelCheckboxState.append(isWheel) - except: - logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.addJointToConfigTab()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) return True def addWheel(self, joint: adsk.fusion.Joint, wheel: Wheel | None = None) -> None: - self.jointWheelIndexMap[joint.entityToken] = self.wheelConfigTable.rowCount - - commandInputs = self.wheelConfigTable.commandInputs - wheelIcon = commandInputs.addImageCommandInput( - "wheelPlaceholder", "Placeholder", IconPaths.wheelIcons["standard"] - ) - wheelIcon.tooltip = "Standard wheel" - wheelName = commandInputs.addTextBoxCommandInput("wheelName", "Joint Name", joint.name, 1, True) - wheelName.tooltip = joint.name # TODO: Should this be the same? - wheelType = commandInputs.addDropDownCommandInput( - "wheelType", "Wheel Type", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle - ) - - selectedWheelType = wheel.wheelType if wheel else WheelType.STANDARD - wheelType.listItems.add("Standard", selectedWheelType is WheelType.STANDARD, "") - wheelType.listItems.add("OMNI", selectedWheelType is WheelType.OMNI, "") - wheelType.listItems.add("Mecanum", selectedWheelType is WheelType.MECANUM, "") - wheelType.tooltip = "Wheel type" - wheelType.tooltipDescription = "".join( - [ - "
Omni-directional wheels can be used just like regular drive wheels", - "but they have the advantage of being able to roll freely perpendicular to", - "the drive direction.
", - ] - ) - - signalType = commandInputs.addDropDownCommandInput( - "wheelSignalType", "Signal Type", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle - ) - signalType.isFullWidth = True - signalType.isEnabled = False - signalType.tooltip = "Wheel signal type is linked with the respective joint signal type." - i = self.selectedJointList.index(joint) - jointSignalType = SignalType(self.jointConfigTable.getInputAtPosition(i + 1, 3).selectedItem.index + 1) - signalType.listItems.add("‎", jointSignalType is SignalType.PWM, IconPaths.signalIcons["PWM"]) - signalType.listItems.add("‎", jointSignalType is SignalType.CAN, IconPaths.signalIcons["CAN"]) - signalType.listItems.add("‎", jointSignalType is SignalType.PASSIVE, IconPaths.signalIcons["PASSIVE"]) - - row = self.wheelConfigTable.rowCount - self.wheelConfigTable.addCommandInput(wheelIcon, row, 0) - self.wheelConfigTable.addCommandInput(wheelName, row, 1) - self.wheelConfigTable.addCommandInput(wheelType, row, 2) - self.wheelConfigTable.addCommandInput(signalType, row, 3) + try: + self.jointWheelIndexMap[joint.entityToken] = self.wheelConfigTable.rowCount + + commandInputs = self.wheelConfigTable.commandInputs + wheelIcon = commandInputs.addImageCommandInput( + "wheelPlaceholder", "Placeholder", IconPaths.wheelIcons["standard"] + ) + wheelIcon.tooltip = "Standard wheel" + wheelName = commandInputs.addTextBoxCommandInput("wheelName", "Joint Name", joint.name, 1, True) + wheelName.tooltip = joint.name # TODO: Should this be the same? + wheelType = commandInputs.addDropDownCommandInput( + "wheelType", "Wheel Type", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle + ) + + selectedWheelType = wheel.wheelType if wheel else WheelType.STANDARD + wheelType.listItems.add("Standard", selectedWheelType is WheelType.STANDARD, "") + wheelType.listItems.add("OMNI", selectedWheelType is WheelType.OMNI, "") + wheelType.listItems.add("Mecanum", selectedWheelType is WheelType.MECANUM, "") + wheelType.tooltip = "Wheel type" + wheelType.tooltipDescription = "".join( + [ + "
Omni-directional wheels can be used just like regular drive wheels", + "but they have the advantage of being able to roll freely perpendicular to", + "the drive direction.
", + ] + ) + + signalType = commandInputs.addDropDownCommandInput( + "wheelSignalType", "Signal Type", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle + ) + signalType.isFullWidth = True + signalType.isEnabled = False + signalType.tooltip = "Wheel signal type is linked with the respective joint signal type." + i = self.selectedJointList.index(joint) + jointSignalType = SignalType(self.jointConfigTable.getInputAtPosition(i + 1, 3).selectedItem.index + 1) + signalType.listItems.add("‎", jointSignalType is SignalType.PWM, IconPaths.signalIcons["PWM"]) + signalType.listItems.add("‎", jointSignalType is SignalType.CAN, IconPaths.signalIcons["CAN"]) + signalType.listItems.add("‎", jointSignalType is SignalType.PASSIVE, IconPaths.signalIcons["PASSIVE"]) + + row = self.wheelConfigTable.rowCount + self.wheelConfigTable.addCommandInput(wheelIcon, row, 0) + self.wheelConfigTable.addCommandInput(wheelName, row, 1) + self.wheelConfigTable.addCommandInput(wheelType, row, 2) + self.wheelConfigTable.addCommandInput(signalType, row, 3) + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) def removeIndexedJoint(self, index: int) -> None: try: self.removeJoint(self.selectedJointList[index]) - except: - logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.removeIndexedJointFromConfigTab()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) def removeJoint(self, joint: adsk.fusion.Joint) -> None: try: @@ -360,10 +357,8 @@ def removeJoint(self, joint: adsk.fusion.Joint) -> None: listItems.item(i).deleteMe() else: listItems.item(i).deleteMe() - except: - logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.removeJointFromConfigTab()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) def removeWheel(self, joint: adsk.fusion.Joint) -> None: try: @@ -373,45 +368,46 @@ def removeWheel(self, joint: adsk.fusion.Joint) -> None: for key, value in self.jointWheelIndexMap.items(): if value > row - 1: self.jointWheelIndexMap[key] -= 1 - except: - logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab.removeJointFromConfigTab()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) def getSelectedJointsAndWheels(self) -> tuple[list[Joint], list[Wheel]]: - joints: list[Joint] = [] - wheels: list[Wheel] = [] - for row in range(1, self.jointConfigTable.rowCount): # Row is 1 indexed - jointEntityToken = self.selectedJointList[row - 1].entityToken - signalTypeIndex = self.jointConfigTable.getInputAtPosition(row, 3).selectedItem.index - signalType = SignalType(signalTypeIndex + 1) - jointSpeed: float = self.jointConfigTable.getInputAtPosition(row, 4).value - jointForce: float = self.jointConfigTable.getInputAtPosition(row, 5).value - isWheel: bool = self.jointConfigTable.getInputAtPosition(row, 6).value - - joints.append( - Joint( - jointEntityToken, - JointParentType.ROOT, - signalType, - jointSpeed, - jointForce / 100.0, - isWheel, - ) - ) - - if isWheel: - wheelRow = self.jointWheelIndexMap[jointEntityToken] - wheelTypeIndex = self.wheelConfigTable.getInputAtPosition(wheelRow, 2).selectedItem.index - wheels.append( - Wheel( + try: + joints: list[Joint] = [] + wheels: list[Wheel] = [] + for row in range(1, self.jointConfigTable.rowCount): # Row is 1 indexed + jointEntityToken = self.selectedJointList[row - 1].entityToken + signalTypeIndex = self.jointConfigTable.getInputAtPosition(row, 3).selectedItem.index + signalType = SignalType(signalTypeIndex + 1) + jointSpeed: float = self.jointConfigTable.getInputAtPosition(row, 4).value + jointForce: float = self.jointConfigTable.getInputAtPosition(row, 5).value + isWheel: bool = self.jointConfigTable.getInputAtPosition(row, 6).value + + joints.append( + Joint( jointEntityToken, - WheelType(wheelTypeIndex + 1), + JointParentType.ROOT, signalType, + jointSpeed, + jointForce / 100.0, + isWheel, ) ) - return (joints, wheels) + if isWheel: + wheelRow = self.jointWheelIndexMap[jointEntityToken] + wheelTypeIndex = self.wheelConfigTable.getInputAtPosition(wheelRow, 2).selectedItem.index + wheels.append( + Wheel( + jointEntityToken, + WheelType(wheelTypeIndex + 1), + signalType, + ) + ) + + return (joints, wheels) + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) def reset(self) -> None: self.selectedJointList.clear() @@ -426,110 +422,122 @@ def reset(self) -> None: def handleInputChanged( self, args: adsk.core.InputChangedEventArgs, globalCommandInputs: adsk.core.CommandInputs ) -> None: - commandInput = args.input - if commandInput.id == "wheelType": - wheelTypeDropdown = adsk.core.DropDownCommandInput.cast(commandInput) - position = self.wheelConfigTable.getPosition(wheelTypeDropdown)[1] - iconInput: adsk.core.ImageCommandInput = self.wheelConfigTable.getInputAtPosition(position, 0) - - if wheelTypeDropdown.selectedItem.index == 0: - iconInput.imageFile = IconPaths.wheelIcons["standard"] - iconInput.tooltip = "Standard wheel" - elif wheelTypeDropdown.selectedItem.index == 1: - iconInput.imageFile = IconPaths.wheelIcons["omni"] - iconInput.tooltip = "Omni wheel" - elif wheelTypeDropdown.selectedItem.index == 2: - iconInput.imageFile = IconPaths.wheelIcons["mecanum"] - iconInput.tooltip = "Mecanum wheel" - - elif commandInput.id == "isWheel": - isWheelCheckbox = adsk.core.BoolValueCommandInput.cast(commandInput) - position = self.jointConfigTable.getPosition(isWheelCheckbox)[1] - 1 - isAlreadyWheel = bool(self.jointWheelIndexMap.get(self.selectedJointList[position].entityToken)) - - if isWheelCheckbox.value != self.previousWheelCheckboxState[position]: - if not isAlreadyWheel: - self.addWheel(self.selectedJointList[position]) - else: - self.removeWheel(self.selectedJointList[position]) + try: + commandInput = args.input + if commandInput.id == "wheelType": + wheelTypeDropdown = adsk.core.DropDownCommandInput.cast(commandInput) + position = self.wheelConfigTable.getPosition(wheelTypeDropdown)[1] + iconInput: adsk.core.ImageCommandInput = self.wheelConfigTable.getInputAtPosition(position, 0) + + if wheelTypeDropdown.selectedItem.index == 0: + iconInput.imageFile = IconPaths.wheelIcons["standard"] + iconInput.tooltip = "Standard wheel" + elif wheelTypeDropdown.selectedItem.index == 1: + iconInput.imageFile = IconPaths.wheelIcons["omni"] + iconInput.tooltip = "Omni wheel" + elif wheelTypeDropdown.selectedItem.index == 2: + iconInput.imageFile = IconPaths.wheelIcons["mecanum"] + iconInput.tooltip = "Mecanum wheel" + + elif commandInput.id == "isWheel": + isWheelCheckbox = adsk.core.BoolValueCommandInput.cast(commandInput) + position = self.jointConfigTable.getPosition(isWheelCheckbox)[1] - 1 + isAlreadyWheel = bool(self.jointWheelIndexMap.get(self.selectedJointList[position].entityToken)) + + if isWheelCheckbox.value != self.previousWheelCheckboxState[position]: + if not isAlreadyWheel: + self.addWheel(self.selectedJointList[position]) + else: + self.removeWheel(self.selectedJointList[position]) - self.previousWheelCheckboxState[position] = isWheelCheckbox.value + self.previousWheelCheckboxState[position] = isWheelCheckbox.value - elif commandInput.id == "signalTypeJoint": - signalTypeDropdown = adsk.core.DropDownCommandInput.cast(commandInput) - jointTabPosition = self.jointConfigTable.getPosition(signalTypeDropdown)[1] # 1 indexed - wheelTabPosition = self.jointWheelIndexMap.get(self.selectedJointList[jointTabPosition - 1].entityToken) + elif commandInput.id == "signalTypeJoint": + signalTypeDropdown = adsk.core.DropDownCommandInput.cast(commandInput) + jointTabPosition = self.jointConfigTable.getPosition(signalTypeDropdown)[1] # 1 indexed + wheelTabPosition = self.jointWheelIndexMap.get(self.selectedJointList[jointTabPosition - 1].entityToken) - if wheelTabPosition: - wheelSignalItems: adsk.core.DropDownCommandInput = self.wheelConfigTable.getInputAtPosition( - wheelTabPosition, 3 - ) - wheelSignalItems.listItems.item(signalTypeDropdown.selectedItem.index).isSelected = True + if wheelTabPosition: + wheelSignalItems: adsk.core.DropDownCommandInput = self.wheelConfigTable.getInputAtPosition( + wheelTabPosition, 3 + ) + wheelSignalItems.listItems.item(signalTypeDropdown.selectedItem.index).isSelected = True - elif commandInput.id == "jointAddButton": - jointAddButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointAddButton") - jointRemoveButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointRemoveButton") - jointSelectCancelButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById( - "jointSelectCancelButton" - ) - jointSelection: adsk.core.SelectionCommandInput = globalCommandInputs.itemById("jointSelection") + elif commandInput.id == "jointAddButton": + jointAddButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointAddButton") + jointRemoveButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointRemoveButton") + jointSelectCancelButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById( + "jointSelectCancelButton" + ) + jointSelection: adsk.core.SelectionCommandInput = globalCommandInputs.itemById("jointSelection") - jointSelection.isVisible = jointSelection.isEnabled = True - jointSelection.clearSelection() - jointAddButton.isEnabled = jointRemoveButton.isEnabled = False - jointSelectCancelButton.isVisible = jointSelectCancelButton.isEnabled = True + jointSelection.isVisible = jointSelection.isEnabled = True + jointSelection.clearSelection() + jointAddButton.isEnabled = jointRemoveButton.isEnabled = False + jointSelectCancelButton.isVisible = jointSelectCancelButton.isEnabled = True - elif commandInput.id == "jointRemoveButton": - jointAddButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointAddButton") - jointTable: adsk.core.TableCommandInput = args.inputs.itemById("jointTable") + elif commandInput.id == "jointRemoveButton": + jointAddButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointAddButton") + jointTable: adsk.core.TableCommandInput = args.inputs.itemById("jointTable") - jointAddButton.isEnabled = True + jointAddButton.isEnabled = True - if jointTable.selectedRow == -1 or jointTable.selectedRow == 0: - ui = adsk.core.Application.get().userInterface - ui.messageBox("Select a row to delete.") - else: - self.removeIndexedJoint(jointTable.selectedRow - 1) # selectedRow is 1 indexed + if jointTable.selectedRow == -1 or jointTable.selectedRow == 0: + ui = adsk.core.Application.get().userInterface + ui.messageBox("Select a row to delete.") + else: + self.removeIndexedJoint(jointTable.selectedRow - 1) # selectedRow is 1 indexed - elif commandInput.id == "jointSelectCancelButton": - jointAddButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointAddButton") - jointRemoveButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointRemoveButton") - jointSelectCancelButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById( - "jointSelectCancelButton" - ) - jointSelection: adsk.core.SelectionCommandInput = globalCommandInputs.itemById("jointSelection") - jointSelection.isEnabled = jointSelection.isVisible = False - jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False - jointAddButton.isEnabled = jointRemoveButton.isEnabled = True + elif commandInput.id == "jointSelectCancelButton": + jointAddButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointAddButton") + jointRemoveButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointRemoveButton") + jointSelectCancelButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById( + "jointSelectCancelButton" + ) + jointSelection: adsk.core.SelectionCommandInput = globalCommandInputs.itemById("jointSelection") + jointSelection.isEnabled = jointSelection.isVisible = False + jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False + jointAddButton.isEnabled = jointRemoveButton.isEnabled = True + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) def handleSelectionEvent(self, args: adsk.core.SelectionEventArgs, selectedJoint: adsk.fusion.Joint) -> None: - selectionInput = args.activeInput - jointType = selectedJoint.jointMotion.jointType - if jointType == adsk.fusion.JointTypes.RevoluteJointType or jointType == adsk.fusion.JointTypes.SliderJointType: - if not self.addJoint(selectedJoint): - ui = adsk.core.Application.get().userInterface - result = ui.messageBox( - "You have already selected this joint.\n" "Would you like to remove it?", - "Synthesis: Remove Joint Confirmation", - adsk.core.MessageBoxButtonTypes.YesNoButtonType, - adsk.core.MessageBoxIconTypes.QuestionIconType, - ) + try: + selectionInput = args.activeInput + jointType = selectedJoint.jointMotion.jointType + if ( + jointType == adsk.fusion.JointTypes.RevoluteJointType + or jointType == adsk.fusion.JointTypes.SliderJointType + ): + if not self.addJoint(selectedJoint): + ui = adsk.core.Application.get().userInterface + result = ui.messageBox( + "You have already selected this joint.\n" "Would you like to remove it?", + "Synthesis: Remove Joint Confirmation", + adsk.core.MessageBoxButtonTypes.YesNoButtonType, + adsk.core.MessageBoxIconTypes.QuestionIconType, + ) - if result == adsk.core.DialogResults.DialogYes: - self.removeJoint(selectedJoint) + if result == adsk.core.DialogResults.DialogYes: + self.removeJoint(selectedJoint) - selectionInput.isEnabled = selectionInput.isVisible = False + selectionInput.isEnabled = selectionInput.isVisible = False + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) def handlePreviewEvent(self, args: adsk.core.CommandEventArgs) -> None: - commandInputs = args.command.commandInputs - jointAddButton: adsk.core.BoolValueCommandInput = commandInputs.itemById("jointAddButton") - jointRemoveButton: adsk.core.BoolValueCommandInput = commandInputs.itemById("jointRemoveButton") - jointSelectCancelButton: adsk.core.BoolValueCommandInput = commandInputs.itemById("jointSelectCancelButton") - jointSelection: adsk.core.SelectionCommandInput = commandInputs.itemById("jointSelection") - - if self.jointConfigTable.rowCount <= 1: - jointRemoveButton.isEnabled = False - - if not jointSelection.isEnabled: - jointAddButton.isEnabled = jointRemoveButton.isEnabled = True - jointSelectCancelButton.isVisible = jointSelectCancelButton.isEnabled = False + try: + commandInputs = args.command.commandInputs + jointAddButton: adsk.core.BoolValueCommandInput = commandInputs.itemById("jointAddButton") + jointRemoveButton: adsk.core.BoolValueCommandInput = commandInputs.itemById("jointRemoveButton") + jointSelectCancelButton: adsk.core.BoolValueCommandInput = commandInputs.itemById("jointSelectCancelButton") + jointSelection: adsk.core.SelectionCommandInput = commandInputs.itemById("jointSelection") + + if self.jointConfigTable.rowCount <= 1: + jointRemoveButton.isEnabled = False + + if not jointSelection.isEnabled: + jointAddButton.isEnabled = jointRemoveButton.isEnabled = True + jointSelectCancelButton.isVisible = jointSelectCancelButton.isEnabled = False + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) From 9a099f2bdd7a31bb217d479575ee0347d3978967 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Mon, 1 Jul 2024 15:53:31 -0700 Subject: [PATCH 023/121] Helper module logging updates --- .../src/UI/CreateCommandInputsHelper.py | 8 +++++--- exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/UI/CreateCommandInputsHelper.py b/exporter/SynthesisFusionAddin/src/UI/CreateCommandInputsHelper.py index 243bd1c386..ac3996b537 100644 --- a/exporter/SynthesisFusionAddin/src/UI/CreateCommandInputsHelper.py +++ b/exporter/SynthesisFusionAddin/src/UI/CreateCommandInputsHelper.py @@ -3,6 +3,8 @@ import adsk.core +from ..general_imports import INTERNAL_ID + def createTableInput( id: str, @@ -24,7 +26,7 @@ def createTableInput( return input except BaseException: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}.createTableInput()").error( + logging.getLogger("{INTERNAL_ID}.UI.CreateCommandInputsHelper").error( "Failed:\n{}".format(traceback.format_exc()) ) @@ -48,7 +50,7 @@ def createBooleanInput( return input except BaseException: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}.createBooleanInput()").error( + logging.getLogger("{INTERNAL_ID}.UI.CreateCommandInputsHelper").error( "Failed:\n{}".format(traceback.format_exc()) ) @@ -89,6 +91,6 @@ def createTextBoxInput( return input except BaseException: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.createTextBoxInput()").error( + logging.getLogger("{INTERNAL_ID}.UI.CreateCommandInputsHelper").error( "Failed:\n{}".format(traceback.format_exc()) ) diff --git a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py index e6f79b0f77..5f9be302b3 100644 --- a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py @@ -4,6 +4,7 @@ import adsk.core import adsk.fusion +from ..general_imports import INTERNAL_ID from ..Parser.ExporterOptions import ( Joint, JointParentType, From c93deb4b0d5f85139924191509c6443f902181a1 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Wed, 3 Jul 2024 11:58:03 -0700 Subject: [PATCH 024/121] added docs to highest level upload file function --- fission/bun.lockb | Bin 305776 -> 307406 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/fission/bun.lockb b/fission/bun.lockb index e8c02486c5fdb040e68adeb96af72f2bad4b5df1..ca97e01c211453fc02dbe43b08f7a213e9aaeda5 100755 GIT binary patch delta 58746 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 PfDTpl!QAHhCl~)8pgdrB delta 57578 zcmeFadz?+>-~Ye&9-CRCiAfkqqMXXeIL&C-Lx>@1ikuRIVa8!FV;Gf&%3(;Ql`bkt z4v|#y5t5=(MyXVaq*5uAN>X(EJzr~GoB8zZ{`~Im<9FY`zgiEkd0+46bv~?Xt+i+N z^5%hazJKqWdzv)bG-g!pT~DTca{DjCmcRbW?ztO2Xq{8N-yOx3e}1`W!(C(V|8!-! zfOpL8UAuV7&mrIOema4`lu>{L?D(Ag>}+TjentHJ?5rW<@khr50%zfuVwJuR@v@yM zRkkbs+1Pg2IP9>3>G z3|8q6P+?u{c5DsoYHUsHT&(itV^v-@R!tr|reOS#f`Y)g=Xiecl%fiLAM>*E#^eqS z1hNYT7v_!{8u;kkK!7w;O0jDGIxPRDEV2A<>)(P^{0Pghx4a#;8oaUft75C;|4Cx; zZ?IJY-2zkg5Fl>Kt5{X|jP)PFDq#the^bWuqbha~R_VK972nGG7rR?Sm$iO|(bgzG zK)2N2=oA->ADWb%Hz9BvoS~btkU`ftt+eSoxs76K7tdr!6`Ng@JtT{T@-0J0@l!s> zGJsRYrNYb#p58z5aHfwxN(#Y&duUr2mtXi=St5&SSY9T#>RXG!L za);zler^FZVOKJREUfdU+;owhCR^Z;oUB~p1AW{BG1ZH^UF^pW&CVX1ls#@#JA4(F zjMchaL}ygZy;xN{zrJt1b)S@5Z~|ZH4_W@N2L5?6+xnBSTK6-^uXN*c^ClIHofMde zUpWvMG9{OQ5{yk6#R!ZKtZL*Z>~DD(n@IfkjI!ox<|Y2Byy8-S^7mPO*!o=?`{}mB zHO|KyyC0Ujp!laGfA}V4jT)6(Fg~!JXr0fGVQI&dUz_;Deiy!m|DMbIr85JoVL!jA zKOR%C8jpOenmv5fn86y&PRV{bWAcV*^aB&~v&PcRfX!cgx`5ogob3GE@dcc)qjHDm zjK7?W8ko^p!*hpZ6^yrM(Uc#X`3>3F+;51%>a4s4t5xr-Wujs&@ z9#?)N*b1%mwl@{wS0BwnlwK)-CbP1dhue?Y7FOLHMX~6b>5D}Y79%-+Eaoy zsE)!y?HYl=dlA14JFpt_XxrXR_w#RrEB(%Pe*WjIU4hkUKR9=2ZqkT?xPWtopJ27y zzC!JUm+*9!ufnSOMeeu?)oaY9FnQ;6vJpKw0$JgmH)bi`CO)u}y%=b3`kWmFi40T|eyEwLX z@qugnk&T|+qwpJ3Nsd}y7jaY0vekZtU$+*sMO*^cE-x#ZIorhJv3fL;x{Op1; zqb6hr5_|cVkh8Ix*#MToFP@TZ8_%XUY9#rO(;Lk)^&|}&l{GxtP=6aM=2FqmuQ=G> zuV7eTf4u&C1^l1+zo(&RQQ`ZPCtHfu$PK;0@5m>_s{>y%qURSA@NNLG-}mrWz!wAj zp1g~%9!4iVI)TxJ5nTw;1+szkHA(+G3rABjX^`LZA=qm8PL|)XY^Ncxbzxcah z{?Ztmo1evw6&QUJ)55WJxZlt5IoWx+!zKluwSIJi{WHgpADx?*J2tDJAiAOdb%Xwh zbgKTo5q|$>U^U=Vn0|QilyTr`Qyg; z{f?IN1`VqmV2ORn2&jeQu&Qv)SUWQX1=$4*;t;q7>>8|2n&{fEX7gQ%Rl_Ic`#npw z>FY2V;$t;@9P#)%36qFdhiWUGmRPi)5xL`Yl`yv;$s6UWg?<4QZH5q54IQ1GKRmk` zg;@SM=_l}PulO+R$h1p3c$l1rpoIwWVz$lRe> z`D`~shvw#w{fezfJs(H=wG^}RCXLP- NsV&FJLK2?0JT~b}_5*sxp&r9&^&3^oH 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 E16YI2!~g&Q From f93a636bf78d61c1ec384f9edfa2ab89f8679306 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Wed, 3 Jul 2024 13:49:16 -0700 Subject: [PATCH 025/121] Updated synthesis logging module --- exporter/SynthesisFusionAddin/Synthesis.py | 97 +- exporter/SynthesisFusionAddin/proto/deps.py | 21 +- .../src/Analytics/alert.py | 19 - .../src/Analytics/poster.py | 219 -- .../src/Analyzer/sniff.py | 148 - .../src/Analyzer/timer.py | 70 - .../SynthesisFusionAddin/src/GlobalManager.py | 13 +- .../src/Parser/ExporterOptions.py | 28 +- .../src/Parser/SynthesisParser/Components.py | 50 +- .../Parser/SynthesisParser/JointHierarchy.py | 92 +- .../src/Parser/SynthesisParser/Joints.py | 126 +- .../src/Parser/SynthesisParser/Materials.py | 128 +- .../src/Parser/SynthesisParser/Parser.py | 388 +-- .../SynthesisParser/PhysicalProperties.py | 35 +- .../src/Parser/SynthesisParser/RigidGroup.py | 28 +- .../src/Parser/SynthesisParser/Utilities.py | 2 +- .../SynthesisFusionAddin/src/UI/Camera.py | 53 +- .../src/UI/ConfigCommand.py | 3065 ++++++++--------- .../src/UI/CustomGraphics.py | 121 +- exporter/SynthesisFusionAddin/src/UI/HUI.py | 27 +- .../SynthesisFusionAddin/src/UI/Helper.py | 8 - .../src/UI/MarkingMenu.py | 392 +-- .../SynthesisFusionAddin/src/UI/Toolbar.py | 57 +- .../SynthesisFusionAddin/src/configure.py | 7 +- .../src/general_imports.py | 26 +- exporter/SynthesisFusionAddin/src/logging.py | 107 +- exporter/SynthesisFusionAddin/src/strings.py | 2 +- 27 files changed, 2338 insertions(+), 2991 deletions(-) delete mode 100644 exporter/SynthesisFusionAddin/src/Analytics/alert.py delete mode 100644 exporter/SynthesisFusionAddin/src/Analytics/poster.py delete mode 100644 exporter/SynthesisFusionAddin/src/Analyzer/sniff.py delete mode 100644 exporter/SynthesisFusionAddin/src/Analyzer/timer.py diff --git a/exporter/SynthesisFusionAddin/Synthesis.py b/exporter/SynthesisFusionAddin/Synthesis.py index 335a2416ce..e0e816785c 100644 --- a/exporter/SynthesisFusionAddin/Synthesis.py +++ b/exporter/SynthesisFusionAddin/Synthesis.py @@ -7,115 +7,100 @@ import adsk.core from .src.configure import setAnalytics, unload_config -from .src.general_imports import APP_NAME, DESCRIPTION, INTERNAL_ID, gm, root_logger +from .src.general_imports import APP_NAME, DESCRIPTION, INTERNAL_ID, gm +from .src.Logging import getLogger, logFailure, setupLogger from .src.Types.OString import OString from .src.UI import HUI, Camera, ConfigCommand, Handlers, Helper, MarkingMenu from .src.UI.Toolbar import Toolbar +@logFailure def run(_): """## Entry point to application from Fusion. Arguments: **context** *context* -- Fusion context to derive app and UI. """ + # Remove all items prior to start just to make sure + setupLogger() + unregister_all() - try: - # Remove all items prior to start just to make sure - unregister_all() + # creates the UI elements + register_ui() - # creates the UI elements - register_ui() + app = adsk.core.Application.get() + ui = app.userInterface - app = adsk.core.Application.get() - ui = app.userInterface - - MarkingMenu.setupMarkingMenu(ui) - - except: - logging.getLogger(f"{INTERNAL_ID}").error("Failed:\n{}".format(traceback.format_exc())) + MarkingMenu.setupMarkingMenu(ui) +@logFailure def stop(_): """## Fusion exit point - deconstructs buttons and handlers Arguments: **context** *context* -- Fusion Data. """ - try: - unregister_all() - - app = adsk.core.Application.get() - ui = app.userInterface + unregister_all() - MarkingMenu.stopMarkingMenu(ui) + app = adsk.core.Application.get() + ui = app.userInterface - # nm.deleteMe() + MarkingMenu.stopMarkingMenu(ui) - # should make a logger class - handlers = root_logger.handlers[:] - for handler in handlers: - handler.close() - # I think this will clear the file completely - # root_logger.removeHandler(handler) + # nm.deleteMe() - unload_config() + logger = getLogger(INTERNAL_ID) + logger.cleanupHandlers() - for file in gm.files: - try: - os.remove(file) - except OSError: - pass + unload_config() - # removes path so that proto files don't get confused + for file in gm.files: + try: + os.remove(file) + except OSError: + pass - import sys + # removes path so that proto files don't get confused - path = os.path.abspath(os.path.dirname(__file__)) + import sys - path_proto_files = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "proto", "proto_out")) + path = os.path.abspath(os.path.dirname(__file__)) - if path in sys.path: - sys.path.remove(path) + path_proto_files = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "proto", "proto_out")) - if path_proto_files in sys.path: - sys.path.remove(path_proto_files) + if path in sys.path: + sys.path.remove(path) - except: - logging.getLogger(f"{INTERNAL_ID}").error("Failed:\n{}".format(traceback.format_exc())) + if path_proto_files in sys.path: + sys.path.remove(path_proto_files) +@logFailure def unregister_all() -> None: """Unregisters all UI elements in case any still exist. - Good place to unregister custom app events that may repeat. """ - try: - Camera.clearIconCache() + Camera.clearIconCache() - for element in gm.elements: - element.deleteMe() + for element in gm.elements: + element.deleteMe() - for tab in gm.tabs: - tab.deleteMe() - - except: - logging.getLogger(f"{INTERNAL_ID}").error("Failed:\n{}".format(traceback.format_exc())) + for tab in gm.tabs: + tab.deleteMe() +@logFailure def register_ui() -> None: """#### Generic Function to add all UI objects in a simple non destructive way.""" - - # if A_EP: - # A_EP.send_view("open") - # toolbar = Toolbar('SketchFab') work_panel = Toolbar.getNewPanel(f"{APP_NAME}", f"{INTERNAL_ID}", "ToolsTab") commandButton = HUI.HButton( "Synthesis Exporter", work_panel, - Helper.check_solid_open, + lambda *_: True, ConfigCommand.ConfigureCommandCreatedHandler, description=f"{DESCRIPTION}", command=True, diff --git a/exporter/SynthesisFusionAddin/proto/deps.py b/exporter/SynthesisFusionAddin/proto/deps.py index ea6da5a406..c4216ea710 100644 --- a/exporter/SynthesisFusionAddin/proto/deps.py +++ b/exporter/SynthesisFusionAddin/proto/deps.py @@ -7,8 +7,10 @@ import adsk.fusion from src.general_imports import INTERNAL_ID +from src.Logging import getLogger, logFailure system = platform.system() +logger = getLogger(f"{INTERNAL_ID}.{__name__}") def getPythonFolder() -> str: @@ -35,7 +37,7 @@ def getPythonFolder() -> str: else: raise ImportError("Unsupported platform! This add-in only supports windows and macos") - logging.getLogger(f"{INTERNAL_ID}").debug(f"Python Folder -> {pythonFolder}") + logger.debug(f"Python Folder -> {pythonFolder}") return pythonFolder @@ -50,12 +52,13 @@ def executeCommand(command: tuple) -> int: """ joinedCommand = str.join(" ", command) - logging.getLogger(f"{INTERNAL_ID}").debug(f"Command -> {joinedCommand}") + logger.debug(f"Command -> {joinedCommand}") executionResult = os.system(joinedCommand) return executionResult +@logFailure(messageBox=True) def installCross(pipDeps: list) -> bool: """Attempts to fetch pip script and resolve dependencies with less user interaction @@ -88,7 +91,7 @@ def installCross(pipDeps: list) -> bool: try: pythonFolder = getPythonFolder() except ImportError as e: - logging.getLogger(f"{INTERNAL_ID}").error(f"Failed to download dependencies: {e.msg}") + logger.error(f"Failed to download dependencies: {e.msg}") return False if system == "Darwin": # macos @@ -125,7 +128,7 @@ def installCross(pipDeps: list) -> bool: ] ) if installResult != 0: - logging.getLogger(f"{INTERNAL_ID}").warn(f'Dep installation "{depName}" exited with code "{installResult}"') + logger.warn(f'Dep installation "{depName}" exited with code "{installResult}"') if system == "Darwin": pipAntiDeps = ["dataclasses", "typing"] @@ -146,16 +149,12 @@ def installCross(pipDeps: list) -> bool: ] ) if uninstallResult != 0: - logging.getLogger(f"{INTERNAL_ID}").warn( - f'AntiDep uninstallation "{depName}" exited with code "{uninstallResult}"' - ) + logger.warn(f"AntiDep uninstallation '{depName}' exited with code '{uninstallResult}'") progressBar.hide() - if _checkDeps(): - return True - else: - ui.messageBox("Failed to install dependencies needed") + if not _checkDeps(): + raise RuntimeError("Could not resolve Proto dependencies") def _checkDeps() -> bool: diff --git a/exporter/SynthesisFusionAddin/src/Analytics/alert.py b/exporter/SynthesisFusionAddin/src/Analytics/alert.py deleted file mode 100644 index c134f1906b..0000000000 --- a/exporter/SynthesisFusionAddin/src/Analytics/alert.py +++ /dev/null @@ -1,19 +0,0 @@ -from ..configure import setAnalytics -from ..general_imports import gm - - -def showAnalyticsAlert(): - ret = "

Unity File Exporter Addin

\n
" - ret += f"The Unity File Exporter would like to collect anonymous analytics information from you. You can find the privacy policy at the following address to review the data that is collected for the individual user session and how that data is used. \n\n" - ret += "May we collect basic usage information to help improve the user workflow and catch problems?" - - res = gm.ui.messageBox( - ret, - "Unity File Exporter Analytics", - 3, # This is yes/no - 1, # This is question icon - ) - - setAnalytics(True) if res == 2 else setAnalytics(False) diff --git a/exporter/SynthesisFusionAddin/src/Analytics/poster.py b/exporter/SynthesisFusionAddin/src/Analytics/poster.py deleted file mode 100644 index 1c5056ff72..0000000000 --- a/exporter/SynthesisFusionAddin/src/Analytics/poster.py +++ /dev/null @@ -1,219 +0,0 @@ -import sys -import urllib - -import adsk.core - -from ..configure import ANALYTICS, CID, DEBUG -from ..general_imports import * - -# Reference https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters - -# Page Example -""" - v=1 // Version. - &tid=UA-XXXXX-Y // Tracking ID / Property ID. - &cid=555 // Anonymous Client ID. - - &t=pageview // Pageview hit type. - &dh=mydemo.com // Document hostname. - &dp=/home // Page. - &dt=homepage // Title. -""" - -# Event example -""" - v=1 // Version. - &tid=UA-XXXXX-Y // Tracking ID / Property ID. - &cid=555 // Anonymous Client ID. - - &t=event // Event hit type - &ec=video // Event Category. Required. - &ea=play // Event Action. Required. - &el=holiday // Event label. - &ev=300 // Event value. -""" - -## Really should batch hits - -# EVENT example final -# www.google-analytics.com/collect?v=1&tid=UA-188467590-1&cid=555&t=event&ec=action&et=export&el=robot&ev=1 - -# VIEW example final -# www.google-analytics.com/collect?v=1&tid=UA-188467590-1&cid=556&t=pageview&dp=exportView&dt=viewer - -# FAILURES -# https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#exception - - -class AnalyticsEndpoint: - def __init__(self, tracking_id: str, version=1): - """Creating a new Endpoint for analytics - - Args: - tracking_id (str): tracking id UA-xxxxx - client_id (str): Anon client ID - check guid - version (int): Client version - """ - self.logger = logging.getLogger(f"HellionFusion.HUI.{self.__class__.__name__}") - self.url = "http://www.google-analytics.com" - self.tracking = tracking_id - self.a_id = CID - self.version = version - - def __str__(self): - return self.identity_string() - - def identity_string(self, cid=None) -> str: - if cid is None: - if self.a_id is None: - cid = "anon" - else: - cid = self.a_id - - return ( - self.__form("tid", self.tracking) - + self.__form("cid", CID) - + self.__form("v", self.version) - + self.__form("aip", 1) - ) - - def send_event(self, category: str, action: str, label="default", value=1) -> bool: - """Send event happened - - Args: - category (str): category of event (click , interaction , etc) - action (str): name of the action (export, change setting, etc) - label (str, optional): label for action for arbitrary data. Defaults to "". - value (str, optional): value for the label. Defaults to "". - """ - if ANALYTICS != True: - return - - body_t = (self.identity_string()) + ( - self.__form("t", "event") - + self.__form("ec", category) - + self.__form("ea", action) - + self.__form("el", label) - + self.__form("ev", value) - ) - # encode properly - # params = json.dumps(body).encode("utf8") - # print(body_t) - return self.__send(body_t) - - def send_view(self, page: str, app="FusionUnity", title="") -> bool: - if ANALYTICS != True: - return - - body_view = (self.identity_string()) + ( - self.__form("t", "pageview") - + self.__form("dh", f"{app}") - + self.__form("dp", f"{page}") - + self.__form("dt", title) - ) - - return self.__send(body_view) - - def send_exception(self) -> bool: - """This sends the exception encountered to the developer through the analytics pageview section - - Args: - e (Exception): exception to be formatted - - Returns: - bool: success - """ - - err = f"{sys.exc_info()[1]}" - - ui = adsk.core.Application.get().userInterface - - res = ui.messageBox( - f"The Unity Exporter Addin encountered an error during execution, would you like to send this information to the developer? \n - {err}", - "Error", - 3, # This is yes/no - 4, # This is question icon - ) - - if res != 2: - return - - body_t = (self.identity_string()) + ( - self.__form("t", "event") - + self.__form("ec", "Exception") - + self.__form("ea", "error") - + self.__form("el", err) - + self.__form("ev", 1) - ) - - self.__send(body_t) - - body_view = (self.identity_string()) + ( - self.__form("t", "pageview") - + self.__form("dh", f"FusionUnity") - + self.__form("dp", f"EXCEPTION") - + self.__form("dt", err) - ) - - return self.__send(body_view) - - def __send(self, body) -> bool: - try: - # define user agent so this works - headers = {} - headers["User-Agent"] = ( - "Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.27 Safari/537.17" - ) - - # print(f"{self.url}/collect?{body}") - - url = f"{self.url}/collect?{body}" - - if DEBUG: - self.logger.debug(f"Sending request: \n {url}") - - req = urllib.request.Request(f"{self.url}/collect?{body}", data=b"", headers=headers) - # makes the request - response = urllib.request.urlopen(req) - - if response.code == 200: - return True - else: - self.logger.error(f"Failed to log req : {body}") - return False - except: - self.logger.error("Failed:\n{}".format(traceback.format_exc())) - - def __form(self, key: str, value: str) -> str: - """This will format any string into a url encodable string - - Args: - key (str): name - value (str): encodable string - - Returns: - str: attribute - """ - value = urllib.parse.quote(str(value)) - return f"{key}={value}&" - - -# Old way I was testing, leave in for now in case I want to add exceptions etc. -# Maybe format into markdown etc -class URLString: - def __init__(self, key: str, value: str): - self.key = key - self.value = value - - def build(self) -> str: - value = urllib.parse.quote(str(value)) - return f"{key}={value}&" - - @staticmethod - def build(key: str, value: str) -> str: - value = urllib.parse.quote(str(value)) - return f"{key}={value}&" - - @classmethod - def build(cls, key: str, value: str) -> object: - return cls(key, value) diff --git a/exporter/SynthesisFusionAddin/src/Analyzer/sniff.py b/exporter/SynthesisFusionAddin/src/Analyzer/sniff.py deleted file mode 100644 index 01d64dadf0..0000000000 --- a/exporter/SynthesisFusionAddin/src/Analyzer/sniff.py +++ /dev/null @@ -1,148 +0,0 @@ -""" Takes in a given function call and times and tests the memory allocations to get data -""" - -import inspect -import linecache -import os -import time -import tracemalloc -from time import time - -from ..general_imports import * - - -class Sniffer: - def __init__(self): - self.logger = logging.getLogger(f"{INTERNAL_ID}.Analyzer.Sniffer") - - (self.filename, self.line_number, _, self.lines, _) = inspect.getframeinfo(inspect.currentframe().f_back.f_back) - - self.stopped = False - - def start(self): - self.stopped = False - self.t0 = time.time() - tracemalloc.start(5) - - def stop(self): - self.stopped = True - self.mem_size, self.mem_peak = tracemalloc.get_traced_memory() - self.snapshot = tracemalloc.take_snapshot() - self.t1 = time.time() - tracemalloc.stop() - - def _str(self) -> str: - if not self.stopped: - self.stop() - - return f"Analyzer \n Location: {os.path.basename(self.filename)} : {self.line_number} \n \t - Time taken: {self.t1-self.t0} seconds \n \t - Memory Allocated: {self.mem_size / 1024} Kb \n" - - def log(self): - """Logs the memory usage and time taken""" - self.logger.debug(self._str()) - - def print(self): - """Prints the memory usage and time taken""" - print(self._str()) - - def print_top(self): - """Prints out the info on the top 10 memory blocks""" - if not self.stopped: - self.stop() - print(display_top(self.snapshot)) - - def log_top(self): - """Logs out the info on the top 10 memory blocks""" - if not self.stopped: - self.stop() - out = self._str() + "\n" - self.logger.debug(out + display_top(self.snapshot)) - - -# this prints the top 10 memory usages in snapchat for the process -# found on : https://docs.python.org/3/library/tracemalloc.html#functions -def display_top(snapshot, key_type="lineno", limit=10): - snapshot = snapshot.filter_traces( - ( - tracemalloc.Filter(False, ""), - tracemalloc.Filter(False, ""), - ) - ) - top_stats = snapshot.statistics(key_type) - - out = "" - - out += "Top %s lines \n" % limit - for index, stat in enumerate(top_stats[:limit], 1): - frame = stat.traceback[0] - out += "#%s: %s:\n\t%s: %.1f KiB" % ( - index, - frame.filename, - frame.lineno, - stat.size / 1024, - ) - line = linecache.getline(frame.filename, frame.lineno).strip() - if line: - out += " %s \n" % line - - other = top_stats[limit:] - if other: - size = sum(stat.size for stat in other) - out += "%s other: %.1f KiB \n" % (len(other), size / 1024) - total = sum(stat.size for stat in top_stats) - out += "Total allocated size: %.1f KiB \n" % (total / 1024) - return out - - -def sniff(func, positional_arguments, keyword_arguments): - """sniffs the current func with the positional and keyword arguments - - ex. - method2(method1, ['spam'], {'ham': 'ham'}) - - Args: - func (function reference): Called function - positional_arguments (positional args): args that are position dependent - keyword_arguments (keyword args): args that are defined by keywords - - Returns: - Any: result of func - """ - t0 = time.time() - tracemalloc.start() - - ret = func(*positional_arguments, **keyword_arguments) - - mem_size, mem_peak = tracemalloc.get_traced_memory() - t1 = time.time() - tracemalloc.stop() - - logging.getLogger(f"{INTERNAL_ID}.Analyzer").debug( - f"Analyzer result on : {func.__name__} \n \t - Time taken: {t1-t0} \n \t - Memory Allocated: {mem_size} \n" - ) - - return ret - - -class Analyze: - def __init__(self, function): - """Function decorater to analyze calls to function - - Args: - function (function): function to be analyzed - """ - self.logger = logging.getLogger(f"{INTERNAL_ID}.Analyzer.Analyze") - self.function = function - self.sniffer = Sniffer() - - def __call__(self, *args, **kwargs): - if not DEBUG: - # Basically just executing the same function - self.function(*args, **kwargs) - - if DEBUG: - self.logger.debug(f"Analyzing: {self.function.__name__}") - self.sniffer.start() - self.function(*args, **kwargs) - self.sniffer.stop() - self.sniffer.log_top() diff --git a/exporter/SynthesisFusionAddin/src/Analyzer/timer.py b/exporter/SynthesisFusionAddin/src/Analyzer/timer.py deleted file mode 100644 index 41bb86135f..0000000000 --- a/exporter/SynthesisFusionAddin/src/Analyzer/timer.py +++ /dev/null @@ -1,70 +0,0 @@ -""" Takes in a given function call and times and tests the memory allocations to get data -""" - -import inspect -import os -from time import time - -from ..general_imports import * - - -class Timer: - def __init__(self): - self.logger = logging.getLogger(f"{INTERNAL_ID}.Analyzer.Timer") - - ( - self.filename, - self.line_number, - self.funcName, - self.lines, - _, - ) = inspect.getframeinfo( - inspect.currentframe().f_back.f_back - ) # func name doesn't always work here - - self.stopped = False - - def start(self): - self.stopped = False - self.t0 = time() - - def stop(self): - self.stopped = True - self.t1 = time() - - def _str(self) -> str: - if not self.stopped: - self.stop() - - # should really just use format and limit the number - return f"Timer \n Location: {os.path.basename(self.filename)} : {self.line_number} -> {str(self.funcName)} \n \t - Time taken: {self.t1-self.t0} seconds \n" - - def log(self): - self.logger.debug(self._str()) - - def print(self): - print(self._str()) - - -class TimeThis: - def __init__(self, function): - """Function decorater to analyze calls to function - - Args: - function (function): function to be analyzed - """ - self.logger = logging.getLogger(f"{INTERNAL_ID}.Analyzer.TimeThis") - self.function = function - self.timer = Timer() - - def __call__(self, *args, **kwargs): - if not DEBUG: - # Basically just executing the same function - self.function(*args, **kwargs) - - if DEBUG: - # self.logger.debug(f"Timing: {self.function.__name__}") - self.timer.start() - self.function(*args, **kwargs) - self.timer.stop() - self.timer.log() diff --git a/exporter/SynthesisFusionAddin/src/GlobalManager.py b/exporter/SynthesisFusionAddin/src/GlobalManager.py index b438a2eb23..a31688d119 100644 --- a/exporter/SynthesisFusionAddin/src/GlobalManager.py +++ b/exporter/SynthesisFusionAddin/src/GlobalManager.py @@ -1,7 +1,6 @@ """ Initializes the global variables that are set in the run method to reduce hanging commands. """ -import inspect -import traceback +import logging import adsk.core import adsk.fusion @@ -16,7 +15,6 @@ class GlobalManager(object): class __GlobalManager: def __init__(self): self.app = adsk.core.Application.get() - self.logger = logging.getLogger(f"{INTERNAL_ID}.{self.__class__.__name__}") if self.app: self.ui = self.app.userInterface @@ -53,15 +51,8 @@ def __str__(self): def __new__(cls): if not GlobalManager.instance: - """ - (filename, line_number, function_name, lines, index) = inspect.getframeinfo( - inspect.currentframe().f_back - ) - logging.getLogger(f"HellionFusion.Runtime").debug( - f"\n Called from {filename}\n \t - {lines} : {line_number} \n" - ) - """ GlobalManager.instance = GlobalManager.__GlobalManager() + return GlobalManager.instance def __getattr__(self, name): diff --git a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py index 31ed4cd0c4..5ff02e249b 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py +++ b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py @@ -13,6 +13,7 @@ import adsk.core from adsk.fusion import CalculationAccuracy, TriangleMeshQualityOptions +from ..Logging import logFailure, timed from ..strings import INTERNAL_ID # Not 100% sure what this is for - Brandon @@ -100,19 +101,26 @@ class ExporterOptions: physicalDepth: PhysicalDepth = field(default=PhysicalDepth.AllOccurrence) physicalCalculationLevel: CalculationAccuracy = field(default=CalculationAccuracy.LowCalculationAccuracy) + @logFailure + @timed def readFromDesign(self) -> None: - designAttributes = adsk.core.Application.get().activeProduct.attributes - for field in fields(self): - attribute = designAttributes.itemByName(INTERNAL_ID, field.name) - if attribute: - setattr( - self, - field.name, - self._makeObjectFromJson(field.type, json.loads(attribute.value)), - ) + try: + designAttributes = adsk.core.Application.get().activeProduct.attributes + for field in fields(self): + attribute = designAttributes.itemByName(INTERNAL_ID, field.name) + if attribute: + setattr( + self, + field.name, + self._makeObjectFromJson(field.type, json.loads(attribute.value)), + ) - return self + return self + except BaseException: + return ExporterOptions() + @logFailure + @timed def writeToDesign(self) -> None: designAttributes = adsk.core.Application.get().activeProduct.attributes for field in fields(self): diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index c6c741b0b2..88b25055c3 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -9,7 +9,7 @@ from proto.proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 -from ...Analyzer.timer import TimeThis +from ...Logging import logFailure, timed from ..ExporterOptions import ExporterOptions, ExportMode from . import PhysicalProperties from .PDMessage import PDMessage @@ -18,7 +18,6 @@ # TODO: Impelement Material overrides -@TimeThis def _MapAllComponents( design: adsk.fusion.Design, options: ExporterOptions, @@ -74,7 +73,6 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody): processBody(body) -@TimeThis def _ParseComponentRoot( component: adsk.fusion.Component, progressDialog: PDMessage, @@ -183,49 +181,45 @@ def GetMatrixWorld(occurrence): return matrix +@logFailure def _ParseBRep( body: adsk.fusion.BRepBody, options: ExporterOptions, trimesh: assembly_pb2.TriangleMesh, ) -> any: - try: - meshManager = body.meshManager - calc = meshManager.createMeshCalculator() - calc.setQuality(options.visualQuality) - mesh = calc.calculate() + meshManager = body.meshManager + calc = meshManager.createMeshCalculator() + calc.setQuality(options.visualQuality) + mesh = calc.calculate() - fill_info(trimesh, body) - trimesh.has_volume = True + fill_info(trimesh, body) + trimesh.has_volume = True - plainmesh_out = trimesh.mesh + plainmesh_out = trimesh.mesh - plainmesh_out.verts.extend(mesh.nodeCoordinatesAsFloat) - plainmesh_out.normals.extend(mesh.normalVectorsAsFloat) - plainmesh_out.indices.extend(mesh.nodeIndices) - plainmesh_out.uv.extend(mesh.textureCoordinatesAsFloat) - except: - logging.getLogger("{INTERNAL_ID}.Parser.BrepBody").error("Failed:\n{}".format(traceback.format_exc())) + plainmesh_out.verts.extend(mesh.nodeCoordinatesAsFloat) + plainmesh_out.normals.extend(mesh.normalVectorsAsFloat) + plainmesh_out.indices.extend(mesh.nodeIndices) + plainmesh_out.uv.extend(mesh.textureCoordinatesAsFloat) +@logFailure def _ParseMesh( meshBody: adsk.fusion.MeshBody, options: ExporterOptions, trimesh: assembly_pb2.TriangleMesh, ) -> any: - try: - mesh = meshBody.displayMesh + mesh = meshBody.displayMesh - fill_info(trimesh, meshBody) - trimesh.has_volume = True + fill_info(trimesh, meshBody) + trimesh.has_volume = True - plainmesh_out = trimesh.mesh + plainmesh_out = trimesh.mesh - plainmesh_out.verts.extend(mesh.nodeCoordinatesAsFloat) - plainmesh_out.normals.extend(mesh.normalVectorsAsFloat) - plainmesh_out.indices.extend(mesh.nodeIndices) - plainmesh_out.uv.extend(mesh.textureCoordinatesAsFloat) - except: - logging.getLogger("{INTERNAL_ID}.Parser.BrepBody").error("Failed:\n{}".format(traceback.format_exc())) + plainmesh_out.verts.extend(mesh.nodeCoordinatesAsFloat) + plainmesh_out.normals.extend(mesh.normalVectorsAsFloat) + plainmesh_out.indices.extend(mesh.nodeIndices) + plainmesh_out.uv.extend(mesh.textureCoordinatesAsFloat) def _MapRigidGroups(rootComponent: adsk.fusion.Component, joints: joint_pb2.Joints) -> None: diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 93d320b987..df17408808 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -9,10 +9,13 @@ from proto.proto_out import joint_pb2, types_pb2 from ...general_imports import * +from ...Logging import getLogger, logFailure, timed from ..ExporterOptions import ExporterOptions from .PDMessage import PDMessage from .Utilities import guid_component, guid_occurrence +logger = getLogger(f"{INTERNAL_ID}.{__name__}") + # ____________________________ DATA TYPES __________________ @@ -178,6 +181,7 @@ def __init__(self, relationship: JointRelationship, node: SimulationNode): class JointParser: + @logFailure def __init__(self, design): # Create hierarchy with just joint assembly # - Assembly @@ -209,9 +213,6 @@ def __init__(self, design): if self.grounded is None: gm.ui.messageBox("There is not currently a Grounded Component in the assembly, stopping kinematic export.") raise RuntimeWarning("There is no grounded component") - return - - self.logger = logging.getLogger("{INTERNAL_ID}.Parser.Joints.Parser") self.currentTraversal = dict() self.groundedConnections = [] @@ -244,60 +245,56 @@ def __init__(self, design): # self.groundSimNode.printLink() + @logFailure def __getAllJoints(self): for joint in list(self.design.rootComponent.allJoints) + list(self.design.rootComponent.allAsBuiltJoints): - try: - if joint and joint.occurrenceOne and joint.occurrenceTwo: - occurrenceOne = joint.occurrenceOne - occurrenceTwo = joint.occurrenceTwo - else: - return None - - if occurrenceOne is None: - try: - occurrenceOne = joint.geometryOrOriginOne.entityOne.assemblyContext - except: - pass - - if occurrenceTwo is None: - try: - occurrenceTwo = joint.geometryOrOriginTwo.entityOne.assemblyContext - except: - pass - - oneEntityToken = "" - twoEntityToken = "" + if joint and joint.occurrenceOne and joint.occurrenceTwo: + occurrenceOne = joint.occurrenceOne + occurrenceTwo = joint.occurrenceTwo + else: + return None + if occurrenceOne is None: try: - oneEntityToken = occurrenceOne.entityToken + occurrenceOne = joint.geometryOrOriginOne.entityOne.assemblyContext except: - oneEntityToken = occurrenceOne.name + pass + if occurrenceTwo is None: try: - twoEntityToken = occurrenceTwo.entityToken + occurrenceTwo = joint.geometryOrOriginTwo.entityOne.assemblyContext except: - twoEntityToken = occurrenceTwo.name - - typeJoint = joint.jointMotion.jointType + pass - if typeJoint != 0: - if oneEntityToken not in self.dynamicJoints.keys(): - self.dynamicJoints[oneEntityToken] = joint + oneEntityToken = "" + twoEntityToken = "" - if occurrenceTwo is None and occurrenceOne is None: - self.logger.error( - f"Occurrences that connect joints could not be found\n\t1: {occurrenceOne}\n\t2: {occurrenceTwo}" - ) - return None - else: - if oneEntityToken == self.grounded.entityToken: - self.groundedConnections.append(occurrenceTwo) - elif twoEntityToken == self.grounded.entityToken: - self.groundedConnections.append(occurrenceOne) + try: + oneEntityToken = occurrenceOne.entityToken + except: + oneEntityToken = occurrenceOne.name + try: + twoEntityToken = occurrenceTwo.entityToken except: - self.logger.error("Failed:\n{}".format(traceback.format_exc())) - continue + twoEntityToken = occurrenceTwo.name + + typeJoint = joint.jointMotion.jointType + + if typeJoint != 0: + if oneEntityToken not in self.dynamicJoints.keys(): + self.dynamicJoints[oneEntityToken] = joint + + if occurrenceTwo is None and occurrenceOne is None: + logger.error( + f"Occurrences that connect joints could not be found\n\t1: {occurrenceOne}\n\t2: {occurrenceTwo}" + ) + return None + else: + if oneEntityToken == self.grounded.entityToken: + self.groundedConnections.append(occurrenceTwo) + elif twoEntityToken == self.grounded.entityToken: + self.groundedConnections.append(occurrenceOne) def _linkAllAxis(self): # looks through each simulation nood starting with ground and orders them using edges @@ -442,6 +439,7 @@ def searchForGrounded( # ________________________ Build implementation ______________________ # +@logFailure def BuildJointPartHierarchy( design: adsk.fusion.Design, joints: joint_pb2.Joints, @@ -472,8 +470,6 @@ def BuildJointPartHierarchy( except Warning: return False - except: - logging.getLogger(f"{INTERNAL_ID}.JointHierarchy").error("Failed:\n{}".format(traceback.format_exc())) def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDialog): @@ -489,7 +485,7 @@ def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDia progressDialog.update() if not proto_joint: - logging.getLogger(f"{INTERNAL_ID}.JointHierarchy").error(f"Could not find protobuf joint for {simNode.name}") + logger.error(f"Could not find protobuf joint for {simNode.name}") return root = types_pb2.Node() diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py index 7ba62c1f5e..b280881eb0 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py @@ -32,10 +32,14 @@ from proto.proto_out import assembly_pb2, joint_pb2, motor_pb2, signal_pb2, types_pb2 from ...general_imports import * +from ...Logging import getLogger from ..ExporterOptions import ExporterOptions, JointParentType, SignalType from .PDMessage import PDMessage from .Utilities import construct_info, fill_info, guid_occurrence +logger = getLogger(f"{INTERNAL_ID}.{__name__}") + + # Need to take in a graphcontainer # Need to create a new base node for each Joint Instance # Need to create at least one grounded Node @@ -69,80 +73,80 @@ def populateJoints( # This is for creating all of the Joint Definition objects # So we need to iterate through the joints and construct them and add them to the map - if options.joints or DEBUG: - # Add the grounded joints object - TODO: rename some of the protobuf stuff for the love of god + if not options.joints: + return - joint_definition_ground = joints.joint_definitions["grounded"] - construct_info("grounded", joint_definition_ground) + # Add the grounded joints object - TODO: rename some of the protobuf stuff for the love of god + joint_definition_ground = joints.joint_definitions["grounded"] + construct_info("grounded", joint_definition_ground) - joint_instance_ground = joints.joint_instances["grounded"] - construct_info("grounded", joint_instance_ground) + joint_instance_ground = joints.joint_instances["grounded"] + construct_info("grounded", joint_instance_ground) - joint_instance_ground.joint_reference = joint_definition_ground.info.GUID + joint_instance_ground.joint_reference = joint_definition_ground.info.GUID - # Add the rest of the dynamic objects + # Add the rest of the dynamic objects - for joint in list(design.rootComponent.allJoints) + list(design.rootComponent.allAsBuiltJoints): - if joint.isSuppressed: - continue + for joint in list(design.rootComponent.allJoints) + list(design.rootComponent.allAsBuiltJoints): + if joint.isSuppressed: + continue - # turn RigidJoints into RigidGroups - if joint.jointMotion.jointType == 0: - _addRigidGroup(joint, assembly) - continue + # turn RigidJoints into RigidGroups + if joint.jointMotion.jointType == 0: + _addRigidGroup(joint, assembly) + continue - # for now if it's not a revolute or slider joint ignore it - if joint.jointMotion.jointType != 1 and joint.jointMotion.jointType != 2: - continue + # for now if it's not a revolute or slider joint ignore it + if joint.jointMotion.jointType != 1 and joint.jointMotion.jointType != 2: + continue - try: - # Fusion has no instances of joints but lets roll with it anyway + try: + # Fusion has no instances of joints but lets roll with it anyway - # progressDialog.message = f"Exporting Joint configuration {joint.name}" - progressDialog.addJoint(joint.name) + # progressDialog.message = f"Exporting Joint configuration {joint.name}" + progressDialog.addJoint(joint.name) - # create the definition - joint_definition = joints.joint_definitions[joint.entityToken] - _addJoint(joint, joint_definition) + # create the definition + joint_definition = joints.joint_definitions[joint.entityToken] + _addJoint(joint, joint_definition) - # create the instance of the single definition - joint_instance = joints.joint_instances[joint.entityToken] + # create the instance of the single definition + joint_instance = joints.joint_instances[joint.entityToken] - for parse_joints in options.joints: - if parse_joints.jointToken == joint.entityToken: - guid = str(uuid.uuid4()) - signal = signals.signal_map[guid] - construct_info(joint.name, signal, GUID=guid) - signal.io = signal_pb2.IOType.OUTPUT + for parse_joints in options.joints: + if parse_joints.jointToken == joint.entityToken: + guid = str(uuid.uuid4()) + signal = signals.signal_map[guid] + construct_info(joint.name, signal, GUID=guid) + signal.io = signal_pb2.IOType.OUTPUT - # really could just map the enum to a friggin string - if parse_joints.signalType != SignalType.PASSIVE and assembly.dynamic: - if parse_joints.signalType == SignalType.CAN: - signal.device_type = signal_pb2.DeviceType.CANBUS - elif parse_joints.signalType == SignalType.PWM: - signal.device_type = signal_pb2.DeviceType.PWM + # really could just map the enum to a friggin string + if parse_joints.signalType != SignalType.PASSIVE and assembly.dynamic: + if parse_joints.signalType == SignalType.CAN: + signal.device_type = signal_pb2.DeviceType.CANBUS + elif parse_joints.signalType == SignalType.PWM: + signal.device_type = signal_pb2.DeviceType.PWM - motor = joints.motor_definitions[joint.entityToken] - fill_info(motor, joint) - simple_motor = motor.simple_motor - simple_motor.stall_torque = parse_joints.force - simple_motor.max_velocity = parse_joints.speed - simple_motor.braking_constant = 0.8 # Default for now - joint_definition.motor_reference = joint.entityToken + motor = joints.motor_definitions[joint.entityToken] + fill_info(motor, joint) + simple_motor = motor.simple_motor + simple_motor.stall_torque = parse_joints.force + simple_motor.max_velocity = parse_joints.speed + simple_motor.braking_constant = 0.8 # Default for now + joint_definition.motor_reference = joint.entityToken - joint_instance.signal_reference = signal.info.GUID - # else: - # signals.signal_map.remove(guid) + joint_instance.signal_reference = signal.info.GUID + # else: + # signals.signal_map.remove(guid) - _addJointInstance(joint, joint_instance, joint_definition, signals, options) + _addJointInstance(joint, joint_instance, joint_definition, signals, options) - # adds information for joint motion and limits - _motionFromJoint(joint.jointMotion, joint_definition) + # adds information for joint motion and limits + _motionFromJoint(joint.jointMotion, joint_definition) - except: - err_msg = "Failed:\n{}".format(traceback.format_exc()) - logging.getLogger(f"{INTERNAL_ID}.JointParser").error("Failed:\n{}".format(traceback.format_exc())) - continue + except: + logger.error("Failed:\n{}".format(traceback.format_exc())) + continue def _addJoint(joint: adsk.fusion.Joint, joint_definition: joint_pb2.Joint): @@ -158,10 +162,8 @@ def _addJoint(joint: adsk.fusion.Joint, joint_definition: joint_pb2.Joint): joint_definition.origin.x = 0.0 joint_definition.origin.y = 0.0 joint_definition.origin.z = 0.0 - if DEBUG: - logging.getLogger(f"{INTERNAL_ID}.JointParser._addJoint").error( - f"Cannot find joint origin on joint {joint.name}" - ) + + logger.error(f"Cannot find joint origin on joint {joint.name}") joint_definition.break_magnitude = 0.0 @@ -461,9 +463,7 @@ def createJointGraph( elif node_map[supplied_joint.parent.value] is not None and node_map[supplied_joint.jointToken] is not None: node_map[supplied_joint.parent].children.append(node_map[supplied_joint.jointToken]) else: - logging.getLogger("JointHierarchy").error( - f"Cannot construct hierarhcy because of detached tree at : {supplied_joint.jointToken}" - ) + logger.error(f"Cannot construct hierarhcy because of detached tree at : {supplied_joint.jointToken}") for node in node_map.values(): # append everything at top level to isolate kinematics diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index b3199a1f9d..be20e373c6 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -8,6 +8,7 @@ from proto.proto_out import material_pb2 from ...general_imports import INTERNAL_ID +from ...Logging import logFailure, timed from .. import ExporterOptions from .PDMessage import PDMessage from .Utilities import * @@ -45,6 +46,7 @@ def setDefaultMaterial(physical_material: material_pb2.PhysicalMaterial): physical_material.matType = 0 +@logFailure def getPhysicalMaterialData(fusion_material, proto_material, options): """Gets the material data and adds it to protobuf @@ -53,74 +55,68 @@ def getPhysicalMaterialData(fusion_material, proto_material, options): proto_material (protomaterial): proto material mirabuf options (parseoptions): parse options """ - try: - construct_info("", proto_material, fus_object=fusion_material) - - proto_material.deformable = False - proto_material.matType = 0 - - materialProperties = fusion_material.materialProperties - - thermalProperties = proto_material.thermal - mechanicalProperties = proto_material.mechanical - strengthProperties = proto_material.strength - - proto_material.dynamic_friction = 0.5 - proto_material.static_friction = 0.5 - proto_material.restitution = 0.5 - - proto_material.description = f"{fusion_material.name} exported from FUSION" - - """ - Thermal Properties - """ - - """ # These are causing temporary failures when trying to find value. Better to not throw this many exceptions. - if materialProperties.itemById( - "thermal_Thermal_conductivity" - ) is not None: - thermalProperties.thermal_conductivity = materialProperties.itemById( - "thermal_Thermal_conductivity" - ).value - if materialProperties.itemById( - "structural_Specific_heat" - ) is not None: - thermalProperties.specific_heat = materialProperties.itemById( - "structural_Specific_heat" - ).value - - if materialProperties.itemById( - "structural_Thermal_expansion_coefficient" - ) is not None: - thermalProperties.thermal_expansion_coefficient = materialProperties.itemById( - "structural_Thermal_expansion_coefficient" - ).value - """ - - """ - Mechanical Properties - """ - mechanicalProperties.young_mod = materialProperties.itemById("structural_Young_modulus").value - mechanicalProperties.poisson_ratio = materialProperties.itemById("structural_Poisson_ratio").value - mechanicalProperties.shear_mod = materialProperties.itemById("structural_Shear_modulus").value - mechanicalProperties.density = materialProperties.itemById("structural_Density").value - mechanicalProperties.damping_coefficient = materialProperties.itemById("structural_Damping_coefficient").value - - """ - Strength Properties - """ - strengthProperties.yield_strength = materialProperties.itemById("structural_Minimum_yield_stress").value - strengthProperties.tensile_strength = materialProperties.itemById("structural_Minimum_tensile_strength").value - """ - strengthProperties.thermal_treatment = materialProperties.itemById( - "structural_Thermally_treated" + construct_info("", proto_material, fus_object=fusion_material) + + proto_material.deformable = False + proto_material.matType = 0 + + materialProperties = fusion_material.materialProperties + + thermalProperties = proto_material.thermal + mechanicalProperties = proto_material.mechanical + strengthProperties = proto_material.strength + + proto_material.dynamic_friction = 0.5 + proto_material.static_friction = 0.5 + proto_material.restitution = 0.5 + + proto_material.description = f"{fusion_material.name} exported from FUSION" + + """ + Thermal Properties + """ + + """ # These are causing temporary failures when trying to find value. Better to not throw this many exceptions. + if materialProperties.itemById( + "thermal_Thermal_conductivity" + ) is not None: + thermalProperties.thermal_conductivity = materialProperties.itemById( + "thermal_Thermal_conductivity" + ).value + if materialProperties.itemById( + "structural_Specific_heat" + ) is not None: + thermalProperties.specific_heat = materialProperties.itemById( + "structural_Specific_heat" ).value - """ + + if materialProperties.itemById( + "structural_Thermal_expansion_coefficient" + ) is not None: + thermalProperties.thermal_expansion_coefficient = materialProperties.itemById( + "structural_Thermal_expansion_coefficient" + ).value + """ + + """ + Mechanical Properties + """ + mechanicalProperties.young_mod = materialProperties.itemById("structural_Young_modulus").value + mechanicalProperties.poisson_ratio = materialProperties.itemById("structural_Poisson_ratio").value + mechanicalProperties.shear_mod = materialProperties.itemById("structural_Shear_modulus").value + mechanicalProperties.density = materialProperties.itemById("structural_Density").value + mechanicalProperties.damping_coefficient = materialProperties.itemById("structural_Damping_coefficient").value - except: - logging.getLogger(f"{INTERNAL_ID}.Parser.Materials.getPhysicalMaterialData").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + """ + Strength Properties + """ + strengthProperties.yield_strength = materialProperties.itemById("structural_Minimum_yield_stress").value + strengthProperties.tensile_strength = materialProperties.itemById("structural_Minimum_tensile_strength").value + """ + strengthProperties.thermal_treatment = materialProperties.itemById( + "structural_Thermally_treated" + ).value + """ def _MapAllAppearances( diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index a925f5b119..e60987e7ea 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -1,4 +1,5 @@ import gzip +import pathlib import traceback import adsk.core @@ -8,11 +9,14 @@ from proto.proto_out import assembly_pb2, types_pb2 from ...general_imports import * +from ...Logging import getLogger, logFailure, timed from ...UI.Camera import captureThumbnail, clearIconCache from ..ExporterOptions import ExporterOptions, ExportMode from . import Components, JointHierarchy, Joints, Materials, PDMessage from .Utilities import * +logger = getLogger(f"{INTERNAL_ID}.{__name__}") + class Parser: def __init__(self, options: ExporterOptions): @@ -22,213 +26,195 @@ def __init__(self, options: ExporterOptions): options (ParserOptions): parser options """ self.exporterOptions = options - self.logger = logging.getLogger(f"{INTERNAL_ID}.Parser") + @logFailure(messageBox=True) + @timed def export(self) -> bool: - try: - app = adsk.core.Application.get() - design: adsk.fusion.Design = app.activeDocument.design - - assembly_out = assembly_pb2.Assembly() - fill_info( - assembly_out, - design.rootComponent, - override_guid=design.parentDocument.name, - ) - - # set int to 0 in dropdown selection for dynamic - assembly_out.dynamic = self.exporterOptions.exportMode == ExportMode.ROBOT - - # Physical Props here when ready - - # ts = time() - - progressDialog = app.userInterface.createProgressDialog() - progressDialog.cancelButtonText = "Cancel" - progressDialog.isBackgroundTranslucent = False - progressDialog.isCancelButtonShown = True - - totalIterations = design.rootComponent.allOccurrences.count + 1 - - progressDialog.title = "Exporting to Synthesis Format" - progressDialog.minimumValue = 0 - progressDialog.maximumValue = totalIterations - progressDialog.show("Synthesis Export", "Currently on %v of %m", 0, totalIterations) - - # this is the formatter for the progress dialog now - self.pdMessage = PDMessage.PDMessage( - assembly_out.info.name, - design.allComponents.count, - design.rootComponent.allOccurrences.count, - design.materials.count, - design.appearances.count, # this is very high for some reason - progressDialog, - ) - - Materials._MapAllAppearances( - design.appearances, - assembly_out.data.materials, - self.exporterOptions, - self.pdMessage, - ) - - Materials._MapAllPhysicalMaterials( - design.materials, - assembly_out.data.materials, - self.exporterOptions, - self.pdMessage, - ) - - Components._MapAllComponents( - design, - self.exporterOptions, - self.pdMessage, - assembly_out.data.parts, - assembly_out.data.materials, - ) - - rootNode = types_pb2.Node() - - Components._ParseComponentRoot( - design.rootComponent, - self.pdMessage, - self.exporterOptions, - assembly_out.data.parts, - assembly_out.data.materials.appearances, - rootNode, - ) - - Components._MapRigidGroups(design.rootComponent, assembly_out.data.joints) - - assembly_out.design_hierarchy.nodes.append(rootNode) - - # Problem Child - Joints.populateJoints( - design, - assembly_out.data.joints, - assembly_out.data.signals, - self.pdMessage, - self.exporterOptions, - assembly_out, - ) - - # add condition in here for advanced joints maybe idk - # should pre-process to find if there are any grounded joints at all - # that or add code to existing parser to determine leftovers - - Joints.createJointGraph( - self.exporterOptions.joints, - self.exporterOptions.wheels, - assembly_out.joint_hierarchy, - self.pdMessage, - ) - - JointHierarchy.BuildJointPartHierarchy( - design, assembly_out.data.joints, self.exporterOptions, self.pdMessage - ) - - # These don't have an effect, I forgot how this is suppose to work - # progressDialog.message = "Taking Photo for thumbnail..." - # progressDialog.title = "Finishing Export" - self.pdMessage.currentMessage = "Taking Photo for Thumbnail..." - self.pdMessage.update() - - # default image size - imgSize = 250 - - # Can only save, cannot get the bytes directly - thumbnailLocation = captureThumbnail(imgSize) - - if thumbnailLocation != None: - # Load bytes into memory and write them to proto - binaryImage = None - with open(thumbnailLocation, "rb") as in_file: - binaryImage = in_file.read() - - if binaryImage != None: - # change these settings in the captureThumbnail Function - assembly_out.thumbnail.width = imgSize - assembly_out.thumbnail.height = imgSize - assembly_out.thumbnail.transparent = True - assembly_out.thumbnail.data = binaryImage - assembly_out.thumbnail.extension = "png" - # clear the icon cache - src/resources/Icons - clearIconCache() - - self.pdMessage.currentMessage = "Compressing File..." - self.pdMessage.update() - - # check if entire path exists and create if not since gzip doesn't do that. - path = pathlib.Path(self.exporterOptions.fileLocation).parent - path.mkdir(parents=True, exist_ok=True) - - ### Print out assembly as JSON - # miraJson = MessageToJson(assembly_out) - # miraJsonFile = open(f'', 'wb') - # miraJsonFile.write(str.encode(miraJson)) - # miraJsonFile.close() - - if self.exporterOptions.compressOutput: - self.logger.debug("Compressing file") - with gzip.open(self.exporterOptions.fileLocation, "wb", 9) as f: - self.pdMessage.currentMessage = "Saving File..." - self.pdMessage.update() - f.write(assembly_out.SerializeToString()) - else: - f = open(self.exporterOptions.fileLocation, "wb") + app = adsk.core.Application.get() + design: adsk.fusion.Design = app.activeDocument.design + + assembly_out = assembly_pb2.Assembly() + fill_info( + assembly_out, + design.rootComponent, + override_guid=design.parentDocument.name, + ) + + # set int to 0 in dropdown selection for dynamic + assembly_out.dynamic = self.exporterOptions.exportMode == ExportMode.ROBOT + + # Physical Props here when ready + + # ts = time() + + progressDialog = app.userInterface.createProgressDialog() + progressDialog.cancelButtonText = "Cancel" + progressDialog.isBackgroundTranslucent = False + progressDialog.isCancelButtonShown = True + + totalIterations = design.rootComponent.allOccurrences.count + 1 + + progressDialog.title = "Exporting to Synthesis Format" + progressDialog.minimumValue = 0 + progressDialog.maximumValue = totalIterations + progressDialog.show("Synthesis Export", "Currently on %v of %m", 0, totalIterations) + + # this is the formatter for the progress dialog now + self.pdMessage = PDMessage.PDMessage( + assembly_out.info.name, + design.allComponents.count, + design.rootComponent.allOccurrences.count, + design.materials.count, + design.appearances.count, # this is very high for some reason + progressDialog, + ) + + Materials._MapAllAppearances( + design.appearances, + assembly_out.data.materials, + self.exporterOptions, + self.pdMessage, + ) + + Materials._MapAllPhysicalMaterials( + design.materials, + assembly_out.data.materials, + self.exporterOptions, + self.pdMessage, + ) + + Components._MapAllComponents( + design, + self.exporterOptions, + self.pdMessage, + assembly_out.data.parts, + assembly_out.data.materials, + ) + + rootNode = types_pb2.Node() + + Components._ParseComponentRoot( + design.rootComponent, + self.pdMessage, + self.exporterOptions, + assembly_out.data.parts, + assembly_out.data.materials.appearances, + rootNode, + ) + + Components._MapRigidGroups(design.rootComponent, assembly_out.data.joints) + + assembly_out.design_hierarchy.nodes.append(rootNode) + + # Problem Child + Joints.populateJoints( + design, + assembly_out.data.joints, + assembly_out.data.signals, + self.pdMessage, + self.exporterOptions, + assembly_out, + ) + + # add condition in here for advanced joints maybe idk + # should pre-process to find if there are any grounded joints at all + # that or add code to existing parser to determine leftovers + + Joints.createJointGraph( + self.exporterOptions.joints, + self.exporterOptions.wheels, + assembly_out.joint_hierarchy, + self.pdMessage, + ) + + JointHierarchy.BuildJointPartHierarchy(design, assembly_out.data.joints, self.exporterOptions, self.pdMessage) + + # These don't have an effect, I forgot how this is suppose to work + # progressDialog.message = "Taking Photo for thumbnail..." + # progressDialog.title = "Finishing Export" + self.pdMessage.currentMessage = "Taking Photo for Thumbnail..." + self.pdMessage.update() + + # default image size + imgSize = 250 + + # Can only save, cannot get the bytes directly + thumbnailLocation = captureThumbnail(imgSize) + + if thumbnailLocation != None: + # Load bytes into memory and write them to proto + binaryImage = None + with open(thumbnailLocation, "rb") as in_file: + binaryImage = in_file.read() + + if binaryImage != None: + # change these settings in the captureThumbnail Function + assembly_out.thumbnail.width = imgSize + assembly_out.thumbnail.height = imgSize + assembly_out.thumbnail.transparent = True + assembly_out.thumbnail.data = binaryImage + assembly_out.thumbnail.extension = "png" + # clear the icon cache - src/resources/Icons + clearIconCache() + + self.pdMessage.currentMessage = "Compressing File..." + self.pdMessage.update() + + # check if entire path exists and create if not since gzip doesn't do that. + path = pathlib.Path(self.exporterOptions.fileLocation).parent + path.mkdir(parents=True, exist_ok=True) + + ### Print out assembly as JSON + # miraJson = MessageToJson(assembly_out) + # miraJsonFile = open(f'', 'wb') + # miraJsonFile.write(str.encode(miraJson)) + # miraJsonFile.close() + + if self.exporterOptions.compressOutput: + logger.debug("Compressing file") + with gzip.open(self.exporterOptions.fileLocation, "wb", 9) as f: + self.pdMessage.currentMessage = "Saving File..." + self.pdMessage.update() f.write(assembly_out.SerializeToString()) - f.close() - - progressDialog.hide() - - if DEBUG: - part_defs = assembly_out.data.parts.part_definitions - parts = assembly_out.data.parts.part_instances - joints = assembly_out.data.joints.joint_definitions - signals = assembly_out.data.signals.signal_map - - joint_hierarchy_out = "Joint Hierarchy :\n" - - # This is just for testing - for node in assembly_out.joint_hierarchy.nodes: - if node.value == "ground": - joint_hierarchy_out = f"{joint_hierarchy_out} |- ground\n" - else: - newnode = assembly_out.data.joints.joint_instances[node.value] - jointdefinition = assembly_out.data.joints.joint_definitions[newnode.joint_reference] - - wheel_ = " wheel : true" if (jointdefinition.user_data.data["wheel"] != "") else "" - - joint_hierarchy_out = f"{joint_hierarchy_out} |- {jointdefinition.info.name} type: {jointdefinition.joint_motion_type} {wheel_}\n" - - for child in node.children: - if child.value == "ground": - joint_hierarchy_out = f"{joint_hierarchy_out} |---> ground\n" - else: - newnode = assembly_out.data.joints.joint_instances[child.value] - jointdefinition = assembly_out.data.joints.joint_definitions[newnode.joint_reference] - wheel_ = " wheel : true" if (jointdefinition.user_data.data["wheel"] != "") else "" - joint_hierarchy_out = f"{joint_hierarchy_out} |---> {jointdefinition.info.name} type: {jointdefinition.joint_motion_type} {wheel_}\n" - - joint_hierarchy_out += "\n\n" - - debug_output = f"Appearances: {len(assembly_out.data.materials.appearances)} \nMaterials: {len(assembly_out.data.materials.physicalMaterials)} \nPart-Definitions: {len(part_defs)} \nParts: {len(parts)} \nSignals: {len(signals)} \nJoints: {len(joints)}\n {joint_hierarchy_out}" + else: + f = open(self.exporterOptions.fileLocation, "wb") + f.write(assembly_out.SerializeToString()) + f.close() + + progressDialog.hide() + + # Transition: AARD-1735 + # Create debug log joint hierarchy graph + # Should consider adding a toggle for performance reasons + part_defs = assembly_out.data.parts.part_definitions + parts = assembly_out.data.parts.part_instances + joints = assembly_out.data.joints.joint_definitions + signals = assembly_out.data.signals.signal_map + + joint_hierarchy_out = "Joint Hierarchy :\n" + + for node in assembly_out.joint_hierarchy.nodes: + if node.value == "ground": + joint_hierarchy_out = f"{joint_hierarchy_out} |- ground\n" + else: + newnode = assembly_out.data.joints.joint_instances[node.value] + jointdefinition = assembly_out.data.joints.joint_definitions[newnode.joint_reference] - gm.ui.messageBox( - debug_output, - "DEBUG - Fusion Synthesis", - ) + wheel_ = " wheel : true" if (jointdefinition.user_data.data["wheel"] != "") else "" - self.logger.debug(debug_output) + joint_hierarchy_out = f"{joint_hierarchy_out} |- {jointdefinition.info.name} type: {jointdefinition.joint_motion_type} {wheel_}\n" - except: - self.logger.error("Failed:\n{}".format(traceback.format_exc())) + for child in node.children: + if child.value == "ground": + joint_hierarchy_out = f"{joint_hierarchy_out} |---> ground\n" + else: + newnode = assembly_out.data.joints.joint_instances[child.value] + jointdefinition = assembly_out.data.joints.joint_definitions[newnode.joint_reference] + wheel_ = " wheel : true" if (jointdefinition.user_data.data["wheel"] != "") else "" + joint_hierarchy_out = f"{joint_hierarchy_out} |---> {jointdefinition.info.name} type: {jointdefinition.joint_motion_type} {wheel_}\n" - if DEBUG: - gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) - else: - gm.ui.messageBox("An error occurred while exporting.") + joint_hierarchy_out += "\n\n" - return False + debug_output = f"Appearances: {len(assembly_out.data.materials.appearances)} \nMaterials: {len(assembly_out.data.materials.physicalMaterials)} \nPart-Definitions: {len(part_defs)} \nParts: {len(parts)} \nSignals: {len(signals)} \nJoints: {len(joints)}\n {joint_hierarchy_out}" - return True + logger.debug(debug_output.strip()) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py index afd9489909..db488c115a 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py @@ -25,8 +25,10 @@ from proto.proto_out import types_pb2 from ...general_imports import INTERNAL_ID +from ...Logging import logFailure +@logFailure def GetPhysicalProperties( fusionObject: Union[adsk.fusion.BRepBody, adsk.fusion.Occurrence, adsk.fusion.Component], physicalProperties: types_pb2.PhysicalProperties, @@ -39,22 +41,17 @@ def GetPhysicalProperties( physicalProperties (any): Unity Joint object for now level (int): Level of accurracy """ - try: - physical = fusionObject.getPhysicalProperties(level) - - physicalProperties.density = physical.density - physicalProperties.mass = physical.mass - physicalProperties.volume = physical.volume - physicalProperties.area = physical.area - - _com = physicalProperties.com - com = physical.centerOfMass.asVector() - - if com is not None: - _com.x = com.x - _com.y = com.y - _com.z = com.z - except: - logging.getLogger(f"{INTERNAL_ID}.Parser.PhysicalProperties").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + physical = fusionObject.getPhysicalProperties(level) + + physicalProperties.density = physical.density + physicalProperties.mass = physical.mass + physicalProperties.volume = physical.volume + physicalProperties.area = physical.area + + _com = physicalProperties.com + com = physical.centerOfMass.asVector() + + if com is not None: + _com.x = com.x + _com.y = com.y + _com.z = com.z diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py index 7e8f592466..0ce92ce1e1 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py @@ -18,9 +18,11 @@ import adsk.core import adsk.fusion +from Logging import logFailure from proto.proto_out import assembly_pb2 +@logFailure def ExportRigidGroups( fus_occ: Union[adsk.fusion.Occurrence, adsk.fusion.Component], hel_occ: assembly_pb2.Occurrence, @@ -34,22 +36,16 @@ def ExportRigidGroups( fus_occ (adsk.fusion.Occurrence): Fusion Occurrence Reference hel_occ (Assembly_pb2.Occurrence): Protobuf Hellion Occurrence Reference """ - try: - log = logging.getLogger("{INTERNAL_ID}.Parser.RigidGroup") + groups = fus_occ.rigidGroups - groups = fus_occ.rigidGroups + if groups is None: + return - if groups is None: - return + _rigidGroups = hel_occ.rigidgroups - _rigidGroups = hel_occ.rigidgroups - - for group in groups: - if not group.isSuppressed and group.isValid: - _group = _rigidGroups.add() - _group.name = group.name - for occurrence in group.occurrences: - _group.occurrences.extend(occurrence.fullPathName) - except: - if log: - log.error(f"Exception thrown while parsing rigidgroups in {fus_occ.name}") + for group in groups: + if not group.isSuppressed and group.isValid: + _group = _rigidGroups.add() + _group.name = group.name + for occurrence in group.occurrences: + _group.occurrences.extend(occurrence.fullPathName) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py index 7169cd4085..f508f32117 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py @@ -54,7 +54,7 @@ def construct_info(name: str, proto_obj, version=5, fus_object=None, GUID=None) try: # attempt to get entity token proto_obj.info.GUID = fus_object.entityToken - except: + except AttributeError: # fails and gets new uuid proto_obj.info.GUID = str(uuid.uuid4()) diff --git a/exporter/SynthesisFusionAddin/src/UI/Camera.py b/exporter/SynthesisFusionAddin/src/UI/Camera.py index a7076fdfd2..9fe68b7700 100644 --- a/exporter/SynthesisFusionAddin/src/UI/Camera.py +++ b/exporter/SynthesisFusionAddin/src/UI/Camera.py @@ -3,55 +3,42 @@ from adsk.core import SaveImageFileOptions from ..general_imports import * +from ..Logging import logFailure, timed from ..Types.OString import OString from . import Helper +@logFailure def captureThumbnail(size=250): """ ## Captures Thumbnail and saves it to a temporary path - needs to be cleared after or on startup - Size: int (Default: 200) : (width & height) """ app = adsk.core.Application.get() + originalCamera = app.activeViewport.camera - log = logging.getLogger("{INTERNAL_ID}.HUI.Camera") + name = "Thumbnail_{0}.png".format( + app.activeDocument.design.rootComponent.name.rsplit(" ", 1)[0].replace( + " ", "" + ) # remove whitespace from just the filename + ) - if Helper.check_solid_open(): - try: - originalCamera = app.activeViewport.camera + path = OString.ThumbnailPath(name) - name = "Thumbnail_{0}.png".format( - app.activeDocument.design.rootComponent.name.rsplit(" ", 1)[0].replace( - " ", "" - ) # remove whitespace from just the filename - ) + saveOptions = SaveImageFileOptions.create(str(path.getPath())) + saveOptions.height = size + saveOptions.width = size + saveOptions.isAntiAliased = True + saveOptions.isBackgroundTransparent = True - path = OString.ThumbnailPath(name) + newCamera = app.activeViewport.camera + newCamera.isFitView = True - saveOptions = SaveImageFileOptions.create(str(path.getPath())) - saveOptions.height = size - saveOptions.width = size - saveOptions.isAntiAliased = True - saveOptions.isBackgroundTransparent = True + app.activeViewport.camera = newCamera + app.activeViewport.saveAsImageFileWithOptions(saveOptions) + app.activeViewport.camera = originalCamera - newCamera = app.activeViewport.camera - newCamera.isFitView = True - - app.activeViewport.camera = newCamera - app.activeViewport.saveAsImageFileWithOptions(saveOptions) - app.activeViewport.camera = originalCamera - - return str(path.getPath()) - - except: - if log: - log.error("Failed\n{}".format(traceback.format_exc())) - - if A_EP: - A_EP.send_exception() - - else: - return None + return str(path.getPath()) def clearIconCache() -> None: diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 6ea4e0b702..1fb4a5819c 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -4,6 +4,7 @@ import logging import os +import pathlib import platform import traceback from enum import Enum @@ -11,9 +12,8 @@ import adsk.core import adsk.fusion -from ..Analytics.alert import showAnalyticsAlert -from ..configure import NOTIFIED, write_configuration from ..general_imports import * +from ..Logging import getLogger, logFailure from ..Parser.ExporterOptions import ( ExporterOptions, ExportMode, @@ -30,6 +30,9 @@ from . import CustomGraphics, FileDialogConfig, Helper, IconPaths, OsHelper from .Configuration.SerialCommand import SerialCommand +logger = getLogger(f"{INTERNAL_ID}.{__name__}") + + # ====================================== CONFIG COMMAND ====================================== """ @@ -117,32 +120,25 @@ def __init__(self): self.bRepMassInRoot() self.traverseOccurrenceHierarchy() + @logFailure(messageBox=True) def bRepMassInRoot(self): - try: - for body in gm.app.activeDocument.design.rootComponent.bRepBodies: + for body in gm.app.activeDocument.design.rootComponent.bRepBodies: + if not body.isLightBulbOn: + continue + physical = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.LowCalculationAccuracy) + self.totalMass += physical.mass + + @logFailure(messageBox=True) + def traverseOccurrenceHierarchy(self): + for occ in gm.app.activeDocument.design.rootComponent.allOccurrences: + if not occ.isLightBulbOn: + continue + + for body in occ.component.bRepBodies: if not body.isLightBulbOn: continue physical = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.LowCalculationAccuracy) self.totalMass += physical.mass - except: - if gm.ui: - gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) - - def traverseOccurrenceHierarchy(self): - try: - for occ in gm.app.activeDocument.design.rootComponent.allOccurrences: - if not occ.isLightBulbOn: - continue - - for body in occ.component.bRepBodies: - if not body.isLightBulbOn: - continue - physical = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.LowCalculationAccuracy) - self.totalMass += physical.mass - except: - pass - if gm.ui: - gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) def getTotalMass(self): return self.totalMass @@ -158,670 +154,646 @@ class ConfigureCommandCreatedHandler(adsk.core.CommandCreatedEventHandler): def __init__(self, configure): super().__init__() - self.log = logging.getLogger(f"{INTERNAL_ID}.UI.{self.__class__.__name__}") self.designAttrs = adsk.core.Application.get().activeProduct.attributes + @logFailure(messageBox=True) def notify(self, args): - try: - exporterOptions = ExporterOptions().readFromDesign() - # exporterOptions = ExporterOptions() - - if not Helper.check_solid_open(): - return - - global NOTIFIED # keep this global - if not NOTIFIED: - showAnalyticsAlert() - NOTIFIED = True - write_configuration("analytics", "notified", "yes") - - if A_EP: - A_EP.send_view("export_panel") - - eventArgs = adsk.core.CommandCreatedEventArgs.cast(args) - cmd = eventArgs.command # adsk.core.Command - - # Set to false so won't automatically export on switch context - cmd.isAutoExecute = False - cmd.isExecutedWhenPreEmpted = False - cmd.okButtonText = "Export" # replace default OK text with "export" - cmd.setDialogInitialSize(400, 350) # these aren't working for some reason... - cmd.setDialogMinimumSize(400, 350) # these aren't working for some reason... - - global INPUTS_ROOT # Global CommandInputs arg - INPUTS_ROOT = cmd.commandInputs - - # ====================================== GENERAL TAB ====================================== - """ - Creates the general tab. - - Parent container for all the command inputs in the tab. - """ - inputs = INPUTS_ROOT.addTabCommandInput("general_settings", "General").children - - # ~~~~~~~~~~~~~~~~ HELP FILE ~~~~~~~~~~~~~~~~ - """ - Sets the small "i" icon in bottom left of the panel. - - This is an HTML file that has a script to redirect to exporter workflow tutorial. - """ - cmd.helpFile = os.path.join(".", "src", "Resources", "HTML", "info.html") - - # ~~~~~~~~~~~~~~~~ EXPORT MODE ~~~~~~~~~~~~~~~~ - """ - Dropdown to choose whether to export robot or field element - """ - dropdownExportMode = inputs.addDropDownCommandInput( - "mode", - "Export Mode", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) + exporterOptions = ExporterOptions().readFromDesign() - dynamic = exporterOptions.exportMode == ExportMode.ROBOT - dropdownExportMode.listItems.add("Dynamic", dynamic) - dropdownExportMode.listItems.add("Static", not dynamic) - - dropdownExportMode.tooltip = "Export Mode" - dropdownExportMode.tooltipDescription = "
Does this object move dynamically?" - - # ~~~~~~~~~~~~~~~~ WEIGHT CONFIGURATION ~~~~~~~~~~~~~~~~ - """ - Table for weight config. - - Used this to align multiple commandInputs on the same row - """ - weightTableInput = self.createTableInput( - "weight_table", - "Weight Table", - inputs, - 4, - "3:2:2:1", - 1, - ) - weightTableInput.tablePresentationStyle = 2 # set transparent background for table - - weight_name = inputs.addStringValueInput("weight_name", "Weight") - weight_name.value = "Weight" - weight_name.isReadOnly = True - - auto_calc_weight = self.createBooleanInput( - "auto_calc_weight", - "‎", - inputs, - checked=False, - tooltip="Approximate the weight of your robot assembly.", - tooltipadvanced="This may take a moment...", - enabled=True, - isCheckBox=False, - ) - auto_calc_weight.resourceFolder = IconPaths.stringIcons["calculate-enabled"] - auto_calc_weight.isFullWidth = True + eventArgs = adsk.core.CommandCreatedEventArgs.cast(args) + cmd = eventArgs.command # adsk.core.Command - imperialUnits = exporterOptions.preferredUnits == PreferredUnits.IMPERIAL - if imperialUnits: - # ExporterOptions always contains the metric value - displayWeight = exporterOptions.robotWeight * 2.2046226218 - else: - displayWeight = exporterOptions.robotWeight + # Set to false so won't automatically export on switch context + cmd.isAutoExecute = False + cmd.isExecutedWhenPreEmpted = False + cmd.okButtonText = "Export" # replace default OK text with "export" + cmd.setDialogInitialSize(400, 350) # these aren't working for some reason... + cmd.setDialogMinimumSize(400, 350) # these aren't working for some reason... - weight_input = inputs.addValueInput( - "weight_input", - "Weight Input", - "", - adsk.core.ValueInput.createByReal(displayWeight), - ) - weight_input.tooltip = "Robot weight" - weight_input.tooltipDescription = ( - """(in pounds)
This is the weight of the entire robot assembly.""" - ) + global INPUTS_ROOT # Global CommandInputs arg + INPUTS_ROOT = cmd.commandInputs - weight_unit = inputs.addDropDownCommandInput( - "weight_unit", - "Weight Unit", - adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) + # ====================================== GENERAL TAB ====================================== + """ + Creates the general tab. + - Parent container for all the command inputs in the tab. + """ + inputs = INPUTS_ROOT.addTabCommandInput("general_settings", "General").children + + # ~~~~~~~~~~~~~~~~ HELP FILE ~~~~~~~~~~~~~~~~ + """ + Sets the small "i" icon in bottom left of the panel. + - This is an HTML file that has a script to redirect to exporter workflow tutorial. + """ + cmd.helpFile = os.path.join(".", "src", "Resources", "HTML", "info.html") + + # ~~~~~~~~~~~~~~~~ EXPORT MODE ~~~~~~~~~~~~~~~~ + """ + Dropdown to choose whether to export robot or field element + """ + dropdownExportMode = inputs.addDropDownCommandInput( + "mode", + "Export Mode", + dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, + ) + + dynamic = exporterOptions.exportMode == ExportMode.ROBOT + dropdownExportMode.listItems.add("Dynamic", dynamic) + dropdownExportMode.listItems.add("Static", not dynamic) + + dropdownExportMode.tooltip = "Export Mode" + dropdownExportMode.tooltipDescription = "
Does this object move dynamically?" + + # ~~~~~~~~~~~~~~~~ WEIGHT CONFIGURATION ~~~~~~~~~~~~~~~~ + """ + Table for weight config. + - Used this to align multiple commandInputs on the same row + """ + weightTableInput = self.createTableInput( + "weight_table", + "Weight Table", + inputs, + 4, + "3:2:2:1", + 1, + ) + weightTableInput.tablePresentationStyle = 2 # set transparent background for table + + weight_name = inputs.addStringValueInput("weight_name", "Weight") + weight_name.value = "Weight" + weight_name.isReadOnly = True + + auto_calc_weight = self.createBooleanInput( + "auto_calc_weight", + "‎", + inputs, + checked=False, + tooltip="Approximate the weight of your robot assembly.", + tooltipadvanced="This may take a moment...", + enabled=True, + isCheckBox=False, + ) + auto_calc_weight.resourceFolder = IconPaths.stringIcons["calculate-enabled"] + auto_calc_weight.isFullWidth = True + + imperialUnits = exporterOptions.preferredUnits == PreferredUnits.IMPERIAL + if imperialUnits: + # ExporterOptions always contains the metric value + displayWeight = exporterOptions.robotWeight * 2.2046226218 + else: + displayWeight = exporterOptions.robotWeight + + weight_input = inputs.addValueInput( + "weight_input", + "Weight Input", + "", + adsk.core.ValueInput.createByReal(displayWeight), + ) + weight_input.tooltip = "Robot weight" + weight_input.tooltipDescription = """(in pounds)
This is the weight of the entire robot assembly.""" + + weight_unit = inputs.addDropDownCommandInput( + "weight_unit", + "Weight Unit", + adsk.core.DropDownStyles.LabeledIconDropDownStyle, + ) + + weight_unit.listItems.add("‎", imperialUnits, IconPaths.massIcons["LBS"]) # add listdropdown mass options + weight_unit.listItems.add("‎", not imperialUnits, IconPaths.massIcons["KG"]) # add listdropdown mass options + weight_unit.tooltip = "Unit of mass" + weight_unit.tooltipDescription = "
Configure the unit of mass for the weight calculation." + + weightTableInput.addCommandInput(weight_name, 0, 0) # add command inputs to table + weightTableInput.addCommandInput(auto_calc_weight, 0, 1) # add command inputs to table + weightTableInput.addCommandInput(weight_input, 0, 2) # add command inputs to table + weightTableInput.addCommandInput(weight_unit, 0, 3) # add command inputs to table + + # ~~~~~~~~~~~~~~~~ WHEEL CONFIGURATION ~~~~~~~~~~~~~~~~ + """ + Wheel configuration command input group + - Container for wheel selection Table + """ + wheelConfig = inputs.addGroupCommandInput("wheel_config", "Wheel Configuration") + wheelConfig.isExpanded = True + wheelConfig.isEnabled = True + wheelConfig.tooltip = "Select and define the drive-train wheels in your assembly." + + wheel_inputs = wheelConfig.children + + # WHEEL SELECTION TABLE + """ + All selected wheel occurrences appear here. + """ + wheelTableInput = self.createTableInput( + "wheel_table", + "Wheel Table", + wheel_inputs, + 4, + "1:4:2:2", + 50, + ) + + addWheelInput = wheel_inputs.addBoolValueInput("wheel_add", "Add", False) # add button + + removeWheelInput = wheel_inputs.addBoolValueInput("wheel_delete", "Remove", False) # remove button + + addWheelInput.tooltip = "Add a wheel joint" # tooltips + removeWheelInput.tooltip = "Remove a wheel joint" + + wheelSelectInput = wheel_inputs.addSelectionInput( + "wheel_select", + "Selection", + "Select the wheels joints in your drive-train assembly.", + ) + wheelSelectInput.addSelectionFilter("Joints") # filter selection to only occurrences - weight_unit.listItems.add("‎", imperialUnits, IconPaths.massIcons["LBS"]) # add listdropdown mass options - weight_unit.listItems.add("‎", not imperialUnits, IconPaths.massIcons["KG"]) # add listdropdown mass options - weight_unit.tooltip = "Unit of mass" - weight_unit.tooltipDescription = "
Configure the unit of mass for the weight calculation." - - weightTableInput.addCommandInput(weight_name, 0, 0) # add command inputs to table - weightTableInput.addCommandInput(auto_calc_weight, 0, 1) # add command inputs to table - weightTableInput.addCommandInput(weight_input, 0, 2) # add command inputs to table - weightTableInput.addCommandInput(weight_unit, 0, 3) # add command inputs to table - - # ~~~~~~~~~~~~~~~~ WHEEL CONFIGURATION ~~~~~~~~~~~~~~~~ - """ - Wheel configuration command input group - - Container for wheel selection Table - """ - wheelConfig = inputs.addGroupCommandInput("wheel_config", "Wheel Configuration") - wheelConfig.isExpanded = True - wheelConfig.isEnabled = True - wheelConfig.tooltip = "Select and define the drive-train wheels in your assembly." - - wheel_inputs = wheelConfig.children - - # WHEEL SELECTION TABLE - """ - All selected wheel occurrences appear here. - """ - wheelTableInput = self.createTableInput( - "wheel_table", - "Wheel Table", + wheelSelectInput.setSelectionLimits(0) # no selection count limit + wheelSelectInput.isEnabled = False + wheelSelectInput.isVisible = False + + wheelTableInput.addToolbarCommandInput(addWheelInput) # add buttons to the toolbar + wheelTableInput.addToolbarCommandInput(removeWheelInput) # add buttons to the toolbar + + wheelTableInput.addCommandInput( # create textbox input using helper (component name) + self.createTextBoxInput("name_header", "Name", wheel_inputs, "Joint name", bold=False), + 0, + 1, + ) + + wheelTableInput.addCommandInput( + self.createTextBoxInput( # wheel type header + "parent_header", + "Parent", wheel_inputs, - 4, - "1:4:2:2", - 50, - ) + "Wheel type", + background="#d9d9d9", # textbox header background color + ), + 0, + 2, + ) - addWheelInput = wheel_inputs.addBoolValueInput("wheel_add", "Add", False) # add button + wheelTableInput.addCommandInput( + self.createTextBoxInput( # Signal type header + "signal_header", + "Signal", + wheel_inputs, + "Signal type", + background="#d9d9d9", # textbox header background color + ), + 0, + 3, + ) - removeWheelInput = wheel_inputs.addBoolValueInput("wheel_delete", "Remove", False) # remove button + if exporterOptions.wheels: + for wheel in exporterOptions.wheels: + wheelEntity = gm.app.activeDocument.design.findEntityByToken(wheel.jointToken)[0] + addWheelToTable(wheelEntity) - addWheelInput.tooltip = "Add a wheel joint" # tooltips - removeWheelInput.tooltip = "Remove a wheel joint" + # ~~~~~~~~~~~~~~~~ JOINT CONFIGURATION ~~~~~~~~~~~~~~~~ + """ + Joint configuration group. Container for joint selection table + """ + jointConfig = inputs.addGroupCommandInput("joint_config", "Joint Configuration") + jointConfig.isExpanded = False + jointConfig.isVisible = True + jointConfig.tooltip = "Select and define joint occurrences in your assembly." - wheelSelectInput = wheel_inputs.addSelectionInput( - "wheel_select", - "Selection", - "Select the wheels joints in your drive-train assembly.", - ) - wheelSelectInput.addSelectionFilter("Joints") # filter selection to only occurrences + joint_inputs = jointConfig.children + + # JOINT SELECTION TABLE + """ + All selection joints appear here. + """ + jointTableInput = self.createTableInput( # create tablecommandinput using helper + "joint_table", + "Joint Table", + joint_inputs, + 6, + "1:2:2:2:2:2", + 50, + ) - wheelSelectInput.setSelectionLimits(0) # no selection count limit - wheelSelectInput.isEnabled = False - wheelSelectInput.isVisible = False + addJointInput = joint_inputs.addBoolValueInput("joint_add", "Add", False) # add button - wheelTableInput.addToolbarCommandInput(addWheelInput) # add buttons to the toolbar - wheelTableInput.addToolbarCommandInput(removeWheelInput) # add buttons to the toolbar + removeJointInput = joint_inputs.addBoolValueInput("joint_delete", "Remove", False) # remove button - wheelTableInput.addCommandInput( # create textbox input using helper (component name) - self.createTextBoxInput("name_header", "Name", wheel_inputs, "Joint name", bold=False), - 0, - 1, - ) + addJointInput.isEnabled = removeJointInput.isEnabled = True - wheelTableInput.addCommandInput( - self.createTextBoxInput( # wheel type header - "parent_header", - "Parent", - wheel_inputs, - "Wheel type", - background="#d9d9d9", # textbox header background color - ), - 0, - 2, - ) + addJointInput.tooltip = "Add a joint selection" # tooltips + removeJointInput.tooltip = "Remove a joint selection" - wheelTableInput.addCommandInput( - self.createTextBoxInput( # Signal type header - "signal_header", - "Signal", - wheel_inputs, - "Signal type", - background="#d9d9d9", # textbox header background color - ), - 0, - 3, - ) + jointSelectInput = joint_inputs.addSelectionInput( + "joint_select", + "Selection", + "Select a joint in your drive-train assembly.", + ) + + jointSelectInput.addSelectionFilter("Joints") # only allow joint selection + jointSelectInput.setSelectionLimits(0) # set no selection count limits + jointSelectInput.isEnabled = False + jointSelectInput.isVisible = False # make selection box invisible + + jointTableInput.addToolbarCommandInput(addJointInput) # add bool inputs to the toolbar + jointTableInput.addToolbarCommandInput(removeJointInput) # add bool inputs to the toolbar - if exporterOptions.wheels: - for wheel in exporterOptions.wheels: - wheelEntity = gm.app.activeDocument.design.findEntityByToken(wheel.jointToken)[0] - addWheelToTable(wheelEntity) - - # ~~~~~~~~~~~~~~~~ JOINT CONFIGURATION ~~~~~~~~~~~~~~~~ - """ - Joint configuration group. Container for joint selection table - """ - jointConfig = inputs.addGroupCommandInput("joint_config", "Joint Configuration") - jointConfig.isExpanded = False - jointConfig.isVisible = True - jointConfig.tooltip = "Select and define joint occurrences in your assembly." - - joint_inputs = jointConfig.children - - # JOINT SELECTION TABLE - """ - All selection joints appear here. - """ - jointTableInput = self.createTableInput( # create tablecommandinput using helper - "joint_table", - "Joint Table", + jointTableInput.addCommandInput( + self.createTextBoxInput( # create a textBoxCommandInput for the table header (Joint Motion), using helper + "motion_header", + "Motion", joint_inputs, - 6, - "1:2:2:2:2:2", - 50, - ) + "Motion", + bold=False, + ), + 0, + 0, + ) - addJointInput = joint_inputs.addBoolValueInput("joint_add", "Add", False) # add button + jointTableInput.addCommandInput( + self.createTextBoxInput( # textBoxCommandInput for table header (Joint Name), using helper + "name_header", "Name", joint_inputs, "Joint name", bold=False + ), + 0, + 1, + ) - removeJointInput = joint_inputs.addBoolValueInput("joint_delete", "Remove", False) # remove button + jointTableInput.addCommandInput( + self.createTextBoxInput( # another header using helper + "parent_header", + "Parent", + joint_inputs, + "Parent joint", + background="#d9d9d9", # background color + ), + 0, + 2, + ) - addJointInput.isEnabled = removeJointInput.isEnabled = True + jointTableInput.addCommandInput( + self.createTextBoxInput( # another header using helper + "signal_header", + "Signal", + joint_inputs, + "Signal type", + background="#d9d9d9", # back color + ), + 0, + 3, + ) - addJointInput.tooltip = "Add a joint selection" # tooltips - removeJointInput.tooltip = "Remove a joint selection" + jointTableInput.addCommandInput( + self.createTextBoxInput( # another header using helper + "speed_header", + "Speed", + joint_inputs, + "Joint Speed", + background="#d9d9d9", # back color + ), + 0, + 4, + ) - jointSelectInput = joint_inputs.addSelectionInput( - "joint_select", - "Selection", - "Select a joint in your drive-train assembly.", - ) + jointTableInput.addCommandInput( + self.createTextBoxInput( # another header using helper + "force_header", + "Force", + joint_inputs, + "Joint Force", + background="#d9d9d9", # back color + ), + 0, + 5, + ) - jointSelectInput.addSelectionFilter("Joints") # only allow joint selection - jointSelectInput.setSelectionLimits(0) # set no selection count limits - jointSelectInput.isEnabled = False - jointSelectInput.isVisible = False # make selection box invisible - - jointTableInput.addToolbarCommandInput(addJointInput) # add bool inputs to the toolbar - jointTableInput.addToolbarCommandInput(removeJointInput) # add bool inputs to the toolbar - - jointTableInput.addCommandInput( - self.createTextBoxInput( # create a textBoxCommandInput for the table header (Joint Motion), using helper - "motion_header", - "Motion", - joint_inputs, - "Motion", - bold=False, - ), - 0, - 0, - ) + # Fill the table with all joints in current design + for joint in list(gm.app.activeDocument.design.rootComponent.allJoints) + list( + gm.app.activeDocument.design.rootComponent.allAsBuiltJoints + ): + if ( + joint.jointMotion.jointType == JointMotions.REVOLUTE.value + or joint.jointMotion.jointType == JointMotions.SLIDER.value + ) and not joint.isSuppressed: + addJointToTable(joint) + + # ~~~~~~~~~~~~~~~~ GAMEPIECE CONFIGURATION ~~~~~~~~~~~~~~~~ + """ + Gamepiece group command input, isVisible=False by default + - Container for gamepiece selection table + """ + gamepieceConfig = inputs.addGroupCommandInput("gamepiece_config", "Gamepiece Configuration") + gamepieceConfig.isExpanded = True + gamepieceConfig.isVisible = False + gamepieceConfig.tooltip = "Select and define the gamepieces in your field." + gamepiece_inputs = gamepieceConfig.children - jointTableInput.addCommandInput( - self.createTextBoxInput( # textBoxCommandInput for table header (Joint Name), using helper - "name_header", "Name", joint_inputs, "Joint name", bold=False - ), - 0, - 1, - ) + # GAMEPIECE MASS CONFIGURATION + """ + Mass unit dropdown and calculation for gamepiece elements + """ + weightTableInput_f = self.createTableInput("weight_table_f", "Weight Table", gamepiece_inputs, 3, "6:2:1", 1) + weightTableInput_f.tablePresentationStyle = 2 # set to clear background + + weight_name_f = gamepiece_inputs.addStringValueInput("weight_name", "Weight") + weight_name_f.value = "Unit of Mass" + weight_name_f.isReadOnly = True + + auto_calc_weight_f = self.createBooleanInput( # CALCULATE button + "auto_calc_weight_f", + "‎", + gamepiece_inputs, + checked=False, + tooltip="Approximate the weight of all your selected gamepieces.", + enabled=True, + isCheckBox=False, + ) + auto_calc_weight_f.resourceFolder = IconPaths.stringIcons["calculate-enabled"] + auto_calc_weight_f.isFullWidth = True - jointTableInput.addCommandInput( - self.createTextBoxInput( # another header using helper - "parent_header", - "Parent", - joint_inputs, - "Parent joint", - background="#d9d9d9", # background color - ), - 0, - 2, - ) + weight_unit_f = gamepiece_inputs.addDropDownCommandInput( + "weight_unit_f", + "Unit of Mass", + adsk.core.DropDownStyles.LabeledIconDropDownStyle, + ) + weight_unit_f.listItems.add("‎", True, IconPaths.massIcons["LBS"]) # add listdropdown mass options + weight_unit_f.listItems.add("‎", False, IconPaths.massIcons["KG"]) # add listdropdown mass options + weight_unit_f.tooltip = "Unit of mass" + weight_unit_f.tooltipDescription = "
Configure the unit of mass for for the weight calculation." - jointTableInput.addCommandInput( - self.createTextBoxInput( # another header using helper - "signal_header", - "Signal", - joint_inputs, - "Signal type", - background="#d9d9d9", # back color - ), - 0, - 3, - ) + weightTableInput_f.addCommandInput(weight_name_f, 0, 0) # add command inputs to table + weightTableInput_f.addCommandInput(auto_calc_weight_f, 0, 1) # add command inputs to table + weightTableInput_f.addCommandInput(weight_unit_f, 0, 2) # add command inputs to table - jointTableInput.addCommandInput( - self.createTextBoxInput( # another header using helper - "speed_header", - "Speed", - joint_inputs, - "Joint Speed", - background="#d9d9d9", # back color - ), - 0, - 4, - ) + # GAMEPIECE SELECTION TABLE + """ + All selected gamepieces appear here + """ + gamepieceTableInput = self.createTableInput( + "gamepiece_table", + "Gamepiece", + gamepiece_inputs, + 4, + "1:8:5:12", + 50, + ) - jointTableInput.addCommandInput( - self.createTextBoxInput( # another header using helper - "force_header", - "Force", - joint_inputs, - "Joint Force", - background="#d9d9d9", # back color - ), - 0, - 5, - ) + addFieldInput = gamepiece_inputs.addBoolValueInput("field_add", "Add", False) - # Fill the table with all joints in current design - for joint in list(gm.app.activeDocument.design.rootComponent.allJoints) + list( - gm.app.activeDocument.design.rootComponent.allAsBuiltJoints - ): - if ( - joint.jointMotion.jointType == JointMotions.REVOLUTE.value - or joint.jointMotion.jointType == JointMotions.SLIDER.value - ) and not joint.isSuppressed: - addJointToTable(joint) - - # ~~~~~~~~~~~~~~~~ GAMEPIECE CONFIGURATION ~~~~~~~~~~~~~~~~ - """ - Gamepiece group command input, isVisible=False by default - - Container for gamepiece selection table - """ - gamepieceConfig = inputs.addGroupCommandInput("gamepiece_config", "Gamepiece Configuration") - gamepieceConfig.isExpanded = True - gamepieceConfig.isVisible = False - gamepieceConfig.tooltip = "Select and define the gamepieces in your field." - gamepiece_inputs = gamepieceConfig.children - - # GAMEPIECE MASS CONFIGURATION - """ - Mass unit dropdown and calculation for gamepiece elements - """ - weightTableInput_f = self.createTableInput( - "weight_table_f", "Weight Table", gamepiece_inputs, 3, "6:2:1", 1 - ) - weightTableInput_f.tablePresentationStyle = 2 # set to clear background + removeFieldInput = gamepiece_inputs.addBoolValueInput("field_delete", "Remove", False) + addFieldInput.isEnabled = removeFieldInput.isEnabled = True - weight_name_f = gamepiece_inputs.addStringValueInput("weight_name", "Weight") - weight_name_f.value = "Unit of Mass" - weight_name_f.isReadOnly = True + removeFieldInput.tooltip = "Remove a field element" + addFieldInput.tooltip = "Add a field element" - auto_calc_weight_f = self.createBooleanInput( # CALCULATE button - "auto_calc_weight_f", - "‎", - gamepiece_inputs, - checked=False, - tooltip="Approximate the weight of all your selected gamepieces.", - enabled=True, - isCheckBox=False, - ) - auto_calc_weight_f.resourceFolder = IconPaths.stringIcons["calculate-enabled"] - auto_calc_weight_f.isFullWidth = True + gamepieceSelectInput = gamepiece_inputs.addSelectionInput( + "gamepiece_select", + "Selection", + "Select the unique gamepieces in your field.", + ) + gamepieceSelectInput.addSelectionFilter("Occurrences") + gamepieceSelectInput.setSelectionLimits(0) + gamepieceSelectInput.isEnabled = True + gamepieceSelectInput.isVisible = False - weight_unit_f = gamepiece_inputs.addDropDownCommandInput( - "weight_unit_f", - "Unit of Mass", - adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) - weight_unit_f.listItems.add("‎", True, IconPaths.massIcons["LBS"]) # add listdropdown mass options - weight_unit_f.listItems.add("‎", False, IconPaths.massIcons["KG"]) # add listdropdown mass options - weight_unit_f.tooltip = "Unit of mass" - weight_unit_f.tooltipDescription = "
Configure the unit of mass for for the weight calculation." - - weightTableInput_f.addCommandInput(weight_name_f, 0, 0) # add command inputs to table - weightTableInput_f.addCommandInput(auto_calc_weight_f, 0, 1) # add command inputs to table - weightTableInput_f.addCommandInput(weight_unit_f, 0, 2) # add command inputs to table - - # GAMEPIECE SELECTION TABLE - """ - All selected gamepieces appear here - """ - gamepieceTableInput = self.createTableInput( - "gamepiece_table", + gamepieceTableInput.addToolbarCommandInput(addFieldInput) + gamepieceTableInput.addToolbarCommandInput(removeFieldInput) + + """ + Gamepiece table column headers. (the permanent captions in the first row of table) + """ + gamepieceTableInput.addCommandInput( + self.createTextBoxInput( + "e_header", + "Gamepiece name", + gamepiece_inputs, "Gamepiece", + bold=False, + ), + 0, + 1, + ) + + gamepieceTableInput.addCommandInput( + self.createTextBoxInput( + "w_header", + "Gamepiece weight", gamepiece_inputs, - 4, - "1:8:5:12", - 50, - ) + "Weight", + background="#d9d9d9", + ), + 0, + 2, + ) - addFieldInput = gamepiece_inputs.addBoolValueInput("field_add", "Add", False) + gamepieceTableInput.addCommandInput( + self.createTextBoxInput( + "f_header", + "Friction coefficient", + gamepiece_inputs, + "Friction coefficient", + background="#d9d9d9", + ), + 0, + 3, + ) - removeFieldInput = gamepiece_inputs.addBoolValueInput("field_delete", "Remove", False) - addFieldInput.isEnabled = removeFieldInput.isEnabled = True + # ====================================== ADVANCED TAB ====================================== + """ + Creates the advanced tab, which is the parent container for internal command inputs + """ + advancedSettings = INPUTS_ROOT.addTabCommandInput("advanced_settings", "Advanced") + advancedSettings.tooltip = ( + "Additional Advanced Settings to change how your model will be translated into Unity." + ) + a_input = advancedSettings.children - removeFieldInput.tooltip = "Remove a field element" - addFieldInput.tooltip = "Add a field element" + # ~~~~~~~~~~~~~~~~ EXPORTER SETTINGS ~~~~~~~~~~~~~~~~ + """ + Exporter settings group command + """ + exporterSettings = a_input.addGroupCommandInput("exporter_settings", "Exporter Settings") + exporterSettings.isExpanded = True + exporterSettings.isEnabled = True + exporterSettings.tooltip = "tooltip" # TODO: update tooltip + exporter_settings = exporterSettings.children + + self.createBooleanInput( + "compress", + "Compress Output", + exporter_settings, + checked=exporterOptions.compressOutput, + tooltip="Compress the output file for a smaller file size.", + tooltipadvanced="
Use the GZIP compression system to compress the resulting file which will be opened in the simulator, perfect if you want to share the file.
", + enabled=True, + ) - gamepieceSelectInput = gamepiece_inputs.addSelectionInput( - "gamepiece_select", - "Selection", - "Select the unique gamepieces in your field.", - ) - gamepieceSelectInput.addSelectionFilter("Occurrences") - gamepieceSelectInput.setSelectionLimits(0) - gamepieceSelectInput.isEnabled = True - gamepieceSelectInput.isVisible = False - - gamepieceTableInput.addToolbarCommandInput(addFieldInput) - gamepieceTableInput.addToolbarCommandInput(removeFieldInput) - - """ - Gamepiece table column headers. (the permanent captions in the first row of table) - """ - gamepieceTableInput.addCommandInput( - self.createTextBoxInput( - "e_header", - "Gamepiece name", - gamepiece_inputs, - "Gamepiece", - bold=False, - ), - 0, - 1, - ) + self.createBooleanInput( + "export_as_part", + "Export As Part", + exporter_settings, + checked=exporterOptions.exportAsPart, + tooltip="Use to export as a part for Mix And Match", + enabled=True, + ) - gamepieceTableInput.addCommandInput( - self.createTextBoxInput( - "w_header", - "Gamepiece weight", - gamepiece_inputs, - "Weight", - background="#d9d9d9", - ), - 0, - 2, - ) + # ~~~~~~~~~~~~~~~~ PHYSICS SETTINGS ~~~~~~~~~~~~~~~~ + """ + Physics settings group command + """ + physicsSettings = a_input.addGroupCommandInput("physics_settings", "Physics Settings") + + physicsSettings.isExpanded = False + physicsSettings.isEnabled = True + physicsSettings.tooltip = "tooltip" # TODO: update tooltip + physics_settings = physicsSettings.children + + # AARD-1687 + # Should also be commented out / removed? + # This would cause problems elsewhere but I can't tell i f + # this is even being used. + frictionOverrideTable = self.createTableInput( + "friction_override_table", + "", + physics_settings, + 2, + "1:2", + 1, + columnSpacing=25, + ) + frictionOverrideTable.tablePresentationStyle = 2 + # frictionOverrideTable.isFullWidth = True - gamepieceTableInput.addCommandInput( - self.createTextBoxInput( - "f_header", - "Friction coefficient", - gamepiece_inputs, - "Friction coefficient", - background="#d9d9d9", - ), - 0, - 3, - ) + frictionOverride = self.createBooleanInput( + "friction_override", + "", + physics_settings, + checked=False, + tooltip="Manually override the default friction values on the bodies in the assembly.", + enabled=True, + isCheckBox=False, + ) + frictionOverride.resourceFolder = IconPaths.stringIcons["friction_override-enabled"] + frictionOverride.isFullWidth = True - # ====================================== ADVANCED TAB ====================================== - """ - Creates the advanced tab, which is the parent container for internal command inputs - """ - advancedSettings = INPUTS_ROOT.addTabCommandInput("advanced_settings", "Advanced") - advancedSettings.tooltip = ( - "Additional Advanced Settings to change how your model will be translated into Unity." - ) - a_input = advancedSettings.children - - # ~~~~~~~~~~~~~~~~ EXPORTER SETTINGS ~~~~~~~~~~~~~~~~ - """ - Exporter settings group command - """ - exporterSettings = a_input.addGroupCommandInput("exporter_settings", "Exporter Settings") - exporterSettings.isExpanded = True - exporterSettings.isEnabled = True - exporterSettings.tooltip = "tooltip" # TODO: update tooltip - exporter_settings = exporterSettings.children - - self.createBooleanInput( - "compress", - "Compress Output", - exporter_settings, - checked=exporterOptions.compressOutput, - tooltip="Compress the output file for a smaller file size.", - tooltipadvanced="
Use the GZIP compression system to compress the resulting file which will be opened in the simulator, perfect if you want to share the file.
", - enabled=True, - ) + valueList = [1] + for i in range(20): + valueList.append(i / 20) - self.createBooleanInput( - "export_as_part", - "Export As Part", - exporter_settings, - checked=exporterOptions.exportAsPart, - tooltip="Use to export as a part for Mix And Match", - enabled=True, - ) + frictionCoeff = physics_settings.addFloatSliderListCommandInput( + "friction_coeff_override", "Friction Coefficient", "", valueList + ) + frictionCoeff.isVisible = False + frictionCoeff.valueOne = 0.5 + frictionCoeff.tooltip = "Friction coefficient of field element." + frictionCoeff.tooltipDescription = "Friction coefficients range from 0 (ice) to 1 (rubber)." - # ~~~~~~~~~~~~~~~~ PHYSICS SETTINGS ~~~~~~~~~~~~~~~~ - """ - Physics settings group command - """ - physicsSettings = a_input.addGroupCommandInput("physics_settings", "Physics Settings") - - physicsSettings.isExpanded = False - physicsSettings.isEnabled = True - physicsSettings.tooltip = "tooltip" # TODO: update tooltip - physics_settings = physicsSettings.children - - # AARD-1687 - # Should also be commented out / removed? - # This would cause problems elsewhere but I can't tell i f - # this is even being used. - frictionOverrideTable = self.createTableInput( - "friction_override_table", - "", - physics_settings, - 2, - "1:2", - 1, - columnSpacing=25, - ) - frictionOverrideTable.tablePresentationStyle = 2 - # frictionOverrideTable.isFullWidth = True - - frictionOverride = self.createBooleanInput( - "friction_override", - "", - physics_settings, - checked=False, - tooltip="Manually override the default friction values on the bodies in the assembly.", - enabled=True, - isCheckBox=False, - ) - frictionOverride.resourceFolder = IconPaths.stringIcons["friction_override-enabled"] - frictionOverride.isFullWidth = True + frictionOverrideTable.addCommandInput(frictionOverride, 0, 0) + frictionOverrideTable.addCommandInput(frictionCoeff, 0, 1) + + # ~~~~~~~~~~~~~~~~ JOINT SETTINGS ~~~~~~~~~~~~~~~~ + """ + Joint settings group command + """ - valueList = [1] - for i in range(20): - valueList.append(i / 20) + # Transition: AARD-1689 + # Should possibly be implemented later? + + # jointsSettings = a_input.addGroupCommandInput( + # "joints_settings", "Joints Settings" + # ) + # jointsSettings.isExpanded = False + # jointsSettings.isEnabled = True + # jointsSettings.tooltip = "tooltip" # TODO: update tooltip + # joints_settings = jointsSettings.children + + # self.createBooleanInput( + # "kinematic_only", + # "Kinematic Only", + # joints_settings, + # checked=False, + # tooltip="tooltip", # TODO: update tooltip + # enabled=True, + # ) + + # self.createBooleanInput( + # "calculate_limits", + # "Calculate Limits", + # joints_settings, + # checked=True, + # tooltip="tooltip", # TODO: update tooltip + # enabled=True, + # ) + + # self.createBooleanInput( + # "auto_assign_ids", + # "Auto-Assign ID's", + # joints_settings, + # checked=True, + # tooltip="tooltip", # TODO: update tooltip + # enabled=True, + # ) + + # ~~~~~~~~~~~~~~~~ CONTROLLER SETTINGS ~~~~~~~~~~~~~~~~ + """ + Controller settings group command + """ - frictionCoeff = physics_settings.addFloatSliderListCommandInput( - "friction_coeff_override", "Friction Coefficient", "", valueList - ) - frictionCoeff.isVisible = False - frictionCoeff.valueOne = 0.5 - frictionCoeff.tooltip = "Friction coefficient of field element." - frictionCoeff.tooltipDescription = "Friction coefficients range from 0 (ice) to 1 (rubber)." - - frictionOverrideTable.addCommandInput(frictionOverride, 0, 0) - frictionOverrideTable.addCommandInput(frictionCoeff, 0, 1) - - # ~~~~~~~~~~~~~~~~ JOINT SETTINGS ~~~~~~~~~~~~~~~~ - """ - Joint settings group command - """ - - # Transition: AARD-1689 - # Should possibly be implemented later? - - # jointsSettings = a_input.addGroupCommandInput( - # "joints_settings", "Joints Settings" - # ) - # jointsSettings.isExpanded = False - # jointsSettings.isEnabled = True - # jointsSettings.tooltip = "tooltip" # TODO: update tooltip - # joints_settings = jointsSettings.children - - # self.createBooleanInput( - # "kinematic_only", - # "Kinematic Only", - # joints_settings, - # checked=False, - # tooltip="tooltip", # TODO: update tooltip - # enabled=True, - # ) - - # self.createBooleanInput( - # "calculate_limits", - # "Calculate Limits", - # joints_settings, - # checked=True, - # tooltip="tooltip", # TODO: update tooltip - # enabled=True, - # ) - - # self.createBooleanInput( - # "auto_assign_ids", - # "Auto-Assign ID's", - # joints_settings, - # checked=True, - # tooltip="tooltip", # TODO: update tooltip - # enabled=True, - # ) - - # ~~~~~~~~~~~~~~~~ CONTROLLER SETTINGS ~~~~~~~~~~~~~~~~ - """ - Controller settings group command - """ - - # Transition: AARD-1689 - # Should possibly be implemented later? - - # controllerSettings = a_input.addGroupCommandInput( - # "controller_settings", "Controller Settings" - # ) - - # controllerSettings.isExpanded = False - # controllerSettings.isEnabled = True - # controllerSettings.tooltip = "tooltip" # TODO: update tooltip - # controller_settings = controllerSettings.children - - # self.createBooleanInput( # export signals checkbox - # "export_signals", - # "Export Signals", - # controller_settings, - # checked=True, - # tooltip="tooltip", - # enabled=True, - # ) - - # clear all selections before instantiating handlers. - gm.ui.activeSelections.clear() + # Transition: AARD-1689 + # Should possibly be implemented later? + + # controllerSettings = a_input.addGroupCommandInput( + # "controller_settings", "Controller Settings" + # ) + + # controllerSettings.isExpanded = False + # controllerSettings.isEnabled = True + # controllerSettings.tooltip = "tooltip" # TODO: update tooltip + # controller_settings = controllerSettings.children - # ====================================== EVENT HANDLERS ====================================== - """ - Instantiating all the event handlers - """ + # self.createBooleanInput( # export signals checkbox + # "export_signals", + # "Export Signals", + # controller_settings, + # checked=True, + # tooltip="tooltip", + # enabled=True, + # ) - onExecute = ConfigureCommandExecuteHandler() - cmd.execute.add(onExecute) - gm.handlers.append(onExecute) # 0 + # clear all selections before instantiating handlers. + gm.ui.activeSelections.clear() - onInputChanged = ConfigureCommandInputChanged(cmd) - cmd.inputChanged.add(onInputChanged) - gm.handlers.append(onInputChanged) # 1 + # ====================================== EVENT HANDLERS ====================================== + """ + Instantiating all the event handlers + """ - onExecutePreview = CommandExecutePreviewHandler(cmd) - cmd.executePreview.add(onExecutePreview) - gm.handlers.append(onExecutePreview) # 2 + onExecute = ConfigureCommandExecuteHandler() + cmd.execute.add(onExecute) + gm.handlers.append(onExecute) # 0 - onSelect = MySelectHandler(cmd) - cmd.select.add(onSelect) - gm.handlers.append(onSelect) # 3 + onInputChanged = ConfigureCommandInputChanged(cmd) + cmd.inputChanged.add(onInputChanged) + gm.handlers.append(onInputChanged) # 1 - onPreSelect = MyPreSelectHandler(cmd) - cmd.preSelect.add(onPreSelect) - gm.handlers.append(onPreSelect) # 4 + onExecutePreview = CommandExecutePreviewHandler(cmd) + cmd.executePreview.add(onExecutePreview) + gm.handlers.append(onExecutePreview) # 2 - onPreSelectEnd = MyPreselectEndHandler(cmd) - cmd.preSelectEnd.add(onPreSelectEnd) - gm.handlers.append(onPreSelectEnd) # 5 + onSelect = MySelectHandler(cmd) + cmd.select.add(onSelect) + gm.handlers.append(onSelect) # 3 - onDestroy = MyCommandDestroyHandler() - cmd.destroy.add(onDestroy) - gm.handlers.append(onDestroy) # 8 + onPreSelect = MyPreSelectHandler(cmd) + cmd.preSelect.add(onPreSelect) + gm.handlers.append(onPreSelect) # 4 - except: - if gm.ui: - gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + onPreSelectEnd = MyPreselectEndHandler(cmd) + cmd.preSelectEnd.add(onPreSelectEnd) + gm.handlers.append(onPreSelectEnd) # 5 + onDestroy = MyCommandDestroyHandler() + cmd.destroy.add(onDestroy) + gm.handlers.append(onDestroy) # 8 + + @logFailure def createBooleanInput( self, _id: str, @@ -846,18 +818,14 @@ def createBooleanInput( Returns: adsk.core.BoolValueCommandInput: Recently created command input """ - try: - _input = inputs.addBoolValueInput(_id, name, isCheckBox) - _input.value = checked - _input.isEnabled = enabled - _input.tooltip = tooltip - _input.tooltipDescription = tooltipadvanced - return _input - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}.createBooleanInput()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) - + _input = inputs.addBoolValueInput(_id, name, isCheckBox) + _input.value = checked + _input.isEnabled = enabled + _input.tooltip = tooltip + _input.tooltipDescription = tooltipadvanced + return _input + + @logFailure def createTableInput( self, _id: str, @@ -886,18 +854,14 @@ def createTableInput( Returns: adsk.core.TableCommandInput: created tableCommandInput """ - try: - _input = inputs.addTableCommandInput(_id, name, columns, ratio) - _input.minimumVisibleRows = minRows - _input.maximumVisibleRows = maxRows - _input.columnSpacing = columnSpacing - _input.rowSpacing = rowSpacing - return _input - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}.createTableInput()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) - + _input = inputs.addTableCommandInput(_id, name, columns, ratio) + _input.minimumVisibleRows = minRows + _input.maximumVisibleRows = maxRows + _input.columnSpacing = columnSpacing + _input.rowSpacing = rowSpacing + return _input + + @logFailure def createTextBoxInput( self, _id: str, @@ -932,37 +896,32 @@ def createTextBoxInput( Returns: adsk.core.TextBoxCommandInput: newly created textBoxCommandInput """ - try: - i = ["", ""] - b = ["", ""] - - if bold: - b[0] = "" - b[1] = "" - if italics: - i[0] = "" - i[1] = "" - - # simple wrapper for html formatting - wrapper = """ -
-

- %s%s{}%s%s -

- - """.format( - text - ) - _text = wrapper % (background, alignment, fontSize, b[0], i[0], i[1], b[1]) + i = ["", ""] + b = ["", ""] + + if bold: + b[0] = "" + b[1] = "" + if italics: + i[0] = "" + i[1] = "" + + # simple wrapper for html formatting + wrapper = """ +
+

+ %s%s{}%s%s +

+ + """.format( + text + ) + _text = wrapper % (background, alignment, fontSize, b[0], i[0], i[1], b[1]) - _input = inputs.addTextBoxCommandInput(_id, name, _text, rowCount, read) - _input.tooltip = tooltip - _input.tooltipDescription = advanced_tooltip - return _input - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.createTextBoxInput()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + _input = inputs.addTextBoxCommandInput(_id, name, _text, rowCount, read) + _input.tooltip = tooltip + _input.tooltipDescription = advanced_tooltip + return _input class ConfigureCommandExecuteHandler(adsk.core.CommandEventHandler): @@ -986,214 +945,208 @@ class ConfigureCommandExecuteHandler(adsk.core.CommandEventHandler): def __init__(self): super().__init__() - self.log = logging.getLogger(f"{INTERNAL_ID}.UI.{self.__class__.__name__}") self.current = SerialCommand() self.designAttrs = adsk.core.Application.get().activeProduct.attributes + @logFailure def notify(self, args): - try: - eventArgs = adsk.core.CommandEventArgs.cast(args) - exporterOptions = ExporterOptions().readFromDesign() - - if eventArgs.executeFailed: - self.log.error("Could not execute configuration due to failure") - return - - export_as_part_boolean = ( - eventArgs.command.commandInputs.itemById("advanced_settings") - .children.itemById("exporter_settings") - .children.itemById("export_as_part") - ).value - - processedFileName = gm.app.activeDocument.name.replace(" ", "_") - dropdownExportMode = INPUTS_ROOT.itemById("mode") - if dropdownExportMode.selectedItem.index == 0: - isRobot = True - elif dropdownExportMode.selectedItem.index == 1: - isRobot = False - - if isRobot: - savepath = FileDialogConfig.SaveFileDialog( - defaultPath=exporterOptions.fileLocation, - ext="Synthesis File (*.synth)", - ) - else: - savepath = FileDialogConfig.SaveFileDialog(defaultPath=exporterOptions.fileLocation) - - if savepath == False: - # save was canceled - return - - updatedPath = pathlib.Path(savepath).parent - if updatedPath != self.current.filePath: - self.current.filePath = str(updatedPath) - - adsk.doEvents() - # get active document - design = gm.app.activeDocument.design - name = design.rootComponent.name.rsplit(" ", 1)[0] - version = design.rootComponent.name.rsplit(" ", 1)[1] - - _exportWheels = [] # all selected wheels, formatted for parseOptions - _exportJoints = [] # all selected joints, formatted for parseOptions - _exportGamepieces = [] # TODO work on the code to populate Gamepiece - _robotWeight = float - _mode = ExportMode.ROBOT + eventArgs = adsk.core.CommandEventArgs.cast(args) + exporterOptions = ExporterOptions().readFromDesign() + + if eventArgs.executeFailed: + # Transition: AARD-1735 + # This is a very helpful error message + logger.error("Could not execute configuration due to failure") + raise RuntimeError("For some reason the execute failed?") + + export_as_part_boolean = ( + eventArgs.command.commandInputs.itemById("advanced_settings") + .children.itemById("exporter_settings") + .children.itemById("export_as_part") + ).value + + processedFileName = gm.app.activeDocument.name.replace(" ", "_") + dropdownExportMode = INPUTS_ROOT.itemById("mode") + if dropdownExportMode.selectedItem.index == 0: + isRobot = True + elif dropdownExportMode.selectedItem.index == 1: + isRobot = False + + if isRobot: + savepath = FileDialogConfig.SaveFileDialog( + defaultPath=exporterOptions.fileLocation, + ext="Synthesis File (*.synth)", + ) + else: + savepath = FileDialogConfig.SaveFileDialog(defaultPath=exporterOptions.fileLocation) - """ - Loops through all rows in the wheel table to extract all the input values - """ - onSelect = gm.handlers[3] - wheelTableInput = wheelTable() - for row in range(wheelTableInput.rowCount): - if row == 0: - continue + if savepath == False: + # save was canceled + return - wheelTypeIndex = wheelTableInput.getInputAtPosition( - row, 2 - ).selectedItem.index # This must be either 0 or 1 for standard or omni + updatedPath = pathlib.Path(savepath).parent + if updatedPath != self.current.filePath: + self.current.filePath = str(updatedPath) - signalTypeIndex = wheelTableInput.getInputAtPosition(row, 3).selectedItem.index + adsk.doEvents() + # get active document + design = gm.app.activeDocument.design + name = design.rootComponent.name.rsplit(" ", 1)[0] + version = design.rootComponent.name.rsplit(" ", 1)[1] - _exportWheels.append( - Wheel( - WheelListGlobal[row - 1].entityToken, - WheelType(wheelTypeIndex + 1), - SignalType(signalTypeIndex + 1), - # onSelect.wheelJointList[row-1][0] # GUID of wheel joint. if no joint found, default to None - ) - ) + _exportWheels = [] # all selected wheels, formatted for parseOptions + _exportJoints = [] # all selected joints, formatted for parseOptions + _exportGamepieces = [] # TODO work on the code to populate Gamepiece + _robotWeight = float + _mode = ExportMode.ROBOT - """ - Loops through all rows in the joint table to extract the input values - """ - jointTableInput = jointTable() - for row in range(jointTableInput.rowCount): - if row == 0: - continue + """ + Loops through all rows in the wheel table to extract all the input values + """ + onSelect = gm.handlers[3] + wheelTableInput = wheelTable() + for row in range(wheelTableInput.rowCount): + if row == 0: + continue - parentJointIndex = jointTableInput.getInputAtPosition( - row, 2 - ).selectedItem.index # parent joint index, int + wheelTypeIndex = wheelTableInput.getInputAtPosition( + row, 2 + ).selectedItem.index # This must be either 0 or 1 for standard or omni - signalTypeIndex = jointTableInput.getInputAtPosition( - row, 3 - ).selectedItem.index # signal type index, int + signalTypeIndex = wheelTableInput.getInputAtPosition(row, 3).selectedItem.index - # typeString = jointTableInput.getInputAtPosition( - # row, 0 - # ).name + _exportWheels.append( + Wheel( + WheelListGlobal[row - 1].entityToken, + WheelType(wheelTypeIndex + 1), + SignalType(signalTypeIndex + 1), + # onSelect.wheelJointList[row-1][0] # GUID of wheel joint. if no joint found, default to None + ) + ) + + """ + Loops through all rows in the joint table to extract the input values + """ + jointTableInput = jointTable() + for row in range(jointTableInput.rowCount): + if row == 0: + continue - jointSpeed = jointTableInput.getInputAtPosition(row, 4).value + parentJointIndex = jointTableInput.getInputAtPosition(row, 2).selectedItem.index # parent joint index, int - jointForce = jointTableInput.getInputAtPosition(row, 5).value + signalTypeIndex = jointTableInput.getInputAtPosition(row, 3).selectedItem.index # signal type index, int - parentJointToken = "" + # typeString = jointTableInput.getInputAtPosition( + # row, 0 + # ).name - if parentJointIndex == 0: - _exportJoints.append( - Joint( - JointListGlobal[row - 1].entityToken, - JointParentType.ROOT, - SignalType(signalTypeIndex + 1), - jointSpeed, - jointForce / 100.0, - ) # parent joint GUID - ) - continue - elif parentJointIndex < row: - parentJointToken = JointListGlobal[parentJointIndex - 1].entityToken # parent joint GUID, str - else: - parentJointToken = JointListGlobal[parentJointIndex + 1].entityToken # parent joint GUID, str + jointSpeed = jointTableInput.getInputAtPosition(row, 4).value - # for wheel in _exportWheels: - # find some way to get joint - # 1. Compare Joint occurrence1 to wheel.occurrenceToken - # 2. if true set the parent to Root + jointForce = jointTableInput.getInputAtPosition(row, 5).value + parentJointToken = "" + + if parentJointIndex == 0: _exportJoints.append( Joint( JointListGlobal[row - 1].entityToken, - parentJointToken, + JointParentType.ROOT, SignalType(signalTypeIndex + 1), jointSpeed, - jointForce, - ) + jointForce / 100.0, + ) # parent joint GUID ) + continue + elif parentJointIndex < row: + parentJointToken = JointListGlobal[parentJointIndex - 1].entityToken # parent joint GUID, str + else: + parentJointToken = JointListGlobal[parentJointIndex + 1].entityToken # parent joint GUID, str + + # for wheel in _exportWheels: + # find some way to get joint + # 1. Compare Joint occurrence1 to wheel.occurrenceToken + # 2. if true set the parent to Root + + _exportJoints.append( + Joint( + JointListGlobal[row - 1].entityToken, + parentJointToken, + SignalType(signalTypeIndex + 1), + jointSpeed, + jointForce, + ) + ) - """ - Loops through all rows in the gamepiece table to extract the input values - """ - gamepieceTableInput = gamepieceTable() - weight_unit_f = INPUTS_ROOT.itemById("weight_unit_f") - for row in range(gamepieceTableInput.rowCount): - if row == 0: - continue + """ + Loops through all rows in the gamepiece table to extract the input values + """ + gamepieceTableInput = gamepieceTable() + weight_unit_f = INPUTS_ROOT.itemById("weight_unit_f") + for row in range(gamepieceTableInput.rowCount): + if row == 0: + continue - weightValue = gamepieceTableInput.getInputAtPosition(row, 2).value # weight/mass input, float + weightValue = gamepieceTableInput.getInputAtPosition(row, 2).value # weight/mass input, float - if weight_unit_f.selectedItem.index == 0: - weightValue /= 2.2046226218 + if weight_unit_f.selectedItem.index == 0: + weightValue /= 2.2046226218 - frictionValue = gamepieceTableInput.getInputAtPosition(row, 3).valueOne # friction value, float + frictionValue = gamepieceTableInput.getInputAtPosition(row, 3).valueOne # friction value, float - _exportGamepieces.append( - Gamepiece( - guid_occurrence(GamepieceListGlobal[row - 1]), - weightValue, - frictionValue, - ) + _exportGamepieces.append( + Gamepiece( + guid_occurrence(GamepieceListGlobal[row - 1]), + weightValue, + frictionValue, ) + ) - """ - Robot Weight - """ - weight_input = INPUTS_ROOT.itemById("weight_input") - weight_unit = INPUTS_ROOT.itemById("weight_unit") + """ + Robot Weight + """ + weight_input = INPUTS_ROOT.itemById("weight_input") + weight_unit = INPUTS_ROOT.itemById("weight_unit") - if weight_unit.selectedItem.index == 0: - selectedUnits = PreferredUnits.IMPERIAL - _robotWeight = float(weight_input.value) / 2.2046226218 - else: - selectedUnits = PreferredUnits.METRIC - _robotWeight = float(weight_input.value) - - """ - Export Mode - """ - dropdownExportMode = INPUTS_ROOT.itemById("mode") - if dropdownExportMode.selectedItem.index == 0: - _mode = ExportMode.ROBOT - elif dropdownExportMode.selectedItem.index == 1: - _mode = ExportMode.FIELD - - global compress - compress = ( - eventArgs.command.commandInputs.itemById("advanced_settings") - .children.itemById("exporter_settings") - .children.itemById("compress") - ).value - - exporterOptions = ExporterOptions( - savepath, - name, - version, - materials=0, - joints=_exportJoints, - wheels=_exportWheels, - gamepieces=_exportGamepieces, - preferredUnits=selectedUnits, - robotWeight=_robotWeight, - exportMode=_mode, - compressOutput=compress, - exportAsPart=export_as_part_boolean, - ) + if weight_unit.selectedItem.index == 0: + selectedUnits = PreferredUnits.IMPERIAL + _robotWeight = float(weight_input.value) / 2.2046226218 + else: + selectedUnits = PreferredUnits.METRIC + _robotWeight = float(weight_input.value) - Parser(exporterOptions).export() - exporterOptions.writeToDesign() - except: - if gm.ui: - gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) + """ + Export Mode + """ + dropdownExportMode = INPUTS_ROOT.itemById("mode") + if dropdownExportMode.selectedItem.index == 0: + _mode = ExportMode.ROBOT + elif dropdownExportMode.selectedItem.index == 1: + _mode = ExportMode.FIELD + + global compress + compress = ( + eventArgs.command.commandInputs.itemById("advanced_settings") + .children.itemById("exporter_settings") + .children.itemById("compress") + ).value + + exporterOptions = ExporterOptions( + savepath, + name, + version, + materials=0, + joints=_exportJoints, + wheels=_exportWheels, + gamepieces=_exportGamepieces, + preferredUnits=selectedUnits, + robotWeight=_robotWeight, + exportMode=_mode, + compressOutput=compress, + exportAsPart=export_as_part_boolean, + ) + + Parser(exporterOptions).export() + exporterOptions.writeToDesign() class CommandExecutePreviewHandler(adsk.core.CommandEventHandler): @@ -1207,6 +1160,7 @@ def __init__(self, cmd) -> None: super().__init__() self.cmd = cmd + @logFailure def notify(self, args): """Notify member called when a command event is triggered @@ -1274,14 +1228,10 @@ def notify(self, args): CustomGraphics.createTextGraphics(gamepiece, GamepieceListGlobal) else: gm.app.activeDocument.design.rootComponent.opacity = 1 + # Transition: AARD-1735 + # Should be revisited and remove except AttributeError: pass - except: - if gm.ui: - gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}").error( - "Failed:\n{}".format(traceback.format_exc()) - ) class MySelectHandler(adsk.core.SelectionEventHandler): @@ -1306,6 +1256,7 @@ def __init__(self, cmd): self.wheelJointList = [] self.algorithmicSelection = True + @logFailure(messageBox=True) def traverseAssembly( self, child_occurrences: adsk.fusion.OccurrenceList, jointedOcc: dict ): # recursive traversal to check if children are jointed @@ -1318,22 +1269,16 @@ def traverseAssembly( occ (Occurrence): if a match is found, return the jointed occurrence None: if no match is found """ - try: - for occ in child_occurrences: - for joint, value in jointedOcc.items(): - if occ in value: - return [joint, occ] # occurrence that is jointed - - if occ.childOccurrences: # if occurrence has children, traverse sub-tree - self.traverseAssembly(occ.childOccurrences, jointedOcc) - return None # no jointed occurrence found - except: - if gm.ui: - gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}.traverseAssembly()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + for occ in child_occurrences: + for joint, value in jointedOcc.items(): + if occ in value: + return [joint, occ] # occurrence that is jointed + + if occ.childOccurrences: # if occurrence has children, traverse sub-tree + self.traverseAssembly(occ.childOccurrences, jointedOcc) + return None # no jointed occurrence found + @logFailure(messageBox=True) def wheelParent(self, occ: adsk.fusion.Occurrence): """### Identify an occurrence that encompasses the entire wheel component. @@ -1355,117 +1300,103 @@ def wheelParent(self, occ: adsk.fusion.Occurrence): Returns: occ (Occurrence): Wheel parent """ + parent = occ.assemblyContext + jointedOcc = {} # dictionary with all jointed occurrences + try: - parent = occ.assemblyContext - jointedOcc = {} # dictionary with all jointed occurrences - - try: - for joint in occ.joints: - if joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: - # gm.ui.messageBox("Selection is directly jointed.\nReturning selection.\n\n" + "Occurrence:\n--> " + occ.name + "\nJoint:\n--> " + joint.name) - return [joint.entityToken, occ] - except: - for joint in occ.component.joints: - if joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: - # gm.ui.messageBox("Selection is directly jointed.\nReturning selection.\n\n" + "Occurrence:\n--> " + occ.name + "\nJoint:\n--> " + joint.name) - return [joint.entityToken, occ] - - if parent == None: # no parent occurrence - # gm.ui.messageBox("Selection has no parent occurrence.\nReturning selection.\n\n" + "Occurrence:\n--> " + occ.name + "\nJoint:\n--> NONE") - return [None, occ] # return what is selected - - for joint in gm.app.activeDocument.design.rootComponent.allJoints: - if joint.jointMotion.jointType != adsk.fusion.JointTypes.RevoluteJointType: - continue - jointedOcc[joint.entityToken] = [ - joint.occurrenceOne, - joint.occurrenceTwo, - ] - - parentLevel = 1 # the number of nodes above the one selected - returned = None # the returned value of traverseAssembly() - parentOccurrence = occ # the parent occurrence that will be returned - treeParent = parent # each parent that will traverse up in algorithm. - - while treeParent != None: # loops until reaches top-level component - returned = self.traverseAssembly(treeParent.childOccurrences, jointedOcc) - - if returned != None: - for i in range(parentLevel): - parentOccurrence = parentOccurrence.assemblyContext - - # gm.ui.messageBox("Joint found.\nReturning parent occurrence.\n\n" + "Selected occurrence:\n--> " + occ.name + "\nParent:\n--> " + parentOccurrence.name + "\nJoint:\n--> " + returned[0] + "\nNodes above selection:\n--> " + str(parentLevel)) - return [returned[0], parentOccurrence] - - parentLevel += 1 - treeParent = treeParent.assemblyContext - # gm.ui.messageBox("No jointed occurrence found.\nReturning selection.\n\n" + "Occurrence:\n--> " + occ.name + "\nJoint:\n--> NONE") - return [None, occ] # no jointed occurrence found, return what is selected + for joint in occ.joints: + if joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: + # gm.ui.messageBox("Selection is directly jointed.\nReturning selection.\n\n" + "Occurrence:\n--> " + occ.name + "\nJoint:\n--> " + joint.name) + return [joint.entityToken, occ] except: - if gm.ui: - gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}.wheelParent()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) - # gm.ui.messageBox("Selection's component has no referenced joints.\nReturning selection.\n\n" + "Occurrence:\n--> " + occ.name + "\nJoint:\n--> NONE") - return [None, occ] + for joint in occ.component.joints: + if joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: + # gm.ui.messageBox("Selection is directly jointed.\nReturning selection.\n\n" + "Occurrence:\n--> " + occ.name + "\nJoint:\n--> " + joint.name) + return [joint.entityToken, occ] - def notify(self, args: adsk.core.SelectionEventArgs): - """### Notify member is called when a selection event is triggered. + if parent == None: # no parent occurrence + # gm.ui.messageBox("Selection has no parent occurrence.\nReturning selection.\n\n" + "Occurrence:\n--> " + occ.name + "\nJoint:\n--> NONE") + return [None, occ] # return what is selected - Args: - args (SelectionEventArgs): A selection event argument - """ - try: - # eventArgs = adsk.core.SelectionEventArgs.cast(args) + for joint in gm.app.activeDocument.design.rootComponent.allJoints: + if joint.jointMotion.jointType != adsk.fusion.JointTypes.RevoluteJointType: + continue + jointedOcc[joint.entityToken] = [ + joint.occurrenceOne, + joint.occurrenceTwo, + ] - self.selectedOcc = adsk.fusion.Occurrence.cast(args.selection.entity) - self.selectedJoint = args.selection.entity + parentLevel = 1 # the number of nodes above the one selected + returned = None # the returned value of traverseAssembly() + parentOccurrence = occ # the parent occurrence that will be returned + treeParent = parent # each parent that will traverse up in algorithm. - selectionInput = args.activeInput + while treeParent != None: # loops until reaches top-level component + returned = self.traverseAssembly(treeParent.childOccurrences, jointedOcc) - dropdownExportMode = INPUTS_ROOT.itemById("mode") - duplicateSelection = INPUTS_ROOT.itemById("duplicate_selection") - # indicator = INPUTS_ROOT.itemById("algorithmic_indicator") + if returned != None: + for i in range(parentLevel): + parentOccurrence = parentOccurrence.assemblyContext - if self.selectedOcc: - self.cmd.setCursor("", 0, 0) - if dropdownExportMode.selectedItem.index == 1: - occurrenceList = gm.app.activeDocument.design.rootComponent.allOccurrencesByComponent( - self.selectedOcc.component - ) - for occ in occurrenceList: - if occ not in GamepieceListGlobal: - addGamepieceToTable(occ) - else: - removeGamePieceFromTable(GamepieceListGlobal.index(occ)) - - selectionInput.isEnabled = False - selectionInput.isVisible = False - - elif self.selectedJoint: - self.cmd.setCursor("", 0, 0) - jointType = self.selectedJoint.jointMotion.jointType - if jointType == JointMotions.REVOLUTE.value or jointType == JointMotions.SLIDER.value: - if jointType == JointMotions.REVOLUTE.value and MySelectHandler.lastInputCmd.id == "wheel_select": - addWheelToTable(self.selectedJoint) - elif jointType == JointMotions.REVOLUTE.value and MySelectHandler.lastInputCmd.id == "wheel_remove": - if self.selectedJoint in WheelListGlobal: - removeWheelFromTable(WheelListGlobal.index(self.selectedJoint)) - else: - if self.selectedJoint not in JointListGlobal: - addJointToTable(self.selectedJoint) - else: - removeJointFromTable(self.selectedJoint) + # gm.ui.messageBox("Joint found.\nReturning parent occurrence.\n\n" + "Selected occurrence:\n--> " + occ.name + "\nParent:\n--> " + parentOccurrence.name + "\nJoint:\n--> " + returned[0] + "\nNodes above selection:\n--> " + str(parentLevel)) + return [returned[0], parentOccurrence] - selectionInput.isEnabled = False - selectionInput.isVisible = False - except: - if gm.ui: - gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}").error( - "Failed:\n{}".format(traceback.format_exc()) + parentLevel += 1 + treeParent = treeParent.assemblyContext + # gm.ui.messageBox("No jointed occurrence found.\nReturning selection.\n\n" + "Occurrence:\n--> " + occ.name + "\nJoint:\n--> NONE") + return [None, occ] # no jointed occurrence found, return what is selected + + +@logFailure(messageBox=True) +def notify(self, args: adsk.core.SelectionEventArgs): + """### Notify member is called when a selection event is triggered. + + Args: + args (SelectionEventArgs): A selection event argument + """ + # eventArgs = adsk.core.SelectionEventArgs.cast(args) + + self.selectedOcc = adsk.fusion.Occurrence.cast(args.selection.entity) + self.selectedJoint = args.selection.entity + + selectionInput = args.activeInput + + dropdownExportMode = INPUTS_ROOT.itemById("mode") + duplicateSelection = INPUTS_ROOT.itemById("duplicate_selection") + # indicator = INPUTS_ROOT.itemById("algorithmic_indicator") + + if self.selectedOcc: + self.cmd.setCursor("", 0, 0) + if dropdownExportMode.selectedItem.index == 1: + occurrenceList = gm.app.activeDocument.design.rootComponent.allOccurrencesByComponent( + self.selectedOcc.component ) + for occ in occurrenceList: + if occ not in GamepieceListGlobal: + addGamepieceToTable(occ) + else: + removeGamePieceFromTable(GamepieceListGlobal.index(occ)) + + selectionInput.isEnabled = False + selectionInput.isVisible = False + + elif self.selectedJoint: + self.cmd.setCursor("", 0, 0) + jointType = self.selectedJoint.jointMotion.jointType + if jointType == JointMotions.REVOLUTE.value or jointType == JointMotions.SLIDER.value: + if jointType == JointMotions.REVOLUTE.value and MySelectHandler.lastInputCmd.id == "wheel_select": + addWheelToTable(self.selectedJoint) + elif jointType == JointMotions.REVOLUTE.value and MySelectHandler.lastInputCmd.id == "wheel_remove": + if self.selectedJoint in WheelListGlobal: + removeWheelFromTable(WheelListGlobal.index(self.selectedJoint)) + else: + if self.selectedJoint not in JointListGlobal: + addJointToTable(self.selectedJoint) + else: + removeJointFromTable(self.selectedJoint) + + selectionInput.isEnabled = False + selectionInput.isVisible = False class MyPreSelectHandler(adsk.core.SelectionEventHandler): @@ -1479,57 +1410,51 @@ def __init__(self, cmd): super().__init__() self.cmd = cmd + @logFailure(messageBox=True) def notify(self, args): - try: - design = adsk.fusion.Design.cast(gm.app.activeProduct) - preSelectedOcc = adsk.fusion.Occurrence.cast(args.selection.entity) - preSelectedJoint = adsk.fusion.Joint.cast(args.selection.entity) - - onSelect = gm.handlers[3] # select handler - - if (not preSelectedOcc and not preSelectedJoint) or not design: - self.cmd.setCursor("", 0, 0) - return - - preSelected = preSelectedOcc if preSelectedOcc else preSelectedJoint - - dropdownExportMode = INPUTS_ROOT.itemById("mode") - if preSelected and design: - if dropdownExportMode.selectedItem.index == 0: # Dynamic - if preSelected.entityToken in onSelect.allWheelPreselections: - self.cmd.setCursor( - IconPaths.mouseIcons["remove"], - 0, - 0, - ) - else: - self.cmd.setCursor( - IconPaths.mouseIcons["add"], - 0, - 0, - ) + design = adsk.fusion.Design.cast(gm.app.activeProduct) + preSelectedOcc = adsk.fusion.Occurrence.cast(args.selection.entity) + preSelectedJoint = adsk.fusion.Joint.cast(args.selection.entity) - elif dropdownExportMode.selectedItem.index == 1: # Static - if preSelected.entityToken in onSelect.allGamepiecePreselections: - self.cmd.setCursor( - IconPaths.mouseIcons["remove"], - 0, - 0, - ) - else: - self.cmd.setCursor( - IconPaths.mouseIcons["add"], - 0, - 0, - ) - else: # Should literally be impossible? - Brandon - self.cmd.setCursor("", 0, 0) - except: - if gm.ui: - gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + onSelect = gm.handlers[3] # select handler + + if (not preSelectedOcc and not preSelectedJoint) or not design: + self.cmd.setCursor("", 0, 0) + return + + preSelected = preSelectedOcc if preSelectedOcc else preSelectedJoint + + dropdownExportMode = INPUTS_ROOT.itemById("mode") + if preSelected and design: + if dropdownExportMode.selectedItem.index == 0: # Dynamic + if preSelected.entityToken in onSelect.allWheelPreselections: + self.cmd.setCursor( + IconPaths.mouseIcons["remove"], + 0, + 0, + ) + else: + self.cmd.setCursor( + IconPaths.mouseIcons["add"], + 0, + 0, + ) + + elif dropdownExportMode.selectedItem.index == 1: # Static + if preSelected.entityToken in onSelect.allGamepiecePreselections: + self.cmd.setCursor( + IconPaths.mouseIcons["remove"], + 0, + 0, + ) + else: + self.cmd.setCursor( + IconPaths.mouseIcons["add"], + 0, + 0, + ) + else: # Should literally be impossible? - Brandon + self.cmd.setCursor("", 0, 0) class MyPreselectEndHandler(adsk.core.SelectionEventHandler): @@ -1542,22 +1467,14 @@ def __init__(self, cmd): super().__init__() self.cmd = cmd + @logFailure(messageBox=True) def notify(self, args): - try: - design = adsk.fusion.Design.cast(gm.app.activeProduct) - preSelectedOcc = adsk.fusion.Occurrence.cast(args.selection.entity) - preSelectedJoint = adsk.fusion.Joint.cast(args.selection.entity) - - if (preSelectedOcc or preSelectedJoint) and design: - self.cmd.setCursor( - "", 0, 0 - ) # if preselection ends (mouse off of design), reset the mouse icon to default - except: - if gm.ui: - gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + design = adsk.fusion.Design.cast(gm.app.activeProduct) + preSelectedOcc = adsk.fusion.Occurrence.cast(args.selection.entity) + preSelectedJoint = adsk.fusion.Joint.cast(args.selection.entity) + + if (preSelectedOcc or preSelectedJoint) and design: + self.cmd.setCursor("", 0, 0) # if preselection ends (mouse off of design), reset the mouse icon to default class ConfigureCommandInputChanged(adsk.core.InputChangedEventHandler): @@ -1569,25 +1486,21 @@ class ConfigureCommandInputChanged(adsk.core.InputChangedEventHandler): def __init__(self, cmd): super().__init__() - self.log = logging.getLogger(f"{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}") self.cmd = cmd self.allWeights = [None, None] # [lbs, kg] self.isLbs = True self.isLbs_f = True + @logFailure def reset(self): """### Process: - Reset the mouse icon to default - Clear active selections """ - try: - self.cmd.setCursor("", 0, 0) - gm.ui.activeSelections.clear() - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}.reset()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + self.cmd.setCursor("", 0, 0) + gm.ui.activeSelections.clear() + @logFailure def weight(self, isLbs=True): # maybe add a progress dialog?? """### Get the total design weight using the predetermined units. @@ -1597,338 +1510,327 @@ def weight(self, isLbs=True): # maybe add a progress dialog?? Returns: value (float): weight value in specified unit """ - try: - if gm.app.activeDocument.design: - massCalculation = FullMassCalculation() - totalMass = massCalculation.getTotalMass() + if gm.app.activeDocument.design: + massCalculation = FullMassCalculation() + totalMass = massCalculation.getTotalMass() - value = float + value = float - self.allWeights[0] = round(totalMass * 2.2046226218, 2) + self.allWeights[0] = round(totalMass * 2.2046226218, 2) - self.allWeights[1] = round(totalMass, 2) + self.allWeights[1] = round(totalMass, 2) - if isLbs: - value = self.allWeights[0] - else: - value = self.allWeights[1] + if isLbs: + value = self.allWeights[0] + else: + value = self.allWeights[1] - value = round(value, 2) # round weight to 2 decimals places - return value - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}.weight()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + value = round(value, 2) # round weight to 2 decimals places + return value + @logFailure(messageBox=True) def notify(self, args): - try: - eventArgs = adsk.core.InputChangedEventArgs.cast(args) - cmdInput = eventArgs.input - MySelectHandler.lastInputCmd = cmdInput - inputs = cmdInput.commandInputs - onSelect = gm.handlers[3] + eventArgs = adsk.core.InputChangedEventArgs.cast(args) + cmdInput = eventArgs.input + MySelectHandler.lastInputCmd = cmdInput + inputs = cmdInput.commandInputs + onSelect = gm.handlers[3] - frictionCoeff = INPUTS_ROOT.itemById("friction_coeff_override") + frictionCoeff = INPUTS_ROOT.itemById("friction_coeff_override") - wheelSelect = inputs.itemById("wheel_select") - jointSelect = inputs.itemById("joint_select") - gamepieceSelect = inputs.itemById("gamepiece_select") + wheelSelect = inputs.itemById("wheel_select") + jointSelect = inputs.itemById("joint_select") + gamepieceSelect = inputs.itemById("gamepiece_select") - wheelTableInput = wheelTable() - jointTableInput = jointTable() - gamepieceTableInput = gamepieceTable() - weightTableInput = inputs.itemById("weight_table") + wheelTableInput = wheelTable() + jointTableInput = jointTable() + gamepieceTableInput = gamepieceTable() + weightTableInput = inputs.itemById("weight_table") - weight_input = INPUTS_ROOT.itemById("weight_input") + weight_input = INPUTS_ROOT.itemById("weight_input") - wheelConfig = inputs.itemById("wheel_config") - jointConfig = inputs.itemById("joint_config") - gamepieceConfig = inputs.itemById("gamepiece_config") + wheelConfig = inputs.itemById("wheel_config") + jointConfig = inputs.itemById("joint_config") + gamepieceConfig = inputs.itemById("gamepiece_config") - addWheelInput = INPUTS_ROOT.itemById("wheel_add") - addJointInput = INPUTS_ROOT.itemById("joint_add") - addFieldInput = INPUTS_ROOT.itemById("field_add") + addWheelInput = INPUTS_ROOT.itemById("wheel_add") + addJointInput = INPUTS_ROOT.itemById("joint_add") + addFieldInput = INPUTS_ROOT.itemById("field_add") - indicator = INPUTS_ROOT.itemById("algorithmic_indicator") + indicator = INPUTS_ROOT.itemById("algorithmic_indicator") - # gm.ui.messageBox(cmdInput.id) # DEBUG statement, displays CommandInput user-defined id + # gm.ui.messageBox(cmdInput.id) # DEBUG statement, displays CommandInput user-defined id - position = int + position = int - if cmdInput.id == "mode": - modeDropdown = adsk.core.DropDownCommandInput.cast(cmdInput) + if cmdInput.id == "mode": + modeDropdown = adsk.core.DropDownCommandInput.cast(cmdInput) - if modeDropdown.selectedItem.index == 0: - if gamepieceConfig: - gm.ui.activeSelections.clear() - gm.app.activeDocument.design.rootComponent.opacity = 1 + if modeDropdown.selectedItem.index == 0: + if gamepieceConfig: + gm.ui.activeSelections.clear() + gm.app.activeDocument.design.rootComponent.opacity = 1 - gamepieceConfig.isVisible = False - weightTableInput.isVisible = True + gamepieceConfig.isVisible = False + weightTableInput.isVisible = True - addFieldInput.isEnabled = wheelConfig.isVisible = jointConfig.isVisible = True + addFieldInput.isEnabled = wheelConfig.isVisible = jointConfig.isVisible = True - elif modeDropdown.selectedItem.index == 1: - if gamepieceConfig: - gm.ui.activeSelections.clear() - gm.app.activeDocument.design.rootComponent.opacity = 1 + elif modeDropdown.selectedItem.index == 1: + if gamepieceConfig: + gm.ui.activeSelections.clear() + gm.app.activeDocument.design.rootComponent.opacity = 1 - addWheelInput.isEnabled = addJointInput.isEnabled = gamepieceConfig.isVisible = True + addWheelInput.isEnabled = addJointInput.isEnabled = gamepieceConfig.isVisible = True - jointConfig.isVisible = wheelConfig.isVisible = weightTableInput.isVisible = False + jointConfig.isVisible = wheelConfig.isVisible = weightTableInput.isVisible = False - elif cmdInput.id == "joint_config": - gm.app.activeDocument.design.rootComponent.opacity = 1 + elif cmdInput.id == "joint_config": + gm.app.activeDocument.design.rootComponent.opacity = 1 - elif cmdInput.id == "placeholder_w" or cmdInput.id == "name_w" or cmdInput.id == "signal_type_w": - self.reset() + elif cmdInput.id == "placeholder_w" or cmdInput.id == "name_w" or cmdInput.id == "signal_type_w": + self.reset() - wheelSelect.isEnabled = False - addWheelInput.isEnabled = True + wheelSelect.isEnabled = False + addWheelInput.isEnabled = True - cmdInput_str = cmdInput.id + cmdInput_str = cmdInput.id - if cmdInput_str == "placeholder_w": - position = wheelTableInput.getPosition(adsk.core.ImageCommandInput.cast(cmdInput))[1] - 1 - elif cmdInput_str == "name_w": - position = wheelTableInput.getPosition(adsk.core.TextBoxCommandInput.cast(cmdInput))[1] - 1 - elif cmdInput_str == "signal_type_w": - position = wheelTableInput.getPosition(adsk.core.DropDownCommandInput.cast(cmdInput))[1] - 1 + if cmdInput_str == "placeholder_w": + position = wheelTableInput.getPosition(adsk.core.ImageCommandInput.cast(cmdInput))[1] - 1 + elif cmdInput_str == "name_w": + position = wheelTableInput.getPosition(adsk.core.TextBoxCommandInput.cast(cmdInput))[1] - 1 + elif cmdInput_str == "signal_type_w": + position = wheelTableInput.getPosition(adsk.core.DropDownCommandInput.cast(cmdInput))[1] - 1 - gm.ui.activeSelections.add(WheelListGlobal[position]) + gm.ui.activeSelections.add(WheelListGlobal[position]) - elif ( - cmdInput.id == "placeholder" - or cmdInput.id == "name_j" - or cmdInput.id == "joint_parent" - or cmdInput.id == "signal_type" - ): - self.reset() - jointSelect.isEnabled = False - addJointInput.isEnabled = True + elif ( + cmdInput.id == "placeholder" + or cmdInput.id == "name_j" + or cmdInput.id == "joint_parent" + or cmdInput.id == "signal_type" + ): + self.reset() + jointSelect.isEnabled = False + addJointInput.isEnabled = True - elif cmdInput.id == "blank_gp" or cmdInput.id == "name_gp" or cmdInput.id == "weight_gp": - self.reset() + elif cmdInput.id == "blank_gp" or cmdInput.id == "name_gp" or cmdInput.id == "weight_gp": + self.reset() - gamepieceSelect.isEnabled = False - addFieldInput.isEnabled = True + gamepieceSelect.isEnabled = False + addFieldInput.isEnabled = True - cmdInput_str = cmdInput.id + cmdInput_str = cmdInput.id - if cmdInput_str == "name_gp": - position = gamepieceTableInput.getPosition(adsk.core.TextBoxCommandInput.cast(cmdInput))[1] - 1 - elif cmdInput_str == "weight_gp": - position = gamepieceTableInput.getPosition(adsk.core.ValueCommandInput.cast(cmdInput))[1] - 1 - elif cmdInput_str == "blank_gp": - position = gamepieceTableInput.getPosition(adsk.core.ImageCommandInput.cast(cmdInput))[1] - 1 - else: - position = gamepieceTableInput.getPosition(adsk.core.FloatSliderCommandInput.cast(cmdInput))[1] - 1 + if cmdInput_str == "name_gp": + position = gamepieceTableInput.getPosition(adsk.core.TextBoxCommandInput.cast(cmdInput))[1] - 1 + elif cmdInput_str == "weight_gp": + position = gamepieceTableInput.getPosition(adsk.core.ValueCommandInput.cast(cmdInput))[1] - 1 + elif cmdInput_str == "blank_gp": + position = gamepieceTableInput.getPosition(adsk.core.ImageCommandInput.cast(cmdInput))[1] - 1 + else: + position = gamepieceTableInput.getPosition(adsk.core.FloatSliderCommandInput.cast(cmdInput))[1] - 1 - gm.ui.activeSelections.add(GamepieceListGlobal[position]) + gm.ui.activeSelections.add(GamepieceListGlobal[position]) - elif cmdInput.id == "wheel_type_w": - self.reset() + elif cmdInput.id == "wheel_type_w": + self.reset() - wheelSelect.isEnabled = False - addWheelInput.isEnabled = True + wheelSelect.isEnabled = False + addWheelInput.isEnabled = True - cmdInput_str = cmdInput.id - position = wheelTableInput.getPosition(adsk.core.DropDownCommandInput.cast(cmdInput))[1] - 1 - wheelDropdown = adsk.core.DropDownCommandInput.cast(cmdInput) - - if wheelDropdown.selectedItem.index == 0: - getPosition = wheelTableInput.getPosition(adsk.core.DropDownCommandInput.cast(cmdInput)) - iconInput = wheelTableInput.getInputAtPosition(getPosition[1], 0) - iconInput.imageFile = IconPaths.wheelIcons["standard"] - iconInput.tooltip = "Standard wheel" - - elif wheelDropdown.selectedItem.index == 1: - getPosition = wheelTableInput.getPosition(adsk.core.DropDownCommandInput.cast(cmdInput)) - iconInput = wheelTableInput.getInputAtPosition(getPosition[1], 0) - iconInput.imageFile = IconPaths.wheelIcons["omni"] - iconInput.tooltip = "Omni wheel" - - elif wheelDropdown.selectedItem.index == 2: - getPosition = wheelTableInput.getPosition(adsk.core.DropDownCommandInput.cast(cmdInput)) - iconInput = wheelTableInput.getInputAtPosition(getPosition[1], 0) - iconInput.imageFile = IconPaths.wheelIcons["mecanum"] - iconInput.tooltip = "Mecanum wheel" - - gm.ui.activeSelections.add(WheelListGlobal[position]) - - elif cmdInput.id == "wheel_add": - self.reset() - - wheelSelect.isVisible = True - wheelSelect.isEnabled = True - wheelSelect.clearSelection() - addJointInput.isEnabled = True - addWheelInput.isEnabled = False - - elif cmdInput.id == "joint_add": - self.reset() - - addWheelInput.isEnabled = True - jointSelect.isVisible = True - jointSelect.isEnabled = True - jointSelect.clearSelection() - addJointInput.isEnabled = False - - elif cmdInput.id == "field_add": - self.reset() - - gamepieceSelect.isVisible = True - gamepieceSelect.isEnabled = True - gamepieceSelect.clearSelection() - addFieldInput.isEnabled = False - - elif cmdInput.id == "wheel_delete": - # Currently causes Internal Autodesk Error - # gm.ui.activeSelections.clear() - - addWheelInput.isEnabled = True - if wheelTableInput.selectedRow == -1 or wheelTableInput.selectedRow == 0: - wheelTableInput.selectedRow = wheelTableInput.rowCount - 1 - gm.ui.messageBox("Select a row to delete.") - else: - index = wheelTableInput.selectedRow - 1 - removeWheelFromTable(index) + cmdInput_str = cmdInput.id + position = wheelTableInput.getPosition(adsk.core.DropDownCommandInput.cast(cmdInput))[1] - 1 + wheelDropdown = adsk.core.DropDownCommandInput.cast(cmdInput) - elif cmdInput.id == "joint_delete": - gm.ui.activeSelections.clear() + if wheelDropdown.selectedItem.index == 0: + getPosition = wheelTableInput.getPosition(adsk.core.DropDownCommandInput.cast(cmdInput)) + iconInput = wheelTableInput.getInputAtPosition(getPosition[1], 0) + iconInput.imageFile = IconPaths.wheelIcons["standard"] + iconInput.tooltip = "Standard wheel" - addJointInput.isEnabled = True - addWheelInput.isEnabled = True + elif wheelDropdown.selectedItem.index == 1: + getPosition = wheelTableInput.getPosition(adsk.core.DropDownCommandInput.cast(cmdInput)) + iconInput = wheelTableInput.getInputAtPosition(getPosition[1], 0) + iconInput.imageFile = IconPaths.wheelIcons["omni"] + iconInput.tooltip = "Omni wheel" - if jointTableInput.selectedRow == -1 or jointTableInput.selectedRow == 0: - jointTableInput.selectedRow = jointTableInput.rowCount - 1 - gm.ui.messageBox("Select a row to delete.") - else: - joint = JointListGlobal[jointTableInput.selectedRow - 1] - removeJointFromTable(joint) + elif wheelDropdown.selectedItem.index == 2: + getPosition = wheelTableInput.getPosition(adsk.core.DropDownCommandInput.cast(cmdInput)) + iconInput = wheelTableInput.getInputAtPosition(getPosition[1], 0) + iconInput.imageFile = IconPaths.wheelIcons["mecanum"] + iconInput.tooltip = "Mecanum wheel" - elif cmdInput.id == "field_delete": - gm.ui.activeSelections.clear() + gm.ui.activeSelections.add(WheelListGlobal[position]) - addFieldInput.isEnabled = True + elif cmdInput.id == "wheel_add": + self.reset() - if gamepieceTableInput.selectedRow == -1 or gamepieceTableInput.selectedRow == 0: - gamepieceTableInput.selectedRow = gamepieceTableInput.rowCount - 1 - gm.ui.messageBox("Select a row to delete.") - else: - index = gamepieceTableInput.selectedRow - 1 - removeGamePieceFromTable(index) + wheelSelect.isVisible = True + wheelSelect.isEnabled = True + wheelSelect.clearSelection() + addJointInput.isEnabled = True + addWheelInput.isEnabled = False - elif cmdInput.id == "wheel_select": - addWheelInput.isEnabled = True + elif cmdInput.id == "joint_add": + self.reset() - elif cmdInput.id == "joint_select": - addJointInput.isEnabled = True + addWheelInput.isEnabled = True + jointSelect.isVisible = True + jointSelect.isEnabled = True + jointSelect.clearSelection() + addJointInput.isEnabled = False - elif cmdInput.id == "gamepiece_select": - addFieldInput.isEnabled = True + elif cmdInput.id == "field_add": + self.reset() - elif cmdInput.id == "friction_override": - boolValue = adsk.core.BoolValueCommandInput.cast(cmdInput) + gamepieceSelect.isVisible = True + gamepieceSelect.isEnabled = True + gamepieceSelect.clearSelection() + addFieldInput.isEnabled = False - if boolValue.value == True: - frictionCoeff.isVisible = True - else: - frictionCoeff.isVisible = False + elif cmdInput.id == "wheel_delete": + # Currently causes Internal Autodesk Error + # gm.ui.activeSelections.clear() - elif cmdInput.id == "weight_unit": - unitDropdown = adsk.core.DropDownCommandInput.cast(cmdInput) - weightInput = weightTableInput.getInputAtPosition(0, 2) - if unitDropdown.selectedItem.index == 0: - self.isLbs = True + addWheelInput.isEnabled = True + if wheelTableInput.selectedRow == -1 or wheelTableInput.selectedRow == 0: + wheelTableInput.selectedRow = wheelTableInput.rowCount - 1 + gm.ui.messageBox("Select a row to delete.") + else: + index = wheelTableInput.selectedRow - 1 + removeWheelFromTable(index) - weightInput.tooltipDescription = ( - """(in pounds)
This is the weight of the entire robot assembly.""" - ) - elif unitDropdown.selectedItem.index == 1: - self.isLbs = False + elif cmdInput.id == "joint_delete": + gm.ui.activeSelections.clear() - weightInput.tooltipDescription = ( - """(in kilograms)
This is the weight of the entire robot assembly.""" - ) + addJointInput.isEnabled = True + addWheelInput.isEnabled = True + + if jointTableInput.selectedRow == -1 or jointTableInput.selectedRow == 0: + jointTableInput.selectedRow = jointTableInput.rowCount - 1 + gm.ui.messageBox("Select a row to delete.") + else: + joint = JointListGlobal[jointTableInput.selectedRow - 1] + removeJointFromTable(joint) + + elif cmdInput.id == "field_delete": + gm.ui.activeSelections.clear() + + addFieldInput.isEnabled = True + + if gamepieceTableInput.selectedRow == -1 or gamepieceTableInput.selectedRow == 0: + gamepieceTableInput.selectedRow = gamepieceTableInput.rowCount - 1 + gm.ui.messageBox("Select a row to delete.") + else: + index = gamepieceTableInput.selectedRow - 1 + removeGamePieceFromTable(index) + + elif cmdInput.id == "wheel_select": + addWheelInput.isEnabled = True + + elif cmdInput.id == "joint_select": + addJointInput.isEnabled = True + + elif cmdInput.id == "gamepiece_select": + addFieldInput.isEnabled = True + + elif cmdInput.id == "friction_override": + boolValue = adsk.core.BoolValueCommandInput.cast(cmdInput) + + if boolValue.value == True: + frictionCoeff.isVisible = True + else: + frictionCoeff.isVisible = False - elif cmdInput.id == "weight_unit_f": - unitDropdown = adsk.core.DropDownCommandInput.cast(cmdInput) - if unitDropdown.selectedItem.index == 0: - self.isLbs_f = True + elif cmdInput.id == "weight_unit": + unitDropdown = adsk.core.DropDownCommandInput.cast(cmdInput) + weightInput = weightTableInput.getInputAtPosition(0, 2) + if unitDropdown.selectedItem.index == 0: + self.isLbs = True + weightInput.tooltipDescription = ( + """(in pounds)
This is the weight of the entire robot assembly.""" + ) + elif unitDropdown.selectedItem.index == 1: + self.isLbs = False + + weightInput.tooltipDescription = ( + """(in kilograms)
This is the weight of the entire robot assembly.""" + ) + + elif cmdInput.id == "weight_unit_f": + unitDropdown = adsk.core.DropDownCommandInput.cast(cmdInput) + if unitDropdown.selectedItem.index == 0: + self.isLbs_f = True + + for row in range(gamepieceTableInput.rowCount): + if row == 0: + continue + weightInput = gamepieceTableInput.getInputAtPosition(row, 2) + weightInput.tooltipDescription = "(in pounds)" + elif unitDropdown.selectedItem.index == 1: + self.isLbs_f = False + + for row in range(gamepieceTableInput.rowCount): + if row == 0: + continue + weightInput = gamepieceTableInput.getInputAtPosition(row, 2) + weightInput.tooltipDescription = "(in kilograms)" + + elif cmdInput.id == "auto_calc_weight": + button = adsk.core.BoolValueCommandInput.cast(cmdInput) + + if button.value == True: # CALCULATE button pressed + if self.allWeights.count(None) == 2: # if button is pressed for the first time + if self.isLbs: # if pounds unit selected + self.allWeights[0] = self.weight() + weight_input.value = self.allWeights[0] + else: # if kg unit selected + self.allWeights[1] = self.weight(False) + weight_input.value = self.allWeights[1] + else: # if a mass value has already been configured + if ( + weight_input.value != self.allWeights[0] + or weight_input.value != self.allWeights[1] + or not weight_input.isValidExpression + ): + if self.isLbs: + weight_input.value = self.allWeights[0] + else: + weight_input.value = self.allWeights[1] + + elif cmdInput.id == "auto_calc_weight_f": + button = adsk.core.BoolValueCommandInput.cast(cmdInput) + + if button.value == True: # CALCULATE button pressed + if self.isLbs_f: for row in range(gamepieceTableInput.rowCount): if row == 0: continue weightInput = gamepieceTableInput.getInputAtPosition(row, 2) - weightInput.tooltipDescription = "(in pounds)" - elif unitDropdown.selectedItem.index == 1: - self.isLbs_f = False + physical = GamepieceListGlobal[row - 1].component.getPhysicalProperties( + adsk.fusion.CalculationAccuracy.LowCalculationAccuracy + ) + value = round(physical.mass * 2.2046226218, 2) + weightInput.value = value + else: for row in range(gamepieceTableInput.rowCount): if row == 0: continue weightInput = gamepieceTableInput.getInputAtPosition(row, 2) - weightInput.tooltipDescription = "(in kilograms)" - - elif cmdInput.id == "auto_calc_weight": - button = adsk.core.BoolValueCommandInput.cast(cmdInput) - - if button.value == True: # CALCULATE button pressed - if self.allWeights.count(None) == 2: # if button is pressed for the first time - if self.isLbs: # if pounds unit selected - self.allWeights[0] = self.weight() - weight_input.value = self.allWeights[0] - else: # if kg unit selected - self.allWeights[1] = self.weight(False) - weight_input.value = self.allWeights[1] - else: # if a mass value has already been configured - if ( - weight_input.value != self.allWeights[0] - or weight_input.value != self.allWeights[1] - or not weight_input.isValidExpression - ): - if self.isLbs: - weight_input.value = self.allWeights[0] - else: - weight_input.value = self.allWeights[1] - - elif cmdInput.id == "auto_calc_weight_f": - button = adsk.core.BoolValueCommandInput.cast(cmdInput) - - if button.value == True: # CALCULATE button pressed - if self.isLbs_f: - for row in range(gamepieceTableInput.rowCount): - if row == 0: - continue - weightInput = gamepieceTableInput.getInputAtPosition(row, 2) - physical = GamepieceListGlobal[row - 1].component.getPhysicalProperties( - adsk.fusion.CalculationAccuracy.LowCalculationAccuracy - ) - value = round(physical.mass * 2.2046226218, 2) - weightInput.value = value - - else: - for row in range(gamepieceTableInput.rowCount): - if row == 0: - continue - weightInput = gamepieceTableInput.getInputAtPosition(row, 2) - physical = GamepieceListGlobal[row - 1].component.getPhysicalProperties( - adsk.fusion.CalculationAccuracy.LowCalculationAccuracy - ) - value = round(physical.mass, 2) - weightInput.value = value - elif cmdInput.id == "compress": - checkBox = adsk.core.BoolValueCommandInput.cast(cmdInput) - if checkBox.value: - global compress - compress = checkBox.value - except: - if gm.ui: - gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + physical = GamepieceListGlobal[row - 1].component.getPhysicalProperties( + adsk.fusion.CalculationAccuracy.LowCalculationAccuracy + ) + value = round(physical.mass, 2) + weightInput.value = value + elif cmdInput.id == "compress": + checkBox = adsk.core.BoolValueCommandInput.cast(cmdInput) + if checkBox.value: + global compress + compress = checkBox.value class MyCommandDestroyHandler(adsk.core.CommandEventHandler): @@ -1941,148 +1843,137 @@ class MyCommandDestroyHandler(adsk.core.CommandEventHandler): def __init__(self): super().__init__() + @logFailure(messageBox=True) def notify(self, args): - try: - onSelect = gm.handlers[3] + onSelect = gm.handlers[3] - WheelListGlobal.clear() - JointListGlobal.clear() - GamepieceListGlobal.clear() - onSelect.allWheelPreselections.clear() - onSelect.wheelJointList.clear() + WheelListGlobal.clear() + JointListGlobal.clear() + GamepieceListGlobal.clear() + onSelect.allWheelPreselections.clear() + onSelect.wheelJointList.clear() - for group in gm.app.activeDocument.design.rootComponent.customGraphicsGroups: - group.deleteMe() + for group in gm.app.activeDocument.design.rootComponent.customGraphicsGroups: + group.deleteMe() - # Currently causes Internal Autodesk Error - # gm.ui.activeSelections.clear() - gm.app.activeDocument.design.rootComponent.opacity = 1 - except: - if gm.ui: - gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.{self.__class__.__name__}").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + # Currently causes Internal Autodesk Error + # gm.ui.activeSelections.clear() + gm.app.activeDocument.design.rootComponent.opacity = 1 +@logFailure def addJointToTable(joint: adsk.fusion.Joint) -> None: """### Adds a Joint object to its global list and joint table. Args: joint (adsk.fusion.Joint): Joint object to be added """ - try: - JointListGlobal.append(joint) - jointTableInput = jointTable() - cmdInputs = adsk.core.CommandInputs.cast(jointTableInput.commandInputs) - - # joint type icons - if joint.jointMotion.jointType == adsk.fusion.JointTypes.RigidJointType: - icon = cmdInputs.addImageCommandInput("placeholder", "Rigid", IconPaths.jointIcons["rigid"]) - icon.tooltip = "Rigid joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: - icon = cmdInputs.addImageCommandInput("placeholder", "Revolute", IconPaths.jointIcons["revolute"]) - icon.tooltip = "Revolute joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: - icon = cmdInputs.addImageCommandInput("placeholder", "Slider", IconPaths.jointIcons["slider"]) - icon.tooltip = "Slider joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.PlanarJointType: - icon = cmdInputs.addImageCommandInput("placeholder", "Planar", IconPaths.jointIcons["planar"]) - icon.tooltip = "Planar joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.PinSlotJointType: - icon = cmdInputs.addImageCommandInput("placeholder", "Pin Slot", IconPaths.jointIcons["pin_slot"]) - icon.tooltip = "Pin slot joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.CylindricalJointType: - icon = cmdInputs.addImageCommandInput("placeholder", "Cylindrical", IconPaths.jointIcons["cylindrical"]) - icon.tooltip = "Cylindrical joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.BallJointType: - icon = cmdInputs.addImageCommandInput("placeholder", "Ball", IconPaths.jointIcons["ball"]) - icon.tooltip = "Ball joint" - - # joint name - name = cmdInputs.addTextBoxCommandInput("name_j", "Occurrence name", "", 1, True) - name.tooltip = joint.name - name.formattedText = "

{}

".format(joint.name) - - jointType = cmdInputs.addDropDownCommandInput( - "joint_parent", - "Joint Type", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, + JointListGlobal.append(joint) + jointTableInput = jointTable() + cmdInputs = adsk.core.CommandInputs.cast(jointTableInput.commandInputs) + + # joint type icons + if joint.jointMotion.jointType == adsk.fusion.JointTypes.RigidJointType: + icon = cmdInputs.addImageCommandInput("placeholder", "Rigid", IconPaths.jointIcons["rigid"]) + icon.tooltip = "Rigid joint" + + elif joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: + icon = cmdInputs.addImageCommandInput("placeholder", "Revolute", IconPaths.jointIcons["revolute"]) + icon.tooltip = "Revolute joint" + + elif joint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: + icon = cmdInputs.addImageCommandInput("placeholder", "Slider", IconPaths.jointIcons["slider"]) + icon.tooltip = "Slider joint" + + elif joint.jointMotion.jointType == adsk.fusion.JointTypes.PlanarJointType: + icon = cmdInputs.addImageCommandInput("placeholder", "Planar", IconPaths.jointIcons["planar"]) + icon.tooltip = "Planar joint" + + elif joint.jointMotion.jointType == adsk.fusion.JointTypes.PinSlotJointType: + icon = cmdInputs.addImageCommandInput("placeholder", "Pin Slot", IconPaths.jointIcons["pin_slot"]) + icon.tooltip = "Pin slot joint" + + elif joint.jointMotion.jointType == adsk.fusion.JointTypes.CylindricalJointType: + icon = cmdInputs.addImageCommandInput("placeholder", "Cylindrical", IconPaths.jointIcons["cylindrical"]) + icon.tooltip = "Cylindrical joint" + + elif joint.jointMotion.jointType == adsk.fusion.JointTypes.BallJointType: + icon = cmdInputs.addImageCommandInput("placeholder", "Ball", IconPaths.jointIcons["ball"]) + icon.tooltip = "Ball joint" + + # joint name + name = cmdInputs.addTextBoxCommandInput("name_j", "Occurrence name", "", 1, True) + name.tooltip = joint.name + name.formattedText = "

{}

".format(joint.name) + + jointType = cmdInputs.addDropDownCommandInput( + "joint_parent", + "Joint Type", + dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, + ) + jointType.isFullWidth = True + jointType.listItems.add("Root", True) + + # after each additional joint added, add joint to the dropdown of all preview rows/joints + for row in range(jointTableInput.rowCount): + if row != 0: + dropDown = jointTableInput.getInputAtPosition(row, 2) + dropDown.listItems.add(JointListGlobal[-1].name, False) + + # add all parent joint options to added joint dropdown + for j in range(len(JointListGlobal) - 1): + jointType.listItems.add(JointListGlobal[j].name, False) + + jointType.tooltip = "Possible parent joints" + jointType.tooltipDescription = "
The root component is usually the parent." + + signalType = cmdInputs.addDropDownCommandInput( + "signal_type", + "Signal Type", + dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, + ) + signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) + signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) + signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) + signalType.tooltip = "Signal type" + + row = jointTableInput.rowCount + + jointTableInput.addCommandInput(icon, row, 0) + jointTableInput.addCommandInput(name, row, 1) + jointTableInput.addCommandInput(jointType, row, 2) + jointTableInput.addCommandInput(signalType, row, 3) + + if joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: + jointSpeed = cmdInputs.addValueInput( + "joint_speed", + "Speed", + "deg", + adsk.core.ValueInput.createByReal(3.1415926), ) - jointType.isFullWidth = True - jointType.listItems.add("Root", True) - - # after each additional joint added, add joint to the dropdown of all preview rows/joints - for row in range(jointTableInput.rowCount): - if row != 0: - dropDown = jointTableInput.getInputAtPosition(row, 2) - dropDown.listItems.add(JointListGlobal[-1].name, False) - - # add all parent joint options to added joint dropdown - for j in range(len(JointListGlobal) - 1): - jointType.listItems.add(JointListGlobal[j].name, False) - - jointType.tooltip = "Possible parent joints" - jointType.tooltipDescription = "
The root component is usually the parent." - - signalType = cmdInputs.addDropDownCommandInput( - "signal_type", - "Signal Type", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, + jointSpeed.tooltip = "Degrees per second" + jointTableInput.addCommandInput(jointSpeed, row, 4) + + jointForce = cmdInputs.addValueInput("joint_force", "Force", "N", adsk.core.ValueInput.createByReal(5000)) + jointForce.tooltip = "Newton-Meters***" + jointTableInput.addCommandInput(jointForce, row, 5) + + if joint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: + jointSpeed = cmdInputs.addValueInput( + "joint_speed", + "Speed", + "m", + adsk.core.ValueInput.createByReal(100), ) - signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) - signalType.tooltip = "Signal type" - - row = jointTableInput.rowCount - - jointTableInput.addCommandInput(icon, row, 0) - jointTableInput.addCommandInput(name, row, 1) - jointTableInput.addCommandInput(jointType, row, 2) - jointTableInput.addCommandInput(signalType, row, 3) - - if joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: - jointSpeed = cmdInputs.addValueInput( - "joint_speed", - "Speed", - "deg", - adsk.core.ValueInput.createByReal(3.1415926), - ) - jointSpeed.tooltip = "Degrees per second" - jointTableInput.addCommandInput(jointSpeed, row, 4) - - jointForce = cmdInputs.addValueInput("joint_force", "Force", "N", adsk.core.ValueInput.createByReal(5000)) - jointForce.tooltip = "Newton-Meters***" - jointTableInput.addCommandInput(jointForce, row, 5) - - if joint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: - jointSpeed = cmdInputs.addValueInput( - "joint_speed", - "Speed", - "m", - adsk.core.ValueInput.createByReal(100), - ) - jointSpeed.tooltip = "Meters per second" - jointTableInput.addCommandInput(jointSpeed, row, 4) + jointSpeed.tooltip = "Meters per second" + jointTableInput.addCommandInput(jointSpeed, row, 4) - jointForce = cmdInputs.addValueInput("joint_force", "Force", "N", adsk.core.ValueInput.createByReal(5000)) - jointForce.tooltip = "Newtons" - jointTableInput.addCommandInput(jointForce, row, 5) - - except: - gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.addJointToTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + jointForce = cmdInputs.addValueInput("joint_force", "Force", "N", adsk.core.ValueInput.createByReal(5000)) + jointForce.tooltip = "Newtons" + jointTableInput.addCommandInput(jointForce, row, 5) +@logFailure def addWheelToTable(wheel: adsk.fusion.Joint) -> None: """### Adds a wheel occurrence to its global list and wheel table. @@ -2090,147 +1981,138 @@ def addWheelToTable(wheel: adsk.fusion.Joint) -> None: wheel (adsk.fusion.Occurrence): wheel Occurrence object to be added. """ try: - try: - onSelect = gm.handlers[3] - onSelect.allWheelPreselections.append(wheel.entityToken) - except IndexError: - # Not 100% sure what we need the select handler here for however it should not run when - # first populating the saved wheel configs. This will naturally throw a IndexError as - # we do this before the initialization of gm.handlers[] - pass - - wheelTableInput = wheelTable() - # def addPreselections(child_occurrences): - # for occ in child_occurrences: - # onSelect.allWheelPreselections.append(occ.entityToken) - - # if occ.childOccurrences: - # addPreselections(occ.childOccurrences) - - # if wheel.childOccurrences: - # addPreselections(wheel.childOccurrences) - # else: - - WheelListGlobal.append(wheel) - cmdInputs = adsk.core.CommandInputs.cast(wheelTableInput.commandInputs) - - icon = cmdInputs.addImageCommandInput("placeholder_w", "Placeholder", IconPaths.wheelIcons["standard"]) - - name = cmdInputs.addTextBoxCommandInput("name_w", "Joint name", wheel.name, 1, True) - name.tooltip = wheel.name - - wheelType = cmdInputs.addDropDownCommandInput( - "wheel_type_w", - "Wheel Type", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) - wheelType.listItems.add("Standard", True, "") - wheelType.listItems.add("Omni", False, "") - wheelType.tooltip = "Wheel type" - wheelType.tooltipDescription = "
Omni-directional wheels can be used just like regular drive wheels but they have the advantage of being able to roll freely perpendicular to the drive direction.
" - wheelType.toolClipFilename = OsHelper.getOSPath(".", "src", "Resources") + os.path.join( - "WheelIcons", "omni-wheel-preview.png" - ) - - signalType = cmdInputs.addDropDownCommandInput( - "signal_type_w", - "Signal Type", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) - signalType.isFullWidth = True - signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) - signalType.tooltip = "Signal type" - - row = wheelTableInput.rowCount - - wheelTableInput.addCommandInput(icon, row, 0) - wheelTableInput.addCommandInput(name, row, 1) - wheelTableInput.addCommandInput(wheelType, row, 2) - wheelTableInput.addCommandInput(signalType, row, 3) - - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.addWheelToTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) - + onSelect = gm.handlers[3] + onSelect.allWheelPreselections.append(wheel.entityToken) + except IndexError: + # Not 100% sure what we need the select handler here for however it should not run when + # first populating the saved wheel configs. This will naturally throw a IndexError as + # we do this before the initialization of gm.handlers[] + pass + wheelTableInput = wheelTable() + # def addPreselections(child_occurrences): + # for occ in child_occurrences: + # onSelect.allWheelPreselections.append(occ.entityToken) + + # if occ.childOccurrences: + # addPreselections(occ.childOccurrences) + + # if wheel.childOccurrences: + # addPreselections(wheel.childOccurrences) + # else: + + WheelListGlobal.append(wheel) + cmdInputs = adsk.core.CommandInputs.cast(wheelTableInput.commandInputs) + + icon = cmdInputs.addImageCommandInput("placeholder_w", "Placeholder", IconPaths.wheelIcons["standard"]) + + name = cmdInputs.addTextBoxCommandInput("name_w", "Joint name", wheel.name, 1, True) + name.tooltip = wheel.name + + wheelType = cmdInputs.addDropDownCommandInput( + "wheel_type_w", + "Wheel Type", + dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, + ) + wheelType.listItems.add("Standard", True, "") + wheelType.listItems.add("Omni", False, "") + wheelType.tooltip = "Wheel type" + wheelType.tooltipDescription = "
Omni-directional wheels can be used just like regular drive wheels but they have the advantage of being able to roll freely perpendicular to the drive direction.
" + wheelType.toolClipFilename = OsHelper.getOSPath(".", "src", "Resources") + os.path.join( + "WheelIcons", "omni-wheel-preview.png" + ) + + signalType = cmdInputs.addDropDownCommandInput( + "signal_type_w", + "Signal Type", + dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, + ) + signalType.isFullWidth = True + signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) + signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) + signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) + signalType.tooltip = "Signal type" + + row = wheelTableInput.rowCount + + wheelTableInput.addCommandInput(icon, row, 0) + wheelTableInput.addCommandInput(name, row, 1) + wheelTableInput.addCommandInput(wheelType, row, 2) + wheelTableInput.addCommandInput(signalType, row, 3) + + +@logFailure def addGamepieceToTable(gamepiece: adsk.fusion.Occurrence) -> None: """### Adds a gamepiece occurrence to its global list and gamepiece table. Args: gamepiece (adsk.fusion.Occurrence): Gamepiece occurrence to be added """ - try: - onSelect = gm.handlers[3] - gamepieceTableInput = gamepieceTable() + onSelect = gm.handlers[3] + gamepieceTableInput = gamepieceTable() - def addPreselections(child_occurrences): - for occ in child_occurrences: - onSelect.allGamepiecePreselections.append(occ.entityToken) + def addPreselections(child_occurrences): + for occ in child_occurrences: + onSelect.allGamepiecePreselections.append(occ.entityToken) - if occ.childOccurrences: - addPreselections(occ.childOccurrences) + if occ.childOccurrences: + addPreselections(occ.childOccurrences) - if gamepiece.childOccurrences: - addPreselections(gamepiece.childOccurrences) - else: - onSelect.allGamepiecePreselections.append(gamepiece.entityToken) + if gamepiece.childOccurrences: + addPreselections(gamepiece.childOccurrences) + else: + onSelect.allGamepiecePreselections.append(gamepiece.entityToken) - GamepieceListGlobal.append(gamepiece) - cmdInputs = adsk.core.CommandInputs.cast(gamepieceTableInput.commandInputs) - blankIcon = cmdInputs.addImageCommandInput("blank_gp", "Blank", IconPaths.gamepieceIcons["blank"]) + GamepieceListGlobal.append(gamepiece) + cmdInputs = adsk.core.CommandInputs.cast(gamepieceTableInput.commandInputs) + blankIcon = cmdInputs.addImageCommandInput("blank_gp", "Blank", IconPaths.gamepieceIcons["blank"]) - type = cmdInputs.addTextBoxCommandInput("name_gp", "Occurrence name", gamepiece.name, 1, True) + type = cmdInputs.addTextBoxCommandInput("name_gp", "Occurrence name", gamepiece.name, 1, True) - value = 0.0 - physical = gamepiece.component.getPhysicalProperties(adsk.fusion.CalculationAccuracy.LowCalculationAccuracy) - value = physical.mass + value = 0.0 + physical = gamepiece.component.getPhysicalProperties(adsk.fusion.CalculationAccuracy.LowCalculationAccuracy) + value = physical.mass - # check if dropdown unit is kg or lbs. bool value taken from ConfigureCommandInputChanged - massUnitInString = "" - onInputChanged = gm.handlers[1] - if onInputChanged.isLbs_f: - value = round(value * 2.2046226218, 2) # lbs - massUnitInString = "(in pounds)" - else: - value = round(value, 2) # kg - massUnitInString = "(in kilograms)" + # check if dropdown unit is kg or lbs. bool value taken from ConfigureCommandInputChanged + massUnitInString = "" + onInputChanged = gm.handlers[1] + if onInputChanged.isLbs_f: + value = round(value * 2.2046226218, 2) # lbs + massUnitInString = "(in pounds)" + else: + value = round(value, 2) # kg + massUnitInString = "(in kilograms)" - weight = cmdInputs.addValueInput( - "weight_gp", - "Weight Input", - "", - adsk.core.ValueInput.createByString(str(value)), - ) + weight = cmdInputs.addValueInput( + "weight_gp", + "Weight Input", + "", + adsk.core.ValueInput.createByString(str(value)), + ) - valueList = [1] - for i in range(20): - valueList.append(i / 20) + valueList = [1] + for i in range(20): + valueList.append(i / 20) - friction_coeff = cmdInputs.addFloatSliderListCommandInput("friction_coeff", "", "", valueList) - friction_coeff.valueOne = 0.5 + friction_coeff = cmdInputs.addFloatSliderListCommandInput("friction_coeff", "", "", valueList) + friction_coeff.valueOne = 0.5 - type.tooltip = gamepiece.name + type.tooltip = gamepiece.name - weight.tooltip = "Weight of field element" - weight.tooltipDescription = massUnitInString + weight.tooltip = "Weight of field element" + weight.tooltipDescription = massUnitInString - friction_coeff.tooltip = "Friction coefficient of field element" - friction_coeff.tooltipDescription = "Friction coefficients range from 0 (ice) to 1 (rubber)." - row = gamepieceTableInput.rowCount + friction_coeff.tooltip = "Friction coefficient of field element" + friction_coeff.tooltipDescription = "Friction coefficients range from 0 (ice) to 1 (rubber)." + row = gamepieceTableInput.rowCount - gamepieceTableInput.addCommandInput(blankIcon, row, 0) - gamepieceTableInput.addCommandInput(type, row, 1) - gamepieceTableInput.addCommandInput(weight, row, 2) - gamepieceTableInput.addCommandInput(friction_coeff, row, 3) - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.addGamepieceToTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + gamepieceTableInput.addCommandInput(blankIcon, row, 0) + gamepieceTableInput.addCommandInput(type, row, 1) + gamepieceTableInput.addCommandInput(weight, row, 2) + gamepieceTableInput.addCommandInput(friction_coeff, row, 3) +@logFailure def removeWheelFromTable(index: int) -> None: """### Removes a wheel joint from its global list and wheel table. @@ -2260,50 +2142,43 @@ def removeWheelFromTable(index: int) -> None: # updateJointTable(wheel) except IndexError: pass - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.removeWheelFromTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) +@logFailure def removeJointFromTable(joint: adsk.fusion.Joint) -> None: """### Removes a joint occurrence from its global list and joint table. Args: joint (adsk.fusion.Joint): Joint object to be removed """ - try: - index = JointListGlobal.index(joint) - jointTableInput = jointTable() - JointListGlobal.remove(joint) + index = JointListGlobal.index(joint) + jointTableInput = jointTable() + JointListGlobal.remove(joint) - jointTableInput.deleteRow(index + 1) + jointTableInput.deleteRow(index + 1) - for row in range(jointTableInput.rowCount): - if row == 0: - continue + for row in range(jointTableInput.rowCount): + if row == 0: + continue - dropDown = jointTableInput.getInputAtPosition(row, 2) - listItems = dropDown.listItems + dropDown = jointTableInput.getInputAtPosition(row, 2) + listItems = dropDown.listItems - if row > index: - if listItems.item(index + 1).isSelected: - listItems.item(index).isSelected = True - listItems.item(index + 1).deleteMe() - else: - listItems.item(index + 1).deleteMe() + if row > index: + if listItems.item(index + 1).isSelected: + listItems.item(index).isSelected = True + listItems.item(index + 1).deleteMe() else: - if listItems.item(index).isSelected: - listItems.item(index - 1).isSelected = True - listItems.item(index).deleteMe() - else: - listItems.item(index).deleteMe() - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.removeJointFromTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) + listItems.item(index + 1).deleteMe() + else: + if listItems.item(index).isSelected: + listItems.item(index - 1).isSelected = True + listItems.item(index).deleteMe() + else: + listItems.item(index).deleteMe() +@logFailure def removeGamePieceFromTable(index: int) -> None: """### Removes a gamepiece occurrence from its global list and gamepiece table. @@ -2331,7 +2206,3 @@ def removePreselections(child_occurrences): gamepieceTableInput.deleteRow(index + 1) except IndexError: pass - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.removeGamePieceFromTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) diff --git a/exporter/SynthesisFusionAddin/src/UI/CustomGraphics.py b/exporter/SynthesisFusionAddin/src/UI/CustomGraphics.py index 17b63c8222..52a49fa5d4 100644 --- a/exporter/SynthesisFusionAddin/src/UI/CustomGraphics.py +++ b/exporter/SynthesisFusionAddin/src/UI/CustomGraphics.py @@ -5,76 +5,75 @@ import adsk.fusion from ..general_imports import * +from ..Logging import logFailure +@logFailure def createTextGraphics(wheel: adsk.fusion.Occurrence, _wheels) -> None: - try: - design = gm.app.activeDocument.design + design = gm.app.activeDocument.design - boundingBox = wheel.boundingBox # occurrence bounding box + boundingBox = wheel.boundingBox # occurrence bounding box - min = boundingBox.minPoint.asArray() # [x, y, z] min coords - max = boundingBox.maxPoint.asArray() # [x, y, z] max coords + min = boundingBox.minPoint.asArray() # [x, y, z] min coords + max = boundingBox.maxPoint.asArray() # [x, y, z] max coords - if design: - graphics = gm.app.activeDocument.design.rootComponent.customGraphicsGroups.add() - matrix = adsk.core.Matrix3D.create() - matrix.translation = adsk.core.Vector3D.create(min[0], min[1] - 5, min[2]) + if design: + graphics = gm.app.activeDocument.design.rootComponent.customGraphicsGroups.add() + matrix = adsk.core.Matrix3D.create() + matrix.translation = adsk.core.Vector3D.create(min[0], min[1] - 5, min[2]) - billBoard = adsk.fusion.CustomGraphicsBillBoard.create(adsk.core.Point3D.create(0, 0, 0)) - billBoard.billBoardStyle = adsk.fusion.CustomGraphicsBillBoardStyles.ScreenBillBoardStyle + billBoard = adsk.fusion.CustomGraphicsBillBoard.create(adsk.core.Point3D.create(0, 0, 0)) + billBoard.billBoardStyle = adsk.fusion.CustomGraphicsBillBoardStyles.ScreenBillBoardStyle - text = str(_wheels.index(wheel) + 1) - graphicsText = graphics.addText(text, "Arial Black", 6, matrix) - graphicsText.billBoarding = billBoard # make the text follow the camera - graphicsText.isSelectable = False # make it non-selectable - graphicsText.cullMode = adsk.fusion.CustomGraphicsCullModes.CustomGraphicsCullBack - graphicsText.color = adsk.fusion.CustomGraphicsShowThroughColorEffect.create( - adsk.core.Color.create(230, 146, 18, 255), 1 - ) # orange/synthesis theme - graphicsText.depthPriority = 0 + text = str(_wheels.index(wheel) + 1) + graphicsText = graphics.addText(text, "Arial Black", 6, matrix) + graphicsText.billBoarding = billBoard # make the text follow the camera + graphicsText.isSelectable = False # make it non-selectable + graphicsText.cullMode = adsk.fusion.CustomGraphicsCullModes.CustomGraphicsCullBack + graphicsText.color = adsk.fusion.CustomGraphicsShowThroughColorEffect.create( + adsk.core.Color.create(230, 146, 18, 255), 1 + ) # orange/synthesis theme + graphicsText.depthPriority = 0 - """ - create a bounding box around a wheel. - """ - allIndices = [ - min[0], - min[1], - max[2], - min[0], - min[1], - min[2], - max[0], - min[1], - min[2], - max[0], - min[1], - max[2], - min[0], - min[1], - max[2], - ] + """ + create a bounding box around a wheel. + """ + allIndices = [ + min[0], + min[1], + max[2], + min[0], + min[1], + min[2], + max[0], + min[1], + min[2], + max[0], + min[1], + max[2], + min[0], + min[1], + max[2], + ] - indexPairs = [] + indexPairs = [] - for index in range(0, len(allIndices), 3): - if index > len(allIndices) - 5: - continue - for i in allIndices[index : index + 6]: - indexPairs.append(i) + for index in range(0, len(allIndices), 3): + if index > len(allIndices) - 5: + continue + for i in allIndices[index : index + 6]: + indexPairs.append(i) - coords = adsk.fusion.CustomGraphicsCoordinates.create(indexPairs) - line = graphics.addLines( - coords, - [], - False, - ) - line.color = adsk.fusion.CustomGraphicsShowThroughColorEffect.create( - adsk.core.Color.create(0, 255, 0, 255), 0.2 - ) # bright-green color - line.weight = 1 - line.isScreenSpaceLineStyle = False - line.isSelectable = False - line.depthPriority = 1 - except: - logging.getLogger("{INTERNAL_ID}.UI.CreateTextGraphics").error("Failed:\n{}".format(traceback.format_exc())) + coords = adsk.fusion.CustomGraphicsCoordinates.create(indexPairs) + line = graphics.addLines( + coords, + [], + False, + ) + line.color = adsk.fusion.CustomGraphicsShowThroughColorEffect.create( + adsk.core.Color.create(0, 255, 0, 255), 0.2 + ) # bright-green color + line.weight = 1 + line.isScreenSpaceLineStyle = False + line.isSelectable = False + line.depthPriority = 1 diff --git a/exporter/SynthesisFusionAddin/src/UI/HUI.py b/exporter/SynthesisFusionAddin/src/UI/HUI.py index b17ba6ea96..3b52de9999 100644 --- a/exporter/SynthesisFusionAddin/src/UI/HUI.py +++ b/exporter/SynthesisFusionAddin/src/UI/HUI.py @@ -1,4 +1,5 @@ from ..general_imports import * +from ..Logging import logFailure from . import Handlers, OsHelper @@ -36,8 +37,6 @@ def __init__( ValueError: If the unique ID is used and not be me ValueError: If the unique ID is used and it is by me """ - self.logger = logging.getLogger(f"{INTERNAL_ID}.HUI.{self.__class__.__name__}") - self.uid = name.replace(" ", "") + f"_p2_{INTERNAL_ID}" self.name = name @@ -72,8 +71,6 @@ def __init__( self.palette.incomingFromHTML.add(onHTML) self.handlers.append(onHTML) - self.logger.info(f"Created Palette {self.uid}") - self.palette.isVisible = True def deleteMe(self) -> None: @@ -83,7 +80,6 @@ def deleteMe(self) -> None: # I hope you like problems because this is nothing but problems # self.events.clear() # self.handlers.clear() - self.logger.debug(f"Deleted Palette {self.uid}") palette.deleteMe() @@ -91,6 +87,7 @@ class HButton: handlers = [] """ Keeps all handler classes alive which is essential apparently. - used in command events """ + @logFailure def __init__( self, name: str, @@ -112,7 +109,6 @@ def __init__( Raises: **ValueError**: if *location* does not exist in the current context """ - self.logger = logging.getLogger(f"{INTERNAL_ID}.HUI.{self.__class__.__name__}") self.uid = name.replace(" ", "") + f"_{INTERNAL_ID}" if self.uid in gm.uniqueIds: @@ -127,8 +123,6 @@ def __init__( cmdDef = gm.ui.commandDefinitions.itemById(self.uid) if cmdDef: - # gm.ui.messageBox("Looks like you have experienced a crash we will do cleanup.") - self.logger.debug("Looks like there was a crash, doing cleanup in button id") self.scrub() # needs to updated with new OString data @@ -154,16 +148,12 @@ def __init__( self.handlers.append(ccEventHandler) panel = gm.ui.allToolbarPanels.itemById(f"{location}") - - if panel: - self.buttonControl = panel.controls.addCommand(self.button) - self.logger.info(f"Created Button {self.uid} in Panel {location}") - else: - self.logger.error(f"Cannot Create Button {self.uid} in Panel {location}") + self.buttonControl = panel.controls.addCommand(self.button) self.promote(True) """ Promote determines whether or not buttons are displayed on toolbar """ + @logFailure def promote(self, flag: bool) -> None: """## Adds button to toolbar @@ -175,11 +165,8 @@ def promote(self, flag: bool) -> None: Raises: **ValueError**: Given type of not bool """ - if self.buttonControl is not None: - self.buttonControl.isPromotedByDefault = flag - self.buttonControl.isPromoted = flag - else: - raise RuntimeError("ButtonControl was not defined for {}".format(self.uid)) + self.buttonControl.isPromotedByDefault = flag + self.buttonControl.isPromoted = flag def deleteMe(self): """## Custom deleteMe method to easily deconstruct button data. @@ -191,12 +178,10 @@ def deleteMe(self): """ cmdDef = gm.ui.commandDefinitions.itemById(self.uid) if cmdDef: - self.logger.debug(f"Removing Button {self.uid}") cmdDef.deleteMe() ctrl = gm.ui.allToolbarPanels.itemById(self.location).controls.itemById(self.uid) if ctrl: - self.logger.debug(f"Removing Button Control {self.location}:{self.uid}") ctrl.deleteMe() def scrub(self): diff --git a/exporter/SynthesisFusionAddin/src/UI/Helper.py b/exporter/SynthesisFusionAddin/src/UI/Helper.py index 8f88240247..7c8e3a5930 100644 --- a/exporter/SynthesisFusionAddin/src/UI/Helper.py +++ b/exporter/SynthesisFusionAddin/src/UI/Helper.py @@ -5,14 +5,6 @@ from . import HUI, Events -def check_solid_open() -> bool: - """### Checks to see if the current design open is Fusion Solid - - Supplied as callback - WARN - THIS NO LONGER FUNCTIONS - """ - return True - - def getDocName() -> str or None: """### Gets the active Document Name - If it can't find one then it will return None diff --git a/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py b/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py index 5c68d2c79a..30c9f078e6 100644 --- a/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py +++ b/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py @@ -4,6 +4,8 @@ import adsk.core import adsk.fusion +from ..Logging import logFailure + # Ripped all the boiler plate from the example code: https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-c90ce6a2-c282-11e6-a365-3417ebc87622 # global mapping list of event handlers to keep them referenced for the duration of the command @@ -14,213 +16,197 @@ occurrencesOfComponents = {} +@logFailure(messageBox=True) def setupMarkingMenu(ui: adsk.core.UserInterface): handlers.clear() - try: - - def setLinearMarkingMenu(args): - try: - menuArgs = adsk.core.MarkingMenuEventArgs.cast(args) - - linearMenu = menuArgs.linearMarkingMenu - linearMenu.controls.addSeparator("LinearSeparator") - - synthDropDown = linearMenu.controls.addDropDown("Synthesis", "", "synthesis") - - cmdSelectDisabled = ui.commandDefinitions.itemById("SelectDisabled") - synthDropDown.controls.addCommand(cmdSelectDisabled) - - synthDropDown.controls.addSeparator() - - cmdEnableAll = ui.commandDefinitions.itemById("EnableAllCollision") - synthDropDown.controls.addCommand(cmdEnableAll) - synthDropDown.controls.addSeparator() - - if args.selectedEntities: - sel0 = args.selectedEntities[0] - occ = adsk.fusion.Occurrence.cast(sel0) - - if occ: - if occ.attributes.itemByName("synthesis", "collision_off") == None: - cmdDisableCollision = ui.commandDefinitions.itemById("DisableCollision") - synthDropDown.controls.addCommand(cmdDisableCollision) - else: - cmdEnableCollision = ui.commandDefinitions.itemById("EnableCollision") - synthDropDown.controls.addCommand(cmdEnableCollision) - except: - if ui: - ui.messageBox("setting linear menu failed: {}").format(traceback.format_exc()) - - def setCollisionAttribute(occ: adsk.fusion.Occurrence, isEnabled: bool = True): - attr = occ.attributes.itemByName("synthesis", "collision_off") - if attr == None and not isEnabled: - occ.attributes.add("synthesis", "collision_off", "true") - elif attr != None and isEnabled: - attr.deleteMe() - - def applyToSelfAndAllChildren(occ: adsk.fusion.Occurrence, modFunc): - modFunc(occ) - childLists = [] - childLists.append(occ.childOccurrences) - counter = 1 - while len(childLists) > 0: - childList = childLists.pop(0) - for o in childList: - counter += 1 - modFunc(o) - if o.childOccurrences.count > 0: - childLists.append(o.childOccurrences) - - class MyCommandCreatedEventHandler(adsk.core.CommandCreatedEventHandler): - def __init__(self): - super().__init__() - - def notify(self, args): - try: - command = args.command - onCommandExcute = MyCommandExecuteHandler() - handlers.append(onCommandExcute) - command.execute.add(onCommandExcute) - except: - ui.messageBox("command created failed: {}").format(traceback.format_exc()) - - class MyCommandExecuteHandler(adsk.core.CommandEventHandler): - def __init__(self): - super().__init__() - - def notify(self, args): - try: - command = args.firingEvent.sender - cmdDef = command.parentCommandDefinition - if cmdDef: - if cmdDef.id == "EnableCollision": - # ui.messageBox('Enable') - if entities: - func = lambda occ: setCollisionAttribute(occ, True) - for e in entities: - occ = adsk.fusion.Occurrence.cast(e) - if occ: - applyToSelfAndAllChildren(occ, func) - elif cmdDef.id == "DisableCollision": - # ui.messageBox('Disable') - if entities: - func = lambda occ: setCollisionAttribute(occ, False) - for e in entities: - occ = adsk.fusion.Occurrence.cast(e) - if occ: - applyToSelfAndAllChildren(occ, func) - elif cmdDef.id == "SelectDisabled": - app = adsk.core.Application.get() - product = app.activeProduct - design = adsk.fusion.Design.cast(product) - ui.activeSelections.clear() - if design: - attrs = design.findAttributes("synthesis", "collision_off") - for attr in attrs: - for b in adsk.fusion.Occurrence.cast(attr.parent).bRepBodies: - ui.activeSelections.add(b) - elif cmdDef.id == "EnableAllCollision": - app = adsk.core.Application.get() - product = app.activeProduct - design = adsk.fusion.Design.cast(product) - if design: - for attr in design.findAttributes("synthesis", "collision_off"): - attr.deleteMe() - else: - ui.messageBox("command {} triggered.".format(cmdDef.id)) - else: - ui.messageBox("No CommandDefinition") - except: - ui.messageBox("command executed failed: {}").format(traceback.format_exc()) - logging.getLogger(f"{INTERNAL_ID}").error("Failed:\n{}".format(traceback.format_exc())) - - class MyMarkingMenuHandler(adsk.core.MarkingMenuEventHandler): - def __init__(self): - super().__init__() - - def notify(self, args): - try: - setLinearMarkingMenu(args) - - global occurrencesOfComponents - - # selected entities - global entities - entities.clear() - entities = args.selectedEntities - except: - if ui: - ui.messageBox("Marking Menu Displaying event failed: {}".format(traceback.format_exc())) - - # Add customized handler for marking menu displaying - onMarkingMenuDisplaying = MyMarkingMenuHandler() - handlers.append(onMarkingMenuDisplaying) - ui.markingMenuDisplaying.add(onMarkingMenuDisplaying) - - # Add customized handler for commands creating - onCommandCreated = MyCommandCreatedEventHandler() - handlers.append(onCommandCreated) - - cmdDisableCollision = ui.commandDefinitions.itemById("DisableCollision") - if not cmdDisableCollision: - cmdDisableCollision = ui.commandDefinitions.addButtonDefinition( - "DisableCollision", - "Disable Collisions", - "Disable collisions with this occurrence inside Synthesis", - ) - cmdDisableCollision.commandCreated.add(onCommandCreated) - cmdDefs.append(cmdDisableCollision) - cmdEnableCollision = ui.commandDefinitions.itemById("EnableCollision") - if not cmdEnableCollision: - cmdEnableCollision = ui.commandDefinitions.addButtonDefinition( - "EnableCollision", - "Enable Collisions", - "Enable collisions with this occurrence inside Synthesis", - ) - cmdEnableCollision.commandCreated.add(onCommandCreated) - cmdDefs.append(cmdEnableCollision) - cmdEnableAllCollision = ui.commandDefinitions.itemById("EnableAllCollision") - if not cmdEnableAllCollision: - cmdEnableAllCollision = ui.commandDefinitions.addButtonDefinition( - "EnableAllCollision", - "Enable All Collision", - "Enable collisions for all occurrences in design", - ) - cmdEnableAllCollision.commandCreated.add(onCommandCreated) - cmdDefs.append(cmdEnableAllCollision) - cmdSelectDisabled = ui.commandDefinitions.itemById("SelectDisabled") - if not cmdSelectDisabled: - cmdSelectDisabled = ui.commandDefinitions.addButtonDefinition( - "SelectDisabled", - "Selected Collision Disabled Occurrences", - "Select all occurrences labeled for collision disabled", - ) - cmdSelectDisabled.commandCreated.add(onCommandCreated) - cmdDefs.append(cmdSelectDisabled) - - cmdDeleteComponent = ui.commandDefinitions.itemById("DeleteComponent") - if not cmdDeleteComponent: - cmdDeleteComponent = ui.commandDefinitions.addButtonDefinition( - "DeleteComponent", - "Delete All Occurrences", - "Delete all occurrences with the same component", - ) - cmdDeleteComponent.commandCreated.add(onCommandCreated) - cmdDefs.append(cmdDeleteComponent) - - except: - ui.messageBox("Failed:\n{}".format(traceback.format_exc())) + @logFailure(messageBox=True) + def setLinearMarkingMenu(args): + menuArgs = adsk.core.MarkingMenuEventArgs.cast(args) + linearMenu = menuArgs.linearMarkingMenu + linearMenu.controls.addSeparator("LinearSeparator") -def stopMarkingMenu(ui: adsk.core.UserInterface): - try: - for obj in cmdDefs: - if obj.isValid: - obj.deleteMe() + synthDropDown = linearMenu.controls.addDropDown("Synthesis", "", "synthesis") + + cmdSelectDisabled = ui.commandDefinitions.itemById("SelectDisabled") + synthDropDown.controls.addCommand(cmdSelectDisabled) + + synthDropDown.controls.addSeparator() + + cmdEnableAll = ui.commandDefinitions.itemById("EnableAllCollision") + synthDropDown.controls.addCommand(cmdEnableAll) + synthDropDown.controls.addSeparator() + + if args.selectedEntities: + sel0 = args.selectedEntities[0] + occ = adsk.fusion.Occurrence.cast(sel0) + + if occ: + if occ.attributes.itemByName("synthesis", "collision_off") == None: + cmdDisableCollision = ui.commandDefinitions.itemById("DisableCollision") + synthDropDown.controls.addCommand(cmdDisableCollision) + else: + cmdEnableCollision = ui.commandDefinitions.itemById("EnableCollision") + synthDropDown.controls.addCommand(cmdEnableCollision) + + def setCollisionAttribute(occ: adsk.fusion.Occurrence, isEnabled: bool = True): + attr = occ.attributes.itemByName("synthesis", "collision_off") + if attr == None and not isEnabled: + occ.attributes.add("synthesis", "collision_off", "true") + elif attr != None and isEnabled: + attr.deleteMe() + + def applyToSelfAndAllChildren(occ: adsk.fusion.Occurrence, modFunc): + modFunc(occ) + childLists = [] + childLists.append(occ.childOccurrences) + counter = 1 + while len(childLists) > 0: + childList = childLists.pop(0) + for o in childList: + counter += 1 + modFunc(o) + if o.childOccurrences.count > 0: + childLists.append(o.childOccurrences) + + class MyCommandCreatedEventHandler(adsk.core.CommandCreatedEventHandler): + def __init__(self): + super().__init__() + + @logFailure(messageBox=True) + def notify(self, args): + command = args.command + onCommandExcute = MyCommandExecuteHandler() + handlers.append(onCommandExcute) + command.execute.add(onCommandExcute) + + class MyCommandExecuteHandler(adsk.core.CommandEventHandler): + def __init__(self): + super().__init__() + + @logFailure(messageBox=True) + def notify(self, args): + command = args.firingEvent.sender + cmdDef = command.parentCommandDefinition + if cmdDef: + if cmdDef.id == "EnableCollision": + # ui.messageBox('Enable') + if entities: + func = lambda occ: setCollisionAttribute(occ, True) + for e in entities: + occ = adsk.fusion.Occurrence.cast(e) + if occ: + applyToSelfAndAllChildren(occ, func) + elif cmdDef.id == "DisableCollision": + # ui.messageBox('Disable') + if entities: + func = lambda occ: setCollisionAttribute(occ, False) + for e in entities: + occ = adsk.fusion.Occurrence.cast(e) + if occ: + applyToSelfAndAllChildren(occ, func) + elif cmdDef.id == "SelectDisabled": + app = adsk.core.Application.get() + product = app.activeProduct + design = adsk.fusion.Design.cast(product) + ui.activeSelections.clear() + if design: + attrs = design.findAttributes("synthesis", "collision_off") + for attr in attrs: + for b in adsk.fusion.Occurrence.cast(attr.parent).bRepBodies: + ui.activeSelections.add(b) + elif cmdDef.id == "EnableAllCollision": + app = adsk.core.Application.get() + product = app.activeProduct + design = adsk.fusion.Design.cast(product) + if design: + for attr in design.findAttributes("synthesis", "collision_off"): + attr.deleteMe() + else: + ui.messageBox("command {} triggered.".format(cmdDef.id)) else: - ui.messageBox(str(obj) + " is not a valid object") + ui.messageBox("No CommandDefinition") + + class MyMarkingMenuHandler(adsk.core.MarkingMenuEventHandler): + def __init__(self): + super().__init__() + + @logFailure(messageBox=True) + def notify(self, args): + setLinearMarkingMenu(args) + + global occurrencesOfComponents + + # selected entities + global entities + entities.clear() + entities = args.selectedEntities + + # Add customized handler for marking menu displaying + onMarkingMenuDisplaying = MyMarkingMenuHandler() + handlers.append(onMarkingMenuDisplaying) + ui.markingMenuDisplaying.add(onMarkingMenuDisplaying) + + # Add customized handler for commands creating + onCommandCreated = MyCommandCreatedEventHandler() + handlers.append(onCommandCreated) + + cmdDisableCollision = ui.commandDefinitions.itemById("DisableCollision") + if not cmdDisableCollision: + cmdDisableCollision = ui.commandDefinitions.addButtonDefinition( + "DisableCollision", + "Disable Collisions", + "Disable collisions with this occurrence inside Synthesis", + ) + cmdDisableCollision.commandCreated.add(onCommandCreated) + cmdDefs.append(cmdDisableCollision) + cmdEnableCollision = ui.commandDefinitions.itemById("EnableCollision") + if not cmdEnableCollision: + cmdEnableCollision = ui.commandDefinitions.addButtonDefinition( + "EnableCollision", + "Enable Collisions", + "Enable collisions with this occurrence inside Synthesis", + ) + cmdEnableCollision.commandCreated.add(onCommandCreated) + cmdDefs.append(cmdEnableCollision) + cmdEnableAllCollision = ui.commandDefinitions.itemById("EnableAllCollision") + if not cmdEnableAllCollision: + cmdEnableAllCollision = ui.commandDefinitions.addButtonDefinition( + "EnableAllCollision", + "Enable All Collision", + "Enable collisions for all occurrences in design", + ) + cmdEnableAllCollision.commandCreated.add(onCommandCreated) + cmdDefs.append(cmdEnableAllCollision) + + cmdSelectDisabled = ui.commandDefinitions.itemById("SelectDisabled") + if not cmdSelectDisabled: + cmdSelectDisabled = ui.commandDefinitions.addButtonDefinition( + "SelectDisabled", + "Selected Collision Disabled Occurrences", + "Select all occurrences labeled for collision disabled", + ) + cmdSelectDisabled.commandCreated.add(onCommandCreated) + cmdDefs.append(cmdSelectDisabled) + + cmdDeleteComponent = ui.commandDefinitions.itemById("DeleteComponent") + if not cmdDeleteComponent: + cmdDeleteComponent = ui.commandDefinitions.addButtonDefinition( + "DeleteComponent", + "Delete All Occurrences", + "Delete all occurrences with the same component", + ) + cmdDeleteComponent.commandCreated.add(onCommandCreated) + cmdDefs.append(cmdDeleteComponent) + + +@logFailure(messageBox=True) +def stopMarkingMenu(ui: adsk.core.UserInterface): + for obj in cmdDefs: + if obj.isValid: + obj.deleteMe() + else: + ui.messageBox(str(obj) + " is not a valid object") - handlers.clear() - except: - ui.messageBox("Failed:\n{}".format(traceback.format_exc())) + handlers.clear() diff --git a/exporter/SynthesisFusionAddin/src/UI/Toolbar.py b/exporter/SynthesisFusionAddin/src/UI/Toolbar.py index e0b8f26f19..bfcc34189a 100644 --- a/exporter/SynthesisFusionAddin/src/UI/Toolbar.py +++ b/exporter/SynthesisFusionAddin/src/UI/Toolbar.py @@ -1,4 +1,5 @@ from ..general_imports import * +from ..Logging import logFailure from ..strings import INTERNAL_ID @@ -13,32 +14,24 @@ class Toolbar: panels = [] controls = [] + @logFailure def __init__(self, name: str): - self.logger = logging.getLogger(f"{INTERNAL_ID}.Toolbar") - self.uid = f"{name}_{INTERNAL_ID}_toolbar" self.name = name designWorkspace = gm.ui.workspaces.itemById("FusionSolidEnvironment") if designWorkspace: - try: - allDesignTabs = designWorkspace.toolbarTabs - - self.tab = allDesignTabs.itemById(self.uid) - - if self.tab is None: - self.tab = allDesignTabs.add(self.uid, name) + allDesignTabs = designWorkspace.toolbarTabs - self.tab.activate() + self.tab = allDesignTabs.itemById(self.uid) - self.logger.debug(f"Created toolbar with {self.uid}") + if self.tab is None: + self.tab = allDesignTabs.add(self.uid, name) - except: - error = traceback.format_exc() - self.logger.error(f"Failed at creating toolbar with {self.uid} due to {error}") + self.tab.activate() - def getPanel(self, name: str, visibility: bool = True) -> str or None: + def getPanel(self, name: str, visibility: bool = True) -> str | None: """# Gets a control for a panel to the tabbed toolbar - optional param for visibility """ @@ -52,47 +45,27 @@ def getPanel(self, name: str, visibility: bool = True) -> str or None: if panel: # panel.isVisible = visibility self.panels.append(panel_uid) - self.logger.debug(f"Created Panel {panel_uid} in Toolbar {self.uid}") return panel_uid else: - self.logger.error(f"Failed to Create Panel {panel_uid} in Toolbar {self.uid}") return None + @logFailure @staticmethod - def getNewPanel(name: str, tab_id: str, toolbar_id: str, visibility: bool = True) -> str or None: + def getNewPanel(name: str, tab_id: str, toolbar_id: str, visibility: bool = True) -> str | None: """# Gets a control for a panel to the tabbed toolbar visibility""" - logger = logging.getLogger(f"{INTERNAL_ID}.Toolbar.getNewPanel") - designWorkspace = gm.ui.workspaces.itemById("FusionSolidEnvironment") - - if designWorkspace: - allDesignTabs = designWorkspace.toolbarTabs - toolbar = allDesignTabs.itemById(toolbar_id) - - if toolbar is None: - logger.error(f"Failed to find Toolbar {toolbar_id}") - return None - - toolbar.activate() - else: - logger.error(f"Failed to find Toolbar {toolbar_id}") - return None + allDesignTabs = designWorkspace.toolbarTabs + toolbar = allDesignTabs.itemById(toolbar_id) + toolbar.activate() panel_uid = f"{name}_{INTERNAL_ID}_tooltab" - panel = toolbar.toolbarPanels.itemById(panel_uid) if panel is None: panel = toolbar.toolbarPanels.add(panel_uid, name) - if panel: - # panel.isVisible = visibility - gm.tabs.append(panel) - logger.debug(f"Created Panel {panel_uid} in Toolbar {toolbar_id}") - return panel_uid - else: - logger.error(f"Failed to Create Panel {panel_uid} in Toolbar {toolbar_id}") - return None + gm.tabs.append(panel) + return panel_uid def toggleVisibility(self, visible: bool) -> None: """# Toggles the visibility of the toolbar to the visibility param diff --git a/exporter/SynthesisFusionAddin/src/configure.py b/exporter/SynthesisFusionAddin/src/configure.py index e121e7ae6e..75d2cb632b 100644 --- a/exporter/SynthesisFusionAddin/src/configure.py +++ b/exporter/SynthesisFusionAddin/src/configure.py @@ -5,9 +5,12 @@ import uuid from configparser import ConfigParser +from .Logging import getLogger from .strings import INTERNAL_ID from .Types.OString import OString +logger = getLogger(f"{INTERNAL_ID}.{__name__}") + try: config = ConfigParser() @@ -38,11 +41,11 @@ CID = uuid.uuid4() config.set("analytics", "c_id", str(CID)) # default values - add exception handling except: - logging.getLogger(f"{INTERNAL_ID}.import_manager").error("Failed\n{}".format(traceback.format_exc())) + logger.error(f"Failed\n{traceback.format_exc()}") def setAnalytics(enabled: bool): - logging.getLogger(f"{INTERNAL_ID}.configure.setAnalytics").info(f"First run , Analytics set to {enabled}") + logger.info(f"First run , Analytics set to {enabled}") ANALYTICS = enabled ans = "yes" if ANALYTICS else "no" write_configuration("analytics", "analytics", ans) diff --git a/exporter/SynthesisFusionAddin/src/general_imports.py b/exporter/SynthesisFusionAddin/src/general_imports.py index 042e4618ea..d659ff49bf 100644 --- a/exporter/SynthesisFusionAddin/src/general_imports.py +++ b/exporter/SynthesisFusionAddin/src/general_imports.py @@ -1,5 +1,4 @@ import json -import logging.handlers import os import pathlib import sys @@ -12,19 +11,17 @@ import adsk.core import adsk.fusion +from .Logging import getLogger +from .strings import INTERNAL_ID + +logger = getLogger(f"{INTERNAL_ID}.{__name__}") + # hard coded to bypass errors for now PROTOBUF = True DEBUG = True -try: - from .GlobalManager import * - from .logging import setupLogger - from .strings import * - - (root_logger, log_handler) = setupLogger() -except ImportError as e: - # nothing to really do here - print(e) +from .GlobalManager import * +from .strings import * try: path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) @@ -40,16 +37,11 @@ from proto import deps except: - logging.getLogger(f"{INTERNAL_ID}.import_manager").error("Failed\n{}".format(traceback.format_exc())) + logger.error("Failed:\n{}".format(traceback.format_exc())) try: - # simple analytics endpoint - # A_EP = AnalyticsEndpoint("UA-188467590-1", 1) - A_EP = None - # Setup the global state gm: GlobalManager = GlobalManager() my_addin_path = os.path.dirname(os.path.realpath(__file__)) except: - # should also log this - logging.getLogger(f"{INTERNAL_ID}.import_manager").error("Failed\n{}".format(traceback.format_exc())) + logger.error("Failed:\n{}".format(traceback.format_exc())) diff --git a/exporter/SynthesisFusionAddin/src/logging.py b/exporter/SynthesisFusionAddin/src/logging.py index 2a0233ae5a..4792c6b7b5 100644 --- a/exporter/SynthesisFusionAddin/src/logging.py +++ b/exporter/SynthesisFusionAddin/src/logging.py @@ -1,33 +1,100 @@ -""" Module to create and setup the logger so that logging is accessible for other internal modules -""" - +import functools import logging.handlers import os import pathlib -from datetime import datetime +import sys +import time +import traceback +from datetime import date, datetime +from typing import cast + +import adsk.core -from .strings import * +from .strings import INTERNAL_ID from .UI.OsHelper import getOSPath +MAX_LOG_FILES_TO_KEEP = 10 +TIMING_LEVEL = 25 + + +class SynthesisLogger(logging.Logger): + def timing(self, msg: str, *args: any, **kwargs: any) -> None: + return self.log(TIMING_LEVEL, msg, *args, **kwargs) + + def cleanupHandlers(self) -> None: + for handler in self.handlers: + handler.close() + + +def setupLogger() -> None: + now = datetime.now().strftime("%H-%M-%S") + today = date.today() + logFileFolder = getOSPath(f"{pathlib.Path(__file__).parent.parent}", "logs") + logFiles = [os.path.join(logFileFolder, file) for file in os.listdir(logFileFolder) if file.endswith(".log")] + logFiles.sort() + if len(logFiles) >= MAX_LOG_FILES_TO_KEEP: + for file in logFiles[: len(logFiles) - MAX_LOG_FILES_TO_KEEP]: + os.remove(file) + + logFileName = f"{logFileFolder}{getOSPath(f'{INTERNAL_ID}-{today}-{now}.log')}" + logHandler = logging.handlers.WatchedFileHandler(logFileName, mode="w") + logHandler.setFormatter(logging.Formatter("%(name)s - %(levelname)s - %(message)s")) + + logging.setLoggerClass(SynthesisLogger) + logging.addLevelName(TIMING_LEVEL, "TIMING") + logger = getLogger(INTERNAL_ID) + logger.setLevel(10) # Debug + logger.addHandler(logHandler) + + +def getLogger(name: str) -> SynthesisLogger: + return cast(SynthesisLogger, logging.getLogger(name)) + + +# Log function failure decorator. +def logFailure(func: callable = None, /, *, messageBox: bool = False) -> callable: + def wrap(func: callable) -> callable: + @functools.wraps(func) + def wrapper(*args: any, **kwargs: any) -> any: + try: + return func(*args, **kwargs) + except BaseException: + excType, excValue, excTrace = sys.exc_info() + tb = traceback.TracebackException(excType, excValue, excTrace) + formattedTb = "".join(list(tb.format())[2:]) # Remove the wrapper func from the traceback. + clsName = "" + if args and hasattr(args[0], "__class__"): + clsName = args[0].__class__.__name__ + "." + + getLogger(f"{INTERNAL_ID}.{clsName}{func.__name__}").error(f"Failed:\n{formattedTb}") + if messageBox: + ui = adsk.core.Application.get().userInterface + ui.messageBox(f"Internal Failure: {formattedTb}", "Synthesis: Error") + + return wrapper -def setupLogger(): - """setupLogger() will setup the file-watcher and writer to write to the log file + if func is None: + # Called with parens. + return wrap - Sub modules can access their own specific logger if they wish using the logging.getLogger(HellionFusion.submodule) - """ - _now = datetime.now().strftime("-%H%M%S") + # Called without parens. + return wrap(func) - loc = pathlib.Path(__file__).parent.parent - path = getOSPath(f"{loc}", "logs") - _log_handler = logging.handlers.WatchedFileHandler(os.environ.get("LOGFILE", f"{path}{INTERNAL_ID}.log"), mode="w") +# Time function decorator. +def timed(func: callable) -> callable: + def wrapper(*args: any, **kwargs: any) -> any: + startTime = time.perf_counter() + result = func(*args, **kwargs) + endTime = time.perf_counter() + runTime = f"{endTime - startTime:5f}s" - # This will make it so I can see the auxiliary logging levels of each of the subclasses - _log_handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")) + clsName = "" + if args and hasattr(args[0], "__class__"): + clsName = args[0].__class__.__name__ + "." - log = logging.getLogger(f"{INTERNAL_ID}") - log.setLevel(os.environ.get("LOGLEVEL", "DEBUG")) - log.addHandler(_log_handler) + logger = getLogger(f"{INTERNAL_ID}.{clsName}{func.__name__}") + logger.timing(f"Runtime of '{func.__name__}' took '{runTime}'.") + return result - # returns the root level logger to the global namespace - return log, _log_handler + return wrapper diff --git a/exporter/SynthesisFusionAddin/src/strings.py b/exporter/SynthesisFusionAddin/src/strings.py index 6e3aa109e2..b5d79c055c 100644 --- a/exporter/SynthesisFusionAddin/src/strings.py +++ b/exporter/SynthesisFusionAddin/src/strings.py @@ -1,4 +1,4 @@ APP_NAME = "Synthesis" APP_TITLE = "Synthesis Robot Exporter" DESCRIPTION = "Exports files from Fusion into the Synthesis Format" -INTERNAL_ID = "synthesis" +INTERNAL_ID = "Synthesis" From 2fb513954ce3577331239266d7305fc29c82d166 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Wed, 3 Jul 2024 14:41:41 -0700 Subject: [PATCH 026/121] Remaining cleanups --- .../Parser/SynthesisParser/JointHierarchy.py | 8 +---- .../src/Parser/SynthesisParser/Parser.py | 34 +++++++++++++------ .../src/Parser/SynthesisParser/RigidGroup.py | 4 +-- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index df17408808..5590669f09 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -9,7 +9,7 @@ from proto.proto_out import joint_pb2, types_pb2 from ...general_imports import * -from ...Logging import getLogger, logFailure, timed +from ...Logging import getLogger, logFailure from ..ExporterOptions import ExporterOptions from .PDMessage import PDMessage from .Utilities import guid_component, guid_occurrence @@ -490,9 +490,6 @@ def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDia root = types_pb2.Node() - # if DEBUG: - # print(f"Configuring {proto_joint.info.name}") - # construct body tree if possible createTreeParts(simNode.data, OccurrenceRelationship.CONNECTION, root, progressDialog) @@ -533,9 +530,6 @@ def createTreeParts( except RuntimeError: node.value = dynNode.data.name - # if DEBUG: - # print(f" -- {dynNode.data.name} + rel : {relationship}\n") - # possibly add additional information for the type of connection made # recurse and add all children connections for edge in dynNode.edges: diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index e60987e7ea..aa44d9324f 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -197,24 +197,36 @@ def export(self) -> bool: if node.value == "ground": joint_hierarchy_out = f"{joint_hierarchy_out} |- ground\n" else: - newnode = assembly_out.data.joints.joint_instances[node.value] - jointdefinition = assembly_out.data.joints.joint_definitions[newnode.joint_reference] + newNode = assembly_out.data.joints.joint_instances[node.value] + jointDefinition = assembly_out.data.joints.joint_definitions[newNode.joint_reference] - wheel_ = " wheel : true" if (jointdefinition.user_data.data["wheel"] != "") else "" - - joint_hierarchy_out = f"{joint_hierarchy_out} |- {jointdefinition.info.name} type: {jointdefinition.joint_motion_type} {wheel_}\n" + wheel_ = " wheel : true" if (jointDefinition.user_data.data["wheel"] != "") else "" + joint_hierarchy_out = ( + f"{joint_hierarchy_out} |---> {jointDefinition.info.name} " + f"type: {jointDefinition.joint_motion_type} {wheel_}\n" + ) for child in node.children: if child.value == "ground": joint_hierarchy_out = f"{joint_hierarchy_out} |---> ground\n" else: - newnode = assembly_out.data.joints.joint_instances[child.value] - jointdefinition = assembly_out.data.joints.joint_definitions[newnode.joint_reference] - wheel_ = " wheel : true" if (jointdefinition.user_data.data["wheel"] != "") else "" - joint_hierarchy_out = f"{joint_hierarchy_out} |---> {jointdefinition.info.name} type: {jointdefinition.joint_motion_type} {wheel_}\n" + newNode = assembly_out.data.joints.joint_instances[child.value] + jointDefinition = assembly_out.data.joints.joint_definitions[newNode.joint_reference] + wheel_ = " wheel : true" if (jointDefinition.user_data.data["wheel"] != "") else "" + joint_hierarchy_out = ( + f"{joint_hierarchy_out} |- {jointDefinition.info.name} " + f"type: {jointDefinition.joint_motion_type} {wheel_}\n" + ) joint_hierarchy_out += "\n\n" - - debug_output = f"Appearances: {len(assembly_out.data.materials.appearances)} \nMaterials: {len(assembly_out.data.materials.physicalMaterials)} \nPart-Definitions: {len(part_defs)} \nParts: {len(parts)} \nSignals: {len(signals)} \nJoints: {len(joints)}\n {joint_hierarchy_out}" + debug_output = ( + f"Appearances: {len(assembly_out.data.materials.appearances)} \n" + f"Materials: {len(assembly_out.data.materials.physicalMaterials)} \n" + f"Part-Definitions: {len(part_defs)} \n" + f"Parts: {len(parts)} \n" + f"Signals: {len(signals)} \n" + f"Joints: {len(joints)}\n" + f"{joint_hierarchy_out}" + ) logger.debug(debug_output.strip()) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py index 0ce92ce1e1..362a2a6e72 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py @@ -12,15 +12,15 @@ - Success """ -import logging from typing import * import adsk.core import adsk.fusion -from Logging import logFailure from proto.proto_out import assembly_pb2 +from ...Logging import logFailure + @logFailure def ExportRigidGroups( From 8e1685e805fe4ed51f1cb2ead3831f05948485c3 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Fri, 5 Jul 2024 10:58:35 -0700 Subject: [PATCH 027/121] added exporter location --- .../src/Parser/ExporterOptions.py | 1 + .../src/UI/ConfigCommand.py | 101 ++++++++++-------- 2 files changed, 56 insertions(+), 46 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py index 31ed4cd0c4..31b4381437 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py +++ b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py @@ -22,6 +22,7 @@ SignalType = Enum("SignalType", ["PWM", "CAN", "PASSIVE"]) ExportMode = Enum("ExportMode", ["ROBOT", "FIELD"]) # Dynamic / Static export PreferredUnits = Enum("PreferredUnits", ["METRIC", "IMPERIAL"]) +ExportLocation = Enum("ExportLocation", ["UPLOAD", "DOWNLOAD"]) @dataclass diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 6ea4e0b702..cde94b7d85 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -4,7 +4,7 @@ import logging import os -import platform +#import platform import traceback from enum import Enum @@ -24,6 +24,7 @@ SignalType, Wheel, WheelType, + ExportLocation ) from ..Parser.SynthesisParser.Parser import Parser from ..Parser.SynthesisParser.Utilities import guid_occurrence @@ -222,6 +223,17 @@ def notify(self, args): dropdownExportMode.tooltip = "Export Mode" dropdownExportMode.tooltipDescription = "
Does this object move dynamically?" + # ~~~~~~~~~~~~~~~~ EXPORT LOCATION ~~~~~~~~~~~~~~~~~~ + + dropdownExportLocation = inputs.addDropDownCommandInput("location", "Export Location", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle) + + upload: bool = exporterOptions.exportLocation == ExportLocation.UPLOAD + dropdownExportLocation.listItems.add("Upload", upload) + dropdownExportLocation.listItems.add("Download", not upload) + + dropdownExportLocation.tooltip = "Export Location" + dropdownExportLocation.tooltipDescription = "
Do you want to upload this mirabuf file to APS, or download it to your local machine?" + # ~~~~~~~~~~~~~~~~ WEIGHT CONFIGURATION ~~~~~~~~~~~~~~~~ """ Table for weight config. @@ -621,11 +633,11 @@ def notify(self, args): """ Creates the advanced tab, which is the parent container for internal command inputs """ - advancedSettings = INPUTS_ROOT.addTabCommandInput("advanced_settings", "Advanced") + advancedSettings: adsk.core.TabCommandInput = INPUTS_ROOT.addTabCommandInput("advanced_settings", "Advanced") advancedSettings.tooltip = ( "Additional Advanced Settings to change how your model will be translated into Unity." ) - a_input = advancedSettings.children + a_input: adsk.core.CommandInputs = advancedSettings.children # ~~~~~~~~~~~~~~~~ EXPORTER SETTINGS ~~~~~~~~~~~~~~~~ """ @@ -660,55 +672,36 @@ def notify(self, args): """ Physics settings group command """ - physicsSettings = a_input.addGroupCommandInput("physics_settings", "Physics Settings") + physicsSettings: adsk.core.GroupCommandInput = a_input.addGroupCommandInput("physics_settings", "Physics Settings") - physicsSettings.isExpanded = False + physicsSettings.isExpanded = True physicsSettings.isEnabled = True - physicsSettings.tooltip = "tooltip" # TODO: update tooltip - physics_settings = physicsSettings.children - - # AARD-1687 - # Should also be commented out / removed? - # This would cause problems elsewhere but I can't tell i f - # this is even being used. - frictionOverrideTable = self.createTableInput( - "friction_override_table", - "", - physics_settings, - 2, - "1:2", - 1, - columnSpacing=25, - ) - frictionOverrideTable.tablePresentationStyle = 2 - # frictionOverrideTable.isFullWidth = True + physicsSettings.tooltip = "Settings relating to the custom physics of the robot, like the wheel friction" + physics_settings: adsk.core.CommandInputs = physicsSettings.children - frictionOverride = self.createBooleanInput( + frictionOverrideInput = self.createBooleanInput( "friction_override", - "", + "Friction Override", physics_settings, - checked=False, + checked=exporterOptions.frictionOverride, # object is missing attribute tooltip="Manually override the default friction values on the bodies in the assembly.", enabled=True, isCheckBox=False, ) - frictionOverride.resourceFolder = IconPaths.stringIcons["friction_override-enabled"] - frictionOverride.isFullWidth = True + frictionOverrideInput.resourceFolder = IconPaths.stringIcons["friction_override-enabled"] + frictionOverrideInput.isFullWidth = True valueList = [1] for i in range(20): valueList.append(i / 20) - frictionCoeff = physics_settings.addFloatSliderListCommandInput( - "friction_coeff_override", "Friction Coefficient", "", valueList + frictionCoeffSlider: adsk.core.FloatSliderCommandInput = physics_settings.addFloatSliderListCommandInput( + "friction_override_coeff", "Friction Coefficient", "", valueList ) - frictionCoeff.isVisible = False - frictionCoeff.valueOne = 0.5 - frictionCoeff.tooltip = "Friction coefficient of field element." - frictionCoeff.tooltipDescription = "Friction coefficients range from 0 (ice) to 1 (rubber)." - - frictionOverrideTable.addCommandInput(frictionOverride, 0, 0) - frictionOverrideTable.addCommandInput(frictionCoeff, 0, 1) + frictionCoeffSlider.isVisible = True + frictionCoeffSlider.valueOne = 0.5 + frictionCoeffSlider.tooltip = "Friction coefficient of field element." + frictionCoeffSlider.tooltipDescription = "Friction coefficients range from 0 (ice) to 1 (rubber)." # ~~~~~~~~~~~~~~~~ JOINT SETTINGS ~~~~~~~~~~~~~~~~ """ @@ -999,12 +992,7 @@ def notify(self, args): self.log.error("Could not execute configuration due to failure") return - export_as_part_boolean = ( - eventArgs.command.commandInputs.itemById("advanced_settings") - .children.itemById("exporter_settings") - .children.itemById("export_as_part") - ).value - + processedFileName = gm.app.activeDocument.name.replace(" ", "_") dropdownExportMode = INPUTS_ROOT.itemById("mode") if dropdownExportMode.selectedItem.index == 0: @@ -1038,7 +1026,9 @@ def notify(self, args): _exportJoints = [] # all selected joints, formatted for parseOptions _exportGamepieces = [] # TODO work on the code to populate Gamepiece _robotWeight = float - _mode = ExportMode.ROBOT + _mode: ExportMode + _location: ExportLocation + """ Loops through all rows in the wheel table to extract all the input values @@ -1167,12 +1157,30 @@ def notify(self, args): elif dropdownExportMode.selectedItem.index == 1: _mode = ExportMode.FIELD + """ + Export Location + """ + dropdownExportLocation = INPUTS_ROOT.itemById("location") + if dropdownExportLocation.select.index == 0: + _location = ExportLocation.UPLOAD + elif dropdownExportLocation.select.index == 1: + _location = ExportLocation.DOWNLOAD + + """ + Advanced Settings + """ global compress compress = ( eventArgs.command.commandInputs.itemById("advanced_settings") .children.itemById("exporter_settings") .children.itemById("compress") ).value + + export_as_part_boolean = ( + eventArgs.command.commandInputs.itemById("advanced_settings") + .children.itemById("exporter_settings") + .children.itemById("export_as_part") + ).value exporterOptions = ExporterOptions( savepath, @@ -1185,11 +1193,12 @@ def notify(self, args): preferredUnits=selectedUnits, robotWeight=_robotWeight, exportMode=_mode, + exportLocation=_location, compressOutput=compress, exportAsPart=export_as_part_boolean, ) - Parser(exporterOptions).export() + _: bool = Parser(exporterOptions).export() exporterOptions.writeToDesign() except: if gm.ui: @@ -1628,7 +1637,7 @@ def notify(self, args): inputs = cmdInput.commandInputs onSelect = gm.handlers[3] - frictionCoeff = INPUTS_ROOT.itemById("friction_coeff_override") + frictionCoeff = INPUTS_ROOT.itemById("friction_override_coeff") wheelSelect = inputs.itemById("wheel_select") jointSelect = inputs.itemById("joint_select") From 0f5c97cb836a5ccdd06ff8fafa8993a9f4f4c6a8 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Fri, 5 Jul 2024 15:13:29 -0700 Subject: [PATCH 028/121] imported upload code to parser --- .../src/Parser/ExporterOptions.py | 2 ++ .../src/Parser/SynthesisParser/Parser.py | 17 +++++++++++++++-- .../src/UI/ConfigCommand.py | 6 +++--- .../SynthesisFusionAddin/src/general_imports.py | 3 +++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py index 31b4381437..df5d21c9c1 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py +++ b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py @@ -96,6 +96,8 @@ class ExporterOptions: compressOutput: bool = field(default=True) exportAsPart: bool = field(default=False) + exportLocation: ExportLocation = field(default=ExportLocation.UPLOAD) + hierarchy: ModelHierarchy = field(default=ModelHierarchy.FusionAssembly) visualQuality: TriangleMeshQualityOptions = field(default=TriangleMeshQualityOptions.LowQualityTriangleMesh) physicalDepth: PhysicalDepth = field(default=PhysicalDepth.AllOccurrence) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index a925f5b119..bf362dc0a6 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -9,9 +9,10 @@ from ...general_imports import * from ...UI.Camera import captureThumbnail, clearIconCache -from ..ExporterOptions import ExporterOptions, ExportMode +from ..ExporterOptions import ExporterOptions, ExportMode, ExportLocation from . import Components, JointHierarchy, Joints, Materials, PDMessage from .Utilities import * +from ...temp_file_upload import upload_mirabuf # This line causes everything to break class Parser: @@ -179,7 +180,19 @@ def export(self) -> bool: f.write(assembly_out.SerializeToString()) f.close() - progressDialog.hide() + # Upload Mirabuf File to APS + if self.exporterOptions.exportLocation == ExportLocation.UPLOAD: + self.logger.debug("Uploading file to APS") + project = app.data.dataProjects.item(0) + if not project.isValid: + return False # add throw later + project_id = project.id + folder_id = project.rootFolder.id + file_location = self.exporterOptions.fileLocation + if upload_mirabuf(project_id, folder_id, file_location).is_err(): + gm.ui.messageBox("FAILED TO UPLOAD FILE TO APS", "ERROR") # add throw later + + _ = progressDialog.hide() if DEBUG: part_defs = assembly_out.data.parts.part_definitions diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index cde94b7d85..30010c9a33 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -683,7 +683,7 @@ def notify(self, args): "friction_override", "Friction Override", physics_settings, - checked=exporterOptions.frictionOverride, # object is missing attribute + checked=True, # object is missing attribute tooltip="Manually override the default friction values on the bodies in the assembly.", enabled=True, isCheckBox=False, @@ -1161,9 +1161,9 @@ def notify(self, args): Export Location """ dropdownExportLocation = INPUTS_ROOT.itemById("location") - if dropdownExportLocation.select.index == 0: + if dropdownExportLocation.selectedItem.index == 0: _location = ExportLocation.UPLOAD - elif dropdownExportLocation.select.index == 1: + elif dropdownExportLocation.selectedItem.index == 1: _location = ExportLocation.DOWNLOAD """ diff --git a/exporter/SynthesisFusionAddin/src/general_imports.py b/exporter/SynthesisFusionAddin/src/general_imports.py index 042e4618ea..87a6874c53 100644 --- a/exporter/SynthesisFusionAddin/src/general_imports.py +++ b/exporter/SynthesisFusionAddin/src/general_imports.py @@ -9,6 +9,9 @@ from time import time from types import FunctionType +from requests import get, post +from result import Ok, Err, is_err + import adsk.core import adsk.fusion From 1b59d9459d1fb96242e36bc6ecdbd042c4543c15 Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Fri, 5 Jul 2024 19:06:32 -0600 Subject: [PATCH 029/121] Working on View panle --- fission/src/Synthesis.tsx | 2 + .../simulation/wpilib_brain/WPILibBrain.ts | 157 +++++++++--------- fission/src/ui/components/MainHUD.tsx | 7 +- fission/src/ui/panels/WSViewPanel.tsx | 42 +++++ 4 files changed, 129 insertions(+), 79 deletions(-) create mode 100644 fission/src/ui/panels/WSViewPanel.tsx diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index bc66c24396..982dc31708 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -55,6 +55,7 @@ import { AddRobotsModal, AddFieldsModal, SpawningModal } from '@/modals/spawning import ImportMirabufModal from '@/modals/mirabuf/ImportMirabufModal.tsx'; import WPILibWSWorker from '@/systems/simulation/wpilib_brain/WPILibWSWorker.ts?worker' +import WSViewPanel from './ui/panels/WSViewPanel.tsx'; const DEFAULT_MIRA_PATH = '/api/mira/Robots/Team 2471 (2018)_v7.mira'; @@ -209,6 +210,7 @@ const initialPanels: ReactElement[] = [ , , , + , ] export default Synthesis diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts index a758549910..7f7985b37a 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts @@ -6,84 +6,88 @@ import WPILibWSWorker from './WPILibWSWorker?worker' const worker = new WPILibWSWorker() -abstract class DeviceType { - protected _device: string +export const PWM_UPDATE_EVENT_KEY = "ws/pwm-update" + +// abstract class DeviceType { +// protected _device: string - constructor(device: string) { - this._device = device - } +// constructor(device: string) { +// this._device = device +// } - public abstract Update(data: any): void -} +// public abstract Update(data: any): void +// } -class Solenoid extends DeviceType { - constructor(device: string) { - super(device) - } +// class Solenoid extends DeviceType { +// constructor(device: string) { +// super(device) +// } - public Update(data: any): void { +// public Update(data: any): void { - } -} -const solenoids: Map = new Map() +// } +// } +// const solenoids: Map = new Map() -class SimDevice extends DeviceType { - constructor(device: string) { - super(device) - } +// class SimDevice extends DeviceType { +// constructor(device: string) { +// super(device) +// } - public Update(data: any): void { +// public Update(data: any): void { - } -} -const simDevices: Map = new Map() +// } +// } +// const simDevices: Map = new Map() -class SparkMax extends SimDevice { +// class SparkMax extends SimDevice { - private _sparkMaxId: number; +// private _sparkMaxId: number; - constructor(device: string) { - super(device) +// constructor(device: string) { +// super(device) - console.debug('Spark Max Constructed') +// console.debug('Spark Max Constructed') - if (device.match(/spark max/i)?.length ?? 0 > 0) { - const endPos = device.indexOf(']') - const startPos = device.indexOf('[') - this._sparkMaxId = parseInt(device.substring(startPos + 1, endPos)) - } else { - throw new Error('Unrecognized Device ID') - } - } - - public Update(data: any): void { - super.Update(data) - - Object.entries(data).forEach(x => { - // if (x[0].startsWith('<')) { - // console.debug(`${x[0]} -> ${x[1]}`) - // } - - switch (x[0]) { - case '': - - break - default: - console.debug(`[${this._sparkMaxId}] ${x[0]} -> ${x[1]}`) - break - } - }) - } - - public SetPosition(val: number) { - worker.postMessage( - { - command: 'update', - data: { type: 'simdevice', device: this._device, data: { '>Position': val } } - } - ) - } -} +// if (device.match(/spark max/i)?.length ?? 0 > 0) { +// const endPos = device.indexOf(']') +// const startPos = device.indexOf('[') +// this._sparkMaxId = parseInt(device.substring(startPos + 1, endPos)) +// } else { +// throw new Error('Unrecognized Device ID') +// } +// } + +// public Update(data: any): void { +// super.Update(data) + +// Object.entries(data).forEach(x => { +// // if (x[0].startsWith('<')) { +// // console.debug(`${x[0]} -> ${x[1]}`) +// // } + +// switch (x[0]) { +// case '': + +// break +// default: +// console.debug(`[${this._sparkMaxId}] ${x[0]} -> ${x[1]}`) +// break +// } +// }) +// } + +// public SetPosition(val: number) { +// worker.postMessage( +// { +// command: 'update', +// data: { type: 'simdevice', device: this._device, data: { '>Position': val } } +// } +// ) +// } +// } + +export const pwmMap = new Map() worker.addEventListener('message', (eventData: MessageEvent) => { let data: any | undefined; @@ -98,23 +102,20 @@ worker.addEventListener('message', (eventData: MessageEvent) => { return } + const device = data.device + const updateData = data.data + switch (data.type.toLowerCase()) { + case 'pwm': { // ESLint wants curly brackets apparently. Doesn't like scoped variables with only colon? + const currentData = pwmMap.get(device) ?? {} + Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) + pwmMap.set(device, currentData) + window.dispatchEvent(new Event(PWM_UPDATE_EVENT_KEY)) + break + } case 'solenoid': - if (!solenoids.has(data.device)) { - solenoids.set(data.device, new Solenoid(data.device)) - } - solenoids.get(data.device)?.Update(data.data) break case 'simdevice': - // console.debug(`SimDevice:\n${JSON.stringify(data, null, '\t')}`) - if (!simDevices.has(data.device)) { - if (data.device.match(/spark max/i)) { - simDevices.set(data.device, new SparkMax(data.device)) - } else { - simDevices.set(data.device, new SimDevice(data.device)) - } - } - simDevices.get(data.device)?.Update(data.data) break default: // console.debug(`Unrecognized Message:\n${data}`) diff --git a/fission/src/ui/components/MainHUD.tsx b/fission/src/ui/components/MainHUD.tsx index f81b3dde21..76da3a2248 100644 --- a/fission/src/ui/components/MainHUD.tsx +++ b/fission/src/ui/components/MainHUD.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react" import { BsCodeSquare } from "react-icons/bs" import { FaCar, FaGear, FaMagnifyingGlass, FaPlus } from "react-icons/fa6" import { BiMenuAltLeft } from "react-icons/bi" -import { GrFormClose } from "react-icons/gr" +import { GrConnect, GrFormClose } from "react-icons/gr" import { GiSteeringWheel } from "react-icons/gi" import { HiDownload } from "react-icons/hi" import { IoGameControllerOutline, IoPeople } from "react-icons/io5" @@ -120,6 +120,11 @@ const MainHUD: React.FC = () => { icon={} onClick={() => openModal("import-mirabuf")} /> + } + onClick={() => openPanel("ws-view")} + />
= ({ panelId }) => { + + const onPwmUpdate = useCallback((_: Event) => { + + }, []) + + useEffect(() => { + window.addEventListener(PWM_UPDATE_EVENT_KEY, onPwmUpdate) + + return () => { + window.removeEventListener(PWM_UPDATE_EVENT_KEY, onPwmUpdate) + } + }, [onPwmUpdate]) + + return ( + } + panelId={panelId} + openLocation="right" + sidePadding={4} + > + + Sup + + + ) +} + +export default WSViewPanel From a4c45b68140f204d60a1013ca17cd060cc669c54 Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Fri, 5 Jul 2024 19:23:52 -0600 Subject: [PATCH 030/121] Fixed dependency installation --- exporter/SynthesisFusionAddin/Synthesis.py | 4 +++- exporter/SynthesisFusionAddin/proto/deps.py | 25 +++++++++++++-------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/exporter/SynthesisFusionAddin/Synthesis.py b/exporter/SynthesisFusionAddin/Synthesis.py index 335a2416ce..1f53f69e93 100644 --- a/exporter/SynthesisFusionAddin/Synthesis.py +++ b/exporter/SynthesisFusionAddin/Synthesis.py @@ -6,13 +6,15 @@ import adsk.core +from .proto.deps import installDependencies +installDependencies() + from .src.configure import setAnalytics, unload_config from .src.general_imports import APP_NAME, DESCRIPTION, INTERNAL_ID, gm, root_logger from .src.Types.OString import OString from .src.UI import HUI, Camera, ConfigCommand, Handlers, Helper, MarkingMenu from .src.UI.Toolbar import Toolbar - def run(_): """## Entry point to application from Fusion. diff --git a/exporter/SynthesisFusionAddin/proto/deps.py b/exporter/SynthesisFusionAddin/proto/deps.py index ea6da5a406..887649c1d6 100644 --- a/exporter/SynthesisFusionAddin/proto/deps.py +++ b/exporter/SynthesisFusionAddin/proto/deps.py @@ -10,7 +10,6 @@ system = platform.system() - def getPythonFolder() -> str: """Retreives the folder that contains the Autodesk python executable @@ -166,14 +165,22 @@ def _checkDeps() -> bool: except ImportError: return False +""" +Checks for, and installs if need be, the dependencies needed by the Synthesis Exporter. Will error if it cannot install the dependencies +correctly. This should crash the exporter, since most of the exporter needs these dependencies to function in +the first place. +""" +def installDependencies(): + try: + import logging.handlers -try: - import logging.handlers + import google.protobuf + import pkg_resources - import google.protobuf - import pkg_resources + from .proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 - from .proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 -except ImportError or ModuleNotFoundError: - installCross(["protobuf==4.23.3"]) - from .proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 + from requests import get, post + from result import Ok, Err, is_err + except ImportError or ModuleNotFoundError: + installCross(["protobuf==4.23.3", "requests==2.32.3", "result==0.17.0"]) + from .proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 From 52e5a162aad976a13f11ba2ed802f0ac4b1142c6 Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Sat, 6 Jul 2024 10:35:30 -0600 Subject: [PATCH 031/121] Discovering some short comings with the WPILib WS sim --- .../simulation/wpilib_brain/WPILibBrain.ts | 39 ++++++++-- .../simulation/wpilib_brain/WPILibWSWorker.ts | 25 +++---- fission/src/ui/components/MainHUD.tsx | 2 +- fission/src/ui/panels/WSViewPanel.tsx | 75 ++++++++++++++++--- 4 files changed, 108 insertions(+), 33 deletions(-) diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts index 7f7985b37a..f7141aa8e8 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts @@ -6,7 +6,7 @@ import WPILibWSWorker from './WPILibWSWorker?worker' const worker = new WPILibWSWorker() -export const PWM_UPDATE_EVENT_KEY = "ws/pwm-update" +export const SIM_MAP_UPDATE_EVENT = "ws/sim-map-update" // abstract class DeviceType { // protected _device: string @@ -88,35 +88,62 @@ export const PWM_UPDATE_EVENT_KEY = "ws/pwm-update" // } export const pwmMap = new Map() +export const simDeviceMap = new Map() +export const canMotorMap = new Map() worker.addEventListener('message', (eventData: MessageEvent) => { let data: any | undefined; try { - data = JSON.parse(eventData.data) + if (typeof(eventData.data) == 'object') { + data = eventData.data + } else { + data = JSON.parse(eventData.data) + } } catch (e) { console.warn(`Failed to parse data:\n${JSON.stringify(eventData.data)}`) } - if (!data) { - // console.log('No data, bailing out') + if (!data || !data.type) { + console.log('No data, bailing out') return } + // console.debug(data) + const device = data.device const updateData = data.data switch (data.type.toLowerCase()) { case 'pwm': { // ESLint wants curly brackets apparently. Doesn't like scoped variables with only colon? + console.debug('pwm') const currentData = pwmMap.get(device) ?? {} Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) pwmMap.set(device, currentData) - window.dispatchEvent(new Event(PWM_UPDATE_EVENT_KEY)) + + window.dispatchEvent(new Event(SIM_MAP_UPDATE_EVENT)) break } case 'solenoid': + console.debug('solenoid') + break + case 'simdevice': { + console.debug('simdevice') + const currentData = simDeviceMap.get(device) ?? {} + Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) + simDeviceMap.set(device, currentData) + + window.dispatchEvent(new Event(SIM_MAP_UPDATE_EVENT)) break - case 'simdevice': + } + case 'canmotor': { + console.debug('canmotor') + const currentData = canMotorMap.get(device) ?? {} + Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) + canMotorMap.set(device, currentData) + + window.dispatchEvent(new Event(SIM_MAP_UPDATE_EVENT)) break + } default: // console.debug(`Unrecognized Message:\n${data}`) break diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts b/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts index 632eb8f99d..784db32306 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts @@ -5,21 +5,18 @@ let socket: WebSocket | undefined = undefined const connectMutex = new Mutex() async function tryConnect(port: number | undefined): Promise { - if (!connectMutex.isLocked()) { - await connectMutex.runExclusive(() => { - if ((socket?.readyState ?? WebSocket.CLOSED) == WebSocket.OPEN) { - socket?.close() - socket = undefined - } - - socket = new WebSocket(`ws://localhost:${port ?? 3300}/wpilibws`) + await connectMutex.runExclusive(() => { + if ((socket?.readyState ?? WebSocket.CLOSED) == WebSocket.OPEN) { + return + } - socket.addEventListener('open', () => { console.log('WS Opened'); self.postMessage({ status: 'open' }); }) - socket.addEventListener('error', () => { console.log('WS Could not open'); self.postMessage({ status: 'error' }) }) + socket = new WebSocket(`ws://localhost:${port ?? 3300}/wpilibws`) - socket.addEventListener('message', onMessage) - }) - } + socket.addEventListener('open', () => { console.log('WS Opened'); self.postMessage({ status: 'open' }); }) + socket.addEventListener('error', () => { console.log('WS Could not open'); self.postMessage({ status: 'error' }) }) + + socket.addEventListener('message', onMessage) + }).then(() => console.debug('Mutex released')) } async function tryDisconnect(): Promise { @@ -28,7 +25,7 @@ async function tryDisconnect(): Promise { return } - socket.close() + socket?.close() socket = undefined }) } diff --git a/fission/src/ui/components/MainHUD.tsx b/fission/src/ui/components/MainHUD.tsx index 76da3a2248..fab368a401 100644 --- a/fission/src/ui/components/MainHUD.tsx +++ b/fission/src/ui/components/MainHUD.tsx @@ -143,7 +143,7 @@ const MainHUD: React.FC = () => { value={"WS Test"} icon={} onClick={() => { - worker?.postMessage({ command: 'connect' }); + // worker?.postMessage({ command: 'connect' }); const miraObjs = [...World.SceneRenderer.sceneObjects.entries()] .filter(x => x[1] instanceof MirabufSceneObject) console.log(`Number of mirabuf scene objects: ${miraObjs.length}`) diff --git a/fission/src/ui/panels/WSViewPanel.tsx b/fission/src/ui/panels/WSViewPanel.tsx index fc87e15965..7c77c18eed 100644 --- a/fission/src/ui/panels/WSViewPanel.tsx +++ b/fission/src/ui/panels/WSViewPanel.tsx @@ -1,23 +1,65 @@ import Panel, { PanelPropsImpl } from "@/components/Panel" -import { PWM_UPDATE_EVENT_KEY } from "@/systems/simulation/wpilib_brain/WPILibBrain" -import { Typography } from "@mui/material" -import { useCallback, useEffect } from "react" +import { SIM_MAP_UPDATE_EVENT, canMotorMap, pwmMap, simDeviceMap } from "@/systems/simulation/wpilib_brain/WPILibBrain" +import { styled, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material" +import { useCallback, useEffect, useState } from "react" import { GrConnect } from "react-icons/gr" import { IoPeople } from "react-icons/io5" +const TypoStyled = styled(Typography)({ + fontFamily: "Artifakt Legend", + fontWeight: 300, + color: "white" +}) + +function generateTableBody() { + return ( + + {[...pwmMap.entries()].filter(x => x[1][" { + return ( + + PWM + {x[0]} + {JSON.stringify(x[1])} + + ) + })} + {[...simDeviceMap.entries()].map(x => { + return ( + + SimDevice + {x[0]} + {JSON.stringify(x[1])} + + ) + })} + {[...canMotorMap.entries()].map(x => { + return ( + + CAN Motor + {x[0]} + {JSON.stringify(x[1])} + + ) + })} + + ) +} + const WSViewPanel: React.FC = ({ panelId }) => { - const onPwmUpdate = useCallback((_: Event) => { + const [tb, setTb] = useState(generateTableBody()) + const onSimMapUpdate = useCallback((_: Event) => { + setTb(generateTableBody()) }, []) useEffect(() => { - window.addEventListener(PWM_UPDATE_EVENT_KEY, onPwmUpdate) + window.addEventListener(SIM_MAP_UPDATE_EVENT, onSimMapUpdate) return () => { - window.removeEventListener(PWM_UPDATE_EVENT_KEY, onPwmUpdate) + window.removeEventListener(SIM_MAP_UPDATE_EVENT, onSimMapUpdate) } - }, [onPwmUpdate]) + }, [onSimMapUpdate]) return ( = ({ panelId }) => { openLocation="right" sidePadding={4} > - - Sup - + + + + Type + Device + Data + + + {tb} +
+
) } From dd28d7aa5791f62dec57a5a73df446c6d1ba8cc2 Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Sat, 6 Jul 2024 11:10:41 -0600 Subject: [PATCH 032/121] CAN Motors and Encoders giving ok ish data. SparkMaxes aren't supported, however --- .../simulation/wpilib_brain/WPILibBrain.ts | 60 +++++++++++-------- fission/src/ui/panels/WSViewPanel.tsx | 24 +++++--- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts index f7141aa8e8..e699509752 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts @@ -8,6 +8,13 @@ const worker = new WPILibWSWorker() export const SIM_MAP_UPDATE_EVENT = "ws/sim-map-update" +export type SimType = + | 'pwm' + | 'canmotor' + | 'solenoid' + | 'simdevice' + | 'canencoder' + // abstract class DeviceType { // protected _device: string @@ -87,9 +94,7 @@ export const SIM_MAP_UPDATE_EVENT = "ws/sim-map-update" // } // } -export const pwmMap = new Map() -export const simDeviceMap = new Map() -export const canMotorMap = new Map() +export const simMap = new Map>() worker.addEventListener('message', (eventData: MessageEvent) => { let data: any | undefined; @@ -114,42 +119,49 @@ worker.addEventListener('message', (eventData: MessageEvent) => { const updateData = data.data switch (data.type.toLowerCase()) { - case 'pwm': { // ESLint wants curly brackets apparently. Doesn't like scoped variables with only colon? + case 'pwm': console.debug('pwm') - const currentData = pwmMap.get(device) ?? {} - Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) - pwmMap.set(device, currentData) - - window.dispatchEvent(new Event(SIM_MAP_UPDATE_EVENT)) + UpdateSimMap('pwm', device, updateData) break - } case 'solenoid': console.debug('solenoid') + UpdateSimMap('solenoid', device, updateData) break - case 'simdevice': { + case 'simdevice': console.debug('simdevice') - const currentData = simDeviceMap.get(device) ?? {} - Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) - simDeviceMap.set(device, currentData) - - window.dispatchEvent(new Event(SIM_MAP_UPDATE_EVENT)) + UpdateSimMap('simdevice', device, updateData) break - } - case 'canmotor': { + case 'canmotor': console.debug('canmotor') - const currentData = canMotorMap.get(device) ?? {} - Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) - canMotorMap.set(device, currentData) - - window.dispatchEvent(new Event(SIM_MAP_UPDATE_EVENT)) + UpdateSimMap('canmotor', device, updateData) + break + case 'canencoder': + console.debug('canencoder') + UpdateSimMap('canencoder', device, updateData) break - } default: // console.debug(`Unrecognized Message:\n${data}`) break } }) +function UpdateSimMap(type: SimType, device: string, updateData: any) { + let typeMap = simMap.get(type) + if (!typeMap) { + typeMap = new Map() + simMap.set(type, typeMap) + } + + let currentData = typeMap.get(device) + if (!currentData) { + currentData = {} + typeMap.set(device, currentData) + } + Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) + + window.dispatchEvent(new Event(SIM_MAP_UPDATE_EVENT)) +} + class WPILibBrain extends Brain { constructor(mech: Mechanism) { diff --git a/fission/src/ui/panels/WSViewPanel.tsx b/fission/src/ui/panels/WSViewPanel.tsx index 7c77c18eed..36356243de 100644 --- a/fission/src/ui/panels/WSViewPanel.tsx +++ b/fission/src/ui/panels/WSViewPanel.tsx @@ -1,9 +1,8 @@ import Panel, { PanelPropsImpl } from "@/components/Panel" -import { SIM_MAP_UPDATE_EVENT, canMotorMap, pwmMap, simDeviceMap } from "@/systems/simulation/wpilib_brain/WPILibBrain" +import { SIM_MAP_UPDATE_EVENT, simMap } from "@/systems/simulation/wpilib_brain/WPILibBrain" import { styled, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material" import { useCallback, useEffect, useState } from "react" import { GrConnect } from "react-icons/gr" -import { IoPeople } from "react-icons/io5" const TypoStyled = styled(Typography)({ fontFamily: "Artifakt Legend", @@ -14,7 +13,7 @@ const TypoStyled = styled(Typography)({ function generateTableBody() { return ( - {[...pwmMap.entries()].filter(x => x[1][" { + {simMap.has('pwm') ? [...simMap.get('pwm')!.entries()].filter(x => x[1][" { return ( PWM @@ -22,8 +21,8 @@ function generateTableBody() { {JSON.stringify(x[1])} ) - })} - {[...simDeviceMap.entries()].map(x => { + }) : <>} + {simMap.has('simdevice') ? [...simMap.get('simdevice')!.entries()].map(x => { return ( SimDevice @@ -31,8 +30,8 @@ function generateTableBody() { {JSON.stringify(x[1])} ) - })} - {[...canMotorMap.entries()].map(x => { + }): <>} + {simMap.has('canmotor') ? [...simMap.get('canmotor')!.entries()].map(x => { return ( CAN Motor @@ -40,7 +39,16 @@ function generateTableBody() { {JSON.stringify(x[1])} ) - })} + }) : <>} + {simMap.has('canencoder') ? [...simMap.get('canencoder')!.entries()].map(x => { + return ( + + CAN Encoder + {x[0]} + {JSON.stringify(x[1])} + + ) + }) : <>} ) } From 63648371344077649057c40cad0bcbdeb93561bb Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Sat, 6 Jul 2024 10:32:12 -0700 Subject: [PATCH 033/121] fix folder ids --- .../SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index bf362dc0a6..dfab55eb5e 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -185,10 +185,12 @@ def export(self) -> bool: self.logger.debug("Uploading file to APS") project = app.data.dataProjects.item(0) if not project.isValid: + gm.ui.messageBox("Project is invalid", "") return False # add throw later project_id = project.id folder_id = project.rootFolder.id file_location = self.exporterOptions.fileLocation + gm.ui.messageBox(f"project: {project_id}\nfolder: {folder_id}\nfile: {file_location}", "ARGS:") if upload_mirabuf(project_id, folder_id, file_location).is_err(): gm.ui.messageBox("FAILED TO UPLOAD FILE TO APS", "ERROR") # add throw later From eb0277737fa2320bc7c63828e4eb4389b2c89d2c Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Sat, 6 Jul 2024 12:39:10 -0600 Subject: [PATCH 034/121] Slowly discovering this may not be the solution --- .../simulation/wpilib_brain/WPILibBrain.ts | 214 +++++++++++------- fission/src/ui/components/MainHUD.tsx | 1 - fission/src/ui/panels/WSViewPanel.tsx | 8 +- 3 files changed, 133 insertions(+), 90 deletions(-) diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts index e699509752..ed5d8bd1d5 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts @@ -8,93 +8,133 @@ const worker = new WPILibWSWorker() export const SIM_MAP_UPDATE_EVENT = "ws/sim-map-update" +const PWM_SPEED = "" ? FieldType.Both : FieldType.Read + case '>': + return FieldType.Write + default: + return FieldType.Unknown + } +} -// public abstract Update(data: any): void -// } +export const simMap = new Map>() -// class Solenoid extends DeviceType { -// constructor(device: string) { -// super(device) -// } +class SimGeneric { + private constructor() { } -// public Update(data: any): void { + public static Get(simType: SimType, device: string, field: string, defaultValue?: T): T | undefined { + const fieldType = GetFieldType(field) + if (fieldType != FieldType.Read && fieldType != FieldType.Both) { + console.warn(`Field '${field}' is not a read or both field type`) + return undefined + } -// } -// } -// const solenoids: Map = new Map() + const map = simMap.get(simType) + if (!map) { + console.warn(`No '${simType}' devices found`) + return undefined + } + + const data = map.get(device) + if (!data) { + console.warn(`No '${simType}' device '${device}' found`) + return undefined + } + + return data[field] as (T | undefined) ?? defaultValue + } + + public static Set(simType: SimType, device: string, field: string, value: T): boolean { + const fieldType = GetFieldType(field) + if (fieldType != FieldType.Write && fieldType != FieldType.Both) { + console.warn(`Field '${field}' is not a write or both field type`) + return false + } -// class SimDevice extends DeviceType { -// constructor(device: string) { -// super(device) -// } + const map = simMap.get(simType) + if (!map) { + console.warn(`No '${simType}' devices found`) + return false + } -// public Update(data: any): void { + const data = map.get(device) + if (!data) { + console.warn(`No '${simType}' device '${device}' found`) + return false + } -// } -// } -// const simDevices: Map = new Map() + const selectedData: any = {} + selectedData[field] = value + + data[field] = value + worker.postMessage({ + command: 'update', + data: { + type: simType, + device: device, + data: selectedData + } + }) + return true + } +} -// class SparkMax extends SimDevice { +class SimPWM { + private constructor() { } -// private _sparkMaxId: number; + public static GetSpeed(device: string): number | undefined { + return SimGeneric.Get('PWM', device, PWM_SPEED, 0.0) + } -// constructor(device: string) { -// super(device) + public static GetPosition(device: string): number | undefined { + return SimGeneric.Get('PWM', device, PWM_POSITION, 0.0) + } +} -// console.debug('Spark Max Constructed') - -// if (device.match(/spark max/i)?.length ?? 0 > 0) { -// const endPos = device.indexOf(']') -// const startPos = device.indexOf('[') -// this._sparkMaxId = parseInt(device.substring(startPos + 1, endPos)) -// } else { -// throw new Error('Unrecognized Device ID') -// } -// } - -// public Update(data: any): void { -// super.Update(data) - -// Object.entries(data).forEach(x => { -// // if (x[0].startsWith('<')) { -// // console.debug(`${x[0]} -> ${x[1]}`) -// // } - -// switch (x[0]) { -// case '': - -// break -// default: -// console.debug(`[${this._sparkMaxId}] ${x[0]} -> ${x[1]}`) -// break -// } -// }) -// } - -// public SetPosition(val: number) { -// worker.postMessage( -// { -// command: 'update', -// data: { type: 'simdevice', device: this._device, data: { '>Position': val } } -// } -// ) -// } -// } +class SimCANMotor { + private constructor() { } -export const simMap = new Map>() + public static GetDutyCycle(device: string): number | undefined { + return SimGeneric.Get('CANMotor', device, CANMOTOR_DUTY_CYCLE, 0.0) + } + + public static SetSupplyVoltage(device: string, voltage: number): boolean { + return SimGeneric.Set('CANMotor', device, CANMOTOR_SUPPLY_VOLTAGE, voltage) + } +} + +class SimCANEncoder { + private constructor() { } + + public static SetRawInputPosition(device: string, rawInputPosition: number): boolean { + return SimGeneric.Set('CANEncoder', device, CANENCODER_RAW_INPUT_POSITION, rawInputPosition) + } +} + +let flag = false worker.addEventListener('message', (eventData: MessageEvent) => { let data: any | undefined; @@ -118,26 +158,30 @@ worker.addEventListener('message', (eventData: MessageEvent) => { const device = data.device const updateData = data.data - switch (data.type.toLowerCase()) { - case 'pwm': + switch (data.type) { + case 'PWM': console.debug('pwm') - UpdateSimMap('pwm', device, updateData) + UpdateSimMap('PWM', device, updateData) break - case 'solenoid': + case 'Solenoid': console.debug('solenoid') - UpdateSimMap('solenoid', device, updateData) + UpdateSimMap('Solenoid', device, updateData) break - case 'simdevice': + case 'SimDevice': console.debug('simdevice') - UpdateSimMap('simdevice', device, updateData) + UpdateSimMap('SimDevice', device, updateData) + if (!flag) { + flag = true + SimGeneric.Set('SimDevice', device, '>init', true) + } break - case 'canmotor': + case 'CANMotor': console.debug('canmotor') - UpdateSimMap('canmotor', device, updateData) + UpdateSimMap('CANMotor', device, updateData) break - case 'canencoder': + case 'CANEncoder': console.debug('canencoder') - UpdateSimMap('canencoder', device, updateData) + UpdateSimMap('CANEncoder', device, updateData) break default: // console.debug(`Unrecognized Message:\n${data}`) diff --git a/fission/src/ui/components/MainHUD.tsx b/fission/src/ui/components/MainHUD.tsx index fab368a401..196d794a2e 100644 --- a/fission/src/ui/components/MainHUD.tsx +++ b/fission/src/ui/components/MainHUD.tsx @@ -12,7 +12,6 @@ import { motion } from "framer-motion" import logo from "@/assets/autodesk_logo.png" import { ToastType, useToastContext } from "@/ui/ToastContext" import { Random } from "@/util/Random" -import { worker } from "@/Synthesis" import World from "@/systems/World" import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" import WPILibBrain from "@/systems/simulation/wpilib_brain/WPILibBrain" diff --git a/fission/src/ui/panels/WSViewPanel.tsx b/fission/src/ui/panels/WSViewPanel.tsx index 36356243de..693581488d 100644 --- a/fission/src/ui/panels/WSViewPanel.tsx +++ b/fission/src/ui/panels/WSViewPanel.tsx @@ -13,7 +13,7 @@ const TypoStyled = styled(Typography)({ function generateTableBody() { return ( - {simMap.has('pwm') ? [...simMap.get('pwm')!.entries()].filter(x => x[1][" { + {simMap.has('PWM') ? [...simMap.get('PWM')!.entries()].filter(x => x[1][" { return ( PWM @@ -22,7 +22,7 @@ function generateTableBody() { ) }) : <>} - {simMap.has('simdevice') ? [...simMap.get('simdevice')!.entries()].map(x => { + {simMap.has('SimDevice') ? [...simMap.get('SimDevice')!.entries()].map(x => { return ( SimDevice @@ -31,7 +31,7 @@ function generateTableBody() { ) }): <>} - {simMap.has('canmotor') ? [...simMap.get('canmotor')!.entries()].map(x => { + {simMap.has('CANMotor') ? [...simMap.get('CANMotor')!.entries()].map(x => { return ( CAN Motor @@ -40,7 +40,7 @@ function generateTableBody() { ) }) : <>} - {simMap.has('canencoder') ? [...simMap.get('canencoder')!.entries()].map(x => { + {simMap.has('CANEncoder') ? [...simMap.get('CANEncoder')!.entries()].map(x => { return ( CAN Encoder From c0d38ad46329c74d682c81cf0d9f786103bd0f39 Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Sat, 6 Jul 2024 14:10:23 -0600 Subject: [PATCH 035/121] Added modification --- .../simulation/wpilib_brain/WPILibBrain.ts | 43 +++++---- fission/src/ui/components/Panel.tsx | 2 +- fission/src/ui/panels/WSViewPanel.tsx | 94 +++++++++++++++++-- 3 files changed, 116 insertions(+), 23 deletions(-) diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts index ed5d8bd1d5..b00d4aabec 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts @@ -6,13 +6,11 @@ import WPILibWSWorker from './WPILibWSWorker?worker' const worker = new WPILibWSWorker() -export const SIM_MAP_UPDATE_EVENT = "ws/sim-map-update" - const PWM_SPEED = ">() -class SimGeneric { +export class SimGeneric { private constructor() { } - public static Get(simType: SimType, device: string, field: string, defaultValue?: T): T | undefined { + public static Get(simType: SimType, device: string, field: string, defaultValue?: T): T | undefined { const fieldType = GetFieldType(field) if (fieldType != FieldType.Read && fieldType != FieldType.Both) { console.warn(`Field '${field}' is not a read or both field type`) @@ -67,7 +65,7 @@ class SimGeneric { return data[field] as (T | undefined) ?? defaultValue } - public static Set(simType: SimType, device: string, field: string, value: T): boolean { + public static Set(simType: SimType, device: string, field: string, value: T): boolean { const fieldType = GetFieldType(field) if (fieldType != FieldType.Write && fieldType != FieldType.Both) { console.warn(`Field '${field}' is not a write or both field type`) @@ -98,11 +96,13 @@ class SimGeneric { data: selectedData } }) + + window.dispatchEvent(new SimMapUpdateEvent(true)) return true } } -class SimPWM { +export class SimPWM { private constructor() { } public static GetSpeed(device: string): number | undefined { @@ -114,7 +114,7 @@ class SimPWM { } } -class SimCANMotor { +export class SimCANMotor { private constructor() { } public static GetDutyCycle(device: string): number | undefined { @@ -126,7 +126,7 @@ class SimCANMotor { } } -class SimCANEncoder { +export class SimCANEncoder { private constructor() { } public static SetRawInputPosition(device: string, rawInputPosition: number): boolean { @@ -134,8 +134,6 @@ class SimCANEncoder { } } -let flag = false - worker.addEventListener('message', (eventData: MessageEvent) => { let data: any | undefined; try { @@ -170,10 +168,6 @@ worker.addEventListener('message', (eventData: MessageEvent) => { case 'SimDevice': console.debug('simdevice') UpdateSimMap('SimDevice', device, updateData) - if (!flag) { - flag = true - SimGeneric.Set('SimDevice', device, '>init', true) - } break case 'CANMotor': console.debug('canmotor') @@ -203,7 +197,7 @@ function UpdateSimMap(type: SimType, device: string, updateData: any) { } Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) - window.dispatchEvent(new Event(SIM_MAP_UPDATE_EVENT)) + window.dispatchEvent(new SimMapUpdateEvent(false)) } class WPILibBrain extends Brain { @@ -224,4 +218,21 @@ class WPILibBrain extends Brain { } +export class SimMapUpdateEvent extends Event { + + public static readonly TYPE: string = "ws/sim-map-update" + + private _internalUpdate: boolean; + + public get internalUpdate(): boolean { + return this._internalUpdate + } + + public constructor(internalUpdate: boolean) { + super(SimMapUpdateEvent.TYPE) + + this._internalUpdate = internalUpdate + } +} + export default WPILibBrain \ No newline at end of file diff --git a/fission/src/ui/components/Panel.tsx b/fission/src/ui/components/Panel.tsx index bf0703ffce..2edcc18a02 100644 --- a/fission/src/ui/components/Panel.tsx +++ b/fission/src/ui/components/Panel.tsx @@ -125,7 +125,7 @@ const Panel: React.FC = ({ return (
diff --git a/fission/src/ui/panels/WSViewPanel.tsx b/fission/src/ui/panels/WSViewPanel.tsx index 693581488d..4df0226f52 100644 --- a/fission/src/ui/panels/WSViewPanel.tsx +++ b/fission/src/ui/panels/WSViewPanel.tsx @@ -1,13 +1,15 @@ import Panel, { PanelPropsImpl } from "@/components/Panel" -import { SIM_MAP_UPDATE_EVENT, simMap } from "@/systems/simulation/wpilib_brain/WPILibBrain" -import { styled, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material" -import { useCallback, useEffect, useState } from "react" +import { SimMapUpdateEvent, SimGeneric, simMap, SimType } from "@/systems/simulation/wpilib_brain/WPILibBrain" +import { Button, Dropdown } from "@mui/base" +import { Box, MenuItem, Select, Stack, styled, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Typography } from "@mui/material" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { GrConnect } from "react-icons/gr" +type ValueType = "string" | "number" | "object" | "boolean" + const TypoStyled = styled(Typography)({ fontFamily: "Artifakt Legend", fontWeight: 300, - color: "white" }) function generateTableBody() { @@ -53,19 +55,62 @@ function generateTableBody() { ) } +function setGeneric(simType: SimType, device: string, field: string, value: string, valueType: ValueType) { + switch (valueType) { + case "number": + SimGeneric.Set(simType, device, field, parseFloat(value)) + break + case "object": + SimGeneric.Set(simType, device, field, JSON.parse(value)) + break + case "boolean": + SimGeneric.Set(simType, device, field, value.toLowerCase() == "true") + break + default: + SimGeneric.Set(simType, device, field, value) + break + } +} + const WSViewPanel: React.FC = ({ panelId }) => { const [tb, setTb] = useState(generateTableBody()) + const [selectedType, setSelectedType] = useState() + const [selectedDevice, setSelectedDevice] = useState() + const [field, setField] = useState("") + const [value, setValue] = useState("") + const [selectedValueType, setSelectedValueType] = useState("string") + + const deviceSelect = useMemo(() => { + if (!selectedType || !simMap.has(selectedType)) { + return (<>) + } + + return ( + + ) + }, [selectedType]) + + useEffect(() => { + setSelectedDevice(undefined) + }, [selectedType]) + const onSimMapUpdate = useCallback((_: Event) => { setTb(generateTableBody()) }, []) useEffect(() => { - window.addEventListener(SIM_MAP_UPDATE_EVENT, onSimMapUpdate) + window.addEventListener(SimMapUpdateEvent.TYPE, onSimMapUpdate) return () => { - window.removeEventListener(SIM_MAP_UPDATE_EVENT, onSimMapUpdate) + window.removeEventListener(SimMapUpdateEvent.TYPE, onSimMapUpdate) } }, [onSimMapUpdate]) @@ -94,6 +139,43 @@ const WSViewPanel: React.FC = ({ panelId }) => { {tb} + + + {deviceSelect} + {selectedDevice + ? + setField(x.target.value as string)} + > + + setValue(x.target.value as string)} + > + + + + + : <> + } + ) } From 5d61696a8c721d88abadea5fe6e6aef0c985f5c8 Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Sat, 6 Jul 2024 19:54:59 -0600 Subject: [PATCH 036/121] Added sample --- simulation/JavaSample/.gitignore | 8 + simulation/JavaSample/WPILib-License.md | 24 ++ simulation/JavaSample/build.gradle | 109 ++++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43462 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + simulation/JavaSample/gradlew | 249 +++++++++++++ simulation/JavaSample/gradlew.bat | 92 +++++ simulation/JavaSample/settings.gradle | 30 ++ .../JavaSample/src/main/deploy/example.txt | 3 + .../src/main/java/frc/robot/Main.java | 25 ++ .../src/main/java/frc/robot/Robot.java | 128 +++++++ simulation/JavaSample/vendordeps/NavX.json | 40 +++ .../JavaSample/vendordeps/Phoenix6.json | 339 ++++++++++++++++++ simulation/JavaSample/vendordeps/REVLib.json | 74 ++++ .../vendordeps/WPILibNewCommands.json | 38 ++ 15 files changed, 1166 insertions(+) create mode 100644 simulation/JavaSample/.gitignore create mode 100644 simulation/JavaSample/WPILib-License.md create mode 100644 simulation/JavaSample/build.gradle create mode 100644 simulation/JavaSample/gradle/wrapper/gradle-wrapper.jar create mode 100644 simulation/JavaSample/gradle/wrapper/gradle-wrapper.properties create mode 100755 simulation/JavaSample/gradlew create mode 100644 simulation/JavaSample/gradlew.bat create mode 100644 simulation/JavaSample/settings.gradle create mode 100644 simulation/JavaSample/src/main/deploy/example.txt create mode 100644 simulation/JavaSample/src/main/java/frc/robot/Main.java create mode 100644 simulation/JavaSample/src/main/java/frc/robot/Robot.java create mode 100644 simulation/JavaSample/vendordeps/NavX.json create mode 100644 simulation/JavaSample/vendordeps/Phoenix6.json create mode 100644 simulation/JavaSample/vendordeps/REVLib.json create mode 100644 simulation/JavaSample/vendordeps/WPILibNewCommands.json diff --git a/simulation/JavaSample/.gitignore b/simulation/JavaSample/.gitignore new file mode 100644 index 0000000000..0a7cba8930 --- /dev/null +++ b/simulation/JavaSample/.gitignore @@ -0,0 +1,8 @@ +*.json + +.gradle/ +.vscode/ +.wpilib/ +build/ +ctre_sim/ +bin/ diff --git a/simulation/JavaSample/WPILib-License.md b/simulation/JavaSample/WPILib-License.md new file mode 100644 index 0000000000..645e54253a --- /dev/null +++ b/simulation/JavaSample/WPILib-License.md @@ -0,0 +1,24 @@ +Copyright (c) 2009-2024 FIRST and other WPILib contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of FIRST, WPILib, nor the names of other WPILib + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY FIRST AND OTHER WPILIB CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/simulation/JavaSample/build.gradle b/simulation/JavaSample/build.gradle new file mode 100644 index 0000000000..f0c731e2d9 --- /dev/null +++ b/simulation/JavaSample/build.gradle @@ -0,0 +1,109 @@ +plugins { + id "java" + // id "edu.wpi.first.GradleRIO" version "2024.3.2" +} + +// wpi.maven.useLocal = false +// wpi.maven.useFrcMavenLocalDevelopment = true +// wpi.versions.wpilibVersion = '2024.424242.+' +// wpi.versions.wpimathVersion = '2024.424242.+' + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +def ROBOT_MAIN_CLASS = "frc.robot.Main" + +// Define my targets (RoboRIO) and artifacts (deployable files) +// This is added by GradleRIO's backing project DeployUtils. +// deploy { +// targets { +// roborio(getTargetTypeClass('RoboRIO')) { +// // Team number is loaded either from the .wpilib/wpilib_preferences.json +// // or from command line. If not found an exception will be thrown. +// // You can use getTeamOrDefault(team) instead of getTeamNumber if you +// // want to store a team number in this file. +// team = project.frc.getTeamNumber() +// debug = project.frc.getDebugOrDefault(false) + +// artifacts { +// // First part is artifact name, 2nd is artifact type +// // getTargetTypeClass is a shortcut to get the class type using a string + +// frcJava(getArtifactTypeClass('FRCJavaArtifact')) { +// } + +// // Static files artifact +// frcStaticFileDeploy(getArtifactTypeClass('FileTreeArtifact')) { +// files = project.fileTree('src/main/deploy') +// directory = '/home/lvuser/deploy' +// } +// } +// } +// } +// } + +// def deployArtifact = deploy.targets.roborio.artifacts.frcJava + +// Set to true to use debug for JNI. +wpi.java.debugJni = false + +// Set this to true to enable desktop support. +def includeDesktopSupport = true + +// Defining my dependencies. In this case, WPILib (+ friends), and vendor libraries. +// Also defines JUnit 5. +dependencies { + implementation wpi.java.deps.wpilib() + implementation wpi.java.vendor.java() + + roborioDebug wpi.java.deps.wpilibJniDebug(wpi.platforms.roborio) + roborioDebug wpi.java.vendor.jniDebug(wpi.platforms.roborio) + + roborioRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.roborio) + roborioRelease wpi.java.vendor.jniRelease(wpi.platforms.roborio) + + nativeDebug wpi.java.deps.wpilibJniDebug(wpi.platforms.desktop) + nativeDebug wpi.java.vendor.jniDebug(wpi.platforms.desktop) + simulationDebug wpi.sim.enableDebug() + + nativeRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.desktop) + nativeRelease wpi.java.vendor.jniRelease(wpi.platforms.desktop) + simulationRelease wpi.sim.enableRelease() + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +test { + useJUnitPlatform() + systemProperty 'junit.jupiter.extensions.autodetection.enabled', 'true' +} + +// Simulation configuration (e.g. environment variables). +wpi.sim.addGui().defaultEnabled = true +wpi.sim.addDriverstation() + +wpi.sim.envVar("HALSIMWS_HOST", "127.0.0.1") +wpi.sim.addWebsocketsServer().defaultEnabled = true + +// Setting up my Jar File. In this case, adding all libraries into the main jar ('fat jar') +// in order to make them all available at runtime. Also adding the manifest so WPILib +// knows where to look for our Robot Class. +jar { + from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } + from sourceSets.main.allSource + manifest edu.wpi.first.gradlerio.GradleRIOPlugin.javaManifest(ROBOT_MAIN_CLASS) + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + +// Configure jar and deploy tasks +deployArtifact.jarTask = jar +wpi.java.configureExecutableTasks(jar) +wpi.java.configureTestTasks(test) + +// Configure string concat to always inline compile +tasks.withType(JavaCompile) { + options.compilerArgs.add '-XDstringConcat=inline' +} diff --git a/simulation/JavaSample/gradle/wrapper/gradle-wrapper.jar b/simulation/JavaSample/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d64cd4917707c1f8861d8cb53dd15194d4248596 GIT binary patch literal 43462 zcma&NWl&^owk(X(xVyW%ySuwf;qI=D6|RlDJ2cR^yEKh!@I- zp9QeisK*rlxC>+~7Dk4IxIRsKBHqdR9b3+fyL=ynHmIDe&|>O*VlvO+%z5;9Z$|DJ zb4dO}-R=MKr^6EKJiOrJdLnCJn>np?~vU-1sSFgPu;pthGwf}bG z(1db%xwr#x)r+`4AGu$j7~u2MpVs3VpLp|mx&;>`0p0vH6kF+D2CY0fVdQOZ@h;A` z{infNyvmFUiu*XG}RNMNwXrbec_*a3N=2zJ|Wh5z* z5rAX$JJR{#zP>KY**>xHTuw?|-Rg|o24V)74HcfVT;WtQHXlE+_4iPE8QE#DUm%x0 zEKr75ur~W%w#-My3Tj`hH6EuEW+8K-^5P62$7Sc5OK+22qj&Pd1;)1#4tKihi=~8C zHiQSst0cpri6%OeaR`PY>HH_;CPaRNty%WTm4{wDK8V6gCZlG@U3$~JQZ;HPvDJcT1V{ z?>H@13MJcCNe#5z+MecYNi@VT5|&UiN1D4ATT+%M+h4c$t;C#UAs3O_q=GxK0}8%8 z8J(_M9bayxN}69ex4dzM_P3oh@ZGREjVvn%%r7=xjkqxJP4kj}5tlf;QosR=%4L5y zWhgejO=vao5oX%mOHbhJ8V+SG&K5dABn6!WiKl{|oPkq(9z8l&Mm%(=qGcFzI=eLu zWc_oCLyf;hVlB@dnwY98?75B20=n$>u3b|NB28H0u-6Rpl((%KWEBOfElVWJx+5yg z#SGqwza7f}$z;n~g%4HDU{;V{gXIhft*q2=4zSezGK~nBgu9-Q*rZ#2f=Q}i2|qOp z!!y4p)4o=LVUNhlkp#JL{tfkhXNbB=Ox>M=n6soptJw-IDI|_$is2w}(XY>a=H52d z3zE$tjPUhWWS+5h=KVH&uqQS=$v3nRs&p$%11b%5qtF}S2#Pc`IiyBIF4%A!;AVoI zXU8-Rpv!DQNcF~(qQnyyMy=-AN~U>#&X1j5BLDP{?K!%h!;hfJI>$mdLSvktEr*89 zdJHvby^$xEX0^l9g$xW-d?J;L0#(`UT~zpL&*cEh$L|HPAu=P8`OQZV!-}l`noSp_ zQ-1$q$R-gDL)?6YaM!=8H=QGW$NT2SeZlb8PKJdc=F-cT@j7Xags+Pr*jPtlHFnf- zh?q<6;)27IdPc^Wdy-mX%2s84C1xZq9Xms+==F4);O`VUASmu3(RlgE#0+#giLh-& zcxm3_e}n4{%|X zJp{G_j+%`j_q5}k{eW&TlP}J2wtZ2^<^E(O)4OQX8FDp6RJq!F{(6eHWSD3=f~(h} zJXCf7=r<16X{pHkm%yzYI_=VDP&9bmI1*)YXZeB}F? z(%QsB5fo*FUZxK$oX~X^69;x~j7ms8xlzpt-T15e9}$4T-pC z6PFg@;B-j|Ywajpe4~bk#S6(fO^|mm1hKOPfA%8-_iGCfICE|=P_~e;Wz6my&)h_~ zkv&_xSAw7AZ%ThYF(4jADW4vg=oEdJGVOs>FqamoL3Np8>?!W#!R-0%2Bg4h?kz5I zKV-rKN2n(vUL%D<4oj@|`eJ>0i#TmYBtYmfla;c!ATW%;xGQ0*TW@PTlGG><@dxUI zg>+3SiGdZ%?5N=8uoLA|$4isK$aJ%i{hECP$bK{J#0W2gQ3YEa zZQ50Stn6hqdfxJ*9#NuSLwKFCUGk@c=(igyVL;;2^wi4o30YXSIb2g_ud$ zgpCr@H0qWtk2hK8Q|&wx)}4+hTYlf;$a4#oUM=V@Cw#!$(nOFFpZ;0lc!qd=c$S}Z zGGI-0jg~S~cgVT=4Vo)b)|4phjStD49*EqC)IPwyeKBLcN;Wu@Aeph;emROAwJ-0< z_#>wVm$)ygH|qyxZaet&(Vf%pVdnvKWJn9`%DAxj3ot;v>S$I}jJ$FLBF*~iZ!ZXE zkvui&p}fI0Y=IDX)mm0@tAd|fEHl~J&K}ZX(Mm3cm1UAuwJ42+AO5@HwYfDH7ipIc zmI;1J;J@+aCNG1M`Btf>YT>~c&3j~Qi@Py5JT6;zjx$cvOQW@3oQ>|}GH?TW-E z1R;q^QFjm5W~7f}c3Ww|awg1BAJ^slEV~Pk`Kd`PS$7;SqJZNj->it4DW2l15}xP6 zoCl$kyEF%yJni0(L!Z&14m!1urXh6Btj_5JYt1{#+H8w?5QI%% zo-$KYWNMJVH?Hh@1n7OSu~QhSswL8x0=$<8QG_zepi_`y_79=nK=_ZP_`Em2UI*tyQoB+r{1QYZCpb?2OrgUw#oRH$?^Tj!Req>XiE#~B|~ z+%HB;=ic+R@px4Ld8mwpY;W^A%8%l8$@B@1m5n`TlKI6bz2mp*^^^1mK$COW$HOfp zUGTz-cN9?BGEp}5A!mDFjaiWa2_J2Iq8qj0mXzk; z66JBKRP{p%wN7XobR0YjhAuW9T1Gw3FDvR5dWJ8ElNYF94eF3ebu+QwKjtvVu4L zI9ip#mQ@4uqVdkl-TUQMb^XBJVLW(-$s;Nq;@5gr4`UfLgF$adIhd?rHOa%D);whv z=;krPp~@I+-Z|r#s3yCH+c1US?dnm+C*)r{m+86sTJusLdNu^sqLrfWed^ndHXH`m zd3#cOe3>w-ga(Dus_^ppG9AC>Iq{y%%CK+Cro_sqLCs{VLuK=dev>OL1dis4(PQ5R zcz)>DjEkfV+MO;~>VUlYF00SgfUo~@(&9$Iy2|G0T9BSP?&T22>K46D zL*~j#yJ?)^*%J3!16f)@Y2Z^kS*BzwfAQ7K96rFRIh>#$*$_Io;z>ux@}G98!fWR@ zGTFxv4r~v)Gsd|pF91*-eaZ3Qw1MH$K^7JhWIdX%o$2kCbvGDXy)a?@8T&1dY4`;L z4Kn+f%SSFWE_rpEpL9bnlmYq`D!6F%di<&Hh=+!VI~j)2mfil03T#jJ_s?}VV0_hp z7T9bWxc>Jm2Z0WMU?`Z$xE74Gu~%s{mW!d4uvKCx@WD+gPUQ zV0vQS(Ig++z=EHN)BR44*EDSWIyT~R4$FcF*VEY*8@l=218Q05D2$|fXKFhRgBIEE zdDFB}1dKkoO^7}{5crKX!p?dZWNz$m>1icsXG2N+((x0OIST9Zo^DW_tytvlwXGpn zs8?pJXjEG;T@qrZi%#h93?FP$!&P4JA(&H61tqQi=opRzNpm zkrG}$^t9&XduK*Qa1?355wd8G2CI6QEh@Ua>AsD;7oRUNLPb76m4HG3K?)wF~IyS3`fXuNM>${?wmB zpVz;?6_(Fiadfd{vUCBM*_kt$+F3J+IojI;9L(gc9n3{sEZyzR9o!_mOwFC#tQ{Q~ zP3-`#uK#tP3Q7~Q;4H|wjZHO8h7e4IuBxl&vz2w~D8)w=Wtg31zpZhz%+kzSzL*dV zwp@{WU4i;hJ7c2f1O;7Mz6qRKeASoIv0_bV=i@NMG*l<#+;INk-^`5w@}Dj~;k=|}qM1vq_P z|GpBGe_IKq|LNy9SJhKOQ$c=5L{Dv|Q_lZl=-ky*BFBJLW9&y_C|!vyM~rQx=!vun z?rZJQB5t}Dctmui5i31C_;_}CEn}_W%>oSXtt>@kE1=JW*4*v4tPp;O6 zmAk{)m!)}34pTWg8{i>($%NQ(Tl;QC@J@FfBoc%Gr&m560^kgSfodAFrIjF}aIw)X zoXZ`@IsMkc8_=w%-7`D6Y4e*CG8k%Ud=GXhsTR50jUnm+R*0A(O3UKFg0`K;qp1bl z7``HN=?39ic_kR|^R^~w-*pa?Vj#7|e9F1iRx{GN2?wK!xR1GW!qa=~pjJb-#u1K8 zeR?Y2i-pt}yJq;SCiVHODIvQJX|ZJaT8nO+(?HXbLefulKKgM^B(UIO1r+S=7;kLJ zcH}1J=Px2jsh3Tec&v8Jcbng8;V-`#*UHt?hB(pmOipKwf3Lz8rG$heEB30Sg*2rx zV<|KN86$soN(I!BwO`1n^^uF2*x&vJ$2d$>+`(romzHP|)K_KkO6Hc>_dwMW-M(#S zK(~SiXT1@fvc#U+?|?PniDRm01)f^#55;nhM|wi?oG>yBsa?~?^xTU|fX-R(sTA+5 zaq}-8Tx7zrOy#3*JLIIVsBmHYLdD}!0NP!+ITW+Thn0)8SS!$@)HXwB3tY!fMxc#1 zMp3H?q3eD?u&Njx4;KQ5G>32+GRp1Ee5qMO0lZjaRRu&{W<&~DoJNGkcYF<5(Ab+J zgO>VhBl{okDPn78<%&e2mR{jwVCz5Og;*Z;;3%VvoGo_;HaGLWYF7q#jDX=Z#Ml`H z858YVV$%J|e<1n`%6Vsvq7GmnAV0wW4$5qQ3uR@1i>tW{xrl|ExywIc?fNgYlA?C5 zh$ezAFb5{rQu6i7BSS5*J-|9DQ{6^BVQ{b*lq`xS@RyrsJN?-t=MTMPY;WYeKBCNg z^2|pN!Q^WPJuuO4!|P@jzt&tY1Y8d%FNK5xK(!@`jO2aEA*4 zkO6b|UVBipci?){-Ke=+1;mGlND8)6+P;8sq}UXw2hn;fc7nM>g}GSMWu&v&fqh

iViYT=fZ(|3Ox^$aWPp4a8h24tD<|8-!aK0lHgL$N7Efw}J zVIB!7=T$U`ao1?upi5V4Et*-lTG0XvExbf!ya{cua==$WJyVG(CmA6Of*8E@DSE%L z`V^$qz&RU$7G5mg;8;=#`@rRG`-uS18$0WPN@!v2d{H2sOqP|!(cQ@ zUHo!d>>yFArLPf1q`uBvY32miqShLT1B@gDL4XoVTK&@owOoD)OIHXrYK-a1d$B{v zF^}8D3Y^g%^cnvScOSJR5QNH+BI%d|;J;wWM3~l>${fb8DNPg)wrf|GBP8p%LNGN# z3EaIiItgwtGgT&iYCFy9-LG}bMI|4LdmmJt@V@% zb6B)1kc=T)(|L@0;wr<>=?r04N;E&ef+7C^`wPWtyQe(*pD1pI_&XHy|0gIGHMekd zF_*M4yi6J&Z4LQj65)S zXwdM{SwUo%3SbPwFsHgqF@V|6afT|R6?&S;lw=8% z3}@9B=#JI3@B*#4s!O))~z zc>2_4Q_#&+5V`GFd?88^;c1i7;Vv_I*qt!_Yx*n=;rj!82rrR2rQ8u5(Ejlo{15P% zs~!{%XJ>FmJ})H^I9bn^Re&38H{xA!0l3^89k(oU;bZWXM@kn$#aoS&Y4l^-WEn-fH39Jb9lA%s*WsKJQl?n9B7_~P z-XM&WL7Z!PcoF6_D>V@$CvUIEy=+Z&0kt{szMk=f1|M+r*a43^$$B^MidrT0J;RI` z(?f!O<8UZkm$_Ny$Hth1J#^4ni+im8M9mr&k|3cIgwvjAgjH z8`N&h25xV#v*d$qBX5jkI|xOhQn!>IYZK7l5#^P4M&twe9&Ey@@GxYMxBZq2e7?`q z$~Szs0!g{2fGcp9PZEt|rdQ6bhAgpcLHPz?f-vB?$dc*!9OL?Q8mn7->bFD2Si60* z!O%y)fCdMSV|lkF9w%x~J*A&srMyYY3{=&$}H zGQ4VG_?$2X(0|vT0{=;W$~icCI{b6W{B!Q8xdGhF|D{25G_5_+%s(46lhvNLkik~R z>nr(&C#5wwOzJZQo9m|U<;&Wk!_#q|V>fsmj1g<6%hB{jGoNUPjgJslld>xmODzGjYc?7JSuA?A_QzjDw5AsRgi@Y|Z0{F{!1=!NES-#*f^s4l0Hu zz468))2IY5dmD9pa*(yT5{EyP^G>@ZWumealS-*WeRcZ}B%gxq{MiJ|RyX-^C1V=0 z@iKdrGi1jTe8Ya^x7yyH$kBNvM4R~`fbPq$BzHum-3Zo8C6=KW@||>zsA8-Y9uV5V z#oq-f5L5}V<&wF4@X@<3^C%ptp6+Ce)~hGl`kwj)bsAjmo_GU^r940Z-|`<)oGnh7 zFF0Tde3>ui?8Yj{sF-Z@)yQd~CGZ*w-6p2U<8}JO-sRsVI5dBji`01W8A&3$?}lxBaC&vn0E$c5tW* zX>5(zzZ=qn&!J~KdsPl;P@bmA-Pr8T*)eh_+Dv5=Ma|XSle6t(k8qcgNyar{*ReQ8 zTXwi=8vr>!3Ywr+BhggHDw8ke==NTQVMCK`$69fhzEFB*4+H9LIvdt-#IbhZvpS}} zO3lz;P?zr0*0$%-Rq_y^k(?I{Mk}h@w}cZpMUp|ucs55bcloL2)($u%mXQw({Wzc~ z;6nu5MkjP)0C(@%6Q_I_vsWrfhl7Zpoxw#WoE~r&GOSCz;_ro6i(^hM>I$8y>`!wW z*U^@?B!MMmb89I}2(hcE4zN2G^kwyWCZp5JG>$Ez7zP~D=J^LMjSM)27_0B_X^C(M z`fFT+%DcKlu?^)FCK>QzSnV%IsXVcUFhFdBP!6~se&xxrIxsvySAWu++IrH;FbcY$ z2DWTvSBRfLwdhr0nMx+URA$j3i7_*6BWv#DXfym?ZRDcX9C?cY9sD3q)uBDR3uWg= z(lUIzB)G$Hr!){>E{s4Dew+tb9kvToZp-1&c?y2wn@Z~(VBhqz`cB;{E4(P3N2*nJ z_>~g@;UF2iG{Kt(<1PyePTKahF8<)pozZ*xH~U-kfoAayCwJViIrnqwqO}7{0pHw$ zs2Kx?s#vQr7XZ264>5RNKSL8|Ty^=PsIx^}QqOOcfpGUU4tRkUc|kc7-!Ae6!+B{o~7nFpm3|G5^=0#Bnm6`V}oSQlrX(u%OWnC zoLPy&Q;1Jui&7ST0~#+}I^&?vcE*t47~Xq#YwvA^6^} z`WkC)$AkNub|t@S!$8CBlwbV~?yp&@9h{D|3z-vJXgzRC5^nYm+PyPcgRzAnEi6Q^gslXYRv4nycsy-SJu?lMps-? zV`U*#WnFsdPLL)Q$AmD|0`UaC4ND07+&UmOu!eHruzV|OUox<+Jl|Mr@6~C`T@P%s zW7sgXLF2SSe9Fl^O(I*{9wsFSYb2l%-;&Pi^dpv!{)C3d0AlNY6!4fgmSgj_wQ*7Am7&$z;Jg&wgR-Ih;lUvWS|KTSg!&s_E9_bXBkZvGiC6bFKDWZxsD$*NZ#_8bl zG1P-#@?OQzED7@jlMJTH@V!6k;W>auvft)}g zhoV{7$q=*;=l{O>Q4a@ ziMjf_u*o^PsO)#BjC%0^h>Xp@;5$p{JSYDt)zbb}s{Kbt!T*I@Pk@X0zds6wsefuU zW$XY%yyRGC94=6mf?x+bbA5CDQ2AgW1T-jVAJbm7K(gp+;v6E0WI#kuACgV$r}6L? zd|Tj?^%^*N&b>Dd{Wr$FS2qI#Ucs1yd4N+RBUQiSZGujH`#I)mG&VKoDh=KKFl4=G z&MagXl6*<)$6P}*Tiebpz5L=oMaPrN+caUXRJ`D?=K9!e0f{@D&cZLKN?iNP@X0aF zE(^pl+;*T5qt?1jRC=5PMgV!XNITRLS_=9{CJExaQj;lt!&pdzpK?8p>%Mb+D z?yO*uSung=-`QQ@yX@Hyd4@CI^r{2oiu`%^bNkz+Nkk!IunjwNC|WcqvX~k=><-I3 zDQdbdb|!v+Iz01$w@aMl!R)koD77Xp;eZwzSl-AT zr@Vu{=xvgfq9akRrrM)}=!=xcs+U1JO}{t(avgz`6RqiiX<|hGG1pmop8k6Q+G_mv zJv|RfDheUp2L3=^C=4aCBMBn0aRCU(DQwX-W(RkRwmLeuJYF<0urcaf(=7)JPg<3P zQs!~G)9CT18o!J4{zX{_e}4eS)U-E)0FAt}wEI(c0%HkxgggW;(1E=>J17_hsH^sP z%lT0LGgbUXHx-K*CI-MCrP66UP0PvGqM$MkeLyqHdbgP|_Cm!7te~b8p+e6sQ_3k| zVcwTh6d83ltdnR>D^)BYQpDKlLk3g0Hdcgz2}%qUs9~~Rie)A-BV1mS&naYai#xcZ z(d{8=-LVpTp}2*y)|gR~;qc7fp26}lPcLZ#=JpYcn3AT9(UIdOyg+d(P5T7D&*P}# zQCYplZO5|7+r19%9e`v^vfSS1sbX1c%=w1;oyruXB%Kl$ACgKQ6=qNWLsc=28xJjg zwvsI5-%SGU|3p>&zXVl^vVtQT3o-#$UT9LI@Npz~6=4!>mc431VRNN8od&Ul^+G_kHC`G=6WVWM z%9eWNyy(FTO|A+@x}Ou3CH)oi;t#7rAxdIXfNFwOj_@Y&TGz6P_sqiB`Q6Lxy|Q{`|fgmRG(k+!#b*M+Z9zFce)f-7;?Km5O=LHV9f9_87; zF7%R2B+$?@sH&&-$@tzaPYkw0;=i|;vWdI|Wl3q_Zu>l;XdIw2FjV=;Mq5t1Q0|f< zs08j54Bp`3RzqE=2enlkZxmX6OF+@|2<)A^RNQpBd6o@OXl+i)zO%D4iGiQNuXd+zIR{_lb96{lc~bxsBveIw6umhShTX+3@ZJ=YHh@ zWY3(d0azg;7oHn>H<>?4@*RQbi>SmM=JrHvIG(~BrvI)#W(EAeO6fS+}mxxcc+X~W6&YVl86W9WFSS}Vz-f9vS?XUDBk)3TcF z8V?$4Q)`uKFq>xT=)Y9mMFVTUk*NIA!0$?RP6Ig0TBmUFrq*Q-Agq~DzxjStQyJ({ zBeZ;o5qUUKg=4Hypm|}>>L=XKsZ!F$yNTDO)jt4H0gdQ5$f|d&bnVCMMXhNh)~mN z@_UV6D7MVlsWz+zM+inZZp&P4fj=tm6fX)SG5H>OsQf_I8c~uGCig$GzuwViK54bcgL;VN|FnyQl>Ed7(@>=8$a_UKIz|V6CeVSd2(P z0Uu>A8A+muM%HLFJQ9UZ5c)BSAv_zH#1f02x?h9C}@pN@6{>UiAp>({Fn(T9Q8B z^`zB;kJ5b`>%dLm+Ol}ty!3;8f1XDSVX0AUe5P#@I+FQ-`$(a;zNgz)4x5hz$Hfbg z!Q(z26wHLXko(1`;(BAOg_wShpX0ixfWq3ponndY+u%1gyX)_h=v1zR#V}#q{au6; z!3K=7fQwnRfg6FXtNQmP>`<;!N137paFS%y?;lb1@BEdbvQHYC{976l`cLqn;b8lp zIDY>~m{gDj(wfnK!lpW6pli)HyLEiUrNc%eXTil|F2s(AY+LW5hkKb>TQ3|Q4S9rr zpDs4uK_co6XPsn_z$LeS{K4jFF`2>U`tbgKdyDne`xmR<@6AA+_hPNKCOR-Zqv;xk zu5!HsBUb^!4uJ7v0RuH-7?l?}b=w5lzzXJ~gZcxRKOovSk@|#V+MuX%Y+=;14i*%{)_gSW9(#4%)AV#3__kac1|qUy!uyP{>?U#5wYNq}y$S9pCc zFc~4mgSC*G~j0u#qqp9 z${>3HV~@->GqEhr_Xwoxq?Hjn#=s2;i~g^&Hn|aDKpA>Oc%HlW(KA1?BXqpxB;Ydx)w;2z^MpjJ(Qi(X!$5RC z*P{~%JGDQqojV>2JbEeCE*OEu!$XJ>bWA9Oa_Hd;y)F%MhBRi*LPcdqR8X`NQ&1L# z5#9L*@qxrx8n}LfeB^J{%-?SU{FCwiWyHp682F+|pa+CQa3ZLzBqN1{)h4d6+vBbV zC#NEbQLC;}me3eeYnOG*nXOJZEU$xLZ1<1Y=7r0(-U0P6-AqwMAM`a(Ed#7vJkn6plb4eI4?2y3yOTGmmDQ!z9`wzbf z_OY#0@5=bnep;MV0X_;;SJJWEf^E6Bd^tVJ9znWx&Ks8t*B>AM@?;D4oWUGc z!H*`6d7Cxo6VuyS4Eye&L1ZRhrRmN6Lr`{NL(wDbif|y&z)JN>Fl5#Wi&mMIr5i;x zBx}3YfF>>8EC(fYnmpu~)CYHuHCyr5*`ECap%t@y=jD>!_%3iiE|LN$mK9>- zHdtpy8fGZtkZF?%TW~29JIAfi2jZT8>OA7=h;8T{{k?c2`nCEx9$r zS+*&vt~2o^^J+}RDG@+9&M^K*z4p{5#IEVbz`1%`m5c2};aGt=V?~vIM}ZdPECDI)47|CWBCfDWUbxBCnmYivQ*0Nu_xb*C>~C9(VjHM zxe<*D<#dQ8TlpMX2c@M<9$w!RP$hpG4cs%AI){jp*Sj|*`m)5(Bw*A0$*i-(CA5#%>a)$+jI2C9r6|(>J8InryENI z$NohnxDUB;wAYDwrb*!N3noBTKPpPN}~09SEL18tkG zxgz(RYU_;DPT{l?Q$+eaZaxnsWCA^ds^0PVRkIM%bOd|G2IEBBiz{&^JtNsODs;5z zICt_Zj8wo^KT$7Bg4H+y!Df#3mbl%%?|EXe!&(Vmac1DJ*y~3+kRKAD=Ovde4^^%~ zw<9av18HLyrf*_>Slp;^i`Uy~`mvBjZ|?Ad63yQa#YK`4+c6;pW4?XIY9G1(Xh9WO8{F-Aju+nS9Vmv=$Ac0ienZ+p9*O%NG zMZKy5?%Z6TAJTE?o5vEr0r>f>hb#2w2U3DL64*au_@P!J!TL`oH2r*{>ffu6|A7tv zL4juf$DZ1MW5ZPsG!5)`k8d8c$J$o;%EIL0va9&GzWvkS%ZsGb#S(?{!UFOZ9<$a| zY|a+5kmD5N&{vRqkgY>aHsBT&`rg|&kezoD)gP0fsNYHsO#TRc_$n6Lf1Z{?+DLziXlHrq4sf(!>O{?Tj;Eh@%)+nRE_2VxbN&&%%caU#JDU%vL3}Cb zsb4AazPI{>8H&d=jUaZDS$-0^AxE@utGs;-Ez_F(qC9T=UZX=>ok2k2 ziTn{K?y~a5reD2A)P${NoI^>JXn>`IeArow(41c-Wm~)wiryEP(OS{YXWi7;%dG9v zI?mwu1MxD{yp_rrk!j^cKM)dc4@p4Ezyo%lRN|XyD}}>v=Xoib0gOcdXrQ^*61HNj z=NP|pd>@yfvr-=m{8$3A8TQGMTE7g=z!%yt`8`Bk-0MMwW~h^++;qyUP!J~ykh1GO z(FZ59xuFR$(WE;F@UUyE@Sp>`aVNjyj=Ty>_Vo}xf`e7`F;j-IgL5`1~-#70$9_=uBMq!2&1l zomRgpD58@)YYfvLtPW}{C5B35R;ZVvB<<#)x%srmc_S=A7F@DW8>QOEGwD6suhwCg z>Pa+YyULhmw%BA*4yjDp|2{!T98~<6Yfd(wo1mQ!KWwq0eg+6)o1>W~f~kL<-S+P@$wx*zeI|1t7z#Sxr5 zt6w+;YblPQNplq4Z#T$GLX#j6yldXAqj>4gAnnWtBICUnA&-dtnlh=t0Ho_vEKwV` z)DlJi#!@nkYV#$!)@>udAU*hF?V`2$Hf=V&6PP_|r#Iv*J$9)pF@X3`k;5})9^o4y z&)~?EjX5yX12O(BsFy-l6}nYeuKkiq`u9145&3Ssg^y{5G3Pse z9w(YVa0)N-fLaBq1`P!_#>SS(8fh_5!f{UrgZ~uEdeMJIz7DzI5!NHHqQtm~#CPij z?=N|J>nPR6_sL7!f4hD_|KH`vf8(Wpnj-(gPWH+ZvID}%?~68SwhPTC3u1_cB`otq z)U?6qo!ZLi5b>*KnYHWW=3F!p%h1;h{L&(Q&{qY6)_qxNfbP6E3yYpW!EO+IW3?@J z);4>g4gnl^8klu7uA>eGF6rIGSynacogr)KUwE_R4E5Xzi*Qir@b-jy55-JPC8c~( zo!W8y9OGZ&`xmc8;=4-U9=h{vCqfCNzYirONmGbRQlR`WWlgnY+1wCXbMz&NT~9*| z6@FrzP!LX&{no2!Ln_3|I==_4`@}V?4a;YZKTdw;vT<+K+z=uWbW(&bXEaWJ^W8Td z-3&1bY^Z*oM<=M}LVt>_j+p=2Iu7pZmbXrhQ_k)ysE9yXKygFNw$5hwDn(M>H+e1&9BM5!|81vd%r%vEm zqxY3?F@fb6O#5UunwgAHR9jp_W2zZ}NGp2%mTW@(hz7$^+a`A?mb8|_G*GNMJ) zjqegXQio=i@AINre&%ofexAr95aop5C+0MZ0m-l=MeO8m3epm7U%vZB8+I+C*iNFM z#T3l`gknX;D$-`2XT^Cg*vrv=RH+P;_dfF++cP?B_msQI4j+lt&rX2)3GaJx%W*Nn zkML%D{z5tpHH=dksQ*gzc|}gzW;lwAbxoR07VNgS*-c3d&8J|;@3t^ zVUz*J*&r7DFRuFVDCJDK8V9NN5hvpgGjwx+5n)qa;YCKe8TKtdnh{I7NU9BCN!0dq zczrBk8pE{{@vJa9ywR@mq*J=v+PG;?fwqlJVhijG!3VmIKs>9T6r7MJpC)m!Tc#>g zMtVsU>wbwFJEfwZ{vB|ZlttNe83)$iz`~#8UJ^r)lJ@HA&G#}W&ZH*;k{=TavpjWE z7hdyLZPf*X%Gm}i`Y{OGeeu^~nB8=`{r#TUrM-`;1cBvEd#d!kPqIgYySYhN-*1;L z^byj%Yi}Gx)Wnkosi337BKs}+5H5dth1JA{Ir-JKN$7zC)*}hqeoD(WfaUDPT>0`- z(6sa0AoIqASwF`>hP}^|)a_j2s^PQn*qVC{Q}htR z5-)duBFXT_V56-+UohKXlq~^6uf!6sA#ttk1o~*QEy_Y-S$gAvq47J9Vtk$5oA$Ct zYhYJ@8{hsC^98${!#Ho?4y5MCa7iGnfz}b9jE~h%EAAv~Qxu)_rAV;^cygV~5r_~?l=B`zObj7S=H=~$W zPtI_m%g$`kL_fVUk9J@>EiBH zOO&jtn~&`hIFMS5S`g8w94R4H40mdNUH4W@@XQk1sr17b{@y|JB*G9z1|CrQjd+GX z6+KyURG3;!*BQrentw{B2R&@2&`2}n(z-2&X7#r!{yg@Soy}cRD~j zj9@UBW+N|4HW4AWapy4wfUI- zZ`gSL6DUlgj*f1hSOGXG0IVH8HxK?o2|3HZ;KW{K+yPAlxtb)NV_2AwJm|E)FRs&& z=c^e7bvUsztY|+f^k7NXs$o1EUq>cR7C0$UKi6IooHWlK_#?IWDkvywnzg&ThWo^? z2O_N{5X39#?eV9l)xI(>@!vSB{DLt*oY!K1R8}_?%+0^C{d9a%N4 zoxHVT1&Lm|uDX%$QrBun5e-F`HJ^T$ zmzv)p@4ZHd_w9!%Hf9UYNvGCw2TTTbrj9pl+T9%-_-}L(tES>Or-}Z4F*{##n3~L~TuxjirGuIY#H7{%$E${?p{Q01 zi6T`n;rbK1yIB9jmQNycD~yZq&mbIsFWHo|ZAChSFPQa<(%d8mGw*V3fh|yFoxOOiWJd(qvVb!Z$b88cg->N=qO*4k~6;R==|9ihg&riu#P~s4Oap9O7f%crSr^rljeIfXDEg>wi)&v*a%7zpz<9w z*r!3q9J|390x`Zk;g$&OeN&ctp)VKRpDSV@kU2Q>jtok($Y-*x8_$2piTxun81@vt z!Vj?COa0fg2RPXMSIo26T=~0d`{oGP*eV+$!0I<(4azk&Vj3SiG=Q!6mX0p$z7I}; z9BJUFgT-K9MQQ-0@Z=^7R<{bn2Fm48endsSs`V7_@%8?Bxkqv>BDoVcj?K#dV#uUP zL1ND~?D-|VGKe3Rw_7-Idpht>H6XRLh*U7epS6byiGvJpr%d}XwfusjH9g;Z98H`x zyde%%5mhGOiL4wljCaWCk-&uE4_OOccb9c!ZaWt4B(wYl!?vyzl%7n~QepN&eFUrw zFIOl9c({``6~QD+43*_tzP{f2x41h(?b43^y6=iwyB)2os5hBE!@YUS5?N_tXd=h( z)WE286Fbd>R4M^P{!G)f;h<3Q>Fipuy+d2q-)!RyTgt;wr$(?9ox3;q+{E*ZQHhOn;lM`cjnu9 zXa48ks-v(~b*;MAI<>YZH(^NV8vjb34beE<_cwKlJoR;k6lJNSP6v}uiyRD?|0w+X@o1ONrH8a$fCxXpf? z?$DL0)7|X}Oc%h^zrMKWc-NS9I0Utu@>*j}b@tJ=ixQSJ={4@854wzW@E>VSL+Y{i z#0b=WpbCZS>kUCO_iQz)LoE>P5LIG-hv9E+oG}DtlIDF>$tJ1aw9^LuhLEHt?BCj& z(O4I8v1s#HUi5A>nIS-JK{v!7dJx)^Yg%XjNmlkWAq2*cv#tHgz`Y(bETc6CuO1VkN^L-L3j_x<4NqYb5rzrLC-7uOv z!5e`GZt%B782C5-fGnn*GhDF$%(qP<74Z}3xx+{$4cYKy2ikxI7B2N+2r07DN;|-T->nU&!=Cm#rZt%O_5c&1Z%nlWq3TKAW0w zQqemZw_ue--2uKQsx+niCUou?HjD`xhEjjQd3%rrBi82crq*~#uA4+>vR<_S{~5ce z-2EIl?~s z1=GVL{NxP1N3%=AOaC}j_Fv=ur&THz zyO!d9kHq|c73kpq`$+t+8Bw7MgeR5~`d7ChYyGCBWSteTB>8WAU(NPYt2Dk`@#+}= zI4SvLlyk#pBgVigEe`?NG*vl7V6m+<}%FwPV=~PvvA)=#ths==DRTDEYh4V5}Cf$z@#;< zyWfLY_5sP$gc3LLl2x+Ii)#b2nhNXJ{R~vk`s5U7Nyu^3yFg&D%Txwj6QezMX`V(x z=C`{76*mNb!qHHs)#GgGZ_7|vkt9izl_&PBrsu@}L`X{95-2jf99K)0=*N)VxBX2q z((vkpP2RneSIiIUEnGb?VqbMb=Zia+rF~+iqslydE34cSLJ&BJW^3knX@M;t*b=EA zNvGzv41Ld_T+WT#XjDB840vovUU^FtN_)G}7v)1lPetgpEK9YS^OWFkPoE{ovj^=@ zO9N$S=G$1ecndT_=5ehth2Lmd1II-PuT~C9`XVePw$y8J#dpZ?Tss<6wtVglm(Ok7 z3?^oi@pPio6l&!z8JY(pJvG=*pI?GIOu}e^EB6QYk$#FJQ%^AIK$I4epJ+9t?KjqA+bkj&PQ*|vLttme+`9G=L% ziadyMw_7-M)hS(3E$QGNCu|o23|%O+VN7;Qggp?PB3K-iSeBa2b}V4_wY`G1Jsfz4 z9|SdB^;|I8E8gWqHKx!vj_@SMY^hLEIbSMCuE?WKq=c2mJK z8LoG-pnY!uhqFv&L?yEuxo{dpMTsmCn)95xanqBrNPTgXP((H$9N${Ow~Is-FBg%h z53;|Y5$MUN)9W2HBe2TD`ct^LHI<(xWrw}$qSoei?}s)&w$;&!14w6B6>Yr6Y8b)S z0r71`WmAvJJ`1h&poLftLUS6Ir zC$bG9!Im_4Zjse)#K=oJM9mHW1{%l8sz$1o?ltdKlLTxWWPB>Vk22czVt|1%^wnN@*!l)}?EgtvhC>vlHm^t+ogpgHI1_$1ox9e;>0!+b(tBrmXRB`PY1vp-R**8N7 zGP|QqI$m(Rdu#=(?!(N}G9QhQ%o!aXE=aN{&wtGP8|_qh+7a_j_sU5|J^)vxq;# zjvzLn%_QPHZZIWu1&mRAj;Sa_97p_lLq_{~j!M9N^1yp3U_SxRqK&JnR%6VI#^E12 z>CdOVI^_9aPK2eZ4h&^{pQs}xsijXgFYRIxJ~N7&BB9jUR1fm!(xl)mvy|3e6-B3j zJn#ajL;bFTYJ2+Q)tDjx=3IklO@Q+FFM}6UJr6km7hj7th9n_&JR7fnqC!hTZoM~T zBeaVFp%)0cbPhejX<8pf5HyRUj2>aXnXBqDJe73~J%P(2C?-RT{c3NjE`)om! zl$uewSgWkE66$Kb34+QZZvRn`fob~Cl9=cRk@Es}KQm=?E~CE%spXaMO6YmrMl%9Q zlA3Q$3|L1QJ4?->UjT&CBd!~ru{Ih^in&JXO=|<6J!&qp zRe*OZ*cj5bHYlz!!~iEKcuE|;U4vN1rk$xq6>bUWD*u(V@8sG^7>kVuo(QL@Ki;yL zWC!FT(q{E8#on>%1iAS0HMZDJg{Z{^!De(vSIq&;1$+b)oRMwA3nc3mdTSG#3uYO_ z>+x;7p4I;uHz?ZB>dA-BKl+t-3IB!jBRgdvAbW!aJ(Q{aT>+iz?91`C-xbe)IBoND z9_Xth{6?(y3rddwY$GD65IT#f3<(0o#`di{sh2gm{dw*#-Vnc3r=4==&PU^hCv$qd zjw;>i&?L*Wq#TxG$mFIUf>eK+170KG;~+o&1;Tom9}}mKo23KwdEM6UonXgc z!6N(@k8q@HPw{O8O!lAyi{rZv|DpgfU{py+j(X_cwpKqcalcqKIr0kM^%Br3SdeD> zHSKV94Yxw;pjzDHo!Q?8^0bb%L|wC;4U^9I#pd5O&eexX+Im{ z?jKnCcsE|H?{uGMqVie_C~w7GX)kYGWAg%-?8|N_1#W-|4F)3YTDC+QSq1s!DnOML3@d`mG%o2YbYd#jww|jD$gotpa)kntakp#K;+yo-_ZF9qrNZw<%#C zuPE@#3RocLgPyiBZ+R_-FJ_$xP!RzWm|aN)S+{$LY9vvN+IW~Kf3TsEIvP+B9Mtm! zpfNNxObWQpLoaO&cJh5>%slZnHl_Q~(-Tfh!DMz(dTWld@LG1VRF`9`DYKhyNv z2pU|UZ$#_yUx_B_|MxUq^glT}O5Xt(Vm4Mr02><%C)@v;vPb@pT$*yzJ4aPc_FZ3z z3}PLoMBIM>q_9U2rl^sGhk1VUJ89=*?7|v`{!Z{6bqFMq(mYiA?%KbsI~JwuqVA9$H5vDE+VocjX+G^%bieqx->s;XWlKcuv(s%y%D5Xbc9+ zc(_2nYS1&^yL*ey664&4`IoOeDIig}y-E~_GS?m;D!xv5-xwz+G`5l6V+}CpeJDi^ z%4ed$qowm88=iYG+(`ld5Uh&>Dgs4uPHSJ^TngXP_V6fPyl~>2bhi20QB%lSd#yYn zO05?KT1z@?^-bqO8Cg`;ft>ilejsw@2%RR7;`$Vs;FmO(Yr3Fp`pHGr@P2hC%QcA|X&N2Dn zYf`MqXdHi%cGR@%y7Rg7?d3?an){s$zA{!H;Ie5exE#c~@NhQUFG8V=SQh%UxUeiV zd7#UcYqD=lk-}sEwlpu&H^T_V0{#G?lZMxL7ih_&{(g)MWBnCZxtXg znr#}>U^6!jA%e}@Gj49LWG@*&t0V>Cxc3?oO7LSG%~)Y5}f7vqUUnQ;STjdDU}P9IF9d9<$;=QaXc zL1^X7>fa^jHBu_}9}J~#-oz3Oq^JmGR#?GO7b9a(=R@fw@}Q{{@`Wy1vIQ#Bw?>@X z-_RGG@wt|%u`XUc%W{J z>iSeiz8C3H7@St3mOr_mU+&bL#Uif;+Xw-aZdNYUpdf>Rvu0i0t6k*}vwU`XNO2he z%miH|1tQ8~ZK!zmL&wa3E;l?!!XzgV#%PMVU!0xrDsNNZUWKlbiOjzH-1Uoxm8E#r`#2Sz;-o&qcqB zC-O_R{QGuynW14@)7&@yw1U}uP(1cov)twxeLus0s|7ayrtT8c#`&2~Fiu2=R;1_4bCaD=*E@cYI>7YSnt)nQc zohw5CsK%m?8Ack)qNx`W0_v$5S}nO|(V|RZKBD+btO?JXe|~^Qqur%@eO~<8-L^9d z=GA3-V14ng9L29~XJ>a5k~xT2152zLhM*@zlp2P5Eu}bywkcqR;ISbas&#T#;HZSf z2m69qTV(V@EkY(1Dk3`}j)JMo%ZVJ*5eB zYOjIisi+igK0#yW*gBGj?@I{~mUOvRFQR^pJbEbzFxTubnrw(Muk%}jI+vXmJ;{Q6 zrSobKD>T%}jV4Ub?L1+MGOD~0Ir%-`iTnWZN^~YPrcP5y3VMAzQ+&en^VzKEb$K!Q z<7Dbg&DNXuow*eD5yMr+#08nF!;%4vGrJI++5HdCFcGLfMW!KS*Oi@=7hFwDG!h2< zPunUEAF+HncQkbfFj&pbzp|MU*~60Z(|Ik%Tn{BXMN!hZOosNIseT?R;A`W?=d?5X zK(FB=9mZusYahp|K-wyb={rOpdn=@;4YI2W0EcbMKyo~-#^?h`BA9~o285%oY zfifCh5Lk$SY@|2A@a!T2V+{^!psQkx4?x0HSV`(w9{l75QxMk!)U52Lbhn{8ol?S) zCKo*7R(z!uk<6*qO=wh!Pul{(qq6g6xW;X68GI_CXp`XwO zxuSgPRAtM8K7}5E#-GM!*ydOOG_{A{)hkCII<|2=ma*71ci_-}VPARm3crFQjLYV! z9zbz82$|l01mv`$WahE2$=fAGWkd^X2kY(J7iz}WGS z@%MyBEO=A?HB9=^?nX`@nh;7;laAjs+fbo!|K^mE!tOB>$2a_O0y-*uaIn8k^6Y zSbuv;5~##*4Y~+y7Z5O*3w4qgI5V^17u*ZeupVGH^nM&$qmAk|anf*>r zWc5CV;-JY-Z@Uq1Irpb^O`L_7AGiqd*YpGUShb==os$uN3yYvb`wm6d=?T*it&pDk zo`vhw)RZX|91^^Wa_ti2zBFyWy4cJu#g)_S6~jT}CC{DJ_kKpT`$oAL%b^!2M;JgT zM3ZNbUB?}kP(*YYvXDIH8^7LUxz5oE%kMhF!rnPqv!GiY0o}NR$OD=ITDo9r%4E>E0Y^R(rS^~XjWyVI6 zMOR5rPXhTp*G*M&X#NTL`Hu*R+u*QNoiOKg4CtNPrjgH>c?Hi4MUG#I917fx**+pJfOo!zFM&*da&G_x)L(`k&TPI*t3e^{crd zX<4I$5nBQ8Ax_lmNRa~E*zS-R0sxkz`|>7q_?*e%7bxqNm3_eRG#1ae3gtV9!fQpY z+!^a38o4ZGy9!J5sylDxZTx$JmG!wg7;>&5H1)>f4dXj;B+@6tMlL=)cLl={jLMxY zbbf1ax3S4>bwB9-$;SN2?+GULu;UA-35;VY*^9Blx)Jwyb$=U!D>HhB&=jSsd^6yw zL)?a|>GxU!W}ocTC(?-%z3!IUhw^uzc`Vz_g>-tv)(XA#JK^)ZnC|l1`@CdX1@|!| z_9gQ)7uOf?cR@KDp97*>6X|;t@Y`k_N@)aH7gY27)COv^P3ya9I{4z~vUjLR9~z1Z z5=G{mVtKH*&$*t0@}-i_v|3B$AHHYale7>E+jP`ClqG%L{u;*ff_h@)al?RuL7tOO z->;I}>%WI{;vbLP3VIQ^iA$4wl6@0sDj|~112Y4OFjMs`13!$JGkp%b&E8QzJw_L5 zOnw9joc0^;O%OpF$Qp)W1HI!$4BaXX84`%@#^dk^hFp^pQ@rx4g(8Xjy#!X%+X5Jd@fs3amGT`}mhq#L97R>OwT5-m|h#yT_-v@(k$q7P*9X~T*3)LTdzP!*B} z+SldbVWrrwQo9wX*%FyK+sRXTa@O?WM^FGWOE?S`R(0P{<6p#f?0NJvnBia?k^fX2 zNQs7K-?EijgHJY}&zsr;qJ<*PCZUd*x|dD=IQPUK_nn)@X4KWtqoJNHkT?ZWL_hF? zS8lp2(q>;RXR|F;1O}EE#}gCrY~#n^O`_I&?&z5~7N;zL0)3Tup`%)oHMK-^r$NT% zbFg|o?b9w(q@)6w5V%si<$!U<#}s#x@0aX-hP>zwS#9*75VXA4K*%gUc>+yzupTDBOKH8WR4V0pM(HrfbQ&eJ79>HdCvE=F z|J>s;;iDLB^3(9}?biKbxf1$lI!*Z%*0&8UUq}wMyPs_hclyQQi4;NUY+x2qy|0J; zhn8;5)4ED1oHwg+VZF|80<4MrL97tGGXc5Sw$wAI#|2*cvQ=jB5+{AjMiDHmhUC*a zlmiZ`LAuAn_}hftXh;`Kq0zblDk8?O-`tnilIh|;3lZp@F_osJUV9`*R29M?7H{Fy z`nfVEIDIWXmU&YW;NjU8)EJpXhxe5t+scf|VXM!^bBlwNh)~7|3?fWwo_~ZFk(22% zTMesYw+LNx3J-_|DM~`v93yXe=jPD{q;li;5PD?Dyk+b? zo21|XpT@)$BM$%F=P9J19Vi&1#{jM3!^Y&fr&_`toi`XB1!n>sbL%U9I5<7!@?t)~ z;&H%z>bAaQ4f$wIzkjH70;<8tpUoxzKrPhn#IQfS%9l5=Iu))^XC<58D!-O z{B+o5R^Z21H0T9JQ5gNJnqh#qH^na|z92=hONIM~@_iuOi|F>jBh-?aA20}Qx~EpDGElELNn~|7WRXRFnw+Wdo`|# zBpU=Cz3z%cUJ0mx_1($X<40XEIYz(`noWeO+x#yb_pwj6)R(__%@_Cf>txOQ74wSJ z0#F3(zWWaR-jMEY$7C*3HJrohc79>MCUu26mfYN)f4M~4gD`}EX4e}A!U}QV8!S47 z6y-U-%+h`1n`*pQuKE%Av0@)+wBZr9mH}@vH@i{v(m-6QK7Ncf17x_D=)32`FOjjo zg|^VPf5c6-!FxN{25dvVh#fog=NNpXz zfB$o+0jbRkHH{!TKhE709f+jI^$3#v1Nmf80w`@7-5$1Iv_`)W^px8P-({xwb;D0y z7LKDAHgX<84?l!I*Dvi2#D@oAE^J|g$3!)x1Ua;_;<@#l1fD}lqU2_tS^6Ht$1Wl} zBESo7o^)9-Tjuz$8YQSGhfs{BQV6zW7dA?0b(Dbt=UnQs&4zHfe_sj{RJ4uS-vQpC zX;Bbsuju4%!o8?&m4UZU@~ZZjeFF6ex2ss5_60_JS_|iNc+R0GIjH1@Z z=rLT9%B|WWgOrR7IiIwr2=T;Ne?30M!@{%Qf8o`!>=s<2CBpCK_TWc(DX51>e^xh8 z&@$^b6CgOd7KXQV&Y4%}_#uN*mbanXq(2=Nj`L7H7*k(6F8s6{FOw@(DzU`4-*77{ zF+dxpv}%mFpYK?>N_2*#Y?oB*qEKB}VoQ@bzm>ptmVS_EC(#}Lxxx730trt0G)#$b zE=wVvtqOct1%*9}U{q<)2?{+0TzZzP0jgf9*)arV)*e!f`|jgT{7_9iS@e)recI#z zbzolURQ+TOzE!ymqvBY7+5NnAbWxvMLsLTwEbFqW=CPyCsmJ}P1^V30|D5E|p3BC5 z)3|qgw@ra7aXb-wsa|l^in~1_fm{7bS9jhVRkYVO#U{qMp z)Wce+|DJ}4<2gp8r0_xfZpMo#{Hl2MfjLcZdRB9(B(A(f;+4s*FxV{1F|4d`*sRNd zp4#@sEY|?^FIJ;tmH{@keZ$P(sLh5IdOk@k^0uB^BWr@pk6mHy$qf&~rI>P*a;h0C{%oA*i!VjWn&D~O#MxN&f@1Po# zKN+ zrGrkSjcr?^R#nGl<#Q722^wbYcgW@{+6CBS<1@%dPA8HC!~a`jTz<`g_l5N1M@9wn9GOAZ>nqNgq!yOCbZ@1z`U_N`Z>}+1HIZxk*5RDc&rd5{3qjRh8QmT$VyS;jK z;AF+r6XnnCp=wQYoG|rT2@8&IvKq*IB_WvS%nt%e{MCFm`&W*#LXc|HrD?nVBo=(8*=Aq?u$sDA_sC_RPDUiQ+wnIJET8vx$&fxkW~kP9qXKt zozR)@xGC!P)CTkjeWvXW5&@2?)qt)jiYWWBU?AUtzAN}{JE1I)dfz~7$;}~BmQF`k zpn11qmObXwRB8&rnEG*#4Xax3XBkKlw(;tb?Np^i+H8m(Wyz9k{~ogba@laiEk;2! zV*QV^6g6(QG%vX5Um#^sT&_e`B1pBW5yVth~xUs#0}nv?~C#l?W+9Lsb_5)!71rirGvY zTIJ$OPOY516Y|_014sNv+Z8cc5t_V=i>lWV=vNu#!58y9Zl&GsMEW#pPYPYGHQ|;vFvd*9eM==$_=vc7xnyz0~ zY}r??$<`wAO?JQk@?RGvkWVJlq2dk9vB(yV^vm{=NVI8dhsX<)O(#nr9YD?I?(VmQ z^r7VfUBn<~p3()8yOBjm$#KWx!5hRW)5Jl7wY@ky9lNM^jaT##8QGVsYeaVywmpv>X|Xj7gWE1Ezai&wVLt3p)k4w~yrskT-!PR!kiyQlaxl(( zXhF%Q9x}1TMt3~u@|#wWm-Vq?ZerK={8@~&@9r5JW}r#45#rWii};t`{5#&3$W)|@ zbAf2yDNe0q}NEUvq_Quq3cTjcw z@H_;$hu&xllCI9CFDLuScEMg|x{S7GdV8<&Mq=ezDnRZAyX-8gv97YTm0bg=d)(>N z+B2FcqvI9>jGtnK%eO%y zoBPkJTk%y`8TLf4)IXPBn`U|9>O~WL2C~C$z~9|0m*YH<-vg2CD^SX#&)B4ngOSG$ zV^wmy_iQk>dfN@Pv(ckfy&#ak@MLC7&Q6Ro#!ezM*VEh`+b3Jt%m(^T&p&WJ2Oqvj zs-4nq0TW6cv~(YI$n0UkfwN}kg3_fp?(ijSV#tR9L0}l2qjc7W?i*q01=St0eZ=4h zyGQbEw`9OEH>NMuIe)hVwYHsGERWOD;JxEiO7cQv%pFCeR+IyhwQ|y@&^24k+|8fD zLiOWFNJ2&vu2&`Jv96_z-Cd5RLgmeY3*4rDOQo?Jm`;I_(+ejsPM03!ly!*Cu}Cco zrQSrEDHNyzT(D5s1rZq!8#?f6@v6dB7a-aWs(Qk>N?UGAo{gytlh$%_IhyL7h?DLXDGx zgxGEBQoCAWo-$LRvM=F5MTle`M})t3vVv;2j0HZY&G z22^iGhV@uaJh(XyyY%} zd4iH_UfdV#T=3n}(Lj^|n;O4|$;xhu*8T3hR1mc_A}fK}jfZ7LX~*n5+`8N2q#rI$ z@<_2VANlYF$vIH$ zl<)+*tIWW78IIINA7Rr7i{<;#^yzxoLNkXL)eSs=%|P>$YQIh+ea_3k z_s7r4%j7%&*NHSl?R4k%1>Z=M9o#zxY!n8sL5>BO-ZP;T3Gut>iLS@U%IBrX6BA3k z)&@q}V8a{X<5B}K5s(c(LQ=%v1ocr`t$EqqY0EqVjr65usa=0bkf|O#ky{j3)WBR(((L^wmyHRzoWuL2~WTC=`yZ zn%VX`L=|Ok0v7?s>IHg?yArBcync5rG#^+u)>a%qjES%dRZoIyA8gQ;StH z1Ao7{<&}6U=5}4v<)1T7t!J_CL%U}CKNs-0xWoTTeqj{5{?Be$L0_tk>M9o8 zo371}S#30rKZFM{`H_(L`EM9DGp+Mifk&IP|C2Zu_)Ghr4Qtpmkm1osCf@%Z$%t+7 zYH$Cr)Ro@3-QDeQJ8m+x6%;?YYT;k6Z0E-?kr>x33`H%*ueBD7Zx~3&HtWn0?2Wt} zTG}*|v?{$ajzt}xPzV%lL1t-URi8*Zn)YljXNGDb>;!905Td|mpa@mHjIH%VIiGx- zd@MqhpYFu4_?y5N4xiHn3vX&|e6r~Xt> zZG`aGq|yTNjv;9E+Txuoa@A(9V7g?1_T5FzRI;!=NP1Kqou1z5?%X~Wwb{trRfd>i z8&y^H)8YnKyA_Fyx>}RNmQIczT?w2J4SNvI{5J&}Wto|8FR(W;Qw#b1G<1%#tmYzQ zQ2mZA-PAdi%RQOhkHy9Ea#TPSw?WxwL@H@cbkZwIq0B!@ns}niALidmn&W?!Vd4Gj zO7FiuV4*6Mr^2xlFSvM;Cp_#r8UaqIzHJQg_z^rEJw&OMm_8NGAY2)rKvki|o1bH~ z$2IbfVeY2L(^*rMRU1lM5Y_sgrDS`Z??nR2lX;zyR=c%UyGb*%TC-Dil?SihkjrQy~TMv6;BMs7P8il`H7DmpVm@rJ;b)hW)BL)GjS154b*xq-NXq2cwE z^;VP7ua2pxvCmxrnqUYQMH%a%nHmwmI33nJM(>4LznvY*k&C0{8f*%?zggpDgkuz&JBx{9mfb@wegEl2v!=}Sq2Gaty0<)UrOT0{MZtZ~j5y&w zXlYa_jY)I_+VA-^#mEox#+G>UgvM!Ac8zI<%JRXM_73Q!#i3O|)lOP*qBeJG#BST0 zqohi)O!|$|2SeJQo(w6w7%*92S})XfnhrH_Z8qe!G5>CglP=nI7JAOW?(Z29;pXJ9 zR9`KzQ=WEhy*)WH>$;7Cdz|>*i>=##0bB)oU0OR>>N<21e4rMCHDemNi2LD>Nc$;& zQRFthpWniC1J6@Zh~iJCoLOxN`oCKD5Q4r%ynwgUKPlIEd#?QViIqovY|czyK8>6B zSP%{2-<;%;1`#0mG^B(8KbtXF;Nf>K#Di72UWE4gQ%(_26Koiad)q$xRL~?pN71ZZ zujaaCx~jXjygw;rI!WB=xrOJO6HJ!!w}7eiivtCg5K|F6$EXa)=xUC za^JXSX98W`7g-tm@uo|BKj39Dl;sg5ta;4qjo^pCh~{-HdLl6qI9Ix6f$+qiZ$}s= zNguKrU;u+T@ko(Vr1>)Q%h$?UKXCY>3se%&;h2osl2D zE4A9bd7_|^njDd)6cI*FupHpE3){4NQ*$k*cOWZ_?CZ>Z4_fl@n(mMnYK62Q1d@+I zr&O))G4hMihgBqRIAJkLdk(p(D~X{-oBUA+If@B}j& zsHbeJ3RzTq96lB7d($h$xTeZ^gP0c{t!Y0c)aQE;$FY2!mACg!GDEMKXFOPI^)nHZ z`aSPJpvV0|bbrzhWWkuPURlDeN%VT8tndV8?d)eN*i4I@u zVKl^6{?}A?P)Fsy?3oi#clf}L18t;TjNI2>eI&(ezDK7RyqFxcv%>?oxUlonv(px) z$vnPzRH`y5A(x!yOIfL0bmgeMQB$H5wenx~!ujQK*nUBW;@Em&6Xv2%s(~H5WcU2R z;%Nw<$tI)a`Ve!>x+qegJnQsN2N7HaKzrFqM>`6R*gvh%O*-%THt zrB$Nk;lE;z{s{r^PPm5qz(&lM{sO*g+W{sK+m3M_z=4=&CC>T`{X}1Vg2PEfSj2x_ zmT*(x;ov%3F?qoEeeM>dUn$a*?SIGyO8m806J1W1o+4HRhc2`9$s6hM#qAm zChQ87b~GEw{ADfs+5}FJ8+|bIlIv(jT$Ap#hSHoXdd9#w<#cA<1Rkq^*EEkknUd4& zoIWIY)sAswy6fSERVm&!SO~#iN$OgOX*{9@_BWFyJTvC%S++ilSfCrO(?u=Dc?CXZ zzCG&0yVR{Z`|ZF0eEApWEo#s9osV>F{uK{QA@BES#&;#KsScf>y zvs?vIbI>VrT<*!;XmQS=bhq%46-aambZ(8KU-wOO2=en~D}MCToB_u;Yz{)1ySrPZ z@=$}EvjTdzTWU7c0ZI6L8=yP+YRD_eMMos}b5vY^S*~VZysrkq<`cK3>>v%uy7jgq z0ilW9KjVDHLv0b<1K_`1IkbTOINs0=m-22c%M~l=^S}%hbli-3?BnNq?b`hx^HX2J zIe6ECljRL0uBWb`%{EA=%!i^4sMcj+U_TaTZRb+~GOk z^ZW!nky0n*Wb*r+Q|9H@ml@Z5gU&W`(z4-j!OzC1wOke`TRAYGZVl$PmQ16{3196( zO*?`--I}Qf(2HIwb2&1FB^!faPA2=sLg(@6P4mN)>Dc3i(B0;@O-y2;lM4akD>@^v z=u>*|!s&9zem70g7zfw9FXl1bpJW(C#5w#uy5!V?Q(U35A~$dR%LDVnq@}kQm13{} zd53q3N(s$Eu{R}k2esbftfjfOITCL;jWa$}(mmm}d(&7JZ6d3%IABCapFFYjdEjdK z&4Edqf$G^MNAtL=uCDRs&Fu@FXRgX{*0<(@c3|PNHa>L%zvxWS={L8%qw`STm+=Rd zA}FLspESSIpE_^41~#5yI2bJ=9`oc;GIL!JuW&7YetZ?0H}$$%8rW@*J37L-~Rsx!)8($nI4 zZhcZ2^=Y+p4YPl%j!nFJA|*M^gc(0o$i3nlphe+~-_m}jVkRN{spFs(o0ajW@f3K{ zDV!#BwL322CET$}Y}^0ixYj2w>&Xh12|R8&yEw|wLDvF!lZ#dOTHM9pK6@Nm-@9Lnng4ZHBgBSrr7KI8YCC9DX5Kg|`HsiwJHg2(7#nS;A{b3tVO?Z% za{m5b3rFV6EpX;=;n#wltDv1LE*|g5pQ+OY&*6qCJZc5oDS6Z6JD#6F)bWxZSF@q% z+1WV;m!lRB!n^PC>RgQCI#D1br_o^#iPk>;K2hB~0^<~)?p}LG%kigm@moD#q3PE+ zA^Qca)(xnqw6x>XFhV6ku9r$E>bWNrVH9fum0?4s?Rn2LG{Vm_+QJHse6xa%nzQ?k zKug4PW~#Gtb;#5+9!QBgyB@q=sk9=$S{4T>wjFICStOM?__fr+Kei1 z3j~xPqW;W@YkiUM;HngG!;>@AITg}vAE`M2Pj9Irl4w1fo4w<|Bu!%rh%a(Ai^Zhi zs92>v5;@Y(Zi#RI*ua*h`d_7;byQSa*v9E{2x$<-_=5Z<7{%)}4XExANcz@rK69T0x3%H<@frW>RA8^swA+^a(FxK| zFl3LD*ImHN=XDUkrRhp6RY5$rQ{bRgSO*(vEHYV)3Mo6Jy3puiLmU&g82p{qr0F?ohmbz)f2r{X2|T2 z$4fdQ=>0BeKbiVM!e-lIIs8wVTuC_m7}y4A_%ikI;Wm5$9j(^Y z(cD%U%k)X>_>9~t8;pGzL6L-fmQO@K; zo&vQzMlgY95;1BSkngY)e{`n0!NfVgf}2mB3t}D9@*N;FQ{HZ3Pb%BK6;5#-O|WI( zb6h@qTLU~AbVW#_6?c!?Dj65Now7*pU{h!1+eCV^KCuPAGs28~3k@ueL5+u|Z-7}t z9|lskE`4B7W8wMs@xJa{#bsCGDFoRSNSnmNYB&U7 zVGKWe%+kFB6kb)e;TyHfqtU6~fRg)f|>=5(N36)0+C z`hv65J<$B}WUc!wFAb^QtY31yNleq4dzmG`1wHTj=c*=hay9iD071Hc?oYoUk|M*_ zU1GihAMBsM@5rUJ(qS?9ZYJ6@{bNqJ`2Mr+5#hKf?doa?F|+^IR!8lq9)wS3tF_9n zW_?hm)G(M+MYb?V9YoX^_mu5h-LP^TL^!Q9Z7|@sO(rg_4+@=PdI)WL(B7`!K^ND- z-uIuVDCVEdH_C@c71YGYT^_Scf_dhB8Z2Xy6vGtBSlYud9vggOqv^L~F{BraSE_t} zIkP+Hp2&nH^-MNEs}^`oMLy11`PQW$T|K(`Bu*(f@)mv1-qY(_YG&J2M2<7k;;RK~ zL{Fqj9yCz8(S{}@c)S!65aF<=&eLI{hAMErCx&>i7OeDN>okvegO87OaG{Jmi<|}D zaT@b|0X{d@OIJ7zvT>r+eTzgLq~|Dpu)Z&db-P4z*`M$UL51lf>FLlq6rfG)%doyp z)3kk_YIM!03eQ8Vu_2fg{+osaEJPtJ-s36R+5_AEG12`NG)IQ#TF9c@$99%0iye+ zUzZ57=m2)$D(5Nx!n)=5Au&O0BBgwxIBaeI(mro$#&UGCr<;C{UjJVAbVi%|+WP(a zL$U@TYCxJ=1{Z~}rnW;7UVb7+ZnzgmrogDxhjLGo>c~MiJAWs&&;AGg@%U?Y^0JhL ze(x6Z74JG6FlOFK(T}SXQfhr}RIFl@QXKnIcXYF)5|V~e-}suHILKT-k|<*~Ij|VF zC;t@=uj=hot~*!C68G8hTA%8SzOfETOXQ|3FSaIEjvBJp(A)7SWUi5!Eu#yWgY+;n zlm<$+UDou*V+246_o#V4kMdto8hF%%Lki#zPh}KYXmMf?hrN0;>Mv%`@{0Qn`Ujp) z=lZe+13>^Q!9zT);H<(#bIeRWz%#*}sgUX9P|9($kexOyKIOc`dLux}c$7It4u|Rl z6SSkY*V~g_B-hMPo_ak>>z@AVQ(_N)VY2kB3IZ0G(iDUYw+2d7W^~(Jq}KY=JnWS( z#rzEa&0uNhJ>QE8iiyz;n2H|SV#Og+wEZv=f2%1ELX!SX-(d3tEj$5$1}70Mp<&eI zCkfbByL7af=qQE@5vDVxx1}FSGt_a1DoE3SDI+G)mBAna)KBG4p8Epxl9QZ4BfdAN zFnF|Y(umr;gRgG6NLQ$?ZWgllEeeq~z^ZS7L?<(~O&$5|y)Al^iMKy}&W+eMm1W z7EMU)u^ke(A1#XCV>CZ71}P}0x)4wtHO8#JRG3MA-6g=`ZM!FcICCZ{IEw8Dm2&LQ z1|r)BUG^0GzI6f946RrBlfB1Vs)~8toZf~7)+G;pv&XiUO(%5bm)pl=p>nV^o*;&T z;}@oZSibzto$arQgfkp|z4Z($P>dTXE{4O=vY0!)kDO* zGF8a4wq#VaFpLfK!iELy@?-SeRrdz%F*}hjKcA*y@mj~VD3!it9lhRhX}5YOaR9$} z3mS%$2Be7{l(+MVx3 z(4?h;P!jnRmX9J9sYN#7i=iyj_5q7n#X(!cdqI2lnr8T$IfOW<_v`eB!d9xY1P=2q&WtOXY=D9QYteP)De?S4}FK6#6Ma z=E*V+#s8>L;8aVroK^6iKo=MH{4yEZ_>N-N z`(|;aOATba1^asjxlILk<4}f~`39dBFlxj>Dw(hMYKPO3EEt1@S`1lxFNM+J@uB7T zZ8WKjz7HF1-5&2=l=fqF-*@>n5J}jIxdDwpT?oKM3s8Nr`x8JnN-kCE?~aM1H!hAE z%%w(3kHfGwMnMmNj(SU(w42OrC-euI>Dsjk&jz3ts}WHqmMpzQ3vZrsXrZ|}+MHA7 z068obeXZTsO*6RS@o3x80E4ok``rV^Y3hr&C1;|ZZ0|*EKO`$lECUYG2gVFtUTw)R z4Um<0ZzlON`zTdvVdL#KFoMFQX*a5wM0Czp%wTtfK4Sjs)P**RW&?lP$(<}q%r68Z zS53Y!d@&~ne9O)A^tNrXHhXBkj~$8j%pT1%%mypa9AW5E&s9)rjF4@O3ytH{0z6riz|@< zB~UPh*wRFg2^7EbQrHf0y?E~dHlkOxof_a?M{LqQ^C!i2dawHTPYUE=X@2(3<=OOxs8qn_(y>pU>u^}3y&df{JarR0@VJn0f+U%UiF=$Wyq zQvnVHESil@d|8&R<%}uidGh7@u^(%?$#|&J$pvFC-n8&A>utA=n3#)yMkz+qnG3wd zP7xCnF|$9Dif@N~L)Vde3hW8W!UY0BgT2v(wzp;tlLmyk2%N|0jfG$%<;A&IVrOI< z!L)o>j>;dFaqA3pL}b-Je(bB@VJ4%!JeX@3x!i{yIeIso^=n?fDX`3bU=eG7sTc%g%ye8$v8P@yKE^XD=NYxTb zbf!Mk=h|otpqjFaA-vs5YOF-*GwWPc7VbaOW&stlANnCN8iftFMMrUdYNJ_Bnn5Vt zxfz@Ah|+4&P;reZxp;MmEI7C|FOv8NKUm8njF7Wb6Gi7DeODLl&G~}G4be&*Hi0Qw z5}77vL0P+7-B%UL@3n1&JPxW^d@vVwp?u#gVcJqY9#@-3X{ok#UfW3<1fb%FT`|)V~ggq z(3AUoUS-;7)^hCjdT0Kf{i}h)mBg4qhtHHBti=~h^n^OTH5U*XMgDLIR@sre`AaB$ zg)IGBET_4??m@cx&c~bA80O7B8CHR7(LX7%HThkeC*@vi{-pL%e)yXp!B2InafbDF zjPXf1mko3h59{lT6EEbxKO1Z5GF71)WwowO6kY|6tjSVSWdQ}NsK2x{>i|MKZK8%Q zfu&_0D;CO-Jg0#YmyfctyJ!mRJp)e#@O0mYdp|8x;G1%OZQ3Q847YWTyy|%^cpA;m zze0(5p{tMu^lDkpe?HynyO?a1$_LJl2L&mpeKu%8YvgRNr=%2z${%WThHG=vrWY@4 zsA`OP#O&)TetZ>s%h!=+CE15lOOls&nvC~$Qz0Ph7tHiP;O$i|eDwpT{cp>+)0-|; zY$|bB+Gbel>5aRN3>c0x)4U=|X+z+{ zn*_p*EQoquRL+=+p;=lm`d71&1NqBz&_ph)MXu(Nv6&XE7(RsS)^MGj5Q?Fwude-(sq zjJ>aOq!7!EN>@(fK7EE#;i_BGvli`5U;r!YA{JRodLBc6-`n8K+Fjgwb%sX;j=qHQ z7&Tr!)!{HXoO<2BQrV9Sw?JRaLXV8HrsNevvnf>Y-6|{T!pYLl7jp$-nEE z#X!4G4L#K0qG_4Z;Cj6=;b|Be$hi4JvMH!-voxqx^@8cXp`B??eFBz2lLD8RRaRGh zn7kUfy!YV~p(R|p7iC1Rdgt$_24i0cd-S8HpG|`@my70g^y`gu%#Tf_L21-k?sRRZHK&at(*ED0P8iw{7?R$9~OF$Ko;Iu5)ur5<->x!m93Eb zFYpIx60s=Wxxw=`$aS-O&dCO_9?b1yKiPCQmSQb>T)963`*U+Ydj5kI(B(B?HNP8r z*bfSBpSu)w(Z3j7HQoRjUG(+d=IaE~tv}y14zHHs|0UcN52fT8V_<@2ep_ee{QgZG zmgp8iv4V{k;~8@I%M3<#B;2R>Ef(Gg_cQM7%}0s*^)SK6!Ym+~P^58*wnwV1BW@eG z4sZLqsUvBbFsr#8u7S1r4teQ;t)Y@jnn_m5jS$CsW1um!p&PqAcc8!zyiXHVta9QC zY~wCwCF0U%xiQPD_INKtTb;A|Zf29(mu9NI;E zc-e>*1%(LSXB`g}kd`#}O;veb<(sk~RWL|f3ljxCnEZDdNSTDV6#Td({6l&y4IjKF z^}lIUq*ZUqgTPumD)RrCN{M^jhY>E~1pn|KOZ5((%F)G|*ZQ|r4zIbrEiV%42hJV8 z3xS)=!X1+=olbdGJ=yZil?oXLct8FM{(6ikLL3E%=q#O6(H$p~gQu6T8N!plf!96| z&Q3=`L~>U0zZh;z(pGR2^S^{#PrPxTRHD1RQOON&f)Siaf`GLj#UOk&(|@0?zm;Sx ztsGt8=29-MZs5CSf1l1jNFtNt5rFNZxJPvkNu~2}7*9468TWm>nN9TP&^!;J{-h)_ z7WsHH9|F%I`Pb!>KAS3jQWKfGivTVkMJLO-HUGM_a4UQ_%RgL6WZvrW+Z4ujZn;y@ zz9$=oO!7qVTaQAA^BhX&ZxS*|5dj803M=k&2%QrXda`-Q#IoZL6E(g+tN!6CA!CP* zCpWtCujIea)ENl0liwVfj)Nc<9mV%+e@=d`haoZ*`B7+PNjEbXBkv=B+Pi^~L#EO$D$ZqTiD8f<5$eyb54-(=3 zh)6i8i|jp(@OnRrY5B8t|LFXFQVQ895n*P16cEKTrT*~yLH6Z4e*bZ5otpRDri&+A zfNbK1D5@O=sm`fN=WzWyse!za5n%^+6dHPGX#8DyIK>?9qyX}2XvBWVqbP%%D)7$= z=#$WulZlZR<{m#gU7lwqK4WS1Ne$#_P{b17qe$~UOXCl>5b|6WVh;5vVnR<%d+Lnp z$uEmML38}U4vaW8>shm6CzB(Wei3s#NAWE3)a2)z@i{4jTn;;aQS)O@l{rUM`J@K& l00vQ5JBs~;vo!vr%%-k{2_Fq1Mn4QF81S)AQ99zk{{c4yR+0b! literal 0 HcmV?d00001 diff --git a/simulation/JavaSample/gradle/wrapper/gradle-wrapper.properties b/simulation/JavaSample/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..5e82d67b9f --- /dev/null +++ b/simulation/JavaSample/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=permwrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=permwrapper/dists diff --git a/simulation/JavaSample/gradlew b/simulation/JavaSample/gradlew new file mode 100755 index 0000000000..1aa94a4269 --- /dev/null +++ b/simulation/JavaSample/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/simulation/JavaSample/gradlew.bat b/simulation/JavaSample/gradlew.bat new file mode 100644 index 0000000000..93e3f59f13 --- /dev/null +++ b/simulation/JavaSample/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/simulation/JavaSample/settings.gradle b/simulation/JavaSample/settings.gradle new file mode 100644 index 0000000000..d94f73c635 --- /dev/null +++ b/simulation/JavaSample/settings.gradle @@ -0,0 +1,30 @@ +import org.gradle.internal.os.OperatingSystem + +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + String frcYear = '2024' + File frcHome + if (OperatingSystem.current().isWindows()) { + String publicFolder = System.getenv('PUBLIC') + if (publicFolder == null) { + publicFolder = "C:\\Users\\Public" + } + def homeRoot = new File(publicFolder, "wpilib") + frcHome = new File(homeRoot, frcYear) + } else { + def userFolder = System.getProperty("user.home") + def homeRoot = new File(userFolder, "wpilib") + frcHome = new File(homeRoot, frcYear) + } + def frcHomeMaven = new File(frcHome, 'maven') + maven { + name 'frcHome' + url frcHomeMaven + } + } +} + +Properties props = System.getProperties(); +props.setProperty("org.gradle.internal.native.headers.unresolved.dependencies.ignore", "true"); diff --git a/simulation/JavaSample/src/main/deploy/example.txt b/simulation/JavaSample/src/main/deploy/example.txt new file mode 100644 index 0000000000..bb82515dad --- /dev/null +++ b/simulation/JavaSample/src/main/deploy/example.txt @@ -0,0 +1,3 @@ +Files placed in this directory will be deployed to the RoboRIO into the +'deploy' directory in the home folder. Use the 'Filesystem.getDeployDirectory' wpilib function +to get a proper path relative to the deploy directory. \ No newline at end of file diff --git a/simulation/JavaSample/src/main/java/frc/robot/Main.java b/simulation/JavaSample/src/main/java/frc/robot/Main.java new file mode 100644 index 0000000000..8776e5dda7 --- /dev/null +++ b/simulation/JavaSample/src/main/java/frc/robot/Main.java @@ -0,0 +1,25 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package frc.robot; + +import edu.wpi.first.wpilibj.RobotBase; + +/** + * Do NOT add any static variables to this class, or any initialization at all. Unless you know what + * you are doing, do not modify this file except to change the parameter class to the startRobot + * call. + */ +public final class Main { + private Main() {} + + /** + * Main initialization function. Do not perform any initialization here. + * + *

If you change your main robot class, change the parameter type. + */ + public static void main(String... args) { + RobotBase.startRobot(Robot::new); + } +} diff --git a/simulation/JavaSample/src/main/java/frc/robot/Robot.java b/simulation/JavaSample/src/main/java/frc/robot/Robot.java new file mode 100644 index 0000000000..3922566c6e --- /dev/null +++ b/simulation/JavaSample/src/main/java/frc/robot/Robot.java @@ -0,0 +1,128 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package frc.robot; + +import com.ctre.phoenix6.hardware.TalonFX; +import com.revrobotics.CANSparkMax; +import com.revrobotics.CANSparkLowLevel.MotorType; + +import edu.wpi.first.wpilibj.TimedRobot; +import edu.wpi.first.wpilibj.motorcontrol.Spark; +import edu.wpi.first.wpilibj.smartdashboard.SendableChooser; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; + +/** + * The VM is configured to automatically run this class, and to call the functions corresponding to + * each mode, as described in the TimedRobot documentation. If you change the name of this class or + * the package after creating this project, you must also update the build.gradle file in the + * project. + */ +public class Robot extends TimedRobot { + private static final String kDefaultAuto = "Default"; + private static final String kCustomAuto = "My Auto"; + private String m_autoSelected; + private final SendableChooser m_chooser = new SendableChooser<>(); + + private Spark m_Spark = new Spark(0); + private CANSparkMax m_SparkMax = new CANSparkMax(1, MotorType.kBrushless); + private TalonFX m_Talon = new TalonFX(2); + + /** + * This function is run when the robot is first started up and should be used for any + * initialization code. + */ + @Override + public void robotInit() { + m_chooser.setDefaultOption("Default Auto", kDefaultAuto); + m_chooser.addOption("My Auto", kCustomAuto); + SmartDashboard.putData("Auto choices", m_chooser); + } + + /** + * This function is called every 20 ms, no matter the mode. Use this for items like diagnostics + * that you want ran during disabled, autonomous, teleoperated and test. + * + *

This runs after the mode specific periodic functions, but before LiveWindow and + * SmartDashboard integrated updating. + */ + @Override + public void robotPeriodic() {} + + /** + * This autonomous (along with the chooser code above) shows how to select between different + * autonomous modes using the dashboard. The sendable chooser code works with the Java + * SmartDashboard. If you prefer the LabVIEW Dashboard, remove all of the chooser code and + * uncomment the getString line to get the auto name from the text box below the Gyro + * + *

You can add additional auto modes by adding additional comparisons to the switch structure + * below with additional strings. If using the SendableChooser make sure to add them to the + * chooser code above as well. + */ + @Override + public void autonomousInit() { + m_autoSelected = m_chooser.getSelected(); + // m_autoSelected = SmartDashboard.getString("Auto Selector", kDefaultAuto); + System.out.println("Auto selected: " + m_autoSelected); + } + + /** This function is called periodically during autonomous. */ + @Override + public void autonomousPeriodic() { + + m_Spark.set(0.5); + m_SparkMax.set(1.0); + m_Talon.set(-1.0); + + switch (m_autoSelected) { + case kCustomAuto: + // Put custom auto code here + break; + case kDefaultAuto: + default: + // Put default auto code here + break; + } + } + + /** This function is called once when teleop is enabled. */ + @Override + public void teleopInit() {} + + /** This function is called periodically during operator control. */ + @Override + public void teleopPeriodic() { + m_Spark.set(0.25); + m_SparkMax.set(0.75); + m_Talon.set(-0.5); + } + + /** This function is called once when the robot is disabled. */ + @Override + public void disabledInit() { + m_Spark.set(0.0); + m_SparkMax.set(0.0); + m_Talon.set(0.0); + } + + /** This function is called periodically when disabled. */ + @Override + public void disabledPeriodic() {} + + /** This function is called once when test mode is enabled. */ + @Override + public void testInit() {} + + /** This function is called periodically during test mode. */ + @Override + public void testPeriodic() {} + + /** This function is called once when the robot is first started up. */ + @Override + public void simulationInit() {} + + /** This function is called periodically whilst in simulation. */ + @Override + public void simulationPeriodic() {} +} diff --git a/simulation/JavaSample/vendordeps/NavX.json b/simulation/JavaSample/vendordeps/NavX.json new file mode 100644 index 0000000000..e978a5f745 --- /dev/null +++ b/simulation/JavaSample/vendordeps/NavX.json @@ -0,0 +1,40 @@ +{ + "fileName": "NavX.json", + "name": "NavX", + "version": "2024.1.0", + "uuid": "cb311d09-36e9-4143-a032-55bb2b94443b", + "frcYear": "2024", + "mavenUrls": [ + "https://dev.studica.com/maven/release/2024/" + ], + "jsonUrl": "https://dev.studica.com/releases/2024/NavX.json", + "javaDependencies": [ + { + "groupId": "com.kauailabs.navx.frc", + "artifactId": "navx-frc-java", + "version": "2024.1.0" + } + ], + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "com.kauailabs.navx.frc", + "artifactId": "navx-frc-cpp", + "version": "2024.1.0", + "headerClassifier": "headers", + "sourcesClassifier": "sources", + "sharedLibrary": false, + "libName": "navx_frc", + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena", + "linuxraspbian", + "linuxarm32", + "linuxarm64", + "linuxx86-64", + "osxuniversal", + "windowsx86-64" + ] + } + ] +} \ No newline at end of file diff --git a/simulation/JavaSample/vendordeps/Phoenix6.json b/simulation/JavaSample/vendordeps/Phoenix6.json new file mode 100644 index 0000000000..032238505f --- /dev/null +++ b/simulation/JavaSample/vendordeps/Phoenix6.json @@ -0,0 +1,339 @@ +{ + "fileName": "Phoenix6.json", + "name": "CTRE-Phoenix (v6)", + "version": "24.3.0", + "frcYear": 2024, + "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", + "mavenUrls": [ + "https://maven.ctr-electronics.com/release/" + ], + "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-frc2024-latest.json", + "conflictsWith": [ + { + "uuid": "3fcf3402-e646-4fa6-971e-18afe8173b1a", + "errorMessage": "The combined Phoenix-6-And-5 vendordep is no longer supported. Please remove the vendordep and instead add both the latest Phoenix 6 vendordep and Phoenix 5 vendordep.", + "offlineFileName": "Phoenix6And5.json" + } + ], + "javaDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "wpiapi-java", + "version": "24.3.0" + } + ], + "jniDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "tools", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "tools-sim", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonSRX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonFX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simVictorSPX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simPigeonIMU", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simCANCoder", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANcoder", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProPigeon2", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + } + ], + "cppDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "wpiapi-cpp", + "version": "24.3.0", + "libName": "CTRE_Phoenix6_WPI", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6", + "artifactId": "tools", + "version": "24.3.0", + "libName": "CTRE_PhoenixTools", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "wpiapi-cpp-sim", + "version": "24.3.0", + "libName": "CTRE_Phoenix6_WPISim", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "tools-sim", + "version": "24.3.0", + "libName": "CTRE_PhoenixTools_Sim", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonSRX", + "version": "24.3.0", + "libName": "CTRE_SimTalonSRX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonFX", + "version": "24.3.0", + "libName": "CTRE_SimTalonFX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simVictorSPX", + "version": "24.3.0", + "libName": "CTRE_SimVictorSPX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simPigeonIMU", + "version": "24.3.0", + "libName": "CTRE_SimPigeonIMU", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simCANCoder", + "version": "24.3.0", + "libName": "CTRE_SimCANCoder", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFX", + "version": "24.3.0", + "libName": "CTRE_SimProTalonFX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANcoder", + "version": "24.3.0", + "libName": "CTRE_SimProCANcoder", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProPigeon2", + "version": "24.3.0", + "libName": "CTRE_SimProPigeon2", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + } + ] +} \ No newline at end of file diff --git a/simulation/JavaSample/vendordeps/REVLib.json b/simulation/JavaSample/vendordeps/REVLib.json new file mode 100644 index 0000000000..f85acd4054 --- /dev/null +++ b/simulation/JavaSample/vendordeps/REVLib.json @@ -0,0 +1,74 @@ +{ + "fileName": "REVLib.json", + "name": "REVLib", + "version": "2024.2.4", + "frcYear": "2024", + "uuid": "3f48eb8c-50fe-43a6-9cb7-44c86353c4cb", + "mavenUrls": [ + "https://maven.revrobotics.com/" + ], + "jsonUrl": "https://software-metadata.revrobotics.com/REVLib-2024.json", + "javaDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-java", + "version": "2024.2.4" + } + ], + "jniDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-driver", + "version": "2024.2.4", + "skipInvalidPlatforms": true, + "isJar": false, + "validPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ], + "cppDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-cpp", + "version": "2024.2.4", + "libName": "REVLib", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + }, + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-driver", + "version": "2024.2.4", + "libName": "REVLibDriver", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ] +} \ No newline at end of file diff --git a/simulation/JavaSample/vendordeps/WPILibNewCommands.json b/simulation/JavaSample/vendordeps/WPILibNewCommands.json new file mode 100644 index 0000000000..67bf3898d5 --- /dev/null +++ b/simulation/JavaSample/vendordeps/WPILibNewCommands.json @@ -0,0 +1,38 @@ +{ + "fileName": "WPILibNewCommands.json", + "name": "WPILib-New-Commands", + "version": "1.0.0", + "uuid": "111e20f7-815e-48f8-9dd6-e675ce75b266", + "frcYear": "2024", + "mavenUrls": [], + "jsonUrl": "", + "javaDependencies": [ + { + "groupId": "edu.wpi.first.wpilibNewCommands", + "artifactId": "wpilibNewCommands-java", + "version": "wpilib" + } + ], + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "edu.wpi.first.wpilibNewCommands", + "artifactId": "wpilibNewCommands-cpp", + "version": "wpilib", + "libName": "wpilibNewCommands", + "headerClassifier": "headers", + "sourcesClassifier": "sources", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena", + "linuxarm32", + "linuxarm64", + "windowsx86-64", + "windowsx86", + "linuxx86-64", + "osxuniversal" + ] + } + ] +} From d6070d6e903fe3f30f9a4369e7e2f11e7d34c16d Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Sun, 7 Jul 2024 03:51:09 -0600 Subject: [PATCH 037/121] This actually appears to be a plausible path --- simulation/JavaSample/build.gradle | 58 +++++++++---------- .../synthesis/revrobotics/CANSparkMax.java | 45 ++++++++++++++ .../src/main/java/frc/robot/Robot.java | 5 +- 3 files changed, 78 insertions(+), 30 deletions(-) create mode 100644 simulation/JavaSample/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java diff --git a/simulation/JavaSample/build.gradle b/simulation/JavaSample/build.gradle index f0c731e2d9..7735bda8e6 100644 --- a/simulation/JavaSample/build.gradle +++ b/simulation/JavaSample/build.gradle @@ -1,6 +1,6 @@ plugins { id "java" - // id "edu.wpi.first.GradleRIO" version "2024.3.2" + id "edu.wpi.first.GradleRIO" version "2024.3.2" } // wpi.maven.useLocal = false @@ -17,34 +17,34 @@ def ROBOT_MAIN_CLASS = "frc.robot.Main" // Define my targets (RoboRIO) and artifacts (deployable files) // This is added by GradleRIO's backing project DeployUtils. -// deploy { -// targets { -// roborio(getTargetTypeClass('RoboRIO')) { -// // Team number is loaded either from the .wpilib/wpilib_preferences.json -// // or from command line. If not found an exception will be thrown. -// // You can use getTeamOrDefault(team) instead of getTeamNumber if you -// // want to store a team number in this file. -// team = project.frc.getTeamNumber() -// debug = project.frc.getDebugOrDefault(false) - -// artifacts { -// // First part is artifact name, 2nd is artifact type -// // getTargetTypeClass is a shortcut to get the class type using a string - -// frcJava(getArtifactTypeClass('FRCJavaArtifact')) { -// } - -// // Static files artifact -// frcStaticFileDeploy(getArtifactTypeClass('FileTreeArtifact')) { -// files = project.fileTree('src/main/deploy') -// directory = '/home/lvuser/deploy' -// } -// } -// } -// } -// } - -// def deployArtifact = deploy.targets.roborio.artifacts.frcJava +deploy { + targets { + roborio(getTargetTypeClass('RoboRIO')) { + // Team number is loaded either from the .wpilib/wpilib_preferences.json + // or from command line. If not found an exception will be thrown. + // You can use getTeamOrDefault(team) instead of getTeamNumber if you + // want to store a team number in this file. + team = project.frc.getTeamOrDefault(997) + debug = project.frc.getDebugOrDefault(false) + + artifacts { + // First part is artifact name, 2nd is artifact type + // getTargetTypeClass is a shortcut to get the class type using a string + + frcJava(getArtifactTypeClass('FRCJavaArtifact')) { + } + + // Static files artifact + frcStaticFileDeploy(getArtifactTypeClass('FileTreeArtifact')) { + files = project.fileTree('src/main/deploy') + directory = '/home/lvuser/deploy' + } + } + } + } +} + +def deployArtifact = deploy.targets.roborio.artifacts.frcJava // Set to true to use debug for JNI. wpi.java.debugJni = false diff --git a/simulation/JavaSample/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java b/simulation/JavaSample/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java new file mode 100644 index 0000000000..bb63a11238 --- /dev/null +++ b/simulation/JavaSample/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java @@ -0,0 +1,45 @@ +package com.autodesk.synthesis.revrobotics; + +import com.revrobotics.REVLibError; + +import edu.wpi.first.hal.SimBoolean; +import edu.wpi.first.hal.SimDevice; +import edu.wpi.first.hal.SimDouble; +import edu.wpi.first.hal.SimDevice.Direction; + +public class CANSparkMax extends com.revrobotics.CANSparkMax { + + private SimDevice m_device; + private SimDouble m_percentOutput; + private SimBoolean m_brakeMode; + private SimDouble m_neutralDeadband; + + public CANSparkMax(int deviceId, MotorType motorType) { + super(deviceId, motorType); + + m_device = SimDevice.create("SYN CANSparkMax", deviceId); + m_percentOutput = m_device.createDouble("percentOutput", Direction.kOutput, 0.0); + m_brakeMode = m_device.createBoolean("brakeMode", Direction.kOutput, false); + } + + @Override + public void set(double percent) { + if (Double.isNaN(percent) || Double.isInfinite(percent)) { + percent = 0.0; + } + + m_percentOutput.set(Math.min(1.0, Math.max(-1.0, percent))); + + super.set(percent); + } + + @Override + public REVLibError setIdleMode(com.revrobotics.CANSparkBase.IdleMode mode) { + if (mode != null) { + m_brakeMode.set(mode.equals(com.revrobotics.CANSparkBase.IdleMode.kBrake)); + } + + return super.setIdleMode(mode); + } + +} diff --git a/simulation/JavaSample/src/main/java/frc/robot/Robot.java b/simulation/JavaSample/src/main/java/frc/robot/Robot.java index 3922566c6e..8a9aa5024b 100644 --- a/simulation/JavaSample/src/main/java/frc/robot/Robot.java +++ b/simulation/JavaSample/src/main/java/frc/robot/Robot.java @@ -5,7 +5,8 @@ package frc.robot; import com.ctre.phoenix6.hardware.TalonFX; -import com.revrobotics.CANSparkMax; +import com.revrobotics.CANSparkBase.IdleMode; +// import com.revrobotics.CANSparkMax; import com.revrobotics.CANSparkLowLevel.MotorType; import edu.wpi.first.wpilibj.TimedRobot; @@ -13,6 +14,8 @@ import edu.wpi.first.wpilibj.smartdashboard.SendableChooser; import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import com.autodesk.synthesis.revrobotics.CANSparkMax; + /** * The VM is configured to automatically run this class, and to call the functions corresponding to * each mode, as described in the TimedRobot documentation. If you change the name of this class or From 71de3f9f3277afceb35d962ea146e5edb00376c0 Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Sun, 7 Jul 2024 12:21:41 -0600 Subject: [PATCH 038/121] Added javadocs to SyntheSimJava, READMEs all around, and now JavaSample is making use of SyntheSimJava --- .../synthesis/revrobotics/CANSparkMax.java | 45 --- simulation/JavaSample/vendordeps/NavX.json | 40 --- .../JavaSample/vendordeps/Phoenix6.json | 339 ------------------ simulation/JavaSample/vendordeps/REVLib.json | 74 ---- .../vendordeps/WPILibNewCommands.json | 38 -- simulation/README.md | 12 + simulation/SyntheSimJava/.gitattributes | 9 + simulation/SyntheSimJava/.gitignore | 6 + simulation/SyntheSimJava/README.md | 86 +++++ simulation/SyntheSimJava/build.gradle | 76 ++++ simulation/SyntheSimJava/gradle.properties | 6 + .../SyntheSimJava/gradle/libs.versions.toml | 10 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + simulation/SyntheSimJava/gradlew | 249 +++++++++++++ simulation/SyntheSimJava/gradlew.bat | 92 +++++ simulation/SyntheSimJava/settings.gradle | 15 + .../com/autodesk/synthesis/CANEncoder.java | 50 +++ .../java/com/autodesk/synthesis/CANMotor.java | 108 ++++++ .../com/autodesk/synthesis/ctre/TalonFX.java | 13 + .../synthesis/revrobotics/CANSparkMax.java | 43 +++ .../{ => samples}/JavaSample/.gitignore | 0 .../JavaSample/WPILib-License.md | 0 .../{ => samples}/JavaSample/build.gradle | 6 + .../gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 simulation/{ => samples}/JavaSample/gradlew | 0 .../{ => samples}/JavaSample/gradlew.bat | 0 .../{ => samples}/JavaSample/settings.gradle | 0 .../JavaSample/src/main/deploy/example.txt | 0 .../src/main/java/frc/robot/Main.java | 0 .../src/main/java/frc/robot/Robot.java | 0 32 files changed, 788 insertions(+), 536 deletions(-) delete mode 100644 simulation/JavaSample/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java delete mode 100644 simulation/JavaSample/vendordeps/NavX.json delete mode 100644 simulation/JavaSample/vendordeps/Phoenix6.json delete mode 100644 simulation/JavaSample/vendordeps/REVLib.json delete mode 100644 simulation/JavaSample/vendordeps/WPILibNewCommands.json create mode 100644 simulation/README.md create mode 100644 simulation/SyntheSimJava/.gitattributes create mode 100644 simulation/SyntheSimJava/.gitignore create mode 100644 simulation/SyntheSimJava/README.md create mode 100644 simulation/SyntheSimJava/build.gradle create mode 100644 simulation/SyntheSimJava/gradle.properties create mode 100644 simulation/SyntheSimJava/gradle/libs.versions.toml create mode 100644 simulation/SyntheSimJava/gradle/wrapper/gradle-wrapper.jar create mode 100644 simulation/SyntheSimJava/gradle/wrapper/gradle-wrapper.properties create mode 100644 simulation/SyntheSimJava/gradlew create mode 100644 simulation/SyntheSimJava/gradlew.bat create mode 100644 simulation/SyntheSimJava/settings.gradle create mode 100644 simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANEncoder.java create mode 100644 simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANMotor.java create mode 100644 simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/ctre/TalonFX.java create mode 100644 simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java rename simulation/{ => samples}/JavaSample/.gitignore (100%) rename simulation/{ => samples}/JavaSample/WPILib-License.md (100%) rename simulation/{ => samples}/JavaSample/build.gradle (97%) rename simulation/{ => samples}/JavaSample/gradle/wrapper/gradle-wrapper.jar (100%) rename simulation/{ => samples}/JavaSample/gradle/wrapper/gradle-wrapper.properties (100%) rename simulation/{ => samples}/JavaSample/gradlew (100%) mode change 100755 => 100644 rename simulation/{ => samples}/JavaSample/gradlew.bat (100%) rename simulation/{ => samples}/JavaSample/settings.gradle (100%) rename simulation/{ => samples}/JavaSample/src/main/deploy/example.txt (100%) rename simulation/{ => samples}/JavaSample/src/main/java/frc/robot/Main.java (100%) rename simulation/{ => samples}/JavaSample/src/main/java/frc/robot/Robot.java (100%) diff --git a/simulation/JavaSample/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java b/simulation/JavaSample/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java deleted file mode 100644 index bb63a11238..0000000000 --- a/simulation/JavaSample/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.autodesk.synthesis.revrobotics; - -import com.revrobotics.REVLibError; - -import edu.wpi.first.hal.SimBoolean; -import edu.wpi.first.hal.SimDevice; -import edu.wpi.first.hal.SimDouble; -import edu.wpi.first.hal.SimDevice.Direction; - -public class CANSparkMax extends com.revrobotics.CANSparkMax { - - private SimDevice m_device; - private SimDouble m_percentOutput; - private SimBoolean m_brakeMode; - private SimDouble m_neutralDeadband; - - public CANSparkMax(int deviceId, MotorType motorType) { - super(deviceId, motorType); - - m_device = SimDevice.create("SYN CANSparkMax", deviceId); - m_percentOutput = m_device.createDouble("percentOutput", Direction.kOutput, 0.0); - m_brakeMode = m_device.createBoolean("brakeMode", Direction.kOutput, false); - } - - @Override - public void set(double percent) { - if (Double.isNaN(percent) || Double.isInfinite(percent)) { - percent = 0.0; - } - - m_percentOutput.set(Math.min(1.0, Math.max(-1.0, percent))); - - super.set(percent); - } - - @Override - public REVLibError setIdleMode(com.revrobotics.CANSparkBase.IdleMode mode) { - if (mode != null) { - m_brakeMode.set(mode.equals(com.revrobotics.CANSparkBase.IdleMode.kBrake)); - } - - return super.setIdleMode(mode); - } - -} diff --git a/simulation/JavaSample/vendordeps/NavX.json b/simulation/JavaSample/vendordeps/NavX.json deleted file mode 100644 index e978a5f745..0000000000 --- a/simulation/JavaSample/vendordeps/NavX.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "fileName": "NavX.json", - "name": "NavX", - "version": "2024.1.0", - "uuid": "cb311d09-36e9-4143-a032-55bb2b94443b", - "frcYear": "2024", - "mavenUrls": [ - "https://dev.studica.com/maven/release/2024/" - ], - "jsonUrl": "https://dev.studica.com/releases/2024/NavX.json", - "javaDependencies": [ - { - "groupId": "com.kauailabs.navx.frc", - "artifactId": "navx-frc-java", - "version": "2024.1.0" - } - ], - "jniDependencies": [], - "cppDependencies": [ - { - "groupId": "com.kauailabs.navx.frc", - "artifactId": "navx-frc-cpp", - "version": "2024.1.0", - "headerClassifier": "headers", - "sourcesClassifier": "sources", - "sharedLibrary": false, - "libName": "navx_frc", - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "linuxathena", - "linuxraspbian", - "linuxarm32", - "linuxarm64", - "linuxx86-64", - "osxuniversal", - "windowsx86-64" - ] - } - ] -} \ No newline at end of file diff --git a/simulation/JavaSample/vendordeps/Phoenix6.json b/simulation/JavaSample/vendordeps/Phoenix6.json deleted file mode 100644 index 032238505f..0000000000 --- a/simulation/JavaSample/vendordeps/Phoenix6.json +++ /dev/null @@ -1,339 +0,0 @@ -{ - "fileName": "Phoenix6.json", - "name": "CTRE-Phoenix (v6)", - "version": "24.3.0", - "frcYear": 2024, - "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", - "mavenUrls": [ - "https://maven.ctr-electronics.com/release/" - ], - "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-frc2024-latest.json", - "conflictsWith": [ - { - "uuid": "3fcf3402-e646-4fa6-971e-18afe8173b1a", - "errorMessage": "The combined Phoenix-6-And-5 vendordep is no longer supported. Please remove the vendordep and instead add both the latest Phoenix 6 vendordep and Phoenix 5 vendordep.", - "offlineFileName": "Phoenix6And5.json" - } - ], - "javaDependencies": [ - { - "groupId": "com.ctre.phoenix6", - "artifactId": "wpiapi-java", - "version": "24.3.0" - } - ], - "jniDependencies": [ - { - "groupId": "com.ctre.phoenix6", - "artifactId": "tools", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxathena" - ], - "simMode": "hwsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "tools-sim", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simTalonSRX", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simTalonFX", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simVictorSPX", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simPigeonIMU", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simCANCoder", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simProTalonFX", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simProCANcoder", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simProPigeon2", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - } - ], - "cppDependencies": [ - { - "groupId": "com.ctre.phoenix6", - "artifactId": "wpiapi-cpp", - "version": "24.3.0", - "libName": "CTRE_Phoenix6_WPI", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxathena" - ], - "simMode": "hwsim" - }, - { - "groupId": "com.ctre.phoenix6", - "artifactId": "tools", - "version": "24.3.0", - "libName": "CTRE_PhoenixTools", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxathena" - ], - "simMode": "hwsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "wpiapi-cpp-sim", - "version": "24.3.0", - "libName": "CTRE_Phoenix6_WPISim", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "tools-sim", - "version": "24.3.0", - "libName": "CTRE_PhoenixTools_Sim", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simTalonSRX", - "version": "24.3.0", - "libName": "CTRE_SimTalonSRX", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simTalonFX", - "version": "24.3.0", - "libName": "CTRE_SimTalonFX", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simVictorSPX", - "version": "24.3.0", - "libName": "CTRE_SimVictorSPX", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simPigeonIMU", - "version": "24.3.0", - "libName": "CTRE_SimPigeonIMU", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simCANCoder", - "version": "24.3.0", - "libName": "CTRE_SimCANCoder", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simProTalonFX", - "version": "24.3.0", - "libName": "CTRE_SimProTalonFX", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simProCANcoder", - "version": "24.3.0", - "libName": "CTRE_SimProCANcoder", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simProPigeon2", - "version": "24.3.0", - "libName": "CTRE_SimProPigeon2", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - } - ] -} \ No newline at end of file diff --git a/simulation/JavaSample/vendordeps/REVLib.json b/simulation/JavaSample/vendordeps/REVLib.json deleted file mode 100644 index f85acd4054..0000000000 --- a/simulation/JavaSample/vendordeps/REVLib.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "fileName": "REVLib.json", - "name": "REVLib", - "version": "2024.2.4", - "frcYear": "2024", - "uuid": "3f48eb8c-50fe-43a6-9cb7-44c86353c4cb", - "mavenUrls": [ - "https://maven.revrobotics.com/" - ], - "jsonUrl": "https://software-metadata.revrobotics.com/REVLib-2024.json", - "javaDependencies": [ - { - "groupId": "com.revrobotics.frc", - "artifactId": "REVLib-java", - "version": "2024.2.4" - } - ], - "jniDependencies": [ - { - "groupId": "com.revrobotics.frc", - "artifactId": "REVLib-driver", - "version": "2024.2.4", - "skipInvalidPlatforms": true, - "isJar": false, - "validPlatforms": [ - "windowsx86-64", - "windowsx86", - "linuxarm64", - "linuxx86-64", - "linuxathena", - "linuxarm32", - "osxuniversal" - ] - } - ], - "cppDependencies": [ - { - "groupId": "com.revrobotics.frc", - "artifactId": "REVLib-cpp", - "version": "2024.2.4", - "libName": "REVLib", - "headerClassifier": "headers", - "sharedLibrary": false, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "windowsx86", - "linuxarm64", - "linuxx86-64", - "linuxathena", - "linuxarm32", - "osxuniversal" - ] - }, - { - "groupId": "com.revrobotics.frc", - "artifactId": "REVLib-driver", - "version": "2024.2.4", - "libName": "REVLibDriver", - "headerClassifier": "headers", - "sharedLibrary": false, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "windowsx86", - "linuxarm64", - "linuxx86-64", - "linuxathena", - "linuxarm32", - "osxuniversal" - ] - } - ] -} \ No newline at end of file diff --git a/simulation/JavaSample/vendordeps/WPILibNewCommands.json b/simulation/JavaSample/vendordeps/WPILibNewCommands.json deleted file mode 100644 index 67bf3898d5..0000000000 --- a/simulation/JavaSample/vendordeps/WPILibNewCommands.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "fileName": "WPILibNewCommands.json", - "name": "WPILib-New-Commands", - "version": "1.0.0", - "uuid": "111e20f7-815e-48f8-9dd6-e675ce75b266", - "frcYear": "2024", - "mavenUrls": [], - "jsonUrl": "", - "javaDependencies": [ - { - "groupId": "edu.wpi.first.wpilibNewCommands", - "artifactId": "wpilibNewCommands-java", - "version": "wpilib" - } - ], - "jniDependencies": [], - "cppDependencies": [ - { - "groupId": "edu.wpi.first.wpilibNewCommands", - "artifactId": "wpilibNewCommands-cpp", - "version": "wpilib", - "libName": "wpilibNewCommands", - "headerClassifier": "headers", - "sourcesClassifier": "sources", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "linuxathena", - "linuxarm32", - "linuxarm64", - "windowsx86-64", - "windowsx86", - "linuxx86-64", - "osxuniversal" - ] - } - ] -} diff --git a/simulation/README.md b/simulation/README.md new file mode 100644 index 0000000000..4f33881b3b --- /dev/null +++ b/simulation/README.md @@ -0,0 +1,12 @@ +# Synthesis Simulation + +A collection of simulation tools and samples to help enchance the use of code simulation inside of Synthesis. + +## SyntheSim + +SyntheSim is a utility used to bolster the simulation capabilities of WPILib compatible libraries commonly used in FRC today. + +## Samples + +This directory includes sample projects that are simulation compatible with Synthesis. These sample projects give +users an idea of how to configure their own projects to best take advantage of simulation. diff --git a/simulation/SyntheSimJava/.gitattributes b/simulation/SyntheSimJava/.gitattributes new file mode 100644 index 0000000000..097f9f98d9 --- /dev/null +++ b/simulation/SyntheSimJava/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/simulation/SyntheSimJava/.gitignore b/simulation/SyntheSimJava/.gitignore new file mode 100644 index 0000000000..3b2f6ee393 --- /dev/null +++ b/simulation/SyntheSimJava/.gitignore @@ -0,0 +1,6 @@ +# Ignore Gradle project-specific cache directory +.gradle +.vscode + +# Ignore Gradle build output directory +build diff --git a/simulation/SyntheSimJava/README.md b/simulation/SyntheSimJava/README.md new file mode 100644 index 0000000000..3e10a613a3 --- /dev/null +++ b/simulation/SyntheSimJava/README.md @@ -0,0 +1,86 @@ +# SyntheSim - Java + +This is the SyntheSim Java utility library. FRC users can add this to their project to enhance the simulation capabilities of commonly used libraries + +## Current 3rd-Party Support + +This is a list of the following 3rd-Party libraries that SyntheSim - Java improves, as well as the level of capability currently offered. + +### REVRobotics +- [ ] CANSparkMax + - [x] Basic motor control + - [x] Basic internal encoder data + - [ ] Motor following + - [ ] Full encoder support + +### CTRE Phoenix +- [ ] TalonFX + - [ ] Basic motor control + - [ ] Basic internal encoder data + - [ ] Motor following + - [ ] Full encoder support + +## Building + +To build the project, run the `build` task: + +

+ Example + + Windows: + ```sh + $ gradlew.bat build + ``` + + MacOS/Linux: + ```sh + $ ./gradlew build + ``` +
+ +## Usage + +Currently, SyntheSimJava is only available as a local repository. This means it will need to be published and accessed locally. + +### Publish (Local) + +To publish the project locally, run the `publishToMavenLocal` task: + +
+ Example + + Windows: + ```sh + $ gradlew.bat publishToMavenLocal + ``` + + MacOS/Linux: + ```sh + $ ./gradlew publishToMavenLocal + ``` +
+ +### Adding to project locally + +In order to add the project locally, you must include the the `mavenLocal()` repository to your projects: + +```groovy +repositories { + mavenLocal() + ... +} +``` + +Then, add the implementation to your dependencies: + +```groovy +dependencies { + ... + implementation "com.autodesk.synthesis:SyntheSimJava:1.0.0" + ... +} +``` + +### Swapping Imports + +SyntheSimJava creates alternative classes that wrap the original ones. Everything that we intercept is passed on to the original class, making it so these classes can (although not recommended) be used when running your robot code on original hardware. Be sure to switch over any and all CAN devices that this project supports in order to effectively simulate your code inside of Synthesis, or with any HALSim, WebSocket supported simulation/device. \ No newline at end of file diff --git a/simulation/SyntheSimJava/build.gradle b/simulation/SyntheSimJava/build.gradle new file mode 100644 index 0000000000..421980fb3d --- /dev/null +++ b/simulation/SyntheSimJava/build.gradle @@ -0,0 +1,76 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Java library project to get you started. + * For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.8/userguide/building_java_projects.html in the Gradle documentation. + * This project uses @Incubating APIs which are subject to change. + */ + +plugins { + // Apply the java-library plugin for API and implementation separation. + id 'java-library' + id 'maven-publish' +} + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() + + // WPILib + maven { + url "https://frcmaven.wpi.edu/artifactory/release/" + } + + // CTRE + maven { + url "https://maven.ctr-electronics.com/release/" + } + + // REV + maven { + url "https://maven.revrobotics.com/" + } +} + +def WPI_Version = '2024.3.2' +def REV_Version = '2024.2.4' +def CTRE_Version = '24.3.0' + +dependencies { + // This dependency is exported to consumers, that is to say found on their compile classpath. + api libs.commons.math3 + + // This dependency is used internally, and not exposed to consumers on their own compile classpath. + implementation libs.guava + + // WPILib + implementation "edu.wpi.first.wpilibj:wpilibj-java:$WPI_Version" + implementation "edu.wpi.first.wpiutil:wpiutil-java:$WPI_Version" + implementation "edu.wpi.first.hal:hal-java:$WPI_Version" + + // REVRobotics + implementation "com.revrobotics.frc:REVLib-java:$REV_Version" + + // CTRE + implementation "com.ctre.phoenix6:wpiapi-java:$CTRE_Version" +} + +java { + withJavadocJar() + withSourcesJar() + + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +publishing() { + publications { + maven(MavenPublication) { + group = 'com.autodesk.synthesis' + artifactId = 'SyntheSimJava' + version = '1.0.0' + + from components.java + } + } +} diff --git a/simulation/SyntheSimJava/gradle.properties b/simulation/SyntheSimJava/gradle.properties new file mode 100644 index 0000000000..18f452c73f --- /dev/null +++ b/simulation/SyntheSimJava/gradle.properties @@ -0,0 +1,6 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties + +org.gradle.parallel=true +org.gradle.caching=true + diff --git a/simulation/SyntheSimJava/gradle/libs.versions.toml b/simulation/SyntheSimJava/gradle/libs.versions.toml new file mode 100644 index 0000000000..db11383cd4 --- /dev/null +++ b/simulation/SyntheSimJava/gradle/libs.versions.toml @@ -0,0 +1,10 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format + +[versions] +commons-math3 = "3.6.1" +guava = "33.0.0-jre" + +[libraries] +commons-math3 = { module = "org.apache.commons:commons-math3", version.ref = "commons-math3" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } diff --git a/simulation/SyntheSimJava/gradle/wrapper/gradle-wrapper.jar b/simulation/SyntheSimJava/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e6441136f3d4ba8a0da8d277868979cfbc8ad796 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ zQccdaaVK$5bz;*rnyK{_SQYM>;aES6Qs^lj9lEs6_J+%nIiuQC*fN;z8md>r_~Mfl zU%p5Dt_YT>gQqfr@`cR!$NWr~+`CZb%dn;WtzrAOI>P_JtsB76PYe*<%H(y>qx-`Kq!X_; z<{RpAqYhE=L1r*M)gNF3B8r(<%8mo*SR2hu zccLRZwGARt)Hlo1euqTyM>^!HK*!Q2P;4UYrysje@;(<|$&%vQekbn|0Ruu_Io(w4#%p6ld2Yp7tlA`Y$cciThP zKzNGIMPXX%&Ud0uQh!uQZz|FB`4KGD?3!ND?wQt6!n*f4EmCoJUh&b?;B{|lxs#F- z31~HQ`SF4x$&v00@(P+j1pAaj5!s`)b2RDBp*PB=2IB>oBF!*6vwr7Dp%zpAx*dPr zb@Zjq^XjN?O4QcZ*O+8>)|HlrR>oD*?WQl5ri3R#2?*W6iJ>>kH%KnnME&TT@ZzrHS$Q%LC?n|e>V+D+8D zYc4)QddFz7I8#}y#Wj6>4P%34dZH~OUDb?uP%-E zwjXM(?Sg~1!|wI(RVuxbu)-rH+O=igSho_pDCw(c6b=P zKk4ATlB?bj9+HHlh<_!&z0rx13K3ZrAR8W)!@Y}o`?a*JJsD+twZIv`W)@Y?Amu_u zz``@-e2X}27$i(2=9rvIu5uTUOVhzwu%mNazS|lZb&PT;XE2|B&W1>=B58#*!~D&) zfVmJGg8UdP*fx(>Cj^?yS^zH#o-$Q-*$SnK(ZVFkw+er=>N^7!)FtP3y~Xxnu^nzY zikgB>Nj0%;WOltWIob|}%lo?_C7<``a5hEkx&1ku$|)i>Rh6@3h*`slY=9U}(Ql_< zaNG*J8vb&@zpdhAvv`?{=zDedJ23TD&Zg__snRAH4eh~^oawdYi6A3w8<Ozh@Kw)#bdktM^GVb zrG08?0bG?|NG+w^&JvD*7LAbjED{_Zkc`3H!My>0u5Q}m!+6VokMLXxl`Mkd=g&Xx z-a>m*#G3SLlhbKB!)tnzfWOBV;u;ftU}S!NdD5+YtOjLg?X}dl>7m^gOpihrf1;PY zvll&>dIuUGs{Qnd- zwIR3oIrct8Va^Tm0t#(bJD7c$Z7DO9*7NnRZorrSm`b`cxz>OIC;jSE3DO8`hX955ui`s%||YQtt2 z5DNA&pG-V+4oI2s*x^>-$6J?p=I>C|9wZF8z;VjR??Icg?1w2v5Me+FgAeGGa8(3S z4vg*$>zC-WIVZtJ7}o9{D-7d>zCe|z#<9>CFve-OPAYsneTb^JH!Enaza#j}^mXy1 z+ULn^10+rWLF6j2>Ya@@Kq?26>AqK{A_| zQKb*~F1>sE*=d?A?W7N2j?L09_7n+HGi{VY;MoTGr_)G9)ot$p!-UY5zZ2Xtbm=t z@dpPSGwgH=QtIcEulQNI>S-#ifbnO5EWkI;$A|pxJd885oM+ zGZ0_0gDvG8q2xebj+fbCHYfAXuZStH2j~|d^sBAzo46(K8n59+T6rzBwK)^rfPT+B zyIFw)9YC-V^rhtK`!3jrhmW-sTmM+tPH+;nwjL#-SjQPUZ53L@A>y*rt(#M(qsiB2 zx6B)dI}6Wlsw%bJ8h|(lhkJVogQZA&n{?Vgs6gNSXzuZpEyu*xySy8ro07QZ7Vk1!3tJphN_5V7qOiyK8p z#@jcDD8nmtYi1^l8ml;AF<#IPK?!pqf9D4moYk>d99Im}Jtwj6c#+A;f)CQ*f-hZ< z=p_T86jog%!p)D&5g9taSwYi&eP z#JuEK%+NULWus;0w32-SYFku#i}d~+{Pkho&^{;RxzP&0!RCm3-9K6`>KZpnzS6?L z^H^V*s!8<>x8bomvD%rh>Zp3>Db%kyin;qtl+jAv8Oo~1g~mqGAC&Qi_wy|xEt2iz zWAJEfTV%cl2Cs<1L&DLRVVH05EDq`pH7Oh7sR`NNkL%wi}8n>IXcO40hp+J+sC!W?!krJf!GJNE8uj zg-y~Ns-<~D?yqbzVRB}G>0A^f0!^N7l=$m0OdZuqAOQqLc zX?AEGr1Ht+inZ-Qiwnl@Z0qukd__a!C*CKuGdy5#nD7VUBM^6OCpxCa2A(X;e0&V4 zM&WR8+wErQ7UIc6LY~Q9x%Sn*Tn>>P`^t&idaOEnOd(Ufw#>NoR^1QdhJ8s`h^|R_ zXX`c5*O~Xdvh%q;7L!_!ohf$NfEBmCde|#uVZvEo>OfEq%+Ns7&_f$OR9xsihRpBb z+cjk8LyDm@U{YN>+r46?nn{7Gh(;WhFw6GAxtcKD+YWV?uge>;+q#Xx4!GpRkVZYu zzsF}1)7$?%s9g9CH=Zs+B%M_)+~*j3L0&Q9u7!|+T`^O{xE6qvAP?XWv9_MrZKdo& z%IyU)$Q95AB4!#hT!_dA>4e@zjOBD*Y=XjtMm)V|+IXzjuM;(l+8aA5#Kaz_$rR6! zj>#&^DidYD$nUY(D$mH`9eb|dtV0b{S>H6FBfq>t5`;OxA4Nn{J(+XihF(stSche7$es&~N$epi&PDM_N`As;*9D^L==2Q7Z2zD+CiU(|+-kL*VG+&9!Yb3LgPy?A zm7Z&^qRG_JIxK7-FBzZI3Q<;{`DIxtc48k> zc|0dmX;Z=W$+)qE)~`yn6MdoJ4co;%!`ddy+FV538Y)j(vg}5*k(WK)KWZ3WaOG!8 z!syGn=s{H$odtpqFrT#JGM*utN7B((abXnpDM6w56nhw}OY}0TiTG1#f*VFZr+^-g zbP10`$LPq_;PvrA1XXlyx2uM^mrjTzX}w{yuLo-cOClE8MMk47T25G8M!9Z5ypOSV zAJUBGEg5L2fY)ZGJb^E34R2zJ?}Vf>{~gB!8=5Z) z9y$>5c)=;o0HeHHSuE4U)#vG&KF|I%-cF6f$~pdYJWk_dD}iOA>iA$O$+4%@>JU08 zS`ep)$XLPJ+n0_i@PkF#ri6T8?ZeAot$6JIYHm&P6EB=BiaNY|aA$W0I+nz*zkz_z zkEru!tj!QUffq%)8y0y`T&`fuus-1p>=^hnBiBqD^hXrPs`PY9tU3m0np~rISY09> z`P3s=-kt_cYcxWd{de@}TwSqg*xVhp;E9zCsnXo6z z?f&Sv^U7n4`xr=mXle94HzOdN!2kB~4=%)u&N!+2;z6UYKUDqi-s6AZ!haB;@&B`? z_TRX0%@suz^TRdCb?!vNJYPY8L_}&07uySH9%W^Tc&1pia6y1q#?*Drf}GjGbPjBS zbOPcUY#*$3sL2x4v_i*Y=N7E$mR}J%|GUI(>WEr+28+V z%v5{#e!UF*6~G&%;l*q*$V?&r$Pp^sE^i-0$+RH3ERUUdQ0>rAq2(2QAbG}$y{de( z>{qD~GGuOk559Y@%$?N^1ApVL_a704>8OD%8Y%8B;FCt%AoPu8*D1 zLB5X>b}Syz81pn;xnB}%0FnwazlWfUV)Z-~rZg6~b z6!9J$EcE&sEbzcy?CI~=boWA&eeIa%z(7SE^qgVLz??1Vbc1*aRvc%Mri)AJaAG!p z$X!_9Ds;Zz)f+;%s&dRcJt2==P{^j3bf0M=nJd&xwUGlUFn?H=2W(*2I2Gdu zv!gYCwM10aeus)`RIZSrCK=&oKaO_Ry~D1B5!y0R=%!i2*KfXGYX&gNv_u+n9wiR5 z*e$Zjju&ODRW3phN925%S(jL+bCHv6rZtc?!*`1TyYXT6%Ju=|X;6D@lq$8T zW{Y|e39ioPez(pBH%k)HzFITXHvnD6hw^lIoUMA;qAJ^CU?top1fo@s7xT13Fvn1H z6JWa-6+FJF#x>~+A;D~;VDs26>^oH0EI`IYT2iagy23?nyJ==i{g4%HrAf1-*v zK1)~@&(KkwR7TL}L(A@C_S0G;-GMDy=MJn2$FP5s<%wC)4jC5PXoxrQBFZ_k0P{{s@sz+gX`-!=T8rcB(=7vW}^K6oLWMmp(rwDh}b zwaGGd>yEy6fHv%jM$yJXo5oMAQ>c9j`**}F?MCry;T@47@r?&sKHgVe$MCqk#Z_3S z1GZI~nOEN*P~+UaFGnj{{Jo@16`(qVNtbU>O0Hf57-P>x8Jikp=`s8xWs^dAJ9lCQ z)GFm+=OV%AMVqVATtN@|vp61VVAHRn87}%PC^RAzJ%JngmZTasWBAWsoAqBU+8L8u z4A&Pe?fmTm0?mK-BL9t+{y7o(7jm+RpOhL9KnY#E&qu^}B6=K_dB}*VlSEiC9fn)+V=J;OnN)Ta5v66ic1rG+dGAJ1 z1%Zb_+!$=tQ~lxQrzv3x#CPb?CekEkA}0MYSgx$Jdd}q8+R=ma$|&1a#)TQ=l$1tQ z=tL9&_^vJ)Pk}EDO-va`UCT1m#Uty1{v^A3P~83_#v^ozH}6*9mIjIr;t3Uv%@VeW zGL6(CwCUp)Jq%G0bIG%?{_*Y#5IHf*5M@wPo6A{$Um++Co$wLC=J1aoG93&T7Ho}P z=mGEPP7GbvoG!uD$k(H3A$Z))+i{Hy?QHdk>3xSBXR0j!11O^mEe9RHmw!pvzv?Ua~2_l2Yh~_!s1qS`|0~0)YsbHSz8!mG)WiJE| z2f($6TQtt6L_f~ApQYQKSb=`053LgrQq7G@98#igV>y#i==-nEjQ!XNu9 z~;mE+gtj4IDDNQJ~JVk5Ux6&LCSFL!y=>79kE9=V}J7tD==Ga+IW zX)r7>VZ9dY=V&}DR))xUoV!u(Z|%3ciQi_2jl}3=$Agc(`RPb z8kEBpvY>1FGQ9W$n>Cq=DIpski};nE)`p3IUw1Oz0|wxll^)4dq3;CCY@RyJgFgc# zKouFh!`?Xuo{IMz^xi-h=StCis_M7yq$u) z?XHvw*HP0VgR+KR6wI)jEMX|ssqYvSf*_3W8zVTQzD?3>H!#>InzpSO)@SC8q*ii- z%%h}_#0{4JG;Jm`4zg};BPTGkYamx$Xo#O~lBirRY)q=5M45n{GCfV7h9qwyu1NxOMoP4)jjZMxmT|IQQh0U7C$EbnMN<3)Kk?fFHYq$d|ICu>KbY_hO zTZM+uKHe(cIZfEqyzyYSUBZa8;Fcut-GN!HSA9ius`ltNebF46ZX_BbZNU}}ZOm{M2&nANL9@0qvih15(|`S~z}m&h!u4x~(%MAO$jHRWNfuxWF#B)E&g3ghSQ9|> z(MFaLQj)NE0lowyjvg8z0#m6FIuKE9lDO~Glg}nSb7`~^&#(Lw{}GVOS>U)m8bF}x zVjbXljBm34Cs-yM6TVusr+3kYFjr28STT3g056y3cH5Tmge~ASxBj z%|yb>$eF;WgrcOZf569sDZOVwoo%8>XO>XQOX1OyN9I-SQgrm;U;+#3OI(zrWyow3 zk==|{lt2xrQ%FIXOTejR>;wv(Pb8u8}BUpx?yd(Abh6? zsoO3VYWkeLnF43&@*#MQ9-i-d0t*xN-UEyNKeyNMHw|A(k(_6QKO=nKMCxD(W(Yop zsRQ)QeL4X3Lxp^L%wzi2-WVSsf61dqliPUM7srDB?Wm6Lzn0&{*}|IsKQW;02(Y&| zaTKv|`U(pSzuvR6Rduu$wzK_W-Y-7>7s?G$)U}&uK;<>vU}^^ns@Z!p+9?St1s)dG zK%y6xkPyyS1$~&6v{kl?Md6gwM|>mt6Upm>oa8RLD^8T{0?HC!Z>;(Bob7el(DV6x zi`I)$&E&ngwFS@bi4^xFLAn`=fzTC;aimE^!cMI2n@Vo%Ae-ne`RF((&5y6xsjjAZ zVguVoQ?Z9uk$2ON;ersE%PU*xGO@T*;j1BO5#TuZKEf(mB7|g7pcEA=nYJ{s3vlbg zd4-DUlD{*6o%Gc^N!Nptgay>j6E5;3psI+C3Q!1ZIbeCubW%w4pq9)MSDyB{HLm|k zxv-{$$A*pS@csolri$Ge<4VZ}e~78JOL-EVyrbxKra^d{?|NnPp86!q>t<&IP07?Z z^>~IK^k#OEKgRH+LjllZXk7iA>2cfH6+(e&9ku5poo~6y{GC5>(bRK7hwjiurqAiZ zg*DmtgY}v83IjE&AbiWgMyFbaRUPZ{lYiz$U^&Zt2YjG<%m((&_JUbZcfJ22(>bi5 z!J?<7AySj0JZ&<-qXX;mcV!f~>G=sB0KnjWca4}vrtunD^1TrpfeS^4dvFr!65knK zZh`d;*VOkPs4*-9kL>$GP0`(M!j~B;#x?Ba~&s6CopvO86oM?-? zOw#dIRc;6A6T?B`Qp%^<U5 z19x(ywSH$_N+Io!6;e?`tWaM$`=Db!gzx|lQ${DG!zb1Zl&|{kX0y6xvO1o z220r<-oaS^^R2pEyY;=Qllqpmue|5yI~D|iI!IGt@iod{Opz@*ml^w2bNs)p`M(Io z|E;;m*Xpjd9l)4G#KaWfV(t8YUn@A;nK^#xgv=LtnArX|vWQVuw3}B${h+frU2>9^ z!l6)!Uo4`5k`<<;E(ido7M6lKTgWezNLq>U*=uz&s=cc$1%>VrAeOoUtA|T6gO4>UNqsdK=NF*8|~*sl&wI=x9-EGiq*aqV!(VVXA57 zw9*o6Ir8Lj1npUXvlevtn(_+^X5rzdR>#(}4YcB9O50q97%rW2me5_L=%ffYPUSRc z!vv?Kv>dH994Qi>U(a<0KF6NH5b16enCp+mw^Hb3Xs1^tThFpz!3QuN#}KBbww`(h z7GO)1olDqy6?T$()R7y%NYx*B0k_2IBiZ14&8|JPFxeMF{vSTxF-Vi3+ZOI=Thq2} zyQgjYY1_7^ZQHh{?P))4+qUiQJLi1&{yE>h?~jU%tjdV0h|FENbM3X(KnJdPKc?~k zh=^Ixv*+smUll!DTWH!jrV*wSh*(mx0o6}1@JExzF(#9FXgmTXVoU+>kDe68N)dkQ zH#_98Zv$}lQwjKL@yBd;U(UD0UCl322=pav<=6g>03{O_3oKTq;9bLFX1ia*lw;#K zOiYDcBJf)82->83N_Y(J7Kr_3lE)hAu;)Q(nUVydv+l+nQ$?|%MWTy`t>{havFSQloHwiIkGK9YZ79^9?AZo0ZyQlVR#}lF%dn5n%xYksXf8gnBm=wO7g_^! zauQ-bH1Dc@3ItZ-9D_*pH}p!IG7j8A_o94#~>$LR|TFq zZ-b00*nuw|-5C2lJDCw&8p5N~Z1J&TrcyErds&!l3$eSz%`(*izc;-?HAFD9AHb-| z>)id`QCrzRws^9(#&=pIx9OEf2rmlob8sK&xPCWS+nD~qzU|qG6KwA{zbikcfQrdH z+ zQg>O<`K4L8rN7`GJB0*3<3`z({lWe#K!4AZLsI{%z#ja^OpfjU{!{)x0ZH~RB0W5X zTwN^w=|nA!4PEU2=LR05x~}|B&ZP?#pNgDMwD*ajI6oJqv!L81gu=KpqH22avXf0w zX3HjbCI!n9>l046)5rr5&v5ja!xkKK42zmqHzPx$9Nn_MZk`gLeSLgC=LFf;H1O#B zn=8|^1iRrujHfbgA+8i<9jaXc;CQBAmQvMGQPhFec2H1knCK2x!T`e6soyrqCamX% zTQ4dX_E*8so)E*TB$*io{$c6X)~{aWfaqdTh=xEeGvOAN9H&-t5tEE-qso<+C!2>+ zskX51H-H}#X{A75wqFe-J{?o8Bx|>fTBtl&tcbdR|132Ztqu5X0i-pisB-z8n71%q%>EF}yy5?z=Ve`}hVh{Drv1YWL zW=%ug_&chF11gDv3D6B)Tz5g54H0mDHNjuKZ+)CKFk4Z|$RD zfRuKLW`1B>B?*RUfVd0+u8h3r-{@fZ{k)c!93t1b0+Q9vOaRnEn1*IL>5Z4E4dZ!7 ztp4GP-^1d>8~LMeb}bW!(aAnB1tM_*la=Xx)q(I0Y@__Zd$!KYb8T2VBRw%e$iSdZ zkwdMwd}eV9q*;YvrBFTv1>1+}{H!JK2M*C|TNe$ZSA>UHKk);wz$(F$rXVc|sI^lD zV^?_J!3cLM;GJuBMbftbaRUs$;F}HDEDtIeHQ)^EJJ1F9FKJTGH<(Jj`phE6OuvE) zqK^K`;3S{Y#1M@8yRQwH`?kHMq4tHX#rJ>5lY3DM#o@or4&^_xtBC(|JpGTfrbGkA z2Tu+AyT^pHannww!4^!$5?@5v`LYy~T`qs7SYt$JgrY(w%C+IWA;ZkwEF)u5sDvOK zGk;G>Mh&elvXDcV69J_h02l&O;!{$({fng9Rlc3ID#tmB^FIG^w{HLUpF+iB`|
NnX)EH+Nua)3Y(c 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/simulation/SyntheSimJava/gradlew.bat b/simulation/SyntheSimJava/gradlew.bat new file mode 100644 index 0000000000..25da30dbde --- /dev/null +++ b/simulation/SyntheSimJava/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/simulation/SyntheSimJava/settings.gradle b/simulation/SyntheSimJava/settings.gradle new file mode 100644 index 0000000000..9bc6b745d3 --- /dev/null +++ b/simulation/SyntheSimJava/settings.gradle @@ -0,0 +1,15 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.8/userguide/multi_project_builds.html in the Gradle documentation. + * This project uses @Incubating APIs which are subject to change. + */ + +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' +} + +// rootProject.name = 'SyntheSimJava' +// include('lib') diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANEncoder.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANEncoder.java new file mode 100644 index 0000000000..fa74dbbe0f --- /dev/null +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANEncoder.java @@ -0,0 +1,50 @@ +package com.autodesk.synthesis; + +import edu.wpi.first.hal.SimDevice; +import edu.wpi.first.hal.SimDouble; +import edu.wpi.first.hal.SimDevice.Direction; + +/** + * CANEncoder class for easy implementation of documentation-compliant simulation data. + * + * See https://github.com/wpilibsuite/allwpilib/blob/6478ba6e3fa317ee041b8a41e562d925602b6ea4/simulation/halsim_ws_core/doc/hardware_ws_api.md + * for documentation on the WebSocket API Specification. + */ +public class CANEncoder { + + private SimDevice m_device; + + private SimDouble m_position; + private SimDouble m_velocity; + + /** + * Creates an Encoder accessed by the CANBus. + * + * @param name Name of the CAN Motor. This is generally the class name of the originating encoder, prefixed with something (ie. "SYN CANSparkMax/Encoder"). + * @param deviceId CAN Device ID. + */ + public CANEncoder(String name, int deviceId) { + m_device = SimDevice.create(name, deviceId); + + m_position = m_device.createDouble("position", Direction.kInput, 0.0); + m_velocity = m_device.createDouble("velocity", Direction.kInput, 0.0); + } + + /** + * Gets the current position of the encoder, simulated. + * + * @return Current Position. + */ + public double getPosition() { + return m_position.get(); + } + + /** + * Gets the current velocity of the encoder, simulated. + * + * @return Current Velocity. + */ + public double getVelocity() { + return m_velocity.get(); + } +} diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANMotor.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANMotor.java new file mode 100644 index 0000000000..eeca38ed7b --- /dev/null +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANMotor.java @@ -0,0 +1,108 @@ +package com.autodesk.synthesis; + +import edu.wpi.first.hal.SimBoolean; +import edu.wpi.first.hal.SimDevice; +import edu.wpi.first.hal.SimDevice.Direction; +import edu.wpi.first.hal.SimDouble; + +/** + * CANMotor class for easy implementation of documentation-compliant simulation data. + * + * See https://github.com/wpilibsuite/allwpilib/blob/6478ba6e3fa317ee041b8a41e562d925602b6ea4/simulation/halsim_ws_core/doc/hardware_ws_api.md + * for documentation on the WebSocket API Specification. + */ +public class CANMotor { + + private SimDevice m_device; + + private SimDouble m_percentOutput; + private SimBoolean m_brakeMode; + private SimDouble m_neutralDeadband; + + private SimDouble m_supplyCurrent; + private SimDouble m_motorCurrent; + private SimDouble m_busVoltage; + + /** + * Creates a CANMotor sim device in accordance with the WebSocket API Specification. + * + * @param name Name of the CAN Motor. This is generally the class name of the originating motor, prefixed with something (ie. "SYN CANSparkMax"). + * @param deviceId CAN Device ID. + * @param defaultPercentOutput Default PercentOutput value. [-1.0, 1.0] + * @param defaultBrakeMode Default BrakeMode value. (true/false) + * @param defaultNeutralDeadband Default Neutral Deadband value. This is used to determine when braking should be enabled. [0.0, 1.0] + */ + public CANMotor(String name, int deviceId, double defaultPercentOutput, boolean defaultBrakeMode, double defaultNeutralDeadband) { + m_device = SimDevice.create(name, deviceId); + + m_percentOutput = m_device.createDouble("percentOutput", Direction.kOutput, 0.0); + m_brakeMode = m_device.createBoolean("brakeMode", Direction.kOutput, false); + m_neutralDeadband = m_device.createDouble("neutralDeadband", Direction.kOutput, deviceId); + + m_supplyCurrent = m_device.createDouble("supplyCurrent", Direction.kInput, 120.0); + m_motorCurrent = m_device.createDouble("motorCurrent", Direction.kInput, 120.0); + m_busVoltage = m_device.createDouble("busVoltage", Direction.kInput, 12.0); + } + + /** + * Set the PercentOutput of the motor. + * + * @param percent [-1.0, 1.0] + */ + public void setPercentOutput(double percent) { + if (Double.isNaN(percent) || Double.isInfinite(percent)) { + percent = 0.0; + } + + m_percentOutput.set(Math.min(1.0, Math.max(-1.0, percent))); + } + + /** + * Set the BrakeMode of the motor. + * + * @param brake True to enable braking. False to not. + */ + public void setBrakeMode(boolean brake) { + m_brakeMode.set(brake); + } + + /** + * Set the neutral deadband of the motor. Essentially when to enable brake mode. + * + * @param deadband [0.0, 1.0] + */ + public void setNeutralDeadband(double deadband) { + if (Double.isNaN(deadband) || Double.isInfinite(deadband)) { + deadband = 0.0; + } + + m_neutralDeadband.set(Math.min(1.0, Math.max(0.0, deadband))); + } + + /** + * Get the supply current, simulated. + * + * @return Supply Current. + */ + public double getSupplyCurrent() { + return m_supplyCurrent.get(); + } + + /** + * Get the motor current, simulated. + * + * @return Motor Current. + */ + public double getMotorCurrent() { + return m_motorCurrent.get(); + } + + /** + * Get the Bus Voltage, simulated. + * + * @return Bus Voltage + */ + public double getBusVoltage() { + return m_busVoltage.get(); + } +} diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/ctre/TalonFX.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/ctre/TalonFX.java new file mode 100644 index 0000000000..63ed5b1e9c --- /dev/null +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/ctre/TalonFX.java @@ -0,0 +1,13 @@ +package com.autodesk.synthesis.ctre; + +import com.autodesk.synthesis.CANMotor; + +public class TalonFX extends com.ctre.phoenix6.hardware.TalonFX { + private CANMotor m_motor; + + public TalonFX(int deviceId) { + super(deviceId); + + m_motor = new CANMotor("SYN TalonFX", deviceId, 0.0, false, 0.3); + } +} diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java new file mode 100644 index 0000000000..da2087e7ab --- /dev/null +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java @@ -0,0 +1,43 @@ +package com.autodesk.synthesis.revrobotics; + +import com.autodesk.synthesis.CANEncoder; +import com.autodesk.synthesis.CANMotor; +import com.revrobotics.REVLibError; + +/** + * CANSparkMax wrapper to add proper WPILib HALSim support. + */ +public class CANSparkMax extends com.revrobotics.CANSparkMax { + + private CANMotor m_motor; + private CANEncoder m_encoder; + + /** + * Creates a new CANSparkMax, wrapped with simulation support. + * + * @param deviceId CAN Device ID. + * @param motorType Motortype. For Simulation purposes, this is discarded at the moment. + */ + public CANSparkMax(int deviceId, MotorType motorType) { + super(deviceId, motorType); + + m_motor = new CANMotor("SYN CANSparkMax", deviceId, 0.0, false, 0.3); + m_encoder = new CANEncoder("SYN CANSparkMax/Encoder", deviceId); + } + + @Override + public void set(double percent) { + super.set(percent); + m_motor.setPercentOutput(percent); + } + + @Override + public REVLibError setIdleMode(com.revrobotics.CANSparkBase.IdleMode mode) { + if (mode != null) { + m_motor.setBrakeMode(mode.equals(com.revrobotics.CANSparkBase.IdleMode.kBrake)); + } + + return super.setIdleMode(mode); + } + +} diff --git a/simulation/JavaSample/.gitignore b/simulation/samples/JavaSample/.gitignore similarity index 100% rename from simulation/JavaSample/.gitignore rename to simulation/samples/JavaSample/.gitignore diff --git a/simulation/JavaSample/WPILib-License.md b/simulation/samples/JavaSample/WPILib-License.md similarity index 100% rename from simulation/JavaSample/WPILib-License.md rename to simulation/samples/JavaSample/WPILib-License.md diff --git a/simulation/JavaSample/build.gradle b/simulation/samples/JavaSample/build.gradle similarity index 97% rename from simulation/JavaSample/build.gradle rename to simulation/samples/JavaSample/build.gradle index 7735bda8e6..5acbea004b 100644 --- a/simulation/JavaSample/build.gradle +++ b/simulation/samples/JavaSample/build.gradle @@ -3,6 +3,10 @@ plugins { id "edu.wpi.first.GradleRIO" version "2024.3.2" } +repositories { + mavenLocal() +} + // wpi.maven.useLocal = false // wpi.maven.useFrcMavenLocalDevelopment = true // wpi.versions.wpilibVersion = '2024.424242.+' @@ -74,6 +78,8 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + implementation "com.autodesk.synthesis:SyntheSimJava:1.0.0" } test { diff --git a/simulation/JavaSample/gradle/wrapper/gradle-wrapper.jar b/simulation/samples/JavaSample/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from simulation/JavaSample/gradle/wrapper/gradle-wrapper.jar rename to simulation/samples/JavaSample/gradle/wrapper/gradle-wrapper.jar diff --git a/simulation/JavaSample/gradle/wrapper/gradle-wrapper.properties b/simulation/samples/JavaSample/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from simulation/JavaSample/gradle/wrapper/gradle-wrapper.properties rename to simulation/samples/JavaSample/gradle/wrapper/gradle-wrapper.properties diff --git a/simulation/JavaSample/gradlew b/simulation/samples/JavaSample/gradlew old mode 100755 new mode 100644 similarity index 100% rename from simulation/JavaSample/gradlew rename to simulation/samples/JavaSample/gradlew diff --git a/simulation/JavaSample/gradlew.bat b/simulation/samples/JavaSample/gradlew.bat similarity index 100% rename from simulation/JavaSample/gradlew.bat rename to simulation/samples/JavaSample/gradlew.bat diff --git a/simulation/JavaSample/settings.gradle b/simulation/samples/JavaSample/settings.gradle similarity index 100% rename from simulation/JavaSample/settings.gradle rename to simulation/samples/JavaSample/settings.gradle diff --git a/simulation/JavaSample/src/main/deploy/example.txt b/simulation/samples/JavaSample/src/main/deploy/example.txt similarity index 100% rename from simulation/JavaSample/src/main/deploy/example.txt rename to simulation/samples/JavaSample/src/main/deploy/example.txt diff --git a/simulation/JavaSample/src/main/java/frc/robot/Main.java b/simulation/samples/JavaSample/src/main/java/frc/robot/Main.java similarity index 100% rename from simulation/JavaSample/src/main/java/frc/robot/Main.java rename to simulation/samples/JavaSample/src/main/java/frc/robot/Main.java diff --git a/simulation/JavaSample/src/main/java/frc/robot/Robot.java b/simulation/samples/JavaSample/src/main/java/frc/robot/Robot.java similarity index 100% rename from simulation/JavaSample/src/main/java/frc/robot/Robot.java rename to simulation/samples/JavaSample/src/main/java/frc/robot/Robot.java From b9db0946aff58929f2c2bd460f06d68a5a0ce928 Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Mon, 8 Jul 2024 11:07:11 -0600 Subject: [PATCH 039/121] Added Cpp sampel --- simulation/samples/CppSample/.gitignore | 4 + .../samples/CppSample/WPILib-License.md | 24 ++ simulation/samples/CppSample/build.gradle | 99 +++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43462 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + simulation/samples/CppSample/gradlew | 249 +++++++++++++ simulation/samples/CppSample/gradlew.bat | 92 +++++ simulation/samples/CppSample/settings.gradle | 30 ++ .../samples/CppSample/src/main/cpp/Robot.cpp | 78 ++++ .../CppSample/src/main/deploy/example.txt | 4 + .../CppSample/src/main/include/Robot.h | 32 ++ .../samples/CppSample/src/test/cpp/main.cpp | 10 + .../samples/CppSample/vendordeps/NavX.json | 40 +++ .../CppSample/vendordeps/Phoenix6.json | 339 ++++++++++++++++++ .../samples/CppSample/vendordeps/REVLib.json | 74 ++++ .../vendordeps/WPILibNewCommands.json | 38 ++ 16 files changed, 1120 insertions(+) create mode 100644 simulation/samples/CppSample/.gitignore create mode 100644 simulation/samples/CppSample/WPILib-License.md create mode 100644 simulation/samples/CppSample/build.gradle create mode 100644 simulation/samples/CppSample/gradle/wrapper/gradle-wrapper.jar create mode 100644 simulation/samples/CppSample/gradle/wrapper/gradle-wrapper.properties create mode 100644 simulation/samples/CppSample/gradlew create mode 100644 simulation/samples/CppSample/gradlew.bat create mode 100644 simulation/samples/CppSample/settings.gradle create mode 100644 simulation/samples/CppSample/src/main/cpp/Robot.cpp create mode 100644 simulation/samples/CppSample/src/main/deploy/example.txt create mode 100644 simulation/samples/CppSample/src/main/include/Robot.h create mode 100644 simulation/samples/CppSample/src/test/cpp/main.cpp create mode 100644 simulation/samples/CppSample/vendordeps/NavX.json create mode 100644 simulation/samples/CppSample/vendordeps/Phoenix6.json create mode 100644 simulation/samples/CppSample/vendordeps/REVLib.json create mode 100644 simulation/samples/CppSample/vendordeps/WPILibNewCommands.json diff --git a/simulation/samples/CppSample/.gitignore b/simulation/samples/CppSample/.gitignore new file mode 100644 index 0000000000..11c9fdd738 --- /dev/null +++ b/simulation/samples/CppSample/.gitignore @@ -0,0 +1,4 @@ +.gradle/ +.vscode/ +.wpilib/ +build/ diff --git a/simulation/samples/CppSample/WPILib-License.md b/simulation/samples/CppSample/WPILib-License.md new file mode 100644 index 0000000000..645e54253a --- /dev/null +++ b/simulation/samples/CppSample/WPILib-License.md @@ -0,0 +1,24 @@ +Copyright (c) 2009-2024 FIRST and other WPILib contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of FIRST, WPILib, nor the names of other WPILib + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY FIRST AND OTHER WPILIB CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/simulation/samples/CppSample/build.gradle b/simulation/samples/CppSample/build.gradle new file mode 100644 index 0000000000..5692953ce6 --- /dev/null +++ b/simulation/samples/CppSample/build.gradle @@ -0,0 +1,99 @@ +plugins { + id "cpp" + id "google-test-test-suite" + id "edu.wpi.first.GradleRIO" version "2024.3.2" +} + +// Define my targets (RoboRIO) and artifacts (deployable files) +// This is added by GradleRIO's backing project DeployUtils. +deploy { + targets { + roborio(getTargetTypeClass('RoboRIO')) { + // Team number is loaded either from the .wpilib/wpilib_preferences.json + // or from command line. If not found an exception will be thrown. + // You can use getTeamOrDefault(team) instead of getTeamNumber if you + // want to store a team number in this file. + team = project.frc.getTeamOrDefault(997) + debug = project.frc.getDebugOrDefault(false) + + artifacts { + // First part is artifact name, 2nd is artifact type + // getTargetTypeClass is a shortcut to get the class type using a string + + frcCpp(getArtifactTypeClass('FRCNativeArtifact')) { + } + + // Static files artifact + frcStaticFileDeploy(getArtifactTypeClass('FileTreeArtifact')) { + files = project.fileTree('src/main/deploy') + directory = '/home/lvuser/deploy' + } + } + } + } +} + +def deployArtifact = deploy.targets.roborio.artifacts.frcCpp + +// Set this to true to enable desktop support. +def includeDesktopSupport = true + +// Set to true to run simulation in debug mode +wpi.cpp.debugSimulation = false + +// Default enable simgui +wpi.sim.addGui().defaultEnabled = true +// Enable DS but not by default +wpi.sim.addDriverstation() + +model { + components { + frcUserProgram(NativeExecutableSpec) { + targetPlatform wpi.platforms.roborio + if (includeDesktopSupport) { + targetPlatform wpi.platforms.desktop + } + + sources.cpp { + source { + srcDir 'src/main/cpp' + include '**/*.cpp', '**/*.cc' + } + exportedHeaders { + srcDir 'src/main/include' + } + } + + // Set deploy task to deploy this component + deployArtifact.component = it + + // Enable run tasks for this component + wpi.cpp.enableExternalTasks(it) + + // Enable simulation for this component + wpi.sim.enable(it) + // Defining my dependencies. In this case, WPILib (+ friends), and vendor libraries. + wpi.cpp.vendor.cpp(it) + wpi.cpp.deps.wpilib(it) + } + } + testSuites { + frcUserProgramTest(GoogleTestTestSuiteSpec) { + testing $.components.frcUserProgram + + sources.cpp { + source { + srcDir 'src/test/cpp' + include '**/*.cpp' + } + } + + // Enable run tasks for this component + wpi.cpp.enableExternalTasks(it) + + wpi.cpp.vendor.cpp(it) + wpi.cpp.deps.wpilib(it) + wpi.cpp.deps.googleTest(it) + } + } +} diff --git a/simulation/samples/CppSample/gradle/wrapper/gradle-wrapper.jar b/simulation/samples/CppSample/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d64cd4917707c1f8861d8cb53dd15194d4248596 GIT binary patch literal 43462 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 literal 0 HcmV?d00001 diff --git a/simulation/samples/CppSample/gradle/wrapper/gradle-wrapper.properties b/simulation/samples/CppSample/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..5e82d67b9f --- /dev/null +++ b/simulation/samples/CppSample/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=permwrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=permwrapper/dists diff --git a/simulation/samples/CppSample/gradlew b/simulation/samples/CppSample/gradlew new file mode 100644 index 0000000000..1aa94a4269 --- /dev/null +++ b/simulation/samples/CppSample/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/simulation/samples/CppSample/gradlew.bat b/simulation/samples/CppSample/gradlew.bat new file mode 100644 index 0000000000..93e3f59f13 --- /dev/null +++ b/simulation/samples/CppSample/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/simulation/samples/CppSample/settings.gradle b/simulation/samples/CppSample/settings.gradle new file mode 100644 index 0000000000..d94f73c635 --- /dev/null +++ b/simulation/samples/CppSample/settings.gradle @@ -0,0 +1,30 @@ +import org.gradle.internal.os.OperatingSystem + +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + String frcYear = '2024' + File frcHome + if (OperatingSystem.current().isWindows()) { + String publicFolder = System.getenv('PUBLIC') + if (publicFolder == null) { + publicFolder = "C:\\Users\\Public" + } + def homeRoot = new File(publicFolder, "wpilib") + frcHome = new File(homeRoot, frcYear) + } else { + def userFolder = System.getProperty("user.home") + def homeRoot = new File(userFolder, "wpilib") + frcHome = new File(homeRoot, frcYear) + } + def frcHomeMaven = new File(frcHome, 'maven') + maven { + name 'frcHome' + url frcHomeMaven + } + } +} + +Properties props = System.getProperties(); +props.setProperty("org.gradle.internal.native.headers.unresolved.dependencies.ignore", "true"); diff --git a/simulation/samples/CppSample/src/main/cpp/Robot.cpp b/simulation/samples/CppSample/src/main/cpp/Robot.cpp new file mode 100644 index 0000000000..f74262bef9 --- /dev/null +++ b/simulation/samples/CppSample/src/main/cpp/Robot.cpp @@ -0,0 +1,78 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "Robot.h" + +#include +#include + +void Robot::RobotInit() { + m_chooser.SetDefaultOption(kAutoNameDefault, kAutoNameDefault); + m_chooser.AddOption(kAutoNameCustom, kAutoNameCustom); + frc::SmartDashboard::PutData("Auto Modes", &m_chooser); +} + +/** + * This function is called every 20 ms, no matter the mode. Use + * this for items like diagnostics that you want ran during disabled, + * autonomous, teleoperated and test. + * + *

This runs after the mode specific periodic functions, but before + * LiveWindow and SmartDashboard integrated updating. + */ +void Robot::RobotPeriodic() {} + +/** + * This autonomous (along with the chooser code above) shows how to select + * between different autonomous modes using the dashboard. The sendable chooser + * code works with the Java SmartDashboard. If you prefer the LabVIEW Dashboard, + * remove all of the chooser code and uncomment the GetString line to get the + * auto name from the text box below the Gyro. + * + * You can add additional auto modes by adding additional comparisons to the + * if-else structure below with additional strings. If using the SendableChooser + * make sure to add them to the chooser code above as well. + */ +void Robot::AutonomousInit() { + m_autoSelected = m_chooser.GetSelected(); + // m_autoSelected = SmartDashboard::GetString("Auto Selector", + // kAutoNameDefault); + fmt::print("Auto selected: {}\n", m_autoSelected); + + if (m_autoSelected == kAutoNameCustom) { + // Custom Auto goes here + } else { + // Default Auto goes here + } +} + +void Robot::AutonomousPeriodic() { + if (m_autoSelected == kAutoNameCustom) { + // Custom Auto goes here + } else { + // Default Auto goes here + } +} + +void Robot::TeleopInit() {} + +void Robot::TeleopPeriodic() {} + +void Robot::DisabledInit() {} + +void Robot::DisabledPeriodic() {} + +void Robot::TestInit() {} + +void Robot::TestPeriodic() {} + +void Robot::SimulationInit() {} + +void Robot::SimulationPeriodic() {} + +#ifndef RUNNING_FRC_TESTS +int main() { + return frc::StartRobot(); +} +#endif diff --git a/simulation/samples/CppSample/src/main/deploy/example.txt b/simulation/samples/CppSample/src/main/deploy/example.txt new file mode 100644 index 0000000000..683953917e --- /dev/null +++ b/simulation/samples/CppSample/src/main/deploy/example.txt @@ -0,0 +1,4 @@ +Files placed in this directory will be deployed to the RoboRIO into the + 'deploy' directory in the home folder. Use the 'frc::filesystem::GetDeployDirectory' + function from the 'frc/Filesystem.h' header to get a proper path relative to the deploy + directory. \ No newline at end of file diff --git a/simulation/samples/CppSample/src/main/include/Robot.h b/simulation/samples/CppSample/src/main/include/Robot.h new file mode 100644 index 0000000000..5677a88fe5 --- /dev/null +++ b/simulation/samples/CppSample/src/main/include/Robot.h @@ -0,0 +1,32 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include + +#include +#include + +class Robot : public frc::TimedRobot { + public: + void RobotInit() override; + void RobotPeriodic() override; + void AutonomousInit() override; + void AutonomousPeriodic() override; + void TeleopInit() override; + void TeleopPeriodic() override; + void DisabledInit() override; + void DisabledPeriodic() override; + void TestInit() override; + void TestPeriodic() override; + void SimulationInit() override; + void SimulationPeriodic() override; + + private: + frc::SendableChooser m_chooser; + const std::string kAutoNameDefault = "Default"; + const std::string kAutoNameCustom = "My Auto"; + std::string m_autoSelected; +}; diff --git a/simulation/samples/CppSample/src/test/cpp/main.cpp b/simulation/samples/CppSample/src/test/cpp/main.cpp new file mode 100644 index 0000000000..b8b23d2382 --- /dev/null +++ b/simulation/samples/CppSample/src/test/cpp/main.cpp @@ -0,0 +1,10 @@ +#include + +#include "gtest/gtest.h" + +int main(int argc, char** argv) { + HAL_Initialize(500, 0); + ::testing::InitGoogleTest(&argc, argv); + int ret = RUN_ALL_TESTS(); + return ret; +} diff --git a/simulation/samples/CppSample/vendordeps/NavX.json b/simulation/samples/CppSample/vendordeps/NavX.json new file mode 100644 index 0000000000..e978a5f745 --- /dev/null +++ b/simulation/samples/CppSample/vendordeps/NavX.json @@ -0,0 +1,40 @@ +{ + "fileName": "NavX.json", + "name": "NavX", + "version": "2024.1.0", + "uuid": "cb311d09-36e9-4143-a032-55bb2b94443b", + "frcYear": "2024", + "mavenUrls": [ + "https://dev.studica.com/maven/release/2024/" + ], + "jsonUrl": "https://dev.studica.com/releases/2024/NavX.json", + "javaDependencies": [ + { + "groupId": "com.kauailabs.navx.frc", + "artifactId": "navx-frc-java", + "version": "2024.1.0" + } + ], + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "com.kauailabs.navx.frc", + "artifactId": "navx-frc-cpp", + "version": "2024.1.0", + "headerClassifier": "headers", + "sourcesClassifier": "sources", + "sharedLibrary": false, + "libName": "navx_frc", + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena", + "linuxraspbian", + "linuxarm32", + "linuxarm64", + "linuxx86-64", + "osxuniversal", + "windowsx86-64" + ] + } + ] +} \ No newline at end of file diff --git a/simulation/samples/CppSample/vendordeps/Phoenix6.json b/simulation/samples/CppSample/vendordeps/Phoenix6.json new file mode 100644 index 0000000000..032238505f --- /dev/null +++ b/simulation/samples/CppSample/vendordeps/Phoenix6.json @@ -0,0 +1,339 @@ +{ + "fileName": "Phoenix6.json", + "name": "CTRE-Phoenix (v6)", + "version": "24.3.0", + "frcYear": 2024, + "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", + "mavenUrls": [ + "https://maven.ctr-electronics.com/release/" + ], + "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-frc2024-latest.json", + "conflictsWith": [ + { + "uuid": "3fcf3402-e646-4fa6-971e-18afe8173b1a", + "errorMessage": "The combined Phoenix-6-And-5 vendordep is no longer supported. Please remove the vendordep and instead add both the latest Phoenix 6 vendordep and Phoenix 5 vendordep.", + "offlineFileName": "Phoenix6And5.json" + } + ], + "javaDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "wpiapi-java", + "version": "24.3.0" + } + ], + "jniDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "tools", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "tools-sim", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonSRX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonFX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simVictorSPX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simPigeonIMU", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simCANCoder", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANcoder", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProPigeon2", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + } + ], + "cppDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "wpiapi-cpp", + "version": "24.3.0", + "libName": "CTRE_Phoenix6_WPI", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6", + "artifactId": "tools", + "version": "24.3.0", + "libName": "CTRE_PhoenixTools", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "wpiapi-cpp-sim", + "version": "24.3.0", + "libName": "CTRE_Phoenix6_WPISim", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "tools-sim", + "version": "24.3.0", + "libName": "CTRE_PhoenixTools_Sim", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonSRX", + "version": "24.3.0", + "libName": "CTRE_SimTalonSRX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonFX", + "version": "24.3.0", + "libName": "CTRE_SimTalonFX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simVictorSPX", + "version": "24.3.0", + "libName": "CTRE_SimVictorSPX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simPigeonIMU", + "version": "24.3.0", + "libName": "CTRE_SimPigeonIMU", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simCANCoder", + "version": "24.3.0", + "libName": "CTRE_SimCANCoder", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFX", + "version": "24.3.0", + "libName": "CTRE_SimProTalonFX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANcoder", + "version": "24.3.0", + "libName": "CTRE_SimProCANcoder", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProPigeon2", + "version": "24.3.0", + "libName": "CTRE_SimProPigeon2", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + } + ] +} \ No newline at end of file diff --git a/simulation/samples/CppSample/vendordeps/REVLib.json b/simulation/samples/CppSample/vendordeps/REVLib.json new file mode 100644 index 0000000000..f85acd4054 --- /dev/null +++ b/simulation/samples/CppSample/vendordeps/REVLib.json @@ -0,0 +1,74 @@ +{ + "fileName": "REVLib.json", + "name": "REVLib", + "version": "2024.2.4", + "frcYear": "2024", + "uuid": "3f48eb8c-50fe-43a6-9cb7-44c86353c4cb", + "mavenUrls": [ + "https://maven.revrobotics.com/" + ], + "jsonUrl": "https://software-metadata.revrobotics.com/REVLib-2024.json", + "javaDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-java", + "version": "2024.2.4" + } + ], + "jniDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-driver", + "version": "2024.2.4", + "skipInvalidPlatforms": true, + "isJar": false, + "validPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ], + "cppDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-cpp", + "version": "2024.2.4", + "libName": "REVLib", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + }, + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-driver", + "version": "2024.2.4", + "libName": "REVLibDriver", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ] +} \ No newline at end of file diff --git a/simulation/samples/CppSample/vendordeps/WPILibNewCommands.json b/simulation/samples/CppSample/vendordeps/WPILibNewCommands.json new file mode 100644 index 0000000000..67bf3898d5 --- /dev/null +++ b/simulation/samples/CppSample/vendordeps/WPILibNewCommands.json @@ -0,0 +1,38 @@ +{ + "fileName": "WPILibNewCommands.json", + "name": "WPILib-New-Commands", + "version": "1.0.0", + "uuid": "111e20f7-815e-48f8-9dd6-e675ce75b266", + "frcYear": "2024", + "mavenUrls": [], + "jsonUrl": "", + "javaDependencies": [ + { + "groupId": "edu.wpi.first.wpilibNewCommands", + "artifactId": "wpilibNewCommands-java", + "version": "wpilib" + } + ], + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "edu.wpi.first.wpilibNewCommands", + "artifactId": "wpilibNewCommands-cpp", + "version": "wpilib", + "libName": "wpilibNewCommands", + "headerClassifier": "headers", + "sourcesClassifier": "sources", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena", + "linuxarm32", + "linuxarm64", + "windowsx86-64", + "windowsx86", + "linuxx86-64", + "osxuniversal" + ] + } + ] +} From 7b85e627d21b970c9f8ad27f50953fbb21759161 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 8 Jul 2024 14:16:31 -0700 Subject: [PATCH 040/121] fixed auth token issues --- exporter/SynthesisFusionAddin/.gitignore | 4 +++- exporter/SynthesisFusionAddin/proto/deps.py | 3 +-- fission/src/aps/APS.ts | 2 +- fission/src/systems/physics/PhysicsSystem.ts | 2 -- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/exporter/SynthesisFusionAddin/.gitignore b/exporter/SynthesisFusionAddin/.gitignore index eca33a87a8..fd821a7742 100644 --- a/exporter/SynthesisFusionAddin/.gitignore +++ b/exporter/SynthesisFusionAddin/.gitignore @@ -107,4 +107,6 @@ site-packages # env files **/.env -proto/proto_out \ No newline at end of file +proto/proto_out + +secrets diff --git a/exporter/SynthesisFusionAddin/proto/deps.py b/exporter/SynthesisFusionAddin/proto/deps.py index 887649c1d6..80887337df 100644 --- a/exporter/SynthesisFusionAddin/proto/deps.py +++ b/exporter/SynthesisFusionAddin/proto/deps.py @@ -5,8 +5,7 @@ import adsk.core import adsk.fusion - -from src.general_imports import INTERNAL_ID +from ..src.general_imports import INTERNAL_ID system = platform.system() diff --git a/fission/src/aps/APS.ts b/fission/src/aps/APS.ts index f3a9854fe9..60fca2fdb2 100644 --- a/fission/src/aps/APS.ts +++ b/fission/src/aps/APS.ts @@ -84,7 +84,7 @@ class APS { ["response_type", "code"], ["client_id", CLIENT_ID], ["redirect_uri", callbackUrl], - ["scope", "data:read"], + ["scope", "data:create"], ["nonce", Date.now().toString()], ["prompt", "login"], ["code_challenge", codeChallenge], diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index dd87a1c79d..b208c6484b 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -723,8 +723,6 @@ class PhysicsSystem extends WorldSystem { let substeps = Math.max(1, Math.floor((lastDeltaT / STANDARD_SIMULATION_PERIOD) * STANDARD_SUB_STEPS)) substeps = Math.min(MAX_SUBSTEPS, Math.max(MIN_SUBSTEPS, substeps)) - console.log(`DeltaT: ${lastDeltaT.toFixed(5)}, Substeps: ${substeps}`) - this._joltInterface.Step(lastDeltaT, substeps) } From ce7d91e9aca2afb66f5b676813bb35f75af30def Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Tue, 9 Jul 2024 09:09:10 -0700 Subject: [PATCH 041/121] Logging file rename --- exporter/SynthesisFusionAddin/src/{logging.py => Logging.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename exporter/SynthesisFusionAddin/src/{logging.py => Logging.py} (100%) diff --git a/exporter/SynthesisFusionAddin/src/logging.py b/exporter/SynthesisFusionAddin/src/Logging.py similarity index 100% rename from exporter/SynthesisFusionAddin/src/logging.py rename to exporter/SynthesisFusionAddin/src/Logging.py From dc434ad22b2d0b63c2be8c7704a44eb382b32f34 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Tue, 9 Jul 2024 09:33:50 -0700 Subject: [PATCH 042/121] Automatic logger naming --- exporter/SynthesisFusionAddin/proto/deps.py | 2 +- exporter/SynthesisFusionAddin/src/Logging.py | 7 ++++++- .../src/Parser/SynthesisParser/JointHierarchy.py | 2 +- .../src/Parser/SynthesisParser/Joints.py | 2 +- .../src/Parser/SynthesisParser/Parser.py | 12 ++++++------ .../SynthesisFusionAddin/src/UI/ConfigCommand.py | 2 +- exporter/SynthesisFusionAddin/src/configure.py | 2 +- exporter/SynthesisFusionAddin/src/general_imports.py | 2 +- 8 files changed, 18 insertions(+), 13 deletions(-) diff --git a/exporter/SynthesisFusionAddin/proto/deps.py b/exporter/SynthesisFusionAddin/proto/deps.py index c4216ea710..cd549803a3 100644 --- a/exporter/SynthesisFusionAddin/proto/deps.py +++ b/exporter/SynthesisFusionAddin/proto/deps.py @@ -10,7 +10,7 @@ from src.Logging import getLogger, logFailure system = platform.system() -logger = getLogger(f"{INTERNAL_ID}.{__name__}") +logger = getLogger() def getPythonFolder() -> str: diff --git a/exporter/SynthesisFusionAddin/src/Logging.py b/exporter/SynthesisFusionAddin/src/Logging.py index 4792c6b7b5..9d8d016245 100644 --- a/exporter/SynthesisFusionAddin/src/Logging.py +++ b/exporter/SynthesisFusionAddin/src/Logging.py @@ -1,3 +1,4 @@ +import inspect import functools import logging.handlers import os @@ -47,7 +48,11 @@ def setupLogger() -> None: logger.addHandler(logHandler) -def getLogger(name: str) -> SynthesisLogger: +def getLogger(name: str | None = None) -> SynthesisLogger: + if not name: + # Inspect the caller stack to automatically get the module from which the function is being called from. + name = f"{INTERNAL_ID}.{'.'.join(inspect.getmodule(inspect.stack()[1][0]).__name__.split('.')[1:])}" + return cast(SynthesisLogger, logging.getLogger(name)) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 5590669f09..53dd3d10fa 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -14,7 +14,7 @@ from .PDMessage import PDMessage from .Utilities import guid_component, guid_occurrence -logger = getLogger(f"{INTERNAL_ID}.{__name__}") +logger = getLogger() # ____________________________ DATA TYPES __________________ diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py index b280881eb0..d2e3b81bc2 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py @@ -37,7 +37,7 @@ from .PDMessage import PDMessage from .Utilities import construct_info, fill_info, guid_occurrence -logger = getLogger(f"{INTERNAL_ID}.{__name__}") +logger = getLogger() # Need to take in a graphcontainer diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index aa44d9324f..50a2719224 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -15,7 +15,7 @@ from . import Components, JointHierarchy, Joints, Materials, PDMessage from .Utilities import * -logger = getLogger(f"{INTERNAL_ID}.{__name__}") +logger = getLogger() class Parser: @@ -220,11 +220,11 @@ def export(self) -> bool: joint_hierarchy_out += "\n\n" debug_output = ( - f"Appearances: {len(assembly_out.data.materials.appearances)} \n" - f"Materials: {len(assembly_out.data.materials.physicalMaterials)} \n" - f"Part-Definitions: {len(part_defs)} \n" - f"Parts: {len(parts)} \n" - f"Signals: {len(signals)} \n" + f"Appearances: {len(assembly_out.data.materials.appearances)}\n" + f"Materials: {len(assembly_out.data.materials.physicalMaterials)}\n" + f"Part-Definitions: {len(part_defs)}\n" + f"Parts: {len(parts)}\n" + f"Signals: {len(signals)}\n" f"Joints: {len(joints)}\n" f"{joint_hierarchy_out}" ) diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 1fb4a5819c..6392ce1f33 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -30,7 +30,7 @@ from . import CustomGraphics, FileDialogConfig, Helper, IconPaths, OsHelper from .Configuration.SerialCommand import SerialCommand -logger = getLogger(f"{INTERNAL_ID}.{__name__}") +logger = getLogger() # ====================================== CONFIG COMMAND ====================================== diff --git a/exporter/SynthesisFusionAddin/src/configure.py b/exporter/SynthesisFusionAddin/src/configure.py index 75d2cb632b..20f64f4b8d 100644 --- a/exporter/SynthesisFusionAddin/src/configure.py +++ b/exporter/SynthesisFusionAddin/src/configure.py @@ -9,7 +9,7 @@ from .strings import INTERNAL_ID from .Types.OString import OString -logger = getLogger(f"{INTERNAL_ID}.{__name__}") +logger = getLogger() try: config = ConfigParser() diff --git a/exporter/SynthesisFusionAddin/src/general_imports.py b/exporter/SynthesisFusionAddin/src/general_imports.py index d659ff49bf..c5121683e9 100644 --- a/exporter/SynthesisFusionAddin/src/general_imports.py +++ b/exporter/SynthesisFusionAddin/src/general_imports.py @@ -14,7 +14,7 @@ from .Logging import getLogger from .strings import INTERNAL_ID -logger = getLogger(f"{INTERNAL_ID}.{__name__}") +logger = getLogger() # hard coded to bypass errors for now PROTOBUF = True From c49d0ad2141b9a36b17d9fb1b5873c516aa11576 Mon Sep 17 00:00:00 2001 From: BrandonPacewic Date: Tue, 9 Jul 2024 09:34:04 -0700 Subject: [PATCH 043/121] Formatting --- exporter/SynthesisFusionAddin/src/Logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/SynthesisFusionAddin/src/Logging.py b/exporter/SynthesisFusionAddin/src/Logging.py index 9d8d016245..2bf9ce191b 100644 --- a/exporter/SynthesisFusionAddin/src/Logging.py +++ b/exporter/SynthesisFusionAddin/src/Logging.py @@ -1,5 +1,5 @@ -import inspect import functools +import inspect import logging.handlers import os import pathlib From df3e6033a708cbd12a7700764a448fa0662def4a Mon Sep 17 00:00:00 2001 From: LucaHaverty Date: Tue, 9 Jul 2024 11:46:56 -0700 Subject: [PATCH 044/121] Modify saved scoring zones with zone config panels --- fission/src/Synthesis.tsx | 8 +- .../systems/preferences/PreferenceTypes.ts | 25 +- .../synthesis_brain/SynthesisBrain.ts | 11 +- fission/src/ui/components/MainHUD.tsx | 24 +- .../modals/configuring/ChangeInputsModal.tsx | 4 +- .../configuring/scoring/ScoringZonesPanel.tsx | 266 ++++++------------ .../configuring/scoring/ZoneConfigPanel.tsx | 69 +++-- 7 files changed, 159 insertions(+), 248 deletions(-) diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 65fa57c87d..73710ecb37 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -55,7 +55,7 @@ import { AddRobotsModal, AddFieldsModal, SpawningModal } from "@/modals/spawning import ImportMirabufModal from "@/modals/mirabuf/ImportMirabufModal.tsx" import ImportLocalMirabufModal from "@/modals/mirabuf/ImportLocalMirabufModal.tsx" import ResetAllInputsModal from "./ui/modals/configuring/ResetAllInputsModal.tsx" -import Skybox from './ui/components/Skybox.tsx'; +import Skybox from './ui/components/Skybox.tsx' const DEFAULT_MIRA_PATH = "/api/mira/Robots/Team 2471 (2018)_v7.mira" @@ -201,7 +201,7 @@ const initialModals = [ , , , -] +] const initialPanels: ReactElement[] = [ , @@ -210,8 +210,8 @@ const initialPanels: ReactElement[] = [ , , , - , - , + , + , ] export default Synthesis diff --git a/fission/src/systems/preferences/PreferenceTypes.ts b/fission/src/systems/preferences/PreferenceTypes.ts index eff4cf6cbe..77f04844bb 100644 --- a/fission/src/systems/preferences/PreferenceTypes.ts +++ b/fission/src/systems/preferences/PreferenceTypes.ts @@ -1,4 +1,4 @@ -import { Vector3Tuple, Vector4Tuple } from "three" +import { Vector3Tuple } from "three" import { InputScheme } from "../input/DefaultInputs" export type GlobalPreference = @@ -26,12 +26,12 @@ export const DefaultGlobalPreferences: { [key: string]: Object } = { } export type IntakePreferences = { - location: Vector3Tuple, + location: [number, number, number], diameter: number } export type EjectorPreferences = { - location: Vector3Tuple, + location: [number, number, number], ejectorVelocity: number } @@ -41,18 +41,17 @@ export type RobotPreferences = { ejector: EjectorPreferences } -export type Alliance = "Blue" | "Red" export type ScoringZonePreferences = { - name: string, - alliance: Alliance, - parent: string, - points: number, - destroyGamepiece: boolean, - persistentPoints: boolean, - localPosition: Vector3Tuple, - localRotation: Vector4Tuple, - localScale: Vector3Tuple + name: string + alliance: "red" | "blue" + parent: string + points: number + destroyGamepiece: boolean + persistentPoints: boolean + position: [number, number, number] + rotation: [number, number, number, number] + scale: [number, number, number] } export type FieldPreferences = { diff --git a/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts b/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts index 0e84bce617..17bafa7493 100644 --- a/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts +++ b/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts @@ -37,6 +37,9 @@ class SynthesisBrain extends Brain { // The total number of robots spawned private static _currentRobotIndex: number = 0 + // A list of all the fields spawned + public static fieldsSpawned: string[] = [] + public constructor(mechanism: Mechanism, assemblyName: string) { super(mechanism) @@ -65,6 +68,7 @@ class SynthesisBrain extends Brain { } else { this.configureField() + SynthesisBrain.fieldsSpawned.push(assemblyName) } SynthesisBrain._currentRobotIndex++ @@ -82,7 +86,7 @@ class SynthesisBrain extends Brain { public clearControls(): void { let index = SynthesisBrain.robotsSpawned.indexOf(`[${this._assemblyIndex}] ${this._assemblyName}`); - SynthesisBrain.robotsSpawned.splice(index, 1); + SynthesisBrain.robotsSpawned.splice(index, 1) } // Creates an instance of ArcadeDriveBehavior and automatically configures it @@ -208,10 +212,9 @@ class SynthesisBrain extends Brain { } private configureField() { - const fieldPrefs = PreferencesSystem.getFieldPreferences(this._assemblyName) - console.log("Loaded field prefs " + fieldPrefs) + //const fieldPrefs = PreferencesSystem.getFieldPreferences(this._assemblyName) - /** Put any scoring zone or other field configuration here */ + /** Put any field configuration here */ } private static parseInputs(rawInputs: InputScheme) { diff --git a/fission/src/ui/components/MainHUD.tsx b/fission/src/ui/components/MainHUD.tsx index f634d80173..0b0cf0ca83 100644 --- a/fission/src/ui/components/MainHUD.tsx +++ b/fission/src/ui/components/MainHUD.tsx @@ -5,7 +5,7 @@ import { BiMenuAltLeft } from "react-icons/bi" import { GrFormClose } from "react-icons/gr" import { GiSteeringWheel } from "react-icons/gi" import { HiDownload } from "react-icons/hi" -import { IoBug, IoGameControllerOutline, IoPeople } from "react-icons/io5" +import { IoBasket, IoBug, IoGameControllerOutline, IoPeople } from "react-icons/io5" import { useModalControlContext } from "@/ui/ModalContext" import { usePanelControlContext } from "@/ui/PanelContext" import { motion } from "framer-motion" @@ -129,20 +129,6 @@ const MainHUD: React.FC = () => { onClick={() => openModal("import-local-mirabuf")} /> } onClick={TestGodMode} /> - 5"} - icon={} - onClick={() => - (PreferencesSystem.getRobotPreferences("Team 2471 (2018) v7").intake.diameter = 5) - } - /> - 2"} - icon={} - onClick={() => - (PreferencesSystem.getRobotPreferences("Team 2471 (2018) v7").intake.diameter = 2) - } - /> } @@ -193,7 +179,13 @@ const MainHUD: React.FC = () => { new TransformGizmo("translate").setMode = "rotate" }} /> -

+ } + onClick={() => { + openPanel("scoring-zones") + }} + />
{userInfo ? ( void + closePanel: (paneId: string) => void deleteZone: () => void + saveZones: () => void } -const ScoringZoneRow: React.FC = ({ zone, openPanel, deleteZone }) => { +export class SelectedZone { + public static zone: ScoringZonePreferences; +} + +const ScoringZoneRow: React.FC = ({ zone, openPanel, closePanel, deleteZone, saveZones }) => { return ( @@ -26,179 +34,43 @@ const ScoringZoneRow: React.FC = ({ zone, openPanel, delete -
+ } onClick={() => openModal("config-robot")} /> +
{userInfo ? ( = ({ panelId, open useEffect(() => { setupGizmo() - }) + }, []) return ( = ({ panelId, openL const direction = selectedEjector.direction transformGizmoRef.current?.mesh.position.set(position[0], position[1], position[2]) - transformGizmoRef.current?.mesh.rotation.setFromQuaternion(new THREE.Quaternion(direction[0], direction[1], direction[2], direction[3])) + transformGizmoRef.current?.mesh.rotation.setFromQuaternion( + new THREE.Quaternion(direction[0], direction[1], direction[2], direction[3]) + ) } // Saves zone preferences to local storage @@ -64,7 +66,7 @@ const ConfigureShotTrajectoryPanel: React.FC = ({ panelId, openL useEffect(() => { setupGizmo() - }) + }, []) return ( = ({ panelId, openL value={robot} onClick={() => { setSelectedEjector(PreferencesSystem.getRobotPreferences(robot)?.ejector) - setEjectorVelocity(PreferencesSystem.getRobotPreferences(robot)?.ejector.ejectorVelocity ?? MIN_VELOCITY) + setEjectorVelocity( + PreferencesSystem.getRobotPreferences(robot)?.ejector.ejectorVelocity ?? + MIN_VELOCITY + ) }} key={robot} > @@ -116,9 +121,7 @@ const ConfigureShotTrajectoryPanel: React.FC = ({ panelId, openL defaultValue={selectedEjector.ejectorVelocity} label="Velocity" format={{ minimumFractionDigits: 2, maximumFractionDigits: 2 }} - onChange={(vel: number) => - setEjectorVelocity(vel) - } + onChange={(vel: number) => setEjectorVelocity(vel)} step={0.01} /> diff --git a/fission/src/ui/panels/configuring/scoring/ScoringZonesPanel.tsx b/fission/src/ui/panels/configuring/scoring/ScoringZonesPanel.tsx index 4a2bc46a6f..a277d51c08 100644 --- a/fission/src/ui/panels/configuring/scoring/ScoringZonesPanel.tsx +++ b/fission/src/ui/panels/configuring/scoring/ScoringZonesPanel.tsx @@ -1,4 +1,4 @@ -import { useState } from "react" +import { useEffect, useState } from "react" import { usePanelControlContext } from "@/ui/PanelContext" import Button from "@/components/Button" import Label, { LabelSize } from "@/components/Label" @@ -12,7 +12,6 @@ import SynthesisBrain from "@/systems/simulation/synthesis_brain/SynthesisBrain" type ScoringZoneRowProps = { zone: ScoringZonePreferences openPanel: (id: string) => void - closePanel: (paneId: string) => void deleteZone: () => void saveZones: () => void } @@ -21,7 +20,7 @@ export class SelectedZone { public static zone: ScoringZonePreferences; } -const ScoringZoneRow: React.FC = ({ zone, openPanel, closePanel, deleteZone, saveZones }) => { +const ScoringZoneRow: React.FC = ({ zone, openPanel, deleteZone, saveZones }) => { return ( @@ -39,7 +38,6 @@ const ScoringZoneRow: React.FC = ({ zone, openPanel, closeP onClick={() => { SelectedZone.zone = zone saveZones() - closePanel("scoring-zones") openPanel("zone-config") }} /> @@ -72,6 +70,10 @@ const ScoringZonesPanel: React.FC = ({ panelId, openLocation, si } } + useEffect(() => { + closePanel("zone-config") + },[]) + return ( = ({ panelId, openLocation, si key={i} zone={z} openPanel={openPanel} - closePanel={closePanel} deleteZone={() => { setZones(zones.filter((_, idx) => idx !== i)) }} @@ -119,7 +120,6 @@ const ScoringZonesPanel: React.FC = ({ panelId, openLocation, si zones.push(newZone) SelectedZone.zone = newZone saveZones() - closePanel(panelId) openPanel("zone-config") }} className="px-36 w-full" diff --git a/fission/src/ui/panels/configuring/scoring/ZoneConfigPanel.tsx b/fission/src/ui/panels/configuring/scoring/ZoneConfigPanel.tsx index b84424fa93..1fa41b3f87 100644 --- a/fission/src/ui/panels/configuring/scoring/ZoneConfigPanel.tsx +++ b/fission/src/ui/panels/configuring/scoring/ZoneConfigPanel.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react" +import { useEffect, useRef, useState } from "react" import Input from "@/components/Input" import Panel, { PanelPropsImpl } from "@/components/Panel" import Button from "@/components/Button" @@ -10,9 +10,13 @@ import { usePanelControlContext } from "@/ui/PanelContext" import Stack, { StackDirection } from "@/ui/components/Stack" import SelectButton from "@/ui/components/SelectButton" import Jolt from "@barclah/jolt-physics" +import TransformGizmos from "@/ui/components/TransformGizmos" +import * as THREE from "three" const ZoneConfigPanel: React.FC = ({ panelId, openLocation, sidePadding }) => { - const { openPanel } = usePanelControlContext() + const { openPanel, closePanel } = usePanelControlContext() + + const transformGizmoRef = useRef() const [name, setName] = useState(SelectedZone.zone.name) const [alliance, setAlliance] = useState<"red" | "blue">(SelectedZone.zone.alliance) @@ -24,8 +28,53 @@ const ZoneConfigPanel: React.FC = ({ panelId, openLocation, side const [transformMode, setTransformMode] = useState<"translate" | "rotate" | "scale">("translate") useEffect(() => { - // TODO: create transform gizmo - }) + closePanel("scoring-zones") + + configureGizmo("translate") + },[]) + + const configureGizmo = (mode: "translate" | "rotate" | "scale") => { + // Remove the old transform gizmo + if (transformGizmoRef.current) transformGizmoRef.current.RemoveGizmos() + + transformGizmoRef.current = new TransformGizmos( + new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ color: 0xffffff })) + ) + transformGizmoRef.current.AddMeshToScene() + + transformGizmoRef.current.CreateGizmo(mode, 1.5) + + const position = SelectedZone.zone.position + const rotation = SelectedZone.zone.rotation + const scale = SelectedZone.zone.scale + + transformGizmoRef.current.mesh.position.set(position[0], position[1], position[2]) + transformGizmoRef.current.mesh.rotation.setFromQuaternion( + new THREE.Quaternion(rotation[0], rotation[1], rotation[2], rotation[3]) + ) + transformGizmoRef.current.mesh.scale.set(scale[0], scale[1], scale[2]) + } + + const saveSettings = () => { + SelectedZone.zone.name = name + SelectedZone.zone.alliance = alliance + SelectedZone.zone.parent = parent + SelectedZone.zone.points = points + SelectedZone.zone.destroyGamepiece = destroy + SelectedZone.zone.persistentPoints = persistent + + if (transformGizmoRef.current != undefined) { + const position = transformGizmoRef.current.mesh.position + const rotation = transformGizmoRef.current.mesh.quaternion + const scale = transformGizmoRef.current.mesh.scale + + SelectedZone.zone.position = [position.x, position.y, position.z] + SelectedZone.zone.rotation = [rotation.x, rotation.y, rotation.z, rotation.w] + SelectedZone.zone.scale = [scale.x, scale.y, scale.z] + } + + PreferencesSystem.savePreferences() + } return ( = ({ panelId, openLocation, side openLocation={openLocation} sidePadding={sidePadding} onAccept={() => { - SelectedZone.zone.name = name - SelectedZone.zone.alliance = alliance - SelectedZone.zone.parent = parent - SelectedZone.zone.points = points - SelectedZone.zone.destroyGamepiece = destroy - SelectedZone.zone.persistentPoints = persistent - - // TODO: Yoink transform info from the transform gizmo - - PreferencesSystem.savePreferences() + saveSettings() + if (transformGizmoRef.current) transformGizmoRef.current.RemoveGizmos() + openPanel("scoring-zones") + }} + onCancel={() => { openPanel("scoring-zones") + if (transformGizmoRef.current) transformGizmoRef.current.RemoveGizmos() }} > @@ -53,7 +98,10 @@ const ZoneConfigPanel: React.FC = ({ panelId, openLocation, side onClick={() => setAlliance(alliance == "blue" ? "red" : "blue")} colorOverrideClass={`bg-match-${alliance}-alliance`} /> - setParent(p)} /> + (setParent(body))} + /> = ({ panelId, openLocation, side onClick={setPersistent} /> - <> -
)} + diff --git a/fission/src/mirabuf/MirabufInstance.ts b/fission/src/mirabuf/MirabufInstance.ts index 390c6f3ef6..c940969b9d 100644 --- a/fission/src/mirabuf/MirabufInstance.ts +++ b/fission/src/mirabuf/MirabufInstance.ts @@ -2,6 +2,7 @@ import * as THREE from "three" import { mirabuf } from "../proto/mirabuf" import MirabufParser, { ParseErrorSeverity } from "./MirabufParser.ts" import World from "@/systems/World.ts" +import { ProgressHandle } from "@/ui/components/ProgressNotification.tsx" const WIREFRAME = false @@ -95,7 +96,7 @@ class MirabufInstance { return this._meshes } - public constructor(parser: MirabufParser, materialStyle?: MaterialStyle) { + public constructor(parser: MirabufParser, materialStyle?: MaterialStyle, progressHandle?: ProgressHandle) { if (parser.errors.some(x => x[0] >= ParseErrorSeverity.Unimportable)) { throw new Error("Parser has significant errors...") } @@ -104,7 +105,10 @@ class MirabufInstance { this._materials = new Map() this._meshes = new Map() + progressHandle?.Update('Loading materials...', 0.4) this.LoadMaterials(materialStyle ?? MaterialStyle.Regular) + + progressHandle?.Update('Creating meshes...', 0.5) this.CreateMeshes() } diff --git a/fission/src/mirabuf/MirabufParser.ts b/fission/src/mirabuf/MirabufParser.ts index a9a814e85a..515dd9ecdc 100644 --- a/fission/src/mirabuf/MirabufParser.ts +++ b/fission/src/mirabuf/MirabufParser.ts @@ -1,6 +1,7 @@ import * as THREE from "three" import { mirabuf } from "@/proto/mirabuf" import { MirabufTransform_ThreeMatrix4 } from "@/util/TypeConversions" +import { ProgressHandle } from "@/ui/components/ProgressNotification" export enum ParseErrorSeverity { Unimportable = 10, @@ -70,11 +71,13 @@ class MirabufParser { return this._rootNode } - public constructor(assembly: mirabuf.Assembly) { + public constructor(assembly: mirabuf.Assembly, progressHandle?: ProgressHandle) { this._assembly = assembly this._errors = new Array() this._globalTransforms = new Map() + progressHandle?.Update("Parsing assembly...", 0.3) + this.GenerateTreeValues() this.LoadGlobalTransforms() diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index be6ae81225..2ac5ad4595 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -12,6 +12,7 @@ import Mechanism from "@/systems/physics/Mechanism" import SynthesisBrain from "@/systems/simulation/synthesis_brain/SynthesisBrain" import InputSystem from "@/systems/input/InputSystem" import TransformGizmos from "@/ui/components/TransformGizmos" +import { ProgressHandle } from "@/ui/components/ProgressNotification" const DEBUG_BODIES = false @@ -40,12 +41,14 @@ class MirabufSceneObject extends SceneObject { return this._mechanism } - public constructor(mirabufInstance: MirabufInstance, assemblyName: string) { + public constructor(mirabufInstance: MirabufInstance, assemblyName: string, progressHandle?: ProgressHandle) { super() this._mirabufInstance = mirabufInstance this._assemblyName = assemblyName + progressHandle?.Update("Creating mechanism...", 0.9) + this._mechanism = World.PhysicsSystem.CreateMechanismFromParser(this._mirabufInstance.parser) if (this._mechanism.layerReserve) { this._physicsLayerReserve = this._mechanism.layerReserve diff --git a/fission/src/ui/components/ProgressNotification.tsx b/fission/src/ui/components/ProgressNotification.tsx new file mode 100644 index 0000000000..097a759039 --- /dev/null +++ b/fission/src/ui/components/ProgressNotification.tsx @@ -0,0 +1,146 @@ +import { styled, Typography } from "@mui/material" +import { Box } from "@mui/system" +import { useEffect, useReducer } from "react" + +const handleMap = new Map() + +const TypoStyled = styled(Typography)((_) => ({ + fontFamily: "Artifakt", + textAlign: "center" +})) + +function ProgressNotifications() { + + const [progressElements, updateProgressElements] = useReducer(() => { + return handleMap.size > 0 + ? + [...handleMap.entries()].map(([id, handle]) => { return ( + + + {handle.title} + { handle.message.length > 0 ? {handle.message} : <> } + + + + )}) + : undefined + }, undefined) + + useEffect(() => { + const onHandleUpdate = (e: Event) => { + const handle = (e as ProgressEvent).handle; + if (handle.status > 0) { + setTimeout(() => handleMap.delete(handle.handleId) && updateProgressElements(), 1000) + console.debug('Deleting') + } + handleMap.set(handle.handleId, handle) + updateProgressElements() + } + + window.addEventListener(ProgressEvent.EVENT_KEY, onHandleUpdate) + return () => { + window.removeEventListener(ProgressEvent.EVENT_KEY, onHandleUpdate) + } + }, [updateProgressElements]) + + return ( + + { progressElements ?? <> } + + ) +} + +let nextHandleId = 0 + +export enum ProgressHandleStatus { + inProgress = 0, + Done = 1, + Error = 2 +} + +export class ProgressHandle { + + private _handleId: number + private _title: string + public message: string = "" + public progress: number = 0.0 + public status: ProgressHandleStatus = ProgressHandleStatus.inProgress + + public get handleId() { return this._handleId } + public get title() { return this._title } + + public constructor(title: string) { + this._handleId = nextHandleId++ + this._title = title + } + + public Update(message: string, progress: number, status?: ProgressHandleStatus) { + this.message = message + this.progress = progress + status && (this.status = status) + + this.Push() + } + + public Push() { + window.dispatchEvent(new ProgressEvent(this)) + } +} + +class ProgressEvent extends Event { + public static readonly EVENT_KEY = 'ProgressEvent' + + public handle: ProgressHandle + + public constructor(handle: ProgressHandle) { + super(ProgressEvent.EVENT_KEY) + + this.handle = handle + } +} + +export default ProgressNotifications \ No newline at end of file From c640873474b37edf0b8da1cc830f881ad3b60e54 Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Mon, 15 Jul 2024 01:48:38 -0600 Subject: [PATCH 055/121] Progress notifications integrated with spawning. Issue with unit tests however --- fission/src/mirabuf/MirabufSceneObject.ts | 6 ++-- .../ui/components/ProgressNotification.tsx | 11 ++++-- .../ui/panels/mirabuf/ImportMirabufPanel.tsx | 35 +++++++++++++++---- fission/vite.config.ts | 1 + 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index 2ac5ad4595..a480b01cc5 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -234,14 +234,14 @@ class MirabufSceneObject extends SceneObject { } } -export async function CreateMirabuf(assembly: mirabuf.Assembly): Promise { - const parser = new MirabufParser(assembly) +export async function CreateMirabuf(assembly: mirabuf.Assembly, progressHandle?: ProgressHandle): Promise { + const parser = new MirabufParser(assembly, progressHandle) if (parser.maxErrorSeverity >= ParseErrorSeverity.Unimportable) { console.error(`Assembly Parser produced significant errors for '${assembly.info!.name!}'`) return } - return new MirabufSceneObject(new MirabufInstance(parser), assembly.info!.name!) + return new MirabufSceneObject(new MirabufInstance(parser), assembly.info!.name!, progressHandle) } export default MirabufSceneObject diff --git a/fission/src/ui/components/ProgressNotification.tsx b/fission/src/ui/components/ProgressNotification.tsx index 097a759039..514fa100c5 100644 --- a/fission/src/ui/components/ProgressNotification.tsx +++ b/fission/src/ui/components/ProgressNotification.tsx @@ -62,8 +62,7 @@ function ProgressNotifications() { const onHandleUpdate = (e: Event) => { const handle = (e as ProgressEvent).handle; if (handle.status > 0) { - setTimeout(() => handleMap.delete(handle.handleId) && updateProgressElements(), 1000) - console.debug('Deleting') + setTimeout(() => handleMap.delete(handle.handleId) && updateProgressElements(), 2000) } handleMap.set(handle.handleId, handle) updateProgressElements() @@ -126,6 +125,14 @@ export class ProgressHandle { this.Push() } + public Fail(message?: string) { + this.Update(message ?? "Failed", 1, ProgressHandleStatus.Error) + } + + public Done(message?: string) { + this.Update(message ?? "Done", 1, ProgressHandleStatus.Done) + } + public Push() { window.dispatchEvent(new ProgressEvent(this)) } diff --git a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx index e4498c7e97..075b841239 100644 --- a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx +++ b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx @@ -22,6 +22,7 @@ import Panel, { PanelPropsImpl } from "@/ui/components/Panel" import { usePanelControlContext } from "@/ui/PanelContext" import TaskStatus from "@/util/TaskStatus" import { BiRefresh } from "react-icons/bi" +import { ProgressHandle, ProgressHandleStatus } from "@/ui/components/ProgressNotification" const DownloadIcon = const AddIcon = @@ -124,20 +125,28 @@ function GetCacheInfo(miraType: MiraType): MirabufCacheInfo[] { return Object.values(MirabufCachingService.GetCacheMap(miraType)) } -function SpawnCachedMira(info: MirabufCacheInfo, type: MiraType) { +function SpawnCachedMira(info: MirabufCacheInfo, type: MiraType, progressHandle?: ProgressHandle) { + if (!progressHandle) { + progressHandle = new ProgressHandle(info.name ?? info.cacheKey) + } + MirabufCachingService.Get(info.id, type).then(assembly => { if (assembly) { CreateMirabuf(assembly).then(x => { if (x) { World.SceneRenderer.RegisterSceneObject(x) + progressHandle.Done() + } else { + progressHandle.Fail() } }) if (!info.name) MirabufCachingService.CacheInfo(info.cacheKey, type, assembly.info?.name ?? undefined) } else { + progressHandle.Fail() console.error("Failed to spawn robot") } - }) + }).catch(() => progressHandle.Fail()) } const ImportMirabufPanel: React.FC = ({ panelId }) => { @@ -233,9 +242,16 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { // Cache a selected remote mirabuf assembly, load from cache. const selectRemote = useCallback( (info: MirabufRemoteInfo, type: MiraType) => { + const status = new ProgressHandle(info.displayName) + status.Update("Downloading from Synthesis...", 0.05) + MirabufCachingService.CacheRemote(info.src, type).then(cacheInfo => { - cacheInfo && SpawnCachedMira(cacheInfo, type) - }) + if (cacheInfo) { + SpawnCachedMira(cacheInfo, type, status) + } else { + status.Fail("Failed to cache") + } + }).catch(() => status.Fail()) closePanel(panelId) }, @@ -244,9 +260,16 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { const selectAPS = useCallback( (data: Data, type: MiraType) => { + const status = new ProgressHandle(data.attributes.displayName ?? data.id) + status.Update("Downloading from APS...", 0.05) + MirabufCachingService.CacheAPS(data, type).then(cacheInfo => { - cacheInfo && SpawnCachedMira(cacheInfo, type) - }) + if (cacheInfo) { + SpawnCachedMira(cacheInfo, type, status) + } else { + status.Fail("Failed to cache") + } + }).catch(() => status.Fail()) closePanel(panelId) }, diff --git a/fission/vite.config.ts b/fission/vite.config.ts index 1f052ab08a..da41628d3d 100644 --- a/fission/vite.config.ts +++ b/fission/vite.config.ts @@ -33,6 +33,7 @@ export default defineConfig({ ], }, test: { + testTimeout: 5000, globals: true, environment: 'jsdom', browser: { From 4660906f651c049af1bc1241fe7ebf238f44102f Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Mon, 15 Jul 2024 02:02:14 -0600 Subject: [PATCH 056/121] I didn't change anything and now the issue with vitest taking too long is gone... --- fission/src/test/PhysicsSystem.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fission/src/test/PhysicsSystem.test.ts b/fission/src/test/PhysicsSystem.test.ts index f60447705c..ed2ea62bc5 100644 --- a/fission/src/test/PhysicsSystem.test.ts +++ b/fission/src/test/PhysicsSystem.test.ts @@ -91,7 +91,9 @@ describe("Mirabuf Physics Loading", () => { const assembly = await MirabufCachingService.CacheRemote( "/api/mira/Robots/Team 2471 (2018)_v7.mira", MiraType.ROBOT - ).then(x => MirabufCachingService.Get(x!.id, MiraType.ROBOT)) + ).then(x => { + return MirabufCachingService.Get(x!.id, MiraType.ROBOT) + }) const parser = new MirabufParser(assembly!) const physSystem = new PhysicsSystem() From 19666ccc74e2c336ed83c526bf9a3ca7467667f0 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 15 Jul 2024 09:43:59 -0700 Subject: [PATCH 057/121] add import to general imports --- exporter/SynthesisFusionAddin/proto/deps.py | 2 +- exporter/SynthesisFusionAddin/src/APS/APS.py | 47 +++++++++---------- .../src/general_imports.py | 4 +- exporter/SynthesisFusionAddin/src/strings.py | 2 - 4 files changed, 24 insertions(+), 31 deletions(-) diff --git a/exporter/SynthesisFusionAddin/proto/deps.py b/exporter/SynthesisFusionAddin/proto/deps.py index 80887337df..88034c300a 100644 --- a/exporter/SynthesisFusionAddin/proto/deps.py +++ b/exporter/SynthesisFusionAddin/proto/deps.py @@ -5,7 +5,7 @@ import adsk.core import adsk.fusion -from ..src.general_imports import INTERNAL_ID +from ..src.strings import INTERNAL_ID system = platform.system() diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index 6c853a7897..423b355630 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -7,24 +7,22 @@ import urllib.parse import urllib.request from dataclasses import dataclass -from typing import Any -from result import Ok, Err, Result, is_err -import requests from ..general_imports import ( APP_NAME, DESCRIPTION, INTERNAL_ID, - APS_AUTH, - APS_USER_INFO, gm, my_addin_path, - root_logger, ) CLIENT_ID = "GCxaewcLjsYlK8ud7Ka9AKf9dPwMR3e4GlybyfhAK2zvl3tU" auth_path = os.path.abspath(os.path.join(my_addin_path, "..", ".aps_auth")) +APS_AUTH = None +APS_USER_INFO = None + + @dataclass class APSAuth: access_token: str @@ -66,26 +64,25 @@ def getCodeChallenge() -> str | None: return data["challenge"] -def getAuth() -> APSAuth | None: +def getAuth() -> APSAuth: + global APS_AUTH if APS_AUTH is not None: return APS_AUTH try: + curr_time = time.time() with open(auth_path, "rb") as f: p = pickle.load(f) - logging.getLogger(f"{INTERNAL_ID}").info(f"Auth Path: {auth_path}\n{json.dumps(p)}") APS_AUTH = APSAuth( access_token=p["access_token"], refresh_token=p["refresh_token"], expires_in=p["expires_in"], - expires_at=p["expires_at"], + expires_at=int(curr_time + p["expires_in"] * 1000), token_type=p["token_type"], ) except: - gm.ui.messageBox("Sign in","Sign in") - return None + gm.ui.messageBox("Please Sign In", "Please Sign In") curr_time = int(time.time() * 1000) if curr_time >= APS_AUTH.expires_at: - logging.getLogger(f"{INTERNAL_ID}").info(f"Refreshing {curr_time}\n{json.dumps(APS_AUTH.__dict__)}") refreshAuthToken() if APS_USER_INFO is None: loadUserInfo() @@ -96,25 +93,24 @@ def convertAuthToken(code: str): global APS_AUTH authUrl = f'http://localhost:80/api/aps/code/?code={code}&redirect_uri={urllib.parse.quote_plus("http://localhost:80/api/aps/exporter/")}' res = urllib.request.urlopen(authUrl) - data = _res_json(res)["response"] - curr_time = time.time() * 1000 + curr_time = time.time() APS_AUTH = APSAuth( access_token=data["access_token"], refresh_token=data["refresh_token"], expires_in=data["expires_in"], - expires_at=int(curr_time + (data["expires_in"] * 1000)), + expires_at=int(curr_time + data["expires_in"] * 1000), token_type=data["token_type"], ) with open(auth_path, "wb") as f: - logging.getLogger(f"{INTERNAL_ID}").info(f"APS AUTH: {json.dumps(APS_AUTH.__dict__)}") - pickle.dump(APS_AUTH.__dict__, f) + pickle.dump(data, f) f.close() loadUserInfo() def removeAuth(): + global APS_AUTH, APS_USER_INFO APS_AUTH = None APS_USER_INFO = None pathlib.Path.unlink(pathlib.Path(auth_path)) @@ -129,7 +125,7 @@ def refreshAuthToken(): "client_id": CLIENT_ID, "grant_type": "refresh_token", "refresh_token": APS_AUTH.refresh_token, - "scope": "data:create", + "scope": "data:read", } ).encode("utf-8") req = urllib.request.Request("https://developer.api.autodesk.com/authentication/v2/token", data=body) @@ -138,12 +134,12 @@ def refreshAuthToken(): try: res = urllib.request.urlopen(req) data = _res_json(res) - curr_time = time.time() * 1000 + curr_time = time.time() APS_AUTH = APSAuth( access_token=data["access_token"], refresh_token=data["refresh_token"], expires_in=data["expires_in"], - expires_at=int(curr_time + (data["expires_in"] * 1000)), + expires_at=int(curr_time + data["expires_in"] * 1000), token_type=data["token_type"], ) except urllib.request.HTTPError as e: @@ -153,6 +149,7 @@ def refreshAuthToken(): def loadUserInfo() -> APSUserInfo | None: + global APS_AUTH if not APS_AUTH: return None global APS_USER_INFO @@ -271,10 +268,10 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> Result[st """ # data:create + global APS_AUTH if not APS_AUTH: gm.ui.messageBox("You must login to upload designs to APS", "USER ERROR") - auth_write = APS_AUTH - auth_read = APS_AUTH + auth = APS_AUTH # Get token from APS API later file_name = file_path_to_file_name(file_path) @@ -296,7 +293,7 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> Result[st """ Create APS Storage Location """ - object_id = create_storage_location(auth_write, project_id, folder_id, file_name) + object_id = create_storage_location(auth, project_id, folder_id, file_name) if object_id.is_err(): return Err(None) object_id = object_id.ok() @@ -309,7 +306,7 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> Result[st """ Create Signed URL For APS Upload """ - generate_signed_url_result = generate_signed_url(auth_read, bucket_key, object_key) + generate_signed_url_result = generate_signed_url(auth, bucket_key, object_key) if generate_signed_url_result.is_err(): return Err(None) @@ -323,7 +320,7 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> Result[st if complete_upload(auth_write, upload_key, bucket_key).is_err(): return Err(None) file_name = file_path_to_file_name(file_path) - (_lineage_id, _lineage_href) = create_first_file_version(auth_write, str(object_id), project_id, str(folder_id), file_name) + (_lineage_id, _lineage_href) = create_first_file_version(auth, str(object_id), project_id, str(folder_id), file_name) return Ok(None) diff --git a/exporter/SynthesisFusionAddin/src/general_imports.py b/exporter/SynthesisFusionAddin/src/general_imports.py index 87a6874c53..e522b17fda 100644 --- a/exporter/SynthesisFusionAddin/src/general_imports.py +++ b/exporter/SynthesisFusionAddin/src/general_imports.py @@ -9,11 +9,9 @@ from time import time from types import FunctionType -from requests import get, post -from result import Ok, Err, is_err - import adsk.core import adsk.fusion +from .strings import INTERNAL_ID # hard coded to bypass errors for now PROTOBUF = True diff --git a/exporter/SynthesisFusionAddin/src/strings.py b/exporter/SynthesisFusionAddin/src/strings.py index 01f85eac40..6e3aa109e2 100644 --- a/exporter/SynthesisFusionAddin/src/strings.py +++ b/exporter/SynthesisFusionAddin/src/strings.py @@ -2,5 +2,3 @@ APP_TITLE = "Synthesis Robot Exporter" DESCRIPTION = "Exports files from Fusion into the Synthesis Format" INTERNAL_ID = "synthesis" -APS_AUTH = None -APS_USER_INFO = None From abf0bb22e0247fcbe6429e737a1b1570e7d37840 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 15 Jul 2024 10:07:27 -0700 Subject: [PATCH 058/121] removed .. from deps import path --- exporter/SynthesisFusionAddin/proto/deps.py | 2 +- exporter/SynthesisFusionAddin/src/general_imports.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/exporter/SynthesisFusionAddin/proto/deps.py b/exporter/SynthesisFusionAddin/proto/deps.py index 88034c300a..7a742944cf 100644 --- a/exporter/SynthesisFusionAddin/proto/deps.py +++ b/exporter/SynthesisFusionAddin/proto/deps.py @@ -5,7 +5,7 @@ import adsk.core import adsk.fusion -from ..src.strings import INTERNAL_ID +from src.strings import INTERNAL_ID system = platform.system() diff --git a/exporter/SynthesisFusionAddin/src/general_imports.py b/exporter/SynthesisFusionAddin/src/general_imports.py index e522b17fda..b30041ee17 100644 --- a/exporter/SynthesisFusionAddin/src/general_imports.py +++ b/exporter/SynthesisFusionAddin/src/general_imports.py @@ -11,7 +11,6 @@ import adsk.core import adsk.fusion -from .strings import INTERNAL_ID # hard coded to bypass errors for now PROTOBUF = True @@ -42,7 +41,6 @@ except: logging.getLogger(f"{INTERNAL_ID}.import_manager").error("Failed\n{}".format(traceback.format_exc())) - try: # simple analytics endpoint # A_EP = AnalyticsEndpoint("UA-188467590-1", 1) From 249cfc988e98133d9d7d80126d999a15140536bf Mon Sep 17 00:00:00 2001 From: LucaHaverty Date: Mon, 15 Jul 2024 10:45:34 -0700 Subject: [PATCH 059/121] Parent node fix and better material --- .../systems/preferences/PreferenceTypes.ts | 3 +- .../configuring/scoring/ScoringZonesPanel.tsx | 2 +- .../configuring/scoring/ZoneConfigPanel.tsx | 82 ++++++++++++++++--- 3 files changed, 71 insertions(+), 16 deletions(-) diff --git a/fission/src/systems/preferences/PreferenceTypes.ts b/fission/src/systems/preferences/PreferenceTypes.ts index 44112cc308..54b7b77311 100644 --- a/fission/src/systems/preferences/PreferenceTypes.ts +++ b/fission/src/systems/preferences/PreferenceTypes.ts @@ -1,6 +1,5 @@ import { Vector3Tuple } from "three" import { InputScheme } from "../input/DefaultInputs" -import Jolt from "@barclah/jolt-physics" export type GlobalPreference = | "ScreenMode" @@ -48,7 +47,7 @@ export type RobotPreferences = { export type ScoringZonePreferences = { name: string alliance: "red" | "blue" - parent: Jolt.Body | undefined + parentNode: string | undefined points: number destroyGamepiece: boolean persistentPoints: boolean diff --git a/fission/src/ui/panels/configuring/scoring/ScoringZonesPanel.tsx b/fission/src/ui/panels/configuring/scoring/ScoringZonesPanel.tsx index a277d51c08..067d37a90e 100644 --- a/fission/src/ui/panels/configuring/scoring/ScoringZonesPanel.tsx +++ b/fission/src/ui/panels/configuring/scoring/ScoringZonesPanel.tsx @@ -109,7 +109,7 @@ const ScoringZonesPanel: React.FC = ({ panelId, openLocation, si const newZone: ScoringZonePreferences = { name: "New Scoring Zone", alliance: "blue", - parent: undefined, + parentNode: undefined, points: 0, destroyGamepiece: false, persistentPoints: false, diff --git a/fission/src/ui/panels/configuring/scoring/ZoneConfigPanel.tsx b/fission/src/ui/panels/configuring/scoring/ZoneConfigPanel.tsx index 1fa41b3f87..bb0a373343 100644 --- a/fission/src/ui/panels/configuring/scoring/ZoneConfigPanel.tsx +++ b/fission/src/ui/panels/configuring/scoring/ZoneConfigPanel.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" import Input from "@/components/Input" import Panel, { PanelPropsImpl } from "@/components/Panel" import Button from "@/components/Button" @@ -12,6 +12,11 @@ import SelectButton from "@/ui/components/SelectButton" import Jolt from "@barclah/jolt-physics" import TransformGizmos from "@/ui/components/TransformGizmos" import * as THREE from "three" +import World from "@/systems/World" +import { ReactRgbaColor_ThreeColor } from "@/util/TypeConversions" +import { useTheme } from "@/ui/ThemeContext" +import MirabufSceneObject, { RigidNodeAssociate } from "@/mirabuf/MirabufSceneObject" +import { MiraType } from "@/mirabuf/MirabufLoader" const ZoneConfigPanel: React.FC = ({ panelId, openLocation, sidePadding }) => { const { openPanel, closePanel } = usePanelControlContext() @@ -20,29 +25,48 @@ const ZoneConfigPanel: React.FC = ({ panelId, openLocation, side const [name, setName] = useState(SelectedZone.zone.name) const [alliance, setAlliance] = useState<"red" | "blue">(SelectedZone.zone.alliance) - const [parent, setParent] = useState(SelectedZone.zone.parent) + const [selectedNode, setSelectedNode] = useState(SelectedZone.zone.parentNode) const [points, setPoints] = useState(SelectedZone.zone.points) const [destroy, setDestroy] = useState(SelectedZone.zone.destroyGamepiece) const [persistent, setPersistent] = useState(SelectedZone.zone.persistentPoints) const [transformMode, setTransformMode] = useState<"translate" | "rotate" | "scale">("translate") + const { currentTheme, themes } = useTheme() + const theme = useMemo(() => { + return themes[currentTheme] + }, [currentTheme, themes]) + + const field = useMemo(() => { + + const assemblies = [...World.SceneRenderer.sceneObjects.values()] + for (let i = 0; i < assemblies.length; i++) { + const assembly = assemblies[i] + if (!(assembly instanceof MirabufSceneObject)) continue + + if ((assembly as MirabufSceneObject).miraType != MiraType.FIELD) continue + + return assembly + } + + return undefined + }, []) + useEffect(() => { closePanel("scoring-zones") - - configureGizmo("translate") + configureGizmo() },[]) - const configureGizmo = (mode: "translate" | "rotate" | "scale") => { - // Remove the old transform gizmo - if (transformGizmoRef.current) transformGizmoRef.current.RemoveGizmos() - + const configureGizmo = () => { transformGizmoRef.current = new TransformGizmos( - new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ color: 0xffffff })) + new THREE.Mesh( + new THREE.BoxGeometry(1, 1, 1), + World.SceneRenderer.CreateToonMaterial(ReactRgbaColor_ThreeColor(theme.HighlightSelect.color)) + ) ) transformGizmoRef.current.AddMeshToScene() - transformGizmoRef.current.CreateGizmo(mode, 1.5) + transformGizmoRef.current.CreateGizmo("translate", 1.5) const position = SelectedZone.zone.position const rotation = SelectedZone.zone.rotation @@ -58,7 +82,7 @@ const ZoneConfigPanel: React.FC = ({ panelId, openLocation, side const saveSettings = () => { SelectedZone.zone.name = name SelectedZone.zone.alliance = alliance - SelectedZone.zone.parent = parent + SelectedZone.zone.parentNode = selectedNode SelectedZone.zone.points = points SelectedZone.zone.destroyGamepiece = destroy SelectedZone.zone.persistentPoints = persistent @@ -76,48 +100,80 @@ const ZoneConfigPanel: React.FC = ({ panelId, openLocation, side PreferencesSystem.savePreferences() } + /** Sets the selected node if it is a part of the currently loaded field */ + const trySetSelectedNode = useCallback( + (body: Jolt.BodyID) => { + if (SelectedZone.zone == undefined || Object.keys(PreferencesSystem.getAllFieldPreferences()).length == 0) + return false + + const assoc = World.PhysicsSystem.GetBodyAssociation(body) + if (!assoc || assoc?.sceneObject != field) { + return false + } + + setSelectedNode(assoc.rigidNodeId) + return true + }, + [field] + ) + return ( { saveSettings() if (transformGizmoRef.current) transformGizmoRef.current.RemoveGizmos() openPanel("scoring-zones") }} + onCancel={() => { openPanel("scoring-zones") if (transformGizmoRef.current) transformGizmoRef.current.RemoveGizmos() }} > + {/** Set the zone name */} + + {/** Set the alliance color */} + ) + })} + + ) : ( <> - {zones.map((z: ScoringZonePreferences, i: number) => ( + {zones.map((zonePrefs: ScoringZonePreferences, i: number) => ( { + return zonePrefs + })()} + field={selectedField} openPanel={openPanel} + save={() => saveZones(zones, selectedField)} deleteZone={() => { setZones(zones.filter((_, idx) => idx !== i)) }} - saveZones={saveZones} /> ))}