├── AxiControl.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ └── AxiControl.xcscheme └── xcuserdata │ └── cabatrac.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── AxiControl ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-MacOS-128x128@1x.png │ │ ├── Icon-MacOS-128x128@2x.png │ │ ├── Icon-MacOS-16x16@1x.png │ │ ├── Icon-MacOS-16x16@2x.png │ │ ├── Icon-MacOS-256x256@1x.png │ │ ├── Icon-MacOS-256x256@2x.png │ │ ├── Icon-MacOS-32x32@1x.png │ │ ├── Icon-MacOS-32x32@2x.png │ │ ├── Icon-MacOS-512x512@1x.png │ │ └── Icon-MacOS-512x512@2x.png │ └── Contents.json ├── AxiControl.entitlements ├── AxiControlApp.swift ├── CommandBarView.swift ├── ConsoleView.swift ├── ContentView.swift ├── DragDropContentView.swift ├── Info.plist ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── SidebarView.swift └── WebhookPopoverView.swift ├── LICENSE ├── README.md └── img └── Screenshot.png /AxiControl.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0520C01D2A50C6F8006133DD /* DragDropContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0520C01C2A50C6F8006133DD /* DragDropContentView.swift */; }; 11 | 0558F3592A4A5E9800D6F831 /* AxiControlApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0558F3582A4A5E9800D6F831 /* AxiControlApp.swift */; }; 12 | 0558F35B2A4A5E9800D6F831 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0558F35A2A4A5E9800D6F831 /* ContentView.swift */; }; 13 | 0558F35D2A4A5E9A00D6F831 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0558F35C2A4A5E9A00D6F831 /* Assets.xcassets */; }; 14 | 0558F3602A4A5E9A00D6F831 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0558F35F2A4A5E9A00D6F831 /* Preview Assets.xcassets */; }; 15 | 056910BD2A622C7A007A2EE8 /* WebhookPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 056910BC2A622C7A007A2EE8 /* WebhookPopoverView.swift */; }; 16 | 058FA4A92A549CD600A1871A /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 058FA4A82A549CD600A1871A /* SidebarView.swift */; }; 17 | 058FA4AB2A54E5EC00A1871A /* CommandBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 058FA4AA2A54E5EC00A1871A /* CommandBarView.swift */; }; 18 | 058FA4AD2A54F17D00A1871A /* ConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 058FA4AC2A54F17D00A1871A /* ConsoleView.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | 0520C01C2A50C6F8006133DD /* DragDropContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DragDropContentView.swift; sourceTree = ""; }; 23 | 0520C0202A50C918006133DD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 24 | 0558F3552A4A5E9800D6F831 /* AxiControl.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AxiControl.app; sourceTree = BUILT_PRODUCTS_DIR; }; 25 | 0558F3582A4A5E9800D6F831 /* AxiControlApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AxiControlApp.swift; sourceTree = ""; }; 26 | 0558F35A2A4A5E9800D6F831 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 27 | 0558F35C2A4A5E9A00D6F831 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 28 | 0558F35F2A4A5E9A00D6F831 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 29 | 0558F3612A4A5E9A00D6F831 /* AxiControl.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AxiControl.entitlements; sourceTree = ""; }; 30 | 056910BC2A622C7A007A2EE8 /* WebhookPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhookPopoverView.swift; sourceTree = ""; }; 31 | 058FA4A82A549CD600A1871A /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = ""; }; 32 | 058FA4AA2A54E5EC00A1871A /* CommandBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandBarView.swift; sourceTree = ""; }; 33 | 058FA4AC2A54F17D00A1871A /* ConsoleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleView.swift; sourceTree = ""; }; 34 | /* End PBXFileReference section */ 35 | 36 | /* Begin PBXFrameworksBuildPhase section */ 37 | 0558F3522A4A5E9800D6F831 /* Frameworks */ = { 38 | isa = PBXFrameworksBuildPhase; 39 | buildActionMask = 2147483647; 40 | files = ( 41 | ); 42 | runOnlyForDeploymentPostprocessing = 0; 43 | }; 44 | /* End PBXFrameworksBuildPhase section */ 45 | 46 | /* Begin PBXGroup section */ 47 | 0558F34C2A4A5E9800D6F831 = { 48 | isa = PBXGroup; 49 | children = ( 50 | 0558F3572A4A5E9800D6F831 /* AxiControl */, 51 | 0558F3562A4A5E9800D6F831 /* Products */, 52 | ); 53 | sourceTree = ""; 54 | }; 55 | 0558F3562A4A5E9800D6F831 /* Products */ = { 56 | isa = PBXGroup; 57 | children = ( 58 | 0558F3552A4A5E9800D6F831 /* AxiControl.app */, 59 | ); 60 | name = Products; 61 | sourceTree = ""; 62 | }; 63 | 0558F3572A4A5E9800D6F831 /* AxiControl */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | 0520C0202A50C918006133DD /* Info.plist */, 67 | 0558F3582A4A5E9800D6F831 /* AxiControlApp.swift */, 68 | 0558F35A2A4A5E9800D6F831 /* ContentView.swift */, 69 | 0558F35C2A4A5E9A00D6F831 /* Assets.xcassets */, 70 | 0558F3612A4A5E9A00D6F831 /* AxiControl.entitlements */, 71 | 0558F35E2A4A5E9A00D6F831 /* Preview Content */, 72 | 0520C01C2A50C6F8006133DD /* DragDropContentView.swift */, 73 | 058FA4A82A549CD600A1871A /* SidebarView.swift */, 74 | 058FA4AA2A54E5EC00A1871A /* CommandBarView.swift */, 75 | 058FA4AC2A54F17D00A1871A /* ConsoleView.swift */, 76 | 056910BC2A622C7A007A2EE8 /* WebhookPopoverView.swift */, 77 | ); 78 | path = AxiControl; 79 | sourceTree = ""; 80 | }; 81 | 0558F35E2A4A5E9A00D6F831 /* Preview Content */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | 0558F35F2A4A5E9A00D6F831 /* Preview Assets.xcassets */, 85 | ); 86 | path = "Preview Content"; 87 | sourceTree = ""; 88 | }; 89 | /* End PBXGroup section */ 90 | 91 | /* Begin PBXNativeTarget section */ 92 | 0558F3542A4A5E9800D6F831 /* AxiControl */ = { 93 | isa = PBXNativeTarget; 94 | buildConfigurationList = 0558F3642A4A5E9A00D6F831 /* Build configuration list for PBXNativeTarget "AxiControl" */; 95 | buildPhases = ( 96 | 0558F3512A4A5E9800D6F831 /* Sources */, 97 | 0558F3522A4A5E9800D6F831 /* Frameworks */, 98 | 0558F3532A4A5E9800D6F831 /* Resources */, 99 | ); 100 | buildRules = ( 101 | ); 102 | dependencies = ( 103 | ); 104 | name = AxiControl; 105 | productName = AxiControl; 106 | productReference = 0558F3552A4A5E9800D6F831 /* AxiControl.app */; 107 | productType = "com.apple.product-type.application"; 108 | }; 109 | /* End PBXNativeTarget section */ 110 | 111 | /* Begin PBXProject section */ 112 | 0558F34D2A4A5E9800D6F831 /* Project object */ = { 113 | isa = PBXProject; 114 | attributes = { 115 | BuildIndependentTargetsInParallel = 1; 116 | LastSwiftUpdateCheck = 1430; 117 | LastUpgradeCheck = 1430; 118 | TargetAttributes = { 119 | 0558F3542A4A5E9800D6F831 = { 120 | CreatedOnToolsVersion = 14.3.1; 121 | }; 122 | }; 123 | }; 124 | buildConfigurationList = 0558F3502A4A5E9800D6F831 /* Build configuration list for PBXProject "AxiControl" */; 125 | compatibilityVersion = "Xcode 14.0"; 126 | developmentRegion = en; 127 | hasScannedForEncodings = 0; 128 | knownRegions = ( 129 | en, 130 | Base, 131 | ); 132 | mainGroup = 0558F34C2A4A5E9800D6F831; 133 | productRefGroup = 0558F3562A4A5E9800D6F831 /* Products */; 134 | projectDirPath = ""; 135 | projectRoot = ""; 136 | targets = ( 137 | 0558F3542A4A5E9800D6F831 /* AxiControl */, 138 | ); 139 | }; 140 | /* End PBXProject section */ 141 | 142 | /* Begin PBXResourcesBuildPhase section */ 143 | 0558F3532A4A5E9800D6F831 /* Resources */ = { 144 | isa = PBXResourcesBuildPhase; 145 | buildActionMask = 2147483647; 146 | files = ( 147 | 0558F3602A4A5E9A00D6F831 /* Preview Assets.xcassets in Resources */, 148 | 0558F35D2A4A5E9A00D6F831 /* Assets.xcassets in Resources */, 149 | ); 150 | runOnlyForDeploymentPostprocessing = 0; 151 | }; 152 | /* End PBXResourcesBuildPhase section */ 153 | 154 | /* Begin PBXSourcesBuildPhase section */ 155 | 0558F3512A4A5E9800D6F831 /* Sources */ = { 156 | isa = PBXSourcesBuildPhase; 157 | buildActionMask = 2147483647; 158 | files = ( 159 | 0558F35B2A4A5E9800D6F831 /* ContentView.swift in Sources */, 160 | 056910BD2A622C7A007A2EE8 /* WebhookPopoverView.swift in Sources */, 161 | 058FA4A92A549CD600A1871A /* SidebarView.swift in Sources */, 162 | 058FA4AB2A54E5EC00A1871A /* CommandBarView.swift in Sources */, 163 | 0520C01D2A50C6F8006133DD /* DragDropContentView.swift in Sources */, 164 | 058FA4AD2A54F17D00A1871A /* ConsoleView.swift in Sources */, 165 | 0558F3592A4A5E9800D6F831 /* AxiControlApp.swift in Sources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXSourcesBuildPhase section */ 170 | 171 | /* Begin XCBuildConfiguration section */ 172 | 0558F3622A4A5E9A00D6F831 /* Debug */ = { 173 | isa = XCBuildConfiguration; 174 | buildSettings = { 175 | ALWAYS_SEARCH_USER_PATHS = NO; 176 | CLANG_ANALYZER_NONNULL = YES; 177 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 178 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 179 | CLANG_ENABLE_MODULES = YES; 180 | CLANG_ENABLE_OBJC_ARC = YES; 181 | CLANG_ENABLE_OBJC_WEAK = YES; 182 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 183 | CLANG_WARN_BOOL_CONVERSION = YES; 184 | CLANG_WARN_COMMA = YES; 185 | CLANG_WARN_CONSTANT_CONVERSION = YES; 186 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 187 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 188 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 189 | CLANG_WARN_EMPTY_BODY = YES; 190 | CLANG_WARN_ENUM_CONVERSION = YES; 191 | CLANG_WARN_INFINITE_RECURSION = YES; 192 | CLANG_WARN_INT_CONVERSION = YES; 193 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 194 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 195 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 196 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 197 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 198 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 199 | CLANG_WARN_STRICT_PROTOTYPES = YES; 200 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 201 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 202 | CLANG_WARN_UNREACHABLE_CODE = YES; 203 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 204 | COPY_PHASE_STRIP = NO; 205 | DEBUG_INFORMATION_FORMAT = dwarf; 206 | ENABLE_STRICT_OBJC_MSGSEND = YES; 207 | ENABLE_TESTABILITY = YES; 208 | GCC_C_LANGUAGE_STANDARD = gnu11; 209 | GCC_DYNAMIC_NO_PIC = NO; 210 | GCC_NO_COMMON_BLOCKS = YES; 211 | GCC_OPTIMIZATION_LEVEL = 0; 212 | GCC_PREPROCESSOR_DEFINITIONS = ( 213 | "DEBUG=1", 214 | "$(inherited)", 215 | ); 216 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 217 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 218 | GCC_WARN_UNDECLARED_SELECTOR = YES; 219 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 220 | GCC_WARN_UNUSED_FUNCTION = YES; 221 | GCC_WARN_UNUSED_VARIABLE = YES; 222 | MACOSX_DEPLOYMENT_TARGET = 13.3; 223 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 224 | MTL_FAST_MATH = YES; 225 | ONLY_ACTIVE_ARCH = YES; 226 | SDKROOT = macosx; 227 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 228 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 229 | }; 230 | name = Debug; 231 | }; 232 | 0558F3632A4A5E9A00D6F831 /* Release */ = { 233 | isa = XCBuildConfiguration; 234 | buildSettings = { 235 | ALWAYS_SEARCH_USER_PATHS = NO; 236 | CLANG_ANALYZER_NONNULL = YES; 237 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 238 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 239 | CLANG_ENABLE_MODULES = YES; 240 | CLANG_ENABLE_OBJC_ARC = YES; 241 | CLANG_ENABLE_OBJC_WEAK = YES; 242 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 243 | CLANG_WARN_BOOL_CONVERSION = YES; 244 | CLANG_WARN_COMMA = YES; 245 | CLANG_WARN_CONSTANT_CONVERSION = YES; 246 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 247 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 248 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 249 | CLANG_WARN_EMPTY_BODY = YES; 250 | CLANG_WARN_ENUM_CONVERSION = YES; 251 | CLANG_WARN_INFINITE_RECURSION = YES; 252 | CLANG_WARN_INT_CONVERSION = YES; 253 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 254 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 255 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 256 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 257 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 258 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 259 | CLANG_WARN_STRICT_PROTOTYPES = YES; 260 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 261 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 262 | CLANG_WARN_UNREACHABLE_CODE = YES; 263 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 264 | COPY_PHASE_STRIP = NO; 265 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 266 | ENABLE_NS_ASSERTIONS = NO; 267 | ENABLE_STRICT_OBJC_MSGSEND = YES; 268 | GCC_C_LANGUAGE_STANDARD = gnu11; 269 | GCC_NO_COMMON_BLOCKS = YES; 270 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 271 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 272 | GCC_WARN_UNDECLARED_SELECTOR = YES; 273 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 274 | GCC_WARN_UNUSED_FUNCTION = YES; 275 | GCC_WARN_UNUSED_VARIABLE = YES; 276 | MACOSX_DEPLOYMENT_TARGET = 13.3; 277 | MTL_ENABLE_DEBUG_INFO = NO; 278 | MTL_FAST_MATH = YES; 279 | SDKROOT = macosx; 280 | SWIFT_COMPILATION_MODE = wholemodule; 281 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 282 | }; 283 | name = Release; 284 | }; 285 | 0558F3652A4A5E9A00D6F831 /* Debug */ = { 286 | isa = XCBuildConfiguration; 287 | buildSettings = { 288 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 289 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 290 | CODE_SIGN_ENTITLEMENTS = AxiControl/AxiControl.entitlements; 291 | CODE_SIGN_STYLE = Automatic; 292 | COMBINE_HIDPI_IMAGES = YES; 293 | DEVELOPMENT_ASSET_PATHS = "\"AxiControl/Preview Content\""; 294 | DEVELOPMENT_TEAM = H29KGAMX89; 295 | ENABLE_HARDENED_RUNTIME = YES; 296 | ENABLE_PREVIEWS = YES; 297 | GENERATE_INFOPLIST_FILE = YES; 298 | INFOPLIST_FILE = AxiControl/Info.plist; 299 | INFOPLIST_KEY_CFBundleDisplayName = AxiControl; 300 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 301 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 302 | LD_RUNPATH_SEARCH_PATHS = ( 303 | "$(inherited)", 304 | "@executable_path/../Frameworks", 305 | ); 306 | MARKETING_VERSION = 1.1.1; 307 | PRODUCT_BUNDLE_IDENTIFIER = com.cadinbatrack.AxiControl; 308 | PRODUCT_NAME = "$(TARGET_NAME)"; 309 | SWIFT_EMIT_LOC_STRINGS = YES; 310 | SWIFT_VERSION = 5.0; 311 | }; 312 | name = Debug; 313 | }; 314 | 0558F3662A4A5E9A00D6F831 /* Release */ = { 315 | isa = XCBuildConfiguration; 316 | buildSettings = { 317 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 318 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 319 | CODE_SIGN_ENTITLEMENTS = AxiControl/AxiControl.entitlements; 320 | CODE_SIGN_STYLE = Automatic; 321 | COMBINE_HIDPI_IMAGES = YES; 322 | DEVELOPMENT_ASSET_PATHS = "\"AxiControl/Preview Content\""; 323 | DEVELOPMENT_TEAM = H29KGAMX89; 324 | ENABLE_HARDENED_RUNTIME = YES; 325 | ENABLE_PREVIEWS = YES; 326 | GENERATE_INFOPLIST_FILE = YES; 327 | INFOPLIST_FILE = AxiControl/Info.plist; 328 | INFOPLIST_KEY_CFBundleDisplayName = AxiControl; 329 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 330 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 331 | LD_RUNPATH_SEARCH_PATHS = ( 332 | "$(inherited)", 333 | "@executable_path/../Frameworks", 334 | ); 335 | MARKETING_VERSION = 1.1.1; 336 | PRODUCT_BUNDLE_IDENTIFIER = com.cadinbatrack.AxiControl; 337 | PRODUCT_NAME = "$(TARGET_NAME)"; 338 | SWIFT_EMIT_LOC_STRINGS = YES; 339 | SWIFT_VERSION = 5.0; 340 | }; 341 | name = Release; 342 | }; 343 | /* End XCBuildConfiguration section */ 344 | 345 | /* Begin XCConfigurationList section */ 346 | 0558F3502A4A5E9800D6F831 /* Build configuration list for PBXProject "AxiControl" */ = { 347 | isa = XCConfigurationList; 348 | buildConfigurations = ( 349 | 0558F3622A4A5E9A00D6F831 /* Debug */, 350 | 0558F3632A4A5E9A00D6F831 /* Release */, 351 | ); 352 | defaultConfigurationIsVisible = 0; 353 | defaultConfigurationName = Release; 354 | }; 355 | 0558F3642A4A5E9A00D6F831 /* Build configuration list for PBXNativeTarget "AxiControl" */ = { 356 | isa = XCConfigurationList; 357 | buildConfigurations = ( 358 | 0558F3652A4A5E9A00D6F831 /* Debug */, 359 | 0558F3662A4A5E9A00D6F831 /* Release */, 360 | ); 361 | defaultConfigurationIsVisible = 0; 362 | defaultConfigurationName = Release; 363 | }; 364 | /* End XCConfigurationList section */ 365 | }; 366 | rootObject = 0558F34D2A4A5E9800D6F831 /* Project object */; 367 | } 368 | -------------------------------------------------------------------------------- /AxiControl.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AxiControl.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AxiControl.xcodeproj/xcshareddata/xcschemes/AxiControl.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 59 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /AxiControl.xcodeproj/xcuserdata/cabatrac.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | AxiControl.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 0558F3542A4A5E9800D6F831 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /AxiControl/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /AxiControl/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-MacOS-16x16@1x.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Icon-MacOS-16x16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Icon-MacOS-32x32@1x.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Icon-MacOS-32x32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Icon-MacOS-128x128@1x.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Icon-MacOS-128x128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Icon-MacOS-256x256@1x.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "Icon-MacOS-256x256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Icon-MacOS-512x512@1x.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "Icon-MacOS-512x512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-128x128@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cadin/axi-control/ccf3758002caa632ab87c9d0c3d4261127911ae5/AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-128x128@1x.png -------------------------------------------------------------------------------- /AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cadin/axi-control/ccf3758002caa632ab87c9d0c3d4261127911ae5/AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-128x128@2x.png -------------------------------------------------------------------------------- /AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-16x16@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cadin/axi-control/ccf3758002caa632ab87c9d0c3d4261127911ae5/AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-16x16@1x.png -------------------------------------------------------------------------------- /AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cadin/axi-control/ccf3758002caa632ab87c9d0c3d4261127911ae5/AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-16x16@2x.png -------------------------------------------------------------------------------- /AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-256x256@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cadin/axi-control/ccf3758002caa632ab87c9d0c3d4261127911ae5/AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-256x256@1x.png -------------------------------------------------------------------------------- /AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cadin/axi-control/ccf3758002caa632ab87c9d0c3d4261127911ae5/AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-256x256@2x.png -------------------------------------------------------------------------------- /AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-32x32@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cadin/axi-control/ccf3758002caa632ab87c9d0c3d4261127911ae5/AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-32x32@1x.png -------------------------------------------------------------------------------- /AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cadin/axi-control/ccf3758002caa632ab87c9d0c3d4261127911ae5/AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-32x32@2x.png -------------------------------------------------------------------------------- /AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-512x512@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cadin/axi-control/ccf3758002caa632ab87c9d0c3d4261127911ae5/AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-512x512@1x.png -------------------------------------------------------------------------------- /AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cadin/axi-control/ccf3758002caa632ab87c9d0c3d4261127911ae5/AxiControl/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-512x512@2x.png -------------------------------------------------------------------------------- /AxiControl/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AxiControl/AxiControl.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /AxiControl/AxiControlApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AxiControlApp.swift 3 | // AxiControl 4 | // 5 | // Created by Cadin Batrack on 6/26/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct AxiControlApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /AxiControl/CommandBarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandBarView.swift 3 | // AxiControl 4 | // 5 | // Created by Cadin Batrack on 7/4/23. 6 | // 7 | import SwiftUI 8 | 9 | 10 | 11 | struct CommandBarView: View { 12 | var startPlot:() -> Void 13 | var resumeFromLocation:() -> Void 14 | var resumeFromHome:() -> Void 15 | var runPreview:() -> Void 16 | 17 | var hasFile: Bool 18 | var hasOutputFile: Bool 19 | 20 | 21 | var body: some View { 22 | 23 | HStack { 24 | HStack { 25 | Button(action: resumeFromLocation) { 26 | Text("Resume") 27 | }.disabled(!hasOutputFile) 28 | Button(action: resumeFromHome) { 29 | Text("Save home") 30 | }.disabled(!hasOutputFile) 31 | } 32 | 33 | Spacer() 34 | 35 | Button(action: runPreview){ 36 | Text("Preview") 37 | }.disabled(!hasFile) 38 | Button(action: startPlot) { 39 | Text("Start plot") 40 | } 41 | .buttonStyle(.borderedProminent) 42 | .disabled(!hasFile) 43 | 44 | } 45 | .padding() 46 | .background(.white) 47 | .overlay(Divider().background(Color(red: 0.88, green: 0.88, blue: 0.88)), alignment: .top) 48 | 49 | } 50 | } 51 | 52 | 53 | 54 | 55 | struct CommandBarView_Previews: PreviewProvider { 56 | 57 | static var previews: some View { 58 | CommandBarView(startPlot: nullCommand, resumeFromLocation: nullCommand, resumeFromHome: nullCommand, runPreview: nullCommand, hasFile: true, hasOutputFile: false) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /AxiControl/ConsoleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConsoleView.swift 3 | // AxiControl 4 | // 5 | // Created by Cadin Batrack on 7/4/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ConsoleView: View { 11 | var output: String 12 | 13 | let monoFont = Font 14 | .system(size: 10) 15 | .monospaced() 16 | 17 | var body: some View { 18 | if(output.count > 0){ 19 | VStack(){ 20 | if(output.count > 0){ 21 | Text(output) 22 | .font(monoFont) 23 | .frame(maxWidth: .infinity, alignment: .leading) 24 | .textSelection(.enabled) 25 | } 26 | 27 | } 28 | 29 | .padding() 30 | .frame(maxWidth: .infinity) 31 | .overlay(Divider().background(Color(red: 95, green: 0.95, blue: 0.95)), alignment: .top) 32 | } 33 | } 34 | } 35 | 36 | struct ConsoleView_Previews: PreviewProvider { 37 | static var previews: some View { 38 | ConsoleView(output: "output") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /AxiControl/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // AxiControl 4 | // 5 | // Created by Cadin Batrack on 6/26/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | 11 | let plotDimensions: [[String: Double]] = [ 12 | ["x": 0, "y": 0], // models are 1-indexed 13 | ["x": 11.81, "y": 8.58], // V2, V3, SE/A4 14 | ["x": 16.93, "y": 11.69], // V3/A3, SE/A3 15 | ["x": 23.42, "y": 8.58], // V3 XLX 16 | ["x": 6.3, "y": 4], // MiniKit 17 | ["x": 34.02, "y": 23.39], // SE/A1 18 | ["x": 23.39, "y": 17.01], // SE/A2 19 | ["x": 7.48, "y": 5.51] // V3/B6 20 | ] 21 | 22 | struct ContentView: View { 23 | @AppStorage("modelNumber") var modelNumber = 1 24 | @AppStorage("modelIndex") var modelIndex = 1 25 | 26 | @AppStorage("reorderNumber") var reorderNumber = 1 27 | @AppStorage("reorderIndex") var reorderIndex = 1 28 | @AppStorage("runWebhook") var runWebhook = false 29 | @AppStorage("webhookURL") var webhookURL = "" 30 | @AppStorage("speed") var speed = 25.0 31 | 32 | 33 | @State var removeHiddenLines = false 34 | 35 | @State var isRunning = false 36 | @State var outputMessage = "" 37 | @State var errorMessage = "" 38 | 39 | @State var currentFileURL: URL? 40 | @State var originalFileURL: URL? 41 | 42 | @State var showPopover = false 43 | 44 | @State var tempCount = 0 45 | 46 | @State var error = "" 47 | @State var showError = false 48 | 49 | @State private var runningProcess: Process? 50 | @State var isPlotting = false 51 | 52 | @State var plotSingleLayer = false 53 | @State var singleLayerNum = "1" 54 | 55 | let axiURL = URL(fileURLWithPath: "/usr/local/bin/axicli") 56 | 57 | func onFilePathChanged(url: URL) { 58 | currentFileURL = url 59 | originalFileURL = url 60 | tempCount = 0 61 | onPreviewInvalidated() 62 | } 63 | 64 | func goHome() { 65 | sendAxiCommand( "-m manual -M walk_home") 66 | } 67 | 68 | func runAxiCLI(args: [String]) { 69 | Task { 70 | do { 71 | let outputPipe = Pipe() 72 | let errorPipe = Pipe() 73 | self.isRunning = true 74 | 75 | print(args.joined(separator: " ")) 76 | 77 | let task = Process() 78 | task.executableURL = axiURL 79 | task.standardOutput = outputPipe 80 | task.standardError = errorPipe 81 | 82 | task.arguments = args 83 | 84 | task.terminationHandler = { _ in 85 | DispatchQueue.main.async { 86 | self.isRunning = false 87 | self.isPlotting = false 88 | 89 | let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() 90 | outputMessage = String(decoding: outputData, as: UTF8.self) 91 | 92 | let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() 93 | errorMessage = String(decoding: errorData, as: UTF8.self) 94 | 95 | if(errorMessage.count > 0){ 96 | // time estimate is sent through standard error 97 | if(errorMessage.hasPrefix("Estimated print time:")) { 98 | var splits = errorMessage.split(separator: "\n\n") 99 | outputMessage = splits[0].trimmingCharacters(in: .whitespacesAndNewlines) 100 | 101 | if(splits.count > 1){ 102 | splits.removeFirst(1) 103 | errorMessage = splits.joined(separator: "\n\n") 104 | showError = true 105 | } 106 | } else { 107 | showError = true 108 | } 109 | } 110 | } 111 | } 112 | 113 | try task.run() 114 | runningProcess = task 115 | 116 | } catch { 117 | errorMessage = "Error sending command" 118 | showError = true 119 | } 120 | } 121 | } 122 | 123 | func showLayerError() { 124 | errorMessage = "Invalid layer number" 125 | showError = true 126 | } 127 | 128 | func getArgsForCommand(_ cmd: String) -> [String]? { 129 | let args = cmd.count > 0 ? cmd.components(separatedBy: " ") : [] 130 | 131 | var newArgs = args + ["--model", String(modelNumber), "--reordering", String(reorderNumber), "--speed_pendown", String(Int(speed)) ] 132 | 133 | if(plotSingleLayer && !newArgs.contains("--mode")){ 134 | if let layerNum = Int(singleLayerNum){ 135 | if(layerNum > 0 && layerNum <= 1000){ 136 | newArgs = newArgs + ["--mode", "layers", "--layer", "\(layerNum)"] 137 | } else { 138 | showLayerError() 139 | return nil 140 | } 141 | } else { 142 | showLayerError() 143 | return nil 144 | } 145 | } 146 | 147 | if(runWebhook && webhookURL.count > 0){ 148 | newArgs = newArgs + ["--webhook", "--webhook_url", webhookURL] 149 | } 150 | 151 | if(removeHiddenLines){ 152 | newArgs = newArgs + ["--hiding"] 153 | } 154 | 155 | return newArgs 156 | } 157 | 158 | func sendAxiCommand(_ cmd: String, withFile path: String, outputPath: String) { 159 | if let args = getArgsForCommand(cmd) { 160 | runAxiCLI(args: [path, "-o", outputPath] + args) 161 | } 162 | } 163 | 164 | func sendAxiCommand(_ cmd:String, withFile path: String) { 165 | if let args = getArgsForCommand(cmd) { 166 | runAxiCLI(args: [path] + args) 167 | } 168 | } 169 | 170 | func sendAxiCommand(_ cmd: String) { 171 | print("command: " + cmd) 172 | let args = cmd.components(separatedBy: " ") 173 | runAxiCLI(args: args) 174 | } 175 | 176 | func version() { 177 | sendAxiCommand( "--version") 178 | } 179 | 180 | func disableMotors() { 181 | sendAxiCommand("-m manual -M disable_xy") 182 | } 183 | 184 | func enableMotors() { 185 | sendAxiCommand("-m manual -M enable_xy") 186 | } 187 | 188 | func walkX() { 189 | if let dist = plotDimensions[modelNumber]["x"] { 190 | sendAxiCommand("-m manual -M walk_x --dist \(dist)") 191 | } 192 | } 193 | 194 | func walkY() { 195 | if let dist = plotDimensions[modelNumber]["y"] { 196 | sendAxiCommand("-m manual -M walk_y --dist \(dist)") 197 | } 198 | } 199 | 200 | func runPreview() { 201 | if let url = currentFileURL { 202 | sendAxiCommand("-v --report_time", withFile: url.path) 203 | } 204 | 205 | } 206 | 207 | func penUp() { 208 | sendAxiCommand("-m manual -M raise_pen") 209 | } 210 | 211 | func penDown() { 212 | sendAxiCommand("-m manual -M lower_pen") 213 | } 214 | 215 | func getNewOutputPath(path: URL)->String { 216 | tempCount = tempCount+1 217 | let outputPath = path.deletingPathExtension().path + "-tmp\(tempCount).svg" 218 | return outputPath 219 | } 220 | 221 | func getCurrentOutputPath(path: URL)->String { 222 | return path.deletingPathExtension().path + "-tmp\(tempCount).svg" 223 | } 224 | 225 | func startPlot() { 226 | if let url = currentFileURL { 227 | if let original = originalFileURL { 228 | let outputPath = getNewOutputPath(path: original) 229 | isPlotting = true 230 | sendAxiCommand("", withFile: url.path, outputPath: outputPath) 231 | } 232 | } 233 | } 234 | 235 | func terminatePlot() { 236 | runningProcess?.terminate() 237 | goHome() 238 | } 239 | 240 | func resumeFromLocation() { 241 | if let original = originalFileURL { 242 | let url = getCurrentOutputPath(path: original) 243 | let outputPath = getNewOutputPath(path: original) 244 | currentFileURL = URL(string: url) 245 | isPlotting = true 246 | sendAxiCommand("--mode res_plot", withFile: url, outputPath: outputPath) 247 | } 248 | } 249 | 250 | func resumeFromHome() { 251 | if let original = originalFileURL { 252 | let url = getCurrentOutputPath(path: original) 253 | let outputPath = getNewOutputPath(path: original) 254 | currentFileURL = URL(string: url) 255 | sendAxiCommand("--mode res_home", withFile: url, outputPath: outputPath) 256 | } 257 | } 258 | 259 | func outputFileExists() -> Bool { 260 | guard let original = originalFileURL else { return false } 261 | let output = getCurrentOutputPath(path: original) 262 | let fileManager = FileManager.default 263 | return fileManager.fileExists(atPath: output) 264 | } 265 | 266 | func onPreviewInvalidated(){ 267 | outputMessage = "" 268 | } 269 | 270 | 271 | var body: some View { 272 | VStack(alignment: .leading, spacing: 0){ 273 | HStack(spacing: 0){ 274 | SidebarView(modelNumber:$modelNumber, modelIndex: $modelIndex, reorderIndex: $reorderIndex, reorderNumber: $reorderNumber, removeHiddenLines: $removeHiddenLines, runWebhook: $runWebhook, webhookURL: $webhookURL, showPopover: $showPopover, speed: $speed, singleLayer: $plotSingleLayer, layerNum: $singleLayerNum, goHome: goHome, walkX: walkX, walkY: walkY, enableMotors: enableMotors, disableMotors: disableMotors, penUp: penUp, penDown: penDown,onPreviewInvalidated: onPreviewInvalidated, hasFile: currentFileURL != nil, output: outputMessage) 275 | DragDropContentView(onFileDropped: onFilePathChanged) 276 | }.frame(maxWidth: .infinity) 277 | CommandBarView(startPlot: startPlot, resumeFromLocation: resumeFromLocation, resumeFromHome: resumeFromHome, runPreview: runPreview, hasFile: currentFileURL != nil, hasOutputFile: outputFileExists()) 278 | }.alert(errorMessage, isPresented: $showError) { 279 | Button("OK", role: .cancel) { } 280 | } 281 | // .alert("Plot is running", isPresented: $isRunning) { 282 | // Button("Cancel Plot", role: .cancel, action: terminatePlot) 283 | // } 284 | // 285 | } 286 | 287 | } 288 | 289 | 290 | struct ContentView_Previews: PreviewProvider { 291 | static var previews: some View { 292 | ContentView() 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /AxiControl/DragDropContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DragAndDropView.swift 3 | // AxiControl 4 | // 5 | // Created by Cadin Batrack on 7/1/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DragDropContentView: View { 11 | @State private var fileURL: URL? 12 | 13 | var onFileDropped: (URL) -> Void 14 | 15 | var body: some View { 16 | VStack { 17 | 18 | if let url = fileURL { 19 | VStack (spacing: 16){ 20 | VStack { 21 | loadImage(from: url) 22 | .resizable() 23 | .aspectRatio(contentMode: .fit) 24 | } 25 | .background(Color.white 26 | .shadow( radius: 2, x: 0, y: 1) 27 | ) 28 | 29 | Text(url.lastPathComponent) 30 | } 31 | 32 | } else { 33 | Text("Drop an SVG file here") 34 | .padding() 35 | } 36 | 37 | }.frame(maxWidth: .infinity, maxHeight: .infinity) 38 | .dropDestination(for:URL.self) { items, _ in 39 | guard let item = items.first else { return false} 40 | 41 | fileURL = item 42 | onFileDropped(fileURL!) 43 | return true 44 | } 45 | .padding() 46 | } 47 | 48 | func loadImage(from url: URL) -> Image { 49 | guard let imageData = try? Data(contentsOf: url), 50 | let image = NSImage(data: imageData) else { 51 | return Image(systemName: "photo") // Placeholder image if loading fails 52 | } 53 | return Image(nsImage: image) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /AxiControl/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | INIntentsSupported 6 | 7 | Intent 8 | 9 | NSDragPboard 10 | NSFilenamesPboardType 11 | 12 | 13 | -------------------------------------------------------------------------------- /AxiControl/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AxiControl/SidebarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SidebarView.swift 3 | // AxiControl 4 | // 5 | // Created by Cadin Batrack on 7/4/23. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | let controlWidth = 180.0 12 | 13 | 14 | let models: [[String: Any]] = [ 15 | ["name": "AxiDraw V2", "value": 1], 16 | ["name": "AxiDraw V3", "value": 1], 17 | ["name": "AxiDraw SE/A4", "value": 1], 18 | ["name": "AxiDraw V3/A3", "value": 2], 19 | ["name": "AxiDraw SE/A3", "value": 2], 20 | ["name": "AxiDraw V3 XLX", "value": 3], 21 | ["name": "AxiDraw MiniKit", "value": 4], 22 | ["name": "AxiDraw SE/A1", "value": 5], 23 | ["name": "AxiDraw SE/A2", "value": 6], 24 | ["name": "AxiDraw V3/B6", "value": 7], 25 | ] 26 | 27 | let reorderOptions: [[String:Any]] = [ 28 | ["name" : "Least", "value": 0], 29 | ["name" : "Basic", "value": 1], 30 | ["name": "Full", "value": 2], 31 | ["name": "None", "value": 4] 32 | ] 33 | 34 | struct SidebarView: View { 35 | @Binding var modelNumber: Int 36 | @Binding var modelIndex: Int 37 | 38 | @Binding var reorderIndex: Int 39 | @Binding var reorderNumber: Int 40 | 41 | @Binding var removeHiddenLines: Bool 42 | @Binding var runWebhook: Bool 43 | @Binding var webhookURL: String 44 | 45 | @Binding var showPopover:Bool 46 | @Binding var speed: Double 47 | 48 | @Binding var singleLayer: Bool 49 | @Binding var layerNum: String 50 | 51 | var goHome:() -> Void 52 | var walkX:() -> Void 53 | var walkY:() -> Void 54 | var enableMotors:() -> Void 55 | var disableMotors:() -> Void 56 | var penUp:() -> Void 57 | var penDown:() -> Void 58 | var onPreviewInvalidated:() -> Void 59 | 60 | var hasFile: Bool 61 | 62 | var output: String 63 | 64 | @State var textFieldDisabled = true 65 | 66 | func onModelSelect(_ index: Int) { 67 | let num = models[index]["value"] as! Int 68 | modelNumber = num; 69 | modelIndex = index; 70 | } 71 | 72 | func onReorderSelect(_ index: Int) { 73 | let num = reorderOptions[index]["value"] as! Int 74 | reorderNumber = num 75 | reorderIndex = index 76 | } 77 | 78 | private let numberFormatter: NumberFormatter = { 79 | let formatter = NumberFormatter() 80 | formatter.numberStyle = .decimal 81 | return formatter 82 | }() 83 | 84 | private let validCharacterSet = CharacterSet.decimalDigits 85 | 86 | var body: some View { 87 | 88 | 89 | VStack() { 90 | VStack(alignment: .trailing) { 91 | 92 | HStack { 93 | Text("Model:").font(.subheadline).foregroundColor(Color.black.opacity(0.5)) 94 | Menu (models[modelIndex]["name"] as? String ?? "Select a model"){ 95 | ForEach(models.indices, id: \.self) { index in 96 | Button(action: { onModelSelect(index) }) { 97 | Text(models[index]["name"] as? String ?? "") 98 | } 99 | } 100 | 101 | }.frame(width: controlWidth ) 102 | .onChange(of: modelIndex) { _ in 103 | onPreviewInvalidated() 104 | } 105 | } 106 | 107 | HStack { 108 | Text("Pen:").font(.subheadline).foregroundColor(Color.black.opacity(0.5)) 109 | HStack { 110 | 111 | Button(action: penUp) { 112 | Text("Up").frame(maxWidth: .infinity) 113 | } 114 | Button(action: penDown) { 115 | Text("Down").frame(maxWidth: .infinity) 116 | } 117 | }.frame(width: controlWidth) 118 | } 119 | 120 | HStack { 121 | Text("Motors:").font(.subheadline).foregroundColor(Color.black.opacity(0.5)) 122 | HStack { 123 | 124 | Button(action: enableMotors) { 125 | Text("Enable").frame(maxWidth: .infinity) 126 | } 127 | Button(action: disableMotors) { 128 | Text("Disable").frame(maxWidth: .infinity) 129 | } 130 | }.frame(width: controlWidth) 131 | } 132 | 133 | HStack { 134 | Text("Walk:").font(.subheadline).foregroundColor(Color.black.opacity(0.5)) 135 | HStack { 136 | Button(action: goHome) { 137 | Text("Home").frame(maxWidth: .infinity) 138 | } 139 | Button(action: walkX) { 140 | Text("Max X").frame(maxWidth: .infinity) 141 | } 142 | Button(action: walkY) { 143 | Text("Max Y").frame(maxWidth: .infinity) 144 | } 145 | }.frame(width: controlWidth) 146 | 147 | } 148 | Spacer().frame(height: 32) 149 | 150 | HStack { 151 | Text("Speed:").font(.subheadline).foregroundColor(Color.black.opacity(0.5)) 152 | Slider( 153 | value: $speed, 154 | in: 1...110, 155 | label: { 156 | Text("\(Int(speed))") 157 | .frame(width: 24, alignment: .leading) 158 | } 159 | ) 160 | .frame(width: controlWidth ) 161 | .onChange(of: speed) { _ in 162 | onPreviewInvalidated() 163 | } 164 | } 165 | 166 | HStack { 167 | Text("Reorder:").font(.subheadline).foregroundColor(Color.black.opacity(0.5)) 168 | Menu (reorderOptions[reorderIndex]["name"] as? String ?? "Select"){ 169 | ForEach(reorderOptions.indices, id: \.self) { index in 170 | Button(action: { onReorderSelect(index) }) { 171 | Text(reorderOptions[index]["name"] as? String ?? "") 172 | } 173 | } 174 | 175 | }.frame(width: controlWidth ) 176 | .onChange(of: reorderIndex) { _ in 177 | onPreviewInvalidated() 178 | } 179 | 180 | } 181 | 182 | 183 | HStack { 184 | 185 | Toggle(isOn: $removeHiddenLines) { 186 | Text("Remove hidden lines") 187 | } 188 | .onChange(of: removeHiddenLines) { _ in 189 | onPreviewInvalidated() 190 | } 191 | Spacer() 192 | }.frame(width: controlWidth) 193 | 194 | HStack { 195 | Toggle(isOn: $singleLayer) { 196 | Text("Single layer").frame(minWidth: 80, alignment: .leading) 197 | } 198 | .onChange(of: singleLayer) { _ in 199 | onPreviewInvalidated() 200 | } 201 | Spacer() 202 | TextField("", text: $layerNum) 203 | .onChange(of: layerNum) { _ in 204 | onPreviewInvalidated() 205 | } 206 | .onReceive(Just(layerNum)) { newValue in 207 | let filteredValue = newValue.filter { validCharacterSet.contains($0.unicodeScalars.first!) } 208 | if numberFormatter.number(from: filteredValue) != nil { 209 | layerNum = filteredValue 210 | } else { 211 | layerNum = "" 212 | } 213 | } 214 | .frame(maxWidth: 64) 215 | .disabled(!singleLayer) 216 | 217 | }.frame(width: controlWidth) 218 | 219 | HStack { 220 | Toggle(isOn: $runWebhook) { 221 | Text("Run webhook") 222 | }.disabled(webhookURL.count < 1) 223 | Spacer() 224 | 225 | Button("􀣌") { 226 | self.showPopover = true 227 | }.popover( 228 | isPresented: $showPopover, 229 | arrowEdge: .bottom 230 | ) { WebhookPopoverView(webhookURL: $webhookURL) } 231 | }.textFieldStyle(.roundedBorder).frame(width: controlWidth) 232 | 233 | }.padding() 234 | 235 | 236 | 237 | Spacer() 238 | 239 | 240 | ConsoleView(output: output) 241 | 242 | } 243 | 244 | .frame(minWidth: 274, maxWidth: 274, maxHeight: .infinity) 245 | .background(Color.white) 246 | .overlay(Divider().background(Color(red: 0.88, green: 0.88, blue: 0.88)), alignment: .trailing) 247 | 248 | } 249 | } 250 | 251 | 252 | func nullCommand() {} 253 | 254 | //struct SidebarView_Previews: PreviewProvider { 255 | // 256 | //static var previews: some View { 257 | // SidebarView( modelNumber: .constant(1), modelIndex: .constant(1), reorderIndex: .constant(0), reorderNumber: .constant(1), removeHiddenLines: .constant(true), webhookURL: .constant(""), goHome: nullCommand, walkX: nullCommand, walkY: nullCommand, enableMotors: nullCommand, disableMotors: nullCommand, penUp: nullCommand, penDown: nullCommand, hasFile: true, output: "", error: "") 258 | //} 259 | //} 260 | -------------------------------------------------------------------------------- /AxiControl/WebhookPopoverView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebhookPopoverView.swift 3 | // AxiControl 4 | // 5 | // Created by Cadin Batrack on 7/14/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WebhookPopoverView: View { 11 | @Binding var webhookURL: String 12 | 13 | var body: some View { 14 | VStack(alignment: .leading, spacing: 4){ 15 | Text("Webhook URL:").font(.subheadline).foregroundColor(Color.black.opacity(0.5)) 16 | TextField("Enter URL", text: $webhookURL, onCommit: { 17 | DispatchQueue.main.async { 18 | NSApp.keyWindow?.makeFirstResponder(nil) 19 | } 20 | }).frame(width: 300) 21 | }.padding([.horizontal, .bottom]).padding([.top], 8) 22 | } 23 | } 24 | 25 | struct WebhookPopoverView_Previews: PreviewProvider { 26 | static var previews: some View { 27 | WebhookPopoverView(webhookURL: .constant("")) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AxiControl 2 | 3 | A Mac GUI for the [AxiDraw Command Line Interface](https://axidraw.com/doc/cli_api/). 4 | 5 | ![AxiControl screenshot](img/Screenshot.png) 6 | 7 | ## Installation 8 | 9 | ### 1. Install the AxiDraw CLI 10 | 11 | This app sends commands to AxiDraw through the AxiDraw CLI, so the CLI must be installed in order for the app to work. 12 | 13 | The app expects the CLI to be installed at `/usr/local/bin/`. 14 | 15 | One line install: 16 | `python -m pip install https://cdn.evilmadscientist.com/dl/ad/public/AxiDraw_API.zip` 17 | 18 | See the full installation instructions on [AxiDraw.com](https://www.axidraw.com/doc/cli_api/#installation). 19 | 20 | ### 2. Download AxiControl 21 | 22 | Download the [`AxiControl.zip`](https://github.com/cadin/axi-control/releases/latest/download/AxiControl.zip) file from the [Releases](https://github.com/cadin/axi-control/releases/) page. 23 | 24 | Unzip and move the app to your Applications folder. 25 | 26 | ### 3. Authorize the app 27 | 28 | This app isn't currently being notarized by Apple. This means you'll get a security warning when trying to launch the app for the first time ([more info here](https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unidentified-developer-mh40616/mac)). 29 | 30 | To launch the app, Control-click (or right-click) the app and choose **Open**. Choose **Open** again from the dialog. 31 | 32 | #### Sequoia 33 | 34 | In Sequoia this process is even more restrictive. 35 | 36 | When you launch the app you'll see a dialog that says '"AxiControl" Not Opened', with no option to open it anyway. 37 | 38 | Go to **Settings > Privacy and Security** and you should see a message that says "AxiControl" was blocked to protect your Mac'. Click **Open Anyway**, then choose **Open Anyway** from the dialog. 39 | 40 | You can open the app normally by double-clicking after completing this process. 41 | 42 | ## Features 43 | 44 | ### Plot & Resume 45 | 46 | Drag an SVG onto the app window and choose **Start plot** to start plotting. 47 | 48 | A temp file will be created to save plot progress. Pause the plot with the hardware button, and choose **Resume** to continue plotting where you left off. 49 | 50 | You can also choose **Save home** when paused. This will move the carriage to the home position and save the move to the temp file. Choosing **Resume** after **Save home** will resume the plot from the home position. 51 | 52 | ### Walk the carriage 53 | 54 | **Home** moves the carriage to the home position. 55 | 56 | **Max X** and **Max Y** moves the carriage to the maximum plot dimensions. This can be useful to ensure your paper is squarely aligned before beginning a plot. 57 | 58 | **Be sure you have your correct AxiDraw model selected**. Otherwise you may damage your device by walking it outside the maximum range. 59 | 60 | ### Speed control 61 | 62 | The Speed slider controls the speed limit for the XY carriage when the pen is down. This value is expressed as a percentage of maximum travel speed. 63 | 64 | ### Layers control 65 | 66 | Specify a number which indicates which layer (or layers) will be plotted when plotting in layers mode. See the [AxiDraw CLI docs](https://www.axidraw.com/doc/cli_api/#layer) for more info. 67 | 68 | Note: Layers are not a native feature of SVG. You probably have to create your SVGs in Inkscape to create files with usable layer numbers. 69 | See _[Adding layers outside of Inkscape](https://wiki.evilmadscientist.com/AxiDraw_Layer_Control#Adding_layers_outside_of_Inkscape)_ on the EMS wiki. 70 | 71 | ### Webhooks 72 | 73 | Run a webhook when your plot completes. You can easily set up a webhook at [IFTTT](https://ifttt.com/maker_webhooks) to send you a notification when your plot completes. 74 | 75 | Click the gear to enter the URL for your webhook. 76 | 77 | ## Support 78 | 79 | If you encounter a problem please file an issue. 80 | This is a small personal project that is mostly unsupported. 81 | 82 | ### Troubleshooting 83 | 84 | #### Error: "Error sending command" 85 | 86 | This most likely means you haven't installed the AxiDraw CLI properly. Try sending a command from the terminal to verify the installation. 87 | 88 | The app expects the CLI to be installed at `/usr/local/bin/`. 89 | 90 | #### Error: "Failed to connect to AxiDraw." 91 | 92 | The CLI couldn't connect to your AxiDraw. 93 | Make sure the USB cable is connected to the AxiDraw and your Mac. 94 | 95 | #### AxiDraw makes a horrible grinding noise 96 | 97 | This happens when a command tells the AxiDraw to move outside of its maximum plot area. 98 | Make sure you have the correct AxiDraw model selected. 99 | 100 | You may also run outside the maximum bounds by invoking Walk Max X or Y from an offset starting location. 101 | -------------------------------------------------------------------------------- /img/Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cadin/axi-control/ccf3758002caa632ab87c9d0c3d4261127911ae5/img/Screenshot.png --------------------------------------------------------------------------------