2023-08-24 15:34:51 -07:00
|
|
|
import { SourceRange } from 'lang/executor'
|
|
|
|
import { Selections } from 'useStore'
|
|
|
|
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
|
2023-08-02 15:41:59 +10:00
|
|
|
import { Models } from '@kittycad/lib'
|
2023-08-24 15:34:51 -07:00
|
|
|
import { exportSave } from 'lib/exportSave'
|
2023-08-09 20:49:10 +10:00
|
|
|
import { v4 as uuidv4 } from 'uuid'
|
2023-06-22 16:43:33 +10:00
|
|
|
|
|
|
|
interface ResultCommand {
|
|
|
|
type: 'result'
|
|
|
|
data: any
|
|
|
|
}
|
|
|
|
interface PendingCommand {
|
|
|
|
type: 'pending'
|
|
|
|
promise: Promise<any>
|
|
|
|
resolve: (val: any) => void
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface ArtifactMap {
|
|
|
|
[key: string]: ResultCommand | PendingCommand
|
|
|
|
}
|
|
|
|
export interface SourceRangeMap {
|
|
|
|
[key: string]: SourceRange
|
|
|
|
}
|
|
|
|
|
|
|
|
interface SelectionsArgs {
|
|
|
|
id: string
|
|
|
|
type: Selections['codeBasedSelections'][number]['type']
|
|
|
|
}
|
|
|
|
|
|
|
|
interface CursorSelectionsArgs {
|
|
|
|
otherSelections: Selections['otherSelections']
|
|
|
|
idBasedSelections: { type: string; id: string }[]
|
|
|
|
}
|
|
|
|
|
2023-08-21 16:53:31 -04:00
|
|
|
interface NewTrackArgs {
|
|
|
|
conn: EngineConnection
|
|
|
|
mediaStream: MediaStream
|
|
|
|
}
|
|
|
|
|
2023-08-25 00:16:37 -04:00
|
|
|
type WebSocketResponse = Models['OkWebSocketResponseData_type']
|
2023-08-09 20:49:10 +10:00
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
// EngineConnection encapsulates the connection(s) to the Engine
|
|
|
|
// for the EngineCommandManager; namely, the underlying WebSocket
|
|
|
|
// and WebRTC connections.
|
2023-08-24 10:46:02 -04:00
|
|
|
export class EngineConnection {
|
2023-08-18 16:16:16 -04:00
|
|
|
websocket?: WebSocket
|
2023-06-22 16:43:33 +10:00
|
|
|
pc?: RTCPeerConnection
|
2023-06-23 09:56:37 +10:00
|
|
|
lossyDataChannel?: RTCDataChannel
|
2023-08-18 16:16:16 -04:00
|
|
|
|
2023-08-21 16:53:31 -04:00
|
|
|
private ready: boolean
|
2023-08-18 16:16:16 -04:00
|
|
|
|
|
|
|
readonly url: string
|
|
|
|
private readonly token?: string
|
2023-08-24 23:46:45 +10:00
|
|
|
private onWebsocketOpen: (engineConnection: EngineConnection) => void
|
|
|
|
private onDataChannelOpen: (engineConnection: EngineConnection) => void
|
|
|
|
private onEngineConnectionOpen: (engineConnection: EngineConnection) => void
|
|
|
|
private onConnectionStarted: (engineConnection: EngineConnection) => void
|
|
|
|
private onClose: (engineConnection: EngineConnection) => void
|
|
|
|
private onNewTrack: (track: NewTrackArgs) => void
|
2023-08-18 16:16:16 -04:00
|
|
|
|
2023-08-24 23:46:45 +10:00
|
|
|
constructor({
|
|
|
|
url,
|
|
|
|
token,
|
|
|
|
onWebsocketOpen = () => {},
|
|
|
|
onNewTrack = () => {},
|
|
|
|
onEngineConnectionOpen = () => {},
|
|
|
|
onConnectionStarted = () => {},
|
|
|
|
onClose = () => {},
|
|
|
|
onDataChannelOpen = () => {},
|
|
|
|
}: {
|
|
|
|
url: string
|
|
|
|
token?: string
|
|
|
|
onWebsocketOpen?: (engineConnection: EngineConnection) => void
|
|
|
|
onDataChannelOpen?: (engineConnection: EngineConnection) => void
|
|
|
|
onEngineConnectionOpen?: (engineConnection: EngineConnection) => void
|
|
|
|
onConnectionStarted?: (engineConnection: EngineConnection) => void
|
|
|
|
onClose?: (engineConnection: EngineConnection) => void
|
|
|
|
onNewTrack?: (track: NewTrackArgs) => void
|
|
|
|
}) {
|
2023-08-18 16:16:16 -04:00
|
|
|
this.url = url
|
|
|
|
this.token = token
|
2023-08-21 16:53:31 -04:00
|
|
|
this.ready = false
|
2023-08-24 23:46:45 +10:00
|
|
|
this.onWebsocketOpen = onWebsocketOpen
|
|
|
|
this.onDataChannelOpen = onDataChannelOpen
|
|
|
|
this.onEngineConnectionOpen = onEngineConnectionOpen
|
|
|
|
this.onConnectionStarted = onConnectionStarted
|
|
|
|
this.onClose = onClose
|
|
|
|
this.onNewTrack = onNewTrack
|
2023-08-21 16:53:31 -04:00
|
|
|
|
|
|
|
// TODO(paultag): This ought to be tweakable.
|
|
|
|
const pingIntervalMs = 10000
|
2023-08-02 16:23:17 -07:00
|
|
|
|
2023-08-21 16:53:31 -04:00
|
|
|
setInterval(() => {
|
|
|
|
if (this.isReady()) {
|
|
|
|
// When we're online, every 10 seconds, we'll attempt to put a 'ping'
|
|
|
|
// command through the WebSocket connection. This will help both ends
|
|
|
|
// of the connection maintain the TCP connection without hitting a
|
|
|
|
// timeout condition.
|
|
|
|
this.send({ type: 'ping' })
|
|
|
|
}
|
|
|
|
}, pingIntervalMs)
|
|
|
|
}
|
|
|
|
// isReady will return true only when the WebRTC *and* WebSocket connection
|
|
|
|
// are connected. During setup, the WebSocket connection comes online first,
|
|
|
|
// which is used to establish the WebRTC connection. The EngineConnection
|
|
|
|
// is not "Ready" until both are connected.
|
|
|
|
isReady() {
|
|
|
|
return this.ready
|
|
|
|
}
|
|
|
|
// connect will attempt to connect to the Engine over a WebSocket, and
|
|
|
|
// establish the WebRTC connections.
|
|
|
|
//
|
|
|
|
// This will attempt the full handshake, and retry if the connection
|
|
|
|
// did not establish.
|
2023-08-18 16:16:16 -04:00
|
|
|
connect() {
|
2023-08-21 16:53:31 -04:00
|
|
|
// TODO(paultag): make this safe to call multiple times, and figure out
|
|
|
|
// when a connection is in progress (state: connecting or something).
|
2023-08-18 16:16:16 -04:00
|
|
|
|
2023-08-21 16:53:31 -04:00
|
|
|
this.websocket = new WebSocket(this.url, [])
|
2023-08-18 16:16:16 -04:00
|
|
|
this.websocket.binaryType = 'arraybuffer'
|
2023-08-02 16:23:17 -07:00
|
|
|
|
2023-06-22 16:43:33 +10:00
|
|
|
this.pc = new RTCPeerConnection()
|
2023-06-23 10:54:19 +10:00
|
|
|
this.pc.createDataChannel('unreliable_modeling_cmds')
|
2023-08-18 16:16:16 -04:00
|
|
|
this.websocket.addEventListener('open', (event) => {
|
2023-06-22 16:43:33 +10:00
|
|
|
console.log('Connected to websocket, waiting for ICE servers')
|
2023-08-18 16:16:16 -04:00
|
|
|
if (this.token) {
|
2023-08-21 16:53:31 -04:00
|
|
|
this.send({ headers: { Authorization: `Bearer ${this.token}` } })
|
2023-07-11 20:34:09 +10:00
|
|
|
}
|
2023-06-22 16:43:33 +10:00
|
|
|
})
|
|
|
|
|
2023-08-21 16:53:31 -04:00
|
|
|
this.websocket.addEventListener('open', (event) => {
|
2023-08-24 23:46:45 +10:00
|
|
|
this.onWebsocketOpen(this)
|
2023-08-21 16:53:31 -04:00
|
|
|
})
|
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
this.websocket.addEventListener('close', (event) => {
|
2023-08-02 13:51:05 -05:00
|
|
|
console.log('websocket connection closed', event)
|
2023-08-21 16:53:31 -04:00
|
|
|
this.close()
|
2023-06-22 16:43:33 +10:00
|
|
|
})
|
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
this.websocket.addEventListener('error', (event) => {
|
2023-08-02 13:51:05 -05:00
|
|
|
console.log('websocket connection error', event)
|
2023-08-21 16:53:31 -04:00
|
|
|
this.close()
|
2023-06-22 16:43:33 +10:00
|
|
|
})
|
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
this.websocket.addEventListener('message', (event) => {
|
|
|
|
// In the EngineConnection, we're looking for messages to/from
|
|
|
|
// the server that relate to the ICE handshake, or WebRTC
|
|
|
|
// negotiation. There may be other messages (including ArrayBuffer
|
|
|
|
// messages) that are intended for the GUI itself, so be careful
|
|
|
|
// when assuming we're the only consumer or that all messages will
|
|
|
|
// be carefully formatted here.
|
|
|
|
|
|
|
|
if (typeof event.data !== 'string') {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-08-25 00:16:37 -04:00
|
|
|
const message: Models['WebSocketResponse_type'] = JSON.parse(event.data)
|
|
|
|
|
|
|
|
if (!message.success) {
|
|
|
|
if (message.request_id) {
|
|
|
|
console.error(`Error in response to request ${message.request_id}:`)
|
|
|
|
} else {
|
|
|
|
console.error(`Error from server:`)
|
|
|
|
}
|
2023-08-29 10:28:27 +10:00
|
|
|
message?.errors?.forEach((error) => {
|
2023-08-25 00:16:37 -04:00
|
|
|
console.error(` - ${error.error_code}: ${error.message}`)
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let resp = message.resp
|
|
|
|
if (!resp) {
|
|
|
|
// If there's no body to the response, we can bail here.
|
2023-08-21 16:53:31 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-08-25 00:16:37 -04:00
|
|
|
if (resp.type === 'sdp_answer') {
|
|
|
|
let answer = resp.data?.answer
|
|
|
|
if (!answer || answer.type === 'unspecified') {
|
|
|
|
return
|
|
|
|
}
|
2023-08-18 16:16:16 -04:00
|
|
|
|
2023-08-21 16:53:31 -04:00
|
|
|
if (this.pc?.signalingState !== 'stable') {
|
|
|
|
// If the connection is stable, we shouldn't bother updating the
|
|
|
|
// SDP, since we have a stable connection to the backend. If we
|
|
|
|
// need to renegotiate, the whole PeerConnection needs to get
|
|
|
|
// tore down.
|
|
|
|
this.pc?.setRemoteDescription(
|
|
|
|
new RTCSessionDescription({
|
2023-08-25 00:16:37 -04:00
|
|
|
type: answer.type,
|
|
|
|
sdp: answer.sdp,
|
2023-08-21 16:53:31 -04:00
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
2023-08-25 00:16:37 -04:00
|
|
|
} else if (resp.type === 'trickle_ice') {
|
|
|
|
let candidate = resp.data?.candidate
|
|
|
|
this.pc?.addIceCandidate(candidate as RTCIceCandidateInit)
|
|
|
|
} else if (resp.type === 'ice_server_info' && this.pc) {
|
2023-08-18 16:16:16 -04:00
|
|
|
console.log('received ice_server_info')
|
2023-08-25 00:16:37 -04:00
|
|
|
let ice_servers = resp.data?.ice_servers
|
2023-06-22 16:43:33 +10:00
|
|
|
|
2023-08-25 00:16:37 -04:00
|
|
|
if (ice_servers?.length > 0) {
|
2023-08-18 16:16:16 -04:00
|
|
|
// When we set the Configuration, we want to always force
|
|
|
|
// iceTransportPolicy to 'relay', since we know the topology
|
|
|
|
// of the ICE/STUN/TUN server and the engine. We don't wish to
|
|
|
|
// talk to the engine in any configuration /other/ than relay
|
|
|
|
// from a infra POV.
|
|
|
|
this.pc.setConfiguration({
|
2023-08-25 00:16:37 -04:00
|
|
|
iceServers: ice_servers,
|
2023-08-18 16:16:16 -04:00
|
|
|
iceTransportPolicy: 'relay',
|
2023-06-22 16:43:33 +10:00
|
|
|
})
|
2023-08-18 16:16:16 -04:00
|
|
|
} else {
|
|
|
|
this.pc?.setConfiguration({})
|
|
|
|
}
|
2023-06-22 16:43:33 +10:00
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
// We have an ICE Servers set now. We just setConfiguration, so let's
|
|
|
|
// start adding things we care about to the PeerConnection and let
|
|
|
|
// ICE negotiation happen in the background. Everything from here
|
|
|
|
// until the end of this function is setup of our end of the
|
|
|
|
// PeerConnection and waiting for events to fire our callbacks.
|
|
|
|
|
2023-08-21 16:53:31 -04:00
|
|
|
this.pc.addEventListener('connectionstatechange', (event) => {
|
|
|
|
// if (this.pc?.iceConnectionState === 'disconnected') {
|
|
|
|
// this.close()
|
|
|
|
// }
|
|
|
|
})
|
2023-08-18 16:16:16 -04:00
|
|
|
|
|
|
|
this.pc.addEventListener('icecandidate', (event) => {
|
|
|
|
if (!this.pc || !this.websocket) return
|
2023-08-25 00:16:37 -04:00
|
|
|
if (event.candidate !== null) {
|
2023-08-18 16:16:16 -04:00
|
|
|
console.log('sending trickle ice candidate')
|
|
|
|
const { candidate } = event
|
2023-08-21 16:53:31 -04:00
|
|
|
this.send({
|
|
|
|
type: 'trickle_ice',
|
|
|
|
candidate: candidate.toJSON(),
|
|
|
|
})
|
2023-08-18 16:16:16 -04:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// Offer to receive 1 video track
|
|
|
|
this.pc.addTransceiver('video', {})
|
|
|
|
|
|
|
|
// Finally (but actually firstly!), to kick things off, we're going to
|
|
|
|
// generate our SDP, set it on our PeerConnection, and let the server
|
|
|
|
// know about our capabilities.
|
|
|
|
this.pc
|
|
|
|
.createOffer()
|
|
|
|
.then(async (descriptionInit) => {
|
|
|
|
await this?.pc?.setLocalDescription(descriptionInit)
|
|
|
|
console.log('sent sdp_offer begin')
|
2023-08-21 16:53:31 -04:00
|
|
|
this.send({
|
2023-08-18 16:16:16 -04:00
|
|
|
type: 'sdp_offer',
|
|
|
|
offer: this.pc?.localDescription,
|
2023-06-23 09:56:37 +10:00
|
|
|
})
|
|
|
|
})
|
2023-08-18 16:16:16 -04:00
|
|
|
.catch(console.log)
|
|
|
|
}
|
2023-08-21 16:53:31 -04:00
|
|
|
|
|
|
|
// TODO(paultag): This ought to be both controllable, as well as something
|
|
|
|
// like exponential backoff to have some grace on the backend, as well as
|
|
|
|
// fix responsiveness for clients that had a weird network hiccup.
|
2023-08-23 16:49:04 -04:00
|
|
|
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
|
2023-08-21 16:53:31 -04:00
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
if (this.isReady()) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
console.log('engine connection timeout on connection, retrying')
|
|
|
|
this.close()
|
|
|
|
this.connect()
|
|
|
|
}, connectionTimeoutMs)
|
|
|
|
})
|
|
|
|
|
|
|
|
this.pc.addEventListener('track', (event) => {
|
|
|
|
console.log('received track', event)
|
|
|
|
const mediaStream = event.streams[0]
|
2023-08-24 23:46:45 +10:00
|
|
|
this.onNewTrack({
|
|
|
|
conn: this,
|
|
|
|
mediaStream: mediaStream,
|
|
|
|
})
|
2023-08-18 16:16:16 -04:00
|
|
|
})
|
|
|
|
|
2023-08-23 15:29:03 -04:00
|
|
|
// During startup, we'll track the time from `connect` being called
|
|
|
|
// until the 'done' event fires.
|
|
|
|
let connectionStarted = new Date()
|
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
this.pc.addEventListener('datachannel', (event) => {
|
|
|
|
this.lossyDataChannel = event.channel
|
|
|
|
|
|
|
|
console.log('accepted lossy data channel', event.channel.label)
|
|
|
|
this.lossyDataChannel.addEventListener('open', (event) => {
|
|
|
|
console.log('lossy data channel opened', event)
|
2023-08-21 16:53:31 -04:00
|
|
|
|
2023-08-24 23:46:45 +10:00
|
|
|
this.onDataChannelOpen(this)
|
2023-08-21 16:53:31 -04:00
|
|
|
|
2023-08-23 15:29:03 -04:00
|
|
|
let timeToConnectMs = new Date().getTime() - connectionStarted.getTime()
|
|
|
|
console.log(`engine connection time to connect: ${timeToConnectMs}ms`)
|
2023-08-24 23:46:45 +10:00
|
|
|
this.onEngineConnectionOpen(this)
|
|
|
|
this.ready = true
|
2023-08-18 16:16:16 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
this.lossyDataChannel.addEventListener('close', (event) => {
|
|
|
|
console.log('lossy data channel closed')
|
2023-08-21 16:53:31 -04:00
|
|
|
this.close()
|
2023-08-18 16:16:16 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
this.lossyDataChannel.addEventListener('error', (event) => {
|
|
|
|
console.log('lossy data channel error')
|
2023-08-21 16:53:31 -04:00
|
|
|
this.close()
|
2023-08-18 16:16:16 -04:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2023-08-24 23:46:45 +10:00
|
|
|
this.onConnectionStarted(this)
|
2023-08-21 16:53:31 -04:00
|
|
|
}
|
|
|
|
send(message: object) {
|
|
|
|
// TODO(paultag): Add in logic to determine the connection state and
|
|
|
|
// take actions if needed?
|
|
|
|
this.websocket?.send(JSON.stringify(message))
|
2023-08-18 16:16:16 -04:00
|
|
|
}
|
|
|
|
close() {
|
|
|
|
this.websocket?.close()
|
|
|
|
this.pc?.close()
|
|
|
|
this.lossyDataChannel?.close()
|
2023-08-22 18:18:22 -04:00
|
|
|
this.websocket = undefined
|
|
|
|
this.pc = undefined
|
|
|
|
this.lossyDataChannel = undefined
|
2023-08-21 16:53:31 -04:00
|
|
|
|
2023-08-24 23:46:45 +10:00
|
|
|
this.onClose(this)
|
|
|
|
this.ready = false
|
2023-08-18 16:16:16 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-25 00:16:37 -04:00
|
|
|
export type EngineCommand = Models['WebSocketRequest_type']
|
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
export class EngineCommandManager {
|
|
|
|
artifactMap: ArtifactMap = {}
|
|
|
|
sourceRangeMap: SourceRangeMap = {}
|
|
|
|
outSequence = 1
|
|
|
|
inSequence = 1
|
|
|
|
engineConnection?: EngineConnection
|
|
|
|
waitForReady: Promise<void> = new Promise(() => {})
|
|
|
|
private resolveReady = () => {}
|
|
|
|
onHoverCallback: (id?: string) => void = () => {}
|
|
|
|
onClickCallback: (selection?: SelectionsArgs) => void = () => {}
|
|
|
|
onCursorsSelectedCallback: (selections: CursorSelectionsArgs) => void =
|
|
|
|
() => {}
|
|
|
|
constructor({
|
|
|
|
setMediaStream,
|
|
|
|
setIsStreamReady,
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
token,
|
|
|
|
}: {
|
|
|
|
setMediaStream: (stream: MediaStream) => void
|
|
|
|
setIsStreamReady: (isStreamReady: boolean) => void
|
|
|
|
width: number
|
|
|
|
height: number
|
|
|
|
token?: string
|
|
|
|
}) {
|
|
|
|
this.waitForReady = new Promise((resolve) => {
|
|
|
|
this.resolveReady = resolve
|
|
|
|
})
|
|
|
|
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}`
|
|
|
|
this.engineConnection = new EngineConnection({
|
|
|
|
url,
|
|
|
|
token,
|
2023-08-24 23:46:45 +10:00
|
|
|
onEngineConnectionOpen: () => {
|
2023-08-21 16:53:31 -04:00
|
|
|
this.resolveReady()
|
|
|
|
setIsStreamReady(true)
|
2023-08-24 23:46:45 +10:00
|
|
|
},
|
|
|
|
onClose: () => {
|
2023-08-21 16:53:31 -04:00
|
|
|
setIsStreamReady(false)
|
2023-08-24 23:46:45 +10:00
|
|
|
},
|
|
|
|
onConnectionStarted: (engineConnection) => {
|
|
|
|
engineConnection?.pc?.addEventListener('datachannel', (event) => {
|
2023-08-18 16:16:16 -04:00
|
|
|
let lossyDataChannel = event.channel
|
2023-08-21 16:53:31 -04:00
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
lossyDataChannel.addEventListener('message', (event) => {
|
2023-08-25 00:16:37 -04:00
|
|
|
const result: Models['OkModelingCmdResponse_type'] = JSON.parse(
|
|
|
|
event.data
|
|
|
|
)
|
2023-08-18 16:16:16 -04:00
|
|
|
if (
|
|
|
|
result.type === 'highlight_set_entity' &&
|
2023-08-25 00:16:37 -04:00
|
|
|
result?.data?.sequence &&
|
|
|
|
result.data.sequence > this.inSequence
|
2023-08-18 16:16:16 -04:00
|
|
|
) {
|
2023-08-25 00:16:37 -04:00
|
|
|
this.onHoverCallback(result.data.entity_id)
|
|
|
|
this.inSequence = result.data.sequence
|
2023-08-18 16:16:16 -04:00
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
// When the EngineConnection starts a connection, we want to register
|
|
|
|
// callbacks into the WebSocket/PeerConnection.
|
2023-08-24 23:46:45 +10:00
|
|
|
engineConnection.websocket?.addEventListener('message', (event) => {
|
2023-08-18 16:16:16 -04:00
|
|
|
if (event.data instanceof ArrayBuffer) {
|
|
|
|
// If the data is an ArrayBuffer, it's the result of an export command,
|
|
|
|
// because in all other cases we send JSON strings. But in the case of
|
|
|
|
// export we send a binary blob.
|
|
|
|
// Pass this to our export function.
|
|
|
|
exportSave(event.data)
|
|
|
|
} else {
|
2023-08-25 00:16:37 -04:00
|
|
|
const message: Models['WebSocketResponse_type'] = JSON.parse(
|
|
|
|
event.data
|
|
|
|
)
|
|
|
|
if (
|
|
|
|
message.success &&
|
|
|
|
message.resp.type === 'modeling' &&
|
|
|
|
message.request_id
|
|
|
|
) {
|
|
|
|
this.handleModelingCommand(message.resp, message.request_id)
|
2023-08-09 20:49:10 +10:00
|
|
|
}
|
|
|
|
}
|
2023-08-18 16:16:16 -04:00
|
|
|
})
|
2023-08-24 23:46:45 +10:00
|
|
|
},
|
|
|
|
onNewTrack: ({ mediaStream }) => {
|
2023-08-21 16:53:31 -04:00
|
|
|
console.log('received track', mediaStream)
|
2023-08-22 20:07:39 -04:00
|
|
|
|
|
|
|
mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
|
|
|
|
console.log('peer is not sending video to us')
|
|
|
|
this.engineConnection?.close()
|
|
|
|
this.engineConnection?.connect()
|
|
|
|
})
|
|
|
|
|
2023-08-21 16:53:31 -04:00
|
|
|
setMediaStream(mediaStream)
|
2023-08-24 23:46:45 +10:00
|
|
|
},
|
|
|
|
})
|
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
this.engineConnection?.connect()
|
2023-06-22 16:43:33 +10:00
|
|
|
}
|
2023-08-25 00:16:37 -04:00
|
|
|
handleModelingCommand(message: WebSocketResponse, id: string) {
|
2023-08-21 16:53:31 -04:00
|
|
|
if (message.type !== 'modeling') {
|
|
|
|
return
|
|
|
|
}
|
2023-08-25 00:16:37 -04:00
|
|
|
const modelingResponse = message.data.modeling_response
|
2023-08-21 16:53:31 -04:00
|
|
|
|
|
|
|
const command = this.artifactMap[id]
|
2023-08-25 00:16:37 -04:00
|
|
|
if (modelingResponse.type === 'select_with_point') {
|
|
|
|
if (modelingResponse?.data?.entity_id) {
|
|
|
|
this.onClickCallback({
|
|
|
|
id: modelingResponse?.data?.entity_id,
|
|
|
|
type: 'default',
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
this.onClickCallback()
|
2023-08-21 16:53:31 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (command && command.type === 'pending') {
|
|
|
|
const resolve = command.resolve
|
|
|
|
this.artifactMap[id] = {
|
|
|
|
type: 'result',
|
2023-08-25 00:16:37 -04:00
|
|
|
data: modelingResponse,
|
2023-08-21 16:53:31 -04:00
|
|
|
}
|
|
|
|
resolve({
|
|
|
|
id,
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
this.artifactMap[id] = {
|
|
|
|
type: 'result',
|
2023-08-25 00:16:37 -04:00
|
|
|
data: modelingResponse,
|
2023-08-21 16:53:31 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-06-23 09:56:37 +10:00
|
|
|
tearDown() {
|
2023-08-18 16:16:16 -04:00
|
|
|
this.engineConnection?.close()
|
2023-06-23 09:56:37 +10:00
|
|
|
}
|
2023-06-22 16:43:33 +10:00
|
|
|
startNewSession() {
|
|
|
|
this.artifactMap = {}
|
|
|
|
this.sourceRangeMap = {}
|
|
|
|
}
|
|
|
|
endSession() {
|
2023-08-18 16:16:16 -04:00
|
|
|
// this.websocket?.close()
|
2023-06-22 16:43:33 +10:00
|
|
|
// socket.off('command')
|
|
|
|
}
|
|
|
|
onHover(callback: (id?: string) => void) {
|
|
|
|
// It's when the user hovers over a part in the 3d scene, and so the engine should tell the
|
|
|
|
// frontend about that (with it's id) so that the FE can highlight code associated with that id
|
|
|
|
this.onHoverCallback = callback
|
|
|
|
}
|
2023-08-09 20:49:10 +10:00
|
|
|
onClick(callback: (selection?: SelectionsArgs) => void) {
|
2023-06-22 16:43:33 +10:00
|
|
|
// It's when the user clicks on a part in the 3d scene, and so the engine should tell the
|
|
|
|
// frontend about that (with it's id) so that the FE can put the user's cursor on the right
|
|
|
|
// line of code
|
|
|
|
this.onClickCallback = callback
|
|
|
|
}
|
|
|
|
cusorsSelected(selections: {
|
|
|
|
otherSelections: Selections['otherSelections']
|
|
|
|
idBasedSelections: { type: string; id: string }[]
|
|
|
|
}) {
|
2023-08-21 16:53:31 -04:00
|
|
|
if (!this.engineConnection?.isReady()) {
|
|
|
|
console.log('engine connection isnt ready')
|
2023-06-22 16:43:33 +10:00
|
|
|
return
|
|
|
|
}
|
2023-08-09 20:49:10 +10:00
|
|
|
this.sendSceneCommand({
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd: {
|
|
|
|
type: 'select_clear',
|
|
|
|
},
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
})
|
|
|
|
this.sendSceneCommand({
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd: {
|
|
|
|
type: 'select_add',
|
|
|
|
entities: selections.idBasedSelections.map((s) => s.id),
|
|
|
|
},
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
})
|
2023-06-22 16:43:33 +10:00
|
|
|
}
|
|
|
|
sendSceneCommand(command: EngineCommand) {
|
2023-08-21 16:53:31 -04:00
|
|
|
if (!this.engineConnection?.isReady()) {
|
2023-06-22 16:43:33 +10:00
|
|
|
console.log('socket not ready')
|
|
|
|
return
|
|
|
|
}
|
2023-08-18 08:12:32 -07:00
|
|
|
if (command.type !== 'modeling_cmd_req') return
|
2023-08-02 15:41:59 +10:00
|
|
|
const cmd = command.cmd
|
2023-08-18 16:16:16 -04:00
|
|
|
if (
|
|
|
|
cmd.type === 'camera_drag_move' &&
|
|
|
|
this.engineConnection?.lossyDataChannel
|
|
|
|
) {
|
2023-08-09 20:49:10 +10:00
|
|
|
cmd.sequence = this.outSequence
|
|
|
|
this.outSequence++
|
2023-08-18 16:16:16 -04:00
|
|
|
this.engineConnection?.lossyDataChannel?.send(JSON.stringify(command))
|
2023-08-09 20:49:10 +10:00
|
|
|
return
|
2023-08-18 16:16:16 -04:00
|
|
|
} else if (
|
|
|
|
cmd.type === 'highlight_set_entity' &&
|
|
|
|
this.engineConnection?.lossyDataChannel
|
|
|
|
) {
|
2023-08-09 20:49:10 +10:00
|
|
|
cmd.sequence = this.outSequence
|
|
|
|
this.outSequence++
|
2023-08-18 16:16:16 -04:00
|
|
|
this.engineConnection?.lossyDataChannel?.send(JSON.stringify(command))
|
2023-06-23 09:56:37 +10:00
|
|
|
return
|
|
|
|
}
|
2023-08-02 16:23:17 -07:00
|
|
|
console.log('sending command', command)
|
2023-08-21 16:53:31 -04:00
|
|
|
this.engineConnection?.send(command)
|
2023-06-22 16:43:33 +10:00
|
|
|
}
|
2023-08-24 11:41:38 -07:00
|
|
|
sendModelingCommand({
|
2023-06-22 16:43:33 +10:00
|
|
|
id,
|
|
|
|
range,
|
|
|
|
command,
|
|
|
|
}: {
|
|
|
|
id: string
|
|
|
|
range: SourceRange
|
|
|
|
command: EngineCommand
|
|
|
|
}): Promise<any> {
|
|
|
|
this.sourceRangeMap[id] = range
|
|
|
|
|
2023-08-21 16:53:31 -04:00
|
|
|
if (!this.engineConnection?.isReady()) {
|
2023-06-22 16:43:33 +10:00
|
|
|
console.log('socket not ready')
|
|
|
|
return new Promise(() => {})
|
|
|
|
}
|
2023-08-21 16:53:31 -04:00
|
|
|
this.engineConnection?.send(command)
|
2023-06-22 16:43:33 +10:00
|
|
|
let resolve: (val: any) => void = () => {}
|
|
|
|
const promise = new Promise((_resolve, reject) => {
|
|
|
|
resolve = _resolve
|
|
|
|
})
|
|
|
|
this.artifactMap[id] = {
|
|
|
|
type: 'pending',
|
|
|
|
promise,
|
|
|
|
resolve,
|
|
|
|
}
|
|
|
|
return promise
|
|
|
|
}
|
2023-08-24 15:34:51 -07:00
|
|
|
sendModelingCommandFromWasm(
|
|
|
|
id: string,
|
|
|
|
rangeStr: string,
|
|
|
|
commandStr: string
|
|
|
|
): Promise<any> {
|
|
|
|
if (id === undefined) {
|
|
|
|
throw new Error('id is undefined')
|
|
|
|
}
|
|
|
|
if (rangeStr === undefined) {
|
|
|
|
throw new Error('rangeStr is undefined')
|
|
|
|
}
|
|
|
|
if (commandStr === undefined) {
|
|
|
|
throw new Error('commandStr is undefined')
|
|
|
|
}
|
|
|
|
const command: EngineCommand = JSON.parse(commandStr)
|
|
|
|
const range: SourceRange = JSON.parse(rangeStr)
|
|
|
|
|
|
|
|
return this.sendModelingCommand({ id, range, command })
|
|
|
|
}
|
2023-06-22 16:43:33 +10:00
|
|
|
commandResult(id: string): Promise<any> {
|
|
|
|
const command = this.artifactMap[id]
|
|
|
|
if (!command) {
|
|
|
|
throw new Error('No command found')
|
|
|
|
}
|
|
|
|
if (command.type === 'result') {
|
|
|
|
return command.data
|
|
|
|
}
|
|
|
|
return command.promise
|
|
|
|
}
|
|
|
|
async waitForAllCommands(): Promise<{
|
|
|
|
artifactMap: ArtifactMap
|
|
|
|
sourceRangeMap: SourceRangeMap
|
|
|
|
}> {
|
|
|
|
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,
|
|
|
|
sourceRangeMap: this.sourceRangeMap,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|