Implement Core Dump for modeling app state (#2653)
This commit is contained in:
@ -5489,3 +5489,72 @@ test('Paste should not work unless an input is focused', async ({
|
|||||||
)
|
)
|
||||||
).toContain(pasteContent)
|
).toContain(pasteContent)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Core dump from keyboard commands success', async ({ page }) => {
|
||||||
|
// This test can run long if it takes a little too long to load
|
||||||
|
// the engine, plus coredump takes bit to process.
|
||||||
|
test.setTimeout(150000)
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.addInitScript(async () => {
|
||||||
|
;(window as any).playwrightSkipFilePicker = true
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`const topAng = 25
|
||||||
|
const bottomAng = 35
|
||||||
|
const baseLen = 3.5
|
||||||
|
const baseHeight = 1
|
||||||
|
const totalHeightHalf = 2
|
||||||
|
const armThick = 0.5
|
||||||
|
const totalLen = 9.5
|
||||||
|
const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([0, 0], %)
|
||||||
|
|> yLine(baseHeight, %)
|
||||||
|
|> xLine(baseLen, %)
|
||||||
|
|> angledLineToY({
|
||||||
|
angle: topAng,
|
||||||
|
to: totalHeightHalf,
|
||||||
|
}, %, 'seg04')
|
||||||
|
|> xLineTo(totalLen, %, 'seg03')
|
||||||
|
|> yLine(-armThick, %, 'seg01')
|
||||||
|
|> angledLineThatIntersects({
|
||||||
|
angle: HALF_TURN,
|
||||||
|
offset: -armThick,
|
||||||
|
intersectTag: 'seg04'
|
||||||
|
}, %)
|
||||||
|
|> angledLineToY([segAng('seg04', %) + 180, ZERO], %)
|
||||||
|
|> angledLineToY({
|
||||||
|
angle: -bottomAng,
|
||||||
|
to: -totalHeightHalf - armThick,
|
||||||
|
}, %, 'seg02')
|
||||||
|
|> xLineTo(segEndX('seg03', %) + 0, %)
|
||||||
|
|> yLine(-segLen('seg01', %), %)
|
||||||
|
|> angledLineThatIntersects({
|
||||||
|
angle: HALF_TURN,
|
||||||
|
offset: -armThick,
|
||||||
|
intersectTag: 'seg02'
|
||||||
|
}, %)
|
||||||
|
|> angledLineToY([segAng('seg02', %) + 180, -baseHeight], %)
|
||||||
|
|> xLineTo(ZERO, %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(4, %)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
|
||||||
|
// Start waiting for popup before clicking. Note no await.
|
||||||
|
const popupPromise = page.waitForEvent('popup')
|
||||||
|
await page.keyboard.press('Meta+Shift+.')
|
||||||
|
// after invoking coredump, a loading toast will appear
|
||||||
|
await expect(page.getByText('Starting core dump')).toBeVisible()
|
||||||
|
// Allow time for core dump processing
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
await expect(page.getByText('Core dump completed successfully')).toBeVisible()
|
||||||
|
const popup = await popupPromise
|
||||||
|
console.log(await popup.title())
|
||||||
|
// GitHub popup will go to unlogged in page. Can't look for "New Issue" here.
|
||||||
|
await expect(popup).toHaveTitle(/GitHub /)
|
||||||
|
})
|
||||||
|
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@ -6181,6 +6181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357"
|
checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"ts-rs-macros",
|
"ts-rs-macros",
|
||||||
"url",
|
"url",
|
||||||
|
@ -121,7 +121,24 @@ export const ModelingMachineProvider = ({
|
|||||||
htmlRef,
|
htmlRef,
|
||||||
token
|
token
|
||||||
)
|
)
|
||||||
useHotkeyWrapper(['meta + shift + .'], () => coreDump(coreDumpManager, true))
|
useHotkeyWrapper(['meta + shift + .'], () => {
|
||||||
|
console.warn('CoreDump: Initializing core dump')
|
||||||
|
toast.promise(
|
||||||
|
coreDump(coreDumpManager, true),
|
||||||
|
{
|
||||||
|
loading: 'Starting core dump...',
|
||||||
|
success: 'Core dump completed successfully',
|
||||||
|
error: 'Error while exporting core dump',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
success: {
|
||||||
|
// Note: this extended duration is especially important for Playwright e2e testing
|
||||||
|
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
|
||||||
|
duration: 6000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
// Settings machine setup
|
// Settings machine setup
|
||||||
// const retrievedSettings = useRef(
|
// const retrievedSettings = useRef(
|
||||||
|
@ -25,7 +25,7 @@ import type { Program } from '../wasm-lib/kcl/bindings/Program'
|
|||||||
import type { Token } from '../wasm-lib/kcl/bindings/Token'
|
import type { Token } from '../wasm-lib/kcl/bindings/Token'
|
||||||
import { Coords2d } from './std/sketch'
|
import { Coords2d } from './std/sketch'
|
||||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||||
import { AppInfo } from 'wasm-lib/kcl/bindings/AppInfo'
|
import { CoreDumpInfo } from 'wasm-lib/kcl/bindings/CoreDumpInfo'
|
||||||
import { CoreDumpManager } from 'lib/coredump'
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
import openWindow from 'lib/openWindow'
|
import openWindow from 'lib/openWindow'
|
||||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||||
@ -335,14 +335,27 @@ export function programMemoryInit(): ProgramMemory {
|
|||||||
export async function coreDump(
|
export async function coreDump(
|
||||||
coreDumpManager: CoreDumpManager,
|
coreDumpManager: CoreDumpManager,
|
||||||
openGithubIssue: boolean = false
|
openGithubIssue: boolean = false
|
||||||
): Promise<AppInfo> {
|
): Promise<CoreDumpInfo> {
|
||||||
try {
|
try {
|
||||||
const dump: AppInfo = await coredump(coreDumpManager)
|
const dump: CoreDumpInfo = await coredump(coreDumpManager)
|
||||||
|
/* NOTE: this console output of the coredump should include the field
|
||||||
|
`github_issue_url` which is not in the uploaded coredump file.
|
||||||
|
`github_issue_url` is added after the file is uploaded
|
||||||
|
and is only needed for the openWindow operation which creates
|
||||||
|
a new GitHub issue for the user.
|
||||||
|
*/
|
||||||
if (openGithubIssue && dump.github_issue_url) {
|
if (openGithubIssue && dump.github_issue_url) {
|
||||||
openWindow(dump.github_issue_url)
|
openWindow(dump.github_issue_url)
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
'github_issue_url undefined. Unable to create GitHub issue for coredump.'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
console.log('CoreDump: final coredump', dump)
|
||||||
|
console.log('CoreDump: final coredump JSON', JSON.stringify(dump))
|
||||||
return dump
|
return dump
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
console.error('CoreDump: error', e)
|
||||||
throw new Error(`Error getting core dump: ${e}`)
|
throw new Error(`Error getting core dump: ${e}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,15 @@ import screenshot from 'lib/screenshot'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { VITE_KC_API_BASE_URL } from 'env'
|
import { VITE_KC_API_BASE_URL } from 'env'
|
||||||
|
|
||||||
// This is a class for getting all the values from the JS world to pass to the Rust world
|
/**
|
||||||
// for a core dump.
|
* CoreDumpManager module
|
||||||
|
* - for getting all the values from the JS world to pass to the Rust world for a core dump.
|
||||||
|
* @module lib/coredump
|
||||||
|
* @class
|
||||||
|
*/
|
||||||
|
// CoreDumpManager is instantiated in ModelingMachineProvider and passed to coreDump() in wasm.ts
|
||||||
|
// The async function coreDump() handles any errors thrown in its Promise catch method and rethrows
|
||||||
|
// them to so the toast handler in ModelingMachineProvider can show the user an error message toast
|
||||||
export class CoreDumpManager {
|
export class CoreDumpManager {
|
||||||
engineCommandManager: EngineCommandManager
|
engineCommandManager: EngineCommandManager
|
||||||
htmlRef: React.RefObject<HTMLDivElement> | null
|
htmlRef: React.RefObject<HTMLDivElement> | null
|
||||||
@ -144,6 +151,293 @@ export class CoreDumpManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Currently just a placeholder to begin loading singleton and xstate data into
|
||||||
|
getClientState(): Promise<string> {
|
||||||
|
/**
|
||||||
|
* Deep clone a JavaScript Object
|
||||||
|
* - NOTE: this function throws on parse errors from things like circular references
|
||||||
|
* - It is also synchronous and could be more performant
|
||||||
|
* - There is a whole rabbit hole to explore here if you like.
|
||||||
|
* - This works for our use case.
|
||||||
|
* @param {object} obj - The object to clone.
|
||||||
|
*/
|
||||||
|
const deepClone = (obj: any) => JSON.parse(JSON.stringify(obj))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a function is private method
|
||||||
|
*/
|
||||||
|
const isPrivateMethod = (key: string) => {
|
||||||
|
return key.length && key[0] === '_'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turn off verbose logging by default
|
||||||
|
const verboseLogging = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle verbose debug logging of step-by-step client state coredump data
|
||||||
|
*/
|
||||||
|
const debugLog = verboseLogging ? console.log : () => {}
|
||||||
|
|
||||||
|
console.warn('CoreDump: Gathering client state')
|
||||||
|
|
||||||
|
// Initialize the clientState object
|
||||||
|
let clientState = {
|
||||||
|
// singletons
|
||||||
|
engine_command_manager: {
|
||||||
|
artifact_map: {},
|
||||||
|
command_logs: [],
|
||||||
|
engine_connection: { state: { type: '' } },
|
||||||
|
default_planes: {},
|
||||||
|
scene_command_artifacts: {},
|
||||||
|
},
|
||||||
|
kcl_manager: {
|
||||||
|
ast: {},
|
||||||
|
kcl_errors: [],
|
||||||
|
},
|
||||||
|
scene_infra: {},
|
||||||
|
scene_entities_manager: {},
|
||||||
|
editor_manager: {},
|
||||||
|
// xstate
|
||||||
|
auth_machine: {},
|
||||||
|
command_bar_machine: {},
|
||||||
|
file_machine: {},
|
||||||
|
home_machine: {},
|
||||||
|
modeling_machine: {},
|
||||||
|
settings_machine: {},
|
||||||
|
}
|
||||||
|
debugLog('CoreDump: initialized clientState', clientState)
|
||||||
|
debugLog('CoreDump: globalThis.window', globalThis.window)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Singletons
|
||||||
|
|
||||||
|
// engine_command_manager
|
||||||
|
debugLog('CoreDump: engineCommandManager', this.engineCommandManager)
|
||||||
|
|
||||||
|
// artifact map - this.engineCommandManager.artifactMap
|
||||||
|
if (this.engineCommandManager?.artifactMap) {
|
||||||
|
debugLog(
|
||||||
|
'CoreDump: Engine Command Manager artifact map',
|
||||||
|
this.engineCommandManager.artifactMap
|
||||||
|
)
|
||||||
|
clientState.engine_command_manager.artifact_map = deepClone(
|
||||||
|
this.engineCommandManager.artifactMap
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// command logs - this.engineCommandManager.commandLogs
|
||||||
|
if (this.engineCommandManager?.commandLogs) {
|
||||||
|
debugLog(
|
||||||
|
'CoreDump: Engine Command Manager command logs',
|
||||||
|
this.engineCommandManager.commandLogs
|
||||||
|
)
|
||||||
|
clientState.engine_command_manager.command_logs = deepClone(
|
||||||
|
this.engineCommandManager.commandLogs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// default planes - this.engineCommandManager.defaultPlanes
|
||||||
|
if (this.engineCommandManager?.defaultPlanes) {
|
||||||
|
debugLog(
|
||||||
|
'CoreDump: Engine Command Manager default planes',
|
||||||
|
this.engineCommandManager.defaultPlanes
|
||||||
|
)
|
||||||
|
clientState.engine_command_manager.default_planes = deepClone(
|
||||||
|
this.engineCommandManager.defaultPlanes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// engine connection state
|
||||||
|
if (this.engineCommandManager?.engineConnection?.state) {
|
||||||
|
debugLog(
|
||||||
|
'CoreDump: Engine Command Manager engine connection state',
|
||||||
|
this.engineCommandManager.engineConnection.state
|
||||||
|
)
|
||||||
|
clientState.engine_command_manager.engine_connection.state =
|
||||||
|
this.engineCommandManager.engineConnection.state
|
||||||
|
}
|
||||||
|
|
||||||
|
// in sequence - this.engineCommandManager.inSequence
|
||||||
|
if (this.engineCommandManager?.inSequence) {
|
||||||
|
debugLog(
|
||||||
|
'CoreDump: Engine Command Manager in sequence',
|
||||||
|
this.engineCommandManager.inSequence
|
||||||
|
)
|
||||||
|
;(clientState.engine_command_manager as any).in_sequence =
|
||||||
|
this.engineCommandManager.inSequence
|
||||||
|
}
|
||||||
|
|
||||||
|
// out sequence - this.engineCommandManager.outSequence
|
||||||
|
if (this.engineCommandManager?.outSequence) {
|
||||||
|
debugLog(
|
||||||
|
'CoreDump: Engine Command Manager out sequence',
|
||||||
|
this.engineCommandManager.outSequence
|
||||||
|
)
|
||||||
|
;(clientState.engine_command_manager as any).out_sequence =
|
||||||
|
this.engineCommandManager.outSequence
|
||||||
|
}
|
||||||
|
|
||||||
|
// scene command artifacts - this.engineCommandManager.sceneCommandArtifacts
|
||||||
|
if (this.engineCommandManager?.sceneCommandArtifacts) {
|
||||||
|
debugLog(
|
||||||
|
'CoreDump: Engine Command Manager scene command artifacts',
|
||||||
|
this.engineCommandManager.sceneCommandArtifacts
|
||||||
|
)
|
||||||
|
clientState.engine_command_manager.scene_command_artifacts = deepClone(
|
||||||
|
this.engineCommandManager.sceneCommandArtifacts
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KCL Manager - globalThis?.window?.kclManager
|
||||||
|
const kclManager = (globalThis?.window as any)?.kclManager
|
||||||
|
debugLog('CoreDump: kclManager', kclManager)
|
||||||
|
|
||||||
|
if (kclManager) {
|
||||||
|
// KCL Manager AST
|
||||||
|
debugLog('CoreDump: KCL Manager AST', kclManager?.ast)
|
||||||
|
if (kclManager?.ast) {
|
||||||
|
clientState.kcl_manager.ast = deepClone(kclManager.ast)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KCL Errors
|
||||||
|
debugLog('CoreDump: KCL Errors', kclManager?.kclErrors)
|
||||||
|
if (kclManager?.kclErrors) {
|
||||||
|
clientState.kcl_manager.kcl_errors = deepClone(kclManager.kclErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KCL isExecuting
|
||||||
|
debugLog('CoreDump: KCL isExecuting', kclManager?.isExecuting)
|
||||||
|
if (kclManager?.isExecuting) {
|
||||||
|
;(clientState.kcl_manager as any).isExecuting = kclManager.isExecuting
|
||||||
|
}
|
||||||
|
|
||||||
|
// KCL logs
|
||||||
|
debugLog('CoreDump: KCL logs', kclManager?.logs)
|
||||||
|
if (kclManager?.logs) {
|
||||||
|
;(clientState.kcl_manager as any).logs = deepClone(kclManager.logs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KCL programMemory
|
||||||
|
debugLog('CoreDump: KCL programMemory', kclManager?.programMemory)
|
||||||
|
if (kclManager?.programMemory) {
|
||||||
|
;(clientState.kcl_manager as any).programMemory = deepClone(
|
||||||
|
kclManager.programMemory
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KCL wasmInitFailed
|
||||||
|
debugLog('CoreDump: KCL wasmInitFailed', kclManager?.wasmInitFailed)
|
||||||
|
if (kclManager?.wasmInitFailed) {
|
||||||
|
;(clientState.kcl_manager as any).wasmInitFailed =
|
||||||
|
kclManager.wasmInitFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scene Infra - globalThis?.window?.sceneInfra
|
||||||
|
const sceneInfra = (globalThis?.window as any)?.sceneInfra
|
||||||
|
debugLog('CoreDump: Scene Infra', sceneInfra)
|
||||||
|
|
||||||
|
if (sceneInfra) {
|
||||||
|
const sceneInfraSkipKeys = ['camControls']
|
||||||
|
const sceneInfraKeys = Object.keys(sceneInfra)
|
||||||
|
.sort()
|
||||||
|
.filter((entry) => {
|
||||||
|
return (
|
||||||
|
typeof sceneInfra[entry] !== 'function' &&
|
||||||
|
!sceneInfraSkipKeys.includes(entry)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
debugLog('CoreDump: Scene Infra keys', sceneInfraKeys)
|
||||||
|
sceneInfraKeys.forEach((key: string) => {
|
||||||
|
debugLog('CoreDump: Scene Infra', key, sceneInfra[key])
|
||||||
|
try {
|
||||||
|
;(clientState.scene_infra as any)[key] = sceneInfra[key]
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'CoreDump: unable to parse Scene Infra ' + key + ' data due to ',
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scene Entities Manager - globalThis?.window?.sceneEntitiesManager
|
||||||
|
const sceneEntitiesManager = (globalThis?.window as any)
|
||||||
|
?.sceneEntitiesManager
|
||||||
|
debugLog('CoreDump: sceneEntitiesManager', sceneEntitiesManager)
|
||||||
|
|
||||||
|
if (sceneEntitiesManager) {
|
||||||
|
// Scene Entities Manager active segments
|
||||||
|
debugLog(
|
||||||
|
'CoreDump: Scene Entities Manager active segments',
|
||||||
|
sceneEntitiesManager?.activeSegments
|
||||||
|
)
|
||||||
|
if (sceneEntitiesManager?.activeSegments) {
|
||||||
|
;(clientState.scene_entities_manager as any).activeSegments =
|
||||||
|
deepClone(sceneEntitiesManager.activeSegments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Editor Manager - globalThis?.window?.editorManager
|
||||||
|
const editorManager = (globalThis?.window as any)?.editorManager
|
||||||
|
debugLog('CoreDump: editorManager', editorManager)
|
||||||
|
|
||||||
|
if (editorManager) {
|
||||||
|
const editorManagerSkipKeys = ['camControls']
|
||||||
|
const editorManagerKeys = Object.keys(editorManager)
|
||||||
|
.sort()
|
||||||
|
.filter((entry) => {
|
||||||
|
return (
|
||||||
|
typeof editorManager[entry] !== 'function' &&
|
||||||
|
!isPrivateMethod(entry) &&
|
||||||
|
!editorManagerSkipKeys.includes(entry)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
debugLog('CoreDump: Editor Manager keys', editorManagerKeys)
|
||||||
|
editorManagerKeys.forEach((key: string) => {
|
||||||
|
debugLog('CoreDump: Editor Manager', key, editorManager[key])
|
||||||
|
try {
|
||||||
|
;(clientState.editor_manager as any)[key] = deepClone(
|
||||||
|
editorManager[key]
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'CoreDump: unable to parse Editor Manager ' +
|
||||||
|
key +
|
||||||
|
' data due to ',
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// enableMousePositionLogs - Not coredumped
|
||||||
|
// See https://github.com/KittyCAD/modeling-app/issues/2338#issuecomment-2136441998
|
||||||
|
debugLog(
|
||||||
|
'CoreDump: enableMousePositionLogs [not coredumped]',
|
||||||
|
(globalThis?.window as any)?.enableMousePositionLogs
|
||||||
|
)
|
||||||
|
|
||||||
|
// XState Machines
|
||||||
|
debugLog(
|
||||||
|
'CoreDump: xstate services',
|
||||||
|
(globalThis?.window as any)?.__xstate__?.services
|
||||||
|
)
|
||||||
|
|
||||||
|
debugLog('CoreDump: final clientState', clientState)
|
||||||
|
|
||||||
|
const clientStateJson = JSON.stringify(clientState)
|
||||||
|
debugLog('CoreDump: final clientState JSON', clientStateJson)
|
||||||
|
|
||||||
|
return Promise.resolve(clientStateJson)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('CoreDump: unable to return data due to ', error)
|
||||||
|
return Promise.reject(JSON.stringify(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Return a data URL (png format) of the screenshot of the current page.
|
// Return a data URL (png format) of the screenshot of the current page.
|
||||||
screenshot(): Promise<string> {
|
screenshot(): Promise<string> {
|
||||||
return screenshot(this.htmlRef)
|
return screenshot(this.htmlRef)
|
||||||
|
1
src/wasm-lib/Cargo.lock
generated
1
src/wasm-lib/Cargo.lock
generated
@ -3272,6 +3272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357"
|
checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"ts-rs-macros",
|
"ts-rs-macros",
|
||||||
"url",
|
"url",
|
||||||
|
@ -37,7 +37,7 @@ serde_json = "1.0.116"
|
|||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
thiserror = "1.0.61"
|
thiserror = "1.0.61"
|
||||||
toml = "0.8.14"
|
toml = "0.8.14"
|
||||||
ts-rs = { version = "9.0.0", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings"] }
|
ts-rs = { version = "9.0.0", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] }
|
||||||
url = { version = "2.5.2", features = ["serde"] }
|
url = { version = "2.5.2", features = ["serde"] }
|
||||||
uuid = { version = "1.8.0", features = ["v4", "js", "serde"] }
|
uuid = { version = "1.8.0", features = ["v4", "js", "serde"] }
|
||||||
validator = { version = "0.18.1", features = ["derive"] }
|
validator = { version = "0.18.1", features = ["derive"] }
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::coredump::CoreDump;
|
use crate::coredump::CoreDump;
|
||||||
|
use serde_json::Value as JValue;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CoreDumper {}
|
pub struct CoreDumper {}
|
||||||
@ -55,6 +56,10 @@ impl CoreDump for CoreDumper {
|
|||||||
Ok(crate::coredump::WebrtcStats::default())
|
Ok(crate::coredump::WebrtcStats::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_client_state(&self) -> Result<JValue> {
|
||||||
|
Ok(JValue::default())
|
||||||
|
}
|
||||||
|
|
||||||
async fn screenshot(&self) -> Result<String> {
|
async fn screenshot(&self) -> Result<String> {
|
||||||
// Take a screenshot of the engine.
|
// Take a screenshot of the engine.
|
||||||
todo!()
|
todo!()
|
||||||
|
@ -7,8 +7,13 @@ pub mod wasm;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
|
use kittycad::Client;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
/// "Value" would be OK. This is imported as "JValue" throughout the rest of this crate.
|
||||||
|
use serde_json::Value as JValue;
|
||||||
|
use std::path::Path;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
pub trait CoreDump: Clone {
|
pub trait CoreDump: Clone {
|
||||||
@ -27,25 +32,24 @@ pub trait CoreDump: Clone {
|
|||||||
|
|
||||||
async fn get_webrtc_stats(&self) -> Result<WebrtcStats>;
|
async fn get_webrtc_stats(&self) -> Result<WebrtcStats>;
|
||||||
|
|
||||||
|
async fn get_client_state(&self) -> Result<JValue>;
|
||||||
|
|
||||||
/// Return a screenshot of the app.
|
/// Return a screenshot of the app.
|
||||||
async fn screenshot(&self) -> Result<String>;
|
async fn screenshot(&self) -> Result<String>;
|
||||||
|
|
||||||
/// Get a screenshot of the app and upload it to public cloud storage.
|
/// Get a screenshot of the app and upload it to public cloud storage.
|
||||||
async fn upload_screenshot(&self) -> Result<String> {
|
async fn upload_screenshot(&self, coredump_id: &Uuid, zoo_client: &Client) -> Result<String> {
|
||||||
let screenshot = self.screenshot().await?;
|
let screenshot = self.screenshot().await?;
|
||||||
let cleaned = screenshot.trim_start_matches("data:image/png;base64,");
|
let cleaned = screenshot.trim_start_matches("data:image/png;base64,");
|
||||||
// Create the zoo client.
|
|
||||||
let mut zoo = kittycad::Client::new(self.token()?);
|
|
||||||
zoo.set_base_url(&self.base_api_url()?);
|
|
||||||
|
|
||||||
// Base64 decode the screenshot.
|
// Base64 decode the screenshot.
|
||||||
let data = base64::engine::general_purpose::STANDARD.decode(cleaned)?;
|
let data = base64::engine::general_purpose::STANDARD.decode(cleaned)?;
|
||||||
// Upload the screenshot.
|
// Upload the screenshot.
|
||||||
let links = zoo
|
let links = zoo_client
|
||||||
.meta()
|
.meta()
|
||||||
.create_debug_uploads(vec![kittycad::types::multipart::Attachment {
|
.create_debug_uploads(vec![kittycad::types::multipart::Attachment {
|
||||||
name: "".to_string(),
|
name: "".to_string(),
|
||||||
filename: Some("modeling-app/core-dump-screenshot.png".to_string()),
|
filename: Some(format!(r#"modeling-app/coredump-{coredump_id}-screenshot.png"#)),
|
||||||
content_type: Some("image/png".to_string()),
|
content_type: Some("image/png".to_string()),
|
||||||
data,
|
data,
|
||||||
}])
|
}])
|
||||||
@ -60,12 +64,19 @@ pub trait CoreDump: Clone {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Dump the app info.
|
/// Dump the app info.
|
||||||
async fn dump(&self) -> Result<AppInfo> {
|
async fn dump(&self) -> Result<CoreDumpInfo> {
|
||||||
|
// Create the zoo client.
|
||||||
|
let mut zoo_client = kittycad::Client::new(self.token()?);
|
||||||
|
zoo_client.set_base_url(&self.base_api_url()?);
|
||||||
|
|
||||||
|
let coredump_id = uuid::Uuid::new_v4();
|
||||||
|
let client_state = self.get_client_state().await?;
|
||||||
let webrtc_stats = self.get_webrtc_stats().await?;
|
let webrtc_stats = self.get_webrtc_stats().await?;
|
||||||
let os = self.os().await?;
|
let os = self.os().await?;
|
||||||
let screenshot_url = self.upload_screenshot().await?;
|
let screenshot_url = self.upload_screenshot(&coredump_id, &zoo_client).await?;
|
||||||
|
|
||||||
let mut app_info = AppInfo {
|
let mut core_dump_info = CoreDumpInfo {
|
||||||
|
id: coredump_id,
|
||||||
version: self.version()?,
|
version: self.version()?,
|
||||||
git_rev: git_rev::try_revision_string!().map_or_else(|| "unknown".to_string(), |s| s.to_string()),
|
git_rev: git_rev::try_revision_string!().map_or_else(|| "unknown".to_string(), |s| s.to_string()),
|
||||||
timestamp: chrono::Utc::now(),
|
timestamp: chrono::Utc::now(),
|
||||||
@ -74,18 +85,44 @@ pub trait CoreDump: Clone {
|
|||||||
webrtc_stats,
|
webrtc_stats,
|
||||||
github_issue_url: None,
|
github_issue_url: None,
|
||||||
pool: self.pool()?,
|
pool: self.pool()?,
|
||||||
|
client_state,
|
||||||
};
|
};
|
||||||
app_info.set_github_issue_url(&screenshot_url)?;
|
|
||||||
|
|
||||||
Ok(app_info)
|
// pretty-printed JSON byte vector of the coredump.
|
||||||
|
let data = serde_json::to_vec_pretty(&core_dump_info)?;
|
||||||
|
|
||||||
|
// Upload the coredump.
|
||||||
|
let links = zoo_client
|
||||||
|
.meta()
|
||||||
|
.create_debug_uploads(vec![kittycad::types::multipart::Attachment {
|
||||||
|
name: "".to_string(),
|
||||||
|
filename: Some(format!(r#"modeling-app/coredump-{}.json"#, coredump_id)),
|
||||||
|
content_type: Some("application/json".to_string()),
|
||||||
|
data,
|
||||||
|
}])
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
||||||
|
|
||||||
|
if links.is_empty() {
|
||||||
|
anyhow::bail!("Failed to upload coredump");
|
||||||
|
}
|
||||||
|
|
||||||
|
let coredump_url = &links[0];
|
||||||
|
|
||||||
|
core_dump_info.set_github_issue_url(&screenshot_url, coredump_url, &coredump_id)?;
|
||||||
|
|
||||||
|
Ok(core_dump_info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The app info structure.
|
/// The app info structure.
|
||||||
|
/// The Core Dump Info structure.
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub struct AppInfo {
|
pub struct CoreDumpInfo {
|
||||||
|
/// The unique id for the core dump - this helps correlate uploaded files with coredump data.
|
||||||
|
pub id: Uuid,
|
||||||
/// The version of the app.
|
/// The version of the app.
|
||||||
pub version: String,
|
pub version: String,
|
||||||
/// The git revision of the app.
|
/// The git revision of the app.
|
||||||
@ -95,45 +132,44 @@ pub struct AppInfo {
|
|||||||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||||
/// If the app is running in tauri or the browser.
|
/// If the app is running in tauri or the browser.
|
||||||
pub tauri: bool,
|
pub tauri: bool,
|
||||||
|
|
||||||
/// The os info.
|
/// The os info.
|
||||||
pub os: OsInfo,
|
pub os: OsInfo,
|
||||||
|
|
||||||
/// The webrtc stats.
|
/// The webrtc stats.
|
||||||
pub webrtc_stats: WebrtcStats,
|
pub webrtc_stats: WebrtcStats,
|
||||||
|
|
||||||
/// A GitHub issue url to report the core dump.
|
/// A GitHub issue url to report the core dump.
|
||||||
/// This gets prepoulated with all the core dump info.
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub github_issue_url: Option<String>,
|
pub github_issue_url: Option<String>,
|
||||||
|
|
||||||
/// Engine pool the client is connected to.
|
/// Engine pool the client is connected to.
|
||||||
pub pool: String,
|
pub pool: String,
|
||||||
|
/// The client state (singletons and xstate).
|
||||||
|
pub client_state: JValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppInfo {
|
impl CoreDumpInfo {
|
||||||
/// Set the github issue url.
|
/// Set the github issue url.
|
||||||
pub fn set_github_issue_url(&mut self, screenshot_url: &str) -> Result<()> {
|
pub fn set_github_issue_url(&mut self, screenshot_url: &str, coredump_url: &str, coredump_id: &Uuid) -> Result<()> {
|
||||||
|
let coredump_filename = Path::new(coredump_url).file_name().unwrap().to_str().unwrap();
|
||||||
let tauri_or_browser_label = if self.tauri { "tauri" } else { "browser" };
|
let tauri_or_browser_label = if self.tauri { "tauri" } else { "browser" };
|
||||||
let labels = ["coredump", "bug", tauri_or_browser_label];
|
let labels = ["coredump", "bug", tauri_or_browser_label];
|
||||||
let body = format!(
|
let body = format!(
|
||||||
r#"[Insert a description of the issue here]
|
r#"[Add a title above and insert a description of the issue here]
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Core Dump</b></summary>
|
<summary><b>Core Dump</b></summary>
|
||||||
|
|
||||||
```json
|
[{coredump_filename}]({coredump_url})
|
||||||
{}
|
|
||||||
```
|
Reference ID: {coredump_id}
|
||||||
</details>
|
</details>
|
||||||
"#,
|
"#
|
||||||
screenshot_url,
|
|
||||||
serde_json::to_string_pretty(&self)?
|
|
||||||
);
|
);
|
||||||
let urlencoded: String = form_urlencoded::byte_serialize(body.as_bytes()).collect();
|
let urlencoded: String = form_urlencoded::byte_serialize(body.as_bytes()).collect();
|
||||||
|
|
||||||
|
// Note that `github_issue_url` is not included in the coredump file.
|
||||||
|
// It has already been encoded and uploaded at this point.
|
||||||
|
// The `github_issue_url` is used in openWindow in wasm.ts.
|
||||||
self.github_issue_url = Some(format!(
|
self.github_issue_url = Some(format!(
|
||||||
r#"https://github.com/{}/{}/issues/new?body={}&labels={}"#,
|
r#"https://github.com/{}/{}/issues/new?body={}&labels={}"#,
|
||||||
"KittyCAD",
|
"KittyCAD",
|
||||||
|
@ -4,6 +4,7 @@ use anyhow::Result;
|
|||||||
use wasm_bindgen::prelude::wasm_bindgen;
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
|
|
||||||
use crate::{coredump::CoreDump, wasm::JsFuture};
|
use crate::{coredump::CoreDump, wasm::JsFuture};
|
||||||
|
use serde_json::Value as JValue;
|
||||||
|
|
||||||
#[wasm_bindgen(module = "/../../lib/coredump.ts")]
|
#[wasm_bindgen(module = "/../../lib/coredump.ts")]
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@ -31,6 +32,9 @@ extern "C" {
|
|||||||
#[wasm_bindgen(method, js_name = getWebrtcStats, catch)]
|
#[wasm_bindgen(method, js_name = getWebrtcStats, catch)]
|
||||||
fn get_webrtc_stats(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>;
|
fn get_webrtc_stats(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = getClientState, catch)]
|
||||||
|
fn get_client_state(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>;
|
||||||
|
|
||||||
#[wasm_bindgen(method, js_name = screenshot, catch)]
|
#[wasm_bindgen(method, js_name = screenshot, catch)]
|
||||||
fn screenshot(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>;
|
fn screenshot(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>;
|
||||||
}
|
}
|
||||||
@ -123,6 +127,27 @@ impl CoreDump for CoreDumper {
|
|||||||
Ok(stats)
|
Ok(stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_client_state(&self) -> Result<JValue> {
|
||||||
|
let promise = self
|
||||||
|
.manager
|
||||||
|
.get_client_state()
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to get promise from get client state: {:?}", e))?;
|
||||||
|
|
||||||
|
let value = JsFuture::from(promise)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to get response from client state: {:?}", e))?;
|
||||||
|
|
||||||
|
// Parse the value as a string.
|
||||||
|
let s = value
|
||||||
|
.as_string()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Failed to get string from response from client stat: `{:?}`", value))?;
|
||||||
|
|
||||||
|
let client_state: JValue =
|
||||||
|
serde_json::from_str(&s).map_err(|e| anyhow::anyhow!("Failed to parse client state: {:?}", e))?;
|
||||||
|
|
||||||
|
Ok(client_state)
|
||||||
|
}
|
||||||
|
|
||||||
async fn screenshot(&self) -> Result<String> {
|
async fn screenshot(&self) -> Result<String> {
|
||||||
let promise = self
|
let promise = self
|
||||||
.manager
|
.manager
|
||||||
|
316
src/wasm-lib/tests/cordump/inputs/coredump.fixture.json
Normal file
316
src/wasm-lib/tests/cordump/inputs/coredump.fixture.json
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
{
|
||||||
|
"version": "0.20.1",
|
||||||
|
"git_rev": "3a05211d306ca045ace2e7bf10b7f8138e1daad5",
|
||||||
|
"timestamp": "2024-05-07T20:06:34.655Z",
|
||||||
|
"tauri": false,
|
||||||
|
"os": {
|
||||||
|
"platform": "Mac OS",
|
||||||
|
"version": "10.15.7",
|
||||||
|
"browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
||||||
|
},
|
||||||
|
"webrtc_stats": {
|
||||||
|
"packets_lost": 0,
|
||||||
|
"frames_received": 672,
|
||||||
|
"frame_width": 1440.0,
|
||||||
|
"frame_height": 712.0,
|
||||||
|
"frame_rate": 58.0,
|
||||||
|
"key_frames_decoded": 7,
|
||||||
|
"frames_dropped": 77,
|
||||||
|
"pause_count": 0,
|
||||||
|
"total_pauses_duration": 0.0,
|
||||||
|
"freeze_count": 12,
|
||||||
|
"total_freezes_duration": 3.057,
|
||||||
|
"pli_count": 6,
|
||||||
|
"jitter": 0.011
|
||||||
|
},
|
||||||
|
"pool": "",
|
||||||
|
"client_state": {
|
||||||
|
"engine_command_manager": {
|
||||||
|
"artifact_map": {
|
||||||
|
"ac7a8c52-7437-42e6-ae2a-25c54d0b4a16": {
|
||||||
|
"type": "result",
|
||||||
|
"range": [0, 0],
|
||||||
|
"pathToNode": [],
|
||||||
|
"commandType": "make_plane",
|
||||||
|
"data": { "type": "empty" },
|
||||||
|
"raw": {
|
||||||
|
"success": true,
|
||||||
|
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
|
||||||
|
"resp": {
|
||||||
|
"type": "modeling",
|
||||||
|
"data": { "modeling_response": { "type": "empty" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"83a2e866-d8e1-47d9-afae-d55a64cd5b40": {
|
||||||
|
"type": "result",
|
||||||
|
"range": [0, 0],
|
||||||
|
"pathToNode": [],
|
||||||
|
"commandType": "plane_set_color",
|
||||||
|
"data": { "type": "empty" },
|
||||||
|
"raw": {
|
||||||
|
"success": true,
|
||||||
|
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
|
||||||
|
"resp": {
|
||||||
|
"type": "modeling",
|
||||||
|
"data": { "modeling_response": { "type": "empty" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"c92d7e84-a03e-456e-aad0-3e302ae0ffb6": {
|
||||||
|
"type": "result",
|
||||||
|
"range": [0, 0],
|
||||||
|
"pathToNode": [],
|
||||||
|
"commandType": "make_plane",
|
||||||
|
"data": { "type": "empty" },
|
||||||
|
"raw": {
|
||||||
|
"success": true,
|
||||||
|
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
|
||||||
|
"resp": {
|
||||||
|
"type": "modeling",
|
||||||
|
"data": { "modeling_response": { "type": "empty" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"a9abb9b8-54b1-4042-8ab3-e67c7ddb4cb3": {
|
||||||
|
"type": "result",
|
||||||
|
"range": [0, 0],
|
||||||
|
"pathToNode": [],
|
||||||
|
"commandType": "plane_set_color",
|
||||||
|
"data": { "type": "empty" },
|
||||||
|
"raw": {
|
||||||
|
"success": true,
|
||||||
|
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
|
||||||
|
"resp": {
|
||||||
|
"type": "modeling",
|
||||||
|
"data": { "modeling_response": { "type": "empty" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bafb884d-ee93-48f5-a667-91d1bdb8178a": {
|
||||||
|
"type": "result",
|
||||||
|
"range": [0, 0],
|
||||||
|
"pathToNode": [],
|
||||||
|
"commandType": "make_plane",
|
||||||
|
"data": { "type": "empty" },
|
||||||
|
"raw": {
|
||||||
|
"success": true,
|
||||||
|
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
|
||||||
|
"resp": {
|
||||||
|
"type": "modeling",
|
||||||
|
"data": { "modeling_response": { "type": "empty" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"6b7f539b-c4ca-4e62-a971-147e1abaca7b": {
|
||||||
|
"type": "result",
|
||||||
|
"range": [0, 0],
|
||||||
|
"pathToNode": [],
|
||||||
|
"commandType": "plane_set_color",
|
||||||
|
"data": { "type": "empty" },
|
||||||
|
"raw": {
|
||||||
|
"success": true,
|
||||||
|
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
|
||||||
|
"resp": {
|
||||||
|
"type": "modeling",
|
||||||
|
"data": { "modeling_response": { "type": "empty" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"6ddaaaa3-b080-4cb3-b08e-8fe277312ccc": {
|
||||||
|
"type": "result",
|
||||||
|
"range": [0, 0],
|
||||||
|
"pathToNode": [],
|
||||||
|
"commandType": "make_plane",
|
||||||
|
"data": { "type": "empty" },
|
||||||
|
"raw": {
|
||||||
|
"success": true,
|
||||||
|
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
|
||||||
|
"resp": {
|
||||||
|
"type": "modeling",
|
||||||
|
"data": { "modeling_response": { "type": "empty" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"f1ef28b0-49b3-45f8-852d-772648149785": {
|
||||||
|
"type": "result",
|
||||||
|
"range": [0, 0],
|
||||||
|
"pathToNode": [],
|
||||||
|
"commandType": "make_plane",
|
||||||
|
"data": { "type": "empty" },
|
||||||
|
"raw": {
|
||||||
|
"success": true,
|
||||||
|
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
|
||||||
|
"resp": {
|
||||||
|
"type": "modeling",
|
||||||
|
"data": { "modeling_response": { "type": "empty" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"7c532be8-f07d-456b-8643-53808df86823": {
|
||||||
|
"type": "result",
|
||||||
|
"range": [0, 0],
|
||||||
|
"pathToNode": [],
|
||||||
|
"commandType": "make_plane",
|
||||||
|
"data": { "type": "empty" },
|
||||||
|
"raw": {
|
||||||
|
"success": true,
|
||||||
|
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
|
||||||
|
"resp": {
|
||||||
|
"type": "modeling",
|
||||||
|
"data": { "modeling_response": { "type": "empty" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cf38f826-6d15-45f4-9c64-a6e9e4909e75": {
|
||||||
|
"type": "result",
|
||||||
|
"range": [0, 0],
|
||||||
|
"pathToNode": [],
|
||||||
|
"commandType": "make_plane",
|
||||||
|
"data": { "type": "empty" },
|
||||||
|
"raw": {
|
||||||
|
"success": true,
|
||||||
|
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
|
||||||
|
"resp": {
|
||||||
|
"type": "modeling",
|
||||||
|
"data": { "modeling_response": { "type": "empty" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"d69e6248-9e0f-4bc8-bc6e-d995931e8886": {
|
||||||
|
"type": "result",
|
||||||
|
"range": [0, 0],
|
||||||
|
"pathToNode": [],
|
||||||
|
"commandType": "plane_set_color",
|
||||||
|
"data": { "type": "empty" },
|
||||||
|
"raw": {
|
||||||
|
"success": true,
|
||||||
|
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
|
||||||
|
"resp": {
|
||||||
|
"type": "modeling",
|
||||||
|
"data": { "modeling_response": { "type": "empty" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cdb44075-ac6d-4626-b321-26d022f5f7f0": {
|
||||||
|
"type": "result",
|
||||||
|
"range": [0, 0],
|
||||||
|
"pathToNode": [],
|
||||||
|
"commandType": "make_plane",
|
||||||
|
"data": { "type": "empty" },
|
||||||
|
"raw": {
|
||||||
|
"success": true,
|
||||||
|
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
|
||||||
|
"resp": {
|
||||||
|
"type": "modeling",
|
||||||
|
"data": { "modeling_response": { "type": "empty" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"44f81391-0f2d-49f3-92b9-7dfc8446d571": {
|
||||||
|
"type": "result",
|
||||||
|
"range": [0, 0],
|
||||||
|
"pathToNode": [],
|
||||||
|
"commandType": "plane_set_color",
|
||||||
|
"data": { "type": "empty" },
|
||||||
|
"raw": {
|
||||||
|
"success": true,
|
||||||
|
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
|
||||||
|
"resp": {
|
||||||
|
"type": "modeling",
|
||||||
|
"data": { "modeling_response": { "type": "empty" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"7b893ecf-8887-49c3-b978-392813d5f625": {
|
||||||
|
"type": "result",
|
||||||
|
"range": [0, 0],
|
||||||
|
"pathToNode": [],
|
||||||
|
"commandType": "make_plane",
|
||||||
|
"data": { "type": "empty" },
|
||||||
|
"raw": {
|
||||||
|
"success": true,
|
||||||
|
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
|
||||||
|
"resp": {
|
||||||
|
"type": "modeling",
|
||||||
|
"data": { "modeling_response": { "type": "empty" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"61247d28-04ba-4768-85b0-fbc582151dbd": {
|
||||||
|
"type": "result",
|
||||||
|
"range": [0, 0],
|
||||||
|
"pathToNode": [],
|
||||||
|
"commandType": "plane_set_color",
|
||||||
|
"data": { "type": "empty" },
|
||||||
|
"raw": {
|
||||||
|
"success": true,
|
||||||
|
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
|
||||||
|
"resp": {
|
||||||
|
"type": "modeling",
|
||||||
|
"data": { "modeling_response": { "type": "empty" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"1318f5af-76a9-4161-9f6a-d410831fd098": {
|
||||||
|
"type": "result",
|
||||||
|
"range": [0, 0],
|
||||||
|
"pathToNode": [],
|
||||||
|
"commandType": "make_plane",
|
||||||
|
"data": { "type": "empty" },
|
||||||
|
"raw": {
|
||||||
|
"success": true,
|
||||||
|
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
|
||||||
|
"resp": {
|
||||||
|
"type": "modeling",
|
||||||
|
"data": { "modeling_response": { "type": "empty" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"908d264c-5989-4030-b2f4-5dfb52fda71a": {
|
||||||
|
"type": "result",
|
||||||
|
"range": [0, 0],
|
||||||
|
"pathToNode": [],
|
||||||
|
"commandType": "make_plane",
|
||||||
|
"data": { "type": "empty" },
|
||||||
|
"raw": {
|
||||||
|
"success": true,
|
||||||
|
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
|
||||||
|
"resp": {
|
||||||
|
"type": "modeling",
|
||||||
|
"data": { "modeling_response": { "type": "empty" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"0de4c253-ee21-4385-93bd-b93264f7eb60": {
|
||||||
|
"type": "result",
|
||||||
|
"range": [0, 0],
|
||||||
|
"pathToNode": [],
|
||||||
|
"commandType": "make_plane",
|
||||||
|
"data": { "type": "empty" },
|
||||||
|
"raw": {
|
||||||
|
"success": true,
|
||||||
|
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
|
||||||
|
"resp": {
|
||||||
|
"type": "modeling",
|
||||||
|
"data": { "modeling_response": { "type": "empty" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"engine_connection": {
|
||||||
|
"state": {
|
||||||
|
"type": "connection-established"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"kcl_manager": {},
|
||||||
|
"scene_infra": {},
|
||||||
|
"auth_machine": {},
|
||||||
|
"command_bar_machine": {},
|
||||||
|
"file_machine": {},
|
||||||
|
"home_machine": {},
|
||||||
|
"modeling_machine": {},
|
||||||
|
"settings_machine": {}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user