diff --git a/.github/workflows/generate-machine-api-types.yml b/.github/workflows/generate-machine-api-types.yml new file mode 100644 index 000000000..3433dd713 --- /dev/null +++ b/.github/workflows/generate-machine-api-types.yml @@ -0,0 +1,49 @@ +name: generate machine-api types + +on: + pull_request: + paths: + - 'openapi/machine-api.json' + - '.github/workflows/generate-machine-api-types.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + + +permissions: + contents: write +jobs: + generate: + runs-on: 'ubuntu-latest' + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + - run: yarn install + - run: yarn generate:machine-api + - run: yarn fmt + - name: check for changes + id: git-check + run: | + git add . + if git status | grep -q "Changes to be committed" + then echo "modified=true" >> $GITHUB_OUTPUT + else echo "modified=false" >> $GITHUB_OUTPUT + fi + - name: Commit changes, if any + if: steps.git-check.outputs.modified == 'true' + run: | + git add . + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git + git fetch origin + echo ${{ github.head_ref }} + git checkout ${{ github.head_ref }} + git commit -am "New machine-api types" || true + git push + git push origin ${{ github.head_ref }} + diff --git a/flake.lock b/flake.lock index 42f2ac436..dff03418b 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1718470082, - "narHash": "sha256-u2F0MMYE+Efc+ocruTbtU/wWHuYHWcJafp5zJ++n/YE=", + "lastModified": 1721933792, + "narHash": "sha256-zYVwABlQnxpbaHMfX6Wt9jhyQstFYwN2XjleOJV3VVg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "3027ba73dfef68eb555fc2fa97aed4e999e74f97", + "rev": "2122a9b35b35719ad9a395fe783eabb092df01b1", "type": "github" }, "original": { @@ -43,11 +43,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1718681902, - "narHash": "sha256-E/T7Ge6ayEQe7FVKMJqDBoHyLhRhjc6u9CmU8MyYfy0=", + "lastModified": 1721960387, + "narHash": "sha256-o21ax+745ETGXrcgc/yUuLw1SI77ymp3xEpJt+w/kks=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "16c8ad83297c278eebe740dea5491c1708960dd1", + "rev": "9cbf831c5b20a53354fc12758abd05966f9f1699", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 79cfa1bbb..cbca77842 100644 --- a/flake.nix +++ b/flake.nix @@ -57,6 +57,7 @@ pkg-config nodejs_22 + yarn ]) ++ pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs; [ libiconv darwin.apple_sdk.frameworks.Security diff --git a/package.json b/package.json index 73eed50a8..628181310 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,8 @@ "bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json", "postinstall": "yarn xstate:typegen", "xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"", - "make:dev": "make dev" + "make:dev": "make dev", + "generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts" }, "prettier": { "trailingComma": "es5", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7a4b4f5ae..f54da0591 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -172,7 +172,9 @@ dependencies = [ "kcl-lib", "kittycad", "log", + "mdns-sd", "oauth2", + "reqwest 0.12.4", "serde_json", "tauri", "tauri-build", @@ -286,7 +288,7 @@ dependencies = [ "futures-io", "futures-lite", "parking", - "polling", + "polling 3.7.0", "rustix", "slab", "tracing", @@ -1570,6 +1572,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2405,6 +2418,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "if-addrs" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabb0019d51a643781ff15c9c8a3e5dedc365c47211270f4e8f82812fedd8f0a" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "image" version = "0.25.2" @@ -2752,7 +2775,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.52.5", + "windows-targets 0.48.5", ] [[package]] @@ -2896,6 +2919,19 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "mdns-sd" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "807457e493076539ff8f202806f9dc2eaa9f13f69701da7ed38eec7a9afd1616" +dependencies = [ + "flume", + "if-addrs", + "log", + "polling 2.8.0", + "socket2", +] + [[package]] name = "memchr" version = "2.7.2" @@ -3635,6 +3671,22 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + [[package]] name = "polling" version = "3.7.0" @@ -4871,6 +4923,9 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "stable_deref_trait" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 419fe6fdf..63ff87664 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -18,7 +18,9 @@ anyhow = "1" kcl-lib = { version = "0.2", path = "../src/wasm-lib/kcl" } kittycad = "0.3.7" log = "0.4.21" +mdns-sd = "0.11.1" oauth2 = "4.4.2" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } serde_json = "1.0" tauri = { version = "2.0.0-beta.23", features = [ "devtools", "unstable"] } tauri-plugin-cli = { version = "2.0.0-beta.7" } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 8ec83645a..d19ae7fd8 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -370,6 +370,70 @@ fn show_in_folder(app: tauri::AppHandle, path: &str) -> Result<(), InvokeError> Ok(()) } +const SERVICE_NAME: &str = "_machine-api._tcp.local."; + +async fn find_machine_api() -> Result> { + println!("Looking for machine API..."); + // Timeout if no response is received after 5 seconds. + let timeout_duration = std::time::Duration::from_secs(5); + + let mdns = mdns_sd::ServiceDaemon::new()?; + + // Browse for a service type. + let receiver = mdns.browse(SERVICE_NAME)?; + let resp = tokio::time::timeout( + timeout_duration, + tokio::spawn(async move { + while let Ok(event) = receiver.recv() { + if let mdns_sd::ServiceEvent::ServiceResolved(info) = event { + if let Some(addr) = info.get_addresses().iter().next() { + return Some(format!("{}:{}", addr, info.get_port())); + } + } + } + + None + }), + ) + .await; + + // Shut down. + mdns.shutdown()?; + + let Ok(Ok(Some(addr))) = resp else { + return Ok(None); + }; + + Ok(Some(addr)) +} + +#[tauri::command] +async fn get_machine_api_ip() -> Result, InvokeError> { + let machine_api = find_machine_api().await.map_err(InvokeError::from_anyhow)?; + + Ok(machine_api) +} + +#[tauri::command] +async fn list_machines() -> Result { + let machine_api = find_machine_api().await.map_err(InvokeError::from_anyhow)?; + + let Some(machine_api) = machine_api else { + // Empty array. + return Ok("[]".to_string()); + }; + + let client = reqwest::Client::new(); + let response = client + .get(format!("http://{}/machines", machine_api)) + .send() + .await + .map_err(|e| InvokeError::from_anyhow(e.into()))?; + + let text = response.text().await.map_err(|e| InvokeError::from_anyhow(e.into()))?; + Ok(text) +} + #[allow(dead_code)] fn open_url_sync(app: &tauri::AppHandle, url: &url::Url) { log::debug!("Opening URL: {:?}", url); @@ -417,6 +481,8 @@ fn main() -> Result<()> { read_project_settings_file, write_project_settings_file, rename_project_directory, + get_machine_api_ip, + list_machines ]) .plugin(tauri_plugin_cli::init()) .plugin(tauri_plugin_deep_link::init()) diff --git a/src/components/CommandBar/CommandBarHeader.tsx b/src/components/CommandBar/CommandBarHeader.tsx index 84093a7e9..6cbde3cd1 100644 --- a/src/components/CommandBar/CommandBarHeader.tsx +++ b/src/components/CommandBar/CommandBarHeader.tsx @@ -124,7 +124,11 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) { 4 ) ) : typeof argValue === 'object' ? ( - JSON.stringify(argValue) + arg.valueSummary ? ( + arg.valueSummary(argValue) + ) : ( + JSON.stringify(argValue) + ) ) : ( {argValue} ) diff --git a/src/components/CustomIcon.tsx b/src/components/CustomIcon.tsx index 2c9eee93b..672ee17f7 100644 --- a/src/components/CustomIcon.tsx +++ b/src/components/CustomIcon.tsx @@ -541,6 +541,16 @@ const CustomIconMap = { /> ), + printer3d: ( + + + + ), polygon: ( + diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index 37ccdcf26..76dbdb89d 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -28,6 +28,7 @@ import { editorManager, sceneEntitiesManager, } from 'lib/singletons' +import { machineManager } from 'lib/machineManager' import { useHotkeys } from 'react-hotkeys-hook' import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance' import { @@ -77,6 +78,7 @@ import { err, trap } from 'lib/trap' import { useCommandsContext } from 'hooks/useCommandsContext' import { modelingMachineEvent } from 'editor/manager' import { hasValidFilletSelection } from 'lang/modifyAst/addFillet' +import { ExportIntent } from 'lang/std/engineConnection' type MachineContext = { state: StateFrom @@ -351,8 +353,57 @@ export const ModelingMachineProvider = ({ return {} }), + Make: async (_, event) => { + if (event.type !== 'Make' || TEST) return + // Check if we already have an export intent. + if (engineCommandManager.exportIntent) { + toast.error('Already exporting') + return + } + // Set the export intent. + engineCommandManager.exportIntent = ExportIntent.Make + + console.log('making', event.data) + // Set the current machine. + machineManager.currentMachine = event.data.machine + + const format: Models['OutputFormat_type'] = { + type: 'stl', + coords: { + forward: { + axis: 'y', + direction: 'negative', + }, + up: { + axis: 'z', + direction: 'positive', + }, + }, + storage: 'ascii', + units: defaultUnit.current, + selection: { type: 'default_scene' }, + } + + toast.promise( + exportFromEngine({ + format: format, + }), + { + loading: 'Starting print...', + success: 'Started print successfully', + error: 'Error while starting print', + } + ) + }, 'Engine export': async (_, event) => { if (event.type !== 'Export' || TEST) return + if (engineCommandManager.exportIntent) { + toast.error('Already exporting') + return + } + // Set the export intent. + engineCommandManager.exportIntent = ExportIntent.Save + console.log('exporting', event.data) const format = { ...event.data, diff --git a/src/components/ModelingSidebar/ModelingSidebar.tsx b/src/components/ModelingSidebar/ModelingSidebar.tsx index abc58ba71..a728b6950 100644 --- a/src/components/ModelingSidebar/ModelingSidebar.tsx +++ b/src/components/ModelingSidebar/ModelingSidebar.tsx @@ -13,6 +13,7 @@ import { CustomIconName } from 'components/CustomIcon' import { useCommandsContext } from 'hooks/useCommandsContext' import { IconDefinition } from '@fortawesome/free-solid-svg-icons' import { useKclContext } from 'lang/KclProvider' +import { machineManager } from 'lib/machineManager' interface ModelingSidebarProps { paneOpacity: '' | 'opacity-20' | 'opacity-40' @@ -45,7 +46,30 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) { data: { name: 'Export', groupId: 'modeling' }, }), }, + { + id: 'make', + title: 'Make part', + icon: 'printer3d', + iconClassName: '!p-0', + keybinding: 'Ctrl + Shift + M', + action: async () => { + commandBarSend({ + type: 'Find and select command', + data: { name: 'Make', groupId: 'modeling' }, + }) + }, + hide: () => machineManager.machineCount() === 0, + hideOnPlatform: 'web', + }, ] + const filteredActions: SidebarAction[] = sidebarActions.filter( + (action) => + (!action.hide || (action.hide instanceof Function && !action.hide())) && + (!action.hideOnPlatform || + (isTauri() + ? action.hideOnPlatform === 'web' + : action.hideOnPlatform === 'desktop')) + ) // // Filter out the debug panel if it's not supposed to be shown // // TODO: abstract out for allowing user to configure which panes to show @@ -135,23 +159,30 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) { /> ))} -
- + {filteredActions.length > 0 && ( + <> +
+ + + )}
    void hideOnPlatform?: 'desktop' | 'web' + hide?: boolean | (() => boolean) } diff --git a/src/components/NetworkHealthIndicator.tsx b/src/components/NetworkHealthIndicator.tsx index 2a5af365f..17459619a 100644 --- a/src/components/NetworkHealthIndicator.tsx +++ b/src/components/NetworkHealthIndicator.tsx @@ -103,8 +103,8 @@ export const NetworkHealthIndicator = () => { 'rounded-sm ' + overallConnectionStateColor[overallState].bg } /> - - Network Health ({NETWORK_HEALTH_TEXT[overallState]}) + + Network health ({NETWORK_HEALTH_TEXT[overallState]}) { + const machineCount = Object.keys(machineManager.machines).length + return isTauri() ? ( + + + + {machineCount > 0 && ( +

    + {machineCount} +

    + )} + + Network machines ({machineCount}) + +
    + +
    +

    Network machines

    +

    + {machineCount} +

    +
    + {machineCount > 0 && ( +
      + {Object.entries(machineManager.machines).map( + ([hostname, machine]) => ( +
    • +

      {machine.model || machine.manufacturer}

      +

      + Hostname {hostname} +

      +
    • + ) + )} +
    + )} +
    +
    + ) : null +} diff --git a/src/components/ProjectSidebarMenu.tsx b/src/components/ProjectSidebarMenu.tsx index 5b992b011..8dc1fca1a 100644 --- a/src/components/ProjectSidebarMenu.tsx +++ b/src/components/ProjectSidebarMenu.tsx @@ -12,6 +12,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext' import { CustomIcon } from './CustomIcon' import { useLspContext } from './LspProvider' import { engineCommandManager } from 'lib/singletons' +import { machineManager } from 'lib/machineManager' import usePlatform from 'hooks/usePlatform' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import Tooltip from './Tooltip' @@ -90,12 +91,14 @@ function ProjectMenuPopover({ const { commandBarState, commandBarSend } = useCommandsContext() const { onProjectClose } = useLspContext() const exportCommandInfo = { name: 'Export', groupId: 'modeling' } + const makeCommandInfo = { name: 'Make', groupId: 'modeling' } const findCommand = (obj: { name: string; groupId: string }) => Boolean( commandBarState.context.commands.find( (c) => c.name === obj.name && c.groupId === obj.groupId ) ) + const machineCount = machineManager.machineCount() // We filter this memoized list so that no orphan "break" elements are rendered. const projectMenuItems = useMemo<(ActionButtonProps | 'break')[]>( @@ -144,6 +147,32 @@ function ProjectMenuPopover({ }), }, 'break', + { + id: 'make', + Element: 'button', + className: !isTauri() ? 'hidden' : '', + children: ( + <> + Make current part + {!findCommand(makeCommandInfo) && ( + + Awaiting engine connection + + )} + + ), + disabled: !findCommand(makeCommandInfo) || machineCount === 0, + onClick: () => { + commandBarSend({ + type: 'Find and select command', + data: makeCommandInfo, + }) + }, + }, + 'break', { id: 'go-home', Element: 'button', diff --git a/src/lang/std/artifactMapGraphs/sketchOnFaceOnFaceEtc.png b/src/lang/std/artifactMapGraphs/sketchOnFaceOnFaceEtc.png index d9c7693b1..0bb9d4cf7 100644 Binary files a/src/lang/std/artifactMapGraphs/sketchOnFaceOnFaceEtc.png and b/src/lang/std/artifactMapGraphs/sketchOnFaceOnFaceEtc.png differ diff --git a/src/lang/std/engineConnection.ts b/src/lang/std/engineConnection.ts index 22c3dba90..f877d5ba7 100644 --- a/src/lang/std/engineConnection.ts +++ b/src/lang/std/engineConnection.ts @@ -13,6 +13,8 @@ import { createArtifactGraph, } from 'lang/std/artifactGraph' import { useModelingContext } from 'hooks/useModelingContext' +import { exportMake } from 'lib/exportMake' +import toast from 'react-hot-toast' // TODO(paultag): This ought to be tweakable. const pingIntervalMs = 10000 @@ -30,6 +32,11 @@ interface NewTrackArgs { mediaStream: MediaStream } +export enum ExportIntent { + Save = 'save', + Make = 'make', +} + type ClientMetrics = Models['ClientMetrics_type'] interface WebRTCClientMetrics extends ClientMetrics { @@ -1153,6 +1160,12 @@ export class EngineCommandManager extends EventTarget { reject: (reason: any) => void commandId: string } + /** + * Export intent traxcks the intent of the export. If it is null there is no + * export in progress. Otherwise it is an enum value of the intent. + * Another export cannot be started if one is already in progress. + */ + private _exportIntent: ExportIntent | null = null _commandLogCallBack: (command: CommandLog[]) => void = () => {} resolveReady = () => {} /** Folks should realize that wait for ready does not get called _everytime_ @@ -1205,6 +1218,14 @@ export class EngineCommandManager extends EventTarget { modelingSend: ReturnType['send'] = (() => {}) as any + set exportIntent(intent: ExportIntent | null) { + this._exportIntent = intent + } + + get exportIntent() { + return this._exportIntent + } + start({ disableWebRTC = false, setMediaStream, @@ -1382,9 +1403,36 @@ export class EngineCommandManager extends EventTarget { // because in all other cases we send JSON strings. But in the case of // export we send a binary blob. // Pass this to our export function. - exportSave(event.data).then(() => { - this.pendingExport?.resolve(null) - }, this.pendingExport?.reject) + if (this.exportIntent === null) { + toast.error( + 'Export intent was not set, but export data was received' + ) + console.error( + 'Export intent was not set, but export data was received' + ) + return + } + + switch (this.exportIntent) { + case ExportIntent.Save: { + exportSave(event.data).then(() => { + this.pendingExport?.resolve(null) + }, this.pendingExport?.reject) + break + } + case ExportIntent.Make: { + exportMake(event.data).then((result) => { + if (result) { + this.pendingExport?.resolve(null) + } else { + this.pendingExport?.reject('Failed to make export') + } + }, this.pendingExport?.reject) + break + } + } + // Set the export intent back to null. + this.exportIntent = null return } @@ -1688,7 +1736,13 @@ export class EngineCommandManager extends EventTarget { return Promise.resolve(null) } else if (cmd.type === 'export') { const promise = new Promise((resolve, reject) => { - this.pendingExport = { resolve, reject, commandId: command.cmd_id } + this.pendingExport = { + resolve, + reject: () => { + this.exportIntent = null + }, + commandId: command.cmd_id, + } }) this.engineConnection?.send(command) return promise diff --git a/src/lib/commandBarConfigs/modelingCommandConfig.ts b/src/lib/commandBarConfigs/modelingCommandConfig.ts index 918683012..dceb4dabb 100644 --- a/src/lib/commandBarConfigs/modelingCommandConfig.ts +++ b/src/lib/commandBarConfigs/modelingCommandConfig.ts @@ -1,7 +1,9 @@ import { Models } from '@kittycad/lib' import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes' import { KCL_DEFAULT_LENGTH } from 'lib/constants' +import { components } from 'lib/machine-api' import { Selections } from 'lib/selections' +import { machineManager } from 'lib/machineManager' import { modelingMachine, SketchTool } from 'machines/modelingMachine' type OutputFormat = Models['OutputFormat_type'] @@ -22,6 +24,9 @@ export type ModelingCommandSchema = { type: OutputTypeKey storage?: StorageUnion } + Make: { + machine: components['schemas']['Machine'] + } Extrude: { selection: Selections // & { type: 'face' } would be cool to lock that down // result: (typeof EXTRUSION_RESULTS)[number] @@ -160,6 +165,36 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< }, }, }, + Make: { + hide: 'web', + displayName: 'Make', + description: + 'Export the current part and send to a 3D printer on the network.', + icon: 'printer3d', + needsReview: true, + args: { + machine: { + inputType: 'options', + required: true, + valueSummary: (machine: components['schemas']['Machine']) => + machine.model || machine.manufacturer, + options: () => { + return Object.entries(machineManager.machines).map( + ([hostname, machine]) => ({ + name: `${machine.model || machine.manufacturer}, ${hostname}`, + isCurrent: false, + value: machine as components['schemas']['Machine'], + }) + ) + }, + defaultValue: () => { + return Object.values( + machineManager.machines + )[0] as components['schemas']['Machine'] + }, + }, + }, + }, Extrude: { description: 'Pull a sketch into 3D along its normal or perpendicular.', icon: 'extrude', diff --git a/src/lib/commandTypes.ts b/src/lib/commandTypes.ts index df747e88f..2ed8eabd4 100644 --- a/src/lib/commandTypes.ts +++ b/src/lib/commandTypes.ts @@ -111,6 +111,10 @@ export type CommandArgumentConfig< machineContext?: C ) => boolean) skip?: boolean + /** For showing a summary display of the current value, such as in + * the command bar's header + */ + valueSummary?: (value: OutputType) => string } & ( | { inputType: 'options' @@ -172,6 +176,10 @@ export type CommandArgument< ) => boolean) skip?: boolean machineActor: InterpreterFrom + /** For showing a summary display of the current value, such as in + * the command bar's header + */ + valueSummary?: (value: OutputType) => string } & ( | { inputType: Extract diff --git a/src/lib/createMachineCommand.ts b/src/lib/createMachineCommand.ts index 053010a77..3cf66d6f9 100644 --- a/src/lib/createMachineCommand.ts +++ b/src/lib/createMachineCommand.ts @@ -52,17 +52,22 @@ export function createMachineCommand< return null } else if (commandConfig instanceof Array) { return commandConfig - .map((config) => - createMachineCommand({ + .map((config) => { + const recursiveCommandBarConfig: Partial< + StateMachineCommandSetConfig + > = { + [type]: config, + } + return createMachineCommand({ groupId, type, state, send, actor, - commandBarConfig: { [type]: config }, + commandBarConfig: recursiveCommandBarConfig, onCancel, }) - ) + }) .filter((c) => c !== null) as Command[] } @@ -145,6 +150,7 @@ export function buildCommandArgument< required: arg.required, skip: arg.skip, machineActor, + valueSummary: arg.valueSummary, } satisfies Omit, 'inputType'> if (arg.inputType === 'options') { diff --git a/src/lib/exportMake.ts b/src/lib/exportMake.ts new file mode 100644 index 000000000..d43f3aa4e --- /dev/null +++ b/src/lib/exportMake.ts @@ -0,0 +1,74 @@ +import { deserialize_files } from 'wasm-lib/pkg/wasm_lib' +import { machineManager } from './machineManager' +import toast from 'react-hot-toast' +import { components } from './machine-api' +import ModelingAppFile from './modelingAppFile' + +// Make files locally from an export call. +export async function exportMake(data: ArrayBuffer): Promise { + if (machineManager.machineCount() === 0) { + console.error('No machines available') + toast.error('No machines available') + return null + } + + const machineApiIp = machineManager.machineApiIp + if (!machineApiIp) { + console.error('No machine api ip available') + toast.error('No machine api ip available') + return null + } + + const currentMachine = machineManager.currentMachine + if (!currentMachine) { + console.error('No current machine available') + toast.error('No current machine available') + return null + } + + let machineId = null + if ('id' in currentMachine) { + machineId = currentMachine.id + } else if ('hostname' in currentMachine && currentMachine.hostname) { + machineId = currentMachine.hostname + } else if ('ip' in currentMachine && currentMachine.ip) { + machineId = currentMachine.ip + } + + if (!machineId) { + console.error('No machine id available', currentMachine) + toast.error('No machine id available') + return null + } + + const params: components['schemas']['PrintParameters'] = { + machine_id: machineId, + job_name: 'Exported Job', // TODO: make this the project name. + } + try { + console.log('params', params) + const formData = new FormData() + formData.append('params', JSON.stringify(params)) + let files: ModelingAppFile[] = deserialize_files(new Uint8Array(data)) + let file = files[0] + const fileBlob = new Blob([new Uint8Array(file.contents)], { + type: 'text/plain', + }) + formData.append('file', fileBlob, file.name) + console.log('formData', formData) + + const response = await fetch('http://' + machineApiIp + '/print', { + mode: 'no-cors', + method: 'POST', + body: formData, + }) + + console.log('response', response) + + return response + } catch (error) { + console.error('Error exporting', error) + toast.error('Error exporting') + return null + } +} diff --git a/src/lib/exportSave.ts b/src/lib/exportSave.ts index b63ade70b..443a2c555 100644 --- a/src/lib/exportSave.ts +++ b/src/lib/exportSave.ts @@ -5,11 +5,7 @@ import { save } from '@tauri-apps/plugin-dialog' import { writeFile } from '@tauri-apps/plugin-fs' import JSZip from 'jszip' - -interface ModelingAppFile { - name: string - contents: number[] -} +import ModelingAppFile from './modelingAppFile' const save_ = async (file: ModelingAppFile) => { try { @@ -51,7 +47,7 @@ const save_ = async (file: ModelingAppFile) => { } } catch (e) { // TODO: do something real with the error. - console.log('export error', e) + console.error('export error', e) } } diff --git a/src/lib/machine-api.d.ts b/src/lib/machine-api.d.ts new file mode 100644 index 000000000..24baae054 --- /dev/null +++ b/src/lib/machine-api.d.ts @@ -0,0 +1,925 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + '/': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Return the OpenAPI schema in JSON format. */ + get: operations['api_get_schema'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/machines': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** List available machines and their statuses */ + get: operations['get_machines'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/machines/{id}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Get the status of a specific machine */ + get: operations['get_machine'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/ping': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Return pong. */ + get: operations['ping'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/print': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** Print a given file. File must be a sliceable 3D model. */ + post: operations['print_file'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } +} +export type webhooks = Record +export interface components { + schemas: { + /** @description The type of accessory. */ + AccessoryType: 'none' + /** @description Error information from a response. */ + Error: { + error_code?: string + message: string + request_id: string + } + /** @description An info command. */ + Info: { + /** @enum {string} */ + command: 'get_version' + /** @description The info module. */ + module: components['schemas']['InfoModule'][] + /** @description The reason of the info command. */ + reason?: components['schemas']['Reason'] | null + /** @description The result of the info command. */ + result: components['schemas']['Result'] + /** @description The sequence id. */ + sequence_id: components['schemas']['SequenceId'] + } & { + [key: string]: unknown + } + /** @description An info module. */ + InfoModule: { + /** @description The hardware version. */ + hw_ver: string + /** @description The loader version. */ + loader_ver?: string | null + /** @description The module name. */ + name: string + /** @description The ota version. */ + ota_ver?: string | null + /** @description The project name. */ + project_name?: string | null + /** @description The serial number. */ + sn: string + /** @description The software version. */ + sw_ver: string + } + /** @description The mode for the led. */ + LedMode: 'on' | 'off' | 'flashing' + /** @description The node for the led. */ + LedNode: 'chamber_light' | 'work_light' + /** @description Details for a 3d printer connected over USB. */ + Machine: + | { + id: string + manufacturer: string + model: string + port: string + /** @enum {string} */ + type: 'UsbPrinter' + } + | { + /** @description The hostname of the printer. */ + hostname?: string | null + /** + * Format: ip + * @description The IP address of the printer. + */ + ip: string + /** @description The manufacturer of the printer. */ + manufacturer: components['schemas']['NetworkPrinterManufacturer'] + /** @description The model of the printer. */ + model?: string | null + /** + * Format: uint16 + * @description The port of the printer. + */ + port?: number | null + /** @description The serial number of the printer. */ + serial?: string | null + /** @enum {string} */ + type: 'NetworkPrinter' + } + /** @description A message from a machine. */ + Message: + | { + UsbPrinter: components['schemas']['Message2'] + } + | { + NetworkPrinter: components['schemas']['Message3'] + } + /** + * @description A message from the printer. + * @enum {string} + */ + Message2: 'ok' + /** @description A message from the printer. */ + Message3: + | { + Bambu: components['schemas']['Message4'] + } + | { + Formlabs: Record + } + /** @description A message from/to the printer. */ + Message4: + | { + print: components['schemas']['Print'] + } + | { + info: components['schemas']['Info'] + } + | { + system: components['schemas']['System'] + } + | { + json: unknown + } + | { + unknown: string | null + } + /** @description Network printer manufacturer. */ + NetworkPrinterManufacturer: 'Bambu' | 'Formlabs' + /** @description A nozzle type. */ + NozzleType: 'hardened_steel' | 'stainless_steel' + /** @description The response from the `/ping` endpoint. */ + Pong: { + /** @description The pong response. */ + message: string + } + /** @description A print command. */ + Print: + | ({ + /** @enum {string} */ + command: 'ams_control' + /** @description The param. */ + param?: string | null + /** @description The reason for the message. */ + reason: components['schemas']['Reason'] + /** @description The result of the command. */ + result: components['schemas']['Result'] + /** @description The sequence id. */ + sequence_id: components['schemas']['SequenceId'] + } & { + [key: string]: unknown + }) + | ({ + /** @description The ams. */ + ams?: components['schemas']['PrintAms'] | null + /** + * Format: int64 + * @description The ams rfid status. + */ + ams_rfid_status?: number | null + /** + * Format: int64 + * @description The ams status. + */ + ams_status?: number | null + /** @description The aux part fan. */ + aux_part_fan?: boolean | null + /** + * Format: double + * @description The target bed temperature. + */ + bed_target_temper?: number | null + /** + * Format: double + * @description The bed temperature. + */ + bed_temper?: number | null + /** @description The big fan 1 speed. */ + big_fan1_speed?: string | null + /** @description The big fan 2 speed. */ + big_fan2_speed?: string | null + /** + * Format: double + * @description The chamber temperature. + */ + chamber_temper?: number | null + /** @enum {string} */ + command: 'push_status' + /** @description The cooling fan speed. */ + cooling_fan_speed?: string | null + /** + * Format: int64 + * @description The fan gear. + */ + fan_gear?: number | null + /** @description Force upgrade? */ + force_upgrade?: boolean | null + /** @description The gcode file. */ + gcode_file?: string | null + /** @description The gcode file prepare percent. */ + gcode_file_prepare_percent?: string | null + /** @description The gcode state. */ + gcode_state?: string | null + /** @description The heatbreak fan speed. */ + heatbreak_fan_speed?: string | null + /** @description The hms. */ + hms?: unknown[] | null + /** + * Format: int64 + * @description The home flag. + */ + home_flag?: number | null + /** + * Format: int64 + * @description The hw switch state. + */ + hw_switch_state?: number | null + /** @description The ipcam. */ + ipcam?: components['schemas']['PrintIpcam'] | null + /** + * Format: int64 + * @description The layer num. + */ + layer_num?: number | null + /** @description The lifecycle. */ + lifecycle?: string | null + /** @description The lights report. */ + lights_report?: components['schemas']['PrintLightsReport'][] | null + /** + * Format: int64 + * @description The percentage of the print completed. + */ + mc_percent?: number | null + /** @description The mc print line number. */ + mc_print_line_number?: string | null + /** @description The print stage. */ + mc_print_stage?: string | null + /** + * Format: int64 + * @description The mc print sub stage. + */ + mc_print_sub_stage?: number | null + /** + * Format: int64 + * @description The remaining time of the print. + */ + mc_remaining_time?: number | null + /** @description The mess production state. */ + mess_production_state?: string | null + /** + * Format: int64 + * @description The message. + */ + msg?: number | null + /** @description The nozzle diameter. */ + nozzle_diameter?: string | null + /** + * Format: double + * @description The target nozzle temperature. + */ + nozzle_target_temper?: number | null + /** + * Format: double + * @description The nozzle temperature. + */ + nozzle_temper?: number | null + /** @description The nozzle type. */ + nozzle_type?: components['schemas']['NozzleType'] | null + /** @description Online status. */ + online?: components['schemas']['PrintOnline'] | null + /** + * Format: int64 + * @description The print error. + */ + print_error?: number | null + /** @description The print type. */ + print_type?: string | null + /** @description The profile id. */ + profile_id?: string | null + /** @description The project id. */ + project_id?: string | null + /** + * Format: int64 + * @description The queue est. + */ + queue_est?: number | null + /** + * Format: int64 + * @description The queue number. + */ + queue_number?: number | null + /** + * Format: int64 + * @description The queue sts. + */ + queue_sts?: number | null + /** + * Format: int64 + * @description The queue total. + */ + queue_total?: number | null + /** @description The s obj. */ + s_obj?: unknown[] | null + /** @description Sdcard? */ + sdcard?: boolean | null + /** @description The sequence id. */ + sequence_id: components['schemas']['SequenceId'] + /** + * Format: int64 + * @description The spd lvl. + */ + spd_lvl?: number | null + /** + * Format: int64 + * @description The spd mag. + */ + spd_mag?: number | null + /** @description The stg. */ + stg?: unknown[] | null + /** + * Format: int64 + * @description The stg cur. + */ + stg_cur?: number | null + /** @description The subtask id. */ + subtask_id?: string | null + /** @description The subtask name. */ + subtask_name?: string | null + /** @description The task id. */ + task_id?: string | null + /** + * Format: int64 + * @description The total layer num. + */ + total_layer_num?: number | null + /** @description The upgrade state. */ + upgrade_state?: components['schemas']['PrintUpgradeState'] | null + /** @description The upload. */ + upload?: components['schemas']['PrintUpload'] | null + /** @description The tray. */ + vt_tray?: components['schemas']['PrintTray'] | null + /** @description The wifi signal. */ + wifi_signal?: string | null + } & { + [key: string]: unknown + }) + | ({ + /** @enum {string} */ + command: 'gcode_line' + /** @description The gcode line. */ + param?: string | null + /** @description The reason for the message. */ + reason: components['schemas']['Reason'] + /** @description The result of the command. */ + result: components['schemas']['Result'] + /** @description The return code. */ + return_code?: string | null + /** @description The sequence id. */ + sequence_id: components['schemas']['SequenceId'] + /** + * Format: int64 + * @description The source. + */ + source?: number | null + } & { + [key: string]: unknown + }) + | ({ + /** @enum {string} */ + command: 'project_file' + /** @description The gcode file. */ + gcode_file?: string | null + /** @description The profile id. */ + profile_id: string + /** @description The project id. */ + project_id: string + /** @description The sequence id. */ + sequence_id: components['schemas']['SequenceId'] + /** @description The subtask id. */ + subtask_id: string + /** @description The subtask name. */ + subtask_name: string + /** @description The task id. */ + task_id: string + } & { + [key: string]: unknown + }) + | ({ + /** @enum {string} */ + command: 'pause' + /** @description The reason for the message. */ + reason: components['schemas']['Reason'] + /** @description The result of the command. */ + result: components['schemas']['Result'] + /** @description The sequence id. */ + sequence_id: components['schemas']['SequenceId'] + } & { + [key: string]: unknown + }) + | ({ + /** @enum {string} */ + command: 'resume' + /** @description The reason for the message. */ + reason: components['schemas']['Reason'] + /** @description The result of the command. */ + result: components['schemas']['Result'] + /** @description The sequence id. */ + sequence_id: components['schemas']['SequenceId'] + } & { + [key: string]: unknown + }) + | ({ + /** @enum {string} */ + command: 'stop' + /** @description The sequence id. */ + sequence_id: components['schemas']['SequenceId'] + } & { + [key: string]: unknown + }) + | ({ + /** @enum {string} */ + command: 'extrusion_cali_get' + /** @description The sequence id. */ + sequence_id: components['schemas']['SequenceId'] + } & { + [key: string]: unknown + }) + /** @description The print ams. */ + PrintAms: { + /** @description The ams. */ + ams?: components['schemas']['PrintAmsData'][] | null + /** @description The ams exist bits. */ + ams_exist_bits?: string | null + /** @description The insert flag. */ + insert_flag?: boolean | null + /** @description The power on flag. */ + power_on_flag?: boolean | null + /** @description The tray exist bits. */ + tray_exist_bits?: string | null + /** @description The tray is bbl bits. */ + tray_is_bbl_bits?: string | null + /** @description The tray now. */ + tray_now?: string | null + /** @description The tray pre. */ + tray_pre?: string | null + /** @description The tray read done bits. */ + tray_read_done_bits?: string | null + /** @description The tray reading bits. */ + tray_reading_bits?: string | null + /** @description The tray tar. */ + tray_tar?: string | null + /** + * Format: int64 + * @description The version. + */ + version?: number | null + } & { + [key: string]: unknown + } + /** @description The print ams data. */ + PrintAmsData: { + /** @description The humidity. */ + humidity: string + /** @description The id. */ + id: string + /** @description The temperature. */ + temp: string + /** @description The tray. */ + tray: components['schemas']['PrintTray'][] + } & { + [key: string]: unknown + } + /** @description The print ipcam. */ + PrintIpcam: { + /** @description The ipcam dev. */ + ipcam_dev?: string | null + /** @description The ipcam record. */ + ipcam_record?: string | null + /** + * Format: int64 + * @description The mode bits. + */ + mode_bits?: number | null + /** @description The timelapse. */ + timelapse?: string | null + } & { + [key: string]: unknown + } + /** @description The response from the `/print` endpoint. */ + PrintJobResponse: { + /** @description The job id used for this print. */ + job_id: string + /** @description The parameters used for this print. */ + parameters: components['schemas']['PrintParameters'] + } + /** @description A print lights report. */ + PrintLightsReport: { + /** @description The mode. */ + mode: components['schemas']['LedMode'] + /** @description The node. */ + node: components['schemas']['LedNode'] + } & { + [key: string]: unknown + } + /** @description The print online. */ + PrintOnline: { + /** @description The ahb. */ + ahb: boolean + /** @description The rfid. */ + rfid?: boolean | null + /** + * Format: int64 + * @description The version. + */ + version: number + } & { + [key: string]: unknown + } + /** @description Parameters for printing. */ + PrintParameters: { + /** @description The name for the job. */ + job_name: string + /** @description The machine id to print to. */ + machine_id: string + } + /** @description The print tray. */ + PrintTray: { + /** @description The bed temperature. */ + bed_temp?: string | null + /** @description The bed temperature type. */ + bed_temp_type?: string | null + /** @description The id. */ + id: string + /** + * Format: double + * @description The tray k. + */ + k?: number | null + /** + * Format: int64 + * @description The tray n. + */ + n?: number | null + /** @description The nozzle temperature max. */ + nozzle_temp_max?: string | null + /** @description The nozzle temperature min. */ + nozzle_temp_min?: string | null + /** + * Format: int64 + * @description The tray remain. + */ + remain?: number | null + /** @description The tag uid. */ + tag_uid?: string | null + /** @description The tray color. */ + tray_color?: string | null + /** @description The tray diameter. */ + tray_diameter?: string | null + /** @description The tray id name. */ + tray_id_name?: string | null + /** @description The tray info index. */ + tray_info_idx?: string | null + /** @description The tray sub brands. */ + tray_sub_brands?: string | null + /** @description The tray temperature. */ + tray_temp?: string | null + /** @description The tray time. */ + tray_time?: string | null + /** @description The tray type. */ + tray_type?: string | null + /** @description The tray uuid. */ + tray_uuid?: string | null + /** @description The tray weight. */ + tray_weight?: string | null + /** @description The xcam info. */ + xcam_info?: string | null + } & { + [key: string]: unknown + } + /** @description A print upgrade state. */ + PrintUpgradeState: { + /** @description The consistency request. */ + consistency_request?: boolean | null + /** + * Format: int64 + * @description The dis state. + */ + dis_state?: number | null + /** + * Format: int64 + * @description The error code. + */ + err_code?: number | null + /** @description Force upgrade? */ + force_upgrade?: boolean | null + /** @description The message. */ + message?: string | null + /** @description The module. */ + module?: string | null + /** @description The new version list. */ + new_ver_list?: unknown[] | null + /** + * Format: int64 + * @description The new version state. + */ + new_version_state?: number | null + /** @description The progress. */ + progress?: string | null + /** + * Format: int64 + * @description The sequence id. + */ + sequence_id?: number | null + /** @description The status. */ + status?: string | null + } & { + [key: string]: unknown + } + /** @description The print upload. */ + PrintUpload: { + /** @description The message. */ + message: string + /** + * Format: int64 + * @description The progress. + */ + progress: number + /** @description The status. */ + status: string + } & { + [key: string]: unknown + } + /** @description A reason for a message. */ + Reason: + | 'SUCCESS' + | 'FAIL' + | { + UNKNOWN: string + } + /** @description The result of a message. */ + Result: 'SUCCESS' | 'FAIL' + /** @description The sequence id type. */ + SequenceId: string | number + /** @description A system command. */ + System: + | ({ + /** @enum {string} */ + command: 'ledctrl' + /** + * Format: uint32 + * @description The interval time. + */ + interval_time: number + /** @description The LED mode. */ + led_mode: components['schemas']['LedMode'] + /** @description The LED node. */ + led_node: components['schemas']['LedNode'] + /** + * Format: uint32 + * @description The LED off time. + */ + led_off_time: number + /** + * Format: uint32 + * @description The LED on time. + */ + led_on_time: number + /** + * Format: uint32 + * @description The loop times. + */ + loop_times: number + /** @description The reason for the message. */ + reason?: components['schemas']['Reason'] | null + /** @description The result of the command. */ + result: components['schemas']['Result'] + /** @description The sequence id. */ + sequence_id: components['schemas']['SequenceId'] + } & { + [key: string]: unknown + }) + | ({ + /** @description The accessory type. */ + accessory_type: components['schemas']['AccessoryType'] + /** @description The aux part fan. */ + aux_part_fan: boolean + /** @enum {string} */ + command: 'get_accessories' + /** + * Format: double + * @description The nozzle diameter. + */ + nozzle_diameter: number + /** @description The nozzle type. */ + nozzle_type: components['schemas']['NozzleType'] + /** @description The reason for the message. */ + reason?: components['schemas']['Reason'] | null + /** @description The result of the command. */ + result: components['schemas']['Result'] + /** @description The sequence id. */ + sequence_id: components['schemas']['SequenceId'] + } & { + [key: string]: unknown + }) + } + responses: { + /** @description Error */ + Error: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + } + parameters: never + requestBodies: never + headers: never + pathItems: never +} +export type $defs = Record +export interface operations { + api_get_schema: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description successful operation */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': unknown + } + } + '4XX': components['responses']['Error'] + '5XX': components['responses']['Error'] + } + } + get_machines: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description successful operation */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + [key: string]: components['schemas']['Machine'] + } + } + } + '4XX': components['responses']['Error'] + '5XX': components['responses']['Error'] + } + } + get_machine: { + parameters: { + query?: never + header?: never + path: { + /** @description The machine ID. */ + id: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description successful operation */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Message'] + } + } + '4XX': components['responses']['Error'] + '5XX': components['responses']['Error'] + } + } + ping: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description successful operation */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Pong'] + } + } + '4XX': components['responses']['Error'] + '5XX': components['responses']['Error'] + } + } + print_file: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'multipart/form-data': string + } + } + responses: { + /** @description successful operation */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['PrintJobResponse'] + } + } + '4XX': components['responses']['Error'] + '5XX': components['responses']['Error'] + } + } +} diff --git a/src/lib/machineManager.ts b/src/lib/machineManager.ts new file mode 100644 index 000000000..99f6e5c73 --- /dev/null +++ b/src/lib/machineManager.ts @@ -0,0 +1,74 @@ +import { isTauri } from './isTauri' +import { components } from './machine-api' +import { getMachineApiIp, listMachines } from './tauri' + +export class MachineManager { + private _isTauri: boolean = isTauri() + private _machines: { + [key: string]: components['schemas']['Machine'] + } = {} + private _machineApiIp: string | null = null + private _currentMachine: components['schemas']['Machine'] | null = null + + constructor() { + if (!this._isTauri) { + return + } + + this.updateMachines() + } + + start() { + if (!this._isTauri) { + return + } + + // Start a background job to update the machines every ten seconds. + setInterval(() => { + this.updateMachineApiIp() + this.updateMachines() + }, 10000) + } + + get machines(): { + [key: string]: components['schemas']['Machine'] + } { + return this._machines + } + + machineCount(): number { + return Object.keys(this._machines).length + } + + get machineApiIp(): string | null { + return this._machineApiIp + } + + get currentMachine(): components['schemas']['Machine'] | null { + return this._currentMachine + } + + set currentMachine(machine: components['schemas']['Machine'] | null) { + this._currentMachine = machine + } + + private async updateMachines(): Promise { + if (!this._isTauri) { + return + } + + this._machines = await listMachines() + console.log('Machines:', this._machines) + } + + private async updateMachineApiIp(): Promise { + if (!this._isTauri) { + return + } + + this._machineApiIp = await getMachineApiIp() + } +} + +export const machineManager = new MachineManager() +machineManager.start() diff --git a/src/lib/modelingAppFile.ts b/src/lib/modelingAppFile.ts new file mode 100644 index 000000000..7990a2167 --- /dev/null +++ b/src/lib/modelingAppFile.ts @@ -0,0 +1,4 @@ +export default interface ModelingAppFile { + name: string + contents: number[] +} diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index 026f0daae..8e1a7fdb2 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -9,6 +9,7 @@ import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry' import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState' import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute' import { isTauri } from './isTauri' +import { components } from './machine-api' // Get the app state from tauri. export async function getState(): Promise { @@ -26,6 +27,19 @@ export async function setState(state: ProjectState | undefined): Promise { return await invoke('set_state', { state }) } +// List machines on the local network. +export async function listMachines(): Promise<{ + [key: string]: components['schemas']['Machine'] +}> { + let machines: string = await invoke('list_machines') + return JSON.parse(machines) +} + +// Get the machine-api ip address. +export async function getMachineApiIp(): Promise { + return await invoke('get_machine_api_ip') +} + export async function renameProjectDirectory( projectPath: string, newName: string diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index 6db1df9e9..322df7186 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -202,6 +202,7 @@ export type ModelingMachineEvent = | { type: 'Constrain remove constraints'; data?: PathToNode } | { type: 'Re-execute' } | { type: 'Export'; data: ModelingCommandSchema['Export'] } + | { type: 'Make'; data: ModelingCommandSchema['Make'] } | { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] } | { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] } | { @@ -334,6 +335,13 @@ export const modelingMachine = createMachine( actions: 'Engine export', }, + Make: { + target: 'idle', + internal: true, + cond: 'Has exportable geometry', + actions: 'Make', + }, + 'Delete selection': { target: 'idle', cond: 'has valid selection for deletion', diff --git a/src/wasm-lib/Cargo.lock b/src/wasm-lib/Cargo.lock index d3fd05abd..2a560085d 100644 --- a/src/wasm-lib/Cargo.lock +++ b/src/wasm-lib/Cargo.lock @@ -335,6 +335,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.5.0" @@ -1251,12 +1257,12 @@ dependencies = [ [[package]] name = "image" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" dependencies = [ "bytemuck", - "byteorder", + "byteorder-lite", "num-traits", "png", ] diff --git a/src/wasm-lib/kcl/src/test_server.rs b/src/wasm-lib/kcl/src/test_server.rs index 0ff917d7a..3eb8a03ef 100644 --- a/src/wasm-lib/kcl/src/test_server.rs +++ b/src/wasm-lib/kcl/src/test_server.rs @@ -27,7 +27,7 @@ pub async fn execute_and_snapshot(code: &str, units: UnitLength) -> anyhow::Resu // Save the snapshot locally, to that temporary file. std::fs::write(&output_file, snapshot.contents.0)?; // Decode the snapshot, return it. - let img = image::io::Reader::open(output_file).unwrap().decode()?; + let img = image::ImageReader::open(output_file).unwrap().decode()?; Ok(img) }