move export to the rust side to make the interface way more clean (#5855)
* move export Signed-off-by: Jess Frazelle <github@jessfraz.com> testing Signed-off-by: Jess Frazelle <github@jessfraz.com> remove debugs Signed-off-by: Jess Frazelle <github@jessfraz.com> fix main Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> fices Signed-off-by: Jess Frazelle <github@jessfraz.com> get rid of logs Signed-off-by: Jess Frazelle <github@jessfraz.com> * Convert async actions anti-pattern to fromPromise actors * Fix tsc by removing a generic type * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup Signed-off-by: Jess Frazelle <github@jessfraz.com> * Update rustContext.ts * fix Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix; Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup Signed-off-by: Jess Frazelle <github@jessfraz.com> * remove weird file Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> --------- Signed-off-by: Jess Frazelle <github@jessfraz.com> Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
This commit is contained in:
@ -405,8 +405,9 @@ extrude001 = extrude(sketch001, length = 50)
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
}
|
||||
)
|
||||
// We updated this test such that you can have multiple exports going at once.
|
||||
test(
|
||||
'ensure you can not export while an export is already going',
|
||||
'ensure you CAN export while an export is already going',
|
||||
{ tag: ['@skipLinux', '@skipWin'] },
|
||||
async ({ page, homePage }) => {
|
||||
const u = await getUtils(page)
|
||||
@ -441,22 +442,13 @@ extrude001 = extrude(sketch001, length = 50)
|
||||
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
|
||||
const successToastMessage = page.getByText(`Exported successfully`)
|
||||
|
||||
await test.step('Blocked second export', async () => {
|
||||
await test.step('second export', async () => {
|
||||
await clickExportButton(page)
|
||||
|
||||
await expect(exportingToastMessage).toBeVisible()
|
||||
|
||||
await clickExportButton(page)
|
||||
|
||||
await test.step('The second export is blocked', async () => {
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
await Promise.all([
|
||||
expect(exportingToastMessage.first()).toBeVisible(),
|
||||
expect(alreadyExportingToastMessage).toBeVisible(),
|
||||
])
|
||||
})
|
||||
|
||||
await test.step('The first export still succeeds', async () => {
|
||||
await Promise.all([
|
||||
expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 }),
|
||||
@ -486,7 +478,7 @@ extrude001 = extrude(sketch001, length = 50)
|
||||
expect(alreadyExportingToastMessage).not.toBeVisible(),
|
||||
])
|
||||
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
await expect(successToastMessage).toHaveCount(2)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
@ -36,6 +36,7 @@
|
||||
"@xstate/inspect": "^0.8.0",
|
||||
"@xstate/react": "^4.1.1",
|
||||
"bonjour-service": "^1.3.0",
|
||||
"bson": "^6.10.3",
|
||||
"chokidar": "^4.0.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"decamelize": "^6.0.0",
|
||||
@ -212,6 +213,7 @@
|
||||
"typescript-eslint": "^8.26.1",
|
||||
"vite": "^5.4.12",
|
||||
"vite-plugin-package-version": "^1.1.0",
|
||||
"vite-plugin-top-level-await": "^1.5.0",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.6.1",
|
||||
"vitest-webgl-canvas-mock": "^1.1.0",
|
||||
|
1
rust/Cargo.lock
generated
1
rust/Cargo.lock
generated
@ -1982,6 +1982,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"kcl-lib",
|
||||
"kittycad",
|
||||
"kittycad-modeling-cmds",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"toml",
|
||||
|
@ -30,6 +30,7 @@ debug = "line-tables-only"
|
||||
[workspace.dependencies]
|
||||
async-trait = "0.1.85"
|
||||
anyhow = { version = "1" }
|
||||
bson = { version = "2.13.0", features = ["uuid-1", "chrono"] }
|
||||
clap = { version = "4.5.31", features = ["derive"] }
|
||||
dashmap = { version = "6.1.0" }
|
||||
http = "1"
|
||||
|
@ -24,6 +24,7 @@ anyhow = { workspace = true, features = ["backtrace"] }
|
||||
async-recursion = "1.1.1"
|
||||
async-trait = { workspace = true }
|
||||
base64 = "0.22.1"
|
||||
bson = { workspace = true }
|
||||
chrono = "0.4.38"
|
||||
clap = { version = "4.5.27", default-features = false, optional = true, features = [
|
||||
"std",
|
||||
@ -95,7 +96,6 @@ wasm-bindgen-futures = "0.4.49"
|
||||
web-sys = { version = "0.3.76", features = ["console"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
bson = { version = "2.13.0", features = ["uuid-1", "chrono"] }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tokio-tungstenite = { version = "0.24.0", features = [
|
||||
"rustls-tls-native-roots",
|
||||
|
@ -104,23 +104,29 @@ impl EngineConnection {
|
||||
})?;
|
||||
|
||||
let value = crate::wasm::JsFuture::from(promise).await.map_err(|e| {
|
||||
KclError::Engine(KclErrorDetails {
|
||||
message: format!("Failed to wait for promise from engine: {:?}", e),
|
||||
source_ranges: vec![source_range],
|
||||
})
|
||||
// Try to parse the error as an engine error.
|
||||
let err_str = e.as_string().unwrap_or_default();
|
||||
if let Ok(kittycad_modeling_cmds::websocket::FailureWebSocketResponse { errors, .. }) =
|
||||
serde_json::from_str(&err_str)
|
||||
{
|
||||
KclError::Engine(KclErrorDetails {
|
||||
message: errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
|
||||
source_ranges: vec![source_range],
|
||||
})
|
||||
} else {
|
||||
KclError::Engine(KclErrorDetails {
|
||||
message: format!("Failed to wait for promise from send modeling command: {:?}", e),
|
||||
source_ranges: vec![source_range],
|
||||
})
|
||||
}
|
||||
})?;
|
||||
|
||||
// Parse the value as a string.
|
||||
let s = value.as_string().ok_or_else(|| {
|
||||
KclError::Engine(KclErrorDetails {
|
||||
message: format!("Failed to get string from response from engine: `{:?}`", value),
|
||||
source_ranges: vec![source_range],
|
||||
})
|
||||
})?;
|
||||
// Convert JsValue to a Uint8Array
|
||||
let data = js_sys::Uint8Array::from(value);
|
||||
|
||||
let ws_result: WebSocketResponse = serde_json::from_str(&s).map_err(|e| {
|
||||
let ws_result: WebSocketResponse = bson::from_slice(&data.to_vec()).map_err(|e| {
|
||||
KclError::Engine(KclErrorDetails {
|
||||
message: format!("Failed to deserialize response from engine: {:?}", e),
|
||||
message: format!("Failed to deserialize bson response from engine: {:?}", e),
|
||||
source_ranges: vec![source_range],
|
||||
})
|
||||
})?;
|
||||
|
@ -11,7 +11,7 @@ crate-type = ["cdylib"]
|
||||
bench = false
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
bson = { version = "2.13.0", features = ["uuid-1", "chrono"] }
|
||||
bson = { workspace = true, features = ["uuid-1", "chrono"] }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
data-encoding = "2.6.0"
|
||||
futures = "0.3.31"
|
||||
@ -22,6 +22,7 @@ gloo-utils = "0.2.0"
|
||||
js-sys = "0.3.72"
|
||||
kcl-lib = { path = "../kcl-lib" }
|
||||
kittycad = { workspace = true }
|
||||
kittycad-modeling-cmds = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
toml = "0.8.19"
|
||||
|
@ -106,4 +106,22 @@ impl Context {
|
||||
Err(err) => Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?),
|
||||
}
|
||||
}
|
||||
|
||||
/// Export a scene to a file.
|
||||
#[wasm_bindgen]
|
||||
pub async fn export(&self, format_json: &str, settings: &str) -> Result<JsValue, String> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let format: kittycad_modeling_cmds::format::OutputFormat3d =
|
||||
serde_json::from_str(format_json).map_err(|e| e.to_string())?;
|
||||
|
||||
let ctx = self.create_executor_ctx(settings, None, false)?;
|
||||
|
||||
match ctx.export(format).await {
|
||||
// The serde-wasm-bindgen does not work here because of weird HashMap issues.
|
||||
// DO NOT USE serde_wasm_bindgen::to_value it will break the frontend.
|
||||
Ok(outcome) => JsValue::from_serde(&outcome).map_err(|e| e.to_string()),
|
||||
Err(err) => Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,25 +18,6 @@ pub async fn kcl_lint(program_ast_json: &str) -> Result<JsValue, JsValue> {
|
||||
Ok(JsValue::from_serde(&findings).map_err(|e| e.to_string())?)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn deserialize_files(data: &[u8]) -> Result<JsValue, JsError> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let ws_resp: kittycad::types::WebSocketResponse = bson::from_slice(data)?;
|
||||
|
||||
if let Some(success) = ws_resp.success {
|
||||
if !success {
|
||||
return Err(JsError::new(&format!("Server returned error: {:?}", ws_resp.errors)));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(kittycad::types::OkWebSocketResponseData::Export { files }) = ws_resp.resp {
|
||||
return Ok(JsValue::from_serde(&files)?);
|
||||
}
|
||||
|
||||
Err(JsError::new(&format!("Invalid response type, got: {:?}", ws_resp)))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_wasm(kcl_program_source: &str) -> Result<JsValue, String> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
@ -27,12 +27,6 @@ export const ModelStateIndicator = () => {
|
||||
name="checkmark"
|
||||
/>
|
||||
)
|
||||
} else if (lastCommandType === 'export-done') {
|
||||
className +=
|
||||
'border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
|
||||
icon = (
|
||||
<CustomIcon data-testid={dataTestId + '-export-done'} name="checkmark" />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -8,7 +8,6 @@ import React, {
|
||||
} from 'react'
|
||||
import {
|
||||
Actor,
|
||||
AnyStateMachine,
|
||||
ContextFrom,
|
||||
Prop,
|
||||
SnapshotFrom,
|
||||
@ -33,8 +32,12 @@ import {
|
||||
codeManager,
|
||||
editorManager,
|
||||
sceneEntitiesManager,
|
||||
rustContext,
|
||||
} from 'lib/singletons'
|
||||
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||
import {
|
||||
MachineManager,
|
||||
MachineManagerContext,
|
||||
} from 'components/MachineManagerProvider'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
|
||||
import {
|
||||
@ -53,7 +56,10 @@ import {
|
||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||
import useStateMachineCommands from 'hooks/useStateMachineCommands'
|
||||
import { modelingMachineCommandConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
|
||||
import {
|
||||
ModelingCommandSchema,
|
||||
modelingMachineCommandConfig,
|
||||
} from 'lib/commandBarConfigs/modelingCommandConfig'
|
||||
import {
|
||||
SEGMENT_BODIES,
|
||||
getParentGroup,
|
||||
@ -84,21 +90,17 @@ import {
|
||||
isCursorInFunctionDefinition,
|
||||
traverse,
|
||||
} from 'lang/queryAst'
|
||||
import { exportFromEngine } from 'lib/exportFromEngine'
|
||||
import { Models } from '@kittycad/lib/dist/types/src'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||
import { err, reportRejection, trap, reject } from 'lib/trap'
|
||||
import {
|
||||
ExportIntent,
|
||||
EngineConnectionStateType,
|
||||
EngineConnectionEvents,
|
||||
} from 'lang/std/engineConnection'
|
||||
import { submitAndAwaitTextToKcl } from 'lib/textToCad'
|
||||
import { useFileContext } from 'hooks/useFileContext'
|
||||
import { platform, uuidv4 } from 'lib/utils'
|
||||
import { IndexLoaderData } from 'lib/types'
|
||||
import { Node } from '@rust/kcl-lib/bindings/Node'
|
||||
import {
|
||||
getFaceCodeRef,
|
||||
@ -111,15 +113,18 @@ import { commandBarActor } from 'machines/commandBarMachine'
|
||||
import { useToken } from 'machines/appMachine'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { useSettings } from 'machines/appMachine'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
context: ContextFrom<T>
|
||||
send: Prop<Actor<T>, 'send'>
|
||||
}
|
||||
import { IndexLoaderData } from 'lib/types'
|
||||
import { OutputFormat3d } from '@rust/kcl-lib/bindings/ModelingCmd'
|
||||
import { EXPORT_TOAST_MESSAGES, MAKE_TOAST_MESSAGES } from 'lib/constants'
|
||||
import { exportMake } from 'lib/exportMake'
|
||||
import { exportSave } from 'lib/exportSave'
|
||||
|
||||
export const ModelingMachineContext = createContext(
|
||||
{} as MachineContext<typeof modelingMachine>
|
||||
{} as {
|
||||
state: StateFrom<typeof modelingMachine>
|
||||
context: ContextFrom<typeof modelingMachine>
|
||||
send: Prop<Actor<typeof modelingMachine>, 'send'>
|
||||
}
|
||||
)
|
||||
|
||||
const commandBarIsClosedSelector = (
|
||||
@ -524,118 +529,6 @@ export const ModelingMachineProvider = ({
|
||||
return {}
|
||||
}
|
||||
),
|
||||
Make: ({ context, event }) => {
|
||||
if (event.type !== 'Make') return
|
||||
// Check if we already have an export intent.
|
||||
if (engineCommandManager.exportInfo) {
|
||||
toast.error('Already exporting')
|
||||
return
|
||||
}
|
||||
// Set the export intent.
|
||||
engineCommandManager.exportInfo = {
|
||||
intent: ExportIntent.Make,
|
||||
name: file?.name || '',
|
||||
}
|
||||
|
||||
// Set the current machine.
|
||||
// Due to our use of singeton pattern, we need to do this to reliably
|
||||
// update this object across React and non-React boundary.
|
||||
// We need to do this eagerly because of the exportToEngine call below.
|
||||
if (engineCommandManager.machineManager === null) {
|
||||
console.warn(
|
||||
"engineCommandManager.machineManager is null. It shouldn't be at this point. Aborting operation."
|
||||
)
|
||||
return
|
||||
} else {
|
||||
engineCommandManager.machineManager.currentMachine =
|
||||
event.data.machine
|
||||
}
|
||||
|
||||
// Update the rest of the UI that needs to know the current machine
|
||||
context.machineManager.setCurrentMachine(event.data.machine)
|
||||
|
||||
const format: Models['OutputFormat_type'] = {
|
||||
type: 'stl',
|
||||
coords: {
|
||||
forward: {
|
||||
axis: 'y',
|
||||
direction: 'negative',
|
||||
},
|
||||
up: {
|
||||
axis: 'z',
|
||||
direction: 'positive',
|
||||
},
|
||||
},
|
||||
storage: 'ascii',
|
||||
// Convert all units to mm since that is what the slicer expects.
|
||||
units: 'mm',
|
||||
selection: { type: 'default_scene' },
|
||||
}
|
||||
|
||||
exportFromEngine({
|
||||
format: format,
|
||||
}).catch(reportRejection)
|
||||
},
|
||||
'Engine export': ({ event }) => {
|
||||
if (event.type !== 'Export') return
|
||||
if (engineCommandManager.exportInfo) {
|
||||
toast.error('Already exporting')
|
||||
return
|
||||
}
|
||||
// Set the export intent.
|
||||
engineCommandManager.exportInfo = {
|
||||
intent: ExportIntent.Save,
|
||||
// This never gets used its only for make.
|
||||
name: file?.name?.replace('.kcl', `.${event.data.type}`) || '',
|
||||
}
|
||||
|
||||
const format = {
|
||||
...event.data,
|
||||
} as Partial<Models['OutputFormat_type']>
|
||||
|
||||
// Set all the un-configurable defaults here.
|
||||
if (format.type === 'gltf') {
|
||||
format.presentation = 'pretty'
|
||||
}
|
||||
|
||||
if (
|
||||
format.type === 'obj' ||
|
||||
format.type === 'ply' ||
|
||||
format.type === 'step' ||
|
||||
format.type === 'stl'
|
||||
) {
|
||||
// Set the default coords.
|
||||
// In the future we can make this configurable.
|
||||
// But for now, its probably best to keep it consistent with the
|
||||
// UI.
|
||||
format.coords = {
|
||||
forward: {
|
||||
axis: 'y',
|
||||
direction: 'negative',
|
||||
},
|
||||
up: {
|
||||
axis: 'z',
|
||||
direction: 'positive',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
format.type === 'obj' ||
|
||||
format.type === 'stl' ||
|
||||
format.type === 'ply'
|
||||
) {
|
||||
format.units = defaultUnit.current
|
||||
}
|
||||
|
||||
if (format.type === 'ply' || format.type === 'stl') {
|
||||
format.selection = { type: 'default_scene' }
|
||||
}
|
||||
|
||||
exportFromEngine({
|
||||
format: format as Models['OutputFormat_type'],
|
||||
}).catch(reportRejection)
|
||||
},
|
||||
'Submit to Text-to-CAD API': ({ event }) => {
|
||||
if (event.type !== 'Text-to-CAD') return
|
||||
const trimmedPrompt = event.data.prompt.trim()
|
||||
@ -696,14 +589,155 @@ export const ModelingMachineProvider = ({
|
||||
else if (kclManager.ast.body.length === 0)
|
||||
errorMessage += 'due to Empty Scene'
|
||||
console.error(errorMessage)
|
||||
toast.error(errorMessage, {
|
||||
id: kclManager.engineCommandManager.pendingExport?.toastId,
|
||||
})
|
||||
toast.error(errorMessage)
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
actors: {
|
||||
exportFromEngine: fromPromise(
|
||||
async ({ input }: { input?: ModelingCommandSchema['Export'] }) => {
|
||||
if (!input) {
|
||||
return new Error('No input provided')
|
||||
}
|
||||
|
||||
let fileName = file?.name?.replace('.kcl', `.${input.type}`) || ''
|
||||
console.log('fileName', fileName)
|
||||
// Ensure the file has an extension.
|
||||
if (!fileName.includes('.')) {
|
||||
fileName += `.${input.type}`
|
||||
}
|
||||
|
||||
const format = {
|
||||
...input,
|
||||
} as Partial<OutputFormat3d>
|
||||
|
||||
// Set all the un-configurable defaults here.
|
||||
if (format.type === 'gltf') {
|
||||
format.presentation = 'pretty'
|
||||
}
|
||||
|
||||
if (
|
||||
format.type === 'obj' ||
|
||||
format.type === 'ply' ||
|
||||
format.type === 'step' ||
|
||||
format.type === 'stl'
|
||||
) {
|
||||
// Set the default coords.
|
||||
// In the future we can make this configurable.
|
||||
// But for now, its probably best to keep it consistent with the
|
||||
// UI.
|
||||
format.coords = {
|
||||
forward: {
|
||||
axis: 'y',
|
||||
direction: 'negative',
|
||||
},
|
||||
up: {
|
||||
axis: 'z',
|
||||
direction: 'positive',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
format.type === 'obj' ||
|
||||
format.type === 'stl' ||
|
||||
format.type === 'ply'
|
||||
) {
|
||||
format.units = defaultUnit.current
|
||||
}
|
||||
|
||||
if (format.type === 'ply' || format.type === 'stl') {
|
||||
format.selection = { type: 'default_scene' }
|
||||
}
|
||||
|
||||
const toastId = toast.loading(EXPORT_TOAST_MESSAGES.START)
|
||||
const files = await rustContext.export(
|
||||
format,
|
||||
{
|
||||
settings: { modeling: { base_unit: defaultUnit.current } },
|
||||
},
|
||||
toastId
|
||||
)
|
||||
|
||||
if (files === undefined) {
|
||||
// We already sent the toast message in the export function.
|
||||
return
|
||||
}
|
||||
|
||||
await exportSave({ files, toastId, fileName })
|
||||
}
|
||||
),
|
||||
makeFromEngine: fromPromise(
|
||||
async ({
|
||||
input,
|
||||
}: {
|
||||
input?: {
|
||||
machineManager: MachineManager
|
||||
} & ModelingCommandSchema['Make']
|
||||
}) => {
|
||||
if (input === undefined) {
|
||||
return new Error('No input provided')
|
||||
}
|
||||
|
||||
const name = file?.name || ''
|
||||
|
||||
// Set the current machine.
|
||||
// Due to our use of singeton pattern, we need to do this to reliably
|
||||
// update this object across React and non-React boundary.
|
||||
// We need to do this eagerly because of the exportToEngine call below.
|
||||
if (engineCommandManager.machineManager === null) {
|
||||
console.warn(
|
||||
"engineCommandManager.machineManager is null. It shouldn't be at this point. Aborting operation."
|
||||
)
|
||||
return new Error('Machine manager is not set')
|
||||
} else {
|
||||
engineCommandManager.machineManager.currentMachine = input.machine
|
||||
}
|
||||
|
||||
// Update the rest of the UI that needs to know the current machine
|
||||
input.machineManager.setCurrentMachine(input.machine)
|
||||
|
||||
const format: OutputFormat3d = {
|
||||
type: 'stl',
|
||||
coords: {
|
||||
forward: {
|
||||
axis: 'y',
|
||||
direction: 'negative',
|
||||
},
|
||||
up: {
|
||||
axis: 'z',
|
||||
direction: 'positive',
|
||||
},
|
||||
},
|
||||
storage: 'ascii',
|
||||
// Convert all units to mm since that is what the slicer expects.
|
||||
units: 'mm',
|
||||
selection: { type: 'default_scene' },
|
||||
}
|
||||
|
||||
const toastId = toast.loading(MAKE_TOAST_MESSAGES.START)
|
||||
const files = await rustContext.export(
|
||||
format,
|
||||
{
|
||||
settings: { modeling: { base_unit: 'mm' } },
|
||||
},
|
||||
toastId
|
||||
)
|
||||
|
||||
if (files === undefined) {
|
||||
// We already sent the toast message in the export function.
|
||||
return
|
||||
}
|
||||
|
||||
await exportMake({
|
||||
files,
|
||||
toastId,
|
||||
name,
|
||||
machineManager: engineCommandManager.machineManager,
|
||||
})
|
||||
}
|
||||
),
|
||||
'AST-undo-startSketchOn': fromPromise(
|
||||
async ({ input: { sketchDetails } }) => {
|
||||
if (!sketchDetails) return
|
||||
|
@ -6,8 +6,8 @@ import {
|
||||
} from 'lang/wasm'
|
||||
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { exportSave } from 'lib/exportSave'
|
||||
import { deferExecution, isOverlap, uuidv4 } from 'lib/utils'
|
||||
import { BSON, Binary as BSONBinary } from 'bson'
|
||||
import {
|
||||
Themes,
|
||||
getThemeColorForEngine,
|
||||
@ -16,14 +16,8 @@ import {
|
||||
} from 'lib/theme'
|
||||
import { EngineCommand, ResponseMap } from 'lang/std/artifactGraph'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { exportMake } from 'lib/exportMake'
|
||||
import toast from 'react-hot-toast'
|
||||
import { SettingsViaQueryString } from 'lib/settings/settingsTypes'
|
||||
import {
|
||||
EXECUTE_AST_INTERRUPT_ERROR_MESSAGE,
|
||||
EXPORT_TOAST_MESSAGES,
|
||||
MAKE_TOAST_MESSAGES,
|
||||
} from 'lib/constants'
|
||||
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
|
||||
import { KclManager } from 'lang/KclSingleton'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { markOnce } from 'lib/performance'
|
||||
@ -47,16 +41,6 @@ interface NewTrackArgs {
|
||||
mediaStream: MediaStream
|
||||
}
|
||||
|
||||
export enum ExportIntent {
|
||||
Save = 'save',
|
||||
Make = 'make',
|
||||
}
|
||||
|
||||
export interface ExportInfo {
|
||||
intent: ExportIntent
|
||||
name: string
|
||||
}
|
||||
|
||||
type ClientMetrics = Models['ClientMetrics_type']
|
||||
|
||||
interface WebRTCClientMetrics extends ClientMetrics {
|
||||
@ -1069,18 +1053,6 @@ class EngineConnection extends EventTarget {
|
||||
`Error in response to request ${message.request_id}:\n${errorsString}
|
||||
failed cmd type was ${artifactThatFailed?.type}`
|
||||
)
|
||||
// Check if this was a pending export command.
|
||||
if (
|
||||
this.engineCommandManager.pendingExport?.commandId ===
|
||||
message.request_id
|
||||
) {
|
||||
// Reject the promise with the error.
|
||||
this.engineCommandManager.pendingExport.reject(errorsString)
|
||||
toast.error(errorsString, {
|
||||
id: this.engineCommandManager.pendingExport.toastId,
|
||||
})
|
||||
this.engineCommandManager.pendingExport = undefined
|
||||
}
|
||||
} else {
|
||||
console.error(`Error from server:\n${errorsString}`)
|
||||
}
|
||||
@ -1365,10 +1337,6 @@ export type CommandLog =
|
||||
type: 'execution-done'
|
||||
data: null
|
||||
}
|
||||
| {
|
||||
type: 'export-done'
|
||||
data: null
|
||||
}
|
||||
|
||||
export enum EngineCommandManagerEvents {
|
||||
// engineConnection is available but scene setup may not have run
|
||||
@ -1432,16 +1400,6 @@ export class EngineCommandManager extends EventTarget {
|
||||
inSequence = 1
|
||||
engineConnection?: EngineConnection
|
||||
commandLogs: CommandLog[] = []
|
||||
pendingExport?: {
|
||||
/** The id of the shared loading/success/error toast for export */
|
||||
toastId: string
|
||||
/** An on-success callback */
|
||||
resolve: (a: null) => void
|
||||
/** An on-error callback */
|
||||
reject: (reason: string) => void
|
||||
/** The engine command uuid */
|
||||
commandId: string
|
||||
}
|
||||
settings: SettingsViaQueryString
|
||||
|
||||
streamDimensions = {
|
||||
@ -1452,12 +1410,6 @@ export class EngineCommandManager extends EventTarget {
|
||||
|
||||
elVideo: HTMLVideoElement | null = null
|
||||
|
||||
/**
|
||||
* Export intent tracks 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 _exportInfo: ExportInfo | null = null
|
||||
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
|
||||
|
||||
subscriptions: {
|
||||
@ -1509,14 +1461,6 @@ export class EngineCommandManager extends EventTarget {
|
||||
// The current "manufacturing machine" aka 3D printer, CNC, etc.
|
||||
public machineManager: MachineManager | null = null
|
||||
|
||||
set exportInfo(info: ExportInfo | null) {
|
||||
this._exportInfo = info
|
||||
}
|
||||
|
||||
get exportInfo() {
|
||||
return this._exportInfo
|
||||
}
|
||||
|
||||
start({
|
||||
setMediaStream,
|
||||
setIsStreamReady,
|
||||
@ -1691,64 +1635,33 @@ export class EngineCommandManager extends EventTarget {
|
||||
engineConnection.websocket?.addEventListener('message', ((
|
||||
event: MessageEvent
|
||||
) => {
|
||||
let message: Models['WebSocketResponse_type'] | null = null
|
||||
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
// If the data is an ArrayBuffer, it's the result of an export command,
|
||||
// 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.
|
||||
if (this.exportInfo === null || this.pendingExport === undefined) {
|
||||
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
|
||||
// BSON deserialize the command.
|
||||
message = BSON.deserialize(
|
||||
new Uint8Array(event.data)
|
||||
) as Models['WebSocketResponse_type']
|
||||
// The request id comes back as binary and we want to get the uuid
|
||||
// string from that.
|
||||
if (message.request_id) {
|
||||
message.request_id = binaryToUuid(message.request_id)
|
||||
}
|
||||
} else {
|
||||
message = JSON.parse(event.data)
|
||||
}
|
||||
|
||||
switch (this.exportInfo.intent) {
|
||||
case ExportIntent.Save: {
|
||||
exportSave({
|
||||
data: event.data,
|
||||
fileName: this.exportInfo.name,
|
||||
toastId: this.pendingExport.toastId,
|
||||
}).then(() => {
|
||||
this.pendingExport?.resolve(null)
|
||||
}, this.pendingExport?.reject)
|
||||
break
|
||||
}
|
||||
case ExportIntent.Make: {
|
||||
if (!this.machineManager) {
|
||||
console.warn('Some how, no manufacturing machine is selected.')
|
||||
break
|
||||
}
|
||||
|
||||
exportMake(
|
||||
event.data,
|
||||
this.exportInfo.name,
|
||||
this.pendingExport.toastId,
|
||||
this.machineManager
|
||||
).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.exportInfo = null
|
||||
if (message === null) {
|
||||
// We should never get here.
|
||||
console.error('Received a null message from the engine', event)
|
||||
return
|
||||
}
|
||||
|
||||
const message: Models['WebSocketResponse_type'] = JSON.parse(event.data)
|
||||
const pending = this.pendingCommands[message.request_id || '']
|
||||
|
||||
if (pending && !message.success) {
|
||||
// handle bad case
|
||||
pending.reject(`engine error: ${JSON.stringify(message.errors)}`)
|
||||
pending.reject(JSON.stringify(message))
|
||||
delete this.pendingCommands[message.request_id || '']
|
||||
}
|
||||
if (
|
||||
@ -1756,12 +1669,15 @@ export class EngineCommandManager extends EventTarget {
|
||||
pending &&
|
||||
message.success &&
|
||||
(message.resp.type === 'modeling' ||
|
||||
message.resp.type === 'modeling_batch')
|
||||
message.resp.type === 'modeling_batch' ||
|
||||
message.resp.type === 'export')
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if (
|
||||
if (message.resp.type === 'export' && message.request_id) {
|
||||
this.responseMap[message.request_id] = message.resp
|
||||
} else if (
|
||||
message.resp.type === 'modeling' &&
|
||||
pending.command.type === 'modeling_cmd_req' &&
|
||||
message.request_id
|
||||
@ -2026,38 +1942,6 @@ export class EngineCommandManager extends EventTarget {
|
||||
this.outSequence++
|
||||
this.engineConnection?.unreliableSend(command)
|
||||
return Promise.resolve(null)
|
||||
} else if (cmd.type === 'export') {
|
||||
const promise = new Promise<null>((resolve, reject) => {
|
||||
if (this.exportInfo === null) {
|
||||
if (this.exportInfo === null) {
|
||||
toast.error('Export intent was not set, but export is being sent')
|
||||
console.error('Export intent was not set, but export is being sent')
|
||||
return
|
||||
}
|
||||
}
|
||||
const toastId = toast.loading(
|
||||
this.exportInfo.intent === ExportIntent.Save
|
||||
? EXPORT_TOAST_MESSAGES.START
|
||||
: MAKE_TOAST_MESSAGES.START
|
||||
)
|
||||
this.pendingExport = {
|
||||
toastId,
|
||||
resolve: (passThrough) => {
|
||||
this.addCommandLog({
|
||||
type: 'export-done',
|
||||
data: null,
|
||||
})
|
||||
resolve(passThrough)
|
||||
},
|
||||
reject: (reason: string) => {
|
||||
this.exportInfo = null
|
||||
reject(reason)
|
||||
},
|
||||
commandId: command.cmd_id,
|
||||
}
|
||||
})
|
||||
this.engineConnection?.send(command)
|
||||
return promise
|
||||
}
|
||||
if (
|
||||
command.cmd.type === 'default_camera_look_at' ||
|
||||
@ -2090,7 +1974,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
rangeStr: string,
|
||||
commandStr: string,
|
||||
idToRangeStr: string
|
||||
): Promise<string | void> {
|
||||
): Promise<Uint8Array | void> {
|
||||
if (this.engineConnection === undefined) return Promise.resolve()
|
||||
if (
|
||||
!this.engineConnection?.isReady() &&
|
||||
@ -2118,7 +2002,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
range,
|
||||
idToRangeMap,
|
||||
})
|
||||
return JSON.stringify(resp[0])
|
||||
return BSON.serialize(resp[0])
|
||||
}
|
||||
/**
|
||||
* Common send command function used for both modeling and scene commands
|
||||
@ -2266,3 +2150,65 @@ function promiseFactory<T>() {
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a binary buffer to a UUID string.
|
||||
*
|
||||
* @param buffer - The binary buffer containing the UUID bytes.
|
||||
* @returns A string representation of the UUID in the format 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'.
|
||||
*/
|
||||
function binaryToUuid(
|
||||
binaryData: Buffer | Uint8Array | BSONBinary | string
|
||||
): string {
|
||||
if (typeof binaryData === 'string') {
|
||||
return binaryData
|
||||
}
|
||||
|
||||
let buffer: Uint8Array
|
||||
|
||||
// Handle MongoDB BSON Binary object
|
||||
if (
|
||||
binaryData &&
|
||||
'_bsontype' in binaryData &&
|
||||
binaryData._bsontype === 'Binary'
|
||||
) {
|
||||
// Extract the buffer from the BSON Binary object
|
||||
buffer = binaryData.buffer
|
||||
}
|
||||
// Handle case where buffer property exists (some MongoDB drivers structure)
|
||||
else if (binaryData && binaryData.buffer instanceof Uint8Array) {
|
||||
buffer = binaryData.buffer
|
||||
}
|
||||
// Handle direct Buffer or Uint8Array
|
||||
else if (binaryData instanceof Uint8Array || Buffer.isBuffer(binaryData)) {
|
||||
buffer = binaryData
|
||||
} else {
|
||||
console.error(
|
||||
'Invalid input type: expected MongoDB BSON Binary, Buffer, or Uint8Array'
|
||||
)
|
||||
return ''
|
||||
}
|
||||
|
||||
// Ensure we have exactly 16 bytes (128 bits) for a UUID
|
||||
if (buffer.length !== 16) {
|
||||
// For debugging
|
||||
console.log('Buffer length:', buffer.length)
|
||||
console.log('Buffer content:', Array.from(buffer))
|
||||
console.error('UUID must be exactly 16 bytes')
|
||||
return ''
|
||||
}
|
||||
|
||||
// Convert each byte to a hex string and pad with zeros if needed
|
||||
const hexValues = Array.from(buffer).map((byte) =>
|
||||
byte.toString(16).padStart(2, '0')
|
||||
)
|
||||
|
||||
// Format into UUID structure (8-4-4-4-12 characters)
|
||||
return [
|
||||
hexValues.slice(0, 4).join(''),
|
||||
hexValues.slice(4, 6).join(''),
|
||||
hexValues.slice(6, 8).join(''),
|
||||
hexValues.slice(8, 10).join(''),
|
||||
hexValues.slice(10, 16).join(''),
|
||||
].join('-')
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { type Models } from '@kittycad/lib'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
|
||||
// Isolating a function to call the engine to export the current scene.
|
||||
// Because it has given us trouble in automated testing environments.
|
||||
export async function exportFromEngine({
|
||||
format,
|
||||
}: {
|
||||
format: Models['OutputFormat_type']
|
||||
}): Promise<Models['WebSocketResponse_type'] | null> {
|
||||
let exportPromise = engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'export',
|
||||
// By default let's leave this blank to export the whole scene.
|
||||
// In the future we might want to let the user choose which entities
|
||||
// in the scene to export. In that case, you'd pass the IDs thru here.
|
||||
entity_ids: [],
|
||||
format,
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
|
||||
return exportPromise
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import { deserialize_files } from '@rust/kcl-wasm-lib/pkg/kcl_wasm_lib'
|
||||
import { MachineManager } from 'components/MachineManagerProvider'
|
||||
import toast from 'react-hot-toast'
|
||||
import { components } from './machine-api'
|
||||
@ -6,12 +5,17 @@ import ModelingAppFile from './modelingAppFile'
|
||||
import { MAKE_TOAST_MESSAGES } from './constants'
|
||||
|
||||
// Make files locally from an export call.
|
||||
export async function exportMake(
|
||||
data: ArrayBuffer,
|
||||
name: string,
|
||||
toastId: string,
|
||||
export async function exportMake({
|
||||
files,
|
||||
name,
|
||||
toastId,
|
||||
machineManager,
|
||||
}: {
|
||||
files: ModelingAppFile[]
|
||||
name: string
|
||||
toastId: string
|
||||
machineManager: MachineManager
|
||||
): Promise<Response | null> {
|
||||
}): Promise<Response | null> {
|
||||
if (name === '') {
|
||||
console.error(MAKE_TOAST_MESSAGES.NO_NAME)
|
||||
toast.error(MAKE_TOAST_MESSAGES.NO_NAME, { id: toastId })
|
||||
@ -50,10 +54,8 @@ export async function exportMake(
|
||||
job_name: 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',
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { isDesktop } from './isDesktop'
|
||||
import { deserialize_files } from '@rust/kcl-wasm-lib/pkg/kcl_wasm_lib'
|
||||
import { browserSaveFile } from './browserSaveFile'
|
||||
|
||||
import JSZip from 'jszip'
|
||||
@ -78,19 +77,14 @@ const save_ = async (file: ModelingAppFile, toastId: string) => {
|
||||
// Saves files locally from an export call.
|
||||
// We override the file's name with one passed in from the client side.
|
||||
export async function exportSave({
|
||||
data,
|
||||
files,
|
||||
fileName,
|
||||
toastId,
|
||||
}: {
|
||||
data: ArrayBuffer
|
||||
files: ModelingAppFile[]
|
||||
fileName: string
|
||||
toastId: string
|
||||
}) {
|
||||
// This converts the ArrayBuffer to a Rust equivalent Vec<u8>.
|
||||
let uintArray = new Uint8Array(data)
|
||||
|
||||
let files: ModelingAppFile[] = deserialize_files(uintArray)
|
||||
|
||||
if (files.length > 1) {
|
||||
let zip = new JSZip()
|
||||
for (const file of files) {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import {
|
||||
emptyExecState,
|
||||
errFromErrWithOutputs,
|
||||
ExecState,
|
||||
execStateFromRust,
|
||||
@ -17,6 +16,10 @@ import { DefaultPlanes } from '@rust/kcl-lib/bindings/DefaultPlanes'
|
||||
import { DefaultPlaneStr, defaultPlaneStrToKey } from 'lib/planes'
|
||||
import { err } from 'lib/trap'
|
||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import { OutputFormat3d } from '@rust/kcl-lib/bindings/ModelingCmd'
|
||||
import ModelingAppFile from './modelingAppFile'
|
||||
import toast from 'react-hot-toast'
|
||||
import { KclError as RustKclError } from '@rust/kcl-lib/bindings/KclError'
|
||||
|
||||
export default class RustContext {
|
||||
private wasmInitFailed: boolean = true
|
||||
@ -43,20 +46,22 @@ export default class RustContext {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.ensureWasmInit().then(async () => {
|
||||
await this.create()
|
||||
this.ctxInstance = await this.create()
|
||||
})
|
||||
}
|
||||
|
||||
// Create a new context instance
|
||||
async create() {
|
||||
async create(): Promise<Context> {
|
||||
this.rustInstance = getModule()
|
||||
// We need this await here, DO NOT REMOVE it even if your editor says it's
|
||||
// unnecessary. The constructor of the module is async and it will not
|
||||
// resolve if you don't await it.
|
||||
this.ctxInstance = await new this.rustInstance.Context(
|
||||
const ctxInstance = await new this.rustInstance.Context(
|
||||
this.engineCommandManager,
|
||||
fileSystemManager
|
||||
)
|
||||
|
||||
return ctxInstance
|
||||
}
|
||||
|
||||
// Execute a program.
|
||||
@ -65,31 +70,26 @@ export default class RustContext {
|
||||
settings: DeepPartial<Configuration>,
|
||||
path?: string
|
||||
): Promise<ExecState> {
|
||||
await this._checkInstance()
|
||||
const instance = await this._checkInstance()
|
||||
|
||||
if (this.ctxInstance) {
|
||||
try {
|
||||
const result = await this.ctxInstance.execute(
|
||||
JSON.stringify(node),
|
||||
path,
|
||||
JSON.stringify(settings)
|
||||
)
|
||||
/* Set the default planes, safe to call after execute. */
|
||||
const outcome = execStateFromRust(result, node)
|
||||
try {
|
||||
const result = await instance.execute(
|
||||
JSON.stringify(node),
|
||||
path,
|
||||
JSON.stringify(settings)
|
||||
)
|
||||
/* Set the default planes, safe to call after execute. */
|
||||
const outcome = execStateFromRust(result, node)
|
||||
|
||||
this._defaultPlanes = outcome.defaultPlanes
|
||||
this._defaultPlanes = outcome.defaultPlanes
|
||||
|
||||
// Return the result.
|
||||
return outcome
|
||||
} catch (e: any) {
|
||||
const err = errFromErrWithOutputs(e)
|
||||
this._defaultPlanes = err.defaultPlanes
|
||||
return Promise.reject(err)
|
||||
}
|
||||
// Return the result.
|
||||
return outcome
|
||||
} catch (e: any) {
|
||||
const err = errFromErrWithOutputs(e)
|
||||
this._defaultPlanes = err.defaultPlanes
|
||||
return Promise.reject(err)
|
||||
}
|
||||
|
||||
// You will never get here.
|
||||
return Promise.reject(emptyExecState())
|
||||
}
|
||||
|
||||
// Execute a program with in mock mode.
|
||||
@ -99,28 +99,43 @@ export default class RustContext {
|
||||
path?: string,
|
||||
usePrevMemory?: boolean
|
||||
): Promise<ExecState> {
|
||||
await this._checkInstance()
|
||||
const instance = await this._checkInstance()
|
||||
|
||||
if (this.ctxInstance) {
|
||||
try {
|
||||
if (usePrevMemory === undefined) {
|
||||
usePrevMemory = true
|
||||
}
|
||||
|
||||
const result = await this.ctxInstance.executeMock(
|
||||
JSON.stringify(node),
|
||||
path,
|
||||
JSON.stringify(settings),
|
||||
usePrevMemory
|
||||
)
|
||||
return mockExecStateFromRust(result)
|
||||
} catch (e: any) {
|
||||
return Promise.reject(errFromErrWithOutputs(e))
|
||||
}
|
||||
if (usePrevMemory === undefined) {
|
||||
usePrevMemory = true
|
||||
}
|
||||
|
||||
// You will never get here.
|
||||
return Promise.reject(emptyExecState())
|
||||
try {
|
||||
const result = await instance.executeMock(
|
||||
JSON.stringify(node),
|
||||
path,
|
||||
JSON.stringify(settings),
|
||||
usePrevMemory
|
||||
)
|
||||
return mockExecStateFromRust(result)
|
||||
} catch (e: any) {
|
||||
return Promise.reject(errFromErrWithOutputs(e))
|
||||
}
|
||||
}
|
||||
|
||||
// Export a scene to a file.
|
||||
async export(
|
||||
format: DeepPartial<OutputFormat3d>,
|
||||
settings: DeepPartial<Configuration>,
|
||||
toastId: string
|
||||
): Promise<ModelingAppFile[] | undefined> {
|
||||
const instance = await this._checkInstance()
|
||||
|
||||
try {
|
||||
return await instance.export(
|
||||
JSON.stringify(format),
|
||||
JSON.stringify(settings)
|
||||
)
|
||||
} catch (e: any) {
|
||||
const parsed: RustKclError = JSON.parse(e.toString())
|
||||
toast.error(parsed.msg, { id: toastId })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
async waitForAllEngineCommands() {
|
||||
@ -169,11 +184,13 @@ export default class RustContext {
|
||||
}
|
||||
|
||||
// Helper to check if context instance exists
|
||||
private async _checkInstance() {
|
||||
private async _checkInstance(): Promise<Context> {
|
||||
if (!this.ctxInstance) {
|
||||
// Create the context instance.
|
||||
await this.create()
|
||||
this.ctxInstance = await this.create()
|
||||
}
|
||||
|
||||
return this.ctxInstance
|
||||
}
|
||||
|
||||
// Clean up resources
|
||||
|
@ -1283,14 +1283,12 @@ export const modelingMachine = setup({
|
||||
},
|
||||
}
|
||||
}),
|
||||
Make: () => {},
|
||||
'enable copilot': () => {},
|
||||
'disable copilot': () => {},
|
||||
'Set selection': () => {},
|
||||
'Set mouse state': () => {},
|
||||
'Set Segment Overlays': () => {},
|
||||
'Center camera on selection': () => {},
|
||||
'Engine export': () => {},
|
||||
'Submit to Text-to-CAD API': () => {},
|
||||
'Set sketchDetails': () => {},
|
||||
'sketch exit execute': () => {},
|
||||
@ -2469,6 +2467,20 @@ export const modelingMachine = setup({
|
||||
}
|
||||
}
|
||||
),
|
||||
exportFromEngine: fromPromise(
|
||||
async ({}: { input?: ModelingCommandSchema['Export'] }) => {
|
||||
return undefined as Error | undefined
|
||||
}
|
||||
),
|
||||
makeFromEngine: fromPromise(
|
||||
async ({}: {
|
||||
input?: {
|
||||
machineManager: MachineManager
|
||||
} & ModelingCommandSchema['Make']
|
||||
}) => {
|
||||
return undefined as Error | undefined
|
||||
}
|
||||
),
|
||||
},
|
||||
// end actors
|
||||
}).createMachine({
|
||||
@ -2528,17 +2540,13 @@ export const modelingMachine = setup({
|
||||
},
|
||||
|
||||
Export: {
|
||||
target: 'idle',
|
||||
reenter: false,
|
||||
target: 'Exporting',
|
||||
guard: 'Has exportable geometry',
|
||||
actions: 'Engine export',
|
||||
},
|
||||
|
||||
Make: {
|
||||
target: 'idle',
|
||||
reenter: false,
|
||||
target: 'Making',
|
||||
guard: 'Has exportable geometry',
|
||||
actions: 'Make',
|
||||
},
|
||||
|
||||
'Delete selection': {
|
||||
@ -3885,6 +3893,35 @@ export const modelingMachine = setup({
|
||||
onError: ['idle'],
|
||||
},
|
||||
},
|
||||
|
||||
Exporting: {
|
||||
invoke: {
|
||||
src: 'exportFromEngine',
|
||||
id: 'exportFromEngine',
|
||||
input: ({ event }) => {
|
||||
if (event.type !== 'Export') return undefined
|
||||
return event.data
|
||||
},
|
||||
onDone: ['idle'],
|
||||
onError: ['idle'],
|
||||
},
|
||||
},
|
||||
|
||||
Making: {
|
||||
invoke: {
|
||||
src: 'makeFromEngine',
|
||||
id: 'makeFromEngine',
|
||||
input: ({ event, context }) => {
|
||||
if (event.type !== 'Make' || !context.machineManager) return undefined
|
||||
return {
|
||||
machineManager: context.machineManager,
|
||||
...event.data,
|
||||
}
|
||||
},
|
||||
onDone: ['idle'],
|
||||
onError: ['idle'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
initial: 'idle',
|
||||
|
@ -3,6 +3,7 @@ import viteTsconfigPaths from 'vite-tsconfig-paths'
|
||||
import eslint from '@nabla/vite-plugin-eslint'
|
||||
import { defineConfig, configDefaults } from 'vitest/config'
|
||||
import version from 'vite-plugin-package-version'
|
||||
import topLevelAwait from 'vite-plugin-top-level-await'
|
||||
// @ts-ignore: No types available
|
||||
import { lezer } from '@lezer/generator/rollup'
|
||||
|
||||
@ -62,7 +63,19 @@ const config = defineConfig({
|
||||
'@rust': '/rust',
|
||||
},
|
||||
},
|
||||
plugins: [react(), viteTsconfigPaths(), eslint(), version(), lezer()],
|
||||
plugins: [
|
||||
react(),
|
||||
viteTsconfigPaths(),
|
||||
eslint(),
|
||||
version(),
|
||||
lezer(),
|
||||
topLevelAwait({
|
||||
// The export name of top-level await promise for each chunk module
|
||||
promiseExportName: '__tla',
|
||||
// The function to generate import names of top-level await promise in each chunk module
|
||||
promiseImportName: (i) => `__tla_${i}`,
|
||||
}),
|
||||
],
|
||||
worker: {
|
||||
plugins: () => [viteTsconfigPaths()],
|
||||
},
|
||||
|
@ -2,6 +2,7 @@ import type { ConfigEnv, UserConfig } from 'vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import { pluginExposeRenderer } from './vite.base.config'
|
||||
import viteTsconfigPaths from 'vite-tsconfig-paths'
|
||||
import topLevelAwait from 'vite-plugin-top-level-await'
|
||||
// @ts-ignore: No types available
|
||||
import { lezer } from '@lezer/generator/rollup'
|
||||
|
||||
@ -18,7 +19,17 @@ export default defineConfig((env) => {
|
||||
build: {
|
||||
outDir: `.vite/renderer/${name}`,
|
||||
},
|
||||
plugins: [pluginExposeRenderer(name), viteTsconfigPaths(), lezer()],
|
||||
plugins: [
|
||||
pluginExposeRenderer(name),
|
||||
viteTsconfigPaths(),
|
||||
lezer(),
|
||||
topLevelAwait({
|
||||
// The export name of top-level await promise for each chunk module
|
||||
promiseExportName: '__tla',
|
||||
// The function to generate import names of top-level await promise in each chunk module
|
||||
promiseImportName: (i) => `__tla_${i}`,
|
||||
}),
|
||||
],
|
||||
worker: {
|
||||
plugins: () => [viteTsconfigPaths()],
|
||||
},
|
||||
|
105
yarn.lock
105
yarn.lock
@ -1995,6 +1995,11 @@
|
||||
dependencies:
|
||||
"@codemirror/state" "^6.2.1"
|
||||
|
||||
"@rollup/plugin-virtual@^3.0.2":
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz#17e17eeecb4c9fa1c0a6e72c9e5f66382fddbb82"
|
||||
integrity sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.29.1":
|
||||
version "4.29.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz#9bd38df6a29afb7f0336d988bc8112af0c8816c0"
|
||||
@ -2105,6 +2110,87 @@
|
||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f"
|
||||
integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==
|
||||
|
||||
"@swc/core-darwin-arm64@1.11.11":
|
||||
version "1.11.11"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.11.tgz#e4b5fc99bab657f8f72217fd4976956faf4132b3"
|
||||
integrity sha512-vJcjGVDB8cZH7zyOkC0AfpFYI/7GHKG0NSsH3tpuKrmoAXJyCYspKPGid7FT53EAlWreN7+Pew+bukYf5j+Fmg==
|
||||
|
||||
"@swc/core-darwin-x64@1.11.11":
|
||||
version "1.11.11"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.11.11.tgz#0f4e810a2cd9c2993a7ccc3b38d1f92ef49894d8"
|
||||
integrity sha512-/N4dGdqEYvD48mCF3QBSycAbbQd3yoZ2YHSzYesQf8usNc2YpIhYqEH3sql02UsxTjEFOJSf1bxZABDdhbSl6A==
|
||||
|
||||
"@swc/core-linux-arm-gnueabihf@1.11.11":
|
||||
version "1.11.11"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.11.tgz#72b4b1e403bca37f051fd194eb0518cda83fad9f"
|
||||
integrity sha512-hsBhKK+wVXdN3x9MrL5GW0yT8o9GxteE5zHAI2HJjRQel3HtW7m5Nvwaq+q8rwMf4YQRd8ydbvwl4iUOZx7i2Q==
|
||||
|
||||
"@swc/core-linux-arm64-gnu@1.11.11":
|
||||
version "1.11.11"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.11.tgz#ea87e183ec53db9e121cca581cef538e9652193f"
|
||||
integrity sha512-YOCdxsqbnn/HMPCNM6nrXUpSndLXMUssGTtzT7ffXqr7WuzRg2e170FVDVQFIkb08E7Ku5uOnnUVAChAJQbMOQ==
|
||||
|
||||
"@swc/core-linux-arm64-musl@1.11.11":
|
||||
version "1.11.11"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.11.tgz#33db0f45b2286bbca9baf2ed84d1f2405c657600"
|
||||
integrity sha512-nR2tfdQRRzwqR2XYw9NnBk9Fdvff/b8IiJzDL28gRR2QiJWLaE8LsRovtWrzCOYq6o5Uu9cJ3WbabWthLo4jLw==
|
||||
|
||||
"@swc/core-linux-x64-gnu@1.11.11":
|
||||
version "1.11.11"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.11.tgz#4a1fe41baa968008bb0fffc7754fd6ee824e76e1"
|
||||
integrity sha512-b4gBp5HA9xNWNC5gsYbdzGBJWx4vKSGybGMGOVWWuF+ynx10+0sA/o4XJGuNHm8TEDuNh9YLKf6QkIO8+GPJ1g==
|
||||
|
||||
"@swc/core-linux-x64-musl@1.11.11":
|
||||
version "1.11.11"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.11.tgz#972d3530d740b3681191590ee08bb9ab7bb6706d"
|
||||
integrity sha512-dEvqmQVswjNvMBwXNb8q5uSvhWrJLdttBSef3s6UC5oDSwOr00t3RQPzyS3n5qmGJ8UMTdPRmsopxmqaODISdg==
|
||||
|
||||
"@swc/core-win32-arm64-msvc@1.11.11":
|
||||
version "1.11.11"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.11.tgz#179846f1f9e3e806a4bf6d8f35af97f577c1a0b3"
|
||||
integrity sha512-aZNZznem9WRnw2FbTqVpnclvl8Q2apOBW2B316gZK+qxbe+ktjOUnYaMhdCG3+BYggyIBDOnaJeQrXbKIMmNdw==
|
||||
|
||||
"@swc/core-win32-ia32-msvc@1.11.11":
|
||||
version "1.11.11"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.11.tgz#b098b72c1b45e237a9598b7b5e83e6c5ecb9ac69"
|
||||
integrity sha512-DjeJn/IfjgOddmJ8IBbWuDK53Fqw7UvOz7kyI/728CSdDYC3LXigzj3ZYs4VvyeOt+ZcQZUB2HA27edOifomGw==
|
||||
|
||||
"@swc/core-win32-x64-msvc@1.11.11":
|
||||
version "1.11.11"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.11.tgz#1d5610c585b903b8c1f4a452725d77ac96f27e84"
|
||||
integrity sha512-Gp/SLoeMtsU4n0uRoKDOlGrRC6wCfifq7bqLwSlAG8u8MyJYJCcwjg7ggm0rhLdC2vbiZ+lLVl3kkETp+JUvKg==
|
||||
|
||||
"@swc/core@^1.10.16":
|
||||
version "1.11.11"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.11.11.tgz#bac3256d7a113f0dd6965206cf428e826981cf0d"
|
||||
integrity sha512-pCVY2Wn6dV/labNvssk9b3Owi4WOYsapcbWm90XkIj4xH/56Z6gzja9fsU+4MdPuEfC2Smw835nZHcdCFGyX6A==
|
||||
dependencies:
|
||||
"@swc/counter" "^0.1.3"
|
||||
"@swc/types" "^0.1.19"
|
||||
optionalDependencies:
|
||||
"@swc/core-darwin-arm64" "1.11.11"
|
||||
"@swc/core-darwin-x64" "1.11.11"
|
||||
"@swc/core-linux-arm-gnueabihf" "1.11.11"
|
||||
"@swc/core-linux-arm64-gnu" "1.11.11"
|
||||
"@swc/core-linux-arm64-musl" "1.11.11"
|
||||
"@swc/core-linux-x64-gnu" "1.11.11"
|
||||
"@swc/core-linux-x64-musl" "1.11.11"
|
||||
"@swc/core-win32-arm64-msvc" "1.11.11"
|
||||
"@swc/core-win32-ia32-msvc" "1.11.11"
|
||||
"@swc/core-win32-x64-msvc" "1.11.11"
|
||||
|
||||
"@swc/counter@^0.1.3":
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
|
||||
integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==
|
||||
|
||||
"@swc/types@^0.1.19":
|
||||
version "0.1.19"
|
||||
resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.19.tgz#65d9fe81e0a1dc7e861ad698dd581abe3703a2d2"
|
||||
integrity sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==
|
||||
dependencies:
|
||||
"@swc/counter" "^0.1.3"
|
||||
|
||||
"@szmarczak/http-timer@^4.0.5":
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807"
|
||||
@ -3284,6 +3370,11 @@ browserslist@^4.24.0, browserslist@^4.24.4:
|
||||
node-releases "^2.0.19"
|
||||
update-browserslist-db "^1.1.1"
|
||||
|
||||
bson@^6.10.3:
|
||||
version "6.10.3"
|
||||
resolved "https://registry.yarnpkg.com/bson/-/bson-6.10.3.tgz#5f9a463af6b83e264bedd08b236d1356a30eda47"
|
||||
integrity sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==
|
||||
|
||||
buffer-crc32@~0.2.3:
|
||||
version "0.2.13"
|
||||
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
|
||||
@ -9249,6 +9340,11 @@ utrie@^1.0.2:
|
||||
dependencies:
|
||||
base64-arraybuffer "^1.0.2"
|
||||
|
||||
uuid@^10.0.0:
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
|
||||
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
|
||||
|
||||
uuid@^11.1.0:
|
||||
version "11.1.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912"
|
||||
@ -9297,6 +9393,15 @@ vite-plugin-package-version@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/vite-plugin-package-version/-/vite-plugin-package-version-1.1.0.tgz#7d8088955aa21e4ec93353c98992b3f58c4bf13c"
|
||||
integrity sha512-TPoFZXNanzcaKCIrC3e2L/TVRkkRLB6l4RPN/S7KbG7rWfyLcCEGsnXvxn6qR7fyZwXalnnSN/I9d6pSFjHpEA==
|
||||
|
||||
vite-plugin-top-level-await@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.5.0.tgz#e3f76302921152bf29d1658f169d168f8937e78b"
|
||||
integrity sha512-r/DtuvHrSqUVk23XpG2cl8gjt1aATMG5cjExXL1BUTcSNab6CzkcPua9BPEc9fuTP5UpwClCxUe3+dNGL0yrgQ==
|
||||
dependencies:
|
||||
"@rollup/plugin-virtual" "^3.0.2"
|
||||
"@swc/core" "^1.10.16"
|
||||
uuid "^10.0.0"
|
||||
|
||||
vite-tsconfig-paths@^4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz#321f02e4b736a90ff62f9086467faf4e2da857a9"
|
||||
|
Reference in New Issue
Block a user