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