Seperate pending messages from artifact map (#3084)
* start of seperating pending message from artifact map * continue migration to sendCommandVersion2 * mostly massage types * process artifact after the fact * clean up
This commit is contained in:
@ -7,8 +7,6 @@ import { getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { Themes, getThemeColorForEngine, getOppositeTheme } from 'lib/theme'
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
|
||||
let lastMessage = ''
|
||||
|
||||
// TODO(paultag): This ought to be tweakable.
|
||||
const pingIntervalMs = 10000
|
||||
|
||||
@ -58,9 +56,6 @@ function isHighlightSetEntity_type(
|
||||
|
||||
type WebSocketResponse = Models['WebSocketResponse_type']
|
||||
type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
|
||||
type BatchResponseMap = {
|
||||
[key: string]: Models['BatchResponse_type']
|
||||
}
|
||||
|
||||
type ResultCommand = CommandInfo & {
|
||||
type: 'result'
|
||||
@ -1169,6 +1164,15 @@ export enum EngineCommandManagerEvents {
|
||||
* It also maintains an {@link artifactMap} that keeps track of the state of each
|
||||
* command, and the artifacts that have been generated by those commands.
|
||||
*/
|
||||
|
||||
interface PendingMessage {
|
||||
command: EngineCommand
|
||||
range: SourceRange
|
||||
idToRangeMap: { [key: string]: SourceRange }
|
||||
resolve: (data: [Models['WebSocketResponse_type']]) => void
|
||||
reject: (reason: string) => void
|
||||
promise: Promise<[Models['WebSocketResponse_type']]>
|
||||
}
|
||||
export class EngineCommandManager extends EventTarget {
|
||||
/**
|
||||
* The artifactMap is a client-side representation of the commands that have been sent
|
||||
@ -1182,6 +1186,25 @@ export class EngineCommandManager extends EventTarget {
|
||||
* of the KCL code that generated it.
|
||||
*/
|
||||
artifactMap: ArtifactMap = {}
|
||||
/**
|
||||
* The pendingCommands object is a map of the commands that have been sent to the engine that are still waiting on a reply
|
||||
*/
|
||||
pendingCommands: {
|
||||
[commandId: string]: PendingMessage
|
||||
} = {}
|
||||
/**
|
||||
* The orderedCommands array of all the the commands sent to the engine, un-folded from batches, and made into one long
|
||||
* list of the individual commands, this is used to process all the commands into the artifactMap
|
||||
*/
|
||||
orderedCommands: {
|
||||
command: EngineCommand
|
||||
range: SourceRange
|
||||
}[] = []
|
||||
/**
|
||||
* A map of the responses to the @this.orderedCommands, when processing the commands into the artifactMap, this response map allow
|
||||
* us to look up the response by command id
|
||||
*/
|
||||
responseMap: { [commandId: string]: OkWebSocketResponseData } = {}
|
||||
/**
|
||||
* The client-side representation of the scene command artifacts that have been sent to the server;
|
||||
* that is, the *non-modeling* commands and corresponding artifacts.
|
||||
@ -1206,7 +1229,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
defaultPlanes: DefaultPlanes | null = null
|
||||
commandLogs: CommandLog[] = []
|
||||
pendingExport?: {
|
||||
resolve: (filename?: string) => void
|
||||
resolve: (a: null) => void
|
||||
reject: (reason: any) => void
|
||||
}
|
||||
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
|
||||
@ -1435,31 +1458,92 @@ export class EngineCommandManager extends EventTarget {
|
||||
// export we send a binary blob.
|
||||
// Pass this to our export function.
|
||||
exportSave(event.data).then(() => {
|
||||
this.pendingExport?.resolve()
|
||||
this.pendingExport?.resolve(null)
|
||||
}, this.pendingExport?.reject)
|
||||
} else {
|
||||
const message: Models['WebSocketResponse_type'] = JSON.parse(
|
||||
event.data
|
||||
)
|
||||
if (
|
||||
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)}`)
|
||||
delete this.pendingCommands[message.request_id || '']
|
||||
}
|
||||
if (
|
||||
!(
|
||||
pending &&
|
||||
message.success &&
|
||||
(message.resp.type === 'modeling' ||
|
||||
message.resp.type === 'modeling_batch') &&
|
||||
message.request_id
|
||||
) {
|
||||
this.handleModelingCommand(
|
||||
message.resp,
|
||||
message.request_id,
|
||||
message
|
||||
)
|
||||
} else if (
|
||||
!message.success &&
|
||||
message.request_id &&
|
||||
this.artifactMap[message.request_id]
|
||||
) {
|
||||
this.handleFailedModelingCommand(message.request_id, message)
|
||||
}
|
||||
message.resp.type === 'modeling_batch')
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if (
|
||||
message.resp.type === 'modeling' &&
|
||||
pending.command.type === 'modeling_cmd_req' &&
|
||||
message.request_id
|
||||
) {
|
||||
this.addCommandLog({
|
||||
type: 'receive-reliable',
|
||||
data: message.resp,
|
||||
id: message?.request_id || '',
|
||||
cmd_type: pending?.command?.cmd?.type,
|
||||
})
|
||||
|
||||
const modelingResponse = message.resp.data.modeling_response
|
||||
|
||||
Object.values(
|
||||
this.subscriptions[modelingResponse.type] || {}
|
||||
).forEach((callback) => callback(modelingResponse))
|
||||
|
||||
this.responseMap[message.request_id] = message.resp
|
||||
} else if (
|
||||
message.resp.type === 'modeling_batch' &&
|
||||
pending.command.type === 'modeling_cmd_batch_req'
|
||||
) {
|
||||
let individualPendingResponses: {
|
||||
[key: string]: Models['WebSocketRequest_type']
|
||||
} = {}
|
||||
pending.command.requests.forEach(({ cmd, cmd_id }) => {
|
||||
individualPendingResponses[cmd_id] = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd,
|
||||
cmd_id,
|
||||
}
|
||||
})
|
||||
Object.entries(message.resp.data.responses).forEach(
|
||||
([key, response]) => {
|
||||
if (!('response' in response)) return
|
||||
const command = individualPendingResponses[key]
|
||||
if (!command) return
|
||||
if (command.type === 'modeling_cmd_req')
|
||||
this.addCommandLog({
|
||||
type: 'receive-reliable',
|
||||
data: {
|
||||
type: 'modeling',
|
||||
data: {
|
||||
modeling_response: response.response,
|
||||
},
|
||||
},
|
||||
id: key,
|
||||
cmd_type: command?.cmd?.type,
|
||||
})
|
||||
|
||||
this.responseMap[key] = {
|
||||
type: 'modeling',
|
||||
data: {
|
||||
modeling_response: response.response,
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pending.resolve([message])
|
||||
delete this.pendingCommands[message.request_id || '']
|
||||
}) as EventListener)
|
||||
|
||||
this.onEngineConnectionNewTrack = ({
|
||||
@ -1485,6 +1569,106 @@ export class EngineCommandManager extends EventTarget {
|
||||
this.onEngineConnectionStarted
|
||||
)
|
||||
}
|
||||
handleIndividualResponse({
|
||||
id,
|
||||
pendingMsg,
|
||||
response,
|
||||
}: {
|
||||
id: string
|
||||
pendingMsg: {
|
||||
command: EngineCommand
|
||||
range: SourceRange
|
||||
}
|
||||
response: OkWebSocketResponseData
|
||||
}) {
|
||||
const command = pendingMsg
|
||||
if (command?.command?.type !== 'modeling_cmd_req') return
|
||||
if (response?.type !== 'modeling') return
|
||||
const command2 = command.command.cmd
|
||||
|
||||
const range = command.range
|
||||
const pathToNode = getNodePathFromSourceRange(this.getAst(), range)
|
||||
const getParentId = (): string | undefined => {
|
||||
if (command2.type === 'extend_path') return command2.path
|
||||
if (command2.type === 'solid3d_get_extrusion_face_info') {
|
||||
const edgeArtifact = this.artifactMap[command2.edge_id]
|
||||
// edges's parent id is to the original "start_path" artifact
|
||||
if (edgeArtifact && edgeArtifact.parentId) {
|
||||
return edgeArtifact.parentId
|
||||
}
|
||||
}
|
||||
if (command2.type === 'close_path') return command2.path_id
|
||||
if (command2.type === 'extrude') return command2.target
|
||||
// handle other commands that have a parent here
|
||||
}
|
||||
const modelingResponse = response.data.modeling_response
|
||||
|
||||
if (command) {
|
||||
const parentId = getParentId()
|
||||
const artifact = {
|
||||
type: 'result',
|
||||
range: range,
|
||||
pathToNode,
|
||||
commandType: command.command.cmd.type,
|
||||
parentId: parentId,
|
||||
} as ArtifactMapCommand & { extrusions?: string[] }
|
||||
this.artifactMap[id] = artifact
|
||||
if (command2.type === 'extrude') {
|
||||
;(artifact as any).target = command2.target
|
||||
if (this.artifactMap[command2.target]?.commandType === 'start_path') {
|
||||
if ((this.artifactMap[command2.target] as any)?.extrusions?.length) {
|
||||
;(this.artifactMap[command2.target] as any).extrusions.push(id)
|
||||
} else {
|
||||
;(this.artifactMap[command2.target] as any).extrusions = [id]
|
||||
}
|
||||
}
|
||||
}
|
||||
this.artifactMap[id] = artifact
|
||||
if (
|
||||
(command2.type === 'entity_linear_pattern' &&
|
||||
modelingResponse.type === 'entity_linear_pattern') ||
|
||||
(command2.type === 'entity_circular_pattern' &&
|
||||
modelingResponse.type === 'entity_circular_pattern')
|
||||
) {
|
||||
const entities = modelingResponse.data.entity_ids
|
||||
entities?.forEach((entity: string) => {
|
||||
this.artifactMap[entity] = artifact
|
||||
})
|
||||
}
|
||||
if (
|
||||
command2.type === 'solid3d_get_extrusion_face_info' &&
|
||||
modelingResponse.type === 'solid3d_get_extrusion_face_info'
|
||||
) {
|
||||
const parent = this.artifactMap[parentId || '']
|
||||
modelingResponse.data.faces.forEach((face) => {
|
||||
if (face.cap !== 'none' && face.face_id && parent) {
|
||||
this.artifactMap[face.face_id] = {
|
||||
...parent,
|
||||
commandType: 'solid3d_get_extrusion_face_info',
|
||||
additionalData: {
|
||||
type: 'cap',
|
||||
info: face.cap === 'bottom' ? 'start' : 'end',
|
||||
},
|
||||
}
|
||||
}
|
||||
const curveArtifact = this.artifactMap[face?.curve_id || '']
|
||||
if (curveArtifact && face?.face_id) {
|
||||
this.artifactMap[face.face_id] = {
|
||||
...curveArtifact,
|
||||
commandType: 'solid3d_get_extrusion_face_info',
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} else if (command) {
|
||||
this.artifactMap[id] = {
|
||||
type: 'result',
|
||||
commandType: command2.type,
|
||||
range,
|
||||
pathToNode,
|
||||
} as ArtifactMapCommand & { extrusions?: string[] }
|
||||
}
|
||||
}
|
||||
|
||||
handleResize({
|
||||
streamWidth,
|
||||
@ -1509,233 +1693,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
}
|
||||
this.engineConnection?.send(resizeCmd)
|
||||
}
|
||||
handleModelingCommand(
|
||||
message: OkWebSocketResponseData,
|
||||
id: string,
|
||||
raw: WebSocketResponse
|
||||
) {
|
||||
if (!(message.type === 'modeling' || message.type === 'modeling_batch')) {
|
||||
return
|
||||
}
|
||||
|
||||
const command = this.artifactMap[id]
|
||||
let modelingResponse: Models['OkModelingCmdResponse_type'] = {
|
||||
type: 'empty',
|
||||
}
|
||||
if ('modeling_response' in message.data) {
|
||||
modelingResponse = message.data.modeling_response
|
||||
}
|
||||
if (
|
||||
command?.type === 'pending' &&
|
||||
command.commandType === 'batch' &&
|
||||
command?.additionalData?.type === 'batch-ids'
|
||||
) {
|
||||
if ('responses' in message.data) {
|
||||
const batchResponse = message.data.responses as BatchResponseMap
|
||||
// Iterate over the map of responses.
|
||||
Object.entries(batchResponse).forEach(([key, response]) => {
|
||||
// If the response is a success, we resolve the promise.
|
||||
if ('response' in response && response.response) {
|
||||
this.handleModelingCommand(
|
||||
{
|
||||
type: 'modeling',
|
||||
data: {
|
||||
modeling_response: response.response,
|
||||
},
|
||||
},
|
||||
key,
|
||||
{
|
||||
request_id: key,
|
||||
resp: {
|
||||
type: 'modeling',
|
||||
data: {
|
||||
modeling_response: response.response,
|
||||
},
|
||||
},
|
||||
success: true,
|
||||
}
|
||||
)
|
||||
} else if ('errors' in response) {
|
||||
this.handleFailedModelingCommand(key, {
|
||||
request_id: key,
|
||||
success: false,
|
||||
errors: response.errors,
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
command.additionalData.ids.forEach((id) => {
|
||||
this.handleModelingCommand(message, id, raw)
|
||||
})
|
||||
}
|
||||
// batch artifact is just a container, we don't need to keep it
|
||||
// once we process all the commands inside it
|
||||
const resolve = command.resolve
|
||||
delete this.artifactMap[id]
|
||||
resolve({
|
||||
id,
|
||||
commandType: command.commandType,
|
||||
range: command.range,
|
||||
raw,
|
||||
})
|
||||
return
|
||||
}
|
||||
const sceneCommand = this.sceneCommandArtifacts[id]
|
||||
this.addCommandLog({
|
||||
type: 'receive-reliable',
|
||||
data: message,
|
||||
id,
|
||||
cmd_type: command?.commandType || sceneCommand?.commandType,
|
||||
})
|
||||
Object.values(this.subscriptions[modelingResponse.type] || {}).forEach(
|
||||
(callback) => callback(modelingResponse)
|
||||
)
|
||||
|
||||
if (command && command.type === 'pending') {
|
||||
const resolve = command.resolve
|
||||
const oldArtifact = this.artifactMap[id] as ArtifactMapCommand & {
|
||||
extrusions?: string[]
|
||||
}
|
||||
const artifact = {
|
||||
type: 'result',
|
||||
range: command.range,
|
||||
pathToNode: command.pathToNode,
|
||||
commandType: command.commandType,
|
||||
parentId: command.parentId ? command.parentId : undefined,
|
||||
data: modelingResponse,
|
||||
raw,
|
||||
} as ArtifactMapCommand & { extrusions?: string[] }
|
||||
if (oldArtifact?.extrusions) {
|
||||
artifact.extrusions = oldArtifact.extrusions
|
||||
}
|
||||
this.artifactMap[id] = artifact
|
||||
if (
|
||||
(command.commandType === 'entity_linear_pattern' &&
|
||||
modelingResponse.type === 'entity_linear_pattern') ||
|
||||
(command.commandType === 'entity_circular_pattern' &&
|
||||
modelingResponse.type === 'entity_circular_pattern')
|
||||
) {
|
||||
const entities = modelingResponse.data.entity_ids
|
||||
entities?.forEach((entity: string) => {
|
||||
this.artifactMap[entity] = artifact
|
||||
})
|
||||
}
|
||||
if (
|
||||
command?.commandType === 'solid3d_get_extrusion_face_info' &&
|
||||
modelingResponse.type === 'solid3d_get_extrusion_face_info'
|
||||
) {
|
||||
const parent = this.artifactMap[command?.parentId || '']
|
||||
modelingResponse.data.faces.forEach((face) => {
|
||||
if (face.cap !== 'none' && face.face_id && parent) {
|
||||
this.artifactMap[face.face_id] = {
|
||||
...parent,
|
||||
commandType: 'solid3d_get_extrusion_face_info',
|
||||
additionalData: {
|
||||
type: 'cap',
|
||||
info: face.cap === 'bottom' ? 'start' : 'end',
|
||||
},
|
||||
}
|
||||
}
|
||||
const curveArtifact = this.artifactMap[face?.curve_id || '']
|
||||
if (curveArtifact && face?.face_id) {
|
||||
this.artifactMap[face.face_id] = {
|
||||
...curveArtifact,
|
||||
commandType: 'solid3d_get_extrusion_face_info',
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
resolve({
|
||||
id,
|
||||
commandType: command.commandType,
|
||||
range: command.range,
|
||||
data: modelingResponse,
|
||||
raw,
|
||||
})
|
||||
} else if (sceneCommand && sceneCommand.type === 'pending') {
|
||||
const resolve = sceneCommand.resolve
|
||||
const artifact = {
|
||||
type: 'result',
|
||||
range: sceneCommand.range,
|
||||
pathToNode: sceneCommand.pathToNode,
|
||||
commandType: sceneCommand.commandType,
|
||||
parentId: sceneCommand.parentId ? sceneCommand.parentId : undefined,
|
||||
data: modelingResponse,
|
||||
raw,
|
||||
} as const
|
||||
this.sceneCommandArtifacts[id] = artifact
|
||||
resolve({
|
||||
id,
|
||||
commandType: sceneCommand.commandType,
|
||||
range: sceneCommand.range,
|
||||
data: modelingResponse,
|
||||
raw,
|
||||
})
|
||||
} else if (command) {
|
||||
this.artifactMap[id] = {
|
||||
type: 'result',
|
||||
commandType: command?.commandType,
|
||||
range: command?.range,
|
||||
pathToNode: command?.pathToNode,
|
||||
data: modelingResponse,
|
||||
raw,
|
||||
}
|
||||
} else {
|
||||
this.sceneCommandArtifacts[id] = {
|
||||
type: 'result',
|
||||
commandType: sceneCommand?.commandType,
|
||||
range: sceneCommand?.range,
|
||||
pathToNode: sceneCommand?.pathToNode,
|
||||
data: modelingResponse,
|
||||
raw,
|
||||
}
|
||||
}
|
||||
}
|
||||
handleFailedModelingCommand(id: string, raw: WebSocketResponse) {
|
||||
const failed = raw as Models['FailureWebSocketResponse_type']
|
||||
const errors = failed.errors
|
||||
if (!id) return
|
||||
const command = this.artifactMap[id]
|
||||
if (command && command.type === 'pending') {
|
||||
this.artifactMap[id] = {
|
||||
type: 'failed',
|
||||
range: command.range,
|
||||
pathToNode: command.pathToNode,
|
||||
commandType: command.commandType,
|
||||
parentId: command.parentId ? command.parentId : undefined,
|
||||
errors,
|
||||
}
|
||||
if (
|
||||
command?.type === 'pending' &&
|
||||
command.commandType === 'batch' &&
|
||||
command?.additionalData?.type === 'batch-ids'
|
||||
) {
|
||||
command.additionalData.ids.forEach((id) => {
|
||||
this.handleFailedModelingCommand(id, raw)
|
||||
})
|
||||
}
|
||||
// batch artifact is just a container, we don't need to keep it
|
||||
// once we process all the commands inside it
|
||||
const resolve = command.resolve
|
||||
delete this.artifactMap[id]
|
||||
resolve({
|
||||
id,
|
||||
commandType: command.commandType,
|
||||
range: command.range,
|
||||
errors,
|
||||
raw,
|
||||
})
|
||||
} else {
|
||||
this.artifactMap[id] = {
|
||||
type: 'failed',
|
||||
range: command.range,
|
||||
pathToNode: command.pathToNode,
|
||||
commandType: command.commandType,
|
||||
parentId: command.parentId ? command.parentId : undefined,
|
||||
errors,
|
||||
}
|
||||
}
|
||||
}
|
||||
tearDown(opts?: { idleMode: boolean }) {
|
||||
if (this.engineConnection) {
|
||||
this.engineConnection.removeEventListener(
|
||||
@ -1768,6 +1726,8 @@ export class EngineCommandManager extends EventTarget {
|
||||
}
|
||||
async startNewSession() {
|
||||
this.artifactMap = {}
|
||||
this.orderedCommands = []
|
||||
this.responseMap = {}
|
||||
await this.initPlanes()
|
||||
}
|
||||
subscribeTo<T extends ModelTypes>({
|
||||
@ -1841,13 +1801,13 @@ export class EngineCommandManager extends EventTarget {
|
||||
sendSceneCommand(
|
||||
command: EngineCommand,
|
||||
forceWebsocket = false
|
||||
): Promise<any> {
|
||||
): Promise<Models['WebSocketResponse_type'] | null> {
|
||||
if (this.engineConnection === undefined) {
|
||||
return Promise.resolve()
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
if (!this.engineConnection?.isReady()) {
|
||||
return Promise.resolve()
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
if (
|
||||
@ -1866,19 +1826,13 @@ export class EngineCommandManager extends EventTarget {
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
command.type === 'modeling_cmd_req' &&
|
||||
command.cmd.type !== lastMessage
|
||||
) {
|
||||
lastMessage = command.cmd.type
|
||||
}
|
||||
if (command.type === 'modeling_cmd_batch_req') {
|
||||
this.engineConnection?.send(command)
|
||||
// TODO - handlePendingCommands does not handle batch commands
|
||||
// return this.handlePendingCommand(command.requests[0].cmd_id, command.cmd)
|
||||
return Promise.resolve()
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
if (command.type !== 'modeling_cmd_req') return Promise.resolve()
|
||||
if (command.type !== 'modeling_cmd_req') return Promise.resolve(null)
|
||||
const cmd = command.cmd
|
||||
if (
|
||||
(cmd.type === 'camera_drag_move' ||
|
||||
@ -1891,7 +1845,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
;(cmd as any).sequence = this.outSequence
|
||||
this.outSequence++
|
||||
this.engineConnection?.unreliableSend(command)
|
||||
return Promise.resolve()
|
||||
return Promise.resolve(null)
|
||||
} else if (
|
||||
cmd.type === 'highlight_set_entity' &&
|
||||
this.engineConnection?.unreliableDataChannel
|
||||
@ -1899,7 +1853,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
cmd.sequence = this.outSequence
|
||||
this.outSequence++
|
||||
this.engineConnection?.unreliableSend(command)
|
||||
return Promise.resolve()
|
||||
return Promise.resolve(null)
|
||||
} else if (
|
||||
cmd.type === 'mouse_move' &&
|
||||
this.engineConnection.unreliableDataChannel
|
||||
@ -1907,9 +1861,9 @@ export class EngineCommandManager extends EventTarget {
|
||||
cmd.sequence = this.outSequence
|
||||
this.outSequence++
|
||||
this.engineConnection?.unreliableSend(command)
|
||||
return Promise.resolve()
|
||||
return Promise.resolve(null)
|
||||
} else if (cmd.type === 'export') {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const promise = new Promise<null>((resolve, reject) => {
|
||||
this.pendingExport = { resolve, reject }
|
||||
})
|
||||
this.engineConnection?.send(command)
|
||||
@ -1922,194 +1876,15 @@ export class EngineCommandManager extends EventTarget {
|
||||
;(cmd as any).sequence = this.outSequence++
|
||||
}
|
||||
// since it's not mouse drag or highlighting send over TCP and keep track of the command
|
||||
this.engineConnection?.send(command)
|
||||
return this.handlePendingSceneCommand(command.cmd_id, command.cmd)
|
||||
}
|
||||
sendModelingCommand({
|
||||
id,
|
||||
range,
|
||||
command,
|
||||
ast,
|
||||
idToRangeMap,
|
||||
}: {
|
||||
id: string
|
||||
range: SourceRange
|
||||
command: EngineCommand
|
||||
ast: Program
|
||||
idToRangeMap?: { [key: string]: SourceRange }
|
||||
}): Promise<ResolveCommand | void> {
|
||||
if (this.engineConnection === undefined) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
if (!this.engineConnection?.isReady()) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (typeof command !== 'string') {
|
||||
this.addCommandLog({
|
||||
type: 'send-modeling',
|
||||
data: command,
|
||||
})
|
||||
} else {
|
||||
this.addCommandLog({
|
||||
type: 'send-modeling',
|
||||
data: JSON.parse(command),
|
||||
})
|
||||
}
|
||||
this.engineConnection?.send(command)
|
||||
if (typeof command !== 'string' && command.type === 'modeling_cmd_req') {
|
||||
return this.handlePendingCommand(id, command?.cmd, ast, range)
|
||||
} else if (
|
||||
typeof command !== 'string' &&
|
||||
command.type === 'modeling_cmd_batch_req'
|
||||
) {
|
||||
return this.handlePendingBatchCommand(id, command.requests, idToRangeMap)
|
||||
} else if (typeof command === 'string') {
|
||||
const parseCommand: EngineCommand = JSON.parse(command)
|
||||
if (parseCommand.type === 'modeling_cmd_req') {
|
||||
return this.handlePendingCommand(id, parseCommand?.cmd, ast, range)
|
||||
} else if (parseCommand.type === 'modeling_cmd_batch_req') {
|
||||
return this.handlePendingBatchCommand(
|
||||
id,
|
||||
parseCommand.requests,
|
||||
idToRangeMap
|
||||
)
|
||||
}
|
||||
}
|
||||
return Promise.reject(new Error('Expected unreachable reached'))
|
||||
}
|
||||
handlePendingSceneCommand(
|
||||
id: string,
|
||||
command: Models['ModelingCmd_type'],
|
||||
ast?: Program,
|
||||
range?: SourceRange
|
||||
) {
|
||||
let resolve: (val: any) => void = () => {}
|
||||
const promise = new Promise((_resolve, reject) => {
|
||||
resolve = _resolve
|
||||
})
|
||||
const pathToNode = ast
|
||||
? getNodePathFromSourceRange(ast, range || [0, 0])
|
||||
: []
|
||||
this.sceneCommandArtifacts[id] = {
|
||||
range: range || [0, 0],
|
||||
pathToNode,
|
||||
type: 'pending',
|
||||
commandType: command.type,
|
||||
promise,
|
||||
resolve,
|
||||
}
|
||||
return promise
|
||||
}
|
||||
handlePendingCommand(
|
||||
id: string,
|
||||
command: Models['ModelingCmd_type'],
|
||||
ast?: Program,
|
||||
range?: SourceRange
|
||||
): Promise<ResolveCommand | void> {
|
||||
let resolve: (val: any) => void = () => {}
|
||||
const promise: Promise<ResolveCommand | void> = new Promise(
|
||||
(_resolve, reject) => {
|
||||
resolve = _resolve
|
||||
}
|
||||
)
|
||||
const getParentId = (): string | undefined => {
|
||||
if (command.type === 'extend_path') return command.path
|
||||
if (command.type === 'solid3d_get_extrusion_face_info') {
|
||||
const edgeArtifact = this.artifactMap[command.edge_id]
|
||||
// edges's parent id is to the original "start_path" artifact
|
||||
if (edgeArtifact && edgeArtifact.parentId) {
|
||||
return edgeArtifact.parentId
|
||||
}
|
||||
}
|
||||
if (command.type === 'close_path') return command.path_id
|
||||
if (command.type === 'extrude') return command.target
|
||||
// handle other commands that have a parent here
|
||||
}
|
||||
const pathToNode = ast
|
||||
? getNodePathFromSourceRange(ast, range || [0, 0])
|
||||
: []
|
||||
this.artifactMap[id] = {
|
||||
range: range || [0, 0],
|
||||
pathToNode,
|
||||
type: 'pending',
|
||||
commandType: command.type,
|
||||
parentId: getParentId(),
|
||||
promise,
|
||||
resolve,
|
||||
}
|
||||
if (command.type === 'extrude') {
|
||||
this.artifactMap[id] = {
|
||||
range: range || [0, 0],
|
||||
pathToNode,
|
||||
type: 'pending',
|
||||
commandType: 'extrude',
|
||||
parentId: getParentId(),
|
||||
promise,
|
||||
target: command.target,
|
||||
resolve,
|
||||
}
|
||||
const target = this.artifactMap[command.target]
|
||||
if (target.commandType === 'start_path') {
|
||||
// tsc cannot infer that target can have extrusions
|
||||
// from the commandType (why?) so we need to cast it
|
||||
const typedTarget = target as (
|
||||
| PendingCommand
|
||||
| ResultCommand
|
||||
| FailedCommand
|
||||
) & { extrusions?: string[] }
|
||||
if (typedTarget?.extrusions?.length) {
|
||||
typedTarget.extrusions.push(id)
|
||||
} else {
|
||||
typedTarget.extrusions = [id]
|
||||
}
|
||||
// Update in the map.
|
||||
this.artifactMap[command.target] = typedTarget
|
||||
}
|
||||
}
|
||||
return promise
|
||||
}
|
||||
async handlePendingBatchCommand(
|
||||
id: string,
|
||||
commands: Models['ModelingCmdReq_type'][],
|
||||
idToRangeMap?: { [key: string]: SourceRange },
|
||||
ast?: Program,
|
||||
range?: SourceRange
|
||||
): Promise<ResolveCommand | void> {
|
||||
let resolve: (val: any) => void = () => {}
|
||||
const promise: Promise<ResolveCommand | void> = new Promise(
|
||||
(_resolve, reject) => {
|
||||
resolve = _resolve
|
||||
}
|
||||
)
|
||||
|
||||
if (!idToRangeMap) {
|
||||
return Promise.reject(
|
||||
new Error('idToRangeMap is required for batch commands')
|
||||
)
|
||||
}
|
||||
|
||||
// Add the overall batch command to the artifact map just so we can track all of the
|
||||
// individual commands that are part of the batch.
|
||||
// we'll delete this artifact once all of the individual commands have been processed.
|
||||
this.artifactMap[id] = {
|
||||
range: range || [0, 0],
|
||||
pathToNode: [],
|
||||
type: 'pending',
|
||||
commandType: 'batch',
|
||||
additionalData: { type: 'batch-ids', ids: commands.map((c) => c.cmd_id) },
|
||||
parentId: undefined,
|
||||
promise,
|
||||
resolve,
|
||||
}
|
||||
|
||||
Promise.all(
|
||||
commands.map((c) =>
|
||||
this.handlePendingCommand(c.cmd_id, c.cmd, ast, idToRangeMap[c.cmd_id])
|
||||
)
|
||||
)
|
||||
return promise
|
||||
return this.sendCommand(command.cmd_id, {
|
||||
command,
|
||||
idToRangeMap: {},
|
||||
range: [0, 0],
|
||||
}).then(([a]) => a)
|
||||
}
|
||||
/**
|
||||
* A wrapper around the sendCommand where all inputs are JSON strings
|
||||
*/
|
||||
async sendModelingCommandFromWasm(
|
||||
id: string,
|
||||
rangeStr: string,
|
||||
@ -2132,53 +1907,88 @@ export class EngineCommandManager extends EventTarget {
|
||||
return Promise.reject(new Error('commandStr is undefined'))
|
||||
}
|
||||
const range: SourceRange = JSON.parse(rangeStr)
|
||||
const command: EngineCommand = JSON.parse(commandStr)
|
||||
const idToRangeMap: { [key: string]: SourceRange } =
|
||||
JSON.parse(idToRangeStr)
|
||||
|
||||
const command: EngineCommand = JSON.parse(commandStr)
|
||||
|
||||
// We only care about the modeling command response.
|
||||
return this.sendModelingCommand({
|
||||
id,
|
||||
range,
|
||||
const resp = await this.sendCommand(id, {
|
||||
command,
|
||||
ast: this.getAst(),
|
||||
range,
|
||||
idToRangeMap,
|
||||
}).then((resp) => {
|
||||
if (!resp) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
'returning modeling cmd response to the rust side is undefined or null'
|
||||
)
|
||||
)
|
||||
}
|
||||
return JSON.stringify(resp.raw)
|
||||
})
|
||||
return JSON.stringify(resp[0])
|
||||
}
|
||||
async commandResult(id: string): Promise<any> {
|
||||
const command = this.artifactMap[id]
|
||||
if (!command) {
|
||||
return Promise.reject(new Error('No command found'))
|
||||
/**
|
||||
* Common send command function used for both modeling and scene commands
|
||||
* So that both have a common way to send pending commands with promises for the responses
|
||||
*/
|
||||
async sendCommand(
|
||||
id: string,
|
||||
message: {
|
||||
command: PendingMessage['command']
|
||||
range: PendingMessage['range']
|
||||
idToRangeMap: PendingMessage['idToRangeMap']
|
||||
}
|
||||
if (command.type === 'result') {
|
||||
return command.data
|
||||
} else if (command.type === 'failed') {
|
||||
return Promise.resolve(command.errors)
|
||||
): Promise<[Models['WebSocketResponse_type']]> {
|
||||
const { promise, resolve, reject } = promiseFactory<any>()
|
||||
this.pendingCommands[id] = {
|
||||
resolve,
|
||||
reject,
|
||||
promise,
|
||||
command: message.command,
|
||||
range: message.range,
|
||||
idToRangeMap: message.idToRangeMap,
|
||||
}
|
||||
return command.promise
|
||||
if (message.command.type === 'modeling_cmd_req') {
|
||||
this.orderedCommands.push({
|
||||
command: message.command,
|
||||
range: message.range,
|
||||
})
|
||||
} else if (message.command.type === 'modeling_cmd_batch_req') {
|
||||
message.command.requests.forEach((req) => {
|
||||
const cmd: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: req.cmd_id,
|
||||
cmd: req.cmd,
|
||||
}
|
||||
this.orderedCommands.push({
|
||||
command: cmd,
|
||||
range: message.idToRangeMap[req.cmd_id || ''],
|
||||
})
|
||||
})
|
||||
}
|
||||
this.engineConnection?.send(message.command)
|
||||
return promise
|
||||
}
|
||||
async waitForAllCommands(): Promise<{
|
||||
artifactMap: ArtifactMap
|
||||
}> {
|
||||
/**
|
||||
* When an execution takes place we want to wait until we've got replies for all of the commands
|
||||
* When this is done when we build the artifact map synchronously.
|
||||
*/
|
||||
async waitForAllCommands() {
|
||||
const pendingCommands = Object.values(this.artifactMap).filter(
|
||||
({ type }) => type === 'pending'
|
||||
) as PendingCommand[]
|
||||
const proms = pendingCommands.map(({ promise }) => promise)
|
||||
await Promise.all(proms)
|
||||
|
||||
return {
|
||||
artifactMap: this.artifactMap,
|
||||
}
|
||||
const otherPending = Object.values(this.pendingCommands).map(
|
||||
(a) => a.promise
|
||||
)
|
||||
await Promise.all([...proms, otherPending])
|
||||
this.orderedCommands.forEach(({ command, range }) => {
|
||||
// expect all to be `modeling_cmd_req` as batch commands have
|
||||
// already been expanded before being added to orderedCommands
|
||||
if (command.type !== 'modeling_cmd_req') return
|
||||
const id = command.cmd_id
|
||||
const response = this.responseMap[id]
|
||||
this.handleIndividualResponse({
|
||||
id,
|
||||
pendingMsg: {
|
||||
command,
|
||||
range,
|
||||
},
|
||||
response,
|
||||
})
|
||||
})
|
||||
}
|
||||
private async initPlanes() {
|
||||
if (this.planesInitialized()) return
|
||||
@ -2209,7 +2019,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
this.onPlaneSelectCallback = callback
|
||||
}
|
||||
|
||||
async setPlaneHidden(id: string, hidden: boolean): Promise<string> {
|
||||
async setPlaneHidden(id: string, hidden: boolean) {
|
||||
return await this.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
@ -2250,3 +2060,13 @@ export class EngineCommandManager extends EventTarget {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function promiseFactory<T>() {
|
||||
let resolve: (value: T | PromiseLike<T>) => void = () => {}
|
||||
let reject: (value: T | PromiseLike<T>) => void = () => {}
|
||||
const promise = new Promise<T>((_resolve, _reject) => {
|
||||
resolve = _resolve
|
||||
reject = _reject
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
Reference in New Issue
Block a user