Add print button (#3133)

* add print button

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* generate more types

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add a github action to generate machine api-types

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* New machine-api types

* actually print on the real machine

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* add more

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* New machine-api types

* get the current machine

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* New machine-api types

* know when error

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fmt

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add fmt

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* New machine-api types

* empty

* empty

* update machine api

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* New machine-api types

* empty

* New machine-api types

* emptuy

* no circular deps

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* New machine-api types

* remove recursive dep

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
This commit is contained in:
Adam Sunderland
2024-08-04 00:51:30 -04:00
committed by GitHub
parent 54a9a50969
commit baf7d3dd9d
29 changed files with 1615 additions and 47 deletions

View File

@ -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 }}

12
flake.lock generated
View File

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1718470082, "lastModified": 1721933792,
"narHash": "sha256-u2F0MMYE+Efc+ocruTbtU/wWHuYHWcJafp5zJ++n/YE=", "narHash": "sha256-zYVwABlQnxpbaHMfX6Wt9jhyQstFYwN2XjleOJV3VVg=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "3027ba73dfef68eb555fc2fa97aed4e999e74f97", "rev": "2122a9b35b35719ad9a395fe783eabb092df01b1",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -43,11 +43,11 @@
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1718681902, "lastModified": 1721960387,
"narHash": "sha256-E/T7Ge6ayEQe7FVKMJqDBoHyLhRhjc6u9CmU8MyYfy0=", "narHash": "sha256-o21ax+745ETGXrcgc/yUuLw1SI77ymp3xEpJt+w/kks=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "16c8ad83297c278eebe740dea5491c1708960dd1", "rev": "9cbf831c5b20a53354fc12758abd05966f9f1699",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -57,6 +57,7 @@
pkg-config pkg-config
nodejs_22 nodejs_22
yarn
]) ++ pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs; [ ]) ++ pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs; [
libiconv libiconv
darwin.apple_sdk.frameworks.Security darwin.apple_sdk.frameworks.Security

View File

@ -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", "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", "postinstall": "yarn xstate:typegen",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"", "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": { "prettier": {
"trailingComma": "es5", "trailingComma": "es5",

59
src-tauri/Cargo.lock generated
View File

@ -172,7 +172,9 @@ dependencies = [
"kcl-lib", "kcl-lib",
"kittycad", "kittycad",
"log", "log",
"mdns-sd",
"oauth2", "oauth2",
"reqwest 0.12.4",
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build", "tauri-build",
@ -286,7 +288,7 @@ dependencies = [
"futures-io", "futures-io",
"futures-lite", "futures-lite",
"parking", "parking",
"polling", "polling 3.7.0",
"rustix", "rustix",
"slab", "slab",
"tracing", "tracing",
@ -1570,6 +1572,17 @@ dependencies = [
"miniz_oxide", "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]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -2405,6 +2418,16 @@ dependencies = [
"unicode-normalization", "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]] [[package]]
name = "image" name = "image"
version = "0.25.2" version = "0.25.2"
@ -2752,7 +2775,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.52.5", "windows-targets 0.48.5",
] ]
[[package]] [[package]]
@ -2896,6 +2919,19 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" 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]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.2" version = "2.7.2"
@ -3635,6 +3671,22 @@ dependencies = [
"miniz_oxide", "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]] [[package]]
name = "polling" name = "polling"
version = "3.7.0" version = "3.7.0"
@ -4871,6 +4923,9 @@ name = "spin"
version = "0.9.8" version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"

View File

@ -18,7 +18,9 @@ anyhow = "1"
kcl-lib = { version = "0.2", path = "../src/wasm-lib/kcl" } kcl-lib = { version = "0.2", path = "../src/wasm-lib/kcl" }
kittycad = "0.3.7" kittycad = "0.3.7"
log = "0.4.21" log = "0.4.21"
mdns-sd = "0.11.1"
oauth2 = "4.4.2" oauth2 = "4.4.2"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
serde_json = "1.0" serde_json = "1.0"
tauri = { version = "2.0.0-beta.23", features = [ "devtools", "unstable"] } tauri = { version = "2.0.0-beta.23", features = [ "devtools", "unstable"] }
tauri-plugin-cli = { version = "2.0.0-beta.7" } tauri-plugin-cli = { version = "2.0.0-beta.7" }

View File

@ -370,6 +370,70 @@ fn show_in_folder(app: tauri::AppHandle, path: &str) -> Result<(), InvokeError>
Ok(()) Ok(())
} }
const SERVICE_NAME: &str = "_machine-api._tcp.local.";
async fn find_machine_api() -> Result<Option<String>> {
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<Option<String>, InvokeError> {
let machine_api = find_machine_api().await.map_err(InvokeError::from_anyhow)?;
Ok(machine_api)
}
#[tauri::command]
async fn list_machines() -> Result<String, InvokeError> {
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)] #[allow(dead_code)]
fn open_url_sync(app: &tauri::AppHandle, url: &url::Url) { fn open_url_sync(app: &tauri::AppHandle, url: &url::Url) {
log::debug!("Opening URL: {:?}", url); log::debug!("Opening URL: {:?}", url);
@ -417,6 +481,8 @@ fn main() -> Result<()> {
read_project_settings_file, read_project_settings_file,
write_project_settings_file, write_project_settings_file,
rename_project_directory, rename_project_directory,
get_machine_api_ip,
list_machines
]) ])
.plugin(tauri_plugin_cli::init()) .plugin(tauri_plugin_cli::init())
.plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_deep_link::init())

View File

@ -124,7 +124,11 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
4 4
) )
) : typeof argValue === 'object' ? ( ) : typeof argValue === 'object' ? (
JSON.stringify(argValue) arg.valueSummary ? (
arg.valueSummary(argValue)
) : (
JSON.stringify(argValue)
)
) : ( ) : (
<em>{argValue}</em> <em>{argValue}</em>
) )

View File

@ -541,6 +541,16 @@ const CustomIconMap = {
/> />
</svg> </svg>
), ),
printer3d: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16 5H4V7.5H7V7V6H8H9H10V7V7.5H16V5ZM17 7.5V8.5V15V16V17H16V16H15H14H6H5H4V17H3V16V15V8.5V7.5V5V4H4H16H17V5V7.5ZM4 8.5V15H5V13.5V13H5.5H14.5H15V13.5V15H16V8.5H10V9H9V10L8.5 10.5L8 10V9H7V8.5H4ZM14 14V15H6V14H14ZM8 7H9V8H8V7Z"
fill="currentColor"
/>
</svg>
),
polygon: ( polygon: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path

View File

@ -10,6 +10,7 @@ import { coreDump } from 'lang/wasm'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import openWindow from 'lib/openWindow' import openWindow from 'lib/openWindow'
import { NetworkMachineIndicator } from './NetworkMachineIndicator'
export function LowerRightControls({ export function LowerRightControls({
children, children,
@ -100,6 +101,7 @@ export function LowerRightControls({
Settings Settings
</Tooltip> </Tooltip>
</Link> </Link>
<NetworkMachineIndicator className={linkOverrideClassName} />
<NetworkHealthIndicator /> <NetworkHealthIndicator />
<HelpMenu /> <HelpMenu />
</menu> </menu>

View File

@ -28,6 +28,7 @@ import {
editorManager, editorManager,
sceneEntitiesManager, sceneEntitiesManager,
} from 'lib/singletons' } from 'lib/singletons'
import { machineManager } from 'lib/machineManager'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance' import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
import { import {
@ -77,6 +78,7 @@ import { err, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { modelingMachineEvent } from 'editor/manager' import { modelingMachineEvent } from 'editor/manager'
import { hasValidFilletSelection } from 'lang/modifyAst/addFillet' import { hasValidFilletSelection } from 'lang/modifyAst/addFillet'
import { ExportIntent } from 'lang/std/engineConnection'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -351,8 +353,57 @@ export const ModelingMachineProvider = ({
return {} 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) => { 'Engine export': async (_, event) => {
if (event.type !== 'Export' || TEST) return 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) console.log('exporting', event.data)
const format = { const format = {
...event.data, ...event.data,

View File

@ -13,6 +13,7 @@ import { CustomIconName } from 'components/CustomIcon'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons' import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { machineManager } from 'lib/machineManager'
interface ModelingSidebarProps { interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40' paneOpacity: '' | 'opacity-20' | 'opacity-40'
@ -45,7 +46,30 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
data: { name: 'Export', groupId: 'modeling' }, 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 // // 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 // // TODO: abstract out for allowing user to configure which panes to show
@ -135,23 +159,30 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
/> />
))} ))}
</ul> </ul>
<hr className="w-full border-chalkboard-20 dark:border-chalkboard-80" /> {filteredActions.length > 0 && (
<ul id="sidebar-actions" className="w-fit p-2 flex flex-col gap-2"> <>
{sidebarActions.map((action) => ( <hr className="w-full border-chalkboard-20 dark:border-chalkboard-80" />
<ModelingPaneButton <ul
key={action.id} id="sidebar-actions"
paneConfig={{ className="w-fit p-2 flex flex-col gap-2"
id: action.id, >
title: action.title, {filteredActions.map((action) => (
icon: action.icon, <ModelingPaneButton
keybinding: action.keybinding, key={action.id}
iconClassName: action.iconClassName, paneConfig={{
iconSize: 'md', id: action.id,
}} title: action.title,
onClick={action.action} icon: action.icon,
/> keybinding: action.keybinding,
))} iconClassName: action.iconClassName,
</ul> iconSize: 'md',
}}
onClick={action.action}
/>
))}
</ul>
</>
)}
</ul> </ul>
<ul <ul
id="pane-section" id="pane-section"
@ -277,4 +308,5 @@ export type SidebarAction = {
keybinding: string keybinding: string
action: () => void action: () => void
hideOnPlatform?: 'desktop' | 'web' hideOnPlatform?: 'desktop' | 'web'
hide?: boolean | (() => boolean)
} }

View File

@ -103,8 +103,8 @@ export const NetworkHealthIndicator = () => {
'rounded-sm ' + overallConnectionStateColor[overallState].bg 'rounded-sm ' + overallConnectionStateColor[overallState].bg
} }
/> />
<Tooltip position="top-right"> <Tooltip position="top-right" wrapperClassName="ui-open:hidden">
Network Health ({NETWORK_HEALTH_TEXT[overallState]}) Network health ({NETWORK_HEALTH_TEXT[overallState]})
</Tooltip> </Tooltip>
</Popover.Button> </Popover.Button>
<Popover.Panel <Popover.Panel

View File

@ -0,0 +1,62 @@
import { Popover } from '@headlessui/react'
import Tooltip from './Tooltip'
import { machineManager } from 'lib/machineManager'
import { isTauri } from 'lib/isTauri'
import { CustomIcon } from './CustomIcon'
export const NetworkMachineIndicator = ({
className,
}: {
className?: string
}) => {
const machineCount = Object.keys(machineManager.machines).length
return isTauri() ? (
<Popover className="relative">
<Popover.Button
className={
'flex items-center p-0 border-none bg-transparent dark:bg-transparent relative ' +
(className || '')
}
data-testid="network-machine-toggle"
>
<CustomIcon name="printer3d" className="w-5 h-5" />
{machineCount > 0 && (
<p aria-hidden className="flex items-center justify-center text-xs">
{machineCount}
</p>
)}
<Tooltip position="top-right" wrapperClassName="ui-open:hidden">
Network machines ({machineCount})
</Tooltip>
</Popover.Button>
<Popover.Panel
className="absolute right-0 left-auto bottom-full mb-1 w-64 flex flex-col gap-1 align-stretch bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm"
data-testid="network-popover"
>
<div className="flex items-center justify-between p-2 rounded-t-sm bg-chalkboard-20 dark:bg-chalkboard-80">
<h2 className="text-sm font-sans font-normal">Network machines</h2>
<p
data-testid="network"
className="font-bold text-xs uppercase px-2 py-1 rounded-sm"
>
{machineCount}
</p>
</div>
{machineCount > 0 && (
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
{Object.entries(machineManager.machines).map(
([hostname, machine]) => (
<li key={hostname} className={'px-2 py-4 gap-1 last:mb-0 '}>
<p className="">{machine.model || machine.manufacturer}</p>
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
Hostname {hostname}
</p>
</li>
)
)}
</ul>
)}
</Popover.Panel>
</Popover>
) : null
}

View File

@ -12,6 +12,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { useLspContext } from './LspProvider' import { useLspContext } from './LspProvider'
import { engineCommandManager } from 'lib/singletons' import { engineCommandManager } from 'lib/singletons'
import { machineManager } from 'lib/machineManager'
import usePlatform from 'hooks/usePlatform' import usePlatform from 'hooks/usePlatform'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
@ -90,12 +91,14 @@ function ProjectMenuPopover({
const { commandBarState, commandBarSend } = useCommandsContext() const { commandBarState, commandBarSend } = useCommandsContext()
const { onProjectClose } = useLspContext() const { onProjectClose } = useLspContext()
const exportCommandInfo = { name: 'Export', groupId: 'modeling' } const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
const makeCommandInfo = { name: 'Make', groupId: 'modeling' }
const findCommand = (obj: { name: string; groupId: string }) => const findCommand = (obj: { name: string; groupId: string }) =>
Boolean( Boolean(
commandBarState.context.commands.find( commandBarState.context.commands.find(
(c) => c.name === obj.name && c.groupId === obj.groupId (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. // We filter this memoized list so that no orphan "break" elements are rendered.
const projectMenuItems = useMemo<(ActionButtonProps | 'break')[]>( const projectMenuItems = useMemo<(ActionButtonProps | 'break')[]>(
@ -144,6 +147,32 @@ function ProjectMenuPopover({
}), }),
}, },
'break', 'break',
{
id: 'make',
Element: 'button',
className: !isTauri() ? 'hidden' : '',
children: (
<>
<span>Make current part</span>
{!findCommand(makeCommandInfo) && (
<Tooltip
position="right"
wrapperClassName="!max-w-none min-w-fit"
>
Awaiting engine connection
</Tooltip>
)}
</>
),
disabled: !findCommand(makeCommandInfo) || machineCount === 0,
onClick: () => {
commandBarSend({
type: 'Find and select command',
data: makeCommandInfo,
})
},
},
'break',
{ {
id: 'go-home', id: 'go-home',
Element: 'button', Element: 'button',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 373 KiB

After

Width:  |  Height:  |  Size: 374 KiB

View File

@ -13,6 +13,8 @@ import {
createArtifactGraph, createArtifactGraph,
} from 'lang/std/artifactGraph' } from 'lang/std/artifactGraph'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { exportMake } from 'lib/exportMake'
import toast from 'react-hot-toast'
// TODO(paultag): This ought to be tweakable. // TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 10000 const pingIntervalMs = 10000
@ -30,6 +32,11 @@ interface NewTrackArgs {
mediaStream: MediaStream mediaStream: MediaStream
} }
export enum ExportIntent {
Save = 'save',
Make = 'make',
}
type ClientMetrics = Models['ClientMetrics_type'] type ClientMetrics = Models['ClientMetrics_type']
interface WebRTCClientMetrics extends ClientMetrics { interface WebRTCClientMetrics extends ClientMetrics {
@ -1153,6 +1160,12 @@ export class EngineCommandManager extends EventTarget {
reject: (reason: any) => void reject: (reason: any) => void
commandId: string 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 = () => {} _commandLogCallBack: (command: CommandLog[]) => void = () => {}
resolveReady = () => {} resolveReady = () => {}
/** Folks should realize that wait for ready does not get called _everytime_ /** Folks should realize that wait for ready does not get called _everytime_
@ -1205,6 +1218,14 @@ export class EngineCommandManager extends EventTarget {
modelingSend: ReturnType<typeof useModelingContext>['send'] = modelingSend: ReturnType<typeof useModelingContext>['send'] =
(() => {}) as any (() => {}) as any
set exportIntent(intent: ExportIntent | null) {
this._exportIntent = intent
}
get exportIntent() {
return this._exportIntent
}
start({ start({
disableWebRTC = false, disableWebRTC = false,
setMediaStream, setMediaStream,
@ -1382,9 +1403,36 @@ export class EngineCommandManager extends EventTarget {
// because in all other cases we send JSON strings. But in the case of // because in all other cases we send JSON strings. But in the case of
// export we send a binary blob. // export we send a binary blob.
// Pass this to our export function. // Pass this to our export function.
exportSave(event.data).then(() => { if (this.exportIntent === null) {
this.pendingExport?.resolve(null) toast.error(
}, this.pendingExport?.reject) '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 return
} }
@ -1688,7 +1736,13 @@ export class EngineCommandManager extends EventTarget {
return Promise.resolve(null) return Promise.resolve(null)
} else if (cmd.type === 'export') { } else if (cmd.type === 'export') {
const promise = new Promise<null>((resolve, reject) => { const promise = new Promise<null>((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) this.engineConnection?.send(command)
return promise return promise

View File

@ -1,7 +1,9 @@
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes' import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { KCL_DEFAULT_LENGTH } from 'lib/constants' import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import { components } from 'lib/machine-api'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { machineManager } from 'lib/machineManager'
import { modelingMachine, SketchTool } from 'machines/modelingMachine' import { modelingMachine, SketchTool } from 'machines/modelingMachine'
type OutputFormat = Models['OutputFormat_type'] type OutputFormat = Models['OutputFormat_type']
@ -22,6 +24,9 @@ export type ModelingCommandSchema = {
type: OutputTypeKey type: OutputTypeKey
storage?: StorageUnion storage?: StorageUnion
} }
Make: {
machine: components['schemas']['Machine']
}
Extrude: { Extrude: {
selection: Selections // & { type: 'face' } would be cool to lock that down selection: Selections // & { type: 'face' } would be cool to lock that down
// result: (typeof EXTRUSION_RESULTS)[number] // 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: { Extrude: {
description: 'Pull a sketch into 3D along its normal or perpendicular.', description: 'Pull a sketch into 3D along its normal or perpendicular.',
icon: 'extrude', icon: 'extrude',

View File

@ -111,6 +111,10 @@ export type CommandArgumentConfig<
machineContext?: C machineContext?: C
) => boolean) ) => boolean)
skip?: 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' inputType: 'options'
@ -172,6 +176,10 @@ export type CommandArgument<
) => boolean) ) => boolean)
skip?: boolean skip?: boolean
machineActor: InterpreterFrom<T> machineActor: InterpreterFrom<T>
/** For showing a summary display of the current value, such as in
* the command bar's header
*/
valueSummary?: (value: OutputType) => string
} & ( } & (
| { | {
inputType: Extract<CommandInputType, 'options'> inputType: Extract<CommandInputType, 'options'>

View File

@ -52,17 +52,22 @@ export function createMachineCommand<
return null return null
} else if (commandConfig instanceof Array) { } else if (commandConfig instanceof Array) {
return commandConfig return commandConfig
.map((config) => .map((config) => {
createMachineCommand({ const recursiveCommandBarConfig: Partial<
StateMachineCommandSetConfig<T, S>
> = {
[type]: config,
}
return createMachineCommand({
groupId, groupId,
type, type,
state, state,
send, send,
actor, actor,
commandBarConfig: { [type]: config }, commandBarConfig: recursiveCommandBarConfig,
onCancel, onCancel,
}) })
) })
.filter((c) => c !== null) as Command<T, typeof type, S[typeof type]>[] .filter((c) => c !== null) as Command<T, typeof type, S[typeof type]>[]
} }
@ -145,6 +150,7 @@ export function buildCommandArgument<
required: arg.required, required: arg.required,
skip: arg.skip, skip: arg.skip,
machineActor, machineActor,
valueSummary: arg.valueSummary,
} satisfies Omit<CommandArgument<O, T>, 'inputType'> } satisfies Omit<CommandArgument<O, T>, 'inputType'>
if (arg.inputType === 'options') { if (arg.inputType === 'options') {

74
src/lib/exportMake.ts Normal file
View File

@ -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<Response | null> {
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
}
}

View File

@ -5,11 +5,7 @@ import { save } from '@tauri-apps/plugin-dialog'
import { writeFile } from '@tauri-apps/plugin-fs' import { writeFile } from '@tauri-apps/plugin-fs'
import JSZip from 'jszip' import JSZip from 'jszip'
import ModelingAppFile from './modelingAppFile'
interface ModelingAppFile {
name: string
contents: number[]
}
const save_ = async (file: ModelingAppFile) => { const save_ = async (file: ModelingAppFile) => {
try { try {
@ -51,7 +47,7 @@ const save_ = async (file: ModelingAppFile) => {
} }
} catch (e) { } catch (e) {
// TODO: do something real with the error. // TODO: do something real with the error.
console.log('export error', e) console.error('export error', e)
} }
} }

925
src/lib/machine-api.d.ts vendored Normal file
View File

@ -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<string, never>
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<string, never>
}
/** @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<string, never>
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']
}
}
}

74
src/lib/machineManager.ts Normal file
View File

@ -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<void> {
if (!this._isTauri) {
return
}
this._machines = await listMachines()
console.log('Machines:', this._machines)
}
private async updateMachineApiIp(): Promise<void> {
if (!this._isTauri) {
return
}
this._machineApiIp = await getMachineApiIp()
}
}
export const machineManager = new MachineManager()
machineManager.start()

View File

@ -0,0 +1,4 @@
export default interface ModelingAppFile {
name: string
contents: number[]
}

View File

@ -9,6 +9,7 @@ import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState' import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute' import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
import { isTauri } from './isTauri' import { isTauri } from './isTauri'
import { components } from './machine-api'
// Get the app state from tauri. // Get the app state from tauri.
export async function getState(): Promise<ProjectState | undefined> { export async function getState(): Promise<ProjectState | undefined> {
@ -26,6 +27,19 @@ export async function setState(state: ProjectState | undefined): Promise<void> {
return await invoke('set_state', { state }) 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<string>('list_machines')
return JSON.parse(machines)
}
// Get the machine-api ip address.
export async function getMachineApiIp(): Promise<string | null> {
return await invoke<string | null>('get_machine_api_ip')
}
export async function renameProjectDirectory( export async function renameProjectDirectory(
projectPath: string, projectPath: string,
newName: string newName: string

View File

@ -202,6 +202,7 @@ export type ModelingMachineEvent =
| { type: 'Constrain remove constraints'; data?: PathToNode } | { type: 'Constrain remove constraints'; data?: PathToNode }
| { type: 'Re-execute' } | { type: 'Re-execute' }
| { type: 'Export'; data: ModelingCommandSchema['Export'] } | { type: 'Export'; data: ModelingCommandSchema['Export'] }
| { type: 'Make'; data: ModelingCommandSchema['Make'] }
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] } | { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
| { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] } | { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] }
| { | {
@ -334,6 +335,13 @@ export const modelingMachine = createMachine(
actions: 'Engine export', actions: 'Engine export',
}, },
Make: {
target: 'idle',
internal: true,
cond: 'Has exportable geometry',
actions: 'Make',
},
'Delete selection': { 'Delete selection': {
target: 'idle', target: 'idle',
cond: 'has valid selection for deletion', cond: 'has valid selection for deletion',

View File

@ -335,6 +335,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.5.0" version = "1.5.0"
@ -1251,12 +1257,12 @@ dependencies = [
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.1" version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"byteorder", "byteorder-lite",
"num-traits", "num-traits",
"png", "png",
] ]

View File

@ -27,7 +27,7 @@ pub async fn execute_and_snapshot(code: &str, units: UnitLength) -> anyhow::Resu
// Save the snapshot locally, to that temporary file. // Save the snapshot locally, to that temporary file.
std::fs::write(&output_file, snapshot.contents.0)?; std::fs::write(&output_file, snapshot.contents.0)?;
// Decode the snapshot, return it. // 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) Ok(img)
} }